From e5832d5c670d83bcb90eaccb1d7a64c2d1a3e033 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 30 Sep 2026 11:03:47 +0800 Subject: [PATCH] fix(firstleg): refresh inbound picking progress after task completion --- .../firstleg/service/LoadingPickService.java | 2 +- .../service/LoadingPickServiceTest.java | 35 +++++++++++++++++++ ...e3-loading-dynamic-final-contract.test.mjs | 2 ++ .../phase3-loading-finalize.mjs | 6 ++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java index 9af36a61d..c4c5d2571 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java @@ -690,7 +690,6 @@ public class LoadingPickService { } runCompletionStage("PLAN_COMPLETE", () -> loadingListService.finishFromPda(task.getLoadingPlanId(), taskId, command.getOperatorId(), command.getOperatorName())); - repository.syncInboundProjections(taskId, command.getOperatorId(), command.getOperatorName()); runCompletionStage("COMPLETED_EVENT", () -> loadingListService.publishCompletedEvent(task.getLoadingPlanId())); for (GroupSnapshot group : taskGroups) { runCompletionStage("COLOAD_CONSUME", () -> coloadGroups.consumeWholeGroup(group.groupId(), @@ -703,6 +702,7 @@ public class LoadingPickService { && repository.completeAccepted(taskId, command.getOperatorId(), command.getOperatorName()) != 1) { throw new CompletionProcessingException("TASK_FINALIZE", conflict("PDA任务完成状态已变化")); } + repository.syncInboundProjections(taskId, command.getOperatorId(), command.getOperatorName()); if (!completionCommands.complete(command.getId(), worker)) { throw new CompletionProcessingException("COMMAND_FINALIZE", conflict("装柜完成命令租约已变化")); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java index 8597ea9d6..448c86838 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java @@ -1060,6 +1060,41 @@ class LoadingPickServiceTest { verify(completionCommands).complete(900L, "worker-1"); } + @ParameterizedTest + @ValueSource(strings = {"COMPLETING", "COMPLETED"}) + void workerRefreshesInboundProgressAfterTaskCompletionIncludingRetry(String initialTaskStatus) { + LoadingPickTaskEntity task = task(); + task.setTaskStatus(initialTaskStatus); + when(repository.task(20L)).thenReturn(task); + when(repository.inboundOwners(20L)).thenReturn(List.of( + 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")).thenAnswer(invocation -> { + task.setTaskStatus("COMPLETED"); + return 1; + }); + AtomicReference projectedStatus = new AtomicReference<>(); + doAnswer(invocation -> { + projectedStatus.set("COMPLETED".equals(task.getTaskStatus()) ? "COMPLETED" : "IN_PROGRESS"); + return null; + }).when(repository).syncInboundProjections(20L, 9L, "operator"); + when(completionCommands.complete(925L, "worker-projection")).thenReturn(true); + + service.processCompletion(completionCommand(925L, "async-projection"), "worker-projection"); + + assertThat(projectedStatus.get()).isEqualTo("COMPLETED"); + verify(repository, times(1)).syncInboundProjections(20L, 9L, "operator"); + if ("COMPLETING".equals(initialTaskStatus)) { + InOrder finalization = inOrder(repository, completionCommands); + finalization.verify(repository).completeAccepted(20L, 9L, "operator"); + finalization.verify(repository).syncInboundProjections(20L, 9L, "operator"); + finalization.verify(completionCommands).complete(925L, "worker-projection"); + } else { + verify(repository, never()).completeAccepted(anyLong(), anyLong(), anyString()); + } + } + @ParameterizedTest(name = "truck state {0} never participates in PDA completion") @ValueSource(strings = {"NO_APPOINTMENT", "DRAFT", "IN_PROGRESS", "COMPLETED"}) void completionWorkerIsIndependentOfEveryTruckAppointmentState(String truckState) { diff --git a/scripts/admin-test-env-modules/phase3-loading-dynamic-final-contract.test.mjs b/scripts/admin-test-env-modules/phase3-loading-dynamic-final-contract.test.mjs index 024329ed1..b731ac364 100644 --- a/scripts/admin-test-env-modules/phase3-loading-dynamic-final-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-loading-dynamic-final-contract.test.mjs @@ -121,6 +121,8 @@ test('HTTP chain refreshes after stale inventory and keeps one stable PDA identi }) test('downstream finalizer uses the frozen loading complete contract without truck coupling', () => { + assert.match(finalize, /unifiedDetail\?\.summary\?\.loading\?\.status === 'COMPLETED'/) + assert.match(finalize, /unifiedRow\?\.loading\?\.status === 'COMPLETED'/) assert.match(convergence, /loadingPlan\?\.status === 'LOADING'/) assert.match(convergence, /!operations\.some\(\(item\) => item\.actionType === 'COMPLETE_LOADING'\)/) assert.match(customs, /\['AWAITING_REVIEW', 'LOADED'\]/) diff --git a/scripts/admin-test-env-modules/phase3-loading-finalize.mjs b/scripts/admin-test-env-modules/phase3-loading-finalize.mjs index cc38656b6..2456d464c 100644 --- a/scripts/admin-test-env-modules/phase3-loading-finalize.mjs +++ b/scripts/admin-test-env-modules/phase3-loading-finalize.mjs @@ -429,6 +429,9 @@ export async function run(context) { id(unifiedDetail?.summary?.orderId) === orderId && unifiedDetail?.summary?.mainStatus === 'LOADED' && Boolean(orderNo), `orderId=${id(unifiedDetail?.summary?.orderId) || 'missing'}, mainStatus=${unifiedDetail?.summary?.mainStatus || 'missing'}, orderNo=${orderNo || 'missing'}`, OPTIONS) + context.assert(MODULE, '装柜完成后订单详情拣货汇总同步完成', unifiedDetailResponse, + unifiedDetail?.summary?.loading?.status === 'COMPLETED', + `loadingStatus=${unifiedDetail?.summary?.loading?.status || 'missing'}`, OPTIONS) if (id(unifiedDetail?.summary?.orderId) !== orderId || unifiedDetail?.summary?.mainStatus !== 'LOADED' || !orderNo) return const unifiedListPath = `/api/platform/v1/orders?page=1&size=100&tenantId=${encodeURIComponent(tenantId)}&orderNo=${encodeURIComponent(orderNo)}` const unifiedListResponse = await context.request('GET', unifiedListPath) @@ -437,6 +440,9 @@ export async function run(context) { context.assert(MODULE, '统一订单列表投影同一订单为 LOADED', unifiedListResponse, Boolean(unifiedRow) && unifiedRow?.mainStatus === 'LOADED', `orderId=${orderId}, found=${Boolean(unifiedRow)}, mainStatus=${unifiedRow?.mainStatus || 'missing'}`, OPTIONS) + context.assert(MODULE, '装柜完成后订单列表拣货汇总同步完成', unifiedListResponse, + unifiedRow?.loading?.status === 'COMPLETED', + `loadingStatus=${unifiedRow?.loading?.status || 'missing'}`, OPTIONS) const evidencePath = `/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(id(fixture.canonicalPdaTaskId))}/evidence?warehouseId=${encodeURIComponent(id(fixture.warehouseId))}` const immutableEvidenceResponse = await context.request('GET', evidencePath)