feat(trucking): refine truck demand contacts vehicle and service times

This commit is contained in:
2026-10-19 14:34:33 +08:00
parent fafcf75014
commit 21128b895f
17 changed files with 424 additions and 197 deletions
@@ -96,14 +96,14 @@ class FleetPoolOrderBridgeTest {
incoming();service.receiving("9","WT-7","pda");service.acceptedReceipt(7,receipt(),3,"op");
assertThat(looseOrder.getPalletRequired()).isNull();assertThat(looseOrder.getContainerType()).isNull();
}
@Test void truckInquiryPreservesUnknownContainerWhileZeroActualCountsRemainRealUntilReceipt(){
@Test void truckInquiryPreservesUnknownVehicleWhileZeroActualCountsRemainRealUntilReceipt(){
var source=input("US_TRUCK");service.confirm(source,"op");
assertThat(truckOrder.getContainerType()).isNull();assertThat(truckOrder.getPalletCount()).isEqualTo(source.expectedPalletCount());
assertThat(truckOrder.getVehicleType()).isNull();assertThat(truckOrder.getPalletCount()).isEqualTo(source.expectedPalletCount());
assertThat(truckOrder.getReceivedPalletCount()).isZero();assertThat(truckOrder.getAllocatedPalletCount()).isZero();
assertThat(truckOrder.getPackageCount()).isEqualTo(source.packageCount());assertThat(truckOrder.getTransportType()).isEqualTo("WAREHOUSE_TO_WAREHOUSE");
assertThat(truckOrder.getPackageCount()).isEqualTo(source.packageCount());assertThat(truckOrder.getDestinationContactName()).isNull();assertThat(truckOrder.getDeliveryAt()).isNull();
assertThat(truckOrder.getOriginAddress()).isNull();assertThat(truckOrder.getDestinationAddress()).isNull();assertThat(truckOrder.getBudgetAmount()).isNull();
incoming();service.receiving("9","WT-7","pda");service.acceptedReceipt(7,receipt(),3,"op");
assertThat(truckOrder.getReceivedPalletCount()).isEqualTo(2);assertThat(truckOrder.getContainerType()).isNull();
assertThat(truckOrder.getReceivedPalletCount()).isEqualTo(2);assertThat(truckOrder.getVehicleType()).isNull();
}
@Test void incomingMustMatchCustomerWarehouseAndDestinationAndNeverBindSecondTailOrder(){
service.confirm(input("LOCAL_LOOSEBOARD"),"op");
@@ -0,0 +1,64 @@
package com.baiyue.lastleg.trucking;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.baiyue.lastleg.trucking.entity.UsTruckOrder;
import com.baiyue.lastleg.trucking.mapper.*;
import com.baiyue.lastleg.trucking.repository.UsTruckOrderRepository;
import org.junit.jupiter.api.Test;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
class UsTruckOrderPersistenceTest {
/** 使用模块基线和真实 Mapper 验证新字段保存、回读、更新与客户隔离。 */
@Test
void contactsVehicleAndLocalMinuteTimesRoundTripThroughActualMapper() throws Exception {
var ds = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
String baseline;
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
baseline = new String(input.readAllBytes(), StandardCharsets.UTF_8);
}
int start = baseline.indexOf("CREATE TABLE IF NOT EXISTS ll_us_truck_order (");
String ddl = baseline.substring(start, baseline.indexOf(';', start));
// H2 使用真实字段定义,去掉 MySQL 专属的索引和表选项。
ddl = ddl.replaceAll("(?m)^\\s*(?:UNIQUE KEY|KEY) [^\\n]*\\n?", "");
ddl = ddl.substring(0, ddl.indexOf(") ENGINE=")) + ")";
ddl = ddl.replaceAll(",\\s*\\)$", ")");
new JdbcTemplate(ds).execute(ddl);
var config = new MybatisConfiguration();
config.setMapUnderscoreToCamelCase(true);
config.addMapper(UsTruckOrderMapper.class);
var factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(ds); factory.setConfiguration(config);
var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(UsTruckOrderMapper.class);
var repository = new UsTruckOrderRepository(mapper, mock(UsTruckOrderPalletMapper.class), mock(UsTruckOrderStatusHistoryMapper.class), mock(UsTruckOrderOperationMapper.class));
var order = new UsTruckOrder();
order.setId(1L); order.setCustomerId("C-1"); order.setOrderNo("TRUCK-TEST-1");
order.setVehicleType("PALLET_15"); order.setOrigin("Los Angeles"); order.setDestination("Ontario");
order.setOriginContactName("发货人"); order.setOriginContactPhone("+1 (555) 0100");
order.setDestinationContactName("收货人"); order.setDestinationContactPhone("+1-555-0101");
order.setPickupAt(LocalDateTime.of(2026, 10, 20, 9, 30)); order.setDeliveryAt(LocalDateTime.of(2026, 10, 20, 15, 45));
order.setTotalWeightKg(BigDecimal.ZERO); order.setTotalVolumeCbm(BigDecimal.ZERO);
order.setCurrency("USD"); order.setStatus("PENDING_REVIEW"); order.setIdempotencyKey("roundtrip"); order.setRequestHash("0".repeat(64));
repository.insertOrder(order);
var saved = repository.findNo("C-1", order.getOrderNo());
assertEquals("PALLET_15", saved.getVehicleType());
assertEquals("发货人", saved.getOriginContactName()); assertEquals("+1 (555) 0100", saved.getOriginContactPhone());
assertEquals("收货人", saved.getDestinationContactName()); assertEquals("+1-555-0101", saved.getDestinationContactPhone());
assertEquals(order.getPickupAt(), saved.getPickupAt()); assertEquals(order.getDeliveryAt(), saved.getDeliveryAt());
assertEquals(0, saved.getPalletCount());
assertNull(repository.findNo("C-2", order.getOrderNo())); assertNull(repository.findKey("C-2", "roundtrip"));
saved.setVehicleType("PALLET_30"); saved.setDestinationContactPhone("+1-555-0102"); saved.setDeliveryAt(saved.getDeliveryAt().plusMinutes(15));
repository.updateOrder(saved);
var updated = repository.findKey("C-1", "roundtrip");
assertEquals("PALLET_30", updated.getVehicleType()); assertEquals("+1-555-0102", updated.getDestinationContactPhone());
assertEquals(LocalDateTime.of(2026, 10, 20, 16, 0), updated.getDeliveryAt());
}
}
@@ -0,0 +1,35 @@
package com.baiyue.lastleg.trucking;
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
import jakarta.validation.Validation;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
class UsTruckOrderRequestsTest {
/** 新增的七项需求必须由服务端校验,不能仅依赖页面星号。 */
@Test
void bothEndpointsVehicleAndTimesAreMandatory() {
try (var factory = Validation.buildDefaultValidatorFactory()) {
var request = new UsTruckOrderRequests.Create(null, "LA", null, "ONT", null, 0,
BigDecimal.ZERO, BigDecimal.ZERO, null, "USD", " ", null, null, "", null, null, null, "required");
var fields = factory.getValidator().validate(request).stream().map(v -> v.getPropertyPath().toString()).collect(Collectors.toSet());
assertEquals(java.util.Set.of("vehicleType", "originContactName", "originContactPhone", "destinationContactName", "destinationContactPhone", "pickupAt", "deliveryAt"), fields);
}
}
/** 两种当前车型及国际电话原样受理,未知车型拒绝。 */
@Test
void onlyCurrentVehicleTypesAreAccepted() {
try (var factory = Validation.buildDefaultValidatorFactory()) {
for (String vehicle : java.util.List.of("PALLET_15", "PALLET_30", "OTHER")) {
var request = new UsTruckOrderRequests.Create(vehicle, "LA", null, "ONT", null, 0,
BigDecimal.ZERO, BigDecimal.ZERO, null, "USD", "A", "+1 (555) 0100", "B", "+1-555-0101",
LocalDateTime.of(2026, 10, 20, 9, 30), LocalDateTime.of(2026, 10, 20, 15, 45), null, "vehicle");
assertEquals(!vehicle.equals("OTHER"), factory.getValidator().validate(request).isEmpty());
}
}
}
}
@@ -18,7 +18,7 @@ class UsTruckOrderSchemaContractTest {
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order_status_history"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order_pallet"));
assertTrue(sql.contains("pallet_count INT NOT NULL DEFAULT 1"));
assertTrue(sql.contains("pallet_count INT NOT NULL DEFAULT 0"));
assertTrue(sql.contains("received_pallet_count INT NOT NULL DEFAULT 0"));
assertTrue(sql.contains("allocated_pallet_count INT NOT NULL DEFAULT 0"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order_operation"));
@@ -50,7 +50,7 @@ class UsTruckOrderSchemaContractTest {
}
@Test
void poolUnknownContainerSurvivesOmittedNullInsertColumnInBothBaselines() throws Exception {
void newTruckDemandFieldsExistAndRemovedInputsAreAbsentInBothBaselines() throws Exception {
String baseline;
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
baseline = new String(input.readAllBytes(), StandardCharsets.UTF_8);
@@ -59,8 +59,9 @@ class UsTruckOrderSchemaContractTest {
for (String sql : java.util.List.of(baseline, unified)) {
String normalized = sql.toLowerCase().replace("create table if not exists", "create table");
String block = tableBlock(normalized, "ll_us_truck_order");
assertTrue(java.util.regex.Pattern.compile("(?:^|,)\\s*container_type\\s+varchar\\(32\\)\\s+(?:collate\\s+\\w+\\s+)?(?:null\\s+)?default\\s+null\\b").matcher(block).find(),
"Unknown container must remain null when MyBatis omits its insert column");
for (String column : java.util.List.of("vehicle_type", "origin_contact_name", "origin_contact_phone", "destination_contact_name", "destination_contact_phone", "pickup_at", "delivery_at")) assertTrue(block.contains(column));
for (String removed : java.util.List.of("transport_type", "container_type", "requested_date")) org.junit.jupiter.api.Assertions.assertFalse(block.contains(removed));
}
}
}
@@ -75,9 +75,9 @@ class UsTruckOrderControllerTest {
@Test
void platformCreateUsesExplicitCustomerAndCreatePermission() {
var request = new UsTruckOrderRequests.Create("PORT_PICKUP", "Los Angeles", null,
"Ontario", null, "40HQ", 1, 0, java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO,
null, "USD", "Alice", "+1-555-0100", java.time.LocalDate.now(), null, "create-key");
var request = new UsTruckOrderRequests.Create("PALLET_15", "Los Angeles", null,
"Ontario", null, 0, java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO,
null, "USD", "Alice", "+1-555-0100", "Bob", "+1-555-0101", java.time.LocalDateTime.now(), java.time.LocalDateTime.now().plusHours(1), null, "create-key");
platform.create(user(0L, "PLATFORM", Set.of(com.baiyue.lastleg.security.OverseasPermissions.TRUCK_CREATE)),
"100", request);
verify(service).create("100", "7", request);
@@ -85,8 +85,8 @@ class UsTruckOrderControllerTest {
@Test
void platformUpdateUsesExplicitCustomerAndEditPermission() {
var request = new UsTruckOrderRequests.Update(0L, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null);
var request = new UsTruckOrderRequests.Update(0L, "PALLET_30", null, null, null, null,
null, null, null, null, null, "Alice", "100", "Bob", "200", java.time.LocalDateTime.now(), java.time.LocalDateTime.now().plusHours(1), null);
platform.update(user(0L, "PLATFORM", Set.of(com.baiyue.lastleg.security.OverseasPermissions.TRUCK_EDIT)),
"100", "TK1", request);
verify(service).update("100", "TK1", "7", request);
@@ -54,18 +54,15 @@ class UsTruckOrderServiceTest {
void setUp() {
service = new UsTruckOrderService();
org.springframework.test.util.ReflectionTestUtils.setField(service, "fleetPool", org.mockito.Mockito.mock(com.baiyue.lastleg.fleet.business.FleetPoolOrderQueries.class));
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "pallets", pallets);
ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "operations", operations);
ReflectionTestUtils.setField(service, "repository", new com.baiyue.lastleg.trucking.repository.UsTruckOrderRepository(orders, pallets, histories, operations));
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
ReflectionTestUtils.setField(service, "orderNoGenerator", orderNoGenerator);
order = order(UsTruckOrderService.PENDING_REVIEW, 0L, 2);
lenient().when(orders.lockNo("100", "TK1")).thenAnswer(call -> order);
when(orders.findNo("100", "TK1")).thenAnswer(call -> order);
when(pallets.listOrder(anyString(), anyLong())).thenAnswer(call -> palletRows);
lenient().when(orders.findNo("100", "TK1")).thenAnswer(call -> order);
lenient().when(pallets.listOrder(anyString(), anyLong())).thenAnswer(call -> palletRows);
lenient().when(histories.listOrder("100", 1L)).thenAnswer(call -> historyRows);
when(histories.insert(any(UsTruckOrderStatusHistory.class))).thenAnswer(call -> {
lenient().when(histories.insert(any(UsTruckOrderStatusHistory.class))).thenAnswer(call -> {
historyRows.add(call.getArgument(0)); return 1;
});
lenient().when(pallets.insert(any(UsTruckOrderPallet.class))).thenAnswer(call -> {
@@ -82,18 +79,39 @@ class UsTruckOrderServiceTest {
}
@Test
void createDefaultsTheContainerTypeAndRejectsRemovedTypes() {
void createStoresBothContactsAndMinuteTimesWithoutInventingPallets() {
when(orderNoGenerator.next(anyString(), anyString(), anyLong())).thenReturn("TK1");
when(histories.listOrder(anyString(), anyLong())).thenReturn(historyRows);
when(orders.insert(any(UsTruckOrder.class))).thenAnswer(call -> {
order = call.getArgument(0);
return 1;
});
when(orders.insert(any(UsTruckOrder.class))).thenAnswer(call -> { order = call.getArgument(0); return 1; });
var request = create("PALLET_15", "new-truck");
var detail = service.create("100", "operator", request);
assertEquals("PALLET_15", detail.summary().vehicleType());
assertEquals("Alice", detail.summary().originContactName());
assertEquals("Bob", detail.summary().destinationContactName());
assertEquals("+1-555-0101", detail.summary().destinationContactPhone());
assertEquals(request.pickupAt(), detail.summary().pickupAt());
assertEquals(request.deliveryAt(), detail.summary().deliveryAt());
assertEquals(0, detail.summary().palletCount());
assertTrue(detail.pallets().isEmpty());
assertThrows(ResponseStatusException.class, () -> service.create("100", "operator", create("OTHER", "invalid")));
}
var detail = service.create("100", "operator", create(null, "default-type"));
@Test
void firstActualReceiptAcceptsRealQuantityWithoutForecastOverage() {
order.setPalletCount(0);
service.review("100", "TK1", "operator", new UsTruckOrderRequests.Review(0L, "APPROVE", "review-new", null));
assertTrue(palletRows.isEmpty());
var received = service.receivePallets("100", "TK1", "receiver", new UsTruckOrderRequests.ReceivePallets(1L, 3, 6, List.of(2, 2, 2), "first-real", null));
assertEquals(UsTruckOrderService.WAITING_CONSOLIDATION, received.summary().status());
assertEquals(3, received.summary().receivedPalletCount());
assertTrue(palletRows.stream().allMatch(row -> "RECEIVED".equals(row.getStatus())));
}
assertEquals("40HQ", detail.summary().containerType());
assertThrows(BizException.class, () -> service.create("100", "operator", create("40HC", "retired-type")));
@Test
void reversedTimesAreRejectedBeforeCreatingAnOrder() {
var request = create("PALLET_30", "reversed");
var invalid = new UsTruckOrderRequests.Create(request.vehicleType(), request.origin(), request.originAddress(), request.destination(), request.destinationAddress(), request.packageCount(), request.totalWeightKg(), request.totalVolumeCbm(), request.budgetAmount(), request.currency(), request.originContactName(), request.originContactPhone(), request.destinationContactName(), request.destinationContactPhone(), request.deliveryAt(), request.pickupAt(), request.remark(), request.idempotencyKey());
assertThrows(ResponseStatusException.class, () -> service.create("100", "operator", invalid));
}
@Test
@@ -188,16 +206,16 @@ class UsTruckOrderServiceTest {
return new UsTruckOrderRequests.Command(version, key, key);
}
private UsTruckOrderRequests.Create create(String containerType, String key) {
return new UsTruckOrderRequests.Create("PORT_PICKUP", "Los Angeles", null, "Ontario", null,
containerType, 1, 0, BigDecimal.ZERO, BigDecimal.ZERO, null, "USD", "Alice",
"+1-555-0100", LocalDate.now().plusDays(1), null, key);
private UsTruckOrderRequests.Create create(String vehicleType, String key) {
return new UsTruckOrderRequests.Create(vehicleType, "Los Angeles", null, "Ontario", null,
0, BigDecimal.ZERO, BigDecimal.ZERO, null, "USD", "Alice", "+1-555-0100", "Bob", "+1-555-0101",
LocalDateTime.of(2026, 10, 20, 9, 30), LocalDateTime.of(2026, 10, 20, 15, 45), null, key);
}
private UsTruckOrder order(String status, long version, int pallets) {
UsTruckOrder value = new UsTruckOrder();
value.setId(1L); value.setCustomerId("100"); value.setOrderNo("TK1");
value.setTransportType("PORT_PICKUP"); value.setOrigin("Los Angeles"); value.setDestination("Ontario");
value.setVehicleType("PALLET_15"); value.setOrigin("Los Angeles"); value.setDestination("Ontario");
value.setPalletCount(pallets); value.setPackageCount(0); value.setReceivedPalletCount(0);
value.setAllocatedPalletCount(0); value.setStatus(status); value.setVersion(version);
value.setCreatedAt(LocalDateTime.now()); value.setUpdatedAt(value.getCreatedAt());