From 37fec1e0123b9c82b83cee89420b17de00bd3cd9 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 7 Aug 2026 18:44:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 12 ++ .../InternalProductImageController.java | 48 ++++++++ .../MerchantProductImageController.java | 62 ++++++++++ .../file/service/MeasurementImageService.java | 6 +- .../file/service/ProductImageService.java | 76 +++++++++++++ .../file/service/ProductImageStorage.java | 107 ++++++++++++++++++ .../MeasurementImageServiceEvidenceTest.java | 48 +++++++- .../PlatformInboundMeasurementController.java | 6 +- .../dto/InboundMeasurementRequests.java | 8 +- .../service/InboundMeasurementService.java | 41 +++++-- .../main/resources/openapi/firstleg-v1.yaml | 28 +++++ ...oundMeasurementControllerContractTest.java | 3 +- .../InboundMeasurementServiceTest.java | 54 ++++++++- .../order/client/ProductImageClient.java | 19 ++++ .../com/baiyue/order/dto/OrderRequests.java | 8 +- .../baiyue/order/entity/OrderCargoEntity.java | 2 +- .../service/OrderApplicationService.java | 12 +- .../order/service/ProductImageGuard.java | 58 ++++++++++ order/src/main/resources/application.yml | 2 + order/src/main/resources/db/order.sql | 6 + .../resources/db/phase1-order-extension.sql | 6 + .../dto/OrderRequestsValidationTest.java | 3 +- .../OrderApplicationServiceStateTest.java | 7 +- .../service/OrderMasterDataGuardTest.java | 3 +- 24 files changed, 601 insertions(+), 24 deletions(-) create mode 100644 file/src/main/java/com/baiyue/file/controller/InternalProductImageController.java create mode 100644 file/src/main/java/com/baiyue/file/controller/MerchantProductImageController.java create mode 100644 file/src/main/java/com/baiyue/file/service/ProductImageService.java create mode 100644 file/src/main/java/com/baiyue/file/service/ProductImageStorage.java create mode 100644 order/src/main/java/com/baiyue/order/client/ProductImageClient.java create mode 100644 order/src/main/java/com/baiyue/order/service/ProductImageGuard.java diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 513ae90b7..ec3695119 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -513,6 +513,12 @@ CREATE TABLE IF NOT EXISTS ord_order_cargo ( product_usage VARCHAR(128) NULL COMMENT '用途', model VARCHAR(64) NULL COMMENT '型号', brand VARCHAR(64) NULL COMMENT '品牌', + product_length_cm DECIMAL(12,2) NULL COMMENT '产品长度(厘米)', + product_width_cm DECIMAL(12,2) NULL COMMENT '产品宽度(厘米)', + product_height_cm DECIMAL(12,2) NULL COMMENT '产品高度(厘米)', + shipment_id VARCHAR(64) NULL COMMENT '货物级 Amazon Shipment ID', + reference_id VARCHAR(64) NULL COMMENT '货物级 Amazon Reference ID', + product_image_file_id BIGINT UNSIGNED NULL COMMENT '文件中心产品图片逻辑ID', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', @@ -1546,6 +1552,12 @@ CALL phase1_add_column('ord_order_cargo', 'material', "VARCHAR(128) NULL COMMENT CALL phase1_add_column('ord_order_cargo', 'product_usage', "VARCHAR(128) NULL COMMENT 'Usage'"); CALL phase1_add_column('ord_order_cargo', 'model', "VARCHAR(64) NULL COMMENT 'Model'"); CALL phase1_add_column('ord_order_cargo', 'brand', "VARCHAR(64) NULL COMMENT 'Brand'"); +CALL phase1_add_column('ord_order_cargo', 'product_length_cm', "DECIMAL(12,2) NULL COMMENT 'Product length in centimeters'"); +CALL phase1_add_column('ord_order_cargo', 'product_width_cm', "DECIMAL(12,2) NULL COMMENT 'Product width in centimeters'"); +CALL phase1_add_column('ord_order_cargo', 'product_height_cm', "DECIMAL(12,2) NULL COMMENT 'Product height in centimeters'"); +CALL phase1_add_column('ord_order_cargo', 'shipment_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Shipment ID'"); +CALL phase1_add_column('ord_order_cargo', 'reference_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Reference ID'"); +CALL phase1_add_column('ord_order_cargo', 'product_image_file_id', "BIGINT UNSIGNED NULL COMMENT 'Product image logical file ID'"); DROP PROCEDURE phase1_add_column; diff --git a/file/src/main/java/com/baiyue/file/controller/InternalProductImageController.java b/file/src/main/java/com/baiyue/file/controller/InternalProductImageController.java new file mode 100644 index 000000000..51a135d08 --- /dev/null +++ b/file/src/main/java/com/baiyue/file/controller/InternalProductImageController.java @@ -0,0 +1,48 @@ +package com.baiyue.file.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.file.service.ProductImageService; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +@RestController +@RequestMapping("/internal/file/v1/product-images") +public class InternalProductImageController { + @Resource private ProductImageService images; + @Value("${baiyue.security.internal-service-token}") private String internalToken; + private byte[] expectedToken; + + @PostConstruct + void initializeToken() { + expectedToken = internalToken.getBytes(StandardCharsets.UTF_8); + } + + @GetMapping("/{tenantId}/{fileId}") + public ApiResponse validate( + @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, + @PathVariable long tenantId, + @PathVariable long fileId) { + requireToken(token); + if (tenantId <= 0 || fileId <= 0) { + throw new BizException(ResultCode.BAD_REQUEST, "客户和文件ID必须大于0"); + } + return ApiResponse.success(images.validate(tenantId, fileId)); + } + + private void requireToken(String token) { + if (token == null || !MessageDigest.isEqual(expectedToken, token.getBytes(StandardCharsets.UTF_8))) { + throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效"); + } + } +} diff --git a/file/src/main/java/com/baiyue/file/controller/MerchantProductImageController.java b/file/src/main/java/com/baiyue/file/controller/MerchantProductImageController.java new file mode 100644 index 000000000..060fea945 --- /dev/null +++ b/file/src/main/java/com/baiyue/file/controller/MerchantProductImageController.java @@ -0,0 +1,62 @@ +package com.baiyue.file.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.file.service.ProductImageService; +import jakarta.annotation.Resource; +import org.springframework.http.CacheControl; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.nio.charset.StandardCharsets; + +@RestController +@RequestMapping("/api/merchant/v1/files/product-images") +public class MerchantProductImageController { + @Resource private ProductImageService images; + + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PreAuthorize("hasAnyAuthority('merchant:order:create','merchant:order:edit')") + public ApiResponse upload( + @AuthenticationPrincipal AuthenticatedUser user, + @RequestPart("file") MultipartFile file) { + long tenantId = merchantTenant(user); + return ApiResponse.success(images.upload(tenantId, user.userId(), file)); + } + + @GetMapping("/{fileId}") + @PreAuthorize("hasAnyAuthority('merchant:order:create','merchant:order:edit','merchant:order:view')") + public ResponseEntity download( + @AuthenticationPrincipal AuthenticatedUser user, + @PathVariable long fileId) { + var image = images.download(merchantTenant(user), fileId); + return ResponseEntity.ok() + .cacheControl(CacheControl.noStore()) + .contentType(MediaType.parseMediaType(image.contentType())) + .contentLength(image.content().length) + .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.inline() + .filename(image.fileName(), StandardCharsets.UTF_8).build().toString()) + .body(image.content()); + } + + private static long merchantTenant(AuthenticatedUser user) { + if (user == null || !"MERCHANT".equalsIgnoreCase(user.appCode()) + || user.tenantId() == null || user.tenantId() <= 0 || user.userId() == null || user.userId() <= 0) { + throw new BizException(ResultCode.FORBIDDEN, "Merchant identity is required"); + } + return user.tenantId(); + } +} diff --git a/file/src/main/java/com/baiyue/file/service/MeasurementImageService.java b/file/src/main/java/com/baiyue/file/service/MeasurementImageService.java index 86532313d..82db4c2b3 100644 --- a/file/src/main/java/com/baiyue/file/service/MeasurementImageService.java +++ b/file/src/main/java/com/baiyue/file/service/MeasurementImageService.java @@ -87,11 +87,12 @@ public class MeasurementImageService { if (!"INBOUND_MEASUREMENT_IMAGE".equals(object.getBusinessType())) { throw new BizException(ResultCode.CONFLICT, "仅允许清理第三方尺寸测量证据图片"); } - object.setDeleted(true); object.setStatus("DELETED"); object.setUpdatedBy(operatorId); object.setUpdatedAt(LocalDateTime.now()); - files.updateById(object); + if (files.updateById(object) != 1 || files.deleteById(object.getId()) != 1) { + throw new BizException(ResultCode.CONFLICT, "测量图片已被删除,请刷新后重试"); + } storage.delete(object.getObjectKey()); } @@ -101,6 +102,7 @@ public class MeasurementImageService { .eq(FileObjectEntity::getId, fileId) .in(FileObjectEntity::getBusinessType, "INBOUND_WEIGHT_IMAGE", "INBOUND_MEASUREMENT_IMAGE") .eq(FileObjectEntity::getBusinessId, Long.toString(inboundId)) + .eq(FileObjectEntity::getStatus, "ACTIVE") .eq(FileObjectEntity::getDeleted, false)); if (object == null) throw new BizException(ResultCode.NOT_FOUND, "称重图片不存在"); return object; diff --git a/file/src/main/java/com/baiyue/file/service/ProductImageService.java b/file/src/main/java/com/baiyue/file/service/ProductImageService.java new file mode 100644 index 000000000..083376fe5 --- /dev/null +++ b/file/src/main/java/com/baiyue/file/service/ProductImageService.java @@ -0,0 +1,76 @@ +package com.baiyue.file.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.file.entity.FileObjectEntity; +import com.baiyue.file.mapper.FileObjectMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; + +@Service +public class ProductImageService { + public static final String BUSINESS_TYPE = "ORDER_PRODUCT_IMAGE"; + @Resource private ProductImageStorage storage; + @Resource private FileObjectMapper files; + + @Transactional + public UploadResult upload(long tenantId, long operatorId, MultipartFile file) { + var stored = storage.store(tenantId, file); + FileObjectEntity object = new FileObjectEntity(); + object.setTenantId(tenantId); + object.setBusinessType(BUSINESS_TYPE); + object.setOriginalName(stored.originalName()); + object.setObjectKey(stored.objectKey()); + object.setStorageProvider("LOCAL"); + object.setBucketName("product-images"); + object.setContentType(stored.contentType()); + object.setSizeBytes(stored.size()); + object.setSha256(stored.sha256()); + object.setStatus("ACTIVE"); + object.setCreatedBy(operatorId); + object.setUpdatedBy(operatorId); + object.setCreatedAt(LocalDateTime.now()); + object.setUpdatedAt(LocalDateTime.now()); + object.setDeleted(false); + object.setVersion(0); + try { + files.insert(object); + } catch (RuntimeException exception) { + storage.delete(stored.objectKey()); + throw exception; + } + return metadata(object); + } + + public UploadResult validate(long tenantId, long fileId) { + return metadata(requireObject(tenantId, fileId)); + } + + public Download download(long tenantId, long fileId) { + FileObjectEntity object = requireObject(tenantId, fileId); + return new Download(object.getOriginalName(), object.getContentType(), storage.read(object.getObjectKey())); + } + + private FileObjectEntity requireObject(long tenantId, long fileId) { + FileObjectEntity object = files.selectOne(new LambdaQueryWrapper() + .eq(FileObjectEntity::getTenantId, tenantId) + .eq(FileObjectEntity::getId, fileId) + .eq(FileObjectEntity::getBusinessType, BUSINESS_TYPE) + .eq(FileObjectEntity::getStatus, "ACTIVE") + .eq(FileObjectEntity::getDeleted, false)); + if (object == null) throw new BizException(ResultCode.NOT_FOUND, "产品图片不存在"); + return object; + } + + private static UploadResult metadata(FileObjectEntity object) { + return new UploadResult(object.getId(), object.getOriginalName(), object.getContentType(), object.getSizeBytes()); + } + + public record UploadResult(Long fileId, String fileName, String contentType, Long sizeBytes) {} + public record Download(String fileName, String contentType, byte[] content) {} +} diff --git a/file/src/main/java/com/baiyue/file/service/ProductImageStorage.java b/file/src/main/java/com/baiyue/file/service/ProductImageStorage.java new file mode 100644 index 000000000..1d73d2fb0 --- /dev/null +++ b/file/src/main/java/com/baiyue/file/service/ProductImageStorage.java @@ -0,0 +1,107 @@ +package com.baiyue.file.service; + +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; + +@Component +public class ProductImageStorage { + private static final long MAX_SIZE = 5L * 1024 * 1024; + private static final Set TYPES = Set.of("image/jpeg", "image/png"); + @Value("${baiyue.file.storage-root}") private Path configuredRoot; + private Path root; + + @PostConstruct + void initializeRoot() { + root = configuredRoot.toAbsolutePath().normalize(); + } + + public Stored store(long tenantId, MultipartFile file) { + if (file == null || file.isEmpty()) throw bad("产品图片不能为空"); + if (file.getSize() > MAX_SIZE) throw bad("产品图片不能超过5MB"); + String type = file.getContentType() == null ? "" : file.getContentType().toLowerCase(Locale.ROOT); + if (!TYPES.contains(type)) throw bad("产品图片仅支持JPG、PNG"); + verifyImage(file); + String extension = "image/png".equals(type) ? ".png" : ".jpg"; + String objectKey = "product-images/" + tenantId + "/" + UUID.randomUUID() + extension; + Path target = root.resolve(objectKey).normalize(); + if (!target.startsWith(root)) throw new BizException(ResultCode.FORBIDDEN, "文件路径越界"); + try { + Files.createDirectories(target.getParent()); + file.transferTo(target); + return new Stored(objectKey, type, file.getSize(), sha256(target), safeName(file)); + } catch (IOException exception) { + throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "产品图片保存失败"); + } + } + + public byte[] read(String objectKey) { + Path path = root.resolve(objectKey).normalize(); + if (!path.startsWith(root) || !Files.isRegularFile(path)) { + throw new BizException(ResultCode.NOT_FOUND, "产品图片不存在"); + } + try { + return Files.readAllBytes(path); + } catch (IOException exception) { + throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "产品图片读取失败"); + } + } + + public void delete(String objectKey) { + Path path = root.resolve(objectKey).normalize(); + if (!path.startsWith(root)) throw new BizException(ResultCode.FORBIDDEN, "文件路径越界"); + try { + Files.deleteIfExists(path); + } catch (IOException exception) { + throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "产品图片清理失败"); + } + } + + private void verifyImage(MultipartFile file) { + try (InputStream input = file.getInputStream()) { + if (ImageIO.read(input) == null) throw bad("上传内容不是有效图片"); + } catch (IOException exception) { + throw bad("上传内容不是有效图片"); + } + } + + private static String sha256(Path path) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (InputStream input = Files.newInputStream(path)) { + byte[] buffer = new byte[8192]; + for (int count; (count = input.read(buffer)) >= 0;) digest.update(buffer, 0, count); + } + return HexFormat.of().formatHex(digest.digest()); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException(exception); + } + } + + private static String safeName(MultipartFile file) { + String name = file.getOriginalFilename(); + if (name == null || name.isBlank()) return "product-image"; + return Path.of(name).getFileName().toString(); + } + + private static BizException bad(String message) { + return new BizException(ResultCode.BAD_REQUEST, message); + } + + public record Stored(String objectKey, String contentType, long size, String sha256, String originalName) {} +} diff --git a/file/src/test/java/com/baiyue/file/service/MeasurementImageServiceEvidenceTest.java b/file/src/test/java/com/baiyue/file/service/MeasurementImageServiceEvidenceTest.java index 8b0b3ec3c..e579cedd5 100644 --- a/file/src/test/java/com/baiyue/file/service/MeasurementImageServiceEvidenceTest.java +++ b/file/src/test/java/com/baiyue/file/service/MeasurementImageServiceEvidenceTest.java @@ -24,6 +24,9 @@ import java.math.BigDecimal; 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.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -85,7 +88,7 @@ class MeasurementImageServiceEvidenceTest { LambdaQueryWrapper query = invocation.getArgument(0); query.getSqlSegment(); assertThat(query.getParamNameValuePairs().values()) - .contains(11L, 41L, "21", false); + .contains(11L, 41L, "21", "ACTIVE", false); return null; }); @@ -116,4 +119,47 @@ class MeasurementImageServiceEvidenceTest { assertThat(result.confidence()).isEqualByComparingTo("0.50972"); assertThat(result.ocrRawText()).isEqualTo("285.5\nKG"); } + + @Test + void deletesMeasurementEvidenceThroughTheConfiguredLogicalDelete() { + FileObjectEntity object = measurementEvidence(); + when(files.selectOne(any())).thenReturn(object); + when(files.updateById(object)).thenReturn(1); + when(files.deleteById(41L)).thenReturn(1); + + service.deleteMeasurementEvidence(11L, 21L, 41L, 31L); + + assertThat(object.getStatus()).isEqualTo("DELETED"); + assertThat(object.getDeleted()).isFalse(); + var ordered = inOrder(files, storage); + ordered.verify(files).updateById(object); + ordered.verify(files).deleteById(41L); + ordered.verify(storage).delete(object.getObjectKey()); + } + + @Test + void doesNotDeleteStoredContentWhenLogicalDeleteLosesTheRace() { + FileObjectEntity object = measurementEvidence(); + when(files.selectOne(any())).thenReturn(object); + when(files.updateById(object)).thenReturn(1); + when(files.deleteById(41L)).thenReturn(0); + + assertThatThrownBy(() -> service.deleteMeasurementEvidence(11L, 21L, 41L, 31L)) + .isInstanceOf(com.baiyue.common.core.exception.BizException.class) + .hasMessageContaining("已被删除"); + + verify(storage, never()).delete(any()); + } + + private FileObjectEntity measurementEvidence() { + FileObjectEntity object = new FileObjectEntity(); + object.setId(41L); + object.setTenantId(11L); + object.setBusinessType("INBOUND_MEASUREMENT_IMAGE"); + object.setBusinessId("21"); + object.setObjectKey("measurements/11/21/evidence.png"); + object.setStatus("ACTIVE"); + object.setDeleted(false); + return object; + } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java index db9f3d215..dc2423f2d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java @@ -151,9 +151,11 @@ public class PlatformInboundMeasurementController { @BusinessAudit(event = "INBOUND_WEIGHT_OCR_CONFIRM", resourceType = "INBOUND_WEIGHT_RESULT", resourceId = "#resultId", description = "确认 OCR 重量并累加实际总毛重") - @Operation(summary = "确认 OCR 重量", description = "统一换算为 KG 并重算所有已确认图片的总重量。") + @Operation(summary = "确认 OCR 重量", + description = "可在同一事务中携带人工修改值并确认;统一换算为 KG 后重算所有已确认图片的总重量。") @PostMapping("/weight-results/{resultId}/confirm") - @PreAuthorize("hasAuthority('platform:firstleg:inbound:weight:confirm')") + @PreAuthorize("hasAuthority('platform:firstleg:inbound:weight:confirm')" + + " and (!#request.hasAdjustment() or hasAuthority('platform:firstleg:inbound:weight:edit'))") public ApiResponse confirmWeightResult( @AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java index 6cdcff982..12d79d666 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementRequests.java @@ -94,7 +94,13 @@ public final class InboundMeasurementRequests { } public record ConfirmWeightResultRequest( - @NotNull @PositiveOrZero Integer version + @NotNull @PositiveOrZero Integer version, + @DecimalMin("0.001") @Digits(integer = 15, fraction = 3) BigDecimal adjustedWeightValue, + @Size(max = 8) String adjustedWeightUnit, + @Size(max = 500) String adjustmentReason ) { + public boolean hasAdjustment() { + return adjustedWeightValue != null || adjustedWeightUnit != null || adjustmentReason != null; + } } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java index aeefd08f9..5cea4873d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java @@ -156,13 +156,22 @@ public class InboundMeasurementService { InboundOrderEntity inbound = requireInbound(tenantId, inboundId); InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId); requireVersion(request.version(), weight.getVersion()); - String adjustedUnit = normalizeUnit(request.adjustedWeightUnit()); + applyWeightCorrection(weight, request.version(), request.adjustedWeightValue(), + request.adjustedWeightUnit(), request.adjustmentReason(), operatorId, operatorName); + return toWeight(weight); + } + + private int applyWeightCorrection( + InboundWeightResultEntity weight, int expectedVersion, BigDecimal adjustedWeightValue, + String adjustedWeightUnit, String adjustmentReason, long operatorId, String operatorName) { + requireCompleteAdjustment(adjustedWeightValue, adjustedWeightUnit, adjustmentReason); + String adjustedUnit = normalizeUnit(adjustedWeightUnit); BigDecimal beforeValue = effectiveValue(weight); String beforeUnit = effectiveUnit(weight); - weight.setAdjustedWeightValue(request.adjustedWeightValue()); + weight.setAdjustedWeightValue(adjustedWeightValue); weight.setAdjustedWeightUnit(adjustedUnit); - weight.setAdjustmentReason(request.adjustmentReason()); + weight.setAdjustmentReason(adjustmentReason.trim()); weight.setAdjustedBy(operatorId); weight.setAdjustedAt(LocalDateTime.now()); weight.setFinalWeightKg(null); @@ -173,10 +182,12 @@ public class InboundMeasurementService { if (!repository.updateWeight(weight)) { throw concurrentModification(); } - insertRevision(weight, request.version() + 1, "CORRECT", beforeValue, beforeUnit, - request.adjustedWeightValue(), adjustedUnit, null, request.adjustmentReason(), + int correctedVersion = expectedVersion + 1; + weight.setVersion(correctedVersion); + insertRevision(weight, correctedVersion, "CORRECT", beforeValue, beforeUnit, + adjustedWeightValue, adjustedUnit, null, adjustmentReason.trim(), operatorId, operatorName); - return toWeight(weight); + return correctedVersion; } @Transactional @@ -186,7 +197,12 @@ public class InboundMeasurementService { InboundOrderEntity inbound = requireInbound(tenantId, inboundId); InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId); requireVersion(request.version(), weight.getVersion()); - if (WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus())) { + int currentVersion = request.version(); + if (request.hasAdjustment()) { + currentVersion = applyWeightCorrection(weight, currentVersion, + request.adjustedWeightValue(), request.adjustedWeightUnit(), + request.adjustmentReason(), operatorId, operatorName); + } else if (WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus())) { return toWeight(weight); } BigDecimal value = effectiveValue(weight); @@ -204,7 +220,9 @@ public class InboundMeasurementService { if (!repository.updateWeight(weight)) { throw concurrentModification(); } - insertRevision(weight, request.version() + 1, "CONFIRM", value, unit, value, unit, + int confirmedVersion = currentVersion + 1; + weight.setVersion(confirmedVersion); + insertRevision(weight, confirmedVersion, "CONFIRM", value, unit, value, unit, finalKg, null, operatorId, operatorName); recalculateConfirmedWeight(inbound, operatorId); return toWeight(weight); @@ -235,6 +253,13 @@ public class InboundMeasurementService { return isPositive(total) ? total : null; } + private void requireCompleteAdjustment(BigDecimal value, String unit, String reason) { + if (!isPositive(value) || !StringUtils.hasText(unit) || !StringUtils.hasText(reason)) { + throw new BizException(ResultCode.VALIDATION_FAILED, + "人工修改重量时必须同时填写有效重量、单位和修改原因"); + } + } + private void insertRevision(InboundWeightResultEntity weight, int revisionNo, String action, BigDecimal beforeValue, String beforeUnit, BigDecimal afterValue, String afterUnit, BigDecimal finalWeightKg, diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index bd05e15ca..258260170 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -224,6 +224,25 @@ paths: responses: '200': {description: 成功或业务失败,按响应体 code 判断} '403': {description: 无测量编辑权限} + /api/platform/v1/first-leg/inbounds/{inboundId}/weight-results/{resultId}/confirm: + post: + summary: 确认 OCR 重量 + description: >- + 需要 platform:firstleg:inbound:weight:confirm 权限;携带人工修改字段时还需要 + platform:firstleg:inbound:weight:edit 权限。修改与确认在同一事务内完成,保留 OCR 原值, + 分别写入 CORRECT 和 CONFIRM 修订记录,并重算所有已确认图片的实际总毛重。 + parameters: + - {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + - {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + - {name: resultId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/ConfirmWeightResultRequest'} + responses: + '200': {description: 成功或业务失败,按响应体 code 判断} + '403': {description: 无确认权限,或修改重量时无编辑权限} /api/platform/v1/inbounds/{inboundId}/measurements/approve: post: summary: 审核通过测量 @@ -339,6 +358,15 @@ components: in: query schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20} schemas: + ConfirmWeightResultRequest: + type: object + required: [version] + description: 人工修改字段必须同时提供;全部省略时直接确认现有 OCR 或已修正重量。 + properties: + version: {type: integer, minimum: 0} + adjustedWeightValue: {type: number, format: double, minimum: 0.001, nullable: true} + adjustedWeightUnit: {type: string, enum: [KG, LB], nullable: true} + adjustmentReason: {type: string, minLength: 1, maxLength: 500, nullable: true} OrderApprovedInboundCommand: type: object required: [tenantId, orderId, inboundCode, customerName, warehouseId] diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java index 880a8db23..a720e8ee5 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java @@ -35,7 +35,8 @@ class PlatformInboundMeasurementControllerContractTest { assertAuthority("correctWeightResult", "hasAuthority('platform:firstleg:inbound:weight:edit')", AuthenticatedUser.class, long.class, long.class, long.class, InboundMeasurementRequests.CorrectWeightResultRequest.class); - assertAuthority("confirmWeightResult", "hasAuthority('platform:firstleg:inbound:weight:confirm')", + assertAuthority("confirmWeightResult", "hasAuthority('platform:firstleg:inbound:weight:confirm')" + + " and (!#request.hasAdjustment() or hasAuthority('platform:firstleg:inbound:weight:edit'))", AuthenticatedUser.class, long.class, long.class, long.class, InboundMeasurementRequests.ConfirmWeightResultRequest.class); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java index 5a0766501..fb480d621 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -22,10 +22,12 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -113,7 +115,7 @@ class InboundMeasurementServiceTest { InboundMeasurementResponses.WeightResult response = service.confirmWeightResult( 1L, 10L, 20L, 9L, "操作员", - new InboundMeasurementRequests.ConfirmWeightResultRequest(0)); + new InboundMeasurementRequests.ConfirmWeightResultRequest(0, null, null, null)); assertThat(response.finalWeightKg()).isEqualByComparingTo("4.536"); assertThat(inbound.getActualGrossWeightKg()).isEqualByComparingTo("6.536"); @@ -121,6 +123,56 @@ class InboundMeasurementServiceTest { verify(repository).insertRevision(any(InboundWeightRevisionEntity.class)); } + @Test + void confirmingAdjustedWeightWritesCorrectionAndConfirmationInOneTransaction() { + InboundOrderEntity inbound = inbound("WAIT_MEASURE"); + InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "PENDING_CONFIRM", 0); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight); + when(repository.updateWeight(weight)).thenReturn(true); + when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight)); + when(repository.updateInbound(inbound)).thenReturn(true); + + InboundMeasurementResponses.WeightResult response = service.confirmWeightResult( + 1L, 10L, 20L, 9L, "操作员", + new InboundMeasurementRequests.ConfirmWeightResultRequest( + 0, new BigDecimal("286.125"), "kg", "OCR 原值 285.500 KG,人工确认 286.125 KG")); + + assertThat(response.adjustedWeightValue()).isEqualByComparingTo("286.125"); + assertThat(response.finalWeightKg()).isEqualByComparingTo("286.125"); + assertThat(response.recognitionStatus()).isEqualTo("CONFIRMED"); + assertThat(response.version()).isEqualTo(2); + assertThat(inbound.getActualGrossWeightKg()).isEqualByComparingTo("286.125"); + verify(repository, times(2)).updateWeight(weight); + ArgumentCaptor revisions = + ArgumentCaptor.forClass(InboundWeightRevisionEntity.class); + verify(repository, times(2)).insertRevision(revisions.capture()); + assertThat(revisions.getAllValues()).extracting( + InboundWeightRevisionEntity::getRevisionNo, + InboundWeightRevisionEntity::getAction) + .containsExactly(tuple(1, "CORRECT"), tuple(2, "CONFIRM")); + assertThat(revisions.getAllValues().get(0).getReason()) + .isEqualTo("OCR 原值 285.500 KG,人工确认 286.125 KG"); + } + + @Test + void confirmingRejectsPartialAdjustmentBeforePersisting() { + InboundOrderEntity inbound = inbound("WAIT_MEASURE"); + InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "PENDING_CONFIRM", 0); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight); + + assertThatThrownBy(() -> service.confirmWeightResult( + 1L, 10L, 20L, 9L, "操作员", + new InboundMeasurementRequests.ConfirmWeightResultRequest( + 0, new BigDecimal("286.125"), null, null))) + .isInstanceOf(BizException.class) + .hasMessageContaining("有效重量、单位和修改原因"); + + verify(repository, never()).updateWeight(any()); + verify(repository, never()).insertRevision(any()); + } + @Test void submitAdvancesOnlyToMeasurePendingAfterAllWeightsConfirmed() { InboundOrderEntity inbound = inbound("MEASURING"); diff --git a/order/src/main/java/com/baiyue/order/client/ProductImageClient.java b/order/src/main/java/com/baiyue/order/client/ProductImageClient.java new file mode 100644 index 000000000..c4c7cf428 --- /dev/null +++ b/order/src/main/java/com/baiyue/order/client/ProductImageClient.java @@ -0,0 +1,19 @@ +package com.baiyue.order.client; + +import com.baiyue.common.core.api.ApiResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "file-service", contextId = "orderProductImageClient", + url = "${baiyue.clients.file.url:}", path = "/internal/file/v1/product-images") +public interface ProductImageClient { + @GetMapping("/{tenantId}/{fileId}") + ApiResponse validate( + @RequestHeader("X-Internal-Service-Token") String token, + @PathVariable long tenantId, + @PathVariable long fileId); + + record FileMetadata(Long fileId, String fileName, String contentType, Long sizeBytes) {} +} diff --git a/order/src/main/java/com/baiyue/order/dto/OrderRequests.java b/order/src/main/java/com/baiyue/order/dto/OrderRequests.java index e455ac1b3..156cec35f 100644 --- a/order/src/main/java/com/baiyue/order/dto/OrderRequests.java +++ b/order/src/main/java/com/baiyue/order/dto/OrderRequests.java @@ -135,7 +135,13 @@ public final class OrderRequests { @Size(max = 128) String material, @Size(max = 128) String usage, @Size(max = 64) String model, - @Size(max = 64) String brand + @Size(max = 64) String brand, + @DecimalMin("0.01") BigDecimal productLengthCm, + @DecimalMin("0.01") BigDecimal productWidthCm, + @DecimalMin("0.01") BigDecimal productHeightCm, + @Size(max = 64) String shipmentId, + @Size(max = 64) String referenceId, + @Schema(description = "Product image file ID / 产品图片文件ID") Long productImageFileId ) { } diff --git a/order/src/main/java/com/baiyue/order/entity/OrderCargoEntity.java b/order/src/main/java/com/baiyue/order/entity/OrderCargoEntity.java index e09ed5295..08a5236d4 100644 --- a/order/src/main/java/com/baiyue/order/entity/OrderCargoEntity.java +++ b/order/src/main/java/com/baiyue/order/entity/OrderCargoEntity.java @@ -12,7 +12,7 @@ import com.baiyue.common.core.model.BaseEntity; import com.baomidou.mybatisplus. * 申报价值及危险品标识等货物信息。

*/ @TableName("ord_order_cargo") public class OrderCargoEntity extends BaseEntity { - @TableId(type=IdType.ASSIGN_ID) private Long id; private Long tenantId; private Long orderId; private Integer itemNo; private String cargoName; private String hsCode; private String packageType; private Integer packageCount; private BigDecimal grossWeightKg; private BigDecimal volumeCbm; private BigDecimal lengthCm; private BigDecimal widthCm; private BigDecimal heightCm; private BigDecimal declaredValue; private String currency; private Boolean dangerousGoods; private String cargoRemark; private String skuNo; private String upcNo; private String cargoNameEn; private String customsHsCode; private Integer unitsPerPackage; private Integer totalQuantity; private BigDecimal netWeightKg; private BigDecimal unitDeclaredValue; private String material; private String productUsage; private String model; private String brand; + @TableId(type=IdType.ASSIGN_ID) private Long id; private Long tenantId; private Long orderId; private Integer itemNo; private String cargoName; private String hsCode; private String packageType; private Integer packageCount; private BigDecimal grossWeightKg; private BigDecimal volumeCbm; private BigDecimal lengthCm; private BigDecimal widthCm; private BigDecimal heightCm; private BigDecimal declaredValue; private String currency; private Boolean dangerousGoods; private String cargoRemark; private String skuNo; private String upcNo; private String cargoNameEn; private String customsHsCode; private Integer unitsPerPackage; private Integer totalQuantity; private BigDecimal netWeightKg; private BigDecimal unitDeclaredValue; private String material; private String productUsage; private String model; private String brand; private BigDecimal productLengthCm; private BigDecimal productWidthCm; private BigDecimal productHeightCm; private String shipmentId; private String referenceId; private Long productImageFileId; } diff --git a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java index aff208993..869d6e2b8 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java +++ b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java @@ -33,6 +33,7 @@ public class OrderApplicationService { @Resource private OrderEligibilityGuard eligibilityGuard; @Resource private OrderMasterDataGuard masterDataGuard; @Resource private OrderPricingChannelGuard pricingChannelGuard; + @Resource private ProductImageGuard productImageGuard; /** 创建草稿时使用数据库唯一键保证请求幂等。 */ @Transactional @@ -49,6 +50,7 @@ public class OrderApplicationService { private Long createDraftInternal(long tenantId, long userId, OrderRequests.SaveRequest request) { validatePrototypeFields(request); + productImageGuard.requireOwned(tenantId, request.cargoes()); var snapshots = masterDataGuard.validate(tenantId, request); pricingChannelGuard.requireAvailable(tenantId, request.receivingWarehouseId(), request.channelCode()); OrderEntity order = new OrderEntity(); @@ -80,6 +82,7 @@ public class OrderApplicationService { boolean allowSubmitted) { OrderEntity order = require(tenantId, id); validatePrototypeFields(request); + productImageGuard.requireOwned(tenantId, request.cargoes()); var snapshots = masterDataGuard.validate(tenantId, request); OrderStatus before = order.getStatus(); if (before != OrderStatus.DRAFT && before != OrderStatus.REJECTED @@ -189,8 +192,9 @@ public class OrderApplicationService { throw new BizException("FBA orders require Amazon warehouse code, Shipment ID and Reference ID"); } for (var cargo : request.cargoes()) { - if (!StringUtils.hasText(cargo.skuNo()) || !StringUtils.hasText(cargo.upcNo())) { - throw new BizException("FBA cargo requires SKU NO. and UPC NO."); + if (!StringUtils.hasText(cargo.skuNo()) || !StringUtils.hasText(cargo.upcNo()) + || !StringUtils.hasText(cargo.shipmentId()) || !StringUtils.hasText(cargo.referenceId())) { + throw new BizException("FBA cargo requires SKU NO., UPC NO., Shipment ID and Reference ID."); } } } @@ -384,6 +388,10 @@ public class OrderApplicationService { item.setTotalQuantity(cargo.totalQuantity()); item.setNetWeightKg(cargo.netWeightKg()); item.setUnitDeclaredValue(cargo.unitDeclaredValue()); item.setMaterial(cargo.material()); item.setProductUsage(cargo.usage()); item.setModel(cargo.model()); item.setBrand(cargo.brand()); + item.setProductLengthCm(cargo.productLengthCm()); item.setProductWidthCm(cargo.productWidthCm()); + item.setProductHeightCm(cargo.productHeightCm()); item.setShipmentId(cargo.shipmentId()); + item.setReferenceId(cargo.referenceId()); + item.setProductImageFileId(cargo.productImageFileId()); item.setCreatedBy(userId); item.setUpdatedBy(userId); repository.insertCargo(item); } if (request.sender() != null) saveParty(tenantId, userId, orderId, "SENDER", request.sender()); diff --git a/order/src/main/java/com/baiyue/order/service/ProductImageGuard.java b/order/src/main/java/com/baiyue/order/service/ProductImageGuard.java new file mode 100644 index 000000000..a70a1026c --- /dev/null +++ b/order/src/main/java/com/baiyue/order/service/ProductImageGuard.java @@ -0,0 +1,58 @@ +package com.baiyue.order.service; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.order.client.ProductImageClient; +import com.baiyue.order.dto.OrderRequests; +import feign.FeignException; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +public class ProductImageGuard { + @Resource private ProductImageClient client; + @Value("${baiyue.security.internal-service-token}") private String internalToken; + + public void requireOwned(long tenantId, List cargoes) { + Set validated = new HashSet<>(); + for (OrderRequests.Cargo cargo : cargoes) { + Long fileId = cargo.productImageFileId(); + if (fileId == null || !validated.add(fileId)) continue; + if (fileId <= 0) throw new BizException(ResultCode.VALIDATION_FAILED, "产品图片文件ID无效"); + validate(tenantId, fileId); + } + } + + private void validate(long tenantId, long fileId) { + try { + ApiResponse response = client.validate(internalToken, tenantId, fileId); + if (response == null) throw unavailable(); + if (!response.successful()) { + ResultCode code = ResultCode.of(response.code()); + String message = response.message() == null || response.message().isBlank() + ? code.message() : response.message(); + throw new BizException(code, message); + } + if (response.data() == null || response.data().fileId() == null + || response.data().fileId() != fileId) { + throw unavailable(); + } + } catch (BizException exception) { + throw exception; + } catch (FeignException exception) { + throw unavailable(); + } catch (RuntimeException exception) { + throw unavailable(); + } + } + + private static BizException unavailable() { + return new BizException(ResultCode.SERVICE_UNAVAILABLE, "产品图片校验服务暂时不可用"); + } +} diff --git a/order/src/main/resources/application.yml b/order/src/main/resources/application.yml index 17e31bf60..19677f6c6 100644 --- a/order/src/main/resources/application.yml +++ b/order/src/main/resources/application.yml @@ -33,6 +33,8 @@ baiyue: url: ${IAM_SERVICE_URL:} warehouse: url: ${WAREHOUSE_SERVICE_URL:} + file: + url: ${FILE_SERVICE_URL:} logging: file: diff --git a/order/src/main/resources/db/order.sql b/order/src/main/resources/db/order.sql index 9c51385fa..f8697e490 100644 --- a/order/src/main/resources/db/order.sql +++ b/order/src/main/resources/db/order.sql @@ -111,6 +111,12 @@ CREATE TABLE IF NOT EXISTS ord_order_cargo ( product_usage VARCHAR(128) NULL COMMENT '用途', model VARCHAR(64) NULL COMMENT '型号', brand VARCHAR(64) NULL COMMENT '品牌', + product_length_cm DECIMAL(12,2) NULL COMMENT '产品长度(厘米)', + product_width_cm DECIMAL(12,2) NULL COMMENT '产品宽度(厘米)', + product_height_cm DECIMAL(12,2) NULL COMMENT '产品高度(厘米)', + shipment_id VARCHAR(64) NULL COMMENT '货物级 Amazon Shipment ID', + reference_id VARCHAR(64) NULL COMMENT '货物级 Amazon Reference ID', + product_image_file_id BIGINT UNSIGNED NULL COMMENT '文件中心产品图片逻辑ID', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', diff --git a/order/src/main/resources/db/phase1-order-extension.sql b/order/src/main/resources/db/phase1-order-extension.sql index 44cfc7162..426311429 100644 --- a/order/src/main/resources/db/phase1-order-extension.sql +++ b/order/src/main/resources/db/phase1-order-extension.sql @@ -54,6 +54,12 @@ CALL phase1_add_column('ord_order_cargo', 'material', "VARCHAR(128) NULL COMMENT CALL phase1_add_column('ord_order_cargo', 'product_usage', "VARCHAR(128) NULL COMMENT 'Usage'"); CALL phase1_add_column('ord_order_cargo', 'model', "VARCHAR(64) NULL COMMENT 'Model'"); CALL phase1_add_column('ord_order_cargo', 'brand', "VARCHAR(64) NULL COMMENT 'Brand'"); +CALL phase1_add_column('ord_order_cargo', 'product_length_cm', "DECIMAL(12,2) NULL COMMENT 'Product length in centimeters'"); +CALL phase1_add_column('ord_order_cargo', 'product_width_cm', "DECIMAL(12,2) NULL COMMENT 'Product width in centimeters'"); +CALL phase1_add_column('ord_order_cargo', 'product_height_cm', "DECIMAL(12,2) NULL COMMENT 'Product height in centimeters'"); +CALL phase1_add_column('ord_order_cargo', 'shipment_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Shipment ID'"); +CALL phase1_add_column('ord_order_cargo', 'reference_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Reference ID'"); +CALL phase1_add_column('ord_order_cargo', 'product_image_file_id', "BIGINT UNSIGNED NULL COMMENT 'Product image logical file ID'"); DROP PROCEDURE phase1_add_column; diff --git a/order/src/test/java/com/baiyue/order/dto/OrderRequestsValidationTest.java b/order/src/test/java/com/baiyue/order/dto/OrderRequestsValidationTest.java index 0e74ff3ba..9ffd224f5 100644 --- a/order/src/test/java/com/baiyue/order/dto/OrderRequestsValidationTest.java +++ b/order/src/test/java/com/baiyue/order/dto/OrderRequestsValidationTest.java @@ -39,7 +39,8 @@ class OrderRequestsValidationTest { var cargo = new OrderRequests.Cargo( "货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE, null, null, null, null, null, false, null, null, null, null, - null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null); var request = new OrderRequests.SaveRequest( null, "当前企业", "SEA", "FOB", "US", 193000000000200003L, "洛杉矶仓", null, null, null, null, List.of(cargo), "SELF_EXPRESS", 193000000000200001L, diff --git a/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java b/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java index 7dfdd7856..31f02569c 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderApplicationServiceStateTest.java @@ -398,6 +398,7 @@ class OrderApplicationServiceStateTest { ReflectionTestUtils.setField(service, "eligibilityGuard", eligibility); ReflectionTestUtils.setField(service, "masterDataGuard", masterData); ReflectionTestUtils.setField(service, "pricingChannelGuard", mock(OrderPricingChannelGuard.class)); + ReflectionTestUtils.setField(service, "productImageGuard", mock(ProductImageGuard.class)); ReflectionTestUtils.setField(service, "json", new ObjectMapper()); return service; } @@ -408,7 +409,8 @@ class OrderApplicationServiceStateTest { var cargo = new OrderRequests.Cargo( "货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE, null, null, null, null, null, false, null, null, null, null, - null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null); return new OrderRequests.SaveRequest( 7L, "客户", "SEA", null, "US", null, "目的仓", null, null, party, party, List.of(cargo), "PICKUP", 8L, "收货仓", "CH", "渠道", 9L, "船司", @@ -421,7 +423,8 @@ class OrderApplicationServiceStateTest { var cargo = new OrderRequests.Cargo( "货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE, null, null, null, null, null, false, null, null, null, null, - null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null); return new OrderRequests.SaveRequest( customerId, "当前企业", "SEA", null, "US", null, "目的仓", null, null, sender, receiver, List.of(cargo), pickupMethod, 8L, "收货仓", "CH", "渠道", 9L, "船司", diff --git a/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java b/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java index d74426d6a..0abaf9e5b 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java @@ -253,7 +253,8 @@ class OrderMasterDataGuardTest { private OrderRequests.SaveRequest request(Long partner,Long warehouse,String destinationType,Long destinationWarehouseId) { var party=new OrderRequests.Party("公司","联系人","13800138000",null,"CN",null,"深圳",null,"地址",null); var cargo=new OrderRequests.Cargo("货物",null,"CARTON",1,BigDecimal.ONE,BigDecimal.ONE, - null,null,null,null,null,false,null,null,null,null,null,null,null,null,null,null,null,null,null); + null,null,null,null,null,false,null,null,null,null,null,null,null,null,null,null,null,null,null, + null,null,null,null,null,null); return new OrderRequests.SaveRequest(partner,"不可信客户名","SEA",null,"CN",destinationWarehouseId,"私人地址",null,null,party,party,List.of(cargo),"PICKUP",warehouse,"不可信收货仓","CH","渠道",9L,"船司","BUY_DECLARATION",null,"LCL",false,destinationType,null,null,null); } private WarehouseClient.Detail warehouse(long id,String type,boolean receiving) {