优化
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@ public interface FleetAppointmentImpactMapper {
|
||||
@Select("""
|
||||
<script>SELECT DISTINCT t.trip_no,t.status,t.planned_departure_at FROM ll_fleet_trip_dispatch d
|
||||
JOIN ll_fleet_trip t ON t.id=d.trip_id WHERE t.status NOT IN ('CANCELLED','COMPLETED') AND
|
||||
<foreach collection='rows' item='r' open='(' separator=' OR ' close=')'>(d.customer_id=#{r.customerId} AND d.dispatch_no=#{r.dispatchNo})</foreach>
|
||||
<foreach collection='rows' item='r' open='(' separator=' OR ' close=')'>(d.dispatch_no=#{r.dispatchNo} AND EXISTS(SELECT 1 FROM ll_dispatch_item i WHERE i.dispatch_order_id=d.dispatch_id AND i.customer_id=#{r.customerId}))</foreach>
|
||||
ORDER BY t.trip_no</script>
|
||||
""")
|
||||
List<Trip> trips(@Param("rows") List<Dispatch> rows);
|
||||
|
||||
+7
-1
@@ -14,7 +14,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Warehouse-scoped projection; exposes only the selected customer dispatch, never a whole-trip snapshot. */
|
||||
/** 由PDA绑定仓读取本车交接状态;实际派送归属逐行核验。 */
|
||||
@Service
|
||||
@Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ)
|
||||
public class FleetHandoverPdaQuery {
|
||||
@@ -45,6 +45,12 @@ public class FleetHandoverPdaQuery {
|
||||
var evidence = dispatches.transitEvidence(dispatchNo, false);
|
||||
if (!Long.toString(warehouseId).equals(evidence.warehouseId())
|
||||
|| !dispatchNo.equals(evidence.dispatchNo()) || !Objects.equals(binding.getPackageCount(), evidence.shippedPackages())) throw notFound();
|
||||
var deliveryNumbers=new java.util.HashSet<String>();
|
||||
if(evidence.deliveries()==null || evidence.deliveries().isEmpty() || evidence.deliveries().stream().anyMatch(row ->
|
||||
row==null || row.cargo()==null || row.cargo().customerId()==null || row.cargo().customerId().isBlank()
|
||||
|| !dispatchNo.equals(row.cargo().dispatchNo()) || row.cargo().deliveryNo()==null
|
||||
|| !deliveryNumbers.add(row.cargo().deliveryNo()) || row.cargo().packages()<=0)
|
||||
|| evidence.deliveries().stream().mapToLong(row->row.cargo().packages()).sum()!=evidence.shippedPackages())throw notFound();
|
||||
var original = history.selectList(new LambdaQueryWrapper<FleetTripHistory>().eq(FleetTripHistory::getTripId, trip.getId())
|
||||
.eq(FleetTripHistory::getAction, "DEPART"));
|
||||
if (original.size() != 1) throw new ResponseStatusException(HttpStatus.CONFLICT, "缺少唯一原始发车记录");
|
||||
|
||||
+10
-1
@@ -90,8 +90,17 @@ public class FleetBookingReservations {
|
||||
}
|
||||
}
|
||||
private void touch(FleetBookingEntity row,String operator){row.setRecordVersion(row.getRecordVersion()+1);row.setUpdatedBy(operator);row.setUpdatedAt(java.time.LocalDateTime.now(ZoneOffset.UTC));if(bookings.updateById(row)!=1)throw conflict("外接关联已变化");}
|
||||
/** 父车次锁下读取有效外接占位支持的卸货目的地,不用展示响应推导。 */
|
||||
public Set<String> requiredRouteDestinations(long tripId) {
|
||||
var keys=new LinkedHashSet<String>();
|
||||
for(var row:bookings.list(tripId)) if(unbound(row)||"BOUND".equals(row.getStatus())) {
|
||||
if(row.getDestinationKey()==null || row.getDestinationKey().isBlank())throw conflict("已确认外接预订缺少真实卸货目的地");
|
||||
keys.add(row.getDestinationKey());
|
||||
}
|
||||
return Collections.unmodifiableSet(keys);
|
||||
}
|
||||
public void requireRoute(FleetTripEntity trip,List<FleetTripModels.Stop> stops) {
|
||||
for(var row:bookings.list(trip.getId())) if((unbound(row)||"BOUND".equals(row.getStatus())) && index(stops,row.getDestinationKey())<1)
|
||||
for(var key:requiredRouteDestinations(trip.getId())) if(index(stops,key)<1)
|
||||
throw conflict("路线仍须包含已确认外接预订的卸货站");
|
||||
}
|
||||
private void add(List<TripCapacityPolicy.Booking> loads,List<FleetTripModels.Stop> stops,FleetBookingEntity row){
|
||||
|
||||
+7
-79
@@ -34,6 +34,11 @@ public class FleetTripExecutionService {
|
||||
private final ObjectMapper json;
|
||||
private final FleetBookingReservations bookingReservations;
|
||||
|
||||
/** 排车域先核对冻结司机及附件引用;本域继续校验该司机当前或历史证照归属。 */
|
||||
public DriverModels.Download dispatchDriverLicense(long driverId,long fileId) {
|
||||
return drivers.download(driverId,fileId);
|
||||
}
|
||||
|
||||
public FleetTripExecutionService(FleetTripMapper trips, FleetTripDispatchMapper bindings, FleetResourceMapper resources,
|
||||
FleetDispatchGateway dispatches, FleetTripService history, DriverService drivers,
|
||||
VehicleRepository vehicles, ObjectMapper json, FleetBookingReservations bookingReservations) {
|
||||
@@ -203,69 +208,6 @@ public class FleetTripExecutionService {
|
||||
|
||||
|
||||
|
||||
@Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ)
|
||||
public FleetExecutionModels.Gate gate(String warehouseId, String dispatchNo) {
|
||||
var binding = bindings.findByDispatchNo(dispatchNo);
|
||||
if (binding == null) return missingGate();
|
||||
return pickingGate(gateForTrip(warehouseId, binding.getTripNo()), binding);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ)
|
||||
public Map<String, FleetExecutionModels.Gate> gates(String warehouseId, List<String> numbers) {
|
||||
if (numbers.isEmpty()) return Map.of();
|
||||
var result = new HashMap<String, FleetExecutionModels.Gate>();
|
||||
numbers.forEach(number -> result.put(number, missingGate()));
|
||||
var byTrip = new HashMap<String, FleetExecutionModels.Gate>();
|
||||
for (var binding : bindings.findByDispatchNos(numbers)) result.put(binding.getDispatchNo(), pickingGate(
|
||||
byTrip.computeIfAbsent(binding.getTripNo(), number -> gateForTrip(warehouseId, number)), binding));
|
||||
return result;
|
||||
}
|
||||
|
||||
private FleetExecutionModels.Gate gateForTrip(String warehouseId, String tripNo) {
|
||||
var trip = requireTrip(tripNo, false);
|
||||
requireWarehouse(trip, warehouseId);
|
||||
var manifest = manifests(trip, false);
|
||||
String reason = blockReason(trip, manifest);
|
||||
String warning = null;
|
||||
if (reason == null) reason = loadBlockReason(trip, cargo(manifest));
|
||||
if (reason == null) reason = loadingReviewBlockReason(trip);
|
||||
if (reason == null) for (var member : bindings.listByTrip(trip.getId())) {
|
||||
reason = appointmentAllocations.departureBlockReason(member.getDispatchNo());
|
||||
if (reason != null) break;
|
||||
}
|
||||
if (reason == null) {
|
||||
var driver = drivers.detail(trip.getDriverId(), trip.getExpectedDriverEndAt().toInstant(ZoneOffset.UTC));
|
||||
if (!driver.qualification().canDispatch()) reason = "司机资格不满足本次任务,请在平台核对";
|
||||
else reason = driverBlockReason(trip, driver);
|
||||
if (reason == null && driver.qualification().expiringSoon()) warning = "司机驾驶证将于 " + driver.qualification().licenseExpiresOn() + " 到期;有效期覆盖本次预计任务,请留意延期风险";
|
||||
}
|
||||
boolean canPick = "PLANNED".equals(trip.getStatus()) && trip.getVehicleId() != null && trip.getDriverId() != null;
|
||||
return new FleetExecutionModels.Gate(trip.getTripNo(), trip.getRecordVersion(), manifest.size(), reason == null, reason, warning,
|
||||
canPick, canPick ? null : "当前车次不允许继续拣货");
|
||||
}
|
||||
|
||||
private static FleetExecutionModels.Gate missingGate() {
|
||||
return new FleetExecutionModels.Gate(null, null, 0, false, "请先在平台将排车单关联车次", null, false, "请先关联车次并通过调度审核");
|
||||
}
|
||||
private FleetExecutionModels.Gate pickingGate(FleetExecutionModels.Gate gate, FleetTripDispatch binding) {
|
||||
if (!gate.canPick()) return gate;
|
||||
var fact=dispatches.workflows(List.of(binding.getDispatchNo())).get(binding.getDispatchNo());
|
||||
if(fact==null)return gate.withPicking(false,"整车审核事实缺失");
|
||||
var review = fact.dispatchReview();
|
||||
String reason = "APPROVED".equals(review.status()) ? null : "REJECTED".equals(review.status())
|
||||
? "调度审核已打回:" + review.reason() : "请先通过调度审核再拣货";
|
||||
return gate.withPicking(reason == null, reason);
|
||||
}
|
||||
private String loadingReviewBlockReason(FleetTripEntity trip) {
|
||||
var members=bindings.listByTrip(trip.getId());
|
||||
var facts=dispatches.workflows(members.stream().map(FleetTripDispatch::getDispatchNo).toList());
|
||||
return !members.isEmpty() && members.stream().allMatch(b->facts.containsKey(b.getDispatchNo()) && "APPROVED".equals(facts.get(b.getDispatchNo()).loadingReview().status()))?null:"请先通过本车理货审核再发车";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private List<FleetExecutionModels.Manifest> manifests(FleetTripEntity trip, boolean lock) {
|
||||
return bindings.listByTrip(trip.getId()).stream().map(binding -> dispatches.manifest(binding.getDispatchNo(), lock)).toList();
|
||||
}
|
||||
@@ -275,7 +217,8 @@ public class FleetTripExecutionService {
|
||||
}
|
||||
private String blockReason(FleetTripEntity trip, List<FleetExecutionModels.Manifest> manifests) {
|
||||
if (!"PLANNED".equals(trip.getStatus())) return "当前车次不是待发车计划";
|
||||
if (trip.getVehicleId() == null || trip.getDriverId() == null) return "请先分配车辆和司机";
|
||||
String vehicleReason=currentVehicleBlockReason(trip,false);if(vehicleReason!=null)return vehicleReason;
|
||||
if(trip.getDriverName()==null || trip.getDriverName().isBlank())return "请先确认本趟司机资料";
|
||||
if (manifests.isEmpty()) return "车次尚未关联排车单";
|
||||
if (bookingReservations.hasConfirmed(trip.getId())) return "已确认外接预订尚未关联实际发运货物,请先完成来货与排车关联";
|
||||
for (var binding : bindings.listByTrip(trip.getId())) {
|
||||
@@ -290,13 +233,6 @@ public class FleetTripExecutionService {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private String loadBlockReason(FleetTripEntity trip, List<FleetExecutionModels.Cargo> cargo) {
|
||||
var load = load(trip);
|
||||
if (load == null) return "请在平台确认本车实际装载";
|
||||
if (!load.manifestHash().equals(hash(write(cargo)))) return "装载确认后货物发生变化,请重新确认";
|
||||
try { capacity(trip, cargo, load.pallets()); return null; }
|
||||
catch (ResponseStatusException error) { return error.getReason(); }
|
||||
}
|
||||
private List<TripCapacityPolicy.Segment> capacity(FleetTripEntity trip, List<FleetExecutionModels.Cargo> cargo,
|
||||
List<FleetExecutionModels.Measurement> pallets) {
|
||||
List<FleetTripModels.Stop> stops;
|
||||
@@ -333,19 +269,11 @@ public class FleetTripExecutionService {
|
||||
var claim = resources.find(kind, id);
|
||||
if (claim == null || claim.tripId() != tripId) throw conflict("车次车辆或司机占用已变化");
|
||||
}
|
||||
private static String driverBlockReason(FleetTripEntity trip, DriverModels.Response driver) {
|
||||
if (!"OVERSEAS".equals(driver.details().region())) return "本车次须使用海外司机,请核对当前司机档案";
|
||||
if (!Objects.equals(driver.details().name(), trip.getDriverName())) return "司机档案已变化,请核对车次司机快照";
|
||||
return null;
|
||||
}
|
||||
private FleetTripEntity requireTrip(String tripNo, boolean lock) {
|
||||
var trip = lock ? trips.lockByNo(tripNo) : trips.findByNo(tripNo);
|
||||
if (trip == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "车次不存在");
|
||||
return trip;
|
||||
}
|
||||
private static void requireWarehouse(FleetTripEntity trip, String warehouseId) {
|
||||
if (!trip.getWarehouseId().toString().equals(warehouseId)) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "当前绑定仓无此车次");
|
||||
}
|
||||
/** 出库装载只能用于派送车次,取货必须沿专用运输链执行。 */
|
||||
private static void requireVersionAndPlan(FleetTripEntity trip, long version) {
|
||||
if ("PICKUP".equals(trip.getTripPurpose())) throw conflict("取货车次请使用取货任务操作");
|
||||
|
||||
@@ -349,13 +349,28 @@ public class FleetTripService {
|
||||
var trip = repository.lockTrip(binding.getTripNo());
|
||||
if (trip == null || !"PLANNED".equals(trip.getStatus())) throw conflict("仅未发车整车可以调整货物");
|
||||
List<FleetTripModels.Stop> existing = read(trip.getStopsJson(),new TypeReference<List<FleetTripModels.Stop>>() { });
|
||||
var stops = new ArrayList<>(existing);
|
||||
if (stops.isEmpty()) stops.add(new FleetTripModels.Stop(trip.getWarehouseName(),null,"ORIGIN:"+trip.getWarehouseId()));
|
||||
var keys = new HashSet<String>(); stops.forEach(stop -> keys.add(stop.destinationKey()));
|
||||
for (var stop : destinations) if (keys.add(stop.destinationKey())) stops.add(new FleetTripModels.Stop(stop.name(),stop.address(),stop.destinationKey()));
|
||||
String originKey="ORIGIN:"+trip.getWarehouseId();
|
||||
var origin=existing.isEmpty()?new FleetTripModels.Stop(trip.getWarehouseName(),null,originKey):existing.get(0);
|
||||
if(!originKey.equals(origin.destinationKey()))throw conflict("本车缺少与起运仓一致的路线起点");
|
||||
var requested=new LinkedHashMap<String,FleetExecutionModels.DispatchStop>();
|
||||
for(var stop:destinations) {
|
||||
if(stop==null || stop.destinationKey()==null || stop.destinationKey().isBlank() || originKey.equals(stop.destinationKey()))throw conflict("实际货物缺少有效卸货目的地");
|
||||
requested.putIfAbsent(stop.destinationKey(),stop);
|
||||
}
|
||||
var supported=new HashSet<>(bookingReservations.requiredRouteDestinations(trip.getId()));
|
||||
supported.addAll(requested.keySet());
|
||||
var stops=new ArrayList<FleetTripModels.Stop>();stops.add(origin);
|
||||
var keys=new HashSet<String>();keys.add(originKey);
|
||||
// 仅删除失去货物和有效预订支持的站;保留其余站的原相对顺序与事实快照。
|
||||
for(var stop:existing.subList(Math.min(1,existing.size()),existing.size()))
|
||||
if(supported.contains(stop.destinationKey()) && keys.add(stop.destinationKey()))stops.add(stop);
|
||||
for(var stop:requested.values()) if(keys.add(stop.destinationKey()))stops.add(new FleetTripModels.Stop(stop.name(),stop.address(),stop.destinationKey()));
|
||||
// 不能为缺失的外接站编造名称/地址;异常承诺必须阻断整笔主单事务。
|
||||
bookingReservations.requireRoute(trip,stops);
|
||||
long version=trip.getRecordVersion();
|
||||
trip.setStopsJson(write(stops)); trip.setPlannedLoadJson(null); trip.setLoadJson(null); trip.setLoadingReviewJson(null);
|
||||
trip.setRecordVersion(trip.getRecordVersion()+1); trip.setUpdatedAt(now());
|
||||
if (repository.updateTrip(trip,new LambdaUpdateWrapper<FleetTripEntity>().eq(FleetTripEntity::getId,trip.getId())) != 1
|
||||
trip.setRecordVersion(Math.addExact(version,1)); trip.setUpdatedAt(now());
|
||||
if (repository.updateTrip(trip,new LambdaUpdateWrapper<FleetTripEntity>().eq(FleetTripEntity::getId,trip.getId()).eq(FleetTripEntity::getRecordVersion,version)) != 1
|
||||
|| repository.wholeQuantity(dispatchNo,packages) != 1) throw conflict("整车货物快照保存失败");
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ public interface VehicleFulfillmentMapper {
|
||||
+ "AND JSON_CONTAINS(ho.snapshot_json,CONCAT('{\"customerId\":',JSON_QUOTE(d.customer_id),',\"deliveryNo\":',JSON_QUOTE(d.delivery_no),'}'),'$.carriedDeliveries')=1)";
|
||||
String DEPARTED = "(" + ORIGINAL_DEPARTURE + " OR " + HANDOVER + ")";
|
||||
String DELIVERIES = " FROM ll_delivery_order d JOIN ll_fleet_trip_dispatch b "
|
||||
+ "ON b.dispatch_id=d.dispatch_order_id AND CAST(b.customer_id AS BINARY(128))=CAST(d.customer_id AS BINARY(128)) "
|
||||
+ "ON b.dispatch_id=d.dispatch_order_id AND EXISTS(SELECT 1 FROM ll_dispatch_item i WHERE i.dispatch_order_id=b.dispatch_id AND CAST(i.customer_id AS BINARY(128))=CAST(d.customer_id AS BINARY(128))) "
|
||||
+ "JOIN ll_fleet_trip t ON t.id=b.trip_id WHERE ((t.status='PLANNED' AND t.vehicle_id=#{vehicleId}) OR " + DEPARTED + ")";
|
||||
String PREVIOUS_VEHICLE = "JSON_UNQUOTE(JSON_EXTRACT(p.snapshot_json,'$.trip.vehicleId'))";
|
||||
String NEXT_VEHICLE = "JSON_UNQUOTE(JSON_EXTRACT(h.snapshot_json,'$.trip.vehicleId'))";
|
||||
|
||||
Reference in New Issue
Block a user