feat(firstleg): expose authoritative measurement identifiers
This commit is contained in:
@@ -63,12 +63,13 @@ public interface OrderCargoClient {
|
||||
}
|
||||
|
||||
record CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
String skuNo, Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
String skuNo, Integer packageCount, BigDecimal lengthCm,
|
||||
BigDecimal widthCm, BigDecimal heightCm, String shipmentId) {
|
||||
public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
this(cargoId, orderId, itemNo, cargoName, null, packageCount, lengthCm, widthCm, heightCm);
|
||||
this(cargoId, orderId, itemNo, cargoName, null,
|
||||
packageCount, lengthCm, widthCm, heightCm, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -49,7 +49,8 @@ public class PlatformInboundMeasurementController {
|
||||
@Resource private MeasurementSubmissionService submission;
|
||||
@Resource private CartonMeasurementService cartonMeasurements;
|
||||
|
||||
@Operation(summary = "查询入库测量详情", description = "返回原单、实测箱规、审核快照和 OCR 重量结果。")
|
||||
@Operation(summary = "查询入库测量详情", description = "返回唯一入仓码、原单、实测箱规、审核快照和 OCR 重量结果;"
|
||||
+ "箱规行按 inboundDetailId 返回 Order 权威 FBA 货件编号,并保留独立设备实扫值。")
|
||||
@GetMapping("/measurement")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:measure:view')")
|
||||
public ApiResponse<InboundMeasurementResponses.Detail> detail(
|
||||
@@ -78,7 +79,8 @@ public class PlatformInboundMeasurementController {
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASUREMENT_DRAFT_SAVE", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "保存入库测量草稿")
|
||||
@Operation(summary = "保存测量草稿", description = "保存人工测量批次和服务端计算明细,不推进入库状态。")
|
||||
@Operation(summary = "保存测量草稿", description = "仅在待测量、测量中或审核驳回状态保存人工测量批次和服务端计算明细;"
|
||||
+ "进入待审核后批次冻结,确认箱数修正须使用 measurement/items/{itemId}/confirmed-package-count 专用接口。")
|
||||
@PutMapping("/measurement/draft")
|
||||
@PreAuthorize("hasAnyAuthority('platform:firstleg:inbound:warehouse-suggestion:edit',"
|
||||
+ " 'platform:firstleg:inbound:measure:edit')")
|
||||
|
||||
@@ -75,6 +75,8 @@ public final class InboundMeasurementResponses {
|
||||
|
||||
public record Item(
|
||||
Long id,
|
||||
@Schema(description = "当前测量详情唯一权威入仓码,与根对象 inboundCode 一致")
|
||||
String inboundCode,
|
||||
Long inboundDetailId,
|
||||
String itemNo,
|
||||
String boxSpecification,
|
||||
@@ -99,6 +101,9 @@ public final class InboundMeasurementResponses {
|
||||
String coefficientMode,
|
||||
String remark,
|
||||
String boxCode,
|
||||
@Schema(description = "Order货物明细按inboundDetailId匹配的权威FBA货件编号;匹配失败为空,禁止回退设备实扫值")
|
||||
String fbaShipmentNo,
|
||||
@Schema(description = "设备从Amazon FBA箱标实扫的Shipment ID;仅作现场证据,不覆盖或回退权威货件编号")
|
||||
String scannedShipmentId,
|
||||
Integer confirmedPackageCount,
|
||||
String machineNo,
|
||||
|
||||
@@ -48,6 +48,8 @@ public class InboundMeasurementService {
|
||||
private static final String WEIGHT_PENDING_CONFIRM = "PENDING_CONFIRM";
|
||||
/** OCR 结果无效或不完整,必须人工录入或修正。 */
|
||||
private static final String WEIGHT_NEEDS_MANUAL = "NEEDS_MANUAL";
|
||||
private static final Set<String> MEASUREMENT_DRAFT_EDITABLE_STATUSES = Set.of(
|
||||
"WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
|
||||
private static final Set<String> WEIGHT_EDITABLE_STATUSES = Set.of(
|
||||
"WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
|
||||
/** 测量完成后的默认普通托盘类型;实际拆托和库位占用由提交编排服务执行。 */
|
||||
@@ -100,7 +102,7 @@ public class InboundMeasurementService {
|
||||
com.baiyue.firstleg.entity.InboundPalletAllocationEntity::getWeightResultId,
|
||||
java.util.function.Function.identity(), (left, right) -> left));
|
||||
MeasureReviewRecordEntity review = repository.findLatestReview(tenantId, inbound.getId());
|
||||
InboundMeasurementResponses.DeclarationSnapshot latestDeclaration = loadLatestDeclaration(inbound);
|
||||
LatestDeclaration latestDeclaration = loadLatestDeclaration(inbound);
|
||||
return toDetail(inbound, measurement, items, weights, allocationSlots, allocationsByWeight,
|
||||
review, latestDeclaration);
|
||||
}
|
||||
@@ -122,6 +124,10 @@ public class InboundMeasurementService {
|
||||
long tenantId, long inboundId, long operatorId,
|
||||
InboundMeasurementRequests.SaveDraftRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
if (!MEASUREMENT_DRAFT_EDITABLE_STATUSES.contains(inbound.getProcessStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"测量已提交待审核,不能再保存整批测量数据;如需修改请先退回后重新提交");
|
||||
}
|
||||
validateInboundDetails(tenantId, inbound, request.items());
|
||||
request.items().stream().map(InboundMeasurementRequests.ItemRequest::chargeRuleId)
|
||||
.filter(java.util.Objects::nonNull).distinct().forEach(ruleId -> {
|
||||
@@ -468,7 +474,8 @@ public class InboundMeasurementService {
|
||||
List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> allocationSlots,
|
||||
Map<Long, com.baiyue.firstleg.entity.InboundPalletAllocationEntity> allocationsByWeight,
|
||||
MeasureReviewRecordEntity review,
|
||||
InboundMeasurementResponses.DeclarationSnapshot latestDeclaration) {
|
||||
LatestDeclaration latestDeclaration) {
|
||||
InboundMeasurementResponses.DeclarationSnapshot declaration = latestDeclaration.snapshot();
|
||||
return new InboundMeasurementResponses.Detail(
|
||||
inbound.getId(), inbound.getInboundCode(), inbound.getCompanyName(), inbound.getChannelCode(),
|
||||
inbound.getChannelName(), inbound.getProcessStatus(), inbound.getMeasureReviewStatus(),
|
||||
@@ -480,21 +487,22 @@ public class InboundMeasurementService {
|
||||
inbound.getVersion(),
|
||||
measurement == null ? null : measurement.getVersion(),
|
||||
measurement != null && StringUtils.hasText(measurement.getOriginalDocumentNo())
|
||||
? measurement.getOriginalDocumentNo() : latestDeclaration.referenceId(),
|
||||
? measurement.getOriginalDocumentNo() : declaration.referenceId(),
|
||||
measurement != null && measurement.getOriginalGrossWeightKg() != null
|
||||
? measurement.getOriginalGrossWeightKg() : latestDeclaration.grossWeightKg(),
|
||||
? measurement.getOriginalGrossWeightKg() : declaration.grossWeightKg(),
|
||||
measurement != null && measurement.getOriginalVolumeCbm() != null
|
||||
? measurement.getOriginalVolumeCbm() : latestDeclaration.volumeCbm(),
|
||||
latestDeclaration.totalPackageCount(),
|
||||
? measurement.getOriginalVolumeCbm() : declaration.volumeCbm(),
|
||||
declaration.totalPackageCount(),
|
||||
measurement == null ? inbound.getTotalPackageCount() : measurement.getTotalPackageCount(),
|
||||
inbound.getActualGrossWeightKg(),
|
||||
inbound.getActualNetWeightKg(),
|
||||
measurement == null ? inbound.getActualVolumeCbm() : measurement.getTotalVolumeCbm(),
|
||||
measurement == null ? inbound.getVolumeWeightKg() : measurement.getVolumeWeightKg(),
|
||||
measurement != null && StringUtils.hasText(measurement.getActualGoodsName())
|
||||
? measurement.getActualGoodsName() : latestDeclaration.goodsName(),
|
||||
latestDeclaration,
|
||||
items.stream().map(this::toItem).toList(), weights.stream()
|
||||
? measurement.getActualGoodsName() : declaration.goodsName(),
|
||||
declaration,
|
||||
items.stream().map(item -> toItem(item, inbound.getInboundCode(), item.getInboundDetailId() == null
|
||||
? null : latestDeclaration.fbaShipmentNos().get(item.getInboundDetailId()))).toList(), weights.stream()
|
||||
.map(weight -> toWeight(weight, allocationsByWeight.get(weight.getId()))).toList(),
|
||||
toPalletLocationGuide(allocationSlots));
|
||||
}
|
||||
@@ -526,10 +534,14 @@ public class InboundMeasurementService {
|
||||
* Latest customer declaration is a live, read-only Order view. It must never be persisted over
|
||||
* the measurement batch, carton-label mapping, weight results or review decision.
|
||||
*/
|
||||
private InboundMeasurementResponses.DeclarationSnapshot loadLatestDeclaration(
|
||||
private LatestDeclaration loadLatestDeclaration(
|
||||
InboundOrderEntity inbound) {
|
||||
OrderCargoClient.MeasurementDeclarationSnapshot aggregate = loadDeclaration(inbound);
|
||||
List<OrderCargoClient.CargoLabelBinding> cargoes = loadDeclarationCargoes(inbound);
|
||||
if (cargoes.stream().anyMatch(cargo -> cargo == null || cargo.cargoId() == null
|
||||
|| cargo.cargoId() <= 0 || !java.util.Objects.equals(cargo.orderId(), inbound.getOrderId()))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "订单服务返回的货物明细归属不一致");
|
||||
}
|
||||
long packages = cargoes.stream()
|
||||
.map(OrderCargoClient.CargoLabelBinding::packageCount)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
@@ -538,7 +550,7 @@ public class InboundMeasurementService {
|
||||
if (packages > Integer.MAX_VALUE) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "客户最新申报总箱数超过系统上限");
|
||||
}
|
||||
return new InboundMeasurementResponses.DeclarationSnapshot(
|
||||
InboundMeasurementResponses.DeclarationSnapshot snapshot = new InboundMeasurementResponses.DeclarationSnapshot(
|
||||
aggregate.referenceId(), (int) packages, aggregate.originalGrossWeightKg(),
|
||||
aggregate.originalVolumeCbm(), aggregate.defaultActualGoodsName(),
|
||||
cargoes.stream()
|
||||
@@ -550,6 +562,19 @@ public class InboundMeasurementService {
|
||||
cargo.cargoId(), cargo.itemNo(), cargo.cargoName(), cargo.packageCount(),
|
||||
cargo.lengthCm(), cargo.widthCm(), cargo.heightCm()))
|
||||
.toList());
|
||||
Map<Long, String> fbaShipmentNos = cargoes.stream()
|
||||
.filter(cargo -> cargo.cargoId() != null && StringUtils.hasText(cargo.shipmentId()))
|
||||
.collect(java.util.stream.Collectors.toMap(
|
||||
OrderCargoClient.CargoLabelBinding::cargoId,
|
||||
cargo -> cargo.shipmentId().trim(),
|
||||
(left, right) -> {
|
||||
if (!java.util.Objects.equals(left, right)) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"订单同一货物明细返回多个FBA货件编号");
|
||||
}
|
||||
return left;
|
||||
}, java.util.LinkedHashMap::new));
|
||||
return new LatestDeclaration(snapshot, Map.copyOf(fbaShipmentNos));
|
||||
}
|
||||
|
||||
private List<OrderCargoClient.CargoLabelBinding> loadDeclarationCargoes(InboundOrderEntity inbound) {
|
||||
@@ -601,9 +626,11 @@ public class InboundMeasurementService {
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundMeasurementResponses.Item toItem(InboundMeasurementItemEntity item) {
|
||||
private InboundMeasurementResponses.Item toItem(InboundMeasurementItemEntity item,
|
||||
String inboundCode,
|
||||
String fbaShipmentNo) {
|
||||
return new InboundMeasurementResponses.Item(
|
||||
item.getId(), item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(),
|
||||
item.getId(), inboundCode, item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(),
|
||||
item.getLengthCm(), item.getWidthCm(), item.getHeightCm(), item.getGirthCm(),
|
||||
item.getUnitVolumeCbm(),
|
||||
item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(),
|
||||
@@ -612,11 +639,17 @@ public class InboundMeasurementService {
|
||||
item.getOuterLengthCm(), item.getOuterWidthCm(), item.getOuterHeightCm(),
|
||||
item.getCalculationRuleVersion(),
|
||||
item.getChargeRuleId(), item.getCoefficientMode(),
|
||||
item.getRemark(), item.getBoxCode(), item.getScannedShipmentId(),
|
||||
item.getRemark(), item.getBoxCode(), fbaShipmentNo, item.getScannedShipmentId(),
|
||||
item.getPackageCount(),
|
||||
item.getMachineNo(), item.getDeviceMeasuredAt(), item.getUpdatedAt());
|
||||
}
|
||||
|
||||
private record LatestDeclaration(
|
||||
InboundMeasurementResponses.DeclarationSnapshot snapshot,
|
||||
Map<Long, String> fbaShipmentNos
|
||||
) {
|
||||
}
|
||||
|
||||
private void requireConfirmedPackageTotal(InboundOrderEntity inbound,
|
||||
List<InboundMeasurementItemEntity> items) {
|
||||
if (inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() < 1) {
|
||||
|
||||
@@ -42,6 +42,8 @@ public class MeasurementBatchService {
|
||||
static final String STATUS_DRAFT = "DRAFT";
|
||||
/** 已接收第三方测量数据,待平台提交审核。 */
|
||||
static final String STATUS_RECEIVED = "RECEIVED";
|
||||
private static final Set<String> EDITABLE_INBOUND_STATUSES = Set.of(
|
||||
"WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
|
||||
static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
|
||||
private static final BigDecimal MAX_RAW_VOLUME_CBM = new BigDecimal("999999999999.999999");
|
||||
private static final BigDecimal MAX_TOTAL_VOLUME_CBM = new BigDecimal("99999999999999.9999");
|
||||
@@ -60,6 +62,10 @@ public class MeasurementBatchService {
|
||||
if (inbound == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
}
|
||||
if (!EDITABLE_INBOUND_STATUSES.contains(inbound.getProcessStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"当前入库状态不允许保存测量批次,已提交批次必须保持冻结");
|
||||
}
|
||||
validateSource(command);
|
||||
List<InboundMeasurementItemEntity> calculatedItems = calculateItems(
|
||||
command, dimensionWeighting.load(command.tenantId()));
|
||||
@@ -71,12 +77,7 @@ public class MeasurementBatchService {
|
||||
repository.insertMeasurement(measurement);
|
||||
} else {
|
||||
measurement = requireEditableBatch(command);
|
||||
// 修正待测量或待审核资料时重新形成可提交的内部草稿;已入库订单只更新资料,
|
||||
// 不回退其主状态,也不把已经审核的批次重新标成待审核。
|
||||
if (Set.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED")
|
||||
.contains(inbound.getProcessStatus())) {
|
||||
measurement.setStatus(STATUS_DRAFT);
|
||||
}
|
||||
measurement.setStatus(STATUS_DRAFT);
|
||||
applyHeader(measurement, command);
|
||||
applyTotals(measurement, calculatedItems);
|
||||
if (!repository.updateMeasurement(measurement)) {
|
||||
|
||||
@@ -916,6 +916,8 @@ paths:
|
||||
grossWeightKg/netWeightKg。两类证据不得合并为无明细归属的统一图片列表。
|
||||
actualGrossWeightKg 是服务端确认后的实测毛重,actualNetWeightKg 是扣除托盘皮重后的货物净重;
|
||||
两者语义独立,消费者不得互相回退,也不得根据图片或 OCR 值自行重算。
|
||||
inboundCode 是当前入库详情唯一入仓码;每条 items 按 inboundDetailId 返回 Order 权威
|
||||
fbaShipmentNo,并保留独立 scannedShipmentId,匹配失败返回空且禁止两者互相回退或覆盖。
|
||||
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]*$'}}
|
||||
@@ -931,7 +933,9 @@ paths:
|
||||
summary: 保存测量草稿
|
||||
description: >-
|
||||
原单据号码、申报毛重和申报体积由服务端从客户订单生成快照,客户端不得提交覆盖;
|
||||
actualGoodsName 默认使用客户货物品名汇总,但允许测量人员修正。
|
||||
actualGoodsName 默认使用客户货物品名汇总,但允许测量人员修正。仅允许 WAIT_MEASURE、MEASURING
|
||||
或 MEASURE_REJECTED 状态保存;进入 MEASURE_PENDING 后整批测量冻结,确认箱数修正使用
|
||||
measurement/items/{itemId}/confirmed-package-count 明细专用接口。
|
||||
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]*$'}}
|
||||
@@ -1749,10 +1753,12 @@ components:
|
||||
type: object
|
||||
description: >-
|
||||
单条尺寸测量明细。id 是稳定 measurementItemId;intelligentImageFileId 仅表示该明细的
|
||||
尺寸测量原图,不得与 weightResults.fileId 混用。
|
||||
尺寸测量原图,不得与 weightResults.fileId 混用。fbaShipmentNo 是按 inboundDetailId
|
||||
从 Order 货物明细实时读取的权威货件编号;scannedShipmentId 是独立设备实扫证据,二者不得回退或覆盖。
|
||||
required: [id]
|
||||
properties:
|
||||
id: {type: string, pattern: '^[1-9][0-9]*$', description: 稳定 measurementItemId}
|
||||
inboundCode: {type: string, nullable: true, description: 当前测量详情唯一权威入仓码,与根对象inboundCode一致}
|
||||
inboundDetailId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
itemNo: {type: string, nullable: true}
|
||||
boxSpecification: {type: string, nullable: true}
|
||||
@@ -1781,7 +1787,8 @@ components:
|
||||
coefficientMode: {type: string, enum: [ACTUAL, ADD, MULTIPLY], nullable: true}
|
||||
remark: {type: string, nullable: true}
|
||||
boxCode: {type: string, nullable: true}
|
||||
scannedShipmentId: {type: string, nullable: true}
|
||||
fbaShipmentNo: {type: string, nullable: true, description: Order货物明细按inboundDetailId匹配的权威FBA货件编号;匹配失败为空}
|
||||
scannedShipmentId: {type: string, nullable: true, description: 设备实扫Shipment ID;仅作现场证据,不覆盖或回退fbaShipmentNo}
|
||||
confirmedPackageCount: {type: integer, nullable: true}
|
||||
machineNo: {type: string, nullable: true}
|
||||
deviceMeasuredAt: {type: string, format: date-time, nullable: true}
|
||||
@@ -1793,7 +1800,7 @@ components:
|
||||
required: [inboundId, inboundVersion, items, weightResults]
|
||||
properties:
|
||||
inboundId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
inboundCode: {type: string, nullable: true}
|
||||
inboundCode: {type: string, nullable: true, description: 当前测量详情唯一入仓码,UI业务主展示字段}
|
||||
companyName: {type: string, nullable: true}
|
||||
channelCode: {type: string, nullable: true}
|
||||
channelName: {type: string, nullable: true}
|
||||
|
||||
+16
-4
@@ -32,8 +32,10 @@ class InboundMeasurementEvidenceOpenApiContractTest {
|
||||
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
|
||||
Map<String, Object> detailProperties = properties(schemas, "InboundMeasurementDetail");
|
||||
assertThat(detailProperties).containsKeys(
|
||||
"actualGrossWeightKg", "actualNetWeightKg", "latestDeclaration", "items", "weightResults",
|
||||
"inboundCode", "actualGrossWeightKg", "actualNetWeightKg", "latestDeclaration", "items", "weightResults",
|
||||
"palletLocationGuide");
|
||||
assertThat(stringMap(detailProperties.get("inboundCode")).get("description").toString())
|
||||
.contains("唯一入仓码", "UI业务主展示字段");
|
||||
assertThat(stringMap(detailProperties.get("latestDeclaration")))
|
||||
.containsEntry("$ref", "#/components/schemas/InboundMeasurementLatestDeclaration");
|
||||
assertThat(properties(schemas, "InboundMeasurementLatestDeclaration"))
|
||||
@@ -49,8 +51,11 @@ class InboundMeasurementEvidenceOpenApiContractTest {
|
||||
Map<String, Object> draftProperties = properties(schemas, "InboundMeasurementSaveDraftRequest");
|
||||
assertThat(draftProperties).containsKeys("measurementId", "version", "actualGoodsName", "items")
|
||||
.doesNotContainKeys("originalDocumentNo", "originalGrossWeightKg", "originalVolumeCbm");
|
||||
assertThat(paths).containsKey(
|
||||
"/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/draft");
|
||||
String draftPath = "/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/draft";
|
||||
assertThat(paths).containsKey(draftPath);
|
||||
Map<String, Object> draftOperation = stringMap(stringMap(paths.get(draftPath)).get("put"));
|
||||
assertThat(draftOperation.get("description").toString())
|
||||
.contains("MEASURE_PENDING", "冻结", "package-count");
|
||||
assertThat(stringMap(stringMap(draftProperties.get("items")).get("items")))
|
||||
.containsEntry("$ref", "#/components/schemas/InboundMeasurementItemRequest");
|
||||
assertThat(properties(schemas, "InboundMeasurementItemRequest")).containsKeys(
|
||||
@@ -58,7 +63,14 @@ class InboundMeasurementEvidenceOpenApiContractTest {
|
||||
"widthCm", "heightCm", "intelligentImageFileId", "remark");
|
||||
|
||||
Map<String, Object> itemProperties = properties(schemas, "InboundMeasurementItem");
|
||||
assertThat(itemProperties).containsKeys("id", "intelligentImageFileId");
|
||||
assertThat(itemProperties).containsKeys(
|
||||
"id", "inboundCode", "inboundDetailId", "intelligentImageFileId", "fbaShipmentNo", "scannedShipmentId");
|
||||
assertThat(stringMap(itemProperties.get("inboundCode")).get("description").toString())
|
||||
.contains("唯一权威入仓码", "根对象inboundCode一致");
|
||||
assertThat(stringMap(itemProperties.get("fbaShipmentNo")).get("description").toString())
|
||||
.contains("Order货物明细", "inboundDetailId", "匹配失败为空");
|
||||
assertThat(stringMap(itemProperties.get("scannedShipmentId")).get("description").toString())
|
||||
.contains("设备实扫", "不覆盖或回退fbaShipmentNo");
|
||||
Map<String, Object> weightProperties = properties(schemas, "InboundWeightResult");
|
||||
assertThat(weightProperties).containsKeys(
|
||||
"id", "fileId", "recognitionStatus", "grossWeightKg", "netWeightKg", "confirmedAt",
|
||||
|
||||
+84
-4
@@ -75,8 +75,8 @@ class InboundMeasurementServiceTest {
|
||||
"REF-50", new BigDecimal("2000"), new BigDecimal("20"), "123123")));
|
||||
when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn(
|
||||
ApiResponse.success(List.of(new OrderCargoClient.CargoLabelBinding(
|
||||
80001L, 80L, 1, "123123", 3, new BigDecimal("40"),
|
||||
new BigDecimal("30"), new BigDecimal("20")))));
|
||||
80001L, 80L, 1, "123123", null, 3, new BigDecimal("40"),
|
||||
new BigDecimal("30"), new BigDecimal("20"), "FBA-DECLARED-001"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -196,7 +196,9 @@ class InboundMeasurementServiceTest {
|
||||
item.setId(40L);
|
||||
item.setTenantId(1L);
|
||||
item.setMeasurementId(30L);
|
||||
item.setInboundDetailId(80001L);
|
||||
item.setSourceImageFileId(140L);
|
||||
item.setScannedShipmentId("FBA-SCANNED-001");
|
||||
item.setOuterLengthCm(new BigDecimal("64.000"));
|
||||
item.setOuterWidthCm(new BigDecimal("54.000"));
|
||||
item.setOuterHeightCm(new BigDecimal("54.000"));
|
||||
@@ -217,11 +219,15 @@ class InboundMeasurementServiceTest {
|
||||
|
||||
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
|
||||
|
||||
assertThat(result.inboundCode()).isEqualTo("RK-001");
|
||||
assertThat(result.actualGrossWeightKg()).isEqualByComparingTo("30.000");
|
||||
assertThat(result.actualNetWeightKg()).isEqualByComparingTo("11.500");
|
||||
assertThat(result.items()).singleElement().satisfies(dimension -> {
|
||||
assertThat(dimension.id()).isEqualTo(40L);
|
||||
assertThat(dimension.inboundCode()).isEqualTo("RK-001");
|
||||
assertThat(dimension.intelligentImageFileId()).isEqualTo(140L);
|
||||
assertThat(dimension.fbaShipmentNo()).isEqualTo("FBA-DECLARED-001");
|
||||
assertThat(dimension.scannedShipmentId()).isEqualTo("FBA-SCANNED-001");
|
||||
assertThat(dimension.outerLengthCm()).isNull();
|
||||
assertThat(dimension.outerWidthCm()).isNull();
|
||||
assertThat(dimension.outerHeightCm()).isNull();
|
||||
@@ -246,6 +252,63 @@ class InboundMeasurementServiceTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailMapsMultipleDeclaredShipmentIdsByInboundDetailWithoutFallingBackToScannedValue() {
|
||||
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(30L);
|
||||
InboundMeasurementItemEntity first = new InboundMeasurementItemEntity();
|
||||
first.setId(41L);
|
||||
first.setInboundDetailId(80001L);
|
||||
first.setScannedShipmentId("SCAN-DIFFERENT-001");
|
||||
InboundMeasurementItemEntity second = new InboundMeasurementItemEntity();
|
||||
second.setId(42L);
|
||||
second.setInboundDetailId(80002L);
|
||||
InboundMeasurementItemEntity unmatched = new InboundMeasurementItemEntity();
|
||||
unmatched.setId(43L);
|
||||
unmatched.setInboundDetailId(89999L);
|
||||
unmatched.setScannedShipmentId("SCAN-ONLY-999");
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findLatestMeasurement(1L, 10L)).thenReturn(measurement);
|
||||
when(repository.listItems(1L, 30L)).thenReturn(List.of(first, second, unmatched));
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
|
||||
when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn(
|
||||
ApiResponse.success(List.of(
|
||||
new OrderCargoClient.CargoLabelBinding(80001L, 80L, 1, "货物1", null,
|
||||
1, null, null, null, "FBA-DECLARED-001"),
|
||||
new OrderCargoClient.CargoLabelBinding(80002L, 80L, 2, "货物2", null,
|
||||
1, null, null, null, "FBA-DECLARED-002"))));
|
||||
|
||||
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
|
||||
|
||||
assertThat(result.inboundCode()).isEqualTo("RK-001");
|
||||
assertThat(result.items())
|
||||
.extracting(InboundMeasurementResponses.Item::inboundDetailId,
|
||||
InboundMeasurementResponses.Item::inboundCode,
|
||||
InboundMeasurementResponses.Item::fbaShipmentNo,
|
||||
InboundMeasurementResponses.Item::scannedShipmentId)
|
||||
.containsExactly(
|
||||
tuple(80001L, "RK-001", "FBA-DECLARED-001", "SCAN-DIFFERENT-001"),
|
||||
tuple(80002L, "RK-001", "FBA-DECLARED-002", null),
|
||||
tuple(89999L, "RK-001", null, "SCAN-ONLY-999"));
|
||||
verify(orderCargoClient, times(1)).listForCartonLabels("internal-token", 1L, 80L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailRejectsCargoBatchWhoseOrderOwnershipDoesNotMatchInbound() {
|
||||
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
|
||||
when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn(
|
||||
ApiResponse.success(List.of(new OrderCargoClient.CargoLabelBinding(
|
||||
80001L, 81L, 1, "其他订单货物", null, 1, null, null, null,
|
||||
"FBA-OTHER-001"))));
|
||||
|
||||
assertThatThrownBy(() -> service.detail(1L, 10L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("货物明细归属不一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsWarehouseOuterDimensionsOnlyAfterManualOverrideWasSaved() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
@@ -333,8 +396,8 @@ class InboundMeasurementServiceTest {
|
||||
"NEW-REF", new BigDecimal("40.000"), new BigDecimal("0.8000"), "最新申报品名")));
|
||||
when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn(
|
||||
ApiResponse.success(List.of(new OrderCargoClient.CargoLabelBinding(
|
||||
80001L, 80L, 1, "最新申报品名", 4, new BigDecimal("50"),
|
||||
new BigDecimal("40"), new BigDecimal("30")))));
|
||||
80001L, 80L, 1, "最新申报品名", null, 4, new BigDecimal("50"),
|
||||
new BigDecimal("40"), new BigDecimal("30"), "FBA-LATEST-001"))));
|
||||
|
||||
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
|
||||
|
||||
@@ -356,6 +419,7 @@ class InboundMeasurementServiceTest {
|
||||
assertThat(result.items()).singleElement().satisfies(measured -> {
|
||||
assertThat(measured.id()).isEqualTo(40L);
|
||||
assertThat(measured.boxCode()).isEqualTo("RK-001#2-1");
|
||||
assertThat(measured.fbaShipmentNo()).isEqualTo("FBA-LATEST-001");
|
||||
assertThat(measured.packageCount()).isEqualTo(2);
|
||||
assertThat(measured.lengthCm()).isEqualByComparingTo("45");
|
||||
});
|
||||
@@ -1116,6 +1180,21 @@ class InboundMeasurementServiceTest {
|
||||
verify(orderCargoClient).detail("internal-token", 1L, 80L, 80001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void savingDraftCannotReopenSubmittedMeasurementWhileReviewIsPending() {
|
||||
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
|
||||
inbound.setMeasureReviewStatus("PENDING");
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
|
||||
assertThatThrownBy(() -> service.saveDraft(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.SaveDraftRequest(30L, 10, null, List.of())))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("已提交待审核");
|
||||
|
||||
verifyNoInteractions(batchService);
|
||||
verify(orderCargoClient, never()).detail(any(), anyLong(), anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void savingDraftRejectsOverlongDefaultGoodsNameBeforeDatabaseWrite() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
@@ -1157,6 +1236,7 @@ class InboundMeasurementServiceTest {
|
||||
inbound.setTenantId(1L);
|
||||
inbound.setOrderId(80L);
|
||||
inbound.setWarehouseId(100L);
|
||||
inbound.setInboundCode("RK-001");
|
||||
inbound.setProcessStatus(status);
|
||||
inbound.setTotalPackageCount(3);
|
||||
inbound.setVersion(0);
|
||||
|
||||
+12
-9
@@ -392,28 +392,31 @@ class MeasurementBatchServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedManualMeasurementCanBeEditedAsDraftBeforeReviewCompletes() {
|
||||
void submittedMeasurementCannotBeResetToDraftWhileReviewIsPending() {
|
||||
InboundMeasurementEntity existing = new InboundMeasurementEntity();
|
||||
existing.setId(100L);
|
||||
existing.setTenantId(1L);
|
||||
existing.setInboundId(10L);
|
||||
existing.setSourceType("MANUAL");
|
||||
existing.setStatus("APPROVED");
|
||||
existing.setStatus("SUBMITTED");
|
||||
existing.setVersion(3);
|
||||
InboundOrderEntity inbound = inbound();
|
||||
inbound.setProcessStatus("MEASURE_PENDING");
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findMeasurement(1L, 10L, 100L)).thenReturn(existing);
|
||||
when(repository.updateMeasurement(existing)).thenReturn(true);
|
||||
|
||||
MeasurementBatchCommand command = new MeasurementBatchCommand(
|
||||
1L, 10L, 100L, 3, "MANUAL", "PLATFORM", null, null, null,
|
||||
LocalDateTime.now(), null, "CKW-2", new BigDecimal("21"),
|
||||
new BigDecimal("0.2"), "服装", 9L, List.of(item(BigDecimal.ONE)));
|
||||
|
||||
InboundMeasurementEntity saved = service.saveBatch(command);
|
||||
|
||||
assertThat(saved.getStatus()).isEqualTo("DRAFT");
|
||||
verify(repository).updateMeasurement(existing);
|
||||
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(100L), any());
|
||||
assertThatThrownBy(() -> service.saveBatch(command))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("已提交批次必须保持冻结");
|
||||
assertThat(existing.getStatus()).isEqualTo("SUBMITTED");
|
||||
verify(repository, never()).updateMeasurement(any());
|
||||
verify(repository, never()).replaceItems(
|
||||
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user