fix(lastleg): repair dispatch cancellation and return execution contracts
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
package com.baiyue.lastleg.fleet.trip;
|
||||
|
||||
/** 原承运登记的真实入仓来源查询入口。 */
|
||||
public class FleetBookingCargoRepository {
|
||||
private final FleetBookingMapper bookings;
|
||||
/** 复用车次事务中的承运登记Mapper。 */
|
||||
public FleetBookingCargoRepository(FleetBookingMapper bookings){this.bookings=bookings;}
|
||||
/** 按客户与真实入仓码定位承运,历史取消记录交由业务层分别处理。 */
|
||||
public FleetBookingEntity forInboundSource(String customerId,String inboundCode){return bookings.forInboundSource(customerId,inboundCode);}
|
||||
}
|
||||
+3
-3
@@ -26,7 +26,7 @@ public class FleetBookingFreight {
|
||||
public record Preview(String snapshotHash, boolean requiresDeliveryFee, List<Quote> quotes,
|
||||
@com.fasterxml.jackson.annotation.JsonIgnore List<String> priorInboundCodes) { }
|
||||
|
||||
/** Caller serializes transfer with the dispatch lock. Never lock the parent trip here. */
|
||||
/** 转派送持有排车锁,按真实入仓组成复核约定运费,不反向申请父车次锁。 */
|
||||
public Preview preview(long dispatchId, String dispatchNo, String customerId,
|
||||
List<FleetDispatchGateway.SourceCargo> sources) {
|
||||
var quotes = new ArrayList<Quote>();
|
||||
@@ -38,7 +38,7 @@ public class FleetBookingFreight {
|
||||
else grouped.computeIfAbsent(source.inboundCode(), key -> new ArrayList<>()).add(source);
|
||||
}
|
||||
for (var entry : grouped.entrySet()) {
|
||||
var booking = bookings.incomingForFreight(entry.getKey());
|
||||
var booking = new FleetBookingCargoRepository(bookings).forInboundSource(customerId, entry.getKey());
|
||||
if (booking == null) { requiresFee = true; continue; }
|
||||
if (!customerId.equals(booking.getCustomerId())) throw conflict("来货来源与排车客户不一致");
|
||||
var accepted = reservations.receipt(booking);
|
||||
@@ -62,7 +62,7 @@ public class FleetBookingFreight {
|
||||
}
|
||||
var confirmed = new TreeMap<String, Integer>();
|
||||
for (var pallet : accepted.source().pallets()) confirmed.merge(pallet.warehousePalletId(), pallet.packages(), Math::addExact);
|
||||
if (!actual.equals(confirmed)) throw conflict("外接发运数量与已确认实收不一致");
|
||||
if (!actual.equals(confirmed)) throw conflict("外接发运托盘或数量与已确认实收不一致;请先解除排车关联,在外接预订重新确认实际收货与运费后再绑定");
|
||||
var cargo = reservations.cargo(booking);
|
||||
BigDecimal quantity = "PALLET".equals(cargo.priceBasis()) ? BigDecimal.valueOf(cargo.palletSpaces()) : BigDecimal.ONE;
|
||||
if (booking.getAgreedAmount() == null || booking.getAgreedAmount().signum() < 0 || cargo.unitPrice() == null
|
||||
|
||||
@@ -6,12 +6,13 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface FleetBookingMapper extends BaseMapper<FleetBookingEntity> {
|
||||
/** 按实际入仓来源定位原承运登记,取消记录保留供费用核对。 */
|
||||
@Select("SELECT booking.* FROM ll_fleet_external_booking booking JOIN ll_warehouse_transfer_order incoming " +
|
||||
"ON incoming.customer_id=booking.customer_id AND incoming.transfer_no=booking.incoming_no " +
|
||||
"WHERE booking.customer_id=#{customerId} AND incoming.inbound_code=#{inboundCode}")
|
||||
FleetBookingEntity forInboundSource(@Param("customerId") String customerId, @Param("inboundCode") String inboundCode);
|
||||
@Select("SELECT * FROM ll_fleet_external_booking WHERE booking_no=#{number}")
|
||||
FleetBookingEntity byNo(@Param("number") String number);
|
||||
@Select("SELECT * FROM ll_fleet_external_booking WHERE incoming_no=#{incomingNo} AND status <> 'CANCELLED'")
|
||||
FleetBookingEntity incoming(@Param("incomingNo") String incomingNo);
|
||||
@Select("SELECT * FROM ll_fleet_external_booking WHERE incoming_no=#{incomingNo}")
|
||||
FleetBookingEntity incomingForFreight(@Param("incomingNo") String incomingNo);
|
||||
@Select("SELECT * FROM ll_fleet_external_booking WHERE trip_id=#{tripId} ORDER BY id")
|
||||
List<FleetBookingEntity> list(@Param("tripId") long tripId);
|
||||
@Select("SELECT * FROM ll_fleet_external_booking WHERE trip_id=#{tripId} AND created_by=#{operator} AND idempotency_key=#{key}")
|
||||
|
||||
@@ -28,7 +28,9 @@ public final class FleetBookingModels {
|
||||
Instant estimatedArrivalAt,@Size(max=1000) String deliveryInstructions,@Size(max=1000) String privateAddress,
|
||||
@Size(max=32) String postalCode,@Size(max=128) String recipientName,@Size(max=64) String recipientPhone,
|
||||
@NotBlank @Size(min=8,max=128) String idempotencyKey) { }
|
||||
public record PalletSpaces(@NotBlank String warehousePalletId,@Min(1) int palletSpaces) { }
|
||||
public record PalletSpaces(@NotBlank String warehousePalletId,@Min(1) int palletSpaces,
|
||||
@DecimalMin("0.001") @Digits(integer=9,fraction=3) BigDecimal weightKg,
|
||||
@DecimalMin("0.001") @Digits(integer=9,fraction=3) BigDecimal volumeCbm) { }
|
||||
public record AcceptReceipt(@NotNull @Min(0) Long tripRecordVersion,@NotNull @Min(0) Long recordVersion,
|
||||
@NotBlank String receiptHash,@NotEmpty List<@Valid PalletSpaces> pallets,
|
||||
@NotNull @DecimalMin("0.00") @Digits(integer=10,fraction=2) BigDecimal unitPrice,
|
||||
|
||||
+3
-2
@@ -48,10 +48,11 @@ public class FleetBookingReservations {
|
||||
}
|
||||
public FleetBookingModels.AcceptedReceipt receipt(FleetBookingEntity row){return row.getReceiptJson()==null?null:read(row.getReceiptJson(),FleetBookingModels.AcceptedReceipt.class);}
|
||||
private static boolean unbound(FleetBookingEntity row){return Set.of("CONFIRMED","RECEIVED").contains(row.getStatus());}
|
||||
/** 按实际入仓来源绑定整票实收,合托不能掩盖外接承诺。 */
|
||||
public void bindIncoming(FleetTripEntity trip,FleetDispatchGateway.Snapshot dispatch,List<FleetDispatchGateway.SourceCargo> sources,String operator){
|
||||
var bySource=sources.stream().filter(row->row.inboundCode()!=null).collect(java.util.stream.Collectors.groupingBy(FleetDispatchGateway.SourceCargo::inboundCode));
|
||||
for(var entry:bySource.entrySet()){
|
||||
var row=bookings.incoming(entry.getKey());if(row==null)continue;
|
||||
var row=new FleetBookingCargoRepository(bookings).forInboundSource(dispatch.customerId(),entry.getKey());if(row==null||"CANCELLED".equals(row.getStatus()))continue;
|
||||
if(!Objects.equals(row.getTripId(),trip.getId()))throw conflict("此外接货已承诺给其他车次,请先调整原预订");
|
||||
if(!Objects.equals(row.getCustomerId(),dispatch.customerId())||!"RECEIVED".equals(row.getStatus()))throw conflict("外接货须先确认实收货量及运费,再关联排车");
|
||||
var accepted=receipt(row);if(accepted==null)throw conflict("外接货缺少实收确认");
|
||||
@@ -62,7 +63,7 @@ public class FleetBookingReservations {
|
||||
if(item.warehousePalletId()==null||!Objects.equals(item.destinationKey(),row.getDestinationKey()))throw conflict("排车货物托盘或目的地与外接实收不一致");
|
||||
selected.merge(item.warehousePalletId(),item.packages(),Math::addExact);
|
||||
}
|
||||
if(!selected.equals(expected))throw conflict("当前排车未完整包含此外接预订的实收货物");
|
||||
if(!selected.equals(expected))throw conflict("当前托盘与外接实收不一致;请在外接预订重新确认实际收货与运费后再关联排车");
|
||||
row.setStatus("BOUND");row.setDispatchNo(dispatch.dispatchNo());touch(row,operator);
|
||||
if(row.getOfferId()!=null)poolOrders.boundDispatch(row.getId(),row.getCustomerId(),dispatch.dispatchNo(),operator);
|
||||
}
|
||||
|
||||
+16
-3
@@ -143,12 +143,13 @@ public class FleetBookingService {
|
||||
save(trip,row,operator,"BOOKING_INCOMING","登记外接来货 "+evidence.incomingNo());return result(trip,row,false);
|
||||
}
|
||||
@Transactional
|
||||
/** 首次或解除排车后的重新核对都要求当前实收摘要、版本、完整实托和容量一致。 */
|
||||
public FleetBookingModels.Result acceptReceipt(String tripNo,String bookingNo,String operator,FleetBookingModels.AcceptReceipt request){
|
||||
var trip=trip(tripNo,true);String vehicleBlock=execution.currentVehicleBlockReason(trip,true);var row=booking(trip,bookingNo);String hash=hash(request);
|
||||
if(Objects.equals(row.getReceiptKey(),request.idempotencyKey())){if(!hash.equals(row.getReceiptHash()))throw conflict("实收确认请求内容不一致");return result(trip,row,true);}
|
||||
if(vehicleBlock!=null)throw conflict(vehicleBlock);
|
||||
requireTrip(trip,request.tripRecordVersion());requireVersion(row,request.recordVersion());
|
||||
if(!"CONFIRMED".equals(row.getStatus())||row.getIncomingNo()==null)throw conflict("请先登记来货并完成仓库实际收货");
|
||||
if(!java.util.Set.of("CONFIRMED","RECEIVED").contains(row.getStatus())||row.getIncomingNo()==null)throw conflict("请先登记来货并完成仓库实际收货;已绑定排车须先解除关联后重新核对");
|
||||
var evidence=incoming.inspect(row.getCustomerId(),row.getIncomingNo(),true);requireEvidence(trip,row,evidence);
|
||||
if(!java.util.Set.of("WAIT_CUSTOMER_CONFIRMATION","READY_FOR_DISPATCH").contains(evidence.status())||evidence.pallets().isEmpty())throw conflict("仓库尚未完成实收及测量");
|
||||
if(!Objects.equals(evidence.receiptHash(),request.receiptHash()))throw conflict("实收数据已变化,请刷新后重新核对");
|
||||
@@ -156,8 +157,19 @@ public class FleetBookingService {
|
||||
for(var entry:request.pallets())if(entry.palletSpaces()<1||spaces.put(entry.warehousePalletId(),entry.palletSpaces())!=null)throw conflict("请为每个实际托盘填写一次占用托位");
|
||||
var physical=evidence.pallets().stream().map(FleetBookingIncomingGateway.Pallet::warehousePalletId).collect(java.util.stream.Collectors.toSet());
|
||||
if(!spaces.keySet().equals(physical)||physical.contains(null))throw conflict("托位确认须与实际收货托盘一致");
|
||||
// 旧测量仅适用于同实托同箱数;组成变化后必须明确输入新的该票实测值。
|
||||
var measured=new java.util.ArrayList<FleetBookingIncomingGateway.Pallet>();
|
||||
for(var pallet:evidence.pallets()) {
|
||||
var input=request.pallets().stream().filter(p->Objects.equals(p.warehousePalletId(),pallet.warehousePalletId())).findFirst().orElseThrow();
|
||||
var kg=pallet.weightKg()==null?input.weightKg():pallet.weightKg();
|
||||
var cbm=pallet.volumeCbm()==null?input.volumeCbm():pallet.volumeCbm();
|
||||
if(pallet.weightKg()!=null&&input.weightKg()!=null&&pallet.weightKg().compareTo(input.weightKg())!=0
|
||||
||pallet.volumeCbm()!=null&&input.volumeCbm()!=null&&pallet.volumeCbm().compareTo(input.volumeCbm())!=0)
|
||||
throw conflict("未变化托盘须使用仓储转运的原实测值,不能覆盖原收货证据");
|
||||
measured.add(new FleetBookingIncomingGateway.Pallet(pallet.warehousePalletId(),pallet.palletNo(),pallet.packages(),kg,cbm));
|
||||
}
|
||||
BigDecimal weight=BigDecimal.ZERO,volume=BigDecimal.ZERO;int count=0;
|
||||
for(var pallet:evidence.pallets()){
|
||||
for(var pallet:measured){
|
||||
if(pallet.packages()<=0||pallet.weightKg()==null||pallet.volumeCbm()==null||pallet.weightKg().signum()<=0||pallet.volumeCbm().signum()<=0)throw conflict("实际托盘缺少有效箱数、重量或体积");
|
||||
weight=weight.add(pallet.weightKg());volume=volume.add(pallet.volumeCbm());count=Math.addExact(count,pallet.packages());
|
||||
}
|
||||
@@ -168,7 +180,8 @@ public class FleetBookingService {
|
||||
reservations.segments(trip,plan.capacityPlan(),row.getId(),row);
|
||||
var confirmed=incoming.confirm(row.getCustomerId(),row.getIncomingNo(),operator,"FLEET:"+bookingNo+":RECEIPT");
|
||||
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())));
|
||||
var measuredReceipt=new FleetBookingIncomingGateway.Evidence(confirmed.incomingNo(),confirmed.inboundCode(),confirmed.warehouseId(),confirmed.destinationKey(),confirmed.status(),confirmed.receiptHash(),java.util.List.copyOf(measured),confirmed.receivedPackages());
|
||||
row.setReceiptJson(write(new FleetBookingModels.AcceptedReceipt(measuredReceipt,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(),new Receipt(
|
||||
confirmed.incomingNo(),confirmed.warehouseId(),confirmed.destinationKey(),confirmed.status(),
|
||||
|
||||
Reference in New Issue
Block a user