This commit is contained in:
2026-10-21 17:26:43 +08:00
parent 0976b6ef0d
commit c442c9eaa7
32 changed files with 587 additions and 43 deletions
@@ -60,4 +60,12 @@ class FleetPickupMapperTest {
db.update("INSERT INTO ll_fleet_pickup_task VALUES(1,'C1','WT-1','TR-1','PICKED',CURRENT_TIMESTAMP)");
assertThat(mapper.unassign("TR-1")).isZero();assertThat(mapper.task(1L).getTripNo()).isEqualTo("TR-1");
}
/** 联动取消保留原车次身份,已取货成员不能被取消SQL覆盖。 */
@Test void cancellationKeepsSourceLinkAndDoesNotRewritePickedFacts() {
db.update("INSERT INTO ll_fleet_pickup_task VALUES(1,'C1','WT-1','TR-1','ASSIGNED',CURRENT_TIMESTAMP)");
db.update("INSERT INTO ll_fleet_pickup_task VALUES(6,'C6','WT-6','TR-1','PICKED',CURRENT_TIMESTAMP)");
assertThat(mapper.cancelTasks("TR-1")).isEqualTo(1);
assertThat(mapper.task(1L).getTripNo()).isEqualTo("TR-1");assertThat(mapper.task(1L).getStatus()).isEqualTo("CANCELLED");
assertThat(mapper.task(6L).getStatus()).isEqualTo("PICKED");assertThat(mapper.cancelTasks("TR-1")).isZero();
}
}
@@ -15,11 +15,15 @@ import static org.mockito.ArgumentMatchers.*;
class FleetPickupServiceTest {
FleetPickupRepository repository;FleetTripService trips;FleetPickupService service;
PickupDepartureReview departureReview;
DriverService drivers;VehicleRepository vehicles;
TripOperationsEvidenceGateway files;FleetTripEntity trip;FleetPickupSource source;FleetPickupTask task;
final Map<String,String> commands=new HashMap<>();
@BeforeEach void setup() {
repository=mock(FleetPickupRepository.class);trips=mock(FleetTripService.class);files=mock(TripOperationsEvidenceGateway.class);
service=new FleetPickupService(repository,trips,mock(FleetWarehouseDirectory.class),mock(DriverService.class),mock(VehicleRepository.class),files,new ObjectMapper().findAndRegisterModules());
departureReview=mock(PickupDepartureReview.class);
drivers=mock(DriverService.class);vehicles=mock(VehicleRepository.class);
service=new FleetPickupService(repository,trips,mock(FleetWarehouseDirectory.class),drivers,vehicles,files,new ObjectMapper().findAndRegisterModules(),departureReview);
trip=new FleetTripEntity();trip.setId(9L);trip.setTripNo("TR-1");trip.setTripPurpose("PICKUP");trip.setStatus("IN_TRANSIT");trip.setWarehouseId(100L);trip.setRecordVersion(2L);
source=source(1L,"C1","WT-1","100");task=new FleetPickupTask();task.setTransferOrderId(1L);task.setCustomerId("C1");task.setTransferNo("WT-1");task.setTripNo("TR-1");task.setStatus("ASSIGNED");
when(repository.trip("TR-1",true)).thenReturn(trip);when(repository.trip("TR-1",false)).thenReturn(trip);
@@ -29,6 +33,29 @@ class FleetPickupServiceTest {
doAnswer(c->{commands.put(c.getArgument(1),c.getArgument(2));return null;}).when(repository).command(anyString(),anyString(),anyString());
doAnswer(c->{trip.setRecordVersion(trip.getRecordVersion()+1);return null;}).when(trips).recordExecution(any(),anyLong(),anyString(),anyString(),anyString());
}
/** 审核完成后直接在装货阶段上传取货照片,全车核齐自动进入派送。 */
@Test void pickupPhotosStartDeliveryWithoutSeparateDeparture() {
trip.setStatus("PLANNED");trip.setVehicleId(20L);trip.setDriverId(30L);trip.setExpectedDriverEndAt(LocalDateTime.now(ZoneOffset.UTC).plusDays(1));
when(repository.ownsResources(9L,20L,30L)).thenReturn(true);
var vehicle=mock(VehicleRepository.VehicleSnapshot.class);when(vehicle.region()).thenReturn("OVERSEAS");when(vehicle.status()).thenReturn("IN_SERVICE");when(vehicles.lockById(20L)).thenReturn(Optional.of(vehicle));
var driver=mock(DriverModels.Response.class);var details=mock(DriverModels.Draft.class);when(details.region()).thenReturn("OVERSEAS");when(driver.details()).thenReturn(details);when(drivers.requireDispatchable(eq(30L),any())).thenReturn(driver);
when(repository.ownsUpload(1L,"TR-1",88L)).thenReturn(true);
when(files.metadata(9L,88L)).thenReturn(new TripOperationsRecordModels.EvidenceFile("88","取货照片.jpg","image/jpeg",500));
var request=new FleetPickupModels.Pickup(2L,"pickup-photo-key",5,List.of(88L),null);
service.picked("TR-1","C1","WT-1","OP",request);
assertThat(task.getStatus()).isEqualTo("PICKED");assertThat(trip.getStatus()).isEqualTo("IN_TRANSIT");assertThat(trip.getDepartedAt()).isNotNull();
verify(departureReview).requireApproved(9L);verify(trips,times(1)).recordExecution(any(),eq(2L),eq("PICKUP_PICKED"),anyString(),eq("OP"));
service.picked("TR-1","C1","WT-1","OP",request);verify(repository,times(1)).update(task);
}
/** 原车队发车接口不能在两级审核未通过时写入实际发车事实。 */
@Test void legacyDepartureCannotBypassReview() {
trip.setStatus("PLANNED");
doThrow(new IllegalStateException("请先完成两级审核")).when(departureReview).requireApproved(9L);
assertThatThrownBy(()->service.depart("TR-1","OP",new FleetPickupModels.Command(2L,"depart-review-key","发车"))).hasMessageContaining("两级审核");
assertThat(trip.getStatus()).isEqualTo("PLANNED");assertThat(trip.getDepartedAt()).isNull();
verify(trips,never()).recordExecution(any(),anyLong(),anyString(),anyString(),anyString());
assertThat(commands).isEmpty();
}
/** 原单所属域锁定来源后,车队仅以该来源核对真实送仓事实。 */
@Test void pickupMustArriveBeforePda() {
assertThatThrownBy(()->service.requireArrived(1L,"WAREHOUSE_PICKUP","WT-1","WT-1")).hasMessageContaining("先完成车辆取货送仓");
@@ -79,10 +106,23 @@ class FleetPickupServiceTest {
}
@Test void arrivalOpensPdaAndCompletionKeepsWarehouseOrderInMeasurement() {
task.setStatus("PICKED");task.setActualPackages(5);task.setPickedAt(LocalDateTime.now(ZoneOffset.UTC));
service.arrive("TR-1","C1","WT-1","OP",new FleetPickupModels.Command(2L,"arrive-key-1","已送海外仓月台"));
when(repository.ownsUpload(1L,"TR-1",89L)).thenReturn(true);
when(files.metadata(9L,89L)).thenReturn(new TripOperationsRecordModels.EvidenceFile("89","到仓照片.jpg","image/jpeg",500));
var request=new FleetPickupModels.Command(2L,"arrive-key-1","已送海外仓月台",List.of(89L));
service.arrive("TR-1","C1","WT-1","OP",request);
assertThat(task.getStatus()).isEqualTo("ARRIVED");assertThat(source.getStatus()).isEqualTo("WAIT_PDA_RECEIVING");
service.complete("TR-1","OP",new FleetPickupModels.Command(3L,"complete-key-1","全部送仓"));
assertThat(trip.getStatus()).isEqualTo("COMPLETED");assertThat(trip.getCompletionJson()).contains("complete-key-1");assertThat(source.getStatus()).isEqualTo("WAIT_PDA_RECEIVING");
assertThat(trip.getStatus()).isEqualTo("COMPLETED");assertThat(trip.getCompletionJson()).contains("arrive-key-1");assertThat(source.getStatus()).isEqualTo("WAIT_PDA_RECEIVING");
assertThat(task.getEvidenceJson()).contains("ARRIVAL","到仓照片");
service.arrive("TR-1","C1","WT-1","OP",request);verify(repository,times(1)).update(task);
}
/** 到仓必须上传独立的新照片,不能用取货照片冒充或提前开放清点。 */
@Test void arrivalRequiresFreshPhotoEvidence() {
task.setStatus("PICKED");
task.setEvidenceJson("[{\"fileId\":\"88\",\"fileName\":\"取货.jpg\",\"contentType\":\"image/jpeg\",\"sizeBytes\":1,\"phase\":\"PICKUP\"}]");
assertThatThrownBy(()->service.arrive("TR-1","C1","WT-1","OP",new FleetPickupModels.Command(2L,"no-photo","送仓"))).hasMessageContaining("上传到仓照片");
when(repository.ownsUpload(1L,"TR-1",88L)).thenReturn(true);
assertThatThrownBy(()->service.arrive("TR-1","C1","WT-1","OP",new FleetPickupModels.Command(2L,"same-photo","送仓",List.of(88L)))).hasMessageContaining("不能复用取货照片");
assertThat(task.getStatus()).isEqualTo("PICKED");verify(repository,never()).update(any());
}
@Test void staleVersionAndWrongTripPurposeAreRejected() {
assertThatThrownBy(()->service.arrive("TR-1","C1","WT-1","OP",new FleetPickupModels.Command(1L,"arrive-key-1","送仓"))).hasMessageContaining("车次已变化");
@@ -122,6 +122,8 @@ class FleetTripAuthorizationTest {
@Bean com.baiyue.lastleg.fleet.business.FleetLooseboardDemandRepository looseboardDemandRepository() { return mock(com.baiyue.lastleg.fleet.business.FleetLooseboardDemandRepository.class); }
/** 既有取货能力依赖原仓储引用;权限测试使用只读替身,不连接数据库。 */
@Bean com.baiyue.lastleg.fleet.pickup.FleetPickupRepository pickups() { return mock(com.baiyue.lastleg.fleet.pickup.FleetPickupRepository.class); }
/** 取消原单使用所属域替身,权限测试不写业务数据。 */
@Bean com.baiyue.lastleg.fleet.pickup.PickupOrderCancellation pickupCancellation() { return mock(com.baiyue.lastleg.fleet.pickup.PickupOrderCancellation.class); }
@Bean com.baiyue.lastleg.fleet.trip.work.AppointmentWorkTripChanges workTripChanges() { return mock(com.baiyue.lastleg.fleet.trip.work.AppointmentWorkTripChanges.class); }
@Bean com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort workDispatches() { return mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class); }
@Bean FleetTripCalendarService calendar() { return mock(FleetTripCalendarService.class); }