fix(firstleg): recover multi-customer PDA completion

This commit is contained in:
2026-08-29 08:25:42 +08:00
parent da32a67d7f
commit e6ee9cb442
4 changed files with 185 additions and 42 deletions
@@ -110,17 +110,20 @@ public class LoadingDeliveryService {
@Transactional
public LoadingActionResponse completeLoading(long tenantId, long inboundId, long operatorId,
String operatorName, String key, CompleteLoadingRequest request) {
return completeLoading(tenantId, inboundId, inboundId, operatorId, operatorName, key, request);
return completeLoading(tenantId, inboundId, tenantId, inboundId,
operatorId, operatorName, key, request);
}
private LoadingActionResponse completeLoading(long tenantId, long inboundId, long evidenceInboundId,
private LoadingActionResponse completeLoading(long tenantId, long inboundId,
long evidenceTenantId, long evidenceInboundId,
long operatorId, String operatorName, String key,
CompleteLoadingRequest request) {
return completeLoading(tenantId, inboundId, evidenceInboundId, operatorId, operatorName,
key, request, false);
return completeLoading(tenantId, inboundId, evidenceTenantId, evidenceInboundId,
operatorId, operatorName, key, request, false);
}
private LoadingActionResponse completeLoading(long tenantId, long inboundId, long evidenceInboundId,
private LoadingActionResponse completeLoading(long tenantId, long inboundId,
long evidenceTenantId, long evidenceInboundId,
long operatorId, String operatorName, String key,
CompleteLoadingRequest request,
boolean acceptedPdaCompletion) {
@@ -131,8 +134,8 @@ public class LoadingDeliveryService {
Snapshot inbound = requireInbound(tenantId, inboundId);
CompletionTarget target = repository.completionTarget(tenantId, inboundId);
if (target != null) {
return completeFulfillmentSlice(tenantId, evidenceInboundId, operatorId, operatorName,
key, request, target, acceptedPdaCompletion);
return completeFulfillmentSlice(tenantId, inboundId, evidenceTenantId, evidenceInboundId,
operatorId, operatorName, key, request, target, acceptedPdaCompletion);
}
if ("LOADED".equals(inbound.processStatus()) || "DELIVERED".equals(inbound.processStatus())) {
requireLoadingPlanGate(tenantId, inboundId, acceptedPdaCompletion);
@@ -143,7 +146,7 @@ public class LoadingDeliveryService {
requireLoadingPlanGate(tenantId, inboundId, acceptedPdaCompletion);
completionGate.requireReleasedBooking(tenantId, inboundId);
requirePickingGate(tenantId, inboundId, acceptedPdaCompletion);
requireCompletionEvidence(tenantId, evidenceInboundId);
requireCompletionEvidence(evidenceTenantId, evidenceInboundId);
if (repository.completeLoading(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) throw concurrent();
addHistory(tenantId, inboundId, "LOADING", "LOADED", COMPLETE_LOADING, request.remark(), operatorId, operatorName);
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
@@ -159,12 +162,14 @@ public class LoadingDeliveryService {
public LoadingActionResponse completeAfterPdaSettlement(long tenantId, long inboundId,
long operatorId, String operatorName,
String key, String remark) {
return completeAfterPdaSettlement(tenantId, inboundId, inboundId, operatorId, operatorName, key, remark);
return completeAfterPdaSettlement(tenantId, inboundId, tenantId, inboundId,
operatorId, operatorName, key, remark);
}
@Transactional
public LoadingActionResponse completeAfterPdaSettlement(long tenantId, long inboundId,
long evidenceInboundId, long operatorId,
long evidenceTenantId, long evidenceInboundId,
long operatorId,
String operatorName, String key, String remark) {
CompletionTarget target = repository.completionTarget(tenantId, inboundId);
long completionInboundId = target == null ? inboundId : target.completionInboundId();
@@ -172,8 +177,8 @@ public class LoadingDeliveryService {
if (current == null || current.version() == null) {
throw new BizException(ResultCode.CONFLICT, "PDA结算后的实装履约分片不存在,请刷新后重试");
}
return completeLoading(tenantId, inboundId, evidenceInboundId, operatorId, operatorName, key,
new CompleteLoadingRequest(current.version(), remark), true);
return completeLoading(tenantId, inboundId, evidenceTenantId, evidenceInboundId,
operatorId, operatorName, key, new CompleteLoadingRequest(current.version(), remark), true);
}
/**
@@ -181,27 +186,29 @@ public class LoadingDeliveryService {
* every external/business hard gate must pass before the PDA task is mutated.
*/
public void requirePdaCompletionReady(long tenantId, long inboundId) {
requirePdaCompletionReady(tenantId, inboundId, inboundId);
requirePdaCompletionReady(tenantId, inboundId, tenantId, inboundId);
}
public void requirePdaCompletionReady(long tenantId, long inboundId, long evidenceInboundId) {
public void requirePdaCompletionReady(long tenantId, long inboundId,
long evidenceTenantId, long evidenceInboundId) {
Snapshot inbound = requireInbound(tenantId, inboundId);
if (!"LOADING".equals(inbound.processStatus())) {
throw new BizException(ResultCode.CONFLICT, "PDA确认装柜完成前订单必须处于LOADING");
}
loadingModelBridge.requireStarted(tenantId, inboundId);
completionGate.requireReleasedBooking(tenantId, inboundId);
requireCompletionEvidence(tenantId, evidenceInboundId);
requireCompletionEvidence(evidenceTenantId, evidenceInboundId);
}
private LoadingActionResponse completeFulfillmentSlice(long tenantId, long evidenceInboundId,
private LoadingActionResponse completeFulfillmentSlice(long tenantId, long inboundId,
long evidenceTenantId, long evidenceInboundId,
long operatorId, String operatorName, String key,
CompleteLoadingRequest request,
CompletionTarget target,
boolean acceptedPdaCompletion) {
if ("LOADED".equals(target.loadedChildStatus())) {
return complete(tenantId, evidenceInboundId, operatorId, COMPLETE_LOADING, key,
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, evidenceInboundId)));
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, inboundId)));
}
if (!"LOADING".equals(target.loadedChildStatus())) {
throw new BizException(ResultCode.CONFLICT, "实装履约分片不在装柜中");
@@ -209,14 +216,14 @@ public class LoadingDeliveryService {
requireLoadingPlanGate(tenantId, target.sourceInboundId(), acceptedPdaCompletion);
completionGate.requireReleasedBooking(tenantId, target.sourceInboundId());
requirePickingGate(tenantId, target.sourceInboundId(), acceptedPdaCompletion);
requireCompletionEvidence(tenantId, evidenceInboundId);
requireCompletionEvidence(evidenceTenantId, evidenceInboundId);
if (repository.completeLoadedChild(tenantId, target.sourceInboundId(), operatorId,
request.expectedVersion()) != 1) throw concurrent();
repository.aggregateRootLoaded(tenantId, target.rootInboundId(), operatorId);
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)));
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, inboundId)));
}
public EvidenceListResponse evidence(long tenantId, long inboundId) {
@@ -512,17 +512,32 @@ public class LoadingPickService {
.thenComparing(LoadingPickItemMapper.InboundOwner::inboundId))
.toList();
if (owners.isEmpty()) throw new CompletionProcessingException("OWNER_DISCOVERY", conflict("PDA任务未关联有效入库单"));
long evidenceInboundId = owners.get(0).inboundId();
LoadingPickItemMapper.InboundOwner evidenceOwner = owners.get(0);
// Phase 1 is read-only: every owner must pass its business gates against the same cabinet evidence owner
// before any warehouse resource or inbound status can be changed. This prevents a later customer gate from
// leaving earlier customers partially completed.
for (int index = 0; index < owners.size(); index++) {
requireCompletionLease(command.getId(), worker);
LoadingPickItemMapper.InboundOwner owner = owners.get(index);
String checkpoint = completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId());
if (!checkpointAtLeast(checkpoint, "READY")) {
if (!checkpointAtLeast(checkpoint, "SETTLED")) {
runCompletionStage("OWNER_READY", () -> loadingDeliveryService.requirePdaCompletionReady(
owner.tenantId(), owner.inboundId(), evidenceInboundId));
requireCompletionLease(command.getId(), worker);
completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId(), index + 1, "READY");
owner.tenantId(), owner.inboundId(), evidenceOwner.tenantId(), evidenceOwner.inboundId()));
if (!checkpointAtLeast(checkpoint, "READY")) {
requireCompletionLease(command.getId(), worker);
completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId(),
index + 1, "READY");
}
}
}
// Phase 2 is recoverable by per-owner checkpoints and stable idempotency keys. It starts only after the
// complete cabinet passed phase 1, while prior deployments' completed checkpoints remain safely resumable.
for (int index = 0; index < owners.size(); index++) {
requireCompletionLease(command.getId(), worker);
LoadingPickItemMapper.InboundOwner owner = owners.get(index);
String checkpoint = completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId());
if (!checkpointAtLeast(checkpoint, "SETTLED")) {
runCompletionStage("WAREHOUSE_SETTLEMENT", () -> settlementService.settleOwner(
taskId, owner.inboundId(), command.getOperatorId(), command.getOperatorName()));
@@ -531,9 +546,9 @@ public class LoadingPickService {
}
if (!checkpointAtLeast(checkpoint, "INBOUND_COMPLETED")) {
runCompletionStage("INBOUND_COMPLETE", () -> loadingDeliveryService.completeAfterPdaSettlement(
owner.tenantId(), owner.inboundId(), evidenceInboundId, command.getOperatorId(),
command.getOperatorName(), "PDA-" + command.getOperationId() + '-' + owner.inboundId(),
command.getRemark()));
owner.tenantId(), owner.inboundId(), evidenceOwner.tenantId(), evidenceOwner.inboundId(),
command.getOperatorId(), command.getOperatorName(),
"PDA-" + command.getOperationId() + '-' + owner.inboundId(), command.getRemark()));
requireCompletionLease(command.getId(), worker);
completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId(), index + 1,
"INBOUND_COMPLETED");