fix(lastleg): expose dispatch reviews and filter available capacity offers
This commit is contained in:
+13
-1
@@ -15,10 +15,22 @@ public final class DispatchRequests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String dispatchNo,
|
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 Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum Stage {
|
||||||
|
BEFORE_LOADING, LOADING, AFTER_LOADING, CANCELLED;
|
||||||
|
public List<String> 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,
|
public record PreArrange(@Size(max = 500) List<@NotNull Long> planPalletIds,
|
||||||
@Size(max = 64) String fbaAppointmentNo,
|
@Size(max = 64) String fbaAppointmentNo,
|
||||||
@Size(max = 32) String fbaAppointmentStatus,
|
@Size(max = 32) String fbaAppointmentStatus,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public record DispatchResponse(Long id, String dispatchNo, String vehiclePlate,
|
|||||||
int itemCount, int palletCount, int packageCount, int pickedPackageCount,
|
int itemCount, int palletCount, int packageCount, int pickedPackageCount,
|
||||||
int shippedPackageCount, int remainingPackageCount, String partialCompletionReason,
|
int shippedPackageCount, int remainingPackageCount, String partialCompletionReason,
|
||||||
LocalDateTime createdAt,
|
LocalDateTime createdAt,
|
||||||
List<Item> items, List<BolLine> bolLines) {
|
List<Item> items, List<BolLine> bolLines, com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery.Workflow workflow) {
|
||||||
public record Item(Long id, Long planPalletId, Long warehousePalletId, String palletNo,
|
public record Item(Long id, Long planPalletId, Long warehousePalletId, String palletNo,
|
||||||
String overseasWarehouseId, String overseasWarehouseName,
|
String overseasWarehouseId, String overseasWarehouseName,
|
||||||
String orderNo, String orderRefs, String inboundCode, String destinationWarehouseCode,
|
String orderNo, String orderRefs, String inboundCode, String destinationWarehouseCode,
|
||||||
|
|||||||
+13
-2
@@ -75,6 +75,7 @@ public class DispatchService {
|
|||||||
@Resource private LastlegOrderMapper tailOrders;
|
@Resource private LastlegOrderMapper tailOrders;
|
||||||
@Resource private OutboundOrderMapper outboundOrders;
|
@Resource private OutboundOrderMapper outboundOrders;
|
||||||
@Resource private FleetDispatchBindingGuard fleetBindings;
|
@Resource private FleetDispatchBindingGuard fleetBindings;
|
||||||
|
@Resource private com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow;
|
||||||
@Resource private RelabelApplicationMapper relabelApplications;
|
@Resource private RelabelApplicationMapper relabelApplications;
|
||||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard relabelBindingGuard;
|
@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.dispatchNo()), DispatchOrder::getDispatchNo, query.dispatchNo())
|
||||||
.like(StringUtils.hasText(query.vehiclePlate()), DispatchOrder::getVehiclePlate, query.vehiclePlate())
|
.like(StringUtils.hasText(query.vehiclePlate()), DispatchOrder::getVehiclePlate, query.vehiclePlate())
|
||||||
.eq(StringUtils.hasText(query.status()), DispatchOrder::getStatus, query.status())
|
.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,
|
.like(StringUtils.hasText(query.fbaAppointmentNo()), DispatchOrder::getFbaAppointmentNo,
|
||||||
query.fbaAppointmentNo())
|
query.fbaAppointmentNo())
|
||||||
.orderByDesc(DispatchOrder::getCreatedAt));
|
.orderByDesc(DispatchOrder::getCreatedAt));
|
||||||
|
var workflows = acrossCustomers ? fleetWorkflow.read(page.getRecords().stream()
|
||||||
|
.map(DispatchOrder::getDispatchNo).toList()) : java.util.Map.<String, com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery.Workflow>of();
|
||||||
List<DispatchResponse> records = page.getRecords().stream()
|
List<DispatchResponse> 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());
|
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<DispatchItem> itemList,
|
private DispatchResponse response(DispatchOrder order, List<DispatchItem> itemList,
|
||||||
List<DispatchResponse.BolLine> bolLineList) {
|
List<DispatchResponse.BolLine> bolLineList) {
|
||||||
|
return response(order, itemList, bolLineList, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DispatchResponse response(DispatchOrder order, List<DispatchItem> itemList,
|
||||||
|
List<DispatchResponse.BolLine> bolLineList, com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery.Workflow workflow) {
|
||||||
String orderNos = itemList.stream().flatMap(item -> splitOrderRefs(item.getOrderRefs(), item.getOrderNo()).stream())
|
String orderNos = itemList.stream().flatMap(item -> splitOrderRefs(item.getOrderRefs(), item.getOrderNo()).stream())
|
||||||
.distinct().collect(Collectors.joining(","));
|
.distinct().collect(Collectors.joining(","));
|
||||||
return new DispatchResponse(order.getId(), order.getDispatchNo(), order.getVehiclePlate(), order.getDriverName(),
|
return new DispatchResponse(order.getId(), order.getDispatchNo(), order.getVehiclePlate(), order.getDriverName(),
|
||||||
@@ -782,7 +793,7 @@ public class DispatchService {
|
|||||||
item.getDestinationWarehouseCode(), item.getDestinationWarehouseName(), item.getFbaWarehouseCode(),
|
item.getDestinationWarehouseCode(), item.getDestinationWarehouseName(), item.getFbaWarehouseCode(),
|
||||||
Objects.requireNonNullElse(item.getPackageCount(), 0), item.getGrossWeightKg(),
|
Objects.requireNonNullElse(item.getPackageCount(), 0), item.getGrossWeightKg(),
|
||||||
item.getVolumeWeightKg(), item.getVolumeCbm(), item.getSourceStage())).toList(),
|
item.getVolumeWeightKg(), item.getVolumeCbm(), item.getSourceStage())).toList(),
|
||||||
bolLineList == null ? List.of() : bolLineList);
|
bolLineList == null ? List.of() : bolLineList, workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String candidateRoute(List<DispatchPalletCandidate> selected) {
|
private static String candidateRoute(List<DispatchPalletCandidate> selected) {
|
||||||
|
|||||||
+4
-2
@@ -7,7 +7,9 @@ public interface FleetCapacityOfferMapper extends BaseMapper<FleetCapacityOfferE
|
|||||||
<script>
|
<script>
|
||||||
SELECT o.* FROM ll_fleet_capacity_offer o JOIN ll_fleet_trip t ON t.id=o.trip_id
|
SELECT o.* FROM ll_fleet_capacity_offer o JOIN ll_fleet_trip t ON t.id=o.trip_id
|
||||||
<where>
|
<where>
|
||||||
<if test="salesOnly">o.status='OPEN'</if>
|
<if test="availableOnly">o.status='OPEN' AND t.status='PLANNED'
|
||||||
|
AND o.cutoff_at > #{now} AND o.cutoff_at <= t.planned_departure_at
|
||||||
|
</if>
|
||||||
<if test="q.status != null">AND o.status=#{q.status}</if>
|
<if test="q.status != null">AND o.status=#{q.status}</if>
|
||||||
<if test="q.tripNo != null and q.tripNo != ''">AND o.trip_no=#{q.tripNo}</if>
|
<if test="q.tripNo != null and q.tripNo != ''">AND o.trip_no=#{q.tripNo}</if>
|
||||||
<if test="q.keyword != null and q.keyword != ''">AND (o.offer_no LIKE CONCAT('%',#{q.keyword},'%') OR o.trip_no LIKE CONCAT('%',#{q.keyword},'%'))</if>
|
<if test="q.keyword != null and q.keyword != ''">AND (o.offer_no LIKE CONCAT('%',#{q.keyword},'%') OR o.trip_no LIKE CONCAT('%',#{q.keyword},'%'))</if>
|
||||||
@@ -21,7 +23,7 @@ public interface FleetCapacityOfferMapper extends BaseMapper<FleetCapacityOfferE
|
|||||||
""")
|
""")
|
||||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<FleetCapacityOfferEntity> pageOffers(
|
com.baomidou.mybatisplus.extension.plugins.pagination.Page<FleetCapacityOfferEntity> pageOffers(
|
||||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<FleetCapacityOfferEntity> page,
|
com.baomidou.mybatisplus.extension.plugins.pagination.Page<FleetCapacityOfferEntity> 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}")
|
@Select("SELECT * FROM ll_fleet_capacity_offer WHERE offer_no=#{offerNo}")
|
||||||
FleetCapacityOfferEntity byNo(@Param("offerNo") String offerNo);
|
FleetCapacityOfferEntity byNo(@Param("offerNo") String offerNo);
|
||||||
@Select("SELECT * FROM ll_fleet_capacity_offer WHERE offer_no=#{offerNo} FOR UPDATE")
|
@Select("SELECT * FROM ll_fleet_capacity_offer WHERE offer_no=#{offerNo} FOR UPDATE")
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@ public final class FleetCapacityOfferModels {
|
|||||||
public record Query(@Min(1) Integer page,@Min(1) @Max(100) Integer size,
|
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,
|
@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,
|
LocalDate departureFrom,LocalDate departureTo,@Pattern(regexp="DRAFT|OPEN|PAUSED|CLOSED") String status,
|
||||||
Boolean canConfirm,@Size(max=64) String tripNo) {
|
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;}
|
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,
|
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,
|
@Size(max=128) String keyword,@Size(max=64) String offerNo,@Size(max=32) String status,
|
||||||
|
|||||||
+2
-1
@@ -52,7 +52,8 @@ public class FleetCapacityOfferService {
|
|||||||
@Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
|
@Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
|
||||||
public PageResult<FleetCapacityOfferModels.Offer> page(FleetCapacityOfferModels.Query query){
|
public PageResult<FleetCapacityOfferModels.Offer> page(FleetCapacityOfferModels.Query query){
|
||||||
readPermission();
|
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趟车次,请缩小起运仓、目的地或日期范围");
|
if(query.canConfirm()!=null&&result.getTotal()>100)throw bad("实时接单资格筛选最多核对100趟车次,请缩小起运仓、目的地或日期范围");
|
||||||
var values=result.getRecords().stream().map(row->project(row,trip(row.getTripNo(),false)))
|
var values=result.getRecords().stream().map(row->project(row,trip(row.getTripNo(),false)))
|
||||||
.filter(value->query.canConfirm()==null||query.canConfirm()==value.canConfirm()).toList();
|
.filter(value->query.canConfirm()==null||query.canConfirm()==value.canConfirm()).toList();
|
||||||
|
|||||||
+43
@@ -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<String, Workflow> read(List<String> 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<String, Workflow> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,6 +92,10 @@ paths:
|
|||||||
- OPEN
|
- OPEN
|
||||||
- PAUSED
|
- PAUSED
|
||||||
- CLOSED
|
- CLOSED
|
||||||
|
- name: scope
|
||||||
|
in: query
|
||||||
|
description: 默认AVAILABLE,仅开放且车次仍PLANNED、当前服务端时间早于交货截止且截止不晚于计划发车;数据库分页前筛选,不以容量已知或有余位为条件。ALL包含草稿和历史,仅管理权限可用;车次发布设置读取已有发布须显式传ALL。
|
||||||
|
schema: { type: string, enum: [AVAILABLE, ALL], default: AVAILABLE }
|
||||||
- name: canConfirm
|
- name: canConfirm
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -586,6 +586,10 @@ paths:
|
|||||||
- { name: vehiclePlate, in: query, schema: { type: string } }
|
- { name: vehiclePlate, in: query, schema: { type: string } }
|
||||||
- { name: status, in: query, schema: { type: string } }
|
- { name: status, in: query, schema: { type: string } }
|
||||||
- { name: fbaAppointmentNo, 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:
|
responses:
|
||||||
'200': { description: 统一 ApiResponse 包装的排车单分页,records 元素为 DispatchResponse }
|
'200': { description: 统一 ApiResponse 包装的排车单分页,records 元素为 DispatchResponse }
|
||||||
/api/platform/v1/last-leg/dispatches/{dispatchNo}:
|
/api/platform/v1/last-leg/dispatches/{dispatchNo}:
|
||||||
@@ -2947,10 +2951,28 @@ components:
|
|||||||
partialCompletionReason: { type: string, nullable: true }
|
partialCompletionReason: { type: string, nullable: true }
|
||||||
createdAt: { type: string, format: date-time }
|
createdAt: { type: string, format: date-time }
|
||||||
items: { type: array, items: { $ref: '#/components/schemas/DispatchItem' } }
|
items: { type: array, items: { $ref: '#/components/schemas/DispatchItem' } }
|
||||||
|
workflow:
|
||||||
|
nullable: true
|
||||||
|
description: 仅平台分页返回当前关联车次及两级审核;未关联为null。批量读取已有审核快照,不由执行状态推导;列表权限可读,审核操作仍需车次独立审核权限。
|
||||||
|
allOf: [{ $ref: '#/components/schemas/DispatchWorkflow' }]
|
||||||
bolLines:
|
bolLines:
|
||||||
type: array
|
type: array
|
||||||
description: 仅详情接口返回的BOL打印聚合行;分页接口返回空数组,避免列表查询放大
|
description: 仅详情接口返回的BOL打印聚合行;分页接口返回空数组,避免列表查询放大
|
||||||
items: { $ref: '#/components/schemas/DispatchBolLine' }
|
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:
|
DispatchItem:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ class DispatchQueryControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void platformPageQueriesAcrossCustomersWithoutCustomerId() {
|
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));
|
when(service.platformPage(query)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||||
|
|
||||||
var response = controller.page(platform(), query);
|
var response = controller.page(platform(), query);
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@ class DispatchBolPrintServiceTest {
|
|||||||
null, null, null, LocalDateTime.of(2026, 9, 12, 2, 40, 20), "PENDING_DEPARTURE",
|
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(),
|
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",
|
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",
|
var address = new DomesticWarehouseDirectoryClient.Address("OPERATING", "US", "California", "Rancho Cucamonga",
|
||||||
null, "91730", "10767 7th Street", null);
|
null, "91730", "10767 7th Street", null);
|
||||||
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
|
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
|
||||||
@@ -87,7 +87,7 @@ class DispatchBolPrintServiceTest {
|
|||||||
new DispatchResponse.BolLine("TSTU3539519", "DP-001", "ABE2", "ABE2 Warehouse",
|
new DispatchResponse.BolLine("TSTU3539519", "DP-001", "ABE2", "ABE2 Warehouse",
|
||||||
"NYEXX2609110214", null, null, 1),
|
"NYEXX2609110214", null, null, 1),
|
||||||
new DispatchResponse.BolLine("TSTU3539519", "DP-002", "ONT8", "ONT8 Warehouse",
|
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,
|
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
|
||||||
"US-LAX", "洛杉矶仓", "OVERSEAS", "US", "ACTIVE");
|
"US-LAX", "洛杉矶仓", "OVERSEAS", "US", "ACTIVE");
|
||||||
when(dispatches.platformDetail(detail.dispatchNo())).thenReturn(detail);
|
when(dispatches.platformDetail(detail.dispatchNo())).thenReturn(detail);
|
||||||
|
|||||||
+21
-1
@@ -63,11 +63,13 @@ class DispatchServiceTest {
|
|||||||
@Mock LastlegOrderMapper tailOrders;
|
@Mock LastlegOrderMapper tailOrders;
|
||||||
@Mock OutboundOrderMapper outboundOrders;
|
@Mock OutboundOrderMapper outboundOrders;
|
||||||
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings;
|
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings;
|
||||||
|
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow;
|
||||||
private DispatchService service;
|
private DispatchService service;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
service = new DispatchService();
|
service = new DispatchService();
|
||||||
|
ReflectionTestUtils.setField(service, "fleetWorkflow", fleetWorkflow);
|
||||||
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
|
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
|
||||||
ReflectionTestUtils.setField(service, "orders", orders);
|
ReflectionTestUtils.setField(service, "orders", orders);
|
||||||
ReflectionTestUtils.setField(service, "items", items);
|
ReflectionTestUtils.setField(service, "items", items);
|
||||||
@@ -435,6 +437,24 @@ class DispatchServiceTest {
|
|||||||
verify(bolLines).findByDispatchOrder("200", 1L);
|
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<DispatchOrder> resultPage = new Page<>(2, 10, 47);
|
||||||
|
resultPage.setRecords(List.of());
|
||||||
|
when(orders.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenAnswer(invocation -> {
|
||||||
|
LambdaQueryWrapper<DispatchOrder> 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
|
@Test
|
||||||
void platformPageQueriesAllCustomersAndLoadsItemsInEachCustomerScope() {
|
void platformPageQueriesAllCustomersAndLoadsItemsInEachCustomerScope() {
|
||||||
DispatchOrder first = new DispatchOrder();
|
DispatchOrder first = new DispatchOrder();
|
||||||
@@ -445,7 +465,7 @@ class DispatchServiceTest {
|
|||||||
resultPage.setRecords(List.of(first, second));
|
resultPage.setRecords(List.of(first, second));
|
||||||
when(orders.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(resultPage);
|
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());
|
assertEquals(2, result.total());
|
||||||
verify(items).findByDispatchOrder("100", 1L);
|
verify(items).findByDispatchOrder("100", 1L);
|
||||||
|
|||||||
+33
-5
@@ -36,22 +36,50 @@ class FleetCapacityOfferServiceTest {
|
|||||||
@AfterEach void clear(){SecurityContextHolder.clearContext();}
|
@AfterEach void clear(){SecurityContextHolder.clearContext();}
|
||||||
@Test void ordinaryPageOnlyCalculatesRequestedDatabasePageAndPreservesAccurateTotal(){
|
@Test void ordinaryPageOnlyCalculatesRequestedDatabasePageAndPreservesAccurateTotal(){
|
||||||
var rows=List.of(offer(1),offer(2));var page=new Page<FleetCapacityOfferEntity>(3,20,2000);page.setRecords(rows);
|
var rows=List.of(offer(1),offer(2));var page=new Page<FleetCapacityOfferEntity>(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);
|
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(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()));}
|
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);
|
var result=service.page(query);
|
||||||
assertThat(result.total()).isEqualTo(2000);assertThat(result.page()).isEqualTo(3);assertThat(result.records()).hasSize(2);
|
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);
|
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);
|
verify(execution,times(2)).capacityPlan(anyString());verify(trips,times(2)).findByNo(anyString());verifyNoMoreInteractions(execution,trips);
|
||||||
}
|
}
|
||||||
@Test void broadDynamicAvailabilityFilterRejectsBeforeAnyPerTripCapacityQuery(){
|
@Test void broadDynamicAvailabilityFilterRejectsBeforeAnyPerTripCapacityQuery(){
|
||||||
when(offers.pageOffers(any(),any(),eq(true))).thenReturn(new Page<>(1,100,101));
|
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))).hasMessageContaining("100");
|
assertThatThrownBy(()->service.page(new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,true,null,null))).hasMessageContaining("100");
|
||||||
verifyNoInteractions(execution,trips);
|
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<String,Object>();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(){
|
@Test void cutoffUsesStrictServerInstantAndRechecksChangedDeparture(){
|
||||||
var trip=trip("FT-A");var offer=offer(1);offer.setStatus("OPEN");
|
var trip=trip("FT-A");var offer=offer(1);offer.setStatus("OPEN");
|
||||||
assertThat(FleetCapacityOfferService.openBlock(offer,trip,now)).isNull();
|
assertThat(FleetCapacityOfferService.openBlock(offer,trip,now)).isNull();
|
||||||
|
|||||||
+33
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user