Merge remote-tracking branch 'origin/test' into dev

This commit is contained in:
2026-09-01 21:51:52 +08:00
42 changed files with 1598 additions and 67 deletions
@@ -44,11 +44,34 @@ public final class InboundMeasurementResponses {
BigDecimal actualVolumeCbm,
BigDecimal volumeWeightKg,
String actualGoodsName,
@Schema(description = "Order 服务中的最新客户申报,只用于和既有实测事实对比;不得回写测量、箱码或审核结果")
DeclarationSnapshot latestDeclaration,
List<Item> items,
List<WeightResult> weightResults
) {
}
public record DeclarationSnapshot(
String referenceId,
Integer totalPackageCount,
BigDecimal grossWeightKg,
BigDecimal volumeCbm,
String goodsName,
List<DeclarationItem> items
) {
}
public record DeclarationItem(
Long inboundDetailId,
Integer itemNo,
String cargoName,
Integer packageCount,
BigDecimal lengthCm,
BigDecimal widthCm,
BigDecimal heightCm
) {
}
public record Item(
Long id,
Long inboundDetailId,
@@ -1,5 +1,5 @@
package com.baiyue.firstleg.dto;
import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.util.List;
import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List;
public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record ChannelSummary(String channelCode, String channelName,
long candidateInboundCount, long availablePackageCount, long availableContainerCount) {}
@@ -9,12 +9,12 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
Integer availablePackageCount, BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg,
BigDecimal averageUnitVolumeCbm, BigDecimal availableVolumeCbm,
Long coLoadGroupId, String groupNo, Integer memberCount, Integer groupTotalBoxes,
Boolean locked, String blockReason) {}
Boolean locked, String blockReason, String orderStatusSnapshot, LocalDateTime expectedArrivalAt) {}
public record Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode,
Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsName, Integer availablePackageCount,
BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal unitVolumeCbm,
Long coLoadGroupId, String groupNo, Integer memberCount, Integer groupTotalBoxes,
Boolean locked, String blockReason) {}
Boolean locked, String blockReason, String orderStatusSnapshot, LocalDateTime expectedArrivalAt) {}
public record AssignRequest(@NotNull @Positive Long inboundId,@NotNull @Positive Long inboundDetailId,
@NotNull @Positive Integer packageCount,@NotNull @Min(0) Integer expectedVersion,@NotBlank @Size(max=128) String operationId) {}
public record AssignInboundRequest(@NotNull @Positive Long inboundId,
@@ -15,6 +15,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
@Select("""
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt,
o.warehouse_id warehouseId,o.channel_code channelCode,o.channel_name channelName,o.destination_warehouse_id destinationWarehouseId,
o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
@@ -64,6 +65,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
@Select("""
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt,
o.warehouse_id warehouseId,o.channel_code channelCode,o.channel_name channelName,o.destination_warehouse_id destinationWarehouseId,
o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
@@ -69,9 +69,8 @@ public class InboundMeasurementService {
? List.of() : repository.listItems(tenantId, measurement.getId());
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
MeasureReviewRecordEntity review = repository.findLatestReview(tenantId, inboundId);
OrderCargoClient.MeasurementDeclarationSnapshot declaration = needsDeclarationFallback(measurement)
? loadDeclaration(inbound) : null;
return toDetail(inbound, measurement, items, weights, review, declaration);
InboundMeasurementResponses.DeclarationSnapshot latestDeclaration = loadLatestDeclaration(inbound);
return toDetail(inbound, measurement, items, weights, review, latestDeclaration);
}
public PageResult<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(
@@ -416,7 +415,8 @@ public class InboundMeasurementService {
private InboundMeasurementResponses.Detail toDetail(
InboundOrderEntity inbound, InboundMeasurementEntity measurement,
List<InboundMeasurementItemEntity> items, List<InboundWeightResultEntity> weights,
MeasureReviewRecordEntity review, OrderCargoClient.MeasurementDeclarationSnapshot declaration) {
MeasureReviewRecordEntity review,
InboundMeasurementResponses.DeclarationSnapshot latestDeclaration) {
return new InboundMeasurementResponses.Detail(
inbound.getId(), inbound.getInboundCode(), inbound.getCompanyName(), inbound.getChannelCode(),
inbound.getChannelName(), inbound.getProcessStatus(), inbound.getMeasureReviewStatus(),
@@ -428,26 +428,67 @@ public class InboundMeasurementService {
inbound.getVersion(),
measurement == null ? null : measurement.getVersion(),
measurement != null && StringUtils.hasText(measurement.getOriginalDocumentNo())
? measurement.getOriginalDocumentNo() : declaration == null ? null : declaration.referenceId(),
? measurement.getOriginalDocumentNo() : latestDeclaration.referenceId(),
measurement != null && measurement.getOriginalGrossWeightKg() != null
? measurement.getOriginalGrossWeightKg() : declaration == null ? null : declaration.originalGrossWeightKg(),
? measurement.getOriginalGrossWeightKg() : latestDeclaration.grossWeightKg(),
measurement != null && measurement.getOriginalVolumeCbm() != null
? measurement.getOriginalVolumeCbm() : declaration == null ? null : declaration.originalVolumeCbm(),
inbound.getTotalPackageCount(),
? measurement.getOriginalVolumeCbm() : latestDeclaration.volumeCbm(),
latestDeclaration.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() : declaration == null ? null : declaration.defaultActualGoodsName(),
? measurement.getActualGoodsName() : latestDeclaration.goodsName(),
latestDeclaration,
items.stream().map(this::toItem).toList(), weights.stream().map(this::toWeight).toList());
}
private boolean needsDeclarationFallback(InboundMeasurementEntity measurement) {
return measurement == null || !StringUtils.hasText(measurement.getOriginalDocumentNo())
|| measurement.getOriginalGrossWeightKg() == null || measurement.getOriginalVolumeCbm() == null
|| !StringUtils.hasText(measurement.getActualGoodsName());
/**
* 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(
InboundOrderEntity inbound) {
OrderCargoClient.MeasurementDeclarationSnapshot aggregate = loadDeclaration(inbound);
List<OrderCargoClient.CargoLabelBinding> cargoes = loadDeclarationCargoes(inbound);
long packages = cargoes.stream()
.map(OrderCargoClient.CargoLabelBinding::packageCount)
.filter(java.util.Objects::nonNull)
.mapToLong(Integer::longValue)
.sum();
if (packages > Integer.MAX_VALUE) {
throw new BizException(ResultCode.VALIDATION_FAILED, "客户最新申报总箱数超过系统上限");
}
return new InboundMeasurementResponses.DeclarationSnapshot(
aggregate.referenceId(), (int) packages, aggregate.originalGrossWeightKg(),
aggregate.originalVolumeCbm(), aggregate.defaultActualGoodsName(),
cargoes.stream()
.sorted(java.util.Comparator.comparing(OrderCargoClient.CargoLabelBinding::itemNo,
java.util.Comparator.nullsLast(Integer::compareTo))
.thenComparing(OrderCargoClient.CargoLabelBinding::cargoId,
java.util.Comparator.nullsLast(Long::compareTo)))
.map(cargo -> new InboundMeasurementResponses.DeclarationItem(
cargo.cargoId(), cargo.itemNo(), cargo.cargoName(), cargo.packageCount(),
cargo.lengthCm(), cargo.widthCm(), cargo.heightCm()))
.toList());
}
private List<OrderCargoClient.CargoLabelBinding> loadDeclarationCargoes(InboundOrderEntity inbound) {
try {
ApiResponse<List<OrderCargoClient.CargoLabelBinding>> response = orderCargoClient.listForCartonLabels(
internalToken, inbound.getTenantId(), inbound.getOrderId());
if (response == null || !response.successful() || response.data() == null) {
String message = response == null || !StringUtils.hasText(response.message())
? "客户订单申报明细查询失败" : response.message();
throw new BizException(response == null ? ResultCode.SERVICE_UNAVAILABLE
: ResultCode.of(response.code()), message);
}
return response.data();
} catch (FeignException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务暂不可用,无法读取客户最新申报明细");
}
}
private OrderCargoClient.MeasurementDeclarationSnapshot loadDeclaration(InboundOrderEntity inbound) {
@@ -113,7 +113,8 @@ public class SmartLoadingService {
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
first.customerName(), goods, packages, first.unitGrossWeightKg(), first.unitNetWeightKg(),
averageVolume, volume, first.coLoadGroupId(), first.groupNo(), first.memberCount(),
first.groupTotalBoxes(), first.locked(), first.blockReason());
first.groupTotalBoxes(), first.locked(), first.blockReason(), first.orderStatusSnapshot(),
first.expectedArrivalAt());
}
public Plan draftByBooking(long bookingId) {
@@ -563,7 +564,8 @@ public class SmartLoadingService {
candidate.customerName(), candidate.goodsName(), candidate.availablePackageCount(),
candidate.unitGrossWeightKg(), candidate.unitNetWeightKg(), candidate.unitVolumeCbm(),
group.groupId(), group.groupNo(), group.members().size(), group.fullPackageCount(),
group.lockedPlanId() != null, blocks.get(group.groupId()));
group.lockedPlanId() != null, blocks.get(group.groupId()), candidate.orderStatusSnapshot(),
candidate.expectedArrivalAt());
}).toList();
}
@@ -409,6 +409,7 @@ paths:
description: >-
需要 platform:firstleg:smart-loading:list 权限。每个入库单/入仓码只返回一行;
平均单箱毛重、净重按整票实测重量除以总箱数,平均单箱体积按剩余规格数量加权。
响应携带订单状态快照 orderStatusSnapshot 与预计到货时间 expectedArrivalAt,供平台筛选候选入仓码;
这些数据用于数量级计划,不表示具体箱号已核验归属。
parameters:
- {name: bookingId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
@@ -1415,10 +1416,10 @@ components:
measurementStatus: {type: string, nullable: true}
inboundVersion: {type: integer, minimum: 0}
version: {type: integer, minimum: 0, nullable: true}
originalDocumentNo: {type: string, nullable: true, description: 客户订单 referenceId 的服务端快照;旧空快照按订单事实回退}
originalGrossWeightKg: {type: number, format: double, nullable: true, description: 客户订单有效货物行 grossWeightKg 合计的服务端快照}
originalVolumeCbm: {type: number, format: double, nullable: true, description: 客户订单有效货物行 volumeCbm 合计的服务端快照}
originalPackageCount: {type: integer, nullable: true}
originalDocumentNo: {type: string, nullable: true, description: 测量批次开始时冻结的客户订单 referenceId;批次为空时才以最新申报回退}
originalGrossWeightKg: {type: number, format: double, nullable: true, description: 测量批次开始时冻结的客户申报毛重;不得被后续订单编辑覆盖}
originalVolumeCbm: {type: number, format: double, nullable: true, description: 测量批次开始时冻结的客户申报体积;不得被后续订单编辑覆盖}
originalPackageCount: {type: integer, nullable: true, description: 最新客户申报总箱数;仅用于与 actualPackageCount 对比}
actualPackageCount: {type: integer, nullable: true}
actualGrossWeightKg:
type: number
@@ -1435,6 +1436,8 @@ components:
actualVolumeCbm: {type: number, format: double, nullable: true}
volumeWeightKg: {type: number, format: double, nullable: true}
actualGoodsName: {type: string, nullable: true, description: 默认由客户货物品名去重汇总,测量阶段允许人工修正}
latestDeclaration:
$ref: '#/components/schemas/InboundMeasurementLatestDeclaration'
items:
type: array
description: DIMENSION 尺寸测量明细及其图片证据
@@ -1443,6 +1446,31 @@ components:
type: array
description: WEIGHT 称重结果及其图片、确认状态和服务端重量事实
items: {$ref: '#/components/schemas/InboundWeightResult'}
InboundMeasurementLatestDeclaration:
type: object
description: Order 服务中的最新客户申报只读视图;订单编辑只能改变本对象,不得覆盖箱码、实测尺寸/重量或审核结果
required: [totalPackageCount, items]
properties:
referenceId: {type: string, nullable: true}
totalPackageCount: {type: integer, minimum: 0}
grossWeightKg: {type: number, format: double, nullable: true}
volumeCbm: {type: number, format: double, nullable: true}
goodsName: {type: string, nullable: true}
items:
type: array
items: {$ref: '#/components/schemas/InboundMeasurementDeclarationItem'}
InboundMeasurementDeclarationItem:
type: object
description: 最新订单货物申报行;inboundDetailId 仅用于与实测明细关联,不作为用户展示编号
required: [inboundDetailId, itemNo, packageCount]
properties:
inboundDetailId: {type: string, pattern: '^[1-9][0-9]*$'}
itemNo: {type: integer, minimum: 1}
cargoName: {type: string, nullable: true}
packageCount: {type: integer, minimum: 0}
lengthCm: {type: number, format: double, nullable: true}
widthCm: {type: number, format: double, nullable: true}
heightCm: {type: number, format: double, nullable: true}
InboundMeasurementSaveDraftRequest:
type: object
required: [version, items]