feat: add self-operated analytics and enforce Lastleg module boundaries

This commit is contained in:
2026-10-12 20:27:35 +08:00
parent c06e788b0f
commit a1f1d93902
174 changed files with 13514 additions and 490 deletions
@@ -0,0 +1,6 @@
package com.baiyue.lastleg.fleet.business;
/** Delivery owns source attribution, returns and full-signature proof. */
public interface FleetPoolDeliveryEvidence {
boolean fullySigned(String customerId,String incomingNo,long orderId,int expectedPackages);
}
@@ -1,15 +1,12 @@
package com.baiyue.lastleg.fleet.business;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.fleet.trip.FleetBookingIncomingGateway;
import com.baiyue.lastleg.looseboard.entity.*;
import com.baiyue.lastleg.looseboard.mapper.*;
import com.baiyue.lastleg.trucking.entity.*;
import com.baiyue.lastleg.trucking.mapper.*;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
import com.baiyue.lastleg.delivery.mapper.DeliverySourceEvidenceMapper;
import com.baiyue.lastleg.delivery.model.SourceDeliveryCompletionPolicy;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@@ -30,13 +27,13 @@ public class FleetPoolOrderBridge {
private final LocalLooseboardHistoryMapper looseHistory;
private final UsTruckOrderMapper trucks;
private final UsTruckOrderStatusHistoryMapper truckHistory;
private final LastlegOrderNoGenerator numbers;
private final FleetPoolOrderNumberGenerator numbers;
private final ObjectMapper json;
private final DeliverySourceEvidenceMapper deliveryEvidence;
private final FleetPoolDeliveryEvidence deliveryEvidence;
private final com.baiyue.lastleg.appointmentplanning.AppointmentSourceLinker appointmentSources;
public FleetPoolOrderBridge(FleetPoolOrderMapper links,LocalLooseboardOrderMapper loose,
LocalLooseboardHistoryMapper looseHistory,UsTruckOrderMapper trucks,
UsTruckOrderStatusHistoryMapper truckHistory,LastlegOrderNoGenerator numbers,ObjectMapper json,DeliverySourceEvidenceMapper deliveryEvidence,
UsTruckOrderStatusHistoryMapper truckHistory,FleetPoolOrderNumberGenerator numbers,ObjectMapper json,FleetPoolDeliveryEvidence deliveryEvidence,
com.baiyue.lastleg.appointmentplanning.AppointmentSourceLinker appointmentSources) {
this.links=links;this.loose=loose;this.looseHistory=looseHistory;this.trucks=trucks;
this.truckHistory=truckHistory;this.numbers=numbers;this.json=json;this.deliveryEvidence=deliveryEvidence;
@@ -111,7 +108,7 @@ public class FleetPoolOrderBridge {
if("WAITING_INBOUND".equals(row.getFulfillmentStage()))move(row,"RECEIVING",operator,"PDA开始实际收货 "+incomingNo,LocalDateTime.now());
}
@Transactional(propagation=Propagation.MANDATORY)
public void acceptedReceipt(long bookingId,FleetBookingIncomingGateway.Evidence evidence,int palletSpaces,String operator) {
public void acceptedReceipt(long bookingId,Receipt evidence,int palletSpaces,String operator) {
var row=requiredBooking(bookingId);
if(evidence==null||!Objects.equals(row.getIncomingNo(),evidence.incomingNo())
||!Objects.equals(row.getWarehouseId(),evidence.warehouseId())||!Objects.equals(row.getDestinationKey(),evidence.destinationKey())
@@ -157,33 +154,19 @@ public class FleetPoolOrderBridge {
}
}
@Transactional(propagation=Propagation.MANDATORY)
public void signed(DeliveryOrder delivery,Map<Long,Long> actualSourcePackages,String operator,LocalDateTime at) {
if(delivery.getDispatchNo()==null)return;
for(var row:links.lockDispatch(delivery.getCustomerId(),delivery.getDispatchNo())) {
public void signed(SignedDelivery delivery,Map<Long,Long> actualSourcePackages,String operator,LocalDateTime at) {
if(delivery.dispatchNo()==null)return;
for(var row:links.lockDispatch(delivery.customerId(),delivery.dispatchNo())) {
if(!actualSourcePackages.containsKey(row.getLastlegOrderId()))continue;
if(row.getActualPackageCount()==null||actualSourcePackages.get(row.getLastlegOrderId())!=row.getActualPackageCount().longValue())throw conflict("商业单签收实际来源数量不完整");
if("COMPLETED".equals(row.getFulfillmentStage()))continue;
if(!"IN_TRANSIT".equals(row.getFulfillmentStage()))throw conflict("商业单须实际出库后才能签收完成");
if("DELIVERED".equals(delivery.getStatus())&&delivery.getSignedAt()!=null&&wholeSourceSigned(row))
move(row,"COMPLETED",operator,"真实全部签收 "+delivery.getDeliveryNo(),at);
if(delivery.delivered()&&delivery.signedAt()!=null&&wholeSourceSigned(row))
move(row,"COMPLETED",operator,"真实全部签收 "+delivery.deliveryNo(),at);
}
}
private boolean wholeSourceSigned(FleetPoolOrderLink row) {
var dispatchIds=deliveryEvidence.dispatchIds(row.getCustomerId(),row.getLastlegOrderId());
if(dispatchIds==null||dispatchIds.isEmpty()||new HashSet<>(dispatchIds).size()!=dispatchIds.size())return false;
var deliveries=new ArrayList<SourceDeliveryCompletionPolicy.Delivery>();
var picks=new ArrayList<SourceDeliveryCompletionPolicy.Pick>();
var returns=new ArrayList<SourceDeliveryCompletionPolicy.Returned>();
for(Long dispatchId:dispatchIds) {
if(dispatchId==null||dispatchId<=0)return false;
var d=deliveryEvidence.deliveries(row.getCustomerId(),dispatchId);
var p=deliveryEvidence.picks(row.getCustomerId(),dispatchId);
var r=deliveryEvidence.returns(row.getCustomerId(),dispatchId);
if(d==null||d.isEmpty()||p==null||p.isEmpty()||r==null)return false;
deliveries.addAll(d);picks.addAll(p);returns.addAll(r);
}
return SourceDeliveryCompletionPolicy.evaluate(new SourceDeliveryCompletionPolicy.Source(row.getIncomingNo(),
row.getLastlegOrderId(),row.getActualPackageCount()),deliveries,picks,returns,true).fullySigned();
return deliveryEvidence.fullySigned(row.getCustomerId(),row.getIncomingNo(),row.getLastlegOrderId(),row.getActualPackageCount());
}
public Map<Long,String> businessOrderNumbers(Collection<Long> ids) {
if(ids==null||ids.isEmpty())return Map.of();var result=new HashMap<Long,String>();
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.fleet.business;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.Instant;
import java.util.List;
public final class FleetPoolOrderModels {
private FleetPoolOrderModels() { }
@@ -13,6 +14,14 @@ public final class FleetPoolOrderModels {
BigDecimal weightKg, BigDecimal volumeCbm, String currency, String contactName, String contactPhone,
String remark) { }
public record BusinessOrderRef(String businessType, String businessOrderNo) { }
/** Receiving facts needed by commercial fulfillment, independent of trip planning DTOs. */
public record ReceivedPallet(String warehousePalletId,int packages) { }
public record Receipt(String incomingNo,String warehouseId,String destinationKey,String status,
List<ReceivedPallet> pallets,int receivedPackages) {
public Receipt { pallets=List.copyOf(pallets); }
}
public record SignedDelivery(String customerId,String dispatchNo,String deliveryNo,
boolean delivered,LocalDateTime signedAt) { }
public record OrderKey(String customerId, String businessType, long businessOrderId) { }
public record Arrangement(String sourceType, String businessType, String businessOrderNo, String bookingNo,
String tripNo, Instant plannedDepartureAt, String originWarehouseName, String destinationLabel,
@@ -0,0 +1,23 @@
package com.baiyue.lastleg.fleet.business;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.regex.Pattern;
/** Commercial pool orders own LB/TR numbering; no tail-leg order sequence is consumed. */
@Component
public class FleetPoolOrderNumberGenerator {
private static final Pattern LOCATION=Pattern.compile("LAX|ONT|NYC|JFK|LGB|EWR|SFO|SEA|DFW|ORD|ATL|MIA|PHX|LAS|YYZ|YVR|AMS|LHR|FRA|CDG|HKG|NRT|SYD");
public String next(String businessType,String destination,long uniqueId){
String prefix=switch(businessType){case "TRUCK"->"TR";case "LOOSEBOARD"->"LB";default->throw new IllegalArgumentException("Unsupported pool business type");};
return prefix+"-"+location(destination)+"-"+LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)
+"-"+String.format(Locale.ROOT,"%06d",Math.floorMod(uniqueId,1_000_000));
}
private static String location(String value){
String code=value==null?"":value.trim().toUpperCase(Locale.ROOT);var match=LOCATION.matcher(code);
if(Pattern.matches(".*("+LOCATION.pattern()+").*",code)&&match.find())return match.group();
return code.matches("[A-Z0-9]{2,8}")?code:"WH";
}
}
@@ -1,4 +1,6 @@
package com.baiyue.lastleg.fleet.trip;
import com.baiyue.lastleg.fleet.business.FleetPoolOrderModels.Receipt;
import com.baiyue.lastleg.fleet.business.FleetPoolOrderModels.ReceivedPallet;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory;
@@ -168,7 +170,10 @@ public class FleetBookingService {
if(!"READY_FOR_DISPATCH".equals(confirmed.status())||!Objects.equals(confirmed.receiptHash(),evidence.receiptHash()))throw conflict("来货实收确认未完成或内容发生变化,请重新核对");
row.setReceiptJson(write(new FleetBookingModels.AcceptedReceipt(confirmed,java.util.List.copyOf(request.pallets()),operator,java.time.Instant.now())));
row.setStatus("RECEIVED");row.setReceiptKey(request.idempotencyKey());row.setReceiptHash(hash);
if(row.getOfferId()!=null)poolOrders.acceptedReceipt(row.getId(),confirmed,totalSpaces,operator);
if(row.getOfferId()!=null)poolOrders.acceptedReceipt(row.getId(),new Receipt(
confirmed.incomingNo(),confirmed.warehouseId(),confirmed.destinationKey(),confirmed.status(),
confirmed.pallets().stream().map(p->new ReceivedPallet(p.warehousePalletId(),p.packages())).toList(),
confirmed.receivedPackages()),totalSpaces,operator);
save(trip,row,operator,"BOOKING_RECEIPT","确认外接实收货量及约定运费 "+row.getBookingNo());return result(trip,row,false);
}
@Transactional