fix(lastleg): expose dispatch reviews and filter available capacity offers

This commit is contained in:
2026-10-08 14:11:55 +08:00
parent 1cb1f2d0c0
commit 136a2bc3b5
14 changed files with 194 additions and 18 deletions
@@ -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<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,
@Size(max = 64) String fbaAppointmentNo,
@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 shippedPackageCount, int remainingPackageCount, String partialCompletionReason,
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,
String overseasWarehouseId, String overseasWarehouseName,
String orderNo, String orderRefs, String inboundCode, String destinationWarehouseCode,
@@ -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.<String, com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery.Workflow>of();
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());
}
@@ -764,6 +770,11 @@ public class DispatchService {
private DispatchResponse response(DispatchOrder order, List<DispatchItem> itemList,
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())
.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<DispatchPalletCandidate> selected) {
@@ -7,7 +7,9 @@ public interface FleetCapacityOfferMapper extends BaseMapper<FleetCapacityOfferE
<script>
SELECT o.* FROM ll_fleet_capacity_offer o JOIN ll_fleet_trip t ON t.id=o.trip_id
<where>
<if test="salesOnly">o.status='OPEN'</if>
<if test="availableOnly">o.status='OPEN' AND t.status='PLANNED'
AND o.cutoff_at &gt; #{now} AND o.cutoff_at &lt;= t.planned_departure_at
</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.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> 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")
@@ -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,
@@ -52,7 +52,8 @@ public class FleetCapacityOfferService {
@Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
public PageResult<FleetCapacityOfferModels.Offer> 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();
@@ -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());
}
}