From 204ae1073700e5e4e84bbe897c2316c515fe8e41 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 15 Oct 2026 18:12:55 +0800 Subject: [PATCH] fix(dispatch): persist and reuse the resolved draft group --- .../dispatch/service/DispatchService.java | 15 ++++- .../dispatch/service/DispatchServiceTest.java | 57 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java index bfdeb2f36..4103f9045 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java @@ -207,7 +207,12 @@ public class DispatchService { DispatchDraft draft = ensureDraft(operatorId, request.draftToken()); DispatchDraftContainer boundContainer = request.draftContainerId() == null ? null : draftContainers.selectById(request.draftContainerId()); if (boundContainer == null && request.draftContainerId() == null) { - boundContainer = new DispatchDraftContainer(); boundContainer.setId(IdWorker.getId()); boundContainer.setDraftId(draft.getId()); boundContainer.setDraftToken(request.draftToken()); boundContainer.setSequenceNo(1); boundContainer.setCapacity(500); boundContainer.setCreatedAt(LocalDateTime.now()); boundContainer.setUpdatedAt(LocalDateTime.now()); draftContainers.insert(boundContainer); + var existing = draftContainers.findByToken(request.draftToken()); + if (existing.size() > 1) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "存在多个装车分组,请明确选择目标分组"); + if (existing.size() == 1) boundContainer = existing.get(0); + else { + boundContainer = new DispatchDraftContainer(); boundContainer.setId(IdWorker.getId()); boundContainer.setDraftId(draft.getId()); boundContainer.setDraftToken(request.draftToken()); boundContainer.setSequenceNo(1); boundContainer.setCapacity(500); boundContainer.setCreatedAt(LocalDateTime.now()); boundContainer.setUpdatedAt(LocalDateTime.now()); draftContainers.insert(boundContainer); + } } if (boundContainer == null || !request.draftToken().equals(boundContainer.getDraftToken()) || !Objects.equals(boundContainer.getDraftId(), draft.getId())) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "装车托盘不存在"); DispatchPalletCandidate candidate = requireDraftSource(customerId, request); @@ -219,7 +224,11 @@ public class DispatchService { } if (active != null) { if (request.draftToken().equals(active.getDraftToken()) - && Objects.equals(trimToNull(operatorId), active.getOperatorId())) return; + && Objects.equals(trimToNull(operatorId), active.getOperatorId())) { + if (!Objects.equals(boundContainer.getId(), active.getDraftContainerId())) + throw new ResponseStatusException(HttpStatus.CONFLICT, "托盘已锁定到其他装车分组,请先移除后重新选择"); + return; + } throw new ResponseStatusException(HttpStatus.CONFLICT, "Pallet is already locked by another dispatch draft: " + candidate.palletNo()); } @@ -235,7 +244,7 @@ public class DispatchService { lock.setReturnReceiptId(candidate.returnReceiptId()); lock.setWarehousePalletId(candidate.warehousePalletId()); lock.setDraftToken(request.draftToken()); - lock.setDraftContainerId(request.draftContainerId()); + lock.setDraftContainerId(boundContainer.getId()); lock.setOperatorId(trimToNull(operatorId)); lock.setStatus("ACTIVE"); lock.setLockedAt(now); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java index e0ac85bfc..7367f9a75 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java @@ -160,6 +160,10 @@ class DispatchServiceTest { verify(locks).insert(lock.capture()); assertEquals("draft-1", lock.getValue().getDraftToken()); assertEquals("7", lock.getValue().getOperatorId()); + ArgumentCaptor group = ArgumentCaptor.forClass(DispatchDraftContainer.class); + verify(draftContainers).insert(group.capture()); + assertNotNull(group.getValue().getId()); + assertEquals(group.getValue().getId(), lock.getValue().getDraftContainerId()); assertNotNull(lock.getValue().getExpiresAt()); assertEquals(null, lock.getValue().getDispatchOrderId()); verify(locks).clearInactiveActiveKey("100", "PLAN:10"); @@ -175,6 +179,59 @@ class DispatchServiceTest { verify(locks).insert(any(DispatchPalletLock.class)); } + private DispatchDraftContainer existingDraftGroup() { + DispatchDraft draft = new DispatchDraft(); + draft.setId(20L); draft.setDraftToken("draft-1"); draft.setOperatorId("7"); + when(drafts.lockActive("draft-1", "7")).thenReturn(draft); + DispatchDraftContainer group = new DispatchDraftContainer(); + group.setId(21L); group.setDraftId(20L); group.setDraftToken("draft-1"); + return group; + } + + @Test void omittedGroupReusesTheOnlyGroupAndPersistsItsIdentity() { + var group = existingDraftGroup(); + when(draftContainers.findByToken("draft-1")).thenReturn(List.of(group)); + when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8"))); + service.lockDraftPallet("100", "7", new DispatchRequests.DraftPallet(10L, "draft-1")); + var lock = ArgumentCaptor.forClass(DispatchPalletLock.class); + verify(locks).insert(lock.capture()); + assertEquals(21L, lock.getValue().getDraftContainerId()); + verify(draftContainers, never()).insert(any(DispatchDraftContainer.class)); + } + + @Test void omittedGroupReplayDoesNotCreateAnotherGroupOrLock() { + var group = existingDraftGroup(); + when(draftContainers.findByToken("draft-1")).thenReturn(List.of(group)); + when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8"))); + var lock = new DispatchPalletLock(); lock.setDraftToken("draft-1"); lock.setOperatorId("7"); lock.setDraftContainerId(21L); + when(locks.lockActive("100", "PLAN:10")).thenReturn(lock); + service.lockDraftPallet("100", "7", new DispatchRequests.DraftPallet(10L, "draft-1")); + verify(draftContainers, never()).insert(any(DispatchDraftContainer.class)); + verify(locks, never()).insert(any(DispatchPalletLock.class)); + } + + @Test void multipleGroupsRequireExplicitSelectionWithoutCreatingAnotherGroup() { + var group = existingDraftGroup(); + var second = new DispatchDraftContainer(); second.setId(22L); + when(draftContainers.findByToken("draft-1")).thenReturn(List.of(group, second)); + assertEquals(400, assertThrows(ResponseStatusException.class, () -> + service.lockDraftPallet("100", "7", new DispatchRequests.DraftPallet(10L, "draft-1"))).getStatusCode().value()); + verify(draftContainers, never()).insert(any(DispatchDraftContainer.class)); + verify(locks, never()).insert(any(DispatchPalletLock.class)); + } + + @Test void replayCannotSilentlyMoveAPalletToAnotherGroup() { + var group = existingDraftGroup(); + when(draftContainers.selectById(21L)).thenReturn(group); + when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8"))); + var lock = new DispatchPalletLock(); lock.setDraftToken("draft-1"); lock.setOperatorId("7"); lock.setDraftContainerId(22L); + when(locks.lockActive("100", "PLAN:10")).thenReturn(lock); + assertEquals(409, assertThrows(ResponseStatusException.class, () -> + service.lockDraftPallet("100", "7", new DispatchRequests.DraftPallet(10L, "draft-1", 21L))).getStatusCode().value()); + assertEquals(22L, lock.getDraftContainerId()); + verify(locks, never()).insert(any(DispatchPalletLock.class)); + } + @Test void preArrangeAcceptsVolumeOnlyPlanPallet() { when(candidates.lockSelected("100", List.of(10L)))