From 358feb55673ad02ac62198a5688c6b1dd4ecbcb7 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 16 Oct 2026 13:57:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TransferReturnSourceRepository.java | 38 +++++++++ .../service/WarehouseTransferService.java | 80 +++++++++++++++---- .../service/TransferReturnSourceTest.java | 60 ++++++++++++++ .../service/WarehouseTransferServiceTest.java | 2 + 4 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferReturnSourceRepository.java create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/TransferReturnSourceTest.java diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferReturnSourceRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferReturnSourceRepository.java new file mode 100644 index 000000000..7a7be18df --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferReturnSourceRepository.java @@ -0,0 +1,38 @@ +package com.baiyue.lastleg.transfer.repository; + +import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton; +import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem; +import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** 封装仓储转运所需的退货来源查询,保留客户和操作仓隔离。 */ +@Repository +public class TransferReturnSourceRepository { + @Resource private ReturnOrderMapper orders; + @Resource private ReturnInventoryCartonMapper cartons; + @Resource private ReturnInventoryCartonItemMapper items; + + /** 按当前客户和退货单号查询来源单据。 */ + public ReturnOrder findOrder(String customerId, String returnNo) { + return orders.findNo(customerId, returnNo); + } + + /** 查询当前仓内已完成清点且可用的退货箱,不影响其他页面的历史库存查询。 */ + public List availableCartons(String customerId, String warehouseId, String returnNo) { + return cartons.currentReturnCartons(customerId, warehouseId, returnNo).stream() + .filter(carton -> "AVAILABLE".equals(carton.getStatus()) && "CONFIRMED".equals(carton.getCountStatus()) + && carton.getRelabelApplicationId() == null) + .toList(); + } + + /** 批量读取已通过客户及仓库隔离校验的来源箱明细。 */ + public List cartonItems(List cartonIds) { + return cartonIds.isEmpty() ? List.of() : items.listCartons(cartonIds); + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java index f9536306e..19431bedb 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java @@ -59,6 +59,10 @@ import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletMapper; import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletItemMapper; import com.baiyue.lastleg.transfer.mapper.WarehouseTransferRelabelBindingMapper; import com.baiyue.lastleg.transfer.mapper.WarehouseTransferMeasurementPhotoMapper; +import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton; +import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem; +import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder; +import com.baiyue.lastleg.transfer.repository.TransferReturnSourceRepository; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.Resource; import org.springframework.http.HttpStatus; @@ -88,6 +92,8 @@ import java.util.stream.Collectors; @Service public class WarehouseTransferService { + /** 退货单在转运来源中的展示编码,避免把退货单号误当成普通入仓码查询。 */ + public static final String RETURN_ORDER_PREFIX = "RETURN_ORDER:"; @Resource private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers; @Resource private com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory customerDirectory; @Resource private com.baiyue.lastleg.fleet.business.FleetPoolOrderBridge fleetPool; @@ -140,10 +146,32 @@ public class WarehouseTransferService { @Resource private InventoryCommandService inventory; @Resource private InventoryCommandMapper inventoryCommands; @Resource private ObjectMapper objectMapper; + @Resource private TransferReturnSourceRepository returnSources; + /** 查询来源单据当前可用的箱唛及箱内 SKU,兼容普通入仓与退货库存。 */ public List sourceCartons(String customerId, String warehouseId, String inboundCode) { if (!StringUtils.hasText(warehouseId) || !StringUtils.hasText(inboundCode)) throw badRequest("请选择海外仓和库存来源单据"); - return inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), inboundCode.trim()).stream().map(row -> { + String source = inboundCode.trim(); + if (isReturnOrderSource(source)) { + String returnNo = returnNoFromSource(source); + ReturnOrder order = returnSources.findOrder(customerId, returnNo); + if (order == null || !warehouseId.trim().equals(order.getOverseasWarehouseId())) { + throw notFound("退货单不存在或不属于当前操作仓"); + } + List cartons = returnSources.availableCartons(customerId, warehouseId.trim(), returnNo); + List cartonIds = cartons.stream().map(ReturnInventoryCarton::getId).filter(Objects::nonNull).toList(); + Map> items = cartonIds.isEmpty() ? Map.of() + : returnSources.cartonItems(cartonIds).stream().collect(Collectors.groupingBy(ReturnInventoryCartonItem::getCartonId)); + return cartons.stream().map(carton -> { + List rows = items.getOrDefault(carton.getId(), List.of()); + List contents = rows.stream() + .filter(item -> item.getQuantity() != null && item.getQuantity() > 0) + .map(item -> new WarehouseTransferResponses.SourceCartonSku(item.getSkuCode(), item.getQuantity())).toList(); + return new WarehouseTransferResponses.SourceCarton(carton.getCartonMark(), null, carton.getTotalQuantity(), + carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), contents); + }).toList(); + } + return inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), source).stream().map(row -> { var carton = new WarehouseTransferCarton(); carton.setSkuItemsJson(row.skuItemsJson()); var contents = cartonContents(carton).stream().map(item -> new WarehouseTransferResponses.SourceCartonSku(item.skuCode(),item.quantity())).toList(); if (contents.isEmpty()) contents = List.of(new WarehouseTransferResponses.SourceCartonSku(row.skuCode(),row.quantity())); @@ -161,6 +189,7 @@ public class WarehouseTransferService { return StringUtils.hasText(request.inboundCode()) ? "" : request.deliveryAddress().trim(); } + /** 创建转运草稿并关联来源单据,退货来源沿其关联尾程订单核对客户归属。 */ @Transactional public WarehouseTransferResponses.Detail create(String customerId, String operatorId, WarehouseTransferRequests.Create request) { @@ -203,18 +232,35 @@ public class WarehouseTransferService { List selectedOrders; if (existingInventoryTransfer) { inboundCode = requestedInboundCode; - inboundOrderId = inboundSnapshots.findId(customerId, inboundCode); - if (inboundOrderId == null) throw notFound("Inbound order was not found for inboundCode " + inboundCode); + if (isReturnOrderSource(inboundCode)) { + ReturnOrder returnOrder = returnSources.findOrder(customerId, returnNoFromSource(inboundCode)); + if (returnOrder == null || !request.operatingWarehouseId().trim().equals(returnOrder.getOverseasWarehouseId()) + || returnOrder.getLastlegOrderId() == null) { + throw notFound("退货单不存在、操作仓不匹配或未关联尾程订单"); + } + LastlegOrder returnOrderTail = orders.selectById(returnOrder.getLastlegOrderId()); + if (returnOrderTail == null || !customerId.equals(returnOrderTail.getCustomerId())) { + throw notFound("退货单关联的尾程订单不存在"); + } + inboundOrderId = StringUtils.hasText(returnOrderTail.getSourceInboundId()) + ? inboundSnapshots.findBySourceInboundId(customerId, returnOrderTail.getSourceInboundId()) + : inboundSnapshots.findId(customerId, returnOrderTail.getInboundCode()); + if (inboundOrderId == null) throw notFound("退货单关联的入仓快照不存在"); + selectedOrders = List.of(returnOrderTail); + } else { + inboundOrderId = inboundSnapshots.findId(customerId, inboundCode); + if (inboundOrderId == null) throw notFound("Inbound order was not found for inboundCode " + inboundCode); + selectedOrders = selectedIds.isEmpty() + ? orders.selectList(new LambdaQueryWrapper() + .eq(LastlegOrder::getCustomerId, customerId) + .eq(LastlegOrder::getInboundCode, inboundCode)) + : orders.selectList(new LambdaQueryWrapper() + .eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, selectedIds)); + } // The merchant form may only submit the inventory inbound code. In that // case derive all eligible tail orders for the inbound instead of // rejecting a valid inventory transfer because source IDs were omitted. - selectedOrders = selectedIds.isEmpty() - ? orders.selectList(new LambdaQueryWrapper() - .eq(LastlegOrder::getCustomerId, customerId) - .eq(LastlegOrder::getInboundCode, inboundCode)) - : orders.selectList(new LambdaQueryWrapper() - .eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, selectedIds)); - if (selectedOrders.isEmpty() || (!selectedIds.isEmpty() && selectedOrders.size() != selectedIds.size())) { + if (selectedOrders.isEmpty() || (!selectedIds.isEmpty() && !isReturnOrderSource(inboundCode) && selectedOrders.size() != selectedIds.size())) { throw notFound("One or more selected customer orders do not exist in the current tenant"); } } else { @@ -393,6 +439,7 @@ public class WarehouseTransferService { } @Transactional + /** 提交草稿前重新校验来源箱归属,防止草稿保存后库存已发生变化。 */ public WarehouseTransferResponses.Detail submit(String customerId, String transferNo, String operatorId) { WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo); if (transfer == null) throw notFound("Warehouse transfer order not found"); @@ -401,8 +448,8 @@ public class WarehouseTransferService { if (Boolean.TRUE.equals(transfer.getNeedRelabel())) { Set allowedMarks = !transfer.getTransferNo().equals(transfer.getInboundCode()) - ? inboundSnapshots.sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode()) - .stream().map(InboundSnapshotMapper.SourceCarton::cartonMark).collect(Collectors.toSet()) + ? sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode()) + .stream().map(WarehouseTransferResponses.SourceCarton::cartonMark).collect(Collectors.toSet()) : transferCartons.listOrder(customerId, transfer.getId()).stream() .flatMap(carton -> java.util.stream.Stream.of(carton.getCartonMark(), carton.getCustomerCartonMark())) .filter(StringUtils::hasText).collect(Collectors.toSet()); @@ -1748,6 +1795,7 @@ public class WarehouseTransferService { } } + /** 校验换标资料及箱唛归属,退货来源同样按当前可用箱校验。 */ private void validateRelabelRequest(String customerId, WarehouseTransferRequests.Create request) { if (!request.needRelabel()) { if (request.relabelItems() != null && !request.relabelItems().isEmpty()) { @@ -1757,8 +1805,8 @@ public class WarehouseTransferService { } if (request.relabelItems() == null || request.relabelItems().isEmpty()) return; Set orderCartonMarks = StringUtils.hasText(request.inboundCode()) - ? inboundSnapshots.sourceCartons(customerId, request.operatingWarehouseId().trim(), request.inboundCode().trim()) - .stream().map(InboundSnapshotMapper.SourceCarton::cartonMark) + ? sourceCartons(customerId, request.operatingWarehouseId().trim(), request.inboundCode().trim()) + .stream().map(WarehouseTransferResponses.SourceCarton::cartonMark) .filter(StringUtils::hasText).map(String::trim).collect(Collectors.toSet()) : request.items().stream().flatMap(item -> splitMarks(item.shippingMark()).stream()).collect(Collectors.toSet()); if (!StringUtils.hasText(request.inboundCode()) && orderCartonMarks.isEmpty()) { @@ -2103,6 +2151,10 @@ public class WarehouseTransferService { return auth == null ? null : auth.getName(); } private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; } + /** 根据独立前缀识别退货来源,避免与普通入仓码混淆。 */ + private static boolean isReturnOrderSource(String value) { return StringUtils.hasText(value) && value.trim().startsWith(RETURN_ORDER_PREFIX); } + /** 从转运来源编码提取真实退货单号。 */ + private static String returnNoFromSource(String value) { return value.trim().substring(RETURN_ORDER_PREFIX.length()).trim(); } private static String firstText(String first, String second) { return StringUtils.hasText(first) ? first.trim() : trim(second); } private static long packages(Long value) { return value == null ? 0L : value; } private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/TransferReturnSourceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/TransferReturnSourceTest.java new file mode 100644 index 000000000..f7532f5ff --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/TransferReturnSourceTest.java @@ -0,0 +1,60 @@ +package com.baiyue.lastleg.transfer.service; + +import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton; +import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem; +import com.baiyue.lastleg.returnreceipt.entity.ReturnOrder; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper; +import com.baiyue.lastleg.transfer.repository.TransferReturnSourceRepository; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class TransferReturnSourceTest { + /** 来源查询应排除占用和未清点箱,并保留整箱内的全部 SKU。 */ + @Test + void returnsOnlyAvailableConfirmedCartonsWithContents() { + var orders = mock(ReturnOrderMapper.class); + var cartons = mock(ReturnInventoryCartonMapper.class); + var items = mock(ReturnInventoryCartonItemMapper.class); + var repository = new TransferReturnSourceRepository(); + ReflectionTestUtils.setField(repository, "orders", orders); + ReflectionTestUtils.setField(repository, "cartons", cartons); + ReflectionTestUtils.setField(repository, "items", items); + var service = new WarehouseTransferService(); + ReflectionTestUtils.setField(service, "returnSources", repository); + var order = new ReturnOrder(); + order.setOverseasWarehouseId("WH-1"); + when(orders.findNo("C-1", "RET-1")).thenReturn(order); + var available = carton(1L, "AVAILABLE", "CONFIRMED"); + when(cartons.currentReturnCartons("C-1", "WH-1", "RET-1")).thenReturn(List.of( + available, carton(2L, "OUTBOUND_PENDING", "CONFIRMED"), carton(3L, "AVAILABLE", "PENDING"))); + var sku = new ReturnInventoryCartonItem(); + sku.setCartonId(1L); sku.setSkuCode("SKU-1"); sku.setQuantity(3L); + when(items.listCartons(List.of(1L))).thenReturn(List.of(sku)); + + var result = service.sourceCartons("C-1", "WH-1", "RETURN_ORDER:RET-1"); + + assertEquals(1, result.size()); + assertEquals("BOX-1", result.get(0).cartonMark()); + assertEquals("SKU-1", result.get(0).items().get(0).skuCode()); + assertEquals(3L, result.get(0).items().get(0).quantity()); + assertThrows(ResponseStatusException.class, + () -> service.sourceCartons("C-1", "OTHER-WH", "RETURN_ORDER:RET-1")); + verify(cartons, never()).currentReturnCartons("C-1", "OTHER-WH", "RET-1"); + } + + /** 构造不同库存状态的退货箱以验证来源筛选边界。 */ + private static ReturnInventoryCarton carton(long id, String status, String countStatus) { + var carton = new ReturnInventoryCarton(); + carton.setId(id); carton.setCartonMark("BOX-" + id); carton.setTotalQuantity(3L); + carton.setStatus(status); carton.setCountStatus(countStatus); + return carton; + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java index 65c687e1f..916557722 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java @@ -490,6 +490,8 @@ class WarehouseTransferServiceTest { @Test void submittingOldDraftRechecksRelabelCartonOwnership() { var transfer = transfer(WarehouseTransferService.DRAFT); + transfer.setOperatingWarehouseId("WH-1"); + transfer.setInboundCode("IN-1"); transfer.setNeedRelabel(true); when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer); var binding = new com.baiyue.lastleg.transfer.entity.WarehouseTransferRelabelBinding();