feat(phase3): 收口父子单与全链路验收
This commit is contained in:
+1
-1
@@ -115,7 +115,7 @@ public class PlatformInboundMeasurementController {
|
||||
requirePlatformTenant(user, tenantId);
|
||||
CartonMeasurementCommand command = new CartonMeasurementCommand(
|
||||
request.boxCode(), request.length(), request.width(), request.height(), request.weight(),
|
||||
request.machineNo(), request.measuredAt(), image);
|
||||
request.machineNo(), request.measuredAt(), image, request.scannedShipmentId());
|
||||
return ApiResponse.success(cartonMeasurements.upsertForInbound(
|
||||
tenantId, user.userId(), inboundId, command));
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public record CartonMeasurementCommand(
|
||||
BigDecimal unitWeightKg,
|
||||
String machineNo,
|
||||
LocalDateTime measuredAt,
|
||||
MultipartFile image
|
||||
MultipartFile image,
|
||||
String scannedShipmentId
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ public record CartonMeasurementResult(
|
||||
Integer measurementVersion,
|
||||
Long itemId,
|
||||
String boxCode,
|
||||
String scannedShipmentId,
|
||||
Integer confirmedPackageCount,
|
||||
BigDecimal lengthCm,
|
||||
BigDecimal widthCm,
|
||||
|
||||
@@ -71,6 +71,7 @@ public final class InboundMeasurementRequests {
|
||||
@NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal width,
|
||||
@NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal height,
|
||||
@DecimalMin("0.001") @Digits(integer = 15, fraction = 3) BigDecimal weight,
|
||||
@Size(max = 64) String scannedShipmentId,
|
||||
@NotBlank @Size(max = 64) String machineNo,
|
||||
@NotNull LocalDateTime measuredAt
|
||||
) {
|
||||
|
||||
@@ -67,6 +67,7 @@ public final class InboundMeasurementResponses {
|
||||
BigDecimal outerHeightCm,
|
||||
String remark,
|
||||
String boxCode,
|
||||
String scannedShipmentId,
|
||||
Integer confirmedPackageCount,
|
||||
String machineNo,
|
||||
LocalDateTime deviceMeasuredAt,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.DecimalMax;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
@@ -16,6 +18,8 @@ public final class MojiaMeasurementDtos {
|
||||
|
||||
public record UploadRequest(
|
||||
@NotBlank @Size(max = 128) String boxCode,
|
||||
@JsonProperty("shipment_id") @JsonAlias("shipmentId")
|
||||
@NotBlank @Size(max = 64) String shipmentId,
|
||||
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal length,
|
||||
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal width,
|
||||
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal height,
|
||||
|
||||
@@ -21,6 +21,7 @@ public class InboundMeasurementItemEntity {
|
||||
private Long inboundDetailId;
|
||||
private String externalDetailNo;
|
||||
private String boxCode;
|
||||
private String scannedShipmentId;
|
||||
private String itemNo;
|
||||
private String boxSpecification;
|
||||
private Integer packageCount;
|
||||
|
||||
@@ -19,6 +19,7 @@ public class MojiaMeasurementReceiptEntity {
|
||||
private Long tenantId;
|
||||
private Long subjectId;
|
||||
private String boxCode;
|
||||
private String scannedShipmentId;
|
||||
private BigDecimal lengthCm;
|
||||
private BigDecimal widthCm;
|
||||
private BigDecimal heightCm;
|
||||
|
||||
@@ -177,6 +177,8 @@ public class CartonMeasurementService {
|
||||
item.setInboundDetailId(binding.inboundDetailId());
|
||||
item.setExternalDetailNo(boxCode);
|
||||
item.setBoxCode(boxCode);
|
||||
item.setScannedShipmentId(StringUtils.hasText(command.scannedShipmentId())
|
||||
? command.scannedShipmentId().trim() : null);
|
||||
item.setItemNo("B" + binding.cartonIndex());
|
||||
item.setBoxSpecification(StringUtils.hasText(binding.boxSpecification())
|
||||
? binding.boxSpecification().trim() : boxCode);
|
||||
@@ -380,6 +382,10 @@ public class CartonMeasurementService {
|
||||
if (!StringUtils.hasText(command.machineNo()) || command.machineNo().trim().length() > 64) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "设备编号不能为空且不能超过64个字符");
|
||||
}
|
||||
if (StringUtils.hasText(command.scannedShipmentId())
|
||||
&& command.scannedShipmentId().trim().length() > 64) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "实扫Shipment ID不能超过64个字符");
|
||||
}
|
||||
if (command.measuredAt() == null) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "设备测量时间不能为空");
|
||||
}
|
||||
@@ -404,7 +410,8 @@ public class CartonMeasurementService {
|
||||
private CartonMeasurementResult result(InboundMeasurementEntity measurement,
|
||||
InboundMeasurementItemEntity item, boolean created) {
|
||||
return new CartonMeasurementResult(measurement.getId(), measurement.getVersion(), item.getId(),
|
||||
item.getBoxCode(), item.getPackageCount(), item.getLengthCm(), item.getWidthCm(),
|
||||
item.getBoxCode(), item.getScannedShipmentId(), item.getPackageCount(),
|
||||
item.getLengthCm(), item.getWidthCm(),
|
||||
item.getHeightCm(), item.getUnitWeightKg(), item.getSourceImageFileId(), item.getMachineNo(),
|
||||
item.getDeviceMeasuredAt(), created);
|
||||
}
|
||||
|
||||
@@ -367,7 +367,8 @@ public class InboundMeasurementService {
|
||||
item.getUnitWeightKg(), item.getUnitVolumeCbm(), item.getTotalWeightKg(),
|
||||
item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(),
|
||||
item.getOverLimitWarning(), item.getOuterLengthCm(), item.getOuterWidthCm(),
|
||||
item.getOuterHeightCm(), item.getRemark(), item.getBoxCode(), item.getPackageCount(),
|
||||
item.getOuterHeightCm(), item.getRemark(), item.getBoxCode(), item.getScannedShipmentId(),
|
||||
item.getPackageCount(),
|
||||
item.getMachineNo(), item.getDeviceMeasuredAt(), item.getUpdatedAt());
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -79,7 +79,7 @@ public class MojiaMeasurementOpenApiService {
|
||||
app.tenantId(), app.subjectId(), new CartonMeasurementCommand(
|
||||
request.boxCode(), request.length(), request.width(), request.height(), request.weight(),
|
||||
request.machineNo(), request.measuredAt() == null ? LocalDateTime.now() : request.measuredAt(),
|
||||
image));
|
||||
image, request.shipmentId().trim()));
|
||||
} catch (BizException exception) {
|
||||
if (exception.getResultCode() == ResultCode.NOT_FOUND) {
|
||||
return saveUnmatched(app, request, image, exception);
|
||||
@@ -117,6 +117,7 @@ public class MojiaMeasurementOpenApiService {
|
||||
receipt.setTenantId(app.tenantId());
|
||||
receipt.setSubjectId(app.subjectId());
|
||||
receipt.setBoxCode(request.boxCode().trim());
|
||||
receipt.setScannedShipmentId(request.shipmentId().trim());
|
||||
receipt.setLengthCm(request.length());
|
||||
receipt.setWidthCm(request.width());
|
||||
receipt.setHeightCm(request.height());
|
||||
|
||||
@@ -203,6 +203,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item (
|
||||
inbound_detail_id BIGINT UNSIGNED NULL COMMENT '绑定的订单货物明细ID,平台手工明细可为空',
|
||||
external_detail_no VARCHAR(128) NULL COMMENT '第三方测量明细唯一号,平台手工明细可为空',
|
||||
box_code VARCHAR(128) NULL COMMENT '柏悦唛头箱号;设备单箱测量时同一客户内唯一',
|
||||
scanned_shipment_id VARCHAR(64) NULL COMMENT '设备从Amazon FBA箱标实扫的Shipment ID',
|
||||
item_no VARCHAR(32) NOT NULL COMMENT '批次内明细号',
|
||||
box_specification VARCHAR(64) NOT NULL COMMENT '箱规标识',
|
||||
package_count INT UNSIGNED NOT NULL COMMENT '相同箱规箱数',
|
||||
@@ -240,6 +241,7 @@ CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt (
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
subject_id BIGINT UNSIGNED NOT NULL COMMENT '设备应用操作主体ID',
|
||||
box_code VARCHAR(128) NOT NULL COMMENT '设备扫描的原始箱码',
|
||||
scanned_shipment_id VARCHAR(64) NULL COMMENT '设备从Amazon FBA箱标实扫的Shipment ID',
|
||||
length_cm DECIMAL(12,3) NOT NULL COMMENT '设备实测长度cm',
|
||||
width_cm DECIMAL(12,3) NOT NULL COMMENT '设备实测宽度cm',
|
||||
height_cm DECIMAL(12,3) NOT NULL COMMENT '设备实测高度cm',
|
||||
|
||||
@@ -239,9 +239,10 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [boxCode, length, width, height, machineNo, picture]
|
||||
required: [boxCode, shipment_id, length, width, height, machineNo, picture]
|
||||
properties:
|
||||
boxCode: {type: string, maxLength: 128, example: 'RK-2085911321565003778#4-1'}
|
||||
shipment_id: {type: string, maxLength: 64, example: 'FBA19L5KMY2CU000002', description: '设备从Amazon FBA箱标实际扫描得到;系统只保存,不生成且不覆盖客户下单值'}
|
||||
length: {type: number, format: decimal, minimum: 0.001}
|
||||
width: {type: number, format: decimal, minimum: 0.001}
|
||||
height: {type: number, format: decimal, minimum: 0.001}
|
||||
|
||||
+2
@@ -57,6 +57,8 @@ class OpenApiInboundMeasurementControllerContractTest {
|
||||
.contains("Nonce")
|
||||
.contains("inboundDetailId")
|
||||
.contains("multipart/form-data")
|
||||
.contains("required: [boxCode, shipment_id, length, width, height, machineNo, picture]")
|
||||
.contains("shipment_id:")
|
||||
.contains("不改变入库单状态");
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -48,12 +48,14 @@ class PlatformCartonScanControllerTest {
|
||||
.setCustomArgumentResolvers(authenticationPrincipal(user))
|
||||
.build();
|
||||
when(cartonMeasurements.upsertForInbound(eq(10001L), eq(90001L), eq(70001L), any()))
|
||||
.thenReturn(new CartonMeasurementResult(30001L, 1, 40001L, "RK-001#5-1", 1,
|
||||
.thenReturn(new CartonMeasurementResult(30001L, 1, 40001L, "RK-001#5-1",
|
||||
"FBA-SCAN-001", 1,
|
||||
new BigDecimal("60.1"), new BigDecimal("40.2"), new BigDecimal("30.3"),
|
||||
new BigDecimal("12.5"), 99001L, "SIM-01",
|
||||
LocalDateTime.parse("2026-08-07T10:30:00"), true));
|
||||
MockPart data = new MockPart("data", "data", ("""
|
||||
{"boxCode":"RK-001#5-1","length":60.1,"width":40.2,"height":30.3,
|
||||
{"boxCode":"RK-001#5-1","scannedShipmentId":"FBA-SCAN-001",
|
||||
"length":60.1,"width":40.2,"height":30.3,
|
||||
"weight":12.5,"machineNo":"SIM-01","measuredAt":"2026-08-07T10:30:00"}
|
||||
""").getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
data.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
@@ -70,6 +72,7 @@ class PlatformCartonScanControllerTest {
|
||||
verify(cartonMeasurements).upsertForInbound(
|
||||
eq(10001L), eq(90001L), eq(70001L), command.capture());
|
||||
assertThat(command.getValue().machineNo()).isEqualTo("SIM-01");
|
||||
assertThat(command.getValue().scannedShipmentId()).isEqualTo("FBA-SCAN-001");
|
||||
assertThat(command.getValue().image().getOriginalFilename()).isEqualTo("carton.jpg");
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -22,6 +22,7 @@ class MojiaMeasurementReceiptSqlContractTest {
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt")
|
||||
.contains("box_code VARCHAR(128) NOT NULL")
|
||||
.contains("scanned_shipment_id VARCHAR(64) NULL")
|
||||
.contains("image_file_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("status VARCHAR(24) NOT NULL DEFAULT 'UNMATCHED'")
|
||||
.contains("idx_mojia_receipt_status_time")
|
||||
@@ -29,6 +30,7 @@ class MojiaMeasurementReceiptSqlContractTest {
|
||||
}
|
||||
assertThat(unified)
|
||||
.contains("墨家未匹配测量收件箱字段未完整创建")
|
||||
.contains("箱级测量实扫Shipment ID字段未创建")
|
||||
.contains("测量计算规则版本字段长度不是64");
|
||||
}
|
||||
}
|
||||
|
||||
+9
-3
@@ -86,6 +86,8 @@ class CartonMeasurementServiceTest {
|
||||
assertThat(result.created()).isTrue();
|
||||
assertThat(result.confirmedPackageCount()).isEqualTo(1);
|
||||
assertThat(result.boxCode()).isEqualTo("RK-001#5-1");
|
||||
assertThat(result.scannedShipmentId()).isEqualTo("FBA-SCAN-002");
|
||||
assertThat(insertedItem.get().getScannedShipmentId()).isEqualTo("FBA-SCAN-002");
|
||||
assertThat(insertedItem.get().getMeasurementId()).isEqualTo(30001L);
|
||||
assertThat(insertedItem.get().getTotalVolumeCbm()).isEqualByComparingTo("0.1870");
|
||||
assertThat(insertedItem.get().getOuterLengthCm()).isEqualByComparingTo("64.000");
|
||||
@@ -112,6 +114,7 @@ class CartonMeasurementServiceTest {
|
||||
assertThat(existing.getTotalVolumeCbm()).isEqualByComparingTo("0.7480");
|
||||
assertThat(existing.getVolumeWeightKg()).isEqualByComparingTo("124.916");
|
||||
assertThat(existing.getSourceImageFileId()).isEqualTo(99001L);
|
||||
assertThat(existing.getScannedShipmentId()).isEqualTo("FBA-SCAN-002");
|
||||
verify(images).delete("internal-token", 90001L, 10001L, 70001L, 88001L);
|
||||
}
|
||||
|
||||
@@ -126,7 +129,8 @@ class CartonMeasurementServiceTest {
|
||||
"RK-001#5-1", new BigDecimal("60.100"), new BigDecimal("50.100"),
|
||||
new BigDecimal("50.100"), new BigDecimal("12.500"), "MJ-01",
|
||||
LocalDateTime.parse("2026-08-07T10:30:00"),
|
||||
new MockMultipartFile("image", "measure.jpg", "image/jpeg", "image".getBytes()));
|
||||
new MockMultipartFile("image", "measure.jpg", "image/jpeg", "image".getBytes()),
|
||||
"FBA-SCAN-001");
|
||||
|
||||
CartonMeasurementResult result = service.upsert(10001L, 90001L, oversize);
|
||||
|
||||
@@ -253,7 +257,8 @@ class CartonMeasurementServiceTest {
|
||||
return new CartonMeasurementCommand("RK-001#5-1", new BigDecimal("59.100"),
|
||||
new BigDecimal("40.200"), new BigDecimal("30.300"), new BigDecimal("12.500"),
|
||||
"MJ-01", LocalDateTime.parse("2026-08-07T10:30:00"),
|
||||
new MockMultipartFile("image", "measure.jpg", "image/jpeg", "image".getBytes()));
|
||||
new MockMultipartFile("image", "measure.jpg", "image/jpeg", "image".getBytes()),
|
||||
"FBA-SCAN-001");
|
||||
}
|
||||
|
||||
private CartonMeasurementCommand acceptanceCommand() {
|
||||
@@ -261,7 +266,8 @@ class CartonMeasurementServiceTest {
|
||||
"RK-001#5-1", new BigDecimal("59.500"), new BigDecimal("41.200"),
|
||||
new BigDecimal("31.300"), new BigDecimal("13.600"), "MJ-ADMIN-E2E-02",
|
||||
LocalDateTime.parse("2026-08-08T10:01:00"),
|
||||
new MockMultipartFile("image", "measure.jpg", "image/jpeg", "image".getBytes()));
|
||||
new MockMultipartFile("image", "measure.jpg", "image/jpeg", "image".getBytes()),
|
||||
"FBA-SCAN-002");
|
||||
}
|
||||
|
||||
private InboundOrderEntity inbound(String status) {
|
||||
|
||||
+25
-2
@@ -6,6 +6,7 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementResult;
|
||||
import com.baiyue.firstleg.dto.MojiaMeasurementDtos.UploadRequest;
|
||||
import com.baiyue.firstleg.dto.MojiaMeasurementDtos.UploadResponse;
|
||||
import com.baiyue.firstleg.entity.MojiaMeasurementReceiptEntity;
|
||||
import com.baiyue.firstleg.repository.MojiaMeasurementReceiptRepository;
|
||||
@@ -21,6 +22,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -30,6 +32,23 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MojiaMeasurementOpenApiServiceTest {
|
||||
@Test
|
||||
void shipmentIdIsRequiredAndCamelCaseAliasIsAccepted() throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
|
||||
var validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||
|
||||
UploadRequest camelCase = mapper.readValue(
|
||||
json().replace("\"shipment_id\"", "\"shipmentId\""), UploadRequest.class);
|
||||
UploadRequest missing = mapper.readValue(
|
||||
json().replace("\"shipment_id\":\"FBA19L5KMY2CU000002\",", ""), UploadRequest.class);
|
||||
|
||||
assertThat(camelCase.shipmentId()).isEqualTo("FBA19L5KMY2CU000002");
|
||||
assertThat(validator.validate(camelCase)).isEmpty();
|
||||
assertThat(validator.validate(missing).stream()
|
||||
.map(violation -> violation.getPropertyPath().toString()).collect(java.util.stream.Collectors.toSet()))
|
||||
.isEqualTo(Set.of("shipmentId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fixedAppKeyJsonIsConvertedToTheExistingCartonMeasurementFlow() throws Exception {
|
||||
MeasurementRequestAuthenticator authenticator = mock(MeasurementRequestAuthenticator.class);
|
||||
@@ -43,7 +62,8 @@ class MojiaMeasurementOpenApiServiceTest {
|
||||
when(authenticator.authenticate("POST", MojiaMeasurementOpenApiService.CALLBACK_PATH,
|
||||
headers, json(), false)).thenReturn(new AuthenticatedMeasurementApp("mojia", 10001L, 90001L, Map.of()));
|
||||
when(cartons.upsert(eq(10001L), eq(90001L), any())).thenReturn(new CartonMeasurementResult(
|
||||
1L, 1, 2L, "RK-001#4-1", 1, new BigDecimal("60.1"), new BigDecimal("40.2"),
|
||||
1L, 1, 2L, "RK-001#4-1", "FBA19L5KMY2CU000002", 1,
|
||||
new BigDecimal("60.1"), new BigDecimal("40.2"),
|
||||
new BigDecimal("30.3"), new BigDecimal("12.5"), 3L, "MJ-01", LocalDateTime.now(), true));
|
||||
|
||||
UploadResponse result = service.receive(headers, json());
|
||||
@@ -53,6 +73,7 @@ class MojiaMeasurementOpenApiServiceTest {
|
||||
ArgumentCaptor<CartonMeasurementCommand> command = ArgumentCaptor.forClass(CartonMeasurementCommand.class);
|
||||
verify(cartons).upsert(eq(10001L), eq(90001L), command.capture());
|
||||
assertThat(command.getValue().boxCode()).isEqualTo("RK-001#4-1");
|
||||
assertThat(command.getValue().scannedShipmentId()).isEqualTo("FBA19L5KMY2CU000002");
|
||||
assertThat(command.getValue().lengthCm()).isEqualByComparingTo("60.1");
|
||||
assertThat(command.getValue().image().getContentType()).isEqualTo("image/jpeg");
|
||||
assertThat(command.getValue().image().getBytes()).containsExactly(0xff, 0xd8, 0xff, 0xd9);
|
||||
@@ -87,6 +108,7 @@ class MojiaMeasurementOpenApiServiceTest {
|
||||
ArgumentCaptor.forClass(MojiaMeasurementReceiptEntity.class);
|
||||
verify(receipts).insert(receipt.capture());
|
||||
assertThat(receipt.getValue().getBoxCode()).isEqualTo("RK-001#4-1");
|
||||
assertThat(receipt.getValue().getScannedShipmentId()).isEqualTo("FBA19L5KMY2CU000002");
|
||||
assertThat(receipt.getValue().getLengthCm()).isEqualByComparingTo("60.1");
|
||||
assertThat(receipt.getValue().getImageFileId()).isEqualTo(3L);
|
||||
assertThat(receipt.getValue().getStatus()).isEqualTo("UNMATCHED");
|
||||
@@ -94,7 +116,8 @@ class MojiaMeasurementOpenApiServiceTest {
|
||||
|
||||
private String json() {
|
||||
return """
|
||||
{"boxCode":"RK-001#4-1","length":60.1,"width":40.2,"height":30.3,
|
||||
{"boxCode":"RK-001#4-1","shipment_id":"FBA19L5KMY2CU000002",
|
||||
"length":60.1,"width":40.2,"height":30.3,
|
||||
"weight":12.5,"machineNo":"MJ-01","picture":"data:image/jpeg;base64,/9j/2Q=="}
|
||||
""";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user