diff --git a/.gitignore b/.gitignore index f0ffe02b8..8d49f9f98 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ logs/ data/ .env .env.* +.env.development !.env.example !.env.*.example application-local.yml diff --git a/iam/src/main/java/com/baiyue/iam/notification/service/NotificationRecipientResolver.java b/iam/src/main/java/com/baiyue/iam/notification/service/NotificationRecipientResolver.java index b38c2b733..fbf30ea30 100644 --- a/iam/src/main/java/com/baiyue/iam/notification/service/NotificationRecipientResolver.java +++ b/iam/src/main/java/com/baiyue/iam/notification/service/NotificationRecipientResolver.java @@ -52,9 +52,16 @@ public class NotificationRecipientResolver { } private List byRole(CreateNotificationRequest request) { - var role = roles.selectOne(new LambdaQueryWrapper() - .eq(com.baiyue.iam.entity.IamRole::getTenantId, request.tenantId()) - .eq(com.baiyue.iam.entity.IamRole::getRoleCode, request.roleCode()).eq(com.baiyue.iam.entity.IamRole::getStatus,"ACTIVE").last("LIMIT 1")); + String appCode = request.recipientType() == com.baiyue.iam.notification.dto.NotificationDtos.RecipientType.TENANT_ROLE + ? "MERCHANT" : "PLATFORM"; + // Merchant members can be assigned tenant-specific roles or platform-provided merchant roles (tenant 0). + var candidates = roles.selectList(new LambdaQueryWrapper() + .in(com.baiyue.iam.entity.IamRole::getTenantId, request.tenantId(), 0L) + .eq(com.baiyue.iam.entity.IamRole::getAppCode, appCode) + .eq(com.baiyue.iam.entity.IamRole::getRoleCode, request.roleCode()) + .eq(com.baiyue.iam.entity.IamRole::getStatus, "ACTIVE") + .orderByDesc(com.baiyue.iam.entity.IamRole::getTenantId)); + var role = candidates.isEmpty() ? null : candidates.get(0); if (role == null) fail("角色编码不存在,请检查角色编码是否正确"); List ids = membershipRoles.selectList(new LambdaQueryWrapper() .eq(com.baiyue.iam.entity.IamMembershipRole::getTenantId,request.tenantId()) diff --git a/iam/src/main/java/com/baiyue/iam/notification/service/NotificationTemplateRenderer.java b/iam/src/main/java/com/baiyue/iam/notification/service/NotificationTemplateRenderer.java index f05b8f7ea..0427c5c3a 100644 --- a/iam/src/main/java/com/baiyue/iam/notification/service/NotificationTemplateRenderer.java +++ b/iam/src/main/java/com/baiyue/iam/notification/service/NotificationTemplateRenderer.java @@ -12,7 +12,9 @@ import java.util.regex.Pattern; @Component public class NotificationTemplateRenderer { - private static final Pattern VARIABLE = Pattern.compile("\\{\\{([a-z][a-z0-9_]{0,31})}}" ); + // ${name} is the format inserted by the template editor; retain {{name}} for existing templates. + private static final Pattern VARIABLE = Pattern.compile( + "\\$\\{([a-z][a-z0-9_]{0,31})}|\\{\\{([a-z][a-z0-9_]{0,31})}}" ); public String render(String template, Set whitelist, Map variables) { Set used = placeholders(template); @@ -20,7 +22,11 @@ public class NotificationTemplateRenderer { if (!whitelist.containsAll(variables.keySet())) fail("NOTIFICATION_VARIABLE_NOT_ALLOWED"); if (!variables.keySet().containsAll(used)) fail("NOTIFICATION_VARIABLE_MISSING"); String rendered = template; - for (String key : used) rendered = rendered.replace("{{" + key + "}}", variables.get(key)); + for (String key : used) { + String value = variables.get(key); + rendered = rendered.replace("${" + key + "}", value) + .replace("{{" + key + "}}", value); + } if (rendered.length() > 10_000) fail("NOTIFICATION_TEMPLATE_RENDER_TOO_LONG"); return rendered; } @@ -33,7 +39,7 @@ public class NotificationTemplateRenderer { private Set placeholders(String value) { Set keys = new HashSet<>(); Matcher matcher = VARIABLE.matcher(value); - while (matcher.find()) keys.add(matcher.group(1)); + while (matcher.find()) keys.add(matcher.group(1) != null ? matcher.group(1) : matcher.group(2)); return keys; } diff --git a/iam/src/main/java/com/baiyue/iam/service/ProfileResourceService.java b/iam/src/main/java/com/baiyue/iam/service/ProfileResourceService.java index bbf988c8e..68f11852d 100644 --- a/iam/src/main/java/com/baiyue/iam/service/ProfileResourceService.java +++ b/iam/src/main/java/com/baiyue/iam/service/ProfileResourceService.java @@ -139,8 +139,8 @@ public class ProfileResourceService { if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) { throw new BizException(ResultCode.NOT_FOUND, "Enterprise not found / 企业客户不存在"); } - if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) { - throw new BizException(ResultCode.CONFLICT, "Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质"); + if (!platformCanUpdateQualifications(tenant)) { + throw new BizException(ResultCode.CONFLICT, "Qualifications can only be uploaded before review / 仅待完善、已驳回或已审核客户可上传资质"); } requireOptionalQualificationType(request.qualificationType()); String certificateNo = resolveCertificateNo(tenant, request.qualificationType(), request.certificateNo()); @@ -163,6 +163,7 @@ public class ProfileResourceService { IamTenantQualification qualification = createQualification(tenant.getId(), request.qualificationType(), certificateNo, request.documentUrl(), request.originalFileName()); qualification.setValidTo(request.validTo()); + markPlatformQualificationApproved(tenant, qualification); repository.saveQualification(qualification); return new PlatformQualificationRegistrationResponse( qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl); @@ -173,7 +174,7 @@ public class ProfileResourceService { resourceId = "#request.tenantId", description = "上传企业必填资质包", captureAfter = false) public RequiredQualificationPackageResponse submitRequiredQualificationPackage( RequiredQualificationPackageRequest request) { - return registerRequiredQualificationPackage(request, "商户在送审前替换文件"); + return registerRequiredQualificationPackage(request, "商户在送审前替换文件", false); } @Transactional @@ -181,16 +182,17 @@ public class ProfileResourceService { resourceId = "#request.tenantId", description = "平台代客户上传必填资质包", captureAfter = false) public RequiredQualificationPackageResponse registerPlatformRequiredQualificationPackage( RequiredQualificationPackageRequest request) { - return registerRequiredQualificationPackage(request, "平台在送审前替换文件"); + return registerRequiredQualificationPackage(request, "平台在送审前替换文件", true); } private RequiredQualificationPackageResponse registerRequiredQualificationPackage( - RequiredQualificationPackageRequest request, String replacementComment) { + RequiredQualificationPackageRequest request, String replacementComment, boolean platformManaged) { IamTenant tenant = tenantRepository.findTenantForUpdate(request.tenantId()); if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) { throw new BizException(ResultCode.NOT_FOUND, "Enterprise not found / 企业客户不存在"); } - if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) { + if (platformManaged ? !platformCanUpdateQualifications(tenant) + : (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus()))) { throw new BizException(ResultCode.CONFLICT, "Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质"); } @@ -212,12 +214,14 @@ public class ProfileResourceService { IamTenantQualification businessLicense = createQualification(tenant.getId(), "BUSINESS_LICENSE", creditCode, request.businessLicenseDocumentUrl(), request.businessLicenseOriginalFileName()); businessLicense.setValidTo(request.businessLicenseValidTo()); + if (platformManaged) markPlatformQualificationApproved(tenant, businessLicense); repository.saveQualification(businessLicense); IamTenantQualification legalPersonId = null; if (hasLegalPersonId) { legalPersonId = createQualification(tenant.getId(), "LEGAL_PERSON_ID", null, request.legalPersonIdDocumentUrl().trim(), request.legalPersonIdOriginalFileName()); legalPersonId.setValidTo(request.legalPersonIdValidTo()); + if (platformManaged) markPlatformQualificationApproved(tenant, legalPersonId); repository.saveQualification(legalPersonId); } if (!creditCode.equals(tenant.getUnifiedSocialCreditCode())) { @@ -314,6 +318,17 @@ public class ProfileResourceService { && !"SUPERSEDED".equals(qualification.getStatus()); } + private boolean platformCanUpdateQualifications(IamTenant tenant) { + return "PROFILE_REQUIRED".equals(tenant.getStatus()) || "REJECTED".equals(tenant.getStatus()) + || "ACTIVE".equals(tenant.getStatus()) || "DISABLED".equals(tenant.getStatus()); + } + + private void markPlatformQualificationApproved(IamTenant tenant, IamTenantQualification qualification) { + if ("ACTIVE".equals(tenant.getStatus()) || "DISABLED".equals(tenant.getStatus())) { + qualification.setStatus("APPROVED"); + } + } + private void requireMerchant(long tenantId) { IamTenant tenant = tenantRepository.findTenant(tenantId); if (tenant == null || tenant.getId() == null || tenant.getId().longValue() != tenantId diff --git a/iam/src/test/java/com/baiyue/iam/notification/NotificationRecipientResolverTest.java b/iam/src/test/java/com/baiyue/iam/notification/NotificationRecipientResolverTest.java index c388bbd20..3704b47cf 100644 --- a/iam/src/test/java/com/baiyue/iam/notification/NotificationRecipientResolverTest.java +++ b/iam/src/test/java/com/baiyue/iam/notification/NotificationRecipientResolverTest.java @@ -2,6 +2,7 @@ package com.baiyue.iam.notification; import com.baiyue.common.core.exception.BizException; import com.baiyue.iam.entity.IamMembership; +import com.baiyue.iam.entity.IamMembershipRole; import com.baiyue.iam.entity.IamRole; import com.baiyue.iam.entity.IamUser; import com.baiyue.iam.mapper.IamMembershipMapper; @@ -22,7 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class NotificationRecipientResolverTest { private final IamMembershipMapper memberships = mock(IamMembershipMapper.class); @@ -70,7 +71,7 @@ class NotificationRecipientResolverTest { @Test void explainsWhenRoleCodeDoesNotExist() { - when(roles.selectOne(any())).thenReturn(null); + when(roles.selectList(any())).thenReturn(List.of()); assertThatThrownBy(() -> resolver.resolve(roleRequest(RecipientType.PLATFORM_ROLE, "MISSING_ROLE"))) .isInstanceOf(BizException.class) @@ -81,7 +82,7 @@ class NotificationRecipientResolverTest { void explainsWhenRoleHasNoActiveMembers() { IamRole role = new IamRole(); role.setId(501L); - when(roles.selectOne(any())).thenReturn(role); + when(roles.selectList(any())).thenReturn(List.of(role)); when(membershipRoles.selectList(any())).thenReturn(List.of()); assertThatThrownBy(() -> resolver.resolve(roleRequest(RecipientType.PLATFORM_ROLE, "CUSTOMER_REVIEWER"))) @@ -89,6 +90,39 @@ class NotificationRecipientResolverTest { .hasMessage("该角色下暂无可接收消息的有效用户,请先为角色分配启用中的成员"); } + @Test + void resolvesEnterpriseMemberAssignedToPlatformProvidedMerchantRole() { + IamRole role = new IamRole(); + role.setId(501L); + role.setTenantId(0L); + role.setAppCode("MERCHANT"); + role.setRoleCode("MERCHANT_OWNER"); + IamMembershipRole assignment = new IamMembershipRole(); + assignment.setTenantId(9L); + assignment.setRoleId(role.getId()); + assignment.setMembershipId(301L); + IamMembership membership = new IamMembership(); + membership.setId(301L); + membership.setTenantId(9L); + membership.setUserId(401L); + membership.setStatus("ACTIVE"); + IamUser user = new IamUser(); + user.setId(401L); + user.setDisplayName("企业管理员"); + user.setStatus("ACTIVE"); + when(roles.selectList(any())).thenReturn(List.of(role)); + when(membershipRoles.selectList(any())).thenReturn(List.of(assignment)); + when(memberships.selectBatchIds(List.of(301L))).thenReturn(List.of(membership)); + when(users.selectById(401L)).thenReturn(user); + + var recipients = resolver.resolve(new CreateNotificationRequest(9L, "ORDER_STATUS_CHANGED", + RecipientType.TENANT_ROLE, null, "MERCHANT_OWNER", Map.of(), null, + "notification-tenant-role-key", null, null)); + + assertThat(recipients).containsExactly(new NotificationRecipientResolver.ResolvedRecipient( + 9L, 301L, 401L, "企业管理员", "MERCHANT")); + } + private CreateNotificationRequest request(RecipientType recipientType, List recipientIds) { return new CreateNotificationRequest(9L, "ORDER_STATUS_CHANGED", recipientType, recipientIds, null, Map.of(), null, "notification-test-key", null, null); diff --git a/iam/src/test/java/com/baiyue/iam/notification/NotificationTemplateRendererTest.java b/iam/src/test/java/com/baiyue/iam/notification/NotificationTemplateRendererTest.java index 38ff24dcd..2fde97a4f 100644 --- a/iam/src/test/java/com/baiyue/iam/notification/NotificationTemplateRendererTest.java +++ b/iam/src/test/java/com/baiyue/iam/notification/NotificationTemplateRendererTest.java @@ -7,6 +7,6 @@ import java.util.Set; import static org.assertj.core.api.Assertions.*; class NotificationTemplateRendererTest { private final NotificationTemplateRenderer renderer=new NotificationTemplateRenderer(); - @Test void rendersOnlyWhitelistedVariables(){assertThat(renderer.render("Order {{order_no}} is {{status}}",Set.of("order_no","status"),Map.of("order_no","A1","status","ready"))).isEqualTo("Order A1 is ready");} - @Test void rejectsMissingAndUnknownVariables(){assertThatThrownBy(()->renderer.render("{{order_no}}",Set.of("order_no"),Map.of())).isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_VARIABLE_MISSING");assertThatThrownBy(()->renderer.render("ok",Set.of(),Map.of("secret","x"))).isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_VARIABLE_NOT_ALLOWED");} + @Test void rendersOnlyWhitelistedVariables(){assertThat(renderer.render("Order ${order_no} is {{status}}",Set.of("order_no","status"),Map.of("order_no","A1","status","ready"))).isEqualTo("Order A1 is ready");} + @Test void rejectsMissingAndUnknownVariables(){assertThatThrownBy(()->renderer.render("${order_no}",Set.of("order_no"),Map.of())).isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_VARIABLE_MISSING");assertThatThrownBy(()->renderer.render("ok",Set.of(),Map.of("secret","x"))).isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_VARIABLE_NOT_ALLOWED");} } diff --git a/iam/src/test/java/com/baiyue/iam/service/PlatformQualificationRegistrationTest.java b/iam/src/test/java/com/baiyue/iam/service/PlatformQualificationRegistrationTest.java index 38c2d7ec3..c79a4429e 100644 --- a/iam/src/test/java/com/baiyue/iam/service/PlatformQualificationRegistrationTest.java +++ b/iam/src/test/java/com/baiyue/iam/service/PlatformQualificationRegistrationTest.java @@ -1,6 +1,7 @@ package com.baiyue.iam.service; import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRequest; +import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageRequest; import com.baiyue.iam.entity.IamTenant; import com.baiyue.iam.entity.IamTenantQualification; import com.baiyue.iam.repository.ProfileResourceRepository; @@ -69,4 +70,51 @@ class PlatformQualificationRegistrationTest { assertEquals(existing.getDocumentUrl(), response.replacedDocumentUrl()); assertEquals("PENDING", captor.getAllValues().get(1).getStatus()); } + + @Test + void platformCanReplaceAnApprovedOptionalQualificationForAnActiveTenant() { + IamTenant tenant = new IamTenant(); + tenant.setId(10001L); tenant.setTenantType("MERCHANT"); tenant.setStatus("ACTIVE"); + IamTenantQualification existing = new IamTenantQualification(); + existing.setId(88L); existing.setTenantId(10001L); existing.setQualificationType("BANK_ACCOUNT_LICENSE"); + existing.setStatus("APPROVED"); + existing.setDocumentUrl("/api/merchant/v1/files/qualifications/old-file.pdf"); + when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant); + when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of(existing)); + + var response = service.registerPlatformQualification(new PlatformQualificationRegistrationRequest( + 10001L, "BANK_ACCOUNT_LICENSE", null, "/api/merchant/v1/files/qualifications/new-file.pdf", + "new-file.pdf", LocalDate.of(2031, 12, 31), true)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(IamTenantQualification.class); + verify(repository, times(2)).saveQualification(captor.capture()); + assertEquals("SUPERSEDED", existing.getStatus()); + assertEquals("APPROVED", captor.getAllValues().get(1).getStatus()); + assertEquals("ACTIVE", response.tenantStatus()); + assertEquals(existing.getDocumentUrl(), response.replacedDocumentUrl()); + } + + @Test + void platformCanReplaceAnApprovedBusinessLicenseForAnActiveTenant() { + IamTenant tenant = new IamTenant(); + tenant.setId(10001L); tenant.setTenantType("MERCHANT"); tenant.setStatus("ACTIVE"); + tenant.setUnifiedSocialCreditCode("91310101ABCDE12345"); + IamTenantQualification existing = new IamTenantQualification(); + existing.setId(88L); existing.setTenantId(10001L); existing.setQualificationType("BUSINESS_LICENSE"); + existing.setStatus("APPROVED"); + existing.setDocumentUrl("/api/merchant/v1/files/qualifications/old-license.pdf"); + when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant); + when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of(existing)); + + var response = service.registerPlatformRequiredQualificationPackage(new RequiredQualificationPackageRequest( + 10001L, "91310101ABCDE12345", "/api/merchant/v1/files/qualifications/new-license.pdf", + "new-license.pdf", LocalDate.of(2031, 12, 31), true, null, null, null, null)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(IamTenantQualification.class); + verify(repository, times(2)).saveQualification(captor.capture()); + assertEquals("SUPERSEDED", existing.getStatus()); + assertEquals("APPROVED", captor.getAllValues().get(1).getStatus()); + assertEquals("ACTIVE", response.tenantStatus()); + assertEquals(existing.getDocumentUrl(), response.replacedBusinessLicenseDocumentUrl()); + } } diff --git a/order/src/main/java/com/baiyue/order/service/CustomerAddressService.java b/order/src/main/java/com/baiyue/order/service/CustomerAddressService.java index b6c22abf3..5dae76824 100644 --- a/order/src/main/java/com/baiyue/order/service/CustomerAddressService.java +++ b/order/src/main/java/com/baiyue/order/service/CustomerAddressService.java @@ -33,6 +33,7 @@ public class CustomerAddressService { @Transactional public Long create(long tenantId, long userId, CustomerAddressDtos.SaveRequest request) { + ensureNoDuplicate(tenantId, request, null); CustomerAddressEntity value = new CustomerAddressEntity(); value.setId(IdWorker.getId()); value.setTenantId(tenantId); @@ -51,6 +52,7 @@ public class CustomerAddressService { if (!value.getAddressType().equals(request.addressType())) { throw new BizException(ResultCode.VALIDATION_FAILED, "地址类型创建后不可修改"); } + ensureNoDuplicate(tenantId, request, id); if (request.defaultAddress()) clearDefault(tenantId, request.addressType(), id); apply(value, request); value.setUpdatedBy(userId); @@ -99,6 +101,25 @@ public class CustomerAddressService { .set(CustomerAddressEntity::getDefaultAddress, false)); } + private void ensureNoDuplicate(long tenantId, CustomerAddressDtos.SaveRequest request, Long excludedId) { + boolean duplicate = mapper.selectList(new LambdaQueryWrapper() + .eq(CustomerAddressEntity::getTenantId, tenantId) + .eq(CustomerAddressEntity::getActive, true) + .eq(CustomerAddressEntity::getAddressType, request.addressType()) + .eq(CustomerAddressEntity::getCompanyName, request.companyName().trim()) + .eq(CustomerAddressEntity::getContactName, request.contactName().trim()) + .eq(CustomerAddressEntity::getPhone, request.phone().trim()) + .eq(CustomerAddressEntity::getCountryCode, request.countryCode().trim().toUpperCase()) + .eq(CustomerAddressEntity::getCity, request.city().trim()) + .eq(CustomerAddressEntity::getAddressLine, request.addressLine().trim()) + .ne(excludedId != null, CustomerAddressEntity::getId, excludedId)) + .stream().findAny().isPresent(); + if (duplicate) { + throw new BizException(ResultCode.CONFLICT, "PICKUP".equals(request.addressType()) + ? "相同的企业提货地址已存在" : "相同的企业私人仓地址已存在"); + } + } + private static void apply(CustomerAddressEntity value, CustomerAddressDtos.SaveRequest source) { value.setAddressType(source.addressType()); value.setAddressName(source.addressName().trim()); value.setCompanyName(source.companyName().trim()); value.setContactName(source.contactName().trim()); diff --git a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java index bacfda9e7..71cdef183 100644 --- a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java +++ b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java @@ -324,11 +324,11 @@ public class UnifiedOrderQueryService { order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null, order.getInboundCode(), order.getExpectedShipDate(), null, null, totalPackageCount(cargo), null, null, - order.getEstimatedVolumeCbm(), volumeCbm(cargo), null, null, + order.getEstimatedVolumeCbm(), volumeCbm(cargo), grossWeightKg(cargo), volumeCbm(cargo), order.getExpectedArrivalAt(), null, null, null, order.getFulfillmentType(), order.getLxCode(), order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(), order.getCustomsInspection(), order.getDestinationType(), order.getAmazonWarehouseCode(), - null, null, null, null, null, null, null, order.getExpressCompany(), order.getExpressTrackingNo(), + null, null, null, null, netWeightKg(cargo), null, null, order.getExpressCompany(), order.getExpressTrackingNo(), StringUtils.hasText(creatorName) ? creatorName : string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), null, null, null, null, null, coLoadGroup, order.getCustomerRemark(), latestInternalNote, order.getWarehouseRemark(), order.getCustomerServiceRemark(), @@ -348,13 +348,14 @@ public class UnifiedOrderQueryService { displayedPackageCount(order, cargo, inbound), null, distinctLocationCodes(inbound.locationCodes()), order.getEstimatedVolumeCbm(), volumeCbm(cargo), - inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(), + measuredOrDeclared(inbound.actualGrossWeightKg(), grossWeightKg(cargo)), + measuredOrDeclared(inbound.actualVolumeCbm(), volumeCbm(cargo)), order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(), inbound.companyName(), inbound.businessType(), inbound.fulfillmentType(), inbound.lxCode(), order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(), inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(), inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(), - inbound.actualNetWeightKg(), + measuredOrDeclared(inbound.actualNetWeightKg(), netWeightKg(cargo)), inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(), StringUtils.hasText(creatorName) ? creatorName : inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), measurement(inbound), preload(summary.preload()), loading(summary.loading()), @@ -378,13 +379,14 @@ public class UnifiedOrderQueryService { inbound.id(), inbound.version(), inbound.inboundCode(), null, inbound.holdFlag(), inbound.currentHoldReason(), displayedPackageCount(order, cargo, inbound), null, distinctLocationCodes(inbound.locationCodes()), order == null ? null : order.getEstimatedVolumeCbm(), volumeCbm(cargo), - inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(), + measuredOrDeclared(inbound.actualGrossWeightKg(), grossWeightKg(cargo)), + measuredOrDeclared(inbound.actualVolumeCbm(), volumeCbm(cargo)), order != null && order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(), inbound.companyName(), inbound.businessType(), inbound.fulfillmentType(), inbound.lxCode(), order == null ? null : order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(), inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(), inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(), - inbound.actualNetWeightKg(), + measuredOrDeclared(inbound.actualNetWeightKg(), netWeightKg(cargo)), inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(), inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), measurement(inbound), preload(summary.preload()), loading(summary.loading()), @@ -490,6 +492,10 @@ public class UnifiedOrderQueryService { return cargo == null ? null : cargo.volumeCbm(); } + private BigDecimal measuredOrDeclared(BigDecimal measured, BigDecimal declared) { + return measured == null ? declared : measured; + } + private record CargoSummary(List skuNos, List shipmentIds, List referenceIds, Integer totalQuantity, Integer totalPackageCount, BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm) { diff --git a/order/src/test/java/com/baiyue/order/service/CustomerAddressServiceTest.java b/order/src/test/java/com/baiyue/order/service/CustomerAddressServiceTest.java index 24ece6108..d09a76f54 100644 --- a/order/src/test/java/com/baiyue/order/service/CustomerAddressServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/CustomerAddressServiceTest.java @@ -7,9 +7,12 @@ import com.baiyue.order.mapper.CustomerAddressMapper; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -38,4 +41,21 @@ class CustomerAddressServiceTest { assertThatThrownBy(() -> service.required(10001L, 90001L, "PICKUP")) .isInstanceOf(BizException.class).hasMessage("客户地址不存在或已停用"); } + + @Test + void rejectsDuplicatePickupAddressBeforeCreatingAnotherRecord() { + CustomerAddressMapper mapper = mock(CustomerAddressMapper.class); + CustomerAddressEntity duplicate = new CustomerAddressEntity(); + duplicate.setId(90001L); + when(mapper.selectList(any())).thenReturn(List.of(duplicate)); + CustomerAddressService service = new CustomerAddressService(); + ReflectionTestUtils.setField(service, "mapper", mapper); + var request = new CustomerAddressDtos.SaveRequest("PICKUP", "深圳工厂", "柏悦物流", + "惠万昌", "13229608341", null, "CN", "江西省", "抚州市", null, + "崇仁县三山乡长仁大队廉家村", null, false); + + assertThatThrownBy(() -> service.create(10001L, 20001L, request)) + .isInstanceOf(BizException.class).hasMessage("相同的企业提货地址已存在"); + verify(mapper, never()).insert(any(CustomerAddressEntity.class)); + } } diff --git a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java index 106ff2e31..272ed6f56 100644 --- a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java @@ -127,7 +127,7 @@ class UnifiedOrderQueryServiceTest { } @Test - void newlyCreatedOrderUsesItsOwnCargoPackageCountBeforeInboundExists() { + void newlyCreatedOrderUsesItsOwnCargoTotalsBeforeInboundExists() { UnifiedOrderQuery query = query(null); OrderEntity order = order(OrderStatus.APPROVED); OrderCargoEntity cargo = new OrderCargoEntity(); @@ -144,13 +144,39 @@ class UnifiedOrderQueryServiceTest { assertThat(service.page(10001L, query).records()).singleElement().satisfies(row -> { assertThat(row.totalPackageCount()).isEqualTo(100); assertThat(row.expectedVolumeCbm()).isEqualByComparingTo("0.375"); - assertThat(row.actualGrossWeightKg()).isNull(); - assertThat(row.totalNetWeightKg()).isNull(); - assertThat(row.actualVolumeCbm()).isNull(); + assertThat(row.actualGrossWeightKg()).isEqualByComparingTo("12.5"); + assertThat(row.totalNetWeightKg()).isEqualByComparingTo("11.2"); + assertThat(row.actualVolumeCbm()).isEqualByComparingTo("0.375"); }); verify(orders).findCargoByOrderIds(10001L, List.of(77L)); } + @Test + void unmeasuredInboundFallsBackToDeclaredCargoTotals() { + UnifiedOrderQuery query = query(null); + OrderEntity order = order(OrderStatus.APPROVED); + OrderCargoEntity cargo = new OrderCargoEntity(); + cargo.setOrderId(77L); + cargo.setPackageCount(50); + cargo.setGrossWeightKg(new BigDecimal("1500")); + cargo.setNetWeightKg(new BigDecimal("25")); + cargo.setVolumeCbm(new BigDecimal("7.5")); + var summary = summary("WAIT_MEASURE", null); + when(summary.inbound().actualGrossWeightKg()).thenReturn(null); + when(summary.inbound().actualNetWeightKg()).thenReturn(null); + when(summary.inbound().actualVolumeCbm()).thenReturn(null); + when(orders.unifiedPage(eq(10001L), eq(query), any())) + .thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1)); + when(firstleg.summaries(eq("internal-token"), any())).thenReturn(ApiResponse.success(List.of(summary))); + when(orders.findCargoByOrderIds(10001L, List.of(77L))).thenReturn(List.of(cargo)); + + assertThat(service.page(10001L, query).records()).singleElement().satisfies(row -> { + assertThat(row.actualGrossWeightKg()).isEqualByComparingTo("1500"); + assertThat(row.totalNetWeightKg()).isEqualByComparingTo("25"); + assertThat(row.actualVolumeCbm()).isEqualByComparingTo("7.5"); + }); + } + @Test void dropshipMixedSkuRowsCountSharedCartonVolumeOnlyOnce() { UnifiedOrderQuery query = query(null);