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)) {
|
||||
|
||||
Reference in New Issue
Block a user