From 136a2bc3b538766beb42f94c5a3277d9a28539e1 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 8 Oct 2026 14:11:55 +0800 Subject: [PATCH] fix(lastleg): expose dispatch reviews and filter available capacity offers --- .../dispatch/dto/DispatchRequests.java | 14 +++++- .../dispatch/dto/DispatchResponse.java | 2 +- .../dispatch/service/DispatchService.java | 15 ++++++- .../fleet/trip/FleetCapacityOfferMapper.java | 6 ++- .../fleet/trip/FleetCapacityOfferModels.java | 4 +- .../fleet/trip/FleetCapacityOfferService.java | 3 +- .../trip/FleetDispatchWorkflowQuery.java | 43 +++++++++++++++++++ .../openapi/fleet-capacity-offers-v1.yaml | 4 ++ .../openapi/overseas-business-v1.yaml | 22 ++++++++++ .../DispatchQueryControllerTest.java | 2 +- .../service/DispatchBolPrintServiceTest.java | 4 +- .../dispatch/service/DispatchServiceTest.java | 22 +++++++++- .../trip/FleetCapacityOfferServiceTest.java | 38 +++++++++++++--- .../trip/FleetDispatchWorkflowQueryTest.java | 33 ++++++++++++++ 14 files changed, 194 insertions(+), 18 deletions(-) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQuery.java create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQueryTest.java diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java index 52a94c557..a8d9e405d 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java @@ -15,10 +15,22 @@ public final class DispatchRequests { } public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String dispatchNo, - String vehiclePlate, String status, String fbaAppointmentNo) { + String vehiclePlate, String status, String fbaAppointmentNo, Stage stage) { public Query { if (page == 0) page = 1; if (size == 0) size = 20; } } + public enum Stage { + BEFORE_LOADING, LOADING, AFTER_LOADING, CANCELLED; + public List statuses() { + return switch (this) { + case BEFORE_LOADING -> List.of("PENDING_DEPARTURE"); + case LOADING -> List.of("PICKING", "READY_TO_DEPART", "PARTIAL_READY_TO_DEPART"); + case AFTER_LOADING -> List.of("DEPARTED", "PARTIAL_DEPARTED"); + case CANCELLED -> List.of("CANCELLED"); + }; + } + } + public record PreArrange(@Size(max = 500) List<@NotNull Long> planPalletIds, @Size(max = 64) String fbaAppointmentNo, @Size(max = 32) String fbaAppointmentStatus, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java index b487c19b0..1eacc603e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java @@ -12,7 +12,7 @@ public record DispatchResponse(Long id, String dispatchNo, String vehiclePlate, int itemCount, int palletCount, int packageCount, int pickedPackageCount, int shippedPackageCount, int remainingPackageCount, String partialCompletionReason, LocalDateTime createdAt, - List items, List bolLines) { + List items, List bolLines, com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery.Workflow workflow) { public record Item(Long id, Long planPalletId, Long warehousePalletId, String palletNo, String overseasWarehouseId, String overseasWarehouseName, String orderNo, String orderRefs, String inboundCode, String destinationWarehouseCode, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java index 62a86b738..800b4c12e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java @@ -75,6 +75,7 @@ public class DispatchService { @Resource private LastlegOrderMapper tailOrders; @Resource private OutboundOrderMapper outboundOrders; @Resource private FleetDispatchBindingGuard fleetBindings; + @Resource private com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow; @Resource private RelabelApplicationMapper relabelApplications; @Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard relabelBindingGuard; @@ -623,11 +624,16 @@ public class DispatchService { .like(StringUtils.hasText(query.dispatchNo()), DispatchOrder::getDispatchNo, query.dispatchNo()) .like(StringUtils.hasText(query.vehiclePlate()), DispatchOrder::getVehiclePlate, query.vehiclePlate()) .eq(StringUtils.hasText(query.status()), DispatchOrder::getStatus, query.status()) + .in(query.stage() != null, DispatchOrder::getStatus, + query.stage() == null ? List.of() : query.stage().statuses()) .like(StringUtils.hasText(query.fbaAppointmentNo()), DispatchOrder::getFbaAppointmentNo, query.fbaAppointmentNo()) .orderByDesc(DispatchOrder::getCreatedAt)); + var workflows = acrossCustomers ? fleetWorkflow.read(page.getRecords().stream() + .map(DispatchOrder::getDispatchNo).toList()) : java.util.Map.of(); List records = page.getRecords().stream() - .map(order -> response(order, items.findByDispatchOrder(order.getCustomerId(), order.getId()))).toList(); + .map(order -> response(order, items.findByDispatchOrder(order.getCustomerId(), order.getId()), + List.of(), workflows.get(order.getDispatchNo()))).toList(); return new PageResult<>(records, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages()); } @@ -764,6 +770,11 @@ public class DispatchService { private DispatchResponse response(DispatchOrder order, List itemList, List bolLineList) { + return response(order, itemList, bolLineList, null); + } + + private DispatchResponse response(DispatchOrder order, List itemList, + List bolLineList, com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery.Workflow workflow) { String orderNos = itemList.stream().flatMap(item -> splitOrderRefs(item.getOrderRefs(), item.getOrderNo()).stream()) .distinct().collect(Collectors.joining(",")); return new DispatchResponse(order.getId(), order.getDispatchNo(), order.getVehiclePlate(), order.getDriverName(), @@ -782,7 +793,7 @@ public class DispatchService { item.getDestinationWarehouseCode(), item.getDestinationWarehouseName(), item.getFbaWarehouseCode(), Objects.requireNonNullElse(item.getPackageCount(), 0), item.getGrossWeightKg(), item.getVolumeWeightKg(), item.getVolumeCbm(), item.getSourceStage())).toList(), - bolLineList == null ? List.of() : bolLineList); + bolLineList == null ? List.of() : bolLineList, workflow); } private static String candidateRoute(List selected) { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferMapper.java index 188c4731c..10f34198d 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferMapper.java @@ -7,7 +7,9 @@ public interface FleetCapacityOfferMapper extends BaseMapper SELECT o.* FROM ll_fleet_capacity_offer o JOIN ll_fleet_trip t ON t.id=o.trip_id - o.status='OPEN' + o.status='OPEN' AND t.status='PLANNED' + AND o.cutoff_at > #{now} AND o.cutoff_at <= t.planned_departure_at + AND o.status=#{q.status} AND o.trip_no=#{q.tripNo} AND (o.offer_no LIKE CONCAT('%',#{q.keyword},'%') OR o.trip_no LIKE CONCAT('%',#{q.keyword},'%')) @@ -21,7 +23,7 @@ public interface FleetCapacityOfferMapper extends BaseMapper pageOffers( com.baomidou.mybatisplus.extension.plugins.pagination.Page page, - @Param("q") FleetCapacityOfferModels.Query query,@Param("salesOnly") boolean salesOnly); + @Param("q") FleetCapacityOfferModels.Query query,@Param("availableOnly") boolean availableOnly,@Param("now") java.time.LocalDateTime now); @Select("SELECT * FROM ll_fleet_capacity_offer WHERE offer_no=#{offerNo}") FleetCapacityOfferEntity byNo(@Param("offerNo") String offerNo); @Select("SELECT * FROM ll_fleet_capacity_offer WHERE offer_no=#{offerNo} FOR UPDATE") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferModels.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferModels.java index 875513689..51d53dd31 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferModels.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferModels.java @@ -16,8 +16,8 @@ public final class FleetCapacityOfferModels { public record Query(@Min(1) Integer page,@Min(1) @Max(100) Integer size, @Size(max=128) String keyword,@Size(max=128) String origin,@Size(max=128) String destination, LocalDate departureFrom,LocalDate departureTo,@Pattern(regexp="DRAFT|OPEN|PAUSED|CLOSED") String status, - Boolean canConfirm,@Size(max=64) String tripNo) { - public Query {page=page==null?1:page;size=size==null?20:size;} + Boolean canConfirm,@Size(max=64) String tripNo,@Pattern(regexp="AVAILABLE|ALL") String scope) { + public Query {page=page==null?1:page;size=size==null?20:size;scope=scope==null?"AVAILABLE":scope;} } public record InquiryQuery(@Min(1) Integer page,@Min(1) @Max(100) Integer size, @Size(max=128) String keyword,@Size(max=64) String offerNo,@Size(max=32) String status, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferService.java index d89d3054f..0d2ac1b1e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferService.java @@ -52,7 +52,8 @@ public class FleetCapacityOfferService { @Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ) public PageResult page(FleetCapacityOfferModels.Query query){ readPermission(); - var result=offers.pageOffers(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(query.canConfirm()==null?query.page():1,query.canConfirm()==null?query.size():100),query,!manage()); + if("ALL".equals(query.scope())&&!manage())throw forbidden(); + var result=offers.pageOffers(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(query.canConfirm()==null?query.page():1,query.canConfirm()==null?query.size():100),query,!"ALL".equals(query.scope()),LocalDateTime.ofInstant(clock.instant(),ZoneOffset.UTC)); if(query.canConfirm()!=null&&result.getTotal()>100)throw bad("实时接单资格筛选最多核对100趟车次,请缩小起运仓、目的地或日期范围"); var values=result.getRecords().stream().map(row->project(row,trip(row.getTripNo(),false))) .filter(value->query.canConfirm()==null||query.canConfirm()==value.canConfirm()).toList(); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQuery.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQuery.java new file mode 100644 index 000000000..2c9fef6c2 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQuery.java @@ -0,0 +1,43 @@ +package com.baiyue.lastleg.fleet.trip; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.stream.Collectors; + +/** Read existing reviews in batches; never infer approval from dispatch execution status. */ +@Service +public class FleetDispatchWorkflowQuery { + public record Review(String status, String reason, java.time.Instant reviewedAt) { } + public record Workflow(String tripNo, Review dispatchReview, Review loadingReview) { } + private final FleetTripDispatchMapper bindings; + private final FleetTripMapper trips; + private final ObjectMapper json; + + public FleetDispatchWorkflowQuery(FleetTripDispatchMapper bindings, FleetTripMapper trips, ObjectMapper json) { + this.bindings = bindings; this.trips = trips; this.json = json; + } + + public Map read(List dispatchNos) { + if (dispatchNos.isEmpty()) return Map.of(); + var members = bindings.findByDispatchNos(dispatchNos); + if (members.isEmpty()) return Map.of(); + var parents = trips.selectBatchIds(members.stream().map(FleetTripDispatch::getTripId).distinct().toList()) + .stream().collect(Collectors.toMap(FleetTripEntity::getId, trip -> trip)); + Map result = new HashMap<>(); + for (var member : members) { + var trip = parents.get(member.getTripId()); + if (trip == null) throw new IllegalStateException("关联车次不存在"); + result.put(member.getDispatchNo(), new Workflow(member.getTripNo(), + review(member.getReviewJson()), review(trip.getLoadingReviewJson()))); + } + return result; + } + + private Review review(String value) { + var state = FleetTripReview.read(json, value); + return new Review(state.status(), state.reason(), state.reviewedAt()); + } +} diff --git a/lastleg-service/src/main/resources/openapi/fleet-capacity-offers-v1.yaml b/lastleg-service/src/main/resources/openapi/fleet-capacity-offers-v1.yaml index 7ec891f89..96ff377c3 100644 --- a/lastleg-service/src/main/resources/openapi/fleet-capacity-offers-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/fleet-capacity-offers-v1.yaml @@ -92,6 +92,10 @@ paths: - OPEN - PAUSED - CLOSED + - name: scope + in: query + description: 默认AVAILABLE,仅开放且车次仍PLANNED、当前服务端时间早于交货截止且截止不晚于计划发车;数据库分页前筛选,不以容量已知或有余位为条件。ALL包含草稿和历史,仅管理权限可用;车次发布设置读取已有发布须显式传ALL。 + schema: { type: string, enum: [AVAILABLE, ALL], default: AVAILABLE } - name: canConfirm in: query required: false diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 6cfc4e2cd..67ebdd190 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -586,6 +586,10 @@ paths: - { name: vehiclePlate, in: query, schema: { type: string } } - { name: status, in: query, schema: { type: string } } - { name: fbaAppointmentNo, in: query, schema: { type: string } } + - name: stage + in: query + description: 服务端在分页前筛选;BEFORE_LOADING=待发车未拣货,LOADING=拣货中或拣货完成待发车,AFTER_LOADING=已发车(含部分),CANCELLED=已取消;与status取交集。 + schema: { type: string, enum: [BEFORE_LOADING, LOADING, AFTER_LOADING, CANCELLED] } responses: '200': { description: 统一 ApiResponse 包装的排车单分页,records 元素为 DispatchResponse } /api/platform/v1/last-leg/dispatches/{dispatchNo}: @@ -2947,10 +2951,28 @@ components: partialCompletionReason: { type: string, nullable: true } createdAt: { type: string, format: date-time } items: { type: array, items: { $ref: '#/components/schemas/DispatchItem' } } + workflow: + nullable: true + description: 仅平台分页返回当前关联车次及两级审核;未关联为null。批量读取已有审核快照,不由执行状态推导;列表权限可读,审核操作仍需车次独立审核权限。 + allOf: [{ $ref: '#/components/schemas/DispatchWorkflow' }] bolLines: type: array description: 仅详情接口返回的BOL打印聚合行;分页接口返回空数组,避免列表查询放大 items: { $ref: '#/components/schemas/DispatchBolLine' } + DispatchWorkflow: + type: object + required: [tripNo, dispatchReview, loadingReview] + properties: + tripNo: { type: string } + dispatchReview: { $ref: '#/components/schemas/DispatchReview' } + loadingReview: { $ref: '#/components/schemas/DispatchReview' } + DispatchReview: + type: object + required: [status] + properties: + status: { type: string, enum: [PENDING, APPROVED, REJECTED] } + reason: { type: string, nullable: true } + reviewedAt: { type: string, format: date-time, nullable: true } DispatchItem: type: object properties: diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryControllerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryControllerTest.java index 483f033f3..692fa2784 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryControllerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryControllerTest.java @@ -38,7 +38,7 @@ class DispatchQueryControllerTest { @Test void platformPageQueriesAcrossCustomersWithoutCustomerId() { - DispatchRequests.Query query = new DispatchRequests.Query(1, 20, null, null, null, null); + DispatchRequests.Query query = new DispatchRequests.Query(1, 20, null, null, null, null, null); when(service.platformPage(query)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0)); var response = controller.page(platform(), query); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintServiceTest.java index c0d40b167..bda740e5f 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintServiceTest.java @@ -55,7 +55,7 @@ class DispatchBolPrintServiceTest { null, null, null, LocalDateTime.of(2026, 9, 12, 2, 40, 20), "PENDING_DEPARTURE", 1, 1, 1, 0, 0, 1, null, LocalDateTime.of(2026, 9, 11, 10, 0), List.of(), List.of(new DispatchResponse.BolLine("TSTU3539519", "DP-260911-0027", "ABE2", "ABE2", - "NYEXX2609110214", null, null, 1))); + "NYEXX2609110214", null, null, 1)), null); var address = new DomesticWarehouseDirectoryClient.Address("OPERATING", "US", "California", "Rancho Cucamonga", null, "91730", "10767 7th Street", null); var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L, @@ -87,7 +87,7 @@ class DispatchBolPrintServiceTest { new DispatchResponse.BolLine("TSTU3539519", "DP-001", "ABE2", "ABE2 Warehouse", "NYEXX2609110214", null, null, 1), new DispatchResponse.BolLine("TSTU3539519", "DP-002", "ONT8", "ONT8 Warehouse", - "NYEXX2609110215", null, null, 1))); + "NYEXX2609110215", null, null, 1)), null); var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L, "US-LAX", "洛杉矶仓", "OVERSEAS", "US", "ACTIVE"); when(dispatches.platformDetail(detail.dispatchNo())).thenReturn(detail); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java index 1cbb76918..b3da416be 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java @@ -63,11 +63,13 @@ class DispatchServiceTest { @Mock LastlegOrderMapper tailOrders; @Mock OutboundOrderMapper outboundOrders; @Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings; + @Mock com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow; private DispatchService service; @BeforeEach void setUp() { service = new DispatchService(); + ReflectionTestUtils.setField(service, "fleetWorkflow", fleetWorkflow); ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings); ReflectionTestUtils.setField(service, "orders", orders); ReflectionTestUtils.setField(service, "items", items); @@ -435,6 +437,24 @@ class DispatchServiceTest { verify(bolLines).findByDispatchOrder("200", 1L); } + @Test + void filtersStageBeforePaginationAndPreservesTheServerTotal() { + com.baomidou.mybatisplus.core.metadata.TableInfoHelper.initTableInfo( + new org.apache.ibatis.builder.MapperBuilderAssistant(new com.baomidou.mybatisplus.core.MybatisConfiguration(), "test"), DispatchOrder.class); + Page resultPage = new Page<>(2, 10, 47); + resultPage.setRecords(List.of()); + when(orders.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenAnswer(invocation -> { + LambdaQueryWrapper wrapper = invocation.getArgument(1); + assertTrue(wrapper.getSqlSegment().contains("status IN")); + assertTrue(wrapper.getParamNameValuePairs().values().containsAll( + List.of("PICKING", "READY_TO_DEPART", "PARTIAL_READY_TO_DEPART"))); + return resultPage; + }); + var result = service.platformPage(new DispatchRequests.Query(2, 10, null, null, null, null, DispatchRequests.Stage.LOADING)); + assertEquals(47, result.total()); + assertEquals(List.of("DEPARTED", "PARTIAL_DEPARTED"), DispatchRequests.Stage.AFTER_LOADING.statuses()); + } + @Test void platformPageQueriesAllCustomersAndLoadsItemsInEachCustomerScope() { DispatchOrder first = new DispatchOrder(); @@ -445,7 +465,7 @@ class DispatchServiceTest { resultPage.setRecords(List.of(first, second)); when(orders.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(resultPage); - var result = service.platformPage(new DispatchRequests.Query(1, 20, null, null, null, null)); + var result = service.platformPage(new DispatchRequests.Query(1, 20, null, null, null, null, null)); assertEquals(2, result.total()); verify(items).findByDispatchOrder("100", 1L); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferServiceTest.java index 946c14a87..25a81baf9 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferServiceTest.java @@ -36,22 +36,50 @@ class FleetCapacityOfferServiceTest { @AfterEach void clear(){SecurityContextHolder.clearContext();} @Test void ordinaryPageOnlyCalculatesRequestedDatabasePageAndPreservesAccurateTotal(){ var rows=List.of(offer(1),offer(2));var page=new Page(3,20,2000);page.setRecords(rows); - when(offers.pageOffers(any(),any(),eq(true))).thenReturn(page); + when(offers.pageOffers(any(),any(),eq(true),any())).thenReturn(page); for(var row:rows){var trip=trip(row.getTripNo());when(trips.findByNo(row.getTripNo())).thenReturn(trip); when(reservations.stops(trip)).thenReturn(List.of(new FleetTripModels.Stop("起点",null,null),new FleetTripModels.Stop("ONT8",null,"FBA:ONT8"))); when(execution.capacityPlan(row.getTripNo())).thenReturn(new FleetExecutionModels.Planning(row.getTripNo(),1L,List.of(),"hash",null,false,"计划待核对",List.of()));} - var query=new FleetCapacityOfferModels.Query(3,20,"FO",null,null,null,null,null,null,null); + var query=new FleetCapacityOfferModels.Query(3,20,"FO",null,null,null,null,null,null,null,null); var result=service.page(query); assertThat(result.total()).isEqualTo(2000);assertThat(result.page()).isEqualTo(3);assertThat(result.records()).hasSize(2); - var captor=ArgumentCaptor.forClass(Page.class);verify(offers).pageOffers(captor.capture(),same(query),eq(true)); + assertThat(result.records()).allMatch(FleetCapacityOfferModels.Offer::canCreateInquiry).allMatch(value -> !value.canConfirm()); + var captor=ArgumentCaptor.forClass(Page.class);verify(offers).pageOffers(captor.capture(),same(query),eq(true),eq(LocalDateTime.ofInstant(now,ZoneOffset.UTC))); assertThat(captor.getValue().getCurrent()).isEqualTo(3);assertThat(captor.getValue().getSize()).isEqualTo(20); verify(execution,times(2)).capacityPlan(anyString());verify(trips,times(2)).findByNo(anyString());verifyNoMoreInteractions(execution,trips); } @Test void broadDynamicAvailabilityFilterRejectsBeforeAnyPerTripCapacityQuery(){ - when(offers.pageOffers(any(),any(),eq(true))).thenReturn(new Page<>(1,100,101)); - assertThatThrownBy(()->service.page(new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,true,null))).hasMessageContaining("100"); + when(offers.pageOffers(any(),any(),eq(true),any())).thenReturn(new Page<>(1,100,101)); + assertThatThrownBy(()->service.page(new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,true,null,null))).hasMessageContaining("100"); verifyNoInteractions(execution,trips); } + @Test void historyScopeRequiresManagementAndDefaultScopeIsAvailable(){ + var query=new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,null,null,null); + assertThat(query.scope()).isEqualTo("AVAILABLE"); + var history=new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,null,null,"ALL"); + assertThatThrownBy(()->service.page(history)).isInstanceOf(org.springframework.web.server.ResponseStatusException.class); + verifyNoInteractions(offers); + var user=new AuthenticatedUser(101L,0L,0L,101L,"admin","ADMIN","WEB",Set.of(FleetCapacityOfferService.MANAGE)); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user,"",List.of(new SimpleGrantedAuthority(FleetCapacityOfferService.MANAGE)))); + when(offers.pageOffers(any(),same(query),eq(true),any())).thenReturn(new Page<>(1,20,0)); + service.page(query); + verify(offers).pageOffers(any(),same(query),eq(true),eq(LocalDateTime.ofInstant(now,ZoneOffset.UTC))); + when(offers.pageOffers(any(),same(history),eq(false),any())).thenReturn(new Page<>(1,20,0)); + assertThat(service.page(history).records()).isEmpty(); + verify(offers).pageOffers(any(),same(history),eq(false),eq(LocalDateTime.ofInstant(now,ZoneOffset.UTC))); + } + @Test void availableSqlFiltersBeforePaginationWithoutRequiringKnownCapacity() throws Exception { + var method=Arrays.stream(FleetCapacityOfferMapper.class.getMethods()).filter(m->m.getName().equals("pageOffers")).findFirst().orElseThrow(); + String script=String.join("\n",method.getAnnotation(org.apache.ibatis.annotations.Select.class).value()); + var sql=new org.apache.ibatis.scripting.xmltags.XMLLanguageDriver().createSqlSource(new org.apache.ibatis.session.Configuration(),script,Map.class); + var query=new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,null,null,null); + var parameters=new HashMap();parameters.put("q",query);parameters.put("availableOnly",true);parameters.put("now",LocalDateTime.ofInstant(now,ZoneOffset.UTC)); + String active=sql.getBoundSql(parameters).getSql().replaceAll("\\s+"," "); + assertThat(active).contains("o.status='OPEN'", "t.status='PLANNED'", "o.cutoff_at > ?", "o.cutoff_at <= t.planned_departure_at"); + assertThat(active).doesNotContain("planned_load_json", "remaining", "NOW()"); + parameters.put("availableOnly",false); + assertThat(sql.getBoundSql(parameters).getSql()).doesNotContain("o.cutoff_at", "t.status='PLANNED'"); + } @Test void cutoffUsesStrictServerInstantAndRechecksChangedDeparture(){ var trip=trip("FT-A");var offer=offer(1);offer.setStatus("OPEN"); assertThat(FleetCapacityOfferService.openBlock(offer,trip,now)).isNull(); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQueryTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQueryTest.java new file mode 100644 index 000000000..0fff22d9f --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQueryTest.java @@ -0,0 +1,33 @@ +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 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()); + } +}