暂存提交

This commit is contained in:
2026-08-19 16:24:02 +08:00
parent f5c4bf130a
commit 6acb6178da
354 changed files with 25000 additions and 198 deletions
@@ -0,0 +1,33 @@
package com.baiyue.lastleg.dispatch;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertTrue;
class DispatchSchemaContractTest {
@Test
void baselineDefinesDispatchHeadersItemsAndActivePalletLock() throws Exception {
String sql = Files.readString(Path.of("src/main/resources/db/lastleg.sql"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_order"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_item"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock"));
assertTrue(sql.contains("uk_ll_dispatch_active_pallet"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pda_task"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pda_pick"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pda_operation"));
assertTrue(sql.contains("active_key"));
assertTrue(sql.contains("order_refs"));
}
@Test
void dispatchCandidatesExcludePalletsReservedByWarehouseTransfers() throws Exception {
String mapper = Files.readString(Path.of(
"src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java"));
assertTrue(mapper.contains("ll_warehouse_transfer_inventory_allocation"));
assertTrue(mapper.contains("ta.status='RESERVED'"));
assertTrue(mapper.contains("tp.pallet_id=p.warehouse_pallet_id"));
}
}
@@ -0,0 +1,121 @@
package com.baiyue.lastleg.dispatch.pda.service;
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaRequests;
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaResponses;
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaOperation;
import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaOperationMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DispatchPdaWorkflowServiceTest {
@Mock private DispatchOrderMapper orders;
@Mock private DispatchPdaOperationMapper operations;
@Mock private OutboundOrderMapper outboundOrders;
@Test
void pushRequiresVehiclePlateBeforeCreatingPdaTask() {
DispatchPdaWorkflowService service = new DispatchPdaWorkflowService();
ReflectionTestUtils.setField(service, "orders", orders);
DispatchOrder order = new DispatchOrder();
order.setId(1L); order.setCustomerId("100"); order.setDispatchNo("PC1"); order.setVehiclePlate(" ");
when(orders.findByDispatchNo("100", "PC1")).thenReturn(order);
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.push("100", "PC1", new DispatchPdaRequests.Push("push-1", null, null)));
assertEquals(409, exception.getStatusCode().value());
}
@Test
void pushRequiresWarehouseSelection() {
DispatchPdaWorkflowService service = new DispatchPdaWorkflowService();
ReflectionTestUtils.setField(service, "orders", orders);
DispatchOrder order = new DispatchOrder();
order.setId(1L); order.setCustomerId("100"); order.setDispatchNo("PC1");
order.setStatus("PENDING_DEPARTURE"); order.setVehiclePlate("CA-100");
order.setOverseasWarehouseId("WH-1");
when(orders.findByDispatchNo("100", "PC1")).thenReturn(order);
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.push("100", "PC1", new DispatchPdaRequests.Push("push-1", null, null)));
assertEquals(400, exception.getStatusCode().value());
}
@Test
void departRequiresVehiclePlate() {
DispatchPdaWorkflowService service = new DispatchPdaWorkflowService();
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "operations", operations);
DispatchOrder order = new DispatchOrder();
order.setId(1L); order.setCustomerId("100"); order.setDispatchNo("PC1");
order.setStatus("READY_TO_DEPART"); order.setVehiclePlate(" ");
when(orders.findByDispatchNo("100", "PC1")).thenReturn(order);
when(orders.lockById("100", 1L)).thenReturn(order);
when(operations.findKey("100", "depart-1")).thenReturn(null);
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.depart("100", "PC1", "operator", new DispatchPdaRequests.Depart("depart-1", null)));
assertEquals(409, exception.getStatusCode().value());
}
@Test
void departRequiresPendingOutboundOrder() {
DispatchPdaWorkflowService service = new DispatchPdaWorkflowService();
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "operations", operations);
ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders);
DispatchOrder order = new DispatchOrder();
order.setId(1L); order.setCustomerId("100"); order.setDispatchNo("PC1");
order.setStatus("READY_TO_DEPART"); order.setVehiclePlate("CA-100");
when(orders.findByDispatchNo("100", "PC1")).thenReturn(order);
when(orders.lockById("100", 1L)).thenReturn(order);
when(operations.findKey("100", "depart-1")).thenReturn(null);
when(outboundOrders.lockByDispatch("100", 1L)).thenReturn(null);
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.depart("100", "PC1", "operator", new DispatchPdaRequests.Depart("depart-1", null)));
assertEquals(409, exception.getStatusCode().value());
}
@Test
void departedRequestReplaysAfterStatusChanged() throws Exception {
DispatchPdaWorkflowService service = new DispatchPdaWorkflowService();
ObjectMapper objectMapper = new ObjectMapper();
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "operations", operations);
ReflectionTestUtils.setField(service, "objectMapper", objectMapper);
DispatchOrder order = new DispatchOrder();
order.setId(1L); order.setCustomerId("100"); order.setDispatchNo("PC1"); order.setStatus("DEPARTED");
when(orders.findByDispatchNo("100", "PC1")).thenReturn(order);
when(orders.lockById("100", 1L)).thenReturn(order);
DispatchPdaOperation operation = new DispatchPdaOperation();
operation.setOperationType("DEPART"); operation.setStatus("COMPLETED");
operation.setResultJson(objectMapper.writeValueAsString(new DispatchPdaResponses.Departed("PC1", "DEPARTED", 12, false)));
when(operations.findKey("100", "depart-1")).thenReturn(operation);
// This exact key/hash pair models the persisted first execution result.
DispatchPdaRequests.Depart request = new DispatchPdaRequests.Depart("depart-1", null);
operation.setRequestHash(java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256")
.digest(objectMapper.writeValueAsString(request).getBytes(java.nio.charset.StandardCharsets.UTF_8))));
DispatchPdaResponses.Departed result = service.depart("100", "PC1", "operator", request);
assertEquals("DEPARTED", result.status());
assertEquals(true, result.replayed());
}
}
@@ -0,0 +1,102 @@
package com.baiyue.lastleg.dispatch.pda.service;
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaPick;
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaPickOrder;
import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaPickOrderMapper;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import org.apache.ibatis.annotations.Update;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DispatchPickOrderAttributionServiceTest {
@Mock private DispatchPdaPickOrderMapper attributions;
@Mock private LastlegOrderMapper orders;
@Mock private LastlegOrderStatusHistoryMapper history;
@Test
void completionStatusIsCalculatedBeforeMysqlMutatesShippedPackages() throws NoSuchMethodException {
Update update = LastlegOrderMapper.class.getMethod("transferPickedToShipped",
String.class, Long.class, long.class, String.class).getAnnotation(Update.class);
String sql = String.join("", Arrays.asList(update.value())).replaceAll("\\s+", " ");
assertTrue(sql.indexOf("current_status=CASE")
< sql.indexOf("shipped_package_count=shipped_package_count+"));
}
@Test
void attributesPackagesAcrossInboundOrdersInReceiptFifoOrder() {
DispatchPickOrderAttributionService service = service();
InboundStockReceipt first = receipt(11L, 101L, 3L, 30L);
InboundStockReceipt second = receipt(12L, 102L, 4L, 40L);
when(attributions.availableReceipts("t1", 91L)).thenReturn(List.of(first, second));
when(attributions.attributedPackages(eq("t1"), anyLong())).thenReturn(0L);
when(orders.increasePicked(eq("t1"), anyLong(), anyLong())).thenReturn(1);
DispatchPdaPick pick = pick(81L, 91L, 5L, 50L);
List<DispatchPickOrderAttributionService.OrderPackages> result = service.attribute("t1", pick);
assertEquals(List.of(new DispatchPickOrderAttributionService.OrderPackages(101L, 3L),
new DispatchPickOrderAttributionService.OrderPackages(102L, 2L)), result);
}
@Test
void departureMovesOnlyAttributedPackagesAndWritesOrderHistory() {
DispatchPickOrderAttributionService service = service();
DispatchPdaPick pick = pick(81L, 91L, 4L, 40L);
DispatchPdaPickOrder row = new DispatchPdaPickOrder();
row.setOrderId(101L); row.setPackages(4L);
when(attributions.listPick("t1", 81L)).thenReturn(List.of(row));
LastlegOrder order = new LastlegOrder();
order.setId(101L); order.setPackageCount(10); order.setPickedPackageCount(4);
order.setShippedPackageCount(2); order.setCurrentStatus("INBOUND_COMPLETED");
when(orders.lockById("t1", 101L)).thenReturn(order);
when(orders.transferPickedToShipped("t1", 101L, 4L, "OUT1")).thenReturn(1);
List<DispatchPickOrderAttributionService.OrderPackages> result = service.depart(
"t1", "OUT1", "D1", List.of(pick), LocalDateTime.of(2026, 8, 17, 15, 0));
assertEquals(List.of(new DispatchPickOrderAttributionService.OrderPackages(101L, 4L)), result);
verify(history).insert(argThat((LastlegOrderStatusHistory value) -> "PARTIALLY_SHIPPED".equals(value.getToStatus())
&& value.getRemark().contains("6/10")));
}
private DispatchPickOrderAttributionService service() {
DispatchPickOrderAttributionService service = new DispatchPickOrderAttributionService();
ReflectionTestUtils.setField(service, "attributions", attributions);
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "history", history);
return service;
}
private static InboundStockReceipt receipt(long id, long orderId, long packages, long quantity) {
InboundStockReceipt receipt = new InboundStockReceipt();
receipt.setId(id); receipt.setOrderId(orderId); receipt.setReceivedPackages(packages);
return receipt;
}
private static DispatchPdaPick pick(long id, long positionId, long packages, long quantity) {
DispatchPdaPick pick = new DispatchPdaPick();
pick.setId(id); pick.setInventoryPositionId(positionId); pick.setPackages(packages);
pick.setProductQuantity(quantity); pick.setPickedAt(LocalDateTime.of(2026, 8, 17, 14, 0));
return pick;
}
}
@@ -0,0 +1,112 @@
package com.baiyue.lastleg.dispatch.service;
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock;
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper;
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper;
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.util.List;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DispatchServiceTest {
@Mock DispatchOrderMapper orders;
@Mock DispatchItemMapper items;
@Mock DispatchPalletLockMapper locks;
@Mock DispatchCandidateMapper candidates;
private DispatchService service;
@BeforeEach
void setUp() {
service = new DispatchService();
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "items", items);
ReflectionTestUtils.setField(service, "locks", locks);
ReflectionTestUtils.setField(service, "candidates", candidates);
lenient().when(items.findByDispatchOrder(any(), any())).thenReturn(List.of());
}
@Test
void preArrangeLocksPalletWithoutVehicleCapacityValidation() throws Exception {
assertNotNull(DispatchService.class.getMethod("preArrange", String.class, String.class,
DispatchRequests.PreArrange.class).getAnnotation(Transactional.class));
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
var result = service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null,
null, null, null));
assertEquals(DispatchService.PENDING_DEPARTURE, result.status());
assertEquals("ONT8", result.route());
ArgumentCaptor<DispatchOrder> dispatch = ArgumentCaptor.forClass(DispatchOrder.class);
verify(orders).insert(dispatch.capture());
assertEquals(1, dispatch.getValue().getPalletCount());
verify(locks).insert(any(DispatchPalletLock.class));
verify(items).insert(any(DispatchItem.class));
}
@Test
void rejectsPalletAlreadyLockedByAnotherDispatch() {
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
DispatchPalletLock locked = new DispatchPalletLock();
when(locks.lockActive(eq("100"), eq("PLAN:10"))).thenReturn(locked);
assertThrows(ResponseStatusException.class, () -> service.preArrange("100", "7",
new DispatchRequests.PreArrange(List.of(10L), null, null, null, null)));
verify(orders, never()).insert(any(DispatchOrder.class));
}
@Test
void invalidCandidateStageIsRejected() {
assertThrows(ResponseStatusException.class, () -> service.candidates("100", null, null, "INVALID"));
verify(candidates, never()).available(any(), any(), any(), any());
}
@Test
void editCannotBypassPdaManagedDispatchStatus() {
DispatchOrder dispatch = new DispatchOrder();
dispatch.setId(1L); dispatch.setCustomerId("100"); dispatch.setDispatchNo("PC1");
dispatch.setStatus(DispatchService.PENDING_DEPARTURE);
when(orders.findByDispatchNo("100", "PC1")).thenReturn(dispatch);
when(orders.lockById("100", 1L)).thenReturn(dispatch);
DispatchRequests.Update request = new DispatchRequests.Update(null, null, null, null, null,
LocalDateTime.now(), null, null, null, null, "COMPLETED");
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.update("100", "PC1", request));
assertEquals(400, exception.getStatusCode().value());
assertEquals(DispatchService.PENDING_DEPARTURE, dispatch.getStatus());
verify(orders, never()).updateById(any(DispatchOrder.class));
}
private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination) {
return new DispatchPalletCandidate(1L, planPalletId, warehousePalletId, 2L, "WH-1", "Overseas warehouse", "LMO1", "LMO1", "RK1",
"LIP1-P001", destination, destination, destination, destination, 12,
new BigDecimal("1.2000"), warehousePalletId == null ? "PLAN" : "RECEIVED");
}
}