This commit is contained in:
2026-08-07 18:44:51 +08:00
parent 1a76602872
commit 37fec1e012
24 changed files with 601 additions and 24 deletions
@@ -513,6 +513,12 @@ CREATE TABLE IF NOT EXISTS ord_order_cargo (
product_usage VARCHAR(128) NULL COMMENT '用途', product_usage VARCHAR(128) NULL COMMENT '用途',
model VARCHAR(64) NULL COMMENT '型号', model VARCHAR(64) NULL COMMENT '型号',
brand 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_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', 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', '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', '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', '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; DROP PROCEDURE phase1_add_column;
@@ -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<ProductImageService.UploadResult> 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, "内部服务凭证无效");
}
}
}
@@ -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<ProductImageService.UploadResult> 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<byte[]> 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();
}
}
@@ -87,11 +87,12 @@ public class MeasurementImageService {
if (!"INBOUND_MEASUREMENT_IMAGE".equals(object.getBusinessType())) { if (!"INBOUND_MEASUREMENT_IMAGE".equals(object.getBusinessType())) {
throw new BizException(ResultCode.CONFLICT, "仅允许清理第三方尺寸测量证据图片"); throw new BizException(ResultCode.CONFLICT, "仅允许清理第三方尺寸测量证据图片");
} }
object.setDeleted(true);
object.setStatus("DELETED"); object.setStatus("DELETED");
object.setUpdatedBy(operatorId); object.setUpdatedBy(operatorId);
object.setUpdatedAt(LocalDateTime.now()); 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()); storage.delete(object.getObjectKey());
} }
@@ -101,6 +102,7 @@ public class MeasurementImageService {
.eq(FileObjectEntity::getId, fileId) .eq(FileObjectEntity::getId, fileId)
.in(FileObjectEntity::getBusinessType, "INBOUND_WEIGHT_IMAGE", "INBOUND_MEASUREMENT_IMAGE") .in(FileObjectEntity::getBusinessType, "INBOUND_WEIGHT_IMAGE", "INBOUND_MEASUREMENT_IMAGE")
.eq(FileObjectEntity::getBusinessId, Long.toString(inboundId)) .eq(FileObjectEntity::getBusinessId, Long.toString(inboundId))
.eq(FileObjectEntity::getStatus, "ACTIVE")
.eq(FileObjectEntity::getDeleted, false)); .eq(FileObjectEntity::getDeleted, false));
if (object == null) throw new BizException(ResultCode.NOT_FOUND, "称重图片不存在"); if (object == null) throw new BizException(ResultCode.NOT_FOUND, "称重图片不存在");
return object; return object;
@@ -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<FileObjectEntity>()
.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) {}
}
@@ -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<String> 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) {}
}
@@ -24,6 +24,9 @@ import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any; 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.doAnswer;
import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -85,7 +88,7 @@ class MeasurementImageServiceEvidenceTest {
LambdaQueryWrapper<FileObjectEntity> query = invocation.getArgument(0); LambdaQueryWrapper<FileObjectEntity> query = invocation.getArgument(0);
query.getSqlSegment(); query.getSqlSegment();
assertThat(query.getParamNameValuePairs().values()) assertThat(query.getParamNameValuePairs().values())
.contains(11L, 41L, "21", false); .contains(11L, 41L, "21", "ACTIVE", false);
return null; return null;
}); });
@@ -116,4 +119,47 @@ class MeasurementImageServiceEvidenceTest {
assertThat(result.confidence()).isEqualByComparingTo("0.50972"); assertThat(result.confidence()).isEqualByComparingTo("0.50972");
assertThat(result.ocrRawText()).isEqualTo("285.5\nKG"); 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;
}
} }
@@ -151,9 +151,11 @@ public class PlatformInboundMeasurementController {
@BusinessAudit(event = "INBOUND_WEIGHT_OCR_CONFIRM", resourceType = "INBOUND_WEIGHT_RESULT", @BusinessAudit(event = "INBOUND_WEIGHT_OCR_CONFIRM", resourceType = "INBOUND_WEIGHT_RESULT",
resourceId = "#resultId", description = "确认 OCR 重量并累加实际总毛重") resourceId = "#resultId", description = "确认 OCR 重量并累加实际总毛重")
@Operation(summary = "确认 OCR 重量", description = "统一换算为 KG 并重算所有已确认图片的总重量。") @Operation(summary = "确认 OCR 重量",
description = "可在同一事务中携带人工修改值并确认;统一换算为 KG 后重算所有已确认图片的总重量。")
@PostMapping("/weight-results/{resultId}/confirm") @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<InboundMeasurementResponses.WeightResult> confirmWeightResult( public ApiResponse<InboundMeasurementResponses.WeightResult> confirmWeightResult(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId, @RequestParam long tenantId,
@@ -94,7 +94,13 @@ public final class InboundMeasurementRequests {
} }
public record ConfirmWeightResultRequest( 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;
}
} }
} }
@@ -156,13 +156,22 @@ public class InboundMeasurementService {
InboundOrderEntity inbound = requireInbound(tenantId, inboundId); InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId); InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
requireVersion(request.version(), weight.getVersion()); 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); BigDecimal beforeValue = effectiveValue(weight);
String beforeUnit = effectiveUnit(weight); String beforeUnit = effectiveUnit(weight);
weight.setAdjustedWeightValue(request.adjustedWeightValue()); weight.setAdjustedWeightValue(adjustedWeightValue);
weight.setAdjustedWeightUnit(adjustedUnit); weight.setAdjustedWeightUnit(adjustedUnit);
weight.setAdjustmentReason(request.adjustmentReason()); weight.setAdjustmentReason(adjustmentReason.trim());
weight.setAdjustedBy(operatorId); weight.setAdjustedBy(operatorId);
weight.setAdjustedAt(LocalDateTime.now()); weight.setAdjustedAt(LocalDateTime.now());
weight.setFinalWeightKg(null); weight.setFinalWeightKg(null);
@@ -173,10 +182,12 @@ public class InboundMeasurementService {
if (!repository.updateWeight(weight)) { if (!repository.updateWeight(weight)) {
throw concurrentModification(); throw concurrentModification();
} }
insertRevision(weight, request.version() + 1, "CORRECT", beforeValue, beforeUnit, int correctedVersion = expectedVersion + 1;
request.adjustedWeightValue(), adjustedUnit, null, request.adjustmentReason(), weight.setVersion(correctedVersion);
insertRevision(weight, correctedVersion, "CORRECT", beforeValue, beforeUnit,
adjustedWeightValue, adjustedUnit, null, adjustmentReason.trim(),
operatorId, operatorName); operatorId, operatorName);
return toWeight(weight); return correctedVersion;
} }
@Transactional @Transactional
@@ -186,7 +197,12 @@ public class InboundMeasurementService {
InboundOrderEntity inbound = requireInbound(tenantId, inboundId); InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId); InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
requireVersion(request.version(), weight.getVersion()); 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); return toWeight(weight);
} }
BigDecimal value = effectiveValue(weight); BigDecimal value = effectiveValue(weight);
@@ -204,7 +220,9 @@ public class InboundMeasurementService {
if (!repository.updateWeight(weight)) { if (!repository.updateWeight(weight)) {
throw concurrentModification(); 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); finalKg, null, operatorId, operatorName);
recalculateConfirmedWeight(inbound, operatorId); recalculateConfirmedWeight(inbound, operatorId);
return toWeight(weight); return toWeight(weight);
@@ -235,6 +253,13 @@ public class InboundMeasurementService {
return isPositive(total) ? total : null; 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, private void insertRevision(InboundWeightResultEntity weight, int revisionNo, String action,
BigDecimal beforeValue, String beforeUnit, BigDecimal beforeValue, String beforeUnit,
BigDecimal afterValue, String afterUnit, BigDecimal finalWeightKg, BigDecimal afterValue, String afterUnit, BigDecimal finalWeightKg,
@@ -224,6 +224,25 @@ paths:
responses: responses:
'200': {description: 成功或业务失败,按响应体 code 判断} '200': {description: 成功或业务失败,按响应体 code 判断}
'403': {description: 无测量编辑权限} '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: /api/platform/v1/inbounds/{inboundId}/measurements/approve:
post: post:
summary: 审核通过测量 summary: 审核通过测量
@@ -339,6 +358,15 @@ components:
in: query in: query
schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20} schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}
schemas: 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: OrderApprovedInboundCommand:
type: object type: object
required: [tenantId, orderId, inboundCode, customerName, warehouseId] required: [tenantId, orderId, inboundCode, customerName, warehouseId]
@@ -35,7 +35,8 @@ class PlatformInboundMeasurementControllerContractTest {
assertAuthority("correctWeightResult", "hasAuthority('platform:firstleg:inbound:weight:edit')", assertAuthority("correctWeightResult", "hasAuthority('platform:firstleg:inbound:weight:edit')",
AuthenticatedUser.class, long.class, long.class, long.class, AuthenticatedUser.class, long.class, long.class, long.class,
InboundMeasurementRequests.CorrectWeightResultRequest.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, AuthenticatedUser.class, long.class, long.class, long.class,
InboundMeasurementRequests.ConfirmWeightResultRequest.class); InboundMeasurementRequests.ConfirmWeightResultRequest.class);
} }
@@ -22,10 +22,12 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; 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.any;
import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -113,7 +115,7 @@ class InboundMeasurementServiceTest {
InboundMeasurementResponses.WeightResult response = service.confirmWeightResult( InboundMeasurementResponses.WeightResult response = service.confirmWeightResult(
1L, 10L, 20L, 9L, "操作员", 1L, 10L, 20L, 9L, "操作员",
new InboundMeasurementRequests.ConfirmWeightResultRequest(0)); new InboundMeasurementRequests.ConfirmWeightResultRequest(0, null, null, null));
assertThat(response.finalWeightKg()).isEqualByComparingTo("4.536"); assertThat(response.finalWeightKg()).isEqualByComparingTo("4.536");
assertThat(inbound.getActualGrossWeightKg()).isEqualByComparingTo("6.536"); assertThat(inbound.getActualGrossWeightKg()).isEqualByComparingTo("6.536");
@@ -121,6 +123,56 @@ class InboundMeasurementServiceTest {
verify(repository).insertRevision(any(InboundWeightRevisionEntity.class)); 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<InboundWeightRevisionEntity> 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 @Test
void submitAdvancesOnlyToMeasurePendingAfterAllWeightsConfirmed() { void submitAdvancesOnlyToMeasurePendingAfterAllWeightsConfirmed() {
InboundOrderEntity inbound = inbound("MEASURING"); InboundOrderEntity inbound = inbound("MEASURING");
@@ -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<FileMetadata> validate(
@RequestHeader("X-Internal-Service-Token") String token,
@PathVariable long tenantId,
@PathVariable long fileId);
record FileMetadata(Long fileId, String fileName, String contentType, Long sizeBytes) {}
}
@@ -135,7 +135,13 @@ public final class OrderRequests {
@Size(max = 128) String material, @Size(max = 128) String material,
@Size(max = 128) String usage, @Size(max = 128) String usage,
@Size(max = 64) String model, @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
) { ) {
} }
@@ -12,7 +12,7 @@ import com.baiyue.common.core.model.BaseEntity; import com.baomidou.mybatisplus.
* 申报价值及危险品标识等货物信息。</p> * 申报价值及危险品标识等货物信息。</p>
*/ */
@TableName("ord_order_cargo") public class OrderCargoEntity extends BaseEntity { @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;
} }
@@ -33,6 +33,7 @@ public class OrderApplicationService {
@Resource private OrderEligibilityGuard eligibilityGuard; @Resource private OrderEligibilityGuard eligibilityGuard;
@Resource private OrderMasterDataGuard masterDataGuard; @Resource private OrderMasterDataGuard masterDataGuard;
@Resource private OrderPricingChannelGuard pricingChannelGuard; @Resource private OrderPricingChannelGuard pricingChannelGuard;
@Resource private ProductImageGuard productImageGuard;
/** 创建草稿时使用数据库唯一键保证请求幂等。 */ /** 创建草稿时使用数据库唯一键保证请求幂等。 */
@Transactional @Transactional
@@ -49,6 +50,7 @@ public class OrderApplicationService {
private Long createDraftInternal(long tenantId, long userId, OrderRequests.SaveRequest request) { private Long createDraftInternal(long tenantId, long userId, OrderRequests.SaveRequest request) {
validatePrototypeFields(request); validatePrototypeFields(request);
productImageGuard.requireOwned(tenantId, request.cargoes());
var snapshots = masterDataGuard.validate(tenantId, request); var snapshots = masterDataGuard.validate(tenantId, request);
pricingChannelGuard.requireAvailable(tenantId, request.receivingWarehouseId(), request.channelCode()); pricingChannelGuard.requireAvailable(tenantId, request.receivingWarehouseId(), request.channelCode());
OrderEntity order = new OrderEntity(); OrderEntity order = new OrderEntity();
@@ -80,6 +82,7 @@ public class OrderApplicationService {
boolean allowSubmitted) { boolean allowSubmitted) {
OrderEntity order = require(tenantId, id); OrderEntity order = require(tenantId, id);
validatePrototypeFields(request); validatePrototypeFields(request);
productImageGuard.requireOwned(tenantId, request.cargoes());
var snapshots = masterDataGuard.validate(tenantId, request); var snapshots = masterDataGuard.validate(tenantId, request);
OrderStatus before = order.getStatus(); OrderStatus before = order.getStatus();
if (before != OrderStatus.DRAFT && before != OrderStatus.REJECTED 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"); throw new BizException("FBA orders require Amazon warehouse code, Shipment ID and Reference ID");
} }
for (var cargo : request.cargoes()) { for (var cargo : request.cargoes()) {
if (!StringUtils.hasText(cargo.skuNo()) || !StringUtils.hasText(cargo.upcNo())) { if (!StringUtils.hasText(cargo.skuNo()) || !StringUtils.hasText(cargo.upcNo())
throw new BizException("FBA cargo requires SKU NO. and UPC NO."); || !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.setTotalQuantity(cargo.totalQuantity()); item.setNetWeightKg(cargo.netWeightKg());
item.setUnitDeclaredValue(cargo.unitDeclaredValue()); item.setMaterial(cargo.material()); item.setUnitDeclaredValue(cargo.unitDeclaredValue()); item.setMaterial(cargo.material());
item.setProductUsage(cargo.usage()); item.setModel(cargo.model()); item.setBrand(cargo.brand()); 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); item.setCreatedBy(userId); item.setUpdatedBy(userId); repository.insertCargo(item);
} }
if (request.sender() != null) saveParty(tenantId, userId, orderId, "SENDER", request.sender()); if (request.sender() != null) saveParty(tenantId, userId, orderId, "SENDER", request.sender());
@@ -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<OrderRequests.Cargo> cargoes) {
Set<Long> 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<ProductImageClient.FileMetadata> 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, "产品图片校验服务暂时不可用");
}
}
+2
View File
@@ -33,6 +33,8 @@ baiyue:
url: ${IAM_SERVICE_URL:} url: ${IAM_SERVICE_URL:}
warehouse: warehouse:
url: ${WAREHOUSE_SERVICE_URL:} url: ${WAREHOUSE_SERVICE_URL:}
file:
url: ${FILE_SERVICE_URL:}
logging: logging:
file: file:
+6
View File
@@ -111,6 +111,12 @@ CREATE TABLE IF NOT EXISTS ord_order_cargo (
product_usage VARCHAR(128) NULL COMMENT '用途', product_usage VARCHAR(128) NULL COMMENT '用途',
model VARCHAR(64) NULL COMMENT '型号', model VARCHAR(64) NULL COMMENT '型号',
brand 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_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
@@ -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', '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', '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', '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; DROP PROCEDURE phase1_add_column;
@@ -39,7 +39,8 @@ class OrderRequestsValidationTest {
var cargo = new OrderRequests.Cargo( var cargo = new OrderRequests.Cargo(
"货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE, "货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE,
null, null, null, null, null, false, 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, null, null, null,
null, null, null, null, null, null);
var request = new OrderRequests.SaveRequest( var request = new OrderRequests.SaveRequest(
null, "当前企业", "SEA", "FOB", "US", 193000000000200003L, "洛杉矶仓", null, "当前企业", "SEA", "FOB", "US", 193000000000200003L, "洛杉矶仓",
null, null, null, null, List.of(cargo), "SELF_EXPRESS", 193000000000200001L, null, null, null, null, List.of(cargo), "SELF_EXPRESS", 193000000000200001L,
@@ -398,6 +398,7 @@ class OrderApplicationServiceStateTest {
ReflectionTestUtils.setField(service, "eligibilityGuard", eligibility); ReflectionTestUtils.setField(service, "eligibilityGuard", eligibility);
ReflectionTestUtils.setField(service, "masterDataGuard", masterData); ReflectionTestUtils.setField(service, "masterDataGuard", masterData);
ReflectionTestUtils.setField(service, "pricingChannelGuard", mock(OrderPricingChannelGuard.class)); ReflectionTestUtils.setField(service, "pricingChannelGuard", mock(OrderPricingChannelGuard.class));
ReflectionTestUtils.setField(service, "productImageGuard", mock(ProductImageGuard.class));
ReflectionTestUtils.setField(service, "json", new ObjectMapper()); ReflectionTestUtils.setField(service, "json", new ObjectMapper());
return service; return service;
} }
@@ -408,7 +409,8 @@ class OrderApplicationServiceStateTest {
var cargo = new OrderRequests.Cargo( var cargo = new OrderRequests.Cargo(
"货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE, "货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE,
null, null, null, null, null, false, 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, null, null, null,
null, null, null, null, null, null);
return new OrderRequests.SaveRequest( return new OrderRequests.SaveRequest(
7L, "客户", "SEA", null, "US", null, "目的仓", null, null, 7L, "客户", "SEA", null, "US", null, "目的仓", null, null,
party, party, List.of(cargo), "PICKUP", 8L, "收货仓", "CH", "渠道", 9L, "船司", party, party, List.of(cargo), "PICKUP", 8L, "收货仓", "CH", "渠道", 9L, "船司",
@@ -421,7 +423,8 @@ class OrderApplicationServiceStateTest {
var cargo = new OrderRequests.Cargo( var cargo = new OrderRequests.Cargo(
"货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE, "货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE,
null, null, null, null, null, false, 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, null, null, null,
null, null, null, null, null, null);
return new OrderRequests.SaveRequest( return new OrderRequests.SaveRequest(
customerId, "当前企业", "SEA", null, "US", null, "目的仓", null, null, customerId, "当前企业", "SEA", null, "US", null, "目的仓", null, null,
sender, receiver, List.of(cargo), pickupMethod, 8L, "收货仓", "CH", "渠道", 9L, "船司", sender, receiver, List.of(cargo), pickupMethod, 8L, "收货仓", "CH", "渠道", 9L, "船司",
@@ -253,7 +253,8 @@ class OrderMasterDataGuardTest {
private OrderRequests.SaveRequest request(Long partner,Long warehouse,String destinationType,Long destinationWarehouseId) { private OrderRequests.SaveRequest request(Long partner,Long warehouse,String destinationType,Long destinationWarehouseId) {
var party=new OrderRequests.Party("公司","联系人","13800138000",null,"CN",null,"深圳",null,"地址",null); var party=new OrderRequests.Party("公司","联系人","13800138000",null,"CN",null,"深圳",null,"地址",null);
var cargo=new OrderRequests.Cargo("货物",null,"CARTON",1,BigDecimal.ONE,BigDecimal.ONE, 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); 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) { private WarehouseClient.Detail warehouse(long id,String type,boolean receiving) {