This commit is contained in:
2026-10-19 17:56:36 +08:00
parent bb917c954c
commit dcfba1053c
232 changed files with 15655 additions and 6005 deletions
@@ -22,7 +22,7 @@ class FleetHandoverPdaQueryTest {
final FleetHandoverPdaQuery service = new FleetHandoverPdaQuery(bindings, trips, history, dispatches, handovers, json);
@BeforeEach void setup() {
var binding = new FleetTripDispatch(); binding.setDispatchNo("PC-A"); binding.setCustomerId("CUSTOMER-A");
var binding = new FleetTripDispatch(); binding.setDispatchNo("PC-A");
binding.setTripId(1L); binding.setTripNo("TR-ONE"); binding.setPackageCount(30);
var trip = new FleetTripEntity(); trip.setId(1L); trip.setTripNo("TR-ONE"); trip.setWarehouseId(92L);
trip.setStatus("IN_TRANSIT"); trip.setRecordVersion(10L);
@@ -31,7 +31,7 @@ class FleetHandoverPdaQueryTest {
{"trip":{"tripNo":"TR-ONE","warehouseId":"92","vehiclePlate":"ORIGINAL","driverName":"原司机"}}
""");
when(history.selectList(any())).thenReturn(List.of(departed));
when(dispatches.transitEvidence("PC-A", false)).thenReturn(new FleetTransitEvidence("PC-A", "CUSTOMER-A", "92", 30, List.of(
when(dispatches.transitEvidence("PC-A", false)).thenReturn(new FleetTransitEvidence("PC-A", "92", 30, List.of(
new FleetTransitEvidence.Delivery(new FleetHandoverPolicy.Delivery("CUSTOMER-A", "PC-A", "DL-DONE", "FBA:ONE", 10, Instant.parse("2026-10-03T01:00:00Z"), List.of()),
"DELIVERED", 2, new FleetTransitEvidence.Transport("ORIGINAL", "原司机", null)),
new FleetTransitEvidence.Delivery(new FleetHandoverPolicy.Delivery("CUSTOMER-A", "PC-A", "DL-PENDING", "FBA:TWO", 20, null, List.of()),
@@ -49,11 +49,11 @@ class FleetHandoverPdaQueryTest {
assertEquals("REPLACEMENT", result.deliveries().get(1).transport().vehiclePlate());
String serialized = json.writeValueAsString(result);
assertFalse(serialized.contains("customerId")); assertFalse(serialized.contains("warehouseId")); assertFalse(serialized.contains("vehicleId"));
verify(handovers).latestForCargo(eq(1L), argThat(value -> value.contains("CUSTOMER-A") && value.contains("PC-A")));
verify(handovers).latestForCargo(eq(1L), argThat(value -> value.contains("PC-A") && !value.contains("customerId")));
verify(dispatches, never()).handover(any(), any());
}
@Test void staleBindingToAnotherCustomerCannotExposeDeliveryResources() {
when(dispatches.transitEvidence("PC-A", false)).thenReturn(new FleetTransitEvidence("PC-A", "CUSTOMER-B", "92", 30, List.of()));
@Test void incompleteTruckEvidenceCannotExposeDeliveryResources() {
when(dispatches.transitEvidence("PC-A", false)).thenReturn(new FleetTransitEvidence("PC-A", "93", 30, List.of()));
assertThrows(org.springframework.web.server.ResponseStatusException.class, () -> service.status(92, "PC-A"));
verifyNoInteractions(history, handovers);
}
@@ -71,7 +71,7 @@ class FleetHandoverTransactionTest {
var inspection = mock(FleetHandoverInspectionService.class);
var cargo = new FleetHandoverPolicy.Cargo("CUSTOMER-A", "PC-A", "item", "pallet", "FBA:ONT8", 10);
var delivery = new FleetHandoverPolicy.Delivery("CUSTOMER-A", "PC-A", "DL-A", "FBA:ONT8", 10, null, List.of());
var evidence = new FleetTransitEvidence("PC-A", "CUSTOMER-A", "92", 10,
var evidence = new FleetTransitEvidence("PC-A", "92", 10,
List.of(new FleetTransitEvidence.Delivery(delivery, "WAIT_DELIVERY", 1, new FleetTransitEvidence.Transport("PLATE-11", "司机", "phone"))));
when(inspection.prepare(anyString(), any(), any(), eq(true))).thenAnswer(call -> new FleetHandoverInspectionService.Prepared(state().getRecordVersion(),
new FleetHandoverPolicy.Inspection(List.of(cargo), List.of(), Set.of("pallet"), new Load(1, BigDecimal.valueOf(100), BigDecimal.valueOf(2))), List.of(evidence)));
@@ -3,7 +3,9 @@ package com.baiyue.lastleg.fleet.exception;
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
import com.baiyue.lastleg.delivery.mapper.*;
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
import com.baiyue.lastleg.dispatch.entity.DispatchAggregate;
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository;
import com.baiyue.lastleg.dispatch.fleet.DispatchFleetAdapter;
import com.baiyue.lastleg.dispatch.mapper.*;
import com.baiyue.lastleg.dispatch.pda.mapper.*;
@@ -17,11 +19,15 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class FleetTransitEvidenceTest {
@Test void removingOriginalPhysicalItemDelegatesToDispatchAuthority() {
var service=mock(com.baiyue.lastleg.dispatch.service.DispatchService.class);
org.springframework.test.util.ReflectionTestUtils.setField(adapter,"dispatchService",service);
adapter.removeItem("PC-1",41L);
verify(service).removeItem("PC-1",41L);verifyNoMoreInteractions(service);
@Test void twoCustomersAreReadAsOneTruckWithoutAHeaderOwner() {
var other=new DispatchItem();other.setCustomerId("CUSTOMER-B");other.setPackageCount(3);
when(aggregates.cargo(11)).thenReturn(List.of(item,other));dispatch.setShippedPackageCount(13);
var outbound=new OutboundOrder();outbound.setId(32L);outbound.setCustomerId("CUSTOMER-B");outbound.setDispatchOrderId(11L);outbound.setOverseasWarehouseId("21");outbound.setStatus("OUTBOUNDED");outbound.setPackageCount(3);
var second=new DeliveryOrder();second.setId(42L);second.setDeliveryNo("DL-2");second.setCustomerId("CUSTOMER-B");second.setDispatchOrderId(11L);second.setOutboundOrderId(32L);second.setOverseasWarehouseId("21");second.setPackageCount(3);second.setStatus("DELIVERING");second.setVersion(0L);second.setDestinationType("FBA");second.setDestinationWarehouseCode("LAX9");
when(outbounds.findAllByDispatch(11)).thenReturn(List.of(this.outbound,outbound));when(deliveries.findAllByDispatch(11)).thenReturn(List.of(delivery,second));
var fact=adapter.terminalEvidence("PC-1",false);
assertEquals(List.of("CUSTOMER-A","CUSTOMER-B"),fact.deliveries().stream().map(com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.TerminalDelivery::customerId).toList());
assertEquals(13,fact.shippedPackages());assertFalse(java.util.Arrays.stream(fact.getClass().getRecordComponents()).anyMatch(c->c.getName().equals("customerId")));
}
@BeforeAll static void initializeDeliveryMetadata() {
com.baomidou.mybatisplus.core.metadata.TableInfoHelper.initTableInfo(
@@ -29,41 +35,30 @@ class FleetTransitEvidenceTest {
DeliveryOrder.class);
}
final DispatchOrderMapper orders = mock(DispatchOrderMapper.class);
final OutboundOrderMapper outbounds = mock(OutboundOrderMapper.class);
final DeliveryOrderMapper deliveries = mock(DeliveryOrderMapper.class);
final DeliveryOperationMapper operations = mock(DeliveryOperationMapper.class);
final DispatchPdaWorkflowService workflow = mock(DispatchPdaWorkflowService.class);
final DispatchFleetAdapter adapter = new DispatchFleetAdapter(orders, mock(DispatchPdaTaskMapper.class),
outbounds, deliveries, mock(DispatchItemMapper.class), mock(DispatchPdaPickMapper.class), workflow, operations);
final DispatchOrder dispatch = new DispatchOrder();
final DeliveryOrder delivery = new DeliveryOrder();
final DispatchAggregateRepository aggregates=mock(DispatchAggregateRepository.class);
final OutboundOrderMapper outbounds=mock(OutboundOrderMapper.class);final DeliveryOrderMapper deliveries=mock(DeliveryOrderMapper.class);
final DeliveryOperationMapper operations=mock(DeliveryOperationMapper.class);
final com.baiyue.lastleg.dispatch.service.DispatchAggregateService workflow=mock(com.baiyue.lastleg.dispatch.service.DispatchAggregateService.class);
final DispatchFleetAdapter adapter=new DispatchFleetAdapter(aggregates,outbounds,deliveries,operations,mock(com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository.class),workflow,new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules());
final DispatchAggregate dispatch=new DispatchAggregate();final DispatchItem item=new DispatchItem();final OutboundOrder outbound=new OutboundOrder();final DeliveryOrder delivery=new DeliveryOrder();
FleetTransitEvidenceTest() {
dispatch.setId(11L); dispatch.setDispatchNo("PC-1"); dispatch.setCustomerId("CUSTOMER-A");
dispatch.setOverseasWarehouseId("21"); dispatch.setStatus("DEPARTED"); dispatch.setShippedPackageCount(10);
var outbound = new OutboundOrder(); outbound.setId(31L); outbound.setStatus("OUTBOUNDED"); outbound.setPackageCount(10);
delivery.setId(41L); delivery.setDeliveryNo("DL-1"); delivery.setCustomerId("CUSTOMER-A");
delivery.setDispatchOrderId(11L); delivery.setOutboundOrderId(31L); delivery.setOverseasWarehouseId("21");
delivery.setPackageCount(10); delivery.setStatus("WAIT_DELIVERY"); delivery.setVersion(0L);
delivery.setDestinationType("FBA"); delivery.setDestinationWarehouseCode("ONT8");
when(orders.findGlobalByDispatchNo("PC-1")).thenReturn(dispatch);
when(orders.lockGlobalByDispatchNo("PC-1")).thenReturn(dispatch);
when(outbounds.findByDispatch("CUSTOMER-A", 11L)).thenReturn(outbound);
when(outbounds.lockByDispatch("CUSTOMER-A", 11L)).thenReturn(outbound);
when(deliveries.findByOutbound("CUSTOMER-A", 31L)).thenReturn(List.of(delivery));
when(deliveries.lockByOutbound("CUSTOMER-A", 31L)).thenReturn(List.of(delivery));
dispatch.setId(11L);dispatch.setDispatchNo("PC-1");dispatch.setOverseasWarehouseId("21");dispatch.setStatus("ACTIVE");dispatch.setStage("DELIVERING");dispatch.setDepartedAt(LocalDateTime.now(java.time.ZoneOffset.UTC).minusDays(1));dispatch.setShippedPackageCount(10);
item.setCustomerId("CUSTOMER-A");item.setPackageCount(10);when(aggregates.cargo(11)).thenReturn(List.of(item));
outbound.setId(31L);outbound.setCustomerId("CUSTOMER-A");outbound.setDispatchOrderId(11L);outbound.setOverseasWarehouseId("21");outbound.setStatus("OUTBOUNDED");outbound.setPackageCount(10);
delivery.setId(41L);delivery.setDeliveryNo("DL-1");delivery.setCustomerId("CUSTOMER-A");delivery.setDispatchOrderId(11L);delivery.setOutboundOrderId(31L);delivery.setOverseasWarehouseId("21");delivery.setPackageCount(10);delivery.setStatus("DELIVERING");delivery.setVersion(0L);delivery.setDestinationType("FBA");delivery.setDestinationWarehouseCode("ONT8");
when(aggregates.find("PC-1")).thenReturn(dispatch);when(aggregates.lock("PC-1")).thenReturn(dispatch);
when(outbounds.findAllByDispatch(11)).thenReturn(List.of(outbound));when(outbounds.lockAllByDispatch(11)).thenReturn(List.of(outbound));
when(deliveries.findAllByDispatch(11)).thenReturn(List.of(delivery));when(deliveries.lockAllByDispatch(11)).thenReturn(List.of(delivery));
}
@Test void unresolvedReturnKeepsCargoAndUsesCustomerScopedLocks() {
delivery.setStatus("RETURN_PENDING");
var fact = adapter.transitEvidence("PC-1", true);
assertNull(fact.deliveries().get(0).cargo().terminalAt());
assertEquals("FBA:ONT8", fact.deliveries().get(0).cargo().destinationKey());
var order = inOrder(orders, outbounds, deliveries);
order.verify(orders).lockGlobalByDispatchNo("PC-1");
order.verify(outbounds).lockByDispatch("CUSTOMER-A", 11L);
order.verify(deliveries).lockByOutbound("CUSTOMER-A", 31L);
var order = inOrder(aggregates, outbounds, deliveries);
order.verify(aggregates).lock("PC-1");
order.verify(outbounds).lockAllByDispatch(11);
order.verify(deliveries).lockAllByDispatch(11);
verifyNoInteractions(workflow);
}
@@ -85,7 +80,7 @@ class FleetTransitEvidenceTest {
assertEquals(returned.atZone(ZoneId.systemDefault()).toInstant(),
adapter.transitEvidence("PC-1", false).deliveries().get(0).cargo().terminalAt());
var terminal=adapter.terminalEvidence("PC-1",false);
assertEquals("CUSTOMER-A",terminal.customerId());assertEquals(10,terminal.shippedPackages());
assertEquals("CUSTOMER-A",terminal.deliveries().get(0).customerId());assertEquals(10,terminal.shippedPackages());
var returnedDelivery=terminal.deliveries().get(0);
assertEquals("FBA:ONT8",returnedDelivery.destinationKey());assertEquals(10,returnedDelivery.packages());
assertEquals(returned.atZone(ZoneId.systemDefault()).toInstant(),returnedDelivery.terminalAt());
@@ -103,9 +98,9 @@ class FleetTransitEvidenceTest {
}
@Test void preDepartureAndCancelledDeliveryCannotBecomeHandoverCargo() {
dispatch.setStatus("READY_TO_DEPART");
dispatch.setStage("AFTER_LOADING");
assertThrows(org.springframework.web.server.ResponseStatusException.class, () -> adapter.transitEvidence("PC-1", false));
dispatch.setStatus("DEPARTED"); delivery.setStatus("CANCELLED");
dispatch.setStage("DELIVERING"); delivery.setStatus("CANCELLED");
assertThrows(org.springframework.web.server.ResponseStatusException.class, () -> adapter.transitEvidence("PC-1", false));
}
@@ -115,7 +110,7 @@ class FleetTransitEvidenceTest {
when(deliveries.update(isNull(), any())).thenReturn(1);
adapter.handover(evidence, new com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.Assignment("NEW-PLATE", "接替司机", "phone"));
verify(deliveries).update(isNull(), any());
verify(orders, never()).update(any(), any());
verify(aggregates, never()).saveState(any(), anyLong());
verifyNoInteractions(workflow);
}
@@ -1,33 +1,23 @@
package com.baiyue.lastleg.fleet.trip;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import java.util.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.any;
class FleetDispatchWorkflowQueryTest {
@Test
void batchesReviewsAndKeepsCustomerDispatchDecisionsIndependent() {
var bindings = mock(FleetTripDispatchMapper.class);
var trips = mock(FleetTripMapper.class);
var first = new FleetTripDispatch(); first.setDispatchNo("PC-A"); first.setTripId(1L); first.setTripNo("TR-A");
first.setReviewJson("{\"status\":\"REJECTED\",\"reason\":\"货物需重新核对\"}");
var second = new FleetTripDispatch(); second.setDispatchNo("PC-B"); second.setTripId(1L); second.setTripNo("TR-A");
var trip = new FleetTripEntity(); trip.setId(1L);
trip.setLoadingReviewJson("{\"status\":\"APPROVED\",\"reason\":\"审核通过\"}");
when(bindings.findByDispatchNos(List.of("PC-A", "PC-B", "PC-C"))).thenReturn(List.of(first, second));
when(trips.selectBatchIds(any())).thenReturn(List.of(trip));
var query = new FleetDispatchWorkflowQuery(bindings, trips, new ObjectMapper().findAndRegisterModules());
var result = query.read(List.of("PC-A", "PC-B", "PC-C"));
assertEquals("REJECTED", result.get("PC-A").dispatchReview().status());
assertEquals("货物需重新核对", result.get("PC-A").dispatchReview().reason());
assertEquals("PENDING", result.get("PC-B").dispatchReview().status());
assertEquals("APPROVED", result.get("PC-B").loadingReview().status());
assertFalse(result.containsKey("PC-C"));
verify(trips).selectBatchIds(List.of(1L));
verifyNoMoreInteractions(trips);
assertTrue(query.read(List.of()).isEmpty());
@Test void batchesCurrentMainReviewsWithoutCustomerOrBindingReviewCopies() {
var bindings=mock(FleetTripDispatchMapper.class);var gateway=mock(FleetDispatchGateway.class);
var first=new FleetTripDispatch();first.setDispatchNo("DS-A");first.setTripNo("TR-A");first.setTripId(1L);
var second=new FleetTripDispatch();second.setDispatchNo("DS-B");second.setTripNo("TR-B");second.setTripId(2L);
var rejected=new FleetTripModels.ReviewState("REJECTED","核对本车来源","reviewer",null,null);
var waiting=new FleetTripModels.ReviewState("NOT_SUBMITTED",null,null,null,null);
var approved=new FleetTripModels.ReviewState("APPROVED",null,"reviewer",null,null);
when(bindings.findByDispatchNos(List.of("DS-A","DS-B","DS-C"))).thenReturn(List.of(first,second));
when(gateway.workflows(List.of("DS-A","DS-B"))).thenReturn(Map.of("DS-A",new FleetDispatchGateway.Workflow("ACTIVE","BEFORE_LOADING",5,rejected,waiting),"DS-B",new FleetDispatchGateway.Workflow("ACTIVE","LOADING",9,approved,approved)));
var query=new FleetDispatchWorkflowQuery(bindings,gateway);var result=query.read(List.of("DS-A","DS-B","DS-C"));
assertThat(result).containsOnlyKeys("DS-A","DS-B");assertThat(result.get("DS-A").dispatchReview().status()).isEqualTo("REJECTED");
assertThat(result.get("DS-A").loadingReview().status()).isEqualTo("NOT_SUBMITTED");assertThat(result.get("DS-B").loadingReview().status()).isEqualTo("APPROVED");
assertThat(result.get("DS-B").tripNo()).isEqualTo("TR-B");verify(gateway).workflows(List.of("DS-A","DS-B"));verifyNoMoreInteractions(gateway);
assertThat(query.read(List.of())).isEmpty();
assertThat(Arrays.stream(FleetTripDispatch.class.getDeclaredFields()).map(java.lang.reflect.Field::getName)).doesNotContain("customerId","reviewJson");
}
}
@@ -49,16 +49,9 @@ class FleetTripAuthorizationTest {
var controller = context.getBean(PlatformFleetTripController.class);
var service = context.getBean(FleetTripService.class);
controller.detail("TR-TEST"); verify(service).detail("TR-TEST");
assertThatThrownBy(() -> controller.create(null, null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.assign(null, "TR-TEST", null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.cancel(null, "TR-TEST", null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.plan(null, "TR-TEST", null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.bind(null, "TR-TEST", null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.prepare(null,null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.unbind(null, "TR-TEST", null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.confirmLoading(null, "TR-TEST", null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.reviewDispatch(null, "TR-TEST", "DP-1", null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.reviewLoading(null, "TR-TEST", null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.complete(null, "TR-TEST", null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.release(null, "TR-TEST", null)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(() -> controller.confirmCapacityPlan(null, "TR-TEST", null)).isInstanceOf(AccessDeniedException.class);
@@ -85,6 +78,23 @@ class FleetTripAuthorizationTest {
assertThatThrownBy(()->pool.saveAndConfirm("FB-1",null)).isInstanceOf(AccessDeniedException.class);
} finally {SecurityContextHolder.clearContext();}
}
@Test void externalArrangeRequiresCurrentCreateAndAllIndependentPermissions() {
var required=List.of("platform:fleet:capacity-offer:manage","platform:fleet:trip:booking-edit","platform:fleet:trip:edit","platform:overseas-warehouse:dispatch:create");
try(var context=new AnnotationConfigApplicationContext(Config.class)) {
var controller=context.getBean(PlatformFleetCapacityOfferController.class);
var service=context.getBean(FleetCapacityOfferService.class);
for(var missing:required) {
var permissions=new java.util.ArrayList<>(required);permissions.remove(missing);permissions.add("platform:overseas-warehouse:inbound-order:smart-dispatch");
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("operator","unused",permissions.stream().map(SimpleGrantedAuthority::new).toList()));
assertThatThrownBy(()->controller.arrange("FB-1",null)).isInstanceOf(AccessDeniedException.class);
}
verifyNoInteractions(service);
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("operator","unused",required.stream().map(SimpleGrantedAuthority::new).toList()));
controller.arrange("FB-1",null);verify(service).arrangeDispatch("FB-1",null);
assertThat(java.util.Arrays.stream(PlatformFleetTripController.class.getDeclaredMethods()).map(java.lang.reflect.Method::getName))
.doesNotContain("create","prepare","dispatchCandidates","bind","unbind","reviewDispatch","reviewLoading","confirmLoading");
}finally{SecurityContextHolder.clearContext();}
}
@Configuration @EnableMethodSecurity
static class Config {
/** 既有取货能力依赖原仓储引用;权限测试使用只读替身,不连接数据库。 */
@@ -94,7 +104,7 @@ class FleetTripAuthorizationTest {
@Bean FleetTripCalendarService calendar() { return mock(FleetTripCalendarService.class); }
@Bean FleetCapacityOfferCommandMapper commands() { return mock(FleetCapacityOfferCommandMapper.class); }
@Bean FleetDispatchPlanningGateway planning() { return mock(FleetDispatchPlanningGateway.class); }
@Bean FleetTripPreparationService preparation() { return mock(FleetTripPreparationService.class); }
@Bean FleetDispatchGateway dispatches() { return mock(FleetDispatchGateway.class); }
@Bean com.baiyue.lastleg.appointmentplanning.AppointmentCoverageService appointmentCoverage() { return mock(com.baiyue.lastleg.appointmentplanning.AppointmentCoverageService.class); }
@Bean FleetCapacityOfferService capacityOffers() { return mock(FleetCapacityOfferService.class); }
@Bean PlatformFleetCapacityOfferController poolController(FleetCapacityOfferService service) { return new PlatformFleetCapacityOfferController(service); }