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
@@ -253,10 +253,72 @@ class LoadingDeliveryServiceTest {
long.class, long.class, long.class, String.class, String.class, String.class)
.getAnnotation(Transactional.class)).isNotNull();
assertThat(LoadingDeliveryService.class.getMethod("completeAfterPdaSettlement",
long.class, long.class, long.class, long.class, String.class, String.class, String.class)
long.class, long.class, long.class, long.class, long.class,
String.class, String.class, String.class)
.getAnnotation(Transactional.class)).isNotNull();
}
@Test
void pdaPreflightUsesCabinetEvidenceOwnerWithoutCrossCustomerLookup() {
when(repository.findInbound(20, 200)).thenReturn(new Snapshot(200L, 20L, "LOADING", 3));
when(repository.listEvidence(10, 100)).thenReturn(List.of(
evidence("PHOTO_STEP_1", 1L, null), evidence("PHOTO_STEP_2", 2L, null),
evidence("PHOTO_STEP_3", 3L, null), evidence("PHOTO_STEP_4", 4L, null),
evidence("SEAL", 5L, "LOCK-001")));
when(files.validate(eq("token"), eq(10L), eq(100L), anyLong())).thenAnswer(invocation -> {
long fileId = invocation.getArgument(3);
String type = fileId == 5L ? "SEAL" : "PHOTO_STEP_" + fileId;
return ApiResponse.success(new LoadingEvidenceFileClient.FileMetadata(
fileId, type.toLowerCase() + ".png", "LOADING_" + type, "image/png"));
});
service.requirePdaCompletionReady(20, 200, 10, 100);
verify(loadingModelBridge).requireStarted(20, 200);
verify(completionGate).requireReleasedBooking(20, 200);
verify(repository).listEvidence(10, 100);
verify(repository, never()).listEvidence(20, 100);
verify(files, times(5)).validate(eq("token"), eq(10L), eq(100L), anyLong());
}
@Test
void pdaFinalizeKeepsOperationOnBusinessOwnerWhileUsingCabinetEvidenceOwner() {
when(repository.claimOperation(eq(20L), eq(200L), eq(7L), eq("COMPLETE_LOADING"),
eq("pda-multi-customer"), anyString())).thenReturn(true);
when(repository.findInboundForUpdate(20, 200)).thenReturn(new Snapshot(200L, 20L, "LOADING", 7));
when(repository.findInbound(20, 200)).thenReturn(new Snapshot(200L, 20L, "LOADING", 7));
LoadingPickItemMapper.InboundProgress progress = mock(LoadingPickItemMapper.InboundProgress.class);
when(progress.getExpectedCount()).thenReturn(1);
when(progress.getPickedCount()).thenReturn(1);
when(progress.getTaskCount()).thenReturn(1);
when(progress.getCompletedTaskCount()).thenReturn(0);
when(progress.getUnresolvedCount()).thenReturn(0);
when(loadingPicks.inboundProgress(20, 200)).thenReturn(progress);
when(repository.listEvidence(10, 100)).thenReturn(List.of(
evidence("PHOTO_STEP_1", 1L, null), evidence("PHOTO_STEP_2", 2L, null),
evidence("PHOTO_STEP_3", 3L, null), evidence("PHOTO_STEP_4", 4L, null),
evidence("SEAL", 5L, "LOCK-001")));
when(files.validate(eq("token"), eq(10L), eq(100L), anyLong())).thenAnswer(invocation -> {
long fileId = invocation.getArgument(3);
String type = fileId == 5L ? "SEAL" : "PHOTO_STEP_" + fileId;
return ApiResponse.success(new LoadingEvidenceFileClient.FileMetadata(
fileId, type.toLowerCase() + ".png", "LOADING_" + type, "image/png"));
});
when(repository.completeLoading(20, 200, 7, 7)).thenReturn(1);
LoadingActionResponse result = service.completeAfterPdaSettlement(
20, 200, 10, 100, 7, "admin", "pda-multi-customer", "完成");
assertThat(result.mainStatus()).isEqualTo("LOADED");
verify(repository).completeLoading(20, 200, 7, 7);
verify(repository).completeOperation(eq(20L), eq(200L), eq(7L),
eq("COMPLETE_LOADING"), eq("pda-multi-customer"), anyString());
verify(repository, never()).completeOperation(eq(20L), eq(100L), anyLong(),
anyString(), anyString(), anyString());
verify(repository).listEvidence(10, 100);
verify(repository, never()).listEvidence(20, 100);
}
@Test
void pdaFinalizeLocksAndUsesLoadedSliceVersionAfterPartialSettlement() {
readyForPdaFinalize();
@@ -567,17 +567,17 @@ class LoadingPickServiceTest {
verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
verify(settlementService).settle(20L, 9L, "operator");
verify(repository).syncInboundProjections(20L, 9L, "operator");
verify(loadingDeliveryService).requirePdaCompletionReady(101L, 50L, 50L);
verify(loadingDeliveryService).requirePdaCompletionReady(101L, 60L, 50L);
verify(loadingDeliveryService).requirePdaCompletionReady(101L, 50L, 101L, 50L);
verify(loadingDeliveryService).requirePdaCompletionReady(101L, 60L, 101L, 50L);
verify(truckAppointmentService).completeLoadingFromPda(99L, 20L, 9L, "operator");
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 50L, 9L, "operator",
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 101L, 50L, 9L, "operator",
"PDA-complete-1-50", "checked");
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 60L, 50L, 9L, "operator",
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 60L, 101L, 50L, 9L, "operator",
"PDA-complete-1-60", "checked");
verify(loadingListService).publishCompletedEvent(10L);
InOrder finalization = inOrder(loadingListService, loadingDeliveryService);
finalization.verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
finalization.verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 50L, 9L, "operator",
finalization.verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 101L, 50L, 9L, "operator",
"PDA-complete-1-50", "checked");
finalization.verify(loadingListService).publishCompletedEvent(10L);
}
@@ -608,7 +608,7 @@ class LoadingPickServiceTest {
verify(settlementService).settle(20L, 9L, "operator");
verify(repository).approve(20L, 9L, "operator", "checked");
verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 50L, 9L, "operator",
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 101L, 50L, 9L, "operator",
"PDA-complete-not-blocked-50", "checked");
verify(loadingListService).publishCompletedEvent(10L);
}
@@ -627,7 +627,7 @@ class LoadingPickServiceTest {
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
"入库单并发修改"))
.when(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 50L, 9L, "operator",
.when(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 101L, 50L, 9L, "operator",
"PDA-complete-finalize-fails-50", "checked");
assertThatThrownBy(() -> service.complete(20L,
@@ -687,7 +687,7 @@ class LoadingPickServiceTest {
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
verify(loadingListService, never()).publishCompletedEvent(anyLong());
verify(loadingDeliveryService, never()).completeAfterPdaSettlement(anyLong(), anyLong(), anyLong(),
anyLong(), anyString(), anyString(), anyString());
anyLong(), anyLong(), anyString(), anyString(), anyString());
}
@Test
@@ -827,6 +827,65 @@ class LoadingPickServiceTest {
verify(completionCommands).complete(900L, "worker-1");
}
@Test
void workerUsesOneStableCabinetEvidenceOwnerAcrossTwoCustomers() {
LoadingPickTaskEntity completing = task();
completing.setTaskStatus("COMPLETING");
when(repository.task(20L)).thenReturn(completing);
when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(202L, 80L),
new LoadingPickItemMapper.InboundOwner(101L, 60L),
new LoadingPickItemMapper.InboundOwner(202L, 70L),
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
when(repository.completeAccepted(20L, 9L, "operator")).thenReturn(1);
when(completionCommands.complete(903L, "worker-multi")).thenReturn(true);
service.processCompletion(completionCommand(903L, "async-multi"), "worker-multi");
verify(loadingDeliveryService).requirePdaCompletionReady(101L, 50L, 101L, 50L);
verify(loadingDeliveryService).requirePdaCompletionReady(101L, 60L, 101L, 50L);
verify(loadingDeliveryService).requirePdaCompletionReady(202L, 70L, 101L, 50L);
verify(loadingDeliveryService).requirePdaCompletionReady(202L, 80L, 101L, 50L);
verify(loadingDeliveryService).completeAfterPdaSettlement(
202L, 70L, 101L, 50L, 9L, "operator", "PDA-async-multi-70", "checked");
verify(settlementService, times(4)).settleOwner(eq(20L), anyLong(), eq(9L), eq("operator"));
verify(repository, times(1)).insert(argThat((LoadingPickOperationEntity operation) ->
"COMPLETE_LOADING".equals(operation.getActionType())
&& "async-multi".equals(operation.getOperationId())));
verify(loadingListService, times(1)).publishCompletedEvent(10L);
verify(completionCommands).complete(903L, "worker-multi");
}
@Test
void secondCustomerOwnerReadyFailureLeavesEveryOwnerUnsettledAndUncompleted() {
LoadingPickTaskEntity completing = task();
completing.setTaskStatus("COMPLETING");
when(repository.task(20L)).thenReturn(completing);
when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(101L, 50L),
new LoadingPickItemMapper.InboundOwner(202L, 70L)));
doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT, "第二客户门禁未就绪"))
.when(loadingDeliveryService).requirePdaCompletionReady(202L, 70L, 101L, 50L);
assertThatThrownBy(() -> service.processCompletion(
completionCommand(904L, "async-owner-ready-fails"), "worker-failure"))
.isInstanceOf(LoadingPickService.CompletionProcessingException.class)
.satisfies(error -> assertThat(((LoadingPickService.CompletionProcessingException) error).stage())
.isEqualTo("OWNER_READY"));
verify(settlementService, never()).settleOwner(anyLong(), anyLong(), anyLong(), anyString());
verify(loadingDeliveryService, never()).completeAfterPdaSettlement(anyLong(), anyLong(), anyLong(),
anyLong(), anyLong(), anyString(), anyString(), anyString());
verify(completionCommands).checkpoint(904L, 101L, 50L, 1, "READY");
verify(completionCommands, never()).checkpoint(eq(904L), anyLong(), anyLong(), anyInt(),
eq("SETTLED"));
verify(repository, never()).insert(any(LoadingPickOperationEntity.class));
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
verify(loadingListService, never()).publishCompletedEvent(anyLong());
}
@Test
void workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements() {
LoadingPickTaskEntity completing = task();
@@ -834,9 +893,9 @@ class LoadingPickServiceTest {
when(repository.task(20L)).thenReturn(completing);
when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(101L, 50L),
new LoadingPickItemMapper.InboundOwner(101L, 60L)));
new LoadingPickItemMapper.InboundOwner(202L, 60L)));
when(completionCommands.checkpoint(901L, 101L, 50L)).thenReturn("INBOUND_COMPLETED");
when(completionCommands.checkpoint(901L, 101L, 60L)).thenReturn("SETTLED");
when(completionCommands.checkpoint(901L, 202L, 60L)).thenReturn("SETTLED");
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
when(repository.completeAccepted(20L, 9L, "operator")).thenReturn(1);
@@ -846,8 +905,8 @@ class LoadingPickServiceTest {
verify(settlementService, never()).settleOwner(anyLong(), anyLong(), anyLong(), anyString());
verify(loadingDeliveryService, never()).completeAfterPdaSettlement(eq(101L), eq(50L), anyLong(),
anyLong(), anyString(), anyString(), anyString());
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 60L, 50L, 9L, "operator",
anyLong(), anyLong(), anyString(), anyString(), anyString());
verify(loadingDeliveryService).completeAfterPdaSettlement(202L, 60L, 101L, 50L, 9L, "operator",
"PDA-async-resume-60", "checked");
verify(loadingListService).publishCompletedEvent(10L);
verify(repository).insert(org.mockito.ArgumentMatchers.<LoadingPickOperationEntity>argThat(