diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java index 5c4c39f3a..f7669d5f6 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java @@ -12,9 +12,11 @@ import java.util.List; @Mapper public interface InboundPlanOrderMapper { - record ContainerOrderRef(String customerId, Long orderId, String currentStatus, String containerNo) { } + record ContainerOrderRef(String customerId, Long orderId, String currentStatus, String containerNo, + String orderType) { } - @Select("SELECT customer_id AS customerId,id AS orderId,current_status AS currentStatus,container_no AS containerNo " + + @Select("SELECT customer_id AS customerId,id AS orderId,current_status AS currentStatus,container_no AS containerNo," + + "order_type AS orderType " + "FROM ll_order WHERE source_container_booking_id=#{sourceContainerBookingId} ORDER BY customer_id,id FOR UPDATE") List lockContainerOrders(@Param("sourceContainerBookingId") String sourceContainerBookingId); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java index 513c59353..ee312d51f 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java @@ -20,6 +20,8 @@ import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapper; import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot; import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper; +import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot; +import com.baiyue.lastleg.dropship.service.DropshipOrderService; import com.baiyue.common.event.ContainerNumbers; import jakarta.annotation.Resource; import org.springframework.http.HttpStatus; @@ -47,6 +49,7 @@ public class InboundPlanService { @Resource private InboundPlanOrderMapper orderMapper; @Resource private InboundPlanQueryMapper queryMapper; @Resource private InventoryBalanceMapper inventoryBalanceMapper; + @Resource private DropshipOrderService dropshipOrders; @Transactional(readOnly = true) public PageResult page(String customerId, InboundPlanRequests.Query query) { @@ -206,6 +209,7 @@ public class InboundPlanService { } List> snapshots = new ArrayList<>(refs.size()); + List> wholePallets = new ArrayList<>(refs.size()); for (InboundPlanOrderMapper.ContainerOrderRef ref : refs) { List orders = orderMapper.lockOrder(ref.customerId(), ref.orderId()); if (orders == null || orders.isEmpty()) { @@ -217,16 +221,33 @@ public class InboundPlanService { throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜内尾程订单柜号不一致"); } snapshots.add(orders); + List sourcePallets = "ONE_CLICK_DROPSHIP".equals(ref.orderType()) + ? dropshipOrders.palletSnapshots(ref.customerId(), ref.orderId()).stream() + .map(this::wholePallet).toList() + : null; + if (sourcePallets != null) { + if (sourcePallets.isEmpty()) { + throw new ResponseStatusException(HttpStatus.CONFLICT, + "一件代发订单缺少不可变来源托盘快照"); + } + validateWholePallets(orders, sourcePallets); + } + wholePallets.add(sourcePallets); } List created = new ArrayList<>(refs.size()); for (int index = 0; index < refs.size(); index++) { InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(index); - created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index), List.of(), null)); + created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index), List.of(), + wholePallets.get(index))); } return List.copyOf(created); } + private WholePalletInput wholePallet(DropshipPalletSnapshot pallet) { + return new WholePalletInput(pallet.sourcePalletNo(), Math.toIntExact(pallet.packageCount())); + } + private String requiredContainerNo(String value) { try { return ContainerNumbers.requireCanonical(value); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanServiceTest.java index 6fa7f21bd..1e52b7615 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanServiceTest.java @@ -13,6 +13,8 @@ import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper; import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot; import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper; +import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot; +import com.baiyue.lastleg.dropship.service.DropshipOrderService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -44,6 +46,7 @@ class InboundPlanServiceTest { @Mock InboundPlanPalletAllocationMapper allocationMapper; @Mock InboundPlanOrderMapper orderMapper; @Mock InventoryBalanceMapper inventoryBalanceMapper; + @Mock DropshipOrderService dropshipOrders; private InboundPlanService service; @@ -56,6 +59,7 @@ class InboundPlanServiceTest { ReflectionTestUtils.setField(service, "allocationMapper", allocationMapper); ReflectionTestUtils.setField(service, "orderMapper", orderMapper); ReflectionTestUtils.setField(service, "inventoryBalanceMapper", inventoryBalanceMapper); + ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders); ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator()); org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of()); org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of()); @@ -278,8 +282,8 @@ class InboundPlanServiceTest { assertNotNull(InboundPlanService.class.getMethod("convertContainer", String.class) .getAnnotation(Transactional.class)); when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of( - new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870"), - new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "DESTINATION_ASSIGNED", "MSCU6639870"))); + new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"), + new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "DESTINATION_ASSIGNED", "MSCU6639870", "STANDARD"))); when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order("100", 1L, "RK-1", "WH-A"))); when(orderMapper.lockOrder("200", 2L)).thenReturn(List.of(order("200", 2L, "RK-2", "WH-B"))); when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any())) @@ -296,11 +300,36 @@ class InboundPlanServiceTest { verify(orderMapper).lockOrder("200", 2L); } + @Test + void containerConversionPreservesDropshipSourcePallets() { + when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of( + new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", + "MSCU6639870", "ONE_CLICK_DROPSHIP"))); + when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1.0000", "WH-A"))); + when(dropshipOrders.palletSnapshots("100", 1L)).thenReturn(List.of( + new DropshipPalletSnapshot("SOURCE-P-1", 3, List.of()), + new DropshipPalletSnapshot("SOURCE-P-2", 7, List.of()))); + when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any())) + .thenReturn(1); + + service.convertContainer("BOOKING-1"); + + ArgumentCaptor pallets = ArgumentCaptor.forClass(InboundPlanPallet.class); + verify(palletMapper, times(2)).insert(pallets.capture()); + assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"), + pallets.getAllValues().stream().map(InboundPlanPallet::getPalletNo).toList()); + ArgumentCaptor allocations = + ArgumentCaptor.forClass(InboundPlanPalletAllocation.class); + verify(allocationMapper, times(2)).insert(allocations.capture()); + assertEquals(List.of(3, 7), allocations.getAllValues().stream() + .map(InboundPlanPalletAllocation::getPlannedPackageCount).toList()); + } + @Test void invalidLaterOrderCausesNoWritesBecauseWholeContainerIsPrevalidated() { when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of( - new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870"), - new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870"))); + new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"), + new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"))); when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A"))); when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2", "PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0))); @@ -315,8 +344,8 @@ class InboundPlanServiceTest { @Test void completeContainerReplayReturnsStableExistingPlansWithoutWriting() { var refs = List.of( - new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870"), - new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_COMPLETED", "MSCU6639870")); + new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"), + new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_COMPLETED", "MSCU6639870", "STANDARD")); when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(refs); when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1")); when(planMapper.findBySourceOrder("200", 2L)).thenReturn(existingPlan("200", 2L, "LIP-2")); @@ -331,8 +360,8 @@ class InboundPlanServiceTest { @Test void partialExistingPlansAreRejectedBeforeAnyWrite() { when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of( - new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870"), - new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870"))); + new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"), + new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"))); when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1")); assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1")); @@ -344,8 +373,8 @@ class InboundPlanServiceTest { @Test void rejectsContainerDriftAcrossCustomersBeforeWritingAnyPlan() { when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of( - new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870"), - new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "PENDING_ASSIGNMENT", "CSQU3054383"))); + new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"), + new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "PENDING_ASSIGNMENT", "CSQU3054383", "STANDARD"))); ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1")); @@ -358,8 +387,8 @@ class InboundPlanServiceTest { @Test void rejectsReplayWhenExistingPlanContainerSnapshotHasDrifted() { when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of( - new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870"), - new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_PLANNED", "MSCU6639870"))); + new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"), + new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"))); InboundPlan first = existingPlan("100", 1L, "LIP-1"); InboundPlan drifted = existingPlan("200", 2L, "LIP-2"); drifted.setContainerNo("CSQU3054383");