feat: add self-operated analytics and enforce Lastleg module boundaries
This commit is contained in:
+6
@@ -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);
|
||||
}
|
||||
+12
-29
@@ -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>();
|
||||
|
||||
+9
@@ -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,
|
||||
|
||||
+23
@@ -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";
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user