This commit is contained in:
2026-08-11 09:25:41 +08:00
parent 66b3a0cc9b
commit 540a6f5d88
18 changed files with 757 additions and 90 deletions
@@ -16,11 +16,12 @@ public interface LoadingInboundMapper {
batch.loaded_child_inbound_id AS loadedChildInboundId,
batch.parent_inbound_id AS rootInboundId,
source.process_status AS sourceStatus, source.version AS sourceVersion,
child.process_status AS loadedChildStatus, child.version AS loadedChildVersion
CASE WHEN batch.loaded_child_inbound_id IS NULL THEN source.process_status ELSE child.process_status END AS loadedChildStatus,
CASE WHEN batch.loaded_child_inbound_id IS NULL THEN source.version ELSE child.version END AS loadedChildVersion
FROM fl_inbound_split_batch batch
JOIN fl_inbound_order source ON source.id=batch.source_inbound_id
AND source.tenant_id=batch.tenant_id AND source.deleted=0
JOIN fl_inbound_order child ON child.id=batch.loaded_child_inbound_id
LEFT JOIN fl_inbound_order child ON child.id=batch.loaded_child_inbound_id
AND child.tenant_id=batch.tenant_id AND child.deleted=0
WHERE batch.tenant_id=#{tenantId} AND batch.status='APPLIED'
AND (batch.source_inbound_id=#{inboundId} OR batch.loaded_child_inbound_id=#{inboundId})
@@ -35,15 +36,14 @@ public interface LoadingInboundMapper {
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
@Update("""
UPDATE fl_inbound_order child
JOIN fl_inbound_split_batch batch ON batch.loaded_child_inbound_id=child.id
AND batch.tenant_id=child.tenant_id AND batch.status='APPLIED'
JOIN fl_inbound_order source ON source.id=batch.source_inbound_id
AND source.tenant_id=batch.tenant_id AND source.deleted=0
SET child.process_status='LOADED', child.updated_by=#{operatorId}, child.version=child.version+1
WHERE child.tenant_id=#{tenantId} AND batch.source_inbound_id=#{sourceInboundId}
AND child.deleted=0 AND child.process_status='LOADING'
AND source.version=#{expectedSourceVersion}
UPDATE fl_inbound_split_batch batch
JOIN fl_inbound_order target
ON target.id=COALESCE(batch.loaded_child_inbound_id,batch.source_inbound_id)
AND target.tenant_id=batch.tenant_id AND target.deleted=0
SET target.process_status='LOADED', target.updated_by=#{operatorId}, target.version=target.version+1
WHERE batch.tenant_id=#{tenantId} AND batch.source_inbound_id=#{sourceInboundId}
AND batch.status='APPLIED' AND target.process_status='LOADING'
AND target.version=#{expectedSourceVersion}
""")
int completeLoadedChild(@Param("tenantId") long tenantId,
@Param("sourceInboundId") long sourceInboundId,
@@ -73,5 +73,9 @@ public interface LoadingInboundMapper {
record Snapshot(Long id, Long tenantId, String processStatus, Integer version) {}
record CompletionTarget(Long sourceInboundId, Long loadedChildInboundId, Long rootInboundId,
String sourceStatus, Integer sourceVersion,
String loadedChildStatus, Integer loadedChildVersion) {}
String loadedChildStatus, Integer loadedChildVersion) {
public Long completionInboundId() {
return loadedChildInboundId == null ? sourceInboundId : loadedChildInboundId;
}
}
}
@@ -138,7 +138,7 @@ public class LoadingDeliveryService {
if (repository.completeLoadedChild(tenantId, target.sourceInboundId(), operatorId,
request.expectedVersion()) != 1) throw concurrent();
repository.aggregateRootLoaded(tenantId, target.rootInboundId(), operatorId);
addHistory(tenantId, target.loadedChildInboundId(), "LOADING", "LOADED", COMPLETE_LOADING,
addHistory(tenantId, target.completionInboundId(), "LOADING", "LOADED", COMPLETE_LOADING,
request.remark(), operatorId, operatorName);
return complete(tenantId, evidenceInboundId, operatorId, COMPLETE_LOADING, key,
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, evidenceInboundId)));
@@ -54,7 +54,7 @@ public interface InboundSplitSettlementMapper {
#{packageCount},#{packageCount},actual_gross_weight_kg,chargeable_weight_kg,actual_volume_cbm,chargeable_volume_cbm,
pallet_count,volume_weight_kg,actual_net_weight_kg,measure_completed_at,inbounded_at,express_company_code,
express_company_name,express_tracking_no,public_remark,#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),
#{operatorId},CURRENT_TIMESTAMP(3),0,0,'CHILD',#{parentInboundId},#{rootInboundId},#{splitSequence},'NOT_SPLIT',#{splitPurpose}
#{operatorId},CURRENT_TIMESTAMP(3),0,0,'CHILD',#{rootInboundId},#{rootInboundId},#{splitSequence},'NOT_SPLIT',#{splitPurpose}
FROM fl_inbound_order WHERE id=#{parentInboundId} AND tenant_id=#{accountId} AND deleted=0
""")
int cloneChildWithPurpose(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
@@ -70,7 +70,7 @@ public interface InboundSplitSettlementMapper {
loaded_package_count,remaining_package_count,loading_plan_id,container_booking_id,status,remark,
created_by,created_by_name,created_at,version)
VALUES (#{batchId},#{accountId},#{parentInboundId},#{sourceInboundId},#{operationId},#{idempotencyKey},#{expectedParentVersion},
#{packageCount},#{loadedChildId},#{remainderChildId},#{loadedPackageCount},#{remainingPackageCount},
#{packageCount},NULL,#{remainderChildId},#{loadedPackageCount},#{remainingPackageCount},
#{loadingPlanId},#{containerBookingId},'PREPARED',#{remark},#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),0)
""")
int insertBatchWithChildren(@Param("batchId") long batchId, @Param("accountId") long accountId,
@@ -157,17 +157,41 @@ public interface InboundSplitSettlementMapper {
@Param("packageCount") int packageCount, @Param("operatorId") long operatorId,
@Param("expectedVersion") int expectedVersion);
@Update("UPDATE fl_inbound_order SET package_count=0,split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 "
+ "WHERE id=#{parentInboundId} AND tenant_id=#{accountId} AND inbound_role='MASTER' AND deleted=0 "
+ "AND process_status IN ('INBOUNDED','LOADING') AND version=#{expectedVersion}")
int markRootSplit(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
@Update("UPDATE fl_inbound_order SET package_count=#{loadedPackageCount},process_status='LOADING',"
+ "split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 "
+ "WHERE id=#{sourceInboundId} AND tenant_id=#{accountId} AND deleted=0 "
+ "AND process_status IN ('INBOUNDED','LOADING') AND split_status='NOT_SPLIT' "
+ "AND COALESCE(package_count,total_package_count)>=#{loadedPackageCount} AND version=#{expectedVersion}")
int markSourceLoaded(@Param("accountId") long accountId, @Param("sourceInboundId") long sourceInboundId,
@Param("loadedPackageCount") int loadedPackageCount,
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
@Update("UPDATE fl_inbound_order SET split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 "
+ "WHERE id=#{sourceInboundId} AND tenant_id=#{accountId} AND inbound_role='CHILD' "
+ "AND split_purpose='REMAINDER' AND split_status='NOT_SPLIT' AND deleted=0 AND version=#{expectedVersion}")
int markRemainderConsumed(@Param("accountId") long accountId, @Param("sourceInboundId") long sourceInboundId,
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
@Update("UPDATE fl_inbound_order SET updated_by=#{operatorId},version=version+1 "
+ "WHERE id=#{rootInboundId} AND tenant_id=#{accountId} AND inbound_role='MASTER' AND deleted=0 "
+ "AND version=#{expectedVersion}")
int touchRootSplit(@Param("accountId") long accountId, @Param("rootInboundId") long rootInboundId,
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
@Update("""
UPDATE fl_inbound_measurement_item
SET package_count=#{packageCount},
total_weight_kg=CASE WHEN unit_weight_kg IS NULL THEN NULL ELSE unit_weight_kg*#{packageCount} END,
total_volume_cbm=unit_volume_cbm*#{packageCount},
volume_weight_kg=volume_weight_kg/NULLIF(package_count,0)*#{packageCount},
updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{measurementItemId} AND tenant_id=#{accountId}
""")
int updateMeasurementItemForLoaded(@Param("accountId") long accountId,
@Param("measurementItemId") long measurementItemId,
@Param("packageCount") int packageCount);
@Update("UPDATE fl_inbound_measurement SET total_package_count=#{packageCount},updated_at=CURRENT_TIMESTAMP(3),"
+ "version=version+1 WHERE id=#{measurementId} AND tenant_id=#{accountId} AND inbound_id=#{inboundId} "
+ "AND status='APPROVED'")
int updateMeasurementForLoaded(@Param("accountId") long accountId,
@Param("inboundId") long inboundId,
@Param("measurementId") long measurementId,
@Param("packageCount") int packageCount);
@Update("""
UPDATE fl_inbound_split_batch
@@ -262,7 +286,7 @@ public interface InboundSplitSettlementMapper {
child.volume_weight_kg=(SELECT COALESCE(SUM(item.volume_weight_kg),0)
FROM fl_inbound_measurement_item item WHERE item.measurement_id=measurement.id),
child.actual_net_weight_kg=NULL,child.pallet_count=0,child.version=child.version+1
WHERE child.id=#{childInboundId} AND child.tenant_id=#{accountId} AND child.inbound_role='CHILD'
WHERE child.id=#{childInboundId} AND child.tenant_id=#{accountId} AND child.deleted=0
""")
int normalizeChildMetrics(@Param("accountId") long accountId, @Param("childInboundId") long childInboundId,
@Param("measurementId") long measurementId);
@@ -161,30 +161,23 @@ public class PdaPickSettlementService {
OrderFulfillmentSplitClient.Response orderSplit = createOrderSplit(parent, root, measurementLines,
pickedPackageCount, childPackageCount, loadingPlanId, containerBookingId, operationId,
"pda-task-" + taskId, operatorId, operatorName);
long loadedChildId = IdWorker.getId();
long remainderChildId = childPackageCount > 0 ? IdWorker.getId() : 0L;
long batchId = IdWorker.getId();
String idempotencyKey = "pda-task-" + taskId;
int parentVersion = root.version() == null ? 0 : root.version();
int rootVersion = root.version() == null ? 0 : root.version();
int sourceVersion = parent.version() == null ? 0 : parent.version();
int sequence = splits.nextSplitSequence(parent.tenantId(), rootInboundId);
String loadedChildCode = childInboundCode(root.inboundCode(), sequence);
String remainderChildCode = childPackageCount > 0
? childInboundCode(root.inboundCode(), sequence + 1) : null;
requireAffected(splits.cloneChildWithPurpose(parent.tenantId(), rootInboundId, rootInboundId, loadedChildId,
orderSplit.loadedChild().orderId(), orderSplit.loadedChild().orderNo(),
loadedChildCode, pickedPackageCount, sequence, "LOADED", operatorId, operatorName), "实装子入库单");
? childInboundCode(root.inboundCode(), sequence) : null;
if (remainderChildId != 0L) {
requireAffected(splits.cloneChildWithPurpose(parent.tenantId(), rootInboundId, rootInboundId, remainderChildId,
requireAffected(splits.cloneChildWithPurpose(parent.tenantId(), parentInboundId, rootInboundId, remainderChildId,
orderSplit.remainderChild().orderId(), orderSplit.remainderChild().orderNo(),
remainderChildCode, childPackageCount, sequence + 1, "REMAINDER", operatorId, operatorName), "余量子入库单");
remainderChildCode, childPackageCount, sequence, "REMAINDER", operatorId, operatorName), "余量子入库单");
}
requireAffected(splits.insertBatchWithChildren(batchId, parent.tenantId(), rootInboundId, operationId, parentInboundId,
idempotencyKey, parentVersion, parentAvailable, loadedChildId, remainderChildId == 0L ? null : remainderChildId,
idempotencyKey, rootVersion, parentAvailable, null, remainderChildId == 0L ? null : remainderChildId,
pickedPackageCount, childPackageCount, loadingPlanId, containerBookingId,
"PDA拣货后生成实装/余量子单", operatorId, operatorName), "拆分批次");
long loadedMeasurementId = IdWorker.getId();
requireAffected(splits.cloneMeasurement(parent.tenantId(), parentInboundId, sourceMeasurementId,
loadedChildId, loadedMeasurementId, batchId, pickedPackageCount, operatorId), "实装子入库测量批次");
long childMeasurementId = childPackageCount > 0 ? IdWorker.getId() : 0L;
if (childPackageCount > 0) {
requireAffected(splits.cloneMeasurement(parent.tenantId(), parentInboundId, sourceMeasurementId,
@@ -192,51 +185,46 @@ public class PdaPickSettlementService {
}
int lineNo = 1;
for (MeasurementSplitLine line : measurementLines) {
if (line.pickedPackageCount() <= 0) continue;
long splitItemId = IdWorker.getId();
requireAffected(splits.insertSplitItem(splitItemId, parent.tenantId(), batchId, rootInboundId,
loadedChildId, lineNo, line.item().inboundDetailId(), sourceMeasurementId,
line.item().measurementItemId(), line.pickedPackageCount()), "实装拆分明细");
requireAffected(splits.cloneMeasurementItem(parent.tenantId(), sourceMeasurementId,
line.item().measurementItemId(), loadedMeasurementId, splitItemId, IdWorker.getId(), lineNo,
line.pickedPackageCount()), "实装子入库测量明细");
lineNo++;
}
for (MeasurementSplitLine line : measurementLines) {
if (line.remainingPackageCount() <= 0) {
continue;
}
long splitItemId = IdWorker.getId();
requireAffected(splits.insertSplitItem(splitItemId, parent.tenantId(), batchId, rootInboundId,
remainderChildId == 0L ? loadedChildId : remainderChildId, lineNo, line.item().inboundDetailId(), sourceMeasurementId,
remainderChildId, lineNo, line.item().inboundDetailId(), sourceMeasurementId,
line.item().measurementItemId(), line.remainingPackageCount()), "拆分明细");
requireAffected(splits.cloneMeasurementItem(parent.tenantId(), sourceMeasurementId,
line.item().measurementItemId(), childMeasurementId, splitItemId, IdWorker.getId(), lineNo,
line.remainingPackageCount()), "子入库测量明细");
lineNo++;
}
requireAffected(splits.normalizeChildMetrics(parent.tenantId(), loadedChildId, loadedMeasurementId),
"实装子单重量体积归一化");
requireAffected(splits.markSourceLoaded(parent.tenantId(), parentInboundId, pickedPackageCount,
operatorId, sourceVersion), "来源分片状态");
if (parentInboundId != rootInboundId) {
requireAffected(splits.touchRootSplit(parent.tenantId(), rootInboundId, operatorId, rootVersion), "根单拆分状态");
}
requireAffected(splits.applyBatch(parent.tenantId(), batchId), "拆分批次生效");
for (MeasurementSplitLine line : measurementLines) {
requireAffected(splits.updateMeasurementItemForLoaded(parent.tenantId(),
line.item().measurementItemId(), line.pickedPackageCount()), "来源测量明细");
}
requireAffected(splits.updateMeasurementForLoaded(parent.tenantId(), parentInboundId, sourceMeasurementId,
pickedPackageCount), "来源测量批次");
requireAffected(splits.normalizeChildMetrics(parent.tenantId(), parentInboundId, sourceMeasurementId),
"来源单重量体积归一化");
if (remainderChildId != 0L) {
requireAffected(splits.normalizeChildMetrics(parent.tenantId(), remainderChildId, childMeasurementId),
"余量子单重量体积归一化");
}
requireAffected(splits.markRootSplit(parent.tenantId(), rootInboundId, operatorId, parentVersion), "主入库单拆分状态");
if ("CHILD".equals(parent.inboundRole())) {
requireAffected(splits.markRemainderConsumed(parent.tenantId(), parentInboundId, operatorId,
parent.version() == null ? 0 : parent.version()), "余量子单消费状态");
}
requireAffected(splits.applyBatch(parent.tenantId(), batchId), "拆分批次生效");
requireAffected(splits.insertHistory(IdWorker.getId(), parent.tenantId(), batchId,
operationId + "-APPLY", "{\"sourceInboundId\":" + parentInboundId
+ ",\"loadedChildInboundId\":" + loadedChildId
+ ",\"loadedChildInboundId\":null"
+ ",\"remainderChildInboundId\":" + (remainderChildId == 0L ? "null" : remainderChildId)
+ ",\"loadedPackageCount\":" + pickedPackageCount
+ ",\"remainingPackageCount\":" + childPackageCount
+ ",\"loadingPlanId\":" + loadingPlanId
+ ",\"containerBookingId\":" + containerBookingId + "}", operatorId, operatorName,
"PDA拣货自动生成实装/余量子入库单"), "拆分审计");
"PDA拣货后来源单承接实装并生成余量子入库单"), "拆分审计");
if (remainderChildId != 0L) {
allocations.copyRemainingToChild(parent.tenantId(), remainderChildId, confirmedAllocations);
@@ -245,7 +233,7 @@ public class PdaPickSettlementService {
releasedReservationIds);
allocations.markReleased(parent.tenantId(), parentInboundId, releasedReservationIds);
settleWarehouseResources(parentInboundId, remainderChildId,
remainderChildCode == null ? loadedChildCode : remainderChildCode,
remainderChildCode == null ? root.inboundCode() : remainderChildCode,
taskId, operatorId, operatorName,
warehouseId, parentAvailable, childPackageCount, releasedReservationIds, transferItems);
return true;
@@ -282,14 +270,17 @@ public class PdaPickSettlementService {
boolean remainderMatches = remainingPackageCount == 0 ? split.remainderChild() == null
: validOrderChild(split.remainderChild(), "REMAINDER", source.orderId(), loadingPlanId,
containerBookingId, remainingPackageCount);
boolean sourceLoadedMatches = split.loadedChild() == null
|| (Objects.equals(split.loadedChild().orderId(), source.orderId())
&& validOrderChild(split.loadedChild(), "LOADED", source.orderId(), loadingPlanId,
containerBookingId, loadedPackageCount));
if (!Objects.equals(split.parentOrderId(), root.orderId())
|| !Objects.equals(split.sourcePackageCount(), loadedPackageCount + remainingPackageCount)
|| !Objects.equals(split.loadedPackageCount(), loadedPackageCount)
|| !Objects.equals(split.remainingPackageCount(), remainingPackageCount)
|| !Objects.equals(split.loadingPlanId(), loadingPlanId)
|| !Objects.equals(split.containerBookingId(), containerBookingId)
|| !validOrderChild(split.loadedChild(), "LOADED", source.orderId(), loadingPlanId,
containerBookingId, loadedPackageCount)
|| !sourceLoadedMatches
|| !remainderMatches) {
throw conflict("订单服务返回的父子订单、柜次或数量不一致");
}