fix(dropship): serialize outbound creation and pick exact carton subsets

This commit is contained in:
2026-09-15 11:42:19 +08:00
parent 88e2fb0eb1
commit 3ebe254c45
10 changed files with 155 additions and 16 deletions
@@ -25,6 +25,7 @@ class DeliverySchemaContractTest {
assertTrue(schema.contains("create table if not exists ll_outbound_order"));
assertTrue(schema.contains("status varchar(32) not null default 'wait_outbound'"));
assertTrue(schema.contains("unique key uk_ll_outbound_idempotency (customer_id,idempotency_key)"));
assertTrue(schema.contains("request_hash char(64) null"));
assertTrue(schema.contains("unique key uk_ll_outbound_dispatch (customer_id,dispatch_order_id)"));
assertTrue(schema.contains("create table if not exists ll_outbound_pallet_item"));
assertTrue(schema.contains("unique key uk_ll_outbound_pallet (customer_id,outbound_order_id,warehouse_pallet_id,inbound_code,sku_code)"));
@@ -66,6 +67,7 @@ class DeliverySchemaContractTest {
StandardCharsets.UTF_8).toLowerCase();
assertTrue(overseas.contains("begin inline empty-database baseline: lastleg"));
assertTrue(overseas.contains(createToken));
assertTrue(overseas.contains("request_hash char(64) null"));
assertTrue(overseas.indexOf(createToken) < overseas.indexOf("assert_phase5_lastleg_identifiers_migratable"));
assertTrue(overseas.contains("alter table ll_delivery_order add column overseas_warehouse_name"));
}
@@ -91,6 +91,7 @@ class DeliveryTransferServiceTest {
@BeforeEach
void setUp() {
service = new DeliveryTransferService();
ReflectionTestUtils.setField(service, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
ReflectionTestUtils.setField(service, "dispatchOrders", dispatchOrders);
ReflectionTestUtils.setField(service, "dispatchItems", dispatchItems);
ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders);
@@ -440,6 +441,7 @@ class DeliveryTransferServiceTest {
.thenReturn(List.of(new DropshipCarton()));
InventoryPosition position = new InventoryPosition();
position.setId(900L + i); position.setCustomerId("TENANT-A"); position.setBalanceId(800L + i);
position.setOverseasWarehouseId("WH-1");
position.setPalletId(501L); position.setPalletNo("DP-SHARED");
InventoryBalance balance = new InventoryBalance();
balance.setCustomerId("TENANT-A"); balance.setInboundCode("IN-" + i);
@@ -460,6 +462,10 @@ class DeliveryTransferServiceTest {
@Test
void oneClickOutboundRejectsSummarySkuWithoutAvailableDropshipCartons() {
var generator = org.mockito.Mockito.mock(LastlegOrderNoGenerator.class);
ReflectionTestUtils.setField(service, "orderNoGenerator", generator);
InventoryPosition position = new InventoryPosition(); position.setCustomerId("TENANT-A"); position.setOverseasWarehouseId("WH-1");
when(inventoryPositions.selectById(901L)).thenReturn(position);
DropshipOutboundCandidate candidate = dropshipCandidate(901L, 1001L, "WH-1", "PLT-501", "MIXED", 1L);
when(dropshipProfiles.lockOutboundCandidate("TENANT-A", 901L, "MIXED")).thenReturn(candidate);
when(dropshipCartons.lockAvailable("TENANT-A", 1001L, "MIXED", 1L)).thenReturn(List.of());
@@ -470,6 +476,45 @@ class DeliveryTransferServiceTest {
assertEquals(409, exception.getStatusCode().value());
assertTrue(exception.getReason().contains("一件代发可用箱数不足"));
verify(outboundPallets, never()).insert(any(OutboundPalletItem.class));
}
@Test
void concurrentOneClickCreateReplaysCurrentCommittedRowBeforeTakingInventoryLocks() {
var generator = org.mockito.Mockito.mock(LastlegOrderNoGenerator.class);
ReflectionTestUtils.setField(service, "orderNoGenerator", generator);
var request = new DeliveryRequests.OneClickOutbound("recipient", List.of(
new DeliveryRequests.OneClickItem(901L, 2L, "SKU-1", List.of("A", "B"))), "remark", "race-key");
OutboundOrder winner = outbound(301L, null, null, "OUT-WINNER", "race-key");
winner.setOutboundType("ONE_CLICK_DROPSHIP"); winner.setRequestHash(service.oneClickRequestHash(request));
OutboundPdaTask task = new OutboundPdaTask(); task.setId(701L); task.setTaskNo("PDA-WINNER"); task.setStatus("PENDING");
InventoryPosition position = new InventoryPosition(); position.setCustomerId("TENANT-A"); position.setOverseasWarehouseId("WH-1");
when(inventoryPositions.selectById(901L)).thenReturn(position);
when(outboundOrders.insert(any(OutboundOrder.class))).thenThrow(new org.springframework.dao.DuplicateKeyException("race"));
when(outboundOrders.lockByIdempotency("TENANT-A", "race-key")).thenReturn(winner);
when(pdaTasks.lockByOutbound("TENANT-A", 301L)).thenReturn(task);
assertEquals("PDA-WINNER", service.createOneClick("TENANT-A", "U-1", request).taskNo());
verify(dropshipProfiles, never()).lockOutboundCandidate(any(), any(), any());
verify(outboundPallets, never()).insert(any(OutboundPalletItem.class));
verify(pdaTasks, never()).insert(any(OutboundPdaTask.class));
}
@Test
void oneClickReplayRejectsChangedRecipientItemsMarksAndRemark() {
var item = new DeliveryRequests.OneClickItem(901L, 2L, "SKU-1", List.of("A", "B"));
var request = new DeliveryRequests.OneClickOutbound("recipient", List.of(item), "remark", "key");
OutboundOrder existing = outbound(301L, null, null, "OUT-1", "key");
existing.setOutboundType("ONE_CLICK_DROPSHIP"); existing.setRequestHash(service.oneClickRequestHash(request));
when(outboundOrders.findByIdempotency("TENANT-A", "key")).thenReturn(existing);
for (var changed : List.of(
new DeliveryRequests.OneClickOutbound("changed", List.of(item), "remark", "key"),
new DeliveryRequests.OneClickOutbound("recipient", List.of(item), "changed", "key"),
new DeliveryRequests.OneClickOutbound("recipient", List.of(new DeliveryRequests.OneClickItem(902L, 2L, "SKU-1", List.of("A", "B"))), "remark", "key"),
new DeliveryRequests.OneClickOutbound("recipient", List.of(new DeliveryRequests.OneClickItem(901L, 2L, "SKU-1", List.of("A", "C"))), "remark", "key"))) {
assertEquals(409, assertThrows(ResponseStatusException.class,
() -> service.createOneClick("TENANT-A", "U-1", changed)).getStatusCode().value());
}
verify(inventoryPositions, never()).selectById(any());
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
}
@@ -493,7 +538,6 @@ class DeliveryTransferServiceTest {
@Test
void oneClickCreationRejectsOrdinaryInventoryBeforeAnyWrite() {
when(dropshipProfiles.lockOutboundCandidate("TENANT-A", 901L, "MIXED")).thenReturn(null);
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> service.createOneClick(
"TENANT-A", "U-1", new DeliveryRequests.OneClickOutbound("recipient", List.of(
@@ -619,6 +663,12 @@ class DeliveryTransferServiceTest {
assertEquals("SKU-1", projected.skuCode());
assertEquals("IN-A", projected.inboundCode());
assertEquals(1L, projected.availablePackages());
item.setCartonMarks("CTN-B");
assertEquals(List.of("CTN-B"), service.pdaTaskDetail("WH-1", "PDA-701").items().get(0).positions()
.stream().map(DeliveryResponses.PdaPickPosition::cartonMark).toList());
second.setStatus("SHIPPED");
assertTrue(service.pdaTaskDetail("WH-1", "PDA-701").items().get(0).positions().isEmpty());
}
@Test
@@ -647,6 +697,7 @@ class DeliveryTransferServiceTest {
DeliveryResponses.PdaTaskDetail detail = service.pdaTaskDetail("WH-1", "PDA-701");
assertEquals("CTN-A", detail.items().get(0).positions().get(0).cartonMark());
assertEquals("SKU-1", detail.items().get(0).positions().get(0).skuCode());
}
@Test
@@ -658,13 +709,15 @@ class DeliveryTransferServiceTest {
OutboundPdaTask task = new OutboundPdaTask(); task.setId(701L); task.setCustomerId("TENANT-A");
task.setOutboundOrderId(301L); task.setTaskNo("PDA-701"); task.setStatus("PENDING");
OutboundPalletItem item = pallet(501L, "DP-1", "ONT8", "Ontario", "IN-A", 2);
item.setSkuCode("SKU-1");
item.setSkuCode("SKU-1"); item.setCartonMarks("BOX-A,BOX-B");
InventoryPosition position = new InventoryPosition(); position.setId(901L); position.setCustomerId("TENANT-A");
position.setBalanceId(801L); position.setPalletId(501L); position.setOverseasWarehouseId("WH-1");
InventoryBalance balance = new InventoryBalance(); balance.setCustomerId("TENANT-A"); balance.setInboundCode("IN-A");
DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(1001L);
DropshipCarton first = new DropshipCarton(); first.setId(1101L); first.setProductQuantity(7L);
DropshipCarton second = new DropshipCarton(); second.setId(1102L); second.setProductQuantity(13L);
first.setStatus("AVAILABLE"); first.setSkuCode("SKU-1"); first.setCartonMark("BOX-A");
second.setStatus("AVAILABLE"); second.setSkuCode("SKU-1"); second.setCartonMark("BOX-B");
when(pdaTasks.lockByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task);
when(outboundOrders.selectById(301L)).thenReturn(outbound);
when(outboundOrders.lockByNo("TENANT-A", "OUT-301")).thenReturn(outbound);
@@ -672,12 +725,18 @@ class DeliveryTransferServiceTest {
when(inventoryPositions.selectById(901L)).thenReturn(position);
when(inventoryBalances.selectById(801L)).thenReturn(balance);
when(dropshipProfiles.findPosition("TENANT-A", 901L)).thenReturn(profile);
when(dropshipCartons.lockAvailable("TENANT-A", 1001L, "SKU-1", 1L)).thenReturn(List.of(first), List.of(second));
when(dropshipCartons.findByProfileAndMark("TENANT-A", 1001L, "BOX-A")).thenReturn(first);
when(dropshipCartons.findByProfileAndMark("TENANT-A", 1001L, "BOX-B")).thenReturn(second);
when(dropshipSkuInventory.deduct("TENANT-A", 1001L, "SKU-1", 1L, 7L)).thenReturn(1);
when(dropshipSkuInventory.deduct("TENANT-A", 1001L, "SKU-1", 1L, 13L)).thenReturn(1);
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-7"));
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-13"));
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "U-1", "PDA-701",
new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "wrong-mark", List.of("WRONG-BOX"))));
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-7", List.of("BOX-A")));
assertEquals(1, item.getPickedPackageCount()); assertEquals("AVAILABLE", second.getStatus());
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "U-1", "PDA-701",
new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "repeat-box-new-key", List.of("BOX-A"))));
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-13", List.of("BOX-B")));
verify(dropshipSkuInventory).deduct("TENANT-A", 1001L, "SKU-1", 1L, 7L);
verify(dropshipSkuInventory).deduct("TENANT-A", 1001L, "SKU-1", 1L, 13L);
assertEquals("SHIPPED", first.getStatus()); assertEquals("SHIPPED", second.getStatus());