From 1574263836d6a5781a95b437646310d2ea4072aa Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 2 Sep 2026 12:09:14 +0800 Subject: [PATCH] fix(lastleg): validate dropship SKU for PDA outbound --- .../service/DeliveryTransferService.java | 46 +++++++++++++------ .../service/DeliveryTransferServiceTest.java | 46 +++++++++++++++++++ 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java index 84fa93aa8..59f1001b9 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java @@ -217,9 +217,7 @@ public class DeliveryTransferService { OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim()); if (existing != null) return taskResult(existing, pdaTasks.lockByOutbound(customerId, existing.getId())); LocalDateTime now = LocalDateTime.now(); - InventoryPosition firstPosition = inventoryPositions.selectById(request.items().get(0).inventoryPositionId()); - if (firstPosition == null || !customerId.equals(firstPosition.getCustomerId())) throw notFound("Inventory position not found"); - String selectedWarehouseId = firstPosition.getOverseasWarehouseId(); + String selectedWarehouseId = validateOneClickItems(customerId, request.items()); OutboundOrder outbound = new OutboundOrder(); outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId); outbound.setOutboundNo(orderNoGenerator.next("ONE_CLICK_DROPSHIP", selectedWarehouseId, outbound.getId())); @@ -230,11 +228,6 @@ public class DeliveryTransferService { for (DeliveryRequests.OneClickItem input : request.items()) { InventoryPosition position = inventoryPositions.selectById(input.inventoryPositionId()); if (position == null || !customerId.equals(position.getCustomerId())) throw notFound("Inventory position not found"); - DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, position.getId()); - if (profile == null) throw conflict("Selected pallet is not one-click dropship inventory"); - if (selectedWarehouseId == null) selectedWarehouseId = position.getOverseasWarehouseId(); - else if (!Objects.equals(selectedWarehouseId, position.getOverseasWarehouseId())) throw conflict("One outbound order must use inventory from the same overseas warehouse"); - if (value(position.getAvailablePackages()) < input.packages()) throw conflict("Insufficient available inventory for pallet " + position.getPalletNo()); OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId()); item.setCustomerId(customerId); item.setOutboundOrderId(outbound.getId()); item.setWarehousePalletId(position.getPalletId()); item.setPalletNo(position.getPalletNo()); item.setPackageCount(input.packages().intValue()); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK"); item.setSkuCode(input.sku().trim()); item.setCreatedAt(now); outboundPallets.insert(item); @@ -245,6 +238,37 @@ public class DeliveryTransferService { return taskResult(outbound, task); } + private String validateOneClickItems(String customerId, List items) { + String warehouseId = null; + Map requestedPackages = new LinkedHashMap<>(); + Map profiles = new HashMap<>(); + for (DeliveryRequests.OneClickItem input : items) { + InventoryPosition position = inventoryPositions.selectById(input.inventoryPositionId()); + if (position == null || !customerId.equals(position.getCustomerId())) throw notFound("Inventory position not found"); + DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, position.getId()); + if (profile == null) throw conflict("Selected pallet is not one-click dropship inventory"); + if (warehouseId == null) warehouseId = position.getOverseasWarehouseId(); + else if (!Objects.equals(warehouseId, position.getOverseasWarehouseId())) { + throw conflict("One outbound order must use inventory from the same overseas warehouse"); + } + if (value(position.getAvailablePackages()) < input.packages()) { + throw conflict("Insufficient available inventory for pallet " + position.getPalletNo()); + } + String key = profile.getId() + "\u0000" + input.sku().trim(); + requestedPackages.merge(key, input.packages(), Long::sum); + profiles.put(key, profile); + } + for (Map.Entry requested : requestedPackages.entrySet()) { + String sku = requested.getKey().substring(requested.getKey().indexOf('\u0000') + 1); + List cartons = dropshipCartons.lockAvailable(customerId, + profiles.get(requested.getKey()).getId(), sku, requested.getValue()); + if (cartons.size() != requested.getValue()) { + throw conflict("Insufficient available one-click dropship cartons for SKU " + sku); + } + } + return warehouseId; + } + @Transactional public DeliveryResponses.PdaTask pdaPick(String warehouseId, String operatorId, String taskNo, DeliveryRequests.PdaPick request) { @@ -278,10 +302,6 @@ public class DeliveryTransferService { if (!warehouseId.equals(selectedPosition.getOverseasWarehouseId())) { throw conflict("Scanned inventory position does not belong to the bound warehouse"); } - InventoryBalance selectedBalance = inventoryBalances.selectById(selectedPosition.getBalanceId()); - if (!matchesOutboundItem(customerId, warehouseId, item, selectedBalance)) { - throw conflict("Scanned inventory position does not belong to the outbound item"); - } DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, request.inventoryPositionId()); if (profile == null) throw conflict("Scanned pallet is not one-click dropship inventory"); List cartons = dropshipCartons.lockAvailable(customerId, profile.getId(), item.getSkuCode(), request.packages()); @@ -395,12 +415,12 @@ public class DeliveryTransferService { long available = value(position.getAvailablePackages()); if (available <= 0 || !warehouseId.equals(position.getOverseasWarehouseId())) continue; InventoryBalance balance = inventoryBalances.selectById(position.getBalanceId()); - if (!matchesOutboundItem(customerId, warehouseId, item, balance)) continue; DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, position.getId()); List marks = profile == null ? List.of() : dropshipCartons.listProfile(customerId, profile.getId()) .stream().filter(carton -> "AVAILABLE".equals(carton.getStatus())) .filter(carton -> Objects.equals(item.getSkuCode(), carton.getSkuCode())) .map(DropshipCarton::getCartonMark).filter(StringUtils::hasText).distinct().limit(available).toList(); + if (!matchesOutboundItem(customerId, warehouseId, item, balance) && marks.isEmpty()) continue; if (marks.isEmpty()) { result.add(pdaPosition(position, item, balance, null, available, value(position.getPickedPackages()))); } else { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java index c0c0aeb6b..be897d926 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java @@ -356,6 +356,24 @@ class DeliveryTransferServiceTest { verify(outboundPallets, never()).findByOutbound(any(), any()); } + @Test + void oneClickOutboundRejectsSummarySkuWithoutAvailableDropshipCartons() { + InventoryPosition position = new InventoryPosition(); + position.setId(901L); position.setCustomerId("TENANT-A"); position.setOverseasWarehouseId("WH-1"); + position.setPalletId(501L); position.setPalletNo("PLT-501"); position.setAvailablePackages(1L); + DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(1001L); + when(inventoryPositions.selectById(901L)).thenReturn(position); + when(dropshipProfiles.findPosition("TENANT-A", 901L)).thenReturn(profile); + when(dropshipCartons.lockAvailable("TENANT-A", 1001L, "MIXED", 1L)).thenReturn(List.of()); + + ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> service.createOneClick( + "TENANT-A", "U-1", new DeliveryRequests.OneClickOutbound("recipient", List.of( + new DeliveryRequests.OneClickItem(901L, 1L, "MIXED")), null, "one-click-mixed"))); + + assertEquals(409, exception.getStatusCode().value()); + verify(outboundOrders, never()).insert(any(OutboundOrder.class)); + } + @Test void outboundPdaListAndDetailUseOnlyBoundWarehouse() throws Exception { assertNotNull(DeliveryTransferService.class.getMethod("pdaPick", String.class, String.class, @@ -415,6 +433,34 @@ class DeliveryTransferServiceTest { assertEquals(1L, projected.availablePackages()); } + @Test + void outboundPdaDetailUsesCartonMarksWhenInventoryBalanceIsMixed() { + OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key"); + outbound.setOverseasWarehouseId("WH-1"); + OutboundPdaTask task = new OutboundPdaTask(); + task.setId(701L); task.setCustomerId("TENANT-A"); task.setOutboundOrderId(301L); task.setTaskNo("PDA-701"); + OutboundPalletItem item = pallet(501L, "PLT-501", "ONT8", "Ontario", null, 1); + item.setSkuCode("SKU-1"); + InventoryPosition position = new InventoryPosition(); + position.setId(901L); position.setCustomerId("TENANT-A"); position.setBalanceId(801L); + position.setOverseasWarehouseId("WH-1"); position.setPalletId(501L); position.setPalletNo("PLT-501"); position.setAvailablePackages(1L); + InventoryBalance balance = new InventoryBalance(); + balance.setId(801L); balance.setCustomerId("TENANT-A"); balance.setOverseasWarehouseId("WH-1"); balance.setSkuCode("MIXED"); + DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(1001L); + DropshipCarton carton = new DropshipCarton(); carton.setStatus("AVAILABLE"); carton.setSkuCode("SKU-1"); carton.setCartonMark("CTN-A"); + when(pdaTasks.findByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task); + when(outboundOrders.selectById(301L)).thenReturn(outbound); + when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item)); + when(inventoryPositions.listPallet("TENANT-A", 501L)).thenReturn(List.of(position)); + when(inventoryBalances.selectById(801L)).thenReturn(balance); + when(dropshipProfiles.findPosition("TENANT-A", 901L)).thenReturn(profile); + when(dropshipCartons.listProfile("TENANT-A", 1001L)).thenReturn(List.of(carton)); + + DeliveryResponses.PdaTaskDetail detail = service.pdaTaskDetail("WH-1", "PDA-701"); + + assertEquals("CTN-A", detail.items().get(0).positions().get(0).cartonMark()); + } + @Test void outboundPdaPickReplayDoesNotDeductInventoryAgainAndRejectsChangedRequest() { OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key");