提交
This commit is contained in:
+4
-1
@@ -25,7 +25,10 @@ public final class DispatchContracts {
|
||||
|
||||
/** 临时资源证据只归本趟,不能据此自动建车辆或司机主档。 */
|
||||
public record DriverEvidence(String licenseNo, LocalDate licenseExpiresOn,
|
||||
List<Long> fileIds, Instant expectedTaskEndAt) { }
|
||||
List<Long> fileIds, Instant expectedTaskEndAt, List<Long> photoFileIds) {
|
||||
/** 司机照片独立于驾驶证,缺省为空且引用须有界、有效、不重复。 */
|
||||
public DriverEvidence { photoFileIds = ReviewAttachments.checked(photoFileIds); }
|
||||
}
|
||||
|
||||
public record VehicleSnapshot(ResourceMode mode, Long vehicleId, String plate,
|
||||
String carrierName, String vehicleStatus, Capacity capacity,
|
||||
|
||||
+38
-12
@@ -51,6 +51,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway evidence;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.repository.DispatchExecutionRepository executionReads;
|
||||
@jakarta.annotation.Resource private OwnedBusinessOwnershipCapture ownedBusinessOwnership;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.transport.TransportTaskService transportTasks;
|
||||
|
||||
/** 领域编排只访问Repository和提供者应用入口。 */
|
||||
public DispatchAggregateService(DispatchAggregateRepository repository, DispatchSourceRepository sources,
|
||||
@@ -304,7 +305,9 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
row.setStatus(Status.ACTIVE.name()); row.setStage(Stage.BEFORE_LOADING.name()); row.setRecordVersion(0L);
|
||||
row.setCargoRevision(0L); row.setCargoHash(hashes.source(List.of())); row.setReviewState(ReviewState.DRAFT.name()); row.setReviewRound(0L);
|
||||
row.setDispatchReviewJson(write(pending(0))); row.setLoadingReviewJson(write(pending(0)));
|
||||
Resources resources=resources(shell); row.setResourcesJson(write(resources));
|
||||
var photos=request.driver()==null || request.driver().evidence()==null?List.<Long>of():request.driver().evidence().photoFileIds();
|
||||
if(!photos.isEmpty())verifyEvidence(id,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER_PHOTO,photos);
|
||||
Resources resources=resources(shell,photos); row.setResourcesJson(write(resources));
|
||||
row.setVehiclePlate(resources.vehicle()==null ? null : resources.vehicle().plate());
|
||||
row.setPlannedDepartureAt(utc(request.plannedDepartureAt())); row.setEstimatedArrivalAt(utc(request.estimatedArrivalAt()));
|
||||
row.setItemCount(0); row.setPalletCount(0); row.setPackageCount(0); row.setPickedPackageCount(0); row.setShippedPackageCount(0);
|
||||
@@ -382,8 +385,11 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
var original=commands.replay(actor,number,identity);if(original.isPresent())return original.get();
|
||||
requireEditing(row,request.recordVersion());
|
||||
String before=basis(row);
|
||||
// 本趟司机照片随资源维护保存,逐个核对本车及照片类别。
|
||||
var photos=request.driver()==null || request.driver().evidence()==null?List.<Long>of():request.driver().evidence().photoFileIds();
|
||||
if(!photos.isEmpty())verifyEvidence(row.getId(),com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER_PHOTO,photos);
|
||||
var actual=fleet.dispatchResources(number,fleetVehicle(request.vehicle()),fleetDriver(request.driver()));
|
||||
row.setResourcesJson(write(resources(actual)));row.setVehiclePlate(actual.vehicle()==null?null:actual.vehicle().plate());
|
||||
row.setResourcesJson(write(resources(actual,photos)));row.setVehiclePlate(actual.vehicle()==null?null:actual.vehicle().plate());
|
||||
row.setPlannedDepartureAt(utc(request.plannedDepartureAt()));row.setEstimatedArrivalAt(utc(request.estimatedArrivalAt()));row.setRemark(request.remark());
|
||||
if(!Objects.equals(before,basis(row)))invalidate(row,"车辆、司机或计划信息已修改");
|
||||
fleet.saveDispatchResources(number,actor.operatorId(),actual,request.plannedDepartureAt(),request.remark());
|
||||
@@ -398,7 +404,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
requireEditing(row,request.recordVersion());
|
||||
if(!canSubmitReview(row.getReviewState()))throw conflict("当前审核已提交,请刷新核对");
|
||||
verifyReviewResources(row);
|
||||
if(n(row.getPackageCount())<=0 || repository.cargo(row.getId()).isEmpty())throw bad("空排车不能提交审核");
|
||||
if(n(row.getPackageCount())<=0 || (!"PICKUP".equals(row.getTaskType()) && repository.cargo(row.getId()).isEmpty()))throw bad("空排车不能提交审核");
|
||||
long round=row.getReviewRound()+1;String basis=basis(row);
|
||||
row.setReviewRound(round);row.setReviewBasisHash(basis);row.setReviewState(ReviewState.DISPATCH_PENDING.name());
|
||||
row.setDispatchReviewJson(write(new Review(ReviewStatus.PENDING,null,null,null,round,basis,null,null)));
|
||||
@@ -415,7 +421,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
if(actor!=null && actor.mode()==ActorMode.PDA) {
|
||||
if(!loading || actor.boundWarehouseId()==null || !hasPermission(actor,action))
|
||||
throw new BizException(ResultCode.FORBIDDEN,"仅当前绑定仓理货员可执行理货审核");
|
||||
required(actor,number,false);fleet.lockDispatchShell(number);row=required(actor,number,true);
|
||||
lockReviewRoot(actor,number);row=required(actor,number,true);
|
||||
} else row=lockPlatform(actor,number);
|
||||
var identity=identity(actor,number,action,request.recordVersion(),request.idempotencyKey(),request);
|
||||
var original=commands.replay(actor,number,identity);if(original.isPresent())return original.get();
|
||||
@@ -610,7 +616,12 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
}
|
||||
|
||||
/** 共同根锁顺序固定,当前阶段资格留给原命令恢复之后。 */
|
||||
private DispatchAggregate lockPlatform(Actor actor,String number) { requirePlatform(actor);fleet.lockDispatchShell(number);return required(actor,number,true); }
|
||||
private DispatchAggregate lockPlatform(Actor actor,String number) { requirePlatform(actor);lockReviewRoot(actor,number);return required(actor,number,true); }
|
||||
/** 运输取货锁原车次,仓库派送锁其整车资源;统一先父后主单。 */
|
||||
private void lockReviewRoot(Actor actor,String number) {
|
||||
var row=required(actor,number,false);
|
||||
if("PICKUP".equals(row.getTaskType()))transportTasks.transport(number,true);else fleet.lockDispatchShell(number);
|
||||
}
|
||||
/** 严格请求身份不接收前端摘要或操作者。 */
|
||||
private CommandIdentity identity(Actor actor,String no,Action action,long version,String key,Object request) {return new CommandIdentity(action,key,version,hashes.command(actor,action,no,version,request));}
|
||||
/** 每次状态变更、历史及不可变命令结果同事务保存。 */
|
||||
@@ -627,7 +638,10 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
/** 只包含可影响两级审核的事实,不因备注修改伪造新依据。 */
|
||||
private String basis(DispatchAggregate row) {
|
||||
var facts=new TreeMap<String,Object>();facts.put("cargoHash",row.getCargoHash());facts.put("resources",read(row.getResourcesJson(),Resources.class));
|
||||
facts.put("plannedDepartureAt",row.getPlannedDepartureAt());facts.put("estimatedArrivalAt",row.getEstimatedArrivalAt());facts.put("batches",repository.batches(row.getId()));facts.put("route",fleet.dispatchStops(row.getDispatchNo()));return hashes.source(facts);
|
||||
facts.put("plannedDepartureAt",row.getPlannedDepartureAt());facts.put("estimatedArrivalAt",row.getEstimatedArrivalAt());
|
||||
if("PICKUP".equals(row.getTaskType()))facts.put("route",transportTasks.transport(row.getDispatchNo(),false).getStopsJson());
|
||||
else {facts.put("batches",repository.batches(row.getId()));facts.put("route",fleet.dispatchStops(row.getDispatchNo()));}
|
||||
return hashes.source(facts);
|
||||
}
|
||||
/** 作废原因进入两级显式记录,旧审核证据仍保留在不可变历史中。 */
|
||||
private void invalidate(DispatchAggregate row,String reason) {
|
||||
@@ -652,6 +666,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
|
||||
/** 临时取证、有效期和实际主档资格必须在提交及通过时再次核验。 */
|
||||
void verifyReviewResources(DispatchAggregate row) {
|
||||
if("PICKUP".equals(row.getTaskType())) {transportTasks.verifyPickupReview(row.getDispatchNo());return;}
|
||||
var r=read(row.getResourcesJson(),Resources.class);var v=r.vehicle();var d=r.driver();
|
||||
if(v==null || d==null || trim(v.plate())==null || trim(d.name())==null || trim(d.phone())==null)throw bad("请补齐车辆、司机和联系电话");
|
||||
var capacity=v.capacity();if(capacity==null || capacity.palletSpaces()==null || capacity.palletSpaces()<=0 || capacity.maxLoadKg()==null || capacity.maxLoadKg().signum()<=0 || capacity.maxVolumeCbm()==null || capacity.maxVolumeCbm().signum()<=0)throw bad("请补齐车辆真实托位、载重及容积");
|
||||
@@ -659,9 +674,10 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
if(trim(e.licenseNo())==null || e.licenseExpiresOn()==null || e.licenseExpiresOn().isBefore(e.expectedTaskEndAt().atZone(ZoneOffset.UTC).toLocalDate()))throw bad("司机证照须覆盖预计任务结束日期");
|
||||
if(v.mode()==ResourceMode.EXTERNAL)verifyEvidence(row.getId(),com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.VEHICLE,v.evidenceFileIds());
|
||||
if(d.mode()==ResourceMode.EXTERNAL)verifyEvidence(row.getId(),com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER,e.fileIds());
|
||||
if(!e.photoFileIds().isEmpty())verifyEvidence(row.getId(),com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER_PHOTO,e.photoFileIds());
|
||||
var actual=fleet.dispatchResources(row.getDispatchNo(),new FleetExecutionModels.DispatchVehicle(v.mode().name(),v.vehicleId(),v.plate(),v.carrierName(),capacity.palletSpaces(),capacity.maxLoadKg(),capacity.maxVolumeCbm(),v.evidenceFileIds()),
|
||||
new FleetExecutionModels.DispatchDriver(d.mode().name(),d.driverId(),d.name(),d.phone(),e.licenseNo(),e.licenseExpiresOn(),e.fileIds(),e.expectedTaskEndAt()));
|
||||
if(!Objects.equals(hashes.source(r),hashes.source(resources(actual))))throw conflict("车辆或司机主档已变化,请重新保存并提交审核");
|
||||
if(!Objects.equals(hashes.source(r),hashes.source(resources(actual,e.photoFileIds()))))throw conflict("车辆或司机主档已变化,请重新保存并提交审核");
|
||||
}
|
||||
/** 同类证据有界且必须属于当前整车,跨车文件不能用于审核。 */
|
||||
private void verifyEvidence(long dispatch,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind kind,List<Long> ids) {
|
||||
@@ -848,8 +864,18 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
// 到仓取货和本土卡车沿运输任务执行,不能进入仓内拣货、扣库存和转派送链。
|
||||
if (row.getTaskType()!=null && !"DELIVERY".equals(row.getTaskType())) {
|
||||
var blocked=new EnumMap<Action,Gate>(Action.class);
|
||||
for(var action:Action.values())if(action!=Action.ARRANGE_DELIVERY && action!=Action.DEPART)
|
||||
blocked.put(action,new Gate(false,"TRANSPORT_TASK","请在排车运输任务中操作"));
|
||||
boolean pickup="PICKUP".equals(row.getTaskType()) && "ACTIVE".equals(row.getStatus());
|
||||
for(var action:Action.values())if(action!=Action.ARRANGE_DELIVERY && action!=Action.DEPART) {
|
||||
boolean state=pickup && switch(action) {
|
||||
case EDIT,CANCEL -> Set.of("BEFORE_LOADING","LOADING").contains(row.getStage());
|
||||
case SUBMIT_REVIEW -> "BEFORE_LOADING".equals(row.getStage()) && canSubmitReview(row.getReviewState());
|
||||
case DISPATCH_REVIEW -> "DISPATCH_PENDING".equals(row.getReviewState());
|
||||
case LOADING_REVIEW -> "LOADING_PENDING".equals(row.getReviewState());
|
||||
default -> false;
|
||||
};
|
||||
boolean allowed=state && hasPermission(actor,action);
|
||||
blocked.put(action,new Gate(allowed,allowed?null:"TRANSPORT_TASK",allowed?null:"请按取货运输审核流程操作"));
|
||||
}
|
||||
return Collections.unmodifiableMap(blocked);
|
||||
}
|
||||
var gates=new EnumMap<Action,Gate>(Action.class); boolean active=Status.ACTIVE.name().equals(row.getStatus());
|
||||
@@ -940,12 +966,12 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
/** 把Fleet权威主档或真实临时输入转换成整车审阅快照。 */
|
||||
private static Resources resources(FleetExecutionModels.DispatchShell shell) {
|
||||
/** 合并Fleet权威资源与本趟司机照片,照片不写入司机主档。 */
|
||||
private static Resources resources(FleetExecutionModels.DispatchShell shell,List<Long> photos) {
|
||||
var v=shell.vehicle(); var d=shell.driver();
|
||||
return new Resources(v==null?null:new VehicleSnapshot(ResourceMode.valueOf(v.mode()),v.vehicleId(),v.plate(),v.carrierName(),shell.vehicleStatus(),
|
||||
new Capacity(v.palletSpaces(),v.maxLoadKg(),v.maxVolumeCbm()),v.evidenceFileIds()),d==null?null:new DriverSnapshot(ResourceMode.valueOf(d.mode()),d.driverId(),d.name(),d.phone(),
|
||||
new DriverEvidence(d.licenseNo(),d.licenseExpiresOn(),d.evidenceFileIds(),d.expectedTaskEndAt())));
|
||||
new DriverEvidence(d.licenseNo(),d.licenseExpiresOn(),d.evidenceFileIds(),d.expectedTaskEndAt(),photos)));
|
||||
}
|
||||
|
||||
/** 主档选择不接受请求覆盖其车牌和容量。 */
|
||||
|
||||
+7
-2
@@ -19,7 +19,7 @@ import java.util.TreeMap;
|
||||
@Component
|
||||
public final class DispatchCommandHasher {
|
||||
private static final Set<String> UNORDERED = Set.of("cargo", "groups", "fees", "feeCoverage", "services",
|
||||
"batchAllocations", "measurements", "paymentVoucherFileIds", "evidenceFileIds", "fileIds");
|
||||
"batchAllocations", "measurements", "paymentVoucherFileIds", "evidenceFileIds", "fileIds", "photoFileIds");
|
||||
private final ObjectMapper json;
|
||||
|
||||
/** 复用已注册的时间、数值和公开字段序列化配置。 */
|
||||
@@ -60,7 +60,12 @@ public final class DispatchCommandHasher {
|
||||
if (node == null || node.isNull()) return "null";
|
||||
if (node.isObject()) {
|
||||
var values = new TreeMap<String, String>();
|
||||
node.fields().forEachRemaining(entry -> values.put(entry.getKey(), canonical(entry.getValue(), entry.getKey())));
|
||||
node.fields().forEachRemaining(entry -> {
|
||||
// 新增可选照片为空时沿用旧摘要,保留历史命令重放与已通过审核的依据。
|
||||
if (entry.getKey().equals("photoFileIds") && (entry.getValue().isNull()
|
||||
|| entry.getValue().isArray() && entry.getValue().isEmpty())) return;
|
||||
values.put(entry.getKey(), canonical(entry.getValue(), entry.getKey()));
|
||||
});
|
||||
var result = new ArrayList<String>();
|
||||
values.forEach((key, value) -> result.add(quoted(key) + ":" + value));
|
||||
return "{" + String.join(",", result) + "}";
|
||||
|
||||
+39
-3
@@ -18,12 +18,13 @@ import static com.baiyue.lastleg.dispatch.port.DispatchTypes.*;
|
||||
|
||||
/** 现有排车主单与运输事实同步,旧执行入口也不能留下不同步的单据。 */
|
||||
@Service
|
||||
public class TransportTaskProjection {
|
||||
public class TransportTaskProjection implements com.baiyue.lastleg.fleet.pickup.PickupDepartureReview {
|
||||
private final DispatchAggregateRepository roots;
|
||||
private final TransportTaskRepository links;
|
||||
private final UsTruckOrderRepository trucks;
|
||||
private final DispatchNumberGenerator numbers;
|
||||
private final ObjectMapper json;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.fleet.pickup.FleetPickupRepository pickupFacts;
|
||||
/** 注入本地聚合与原单持久化能力,无跨服务数据库访问。 */
|
||||
public TransportTaskProjection(DispatchAggregateRepository roots,TransportTaskRepository links,UsTruckOrderRepository trucks,DispatchNumberGenerator numbers,ObjectMapper json) {
|
||||
this.roots=roots;this.links=links;this.trucks=trucks;this.numbers=numbers;this.json=json;
|
||||
@@ -50,11 +51,22 @@ public class TransportTaskProjection {
|
||||
var trip=event.trip();if(!Set.of("PICKUP","LOCAL_TRUCK").contains(Objects.toString(trip.getTripPurpose(),"")))return;
|
||||
var found=links.byTrip(trip.getId());if(found==null)return;
|
||||
var row=roots.lock(found.getDispatchNo());long version=row.getRecordVersion();
|
||||
row.setRecordVersion(trip.getRecordVersion());row.setUpdatedAt(trip.getUpdatedAt());
|
||||
row.setRecordVersion(version+1);row.setUpdatedAt(trip.getUpdatedAt());
|
||||
row.setStatus("CANCELLED".equals(trip.getStatus())?"CANCELLED":"ACTIVE");
|
||||
row.setStage(switch(trip.getStatus()){case "IN_TRANSIT"->"DELIVERING";case "COMPLETED"->"DELIVERED";default->"BEFORE_LOADING";});
|
||||
row.setStage(switch(trip.getStatus()){case "IN_TRANSIT"->"DELIVERING";case "COMPLETED"->"DELIVERED";case "PLANNED"->row.getStage();default->"BEFORE_LOADING";});
|
||||
// 旧发车入口只记录出发事实,取货照片尚未齐全时仍在装货中。
|
||||
if("PICKUP".equals(row.getTaskType()) && "IN_TRANSIT".equals(trip.getStatus())
|
||||
&& pickupFacts.tasks(trip.getTripNo()).stream().anyMatch(task->"ASSIGNED".equals(task.getStatus())))row.setStage("LOADING");
|
||||
row.setDepartedAt(trip.getDepartedAt());row.setDeliveryCompletedAt(trip.getCompletedAt());
|
||||
String oldResources=row.getResourcesJson();var oldDeparture=row.getPlannedDepartureAt();var oldArrival=row.getEstimatedArrivalAt();
|
||||
row.setPlannedDepartureAt(trip.getPlannedDepartureAt());row.setEstimatedArrivalAt(trip.getExpectedDriverEndAt());row.setRemark(trip.getRemark());resources(row,trip);
|
||||
// 资源或计划改变后作废本轮审核,保留审核驳回原因供修改时核对。
|
||||
if("PICKUP".equals(row.getTaskType()) && "PLANNED".equals(trip.getStatus())
|
||||
&& (!Objects.equals(oldResources,row.getResourcesJson()) || !Objects.equals(oldDeparture,row.getPlannedDepartureAt()) || !Objects.equals(oldArrival,row.getEstimatedArrivalAt()))) {
|
||||
row.setReviewState("DRAFT");row.setReviewBasisHash(null);row.setStage("BEFORE_LOADING");
|
||||
row.setDispatchReviewJson(invalidated(row.getDispatchReviewJson(),row.getReviewRound()));
|
||||
row.setLoadingReviewJson(invalidated(row.getLoadingReviewJson(),row.getReviewRound()));
|
||||
}
|
||||
if(roots.saveState(row,version)!=1)throw new IllegalStateException("运输排车版本保存失败");
|
||||
roots.history(IdWorker.getId(),row,event.action(),event.operator(),event.reason(),write(row),trip.getUpdatedAt());
|
||||
var link=links.truck(row.getDispatchNo());if(link==null)return;
|
||||
@@ -62,6 +74,30 @@ public class TransportTaskProjection {
|
||||
transition(link.customerId(),link.orderNo(),target,event.action(),event.operator(),event.reason(),row.getDispatchNo()+":"+trip.getRecordVersion());
|
||||
if("CANCELLED".equals(trip.getStatus()))links.cancel(row.getDispatchNo());
|
||||
}
|
||||
/** 旧取货发车入口也必须锁定并核验同轮两级批准,不允许无主单发车。 */
|
||||
@Override
|
||||
public void requireApproved(long tripId) {
|
||||
var found=links.byTrip(tripId);
|
||||
if(found==null)throw new IllegalStateException("取货排车缺失,请先接入排车审核");
|
||||
var row=roots.lock(found.getDispatchNo());
|
||||
if(!"PICKUP".equals(row.getTaskType()) || !"ACTIVE".equals(row.getStatus()) || !"APPROVED".equals(row.getReviewState()) || row.getReviewBasisHash()==null)
|
||||
throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT,"请先完成调度审核和理货员审核");
|
||||
for(String value:List.of(row.getDispatchReviewJson(),row.getLoadingReviewJson())) {
|
||||
try {
|
||||
var review=json.readValue(value,Review.class);
|
||||
if(review.status()!=ReviewStatus.APPROVED || review.reviewRound()!=row.getReviewRound() || !Objects.equals(review.basisHash(),row.getReviewBasisHash()))
|
||||
throw new IllegalStateException("取货审核依据已失效,请重新提交审核");
|
||||
} catch(com.fasterxml.jackson.core.JsonProcessingException e){throw new IllegalStateException("取货审核记录读取失败",e);}
|
||||
}
|
||||
}
|
||||
/** 保留驳回事实,同时明确此前批准已因资源变更失效。 */
|
||||
private String invalidated(String value,long round) {
|
||||
try {
|
||||
var old=json.readValue(value,Review.class);
|
||||
return write(new Review(old.status()==ReviewStatus.REJECTED?ReviewStatus.REJECTED:ReviewStatus.NOT_SUBMITTED,
|
||||
old.reason(),old.reviewerId(),old.reviewedAt(),round,null,"车辆、司机或计划已变更,请重新提交两级审核",old.attachments()));
|
||||
} catch(com.fasterxml.jackson.core.JsonProcessingException e){throw new IllegalStateException("取货审核记录读取失败",e);}
|
||||
}
|
||||
/** 本土卡车只回写真实运输状态并追加原业务不可变历史。 */
|
||||
public void transition(String customer,String number,String target,String action,String operator,String reason,String key) {
|
||||
var order=trucks.lockNo(customer,number);if(order==null)throw new IllegalStateException("关联本土卡车单不存在");
|
||||
|
||||
+25
@@ -51,6 +51,22 @@ public class TransportTaskService {
|
||||
var tasks=pickups.tasks(trip.getTripNo());var row=projection.register(trip,tasks.size(),0,tasks.stream().mapToInt(FleetPickupModels.Task::packageCount).sum(),operator);
|
||||
return detail(row.getDispatchNo());
|
||||
}
|
||||
/** 审核通过后按原单锁幂等生成待发车排车;重复审核保留原安排及取消结果。 */
|
||||
@Transactional
|
||||
public void ensureApprovedPickup(String customer,String number,String operator) {
|
||||
var source=pickupFacts.source(customer,number,true);
|
||||
if(source==null)throw missing("转运单不存在");
|
||||
if(!"WAREHOUSE_PICKUP".equals(source.getDeliveryMethod()) || !number.equals(source.getInboundCode())
|
||||
|| !"WAIT_PDA_RECEIVING".equals(source.getStatus()))return;
|
||||
var task=pickupFacts.lockTask(source.getId());
|
||||
// 已安排、执行或主动取消的取货任务均不因审核重放重新建单。
|
||||
if(task!=null)return;
|
||||
var departure=Instant.now().truncatedTo(java.time.temporal.ChronoUnit.SECONDS);
|
||||
createPickup(operator,new FleetPickupModels.Create(List.of(new FleetPickupModels.Source(customer,number)),
|
||||
departure,departure.plus(Duration.ofHours(24)),
|
||||
"审核自动生成到仓取货排车;时间为暂定计划,调度确认后分配车辆司机",
|
||||
"transfer-approval:"+source.getId()));
|
||||
}
|
||||
/** 原单锁和有效关联唯一约束共同阻止同单重复排车。 */
|
||||
@Transactional
|
||||
public TransportTaskModels.Detail createTruck(String operator,TransportTaskModels.TruckCreate request) {
|
||||
@@ -97,6 +113,15 @@ public class TransportTaskService {
|
||||
var trip=transport(number,true);trips.assign(trip.getTripNo(),operator,request);
|
||||
if("LOCAL_TRUCK".equals(trip.getTripPurpose()))capacity(tripRepo.findTrip(trip.getTripNo()),truck(number));return detail(number);
|
||||
}
|
||||
/** 取货提交与两级通过时复验真实占用、有效证照和车辆容量。 */
|
||||
public void verifyPickupReview(String number) {
|
||||
var trip=transport(number,true);
|
||||
if(!"PICKUP".equals(trip.getTripPurpose()) || !"PLANNED".equals(trip.getStatus()) || pickups.tasks(trip.getTripNo()).isEmpty())throw conflict("仅有真实取货任务的未发车排车可以审核");
|
||||
if(trip.getPlannedDepartureAt()==null || trip.getExpectedDriverEndAt()==null || !trip.getExpectedDriverEndAt().isAfter(trip.getPlannedDepartureAt()) || !trip.getExpectedDriverEndAt().isAfter(now()))throw conflict("请维护有效的计划发车与任务结束时间");
|
||||
var pallets=trip.getMaxPalletSpaces();var weight=trip.getMaxLoadKg();var volume=trip.getMaxVolumeCbm();
|
||||
requireResources(trip);
|
||||
if(!Objects.equals(pallets,trip.getMaxPalletSpaces()) || !Objects.equals(weight,trip.getMaxLoadKg()) || !Objects.equals(volume,trip.getMaxVolumeCbm()))throw conflict("车辆主档已变化,请重新维护车辆信息");
|
||||
}
|
||||
/** 取消运输并保留原单、排车及资源审计。 */
|
||||
@Transactional
|
||||
public TransportTaskModels.Detail cancel(String number,String operator,FleetTripModels.Cancel request) {
|
||||
|
||||
@@ -39,6 +39,9 @@ public interface FleetPickupMapper extends BaseMapper<FleetPickupTask> {
|
||||
/** 解除计划中的取货关联,保留历史后允许再次安排。 */
|
||||
@Update("UPDATE ll_fleet_pickup_task SET trip_no=NULL,status='WAITING',updated_at=CURRENT_TIMESTAMP WHERE trip_no=#{tripNo} AND status='ASSIGNED'")
|
||||
int unassign(@Param("tripNo") String tripNo);
|
||||
/** 原单同时取消时保留车次关联供详情和审计追溯,不再回到待安排。 */
|
||||
@Update("UPDATE ll_fleet_pickup_task SET status='CANCELLED',updated_at=CURRENT_TIMESTAMP WHERE trip_no=#{tripNo} AND status='ASSIGNED'")
|
||||
int cancelTasks(@Param("tripNo") String tripNo);
|
||||
/** 不可变操作历史覆盖安排、解除、取货、到仓及结案。 */
|
||||
@Insert("INSERT INTO ll_fleet_pickup_history(customer_id,transfer_no,trip_no,action,operator_id,occurred_at,detail) VALUES(#{customer},#{number},#{trip},#{action},#{operator},CURRENT_TIMESTAMP,#{detail})")
|
||||
int history(@Param("customer") String customer,@Param("number") String number,@Param("trip") String trip,
|
||||
|
||||
+8
-2
@@ -24,7 +24,10 @@ public final class FleetPickupModels {
|
||||
@NotBlank @Size(min=8,max=128) String idempotencyKey) { }
|
||||
public record Command(@NotNull @Min(0) Long recordVersion,
|
||||
@NotBlank @Size(min=8,max=128) String idempotencyKey,
|
||||
@NotBlank @Size(max=1000) String reason) { }
|
||||
@NotBlank @Size(max=1000) String reason,List<@NotNull @Positive Long> evidenceFileIds) {
|
||||
/** 无照片的非送仓命令保持原调用形式。 */
|
||||
public Command(Long recordVersion,String idempotencyKey,String reason){this(recordVersion,idempotencyKey,reason,null);}
|
||||
}
|
||||
public record Pickup(@NotNull @Min(0) Long recordVersion,
|
||||
@NotBlank @Size(min=8,max=128) String idempotencyKey,
|
||||
@Min(1) @Max(5000) int actualPackages,
|
||||
@@ -35,6 +38,9 @@ public final class FleetPickupModels {
|
||||
String status, String tripNo, String vehiclePlate, String driverName, String driverPhone,
|
||||
Instant plannedDepartureAt, Integer actualPackages, Instant pickedAt, Instant arrivedAt,
|
||||
String remark, List<Evidence> evidence) { }
|
||||
public record Evidence(String fileId, String fileName, String contentType, long sizeBytes) { }
|
||||
public record Evidence(String fileId, String fileName, String contentType, long sizeBytes,String phase) {
|
||||
/** 旧取货凭证明确归入取货阶段,不冒充到仓照片。 */
|
||||
public Evidence(String fileId,String fileName,String contentType,long sizeBytes){this(fileId,fileName,contentType,sizeBytes,"PICKUP");}
|
||||
}
|
||||
public record History(String action, String tripNo, String operatorId, Instant occurredAt, String detail) { }
|
||||
}
|
||||
|
||||
+2
@@ -34,6 +34,8 @@ public class FleetPickupRepository {
|
||||
public FleetPickupTask lockTask(long id) { return mapper.lockTask(id); }
|
||||
/** 释放未开始的成员关联。 */
|
||||
public int unassign(String number) { return mapper.unassign(number); }
|
||||
/** 联动取消保留原车次与转运单的可追溯关联。 */
|
||||
public int cancelTasks(String number) { return mapper.cancelTasks(number); }
|
||||
/** 保存不可变业务操作记录。 */
|
||||
public void history(FleetPickupTask task,String action,String operator,String detail) { mapper.history(task.getCustomerId(),task.getTransferNo(),task.getTripNo(),action,operator,detail); }
|
||||
/** 核对旧拼车承运关系,防止同票走两条运输链。 */
|
||||
|
||||
+26
-4
@@ -28,11 +28,13 @@ public class FleetPickupService {
|
||||
private final VehicleRepository vehicles;
|
||||
private final TripOperationsEvidenceGateway files;
|
||||
private final ObjectMapper json;
|
||||
private final PickupDepartureReview departureReview;
|
||||
/** 共用现有车次、司机资格、车辆占用及鉴权文件能力。 */
|
||||
public FleetPickupService(FleetPickupRepository repository,FleetTripService trips,FleetWarehouseDirectory warehouses,
|
||||
DriverService drivers,VehicleRepository vehicles,TripOperationsEvidenceGateway files,ObjectMapper json) {
|
||||
DriverService drivers,VehicleRepository vehicles,TripOperationsEvidenceGateway files,ObjectMapper json,PickupDepartureReview departureReview) {
|
||||
this.repository=repository;this.trips=trips;this.warehouses=warehouses;this.drivers=drivers;
|
||||
this.vehicles=vehicles;this.files=files;this.json=json;
|
||||
this.departureReview=departureReview;
|
||||
}
|
||||
/** 审核通过的原单自动出现在候选中,客户自送和库存转运不参与。 */
|
||||
@Transactional(readOnly=true,isolation=org.springframework.transaction.annotation.Isolation.REPEATABLE_READ)
|
||||
@@ -120,6 +122,7 @@ public class FleetPickupService {
|
||||
if(replayed(trip,request.idempotencyKey(),hash))return trips.detail(number);
|
||||
version(trip,request.recordVersion());
|
||||
if(!"PLANNED".equals(trip.getStatus()) || repository.tasks(number).isEmpty())throw conflict("仅有取货任务的计划车次可以发车");
|
||||
departureReview.requireApproved(trip.getId());
|
||||
requireResources(trip);
|
||||
for(var task:repository.tasks(number))eligible(source(task.getCustomerId(),task.getTransferNo(),true));
|
||||
trip.setStatus("IN_TRANSIT");trip.setDepartedAt(now());
|
||||
@@ -142,28 +145,47 @@ public class FleetPickupService {
|
||||
var trip=pickupTrip(number,true);String hash=commandHash("PICKED:"+customer+":"+transferNo,request);
|
||||
if(replayed(trip,request.idempotencyKey(),hash))return tasks(number);
|
||||
version(trip,request.recordVersion());var task=member(trip,customer,transferNo);
|
||||
if(!"IN_TRANSIT".equals(trip.getStatus()) || !"ASSIGNED".equals(task.getStatus()))throw conflict("请先发车,且该单尚未确认取货");
|
||||
if(!Set.of("PLANNED","IN_TRANSIT").contains(trip.getStatus()) || !"ASSIGNED".equals(task.getStatus()))throw conflict("该单尚未处于可确认取货阶段");
|
||||
if("PLANNED".equals(trip.getStatus()))departureReview.requireApproved(trip.getId());
|
||||
if(request.actualPackages()<=0 || request.evidenceFileIds()==null || request.evidenceFileIds().isEmpty() || request.evidenceFileIds().size()>10)throw bad("请填写实提箱数并上传取货照片");
|
||||
eligible(source(customer,transferNo,true));
|
||||
var evidence=request.evidenceFileIds().stream().distinct().map(id->{
|
||||
if(!repository.ownsUpload(task.getTransferOrderId(),number,id))throw bad("取货凭证不属于当前订单或车次");
|
||||
var file=files.metadata(trip.getId(),id);
|
||||
if(file.contentType()==null || !file.contentType().startsWith("image/"))throw bad("请上传有效取货照片");
|
||||
return new FleetPickupModels.Evidence(String.valueOf(file.fileId()),file.fileName(),file.contentType(),file.sizeBytes());
|
||||
}).toList();
|
||||
task.setStatus("PICKED");task.setActualPackages(request.actualPackages());task.setPickedAt(now());
|
||||
task.setEvidenceJson(write(evidence));task.setRemark(blank(request.remark()));task.setUpdatedAt(now());repository.update(task);
|
||||
repository.history(task,"PICKED",operator,"实提"+request.actualPackages()+"箱;"+Objects.toString(task.getRemark(),""));
|
||||
// 全车照片和实提数量核齐后自动进入派送中,不能以发车按钮代替取货照片。
|
||||
if("PLANNED".equals(trip.getStatus()) && repository.tasks(number).stream().allMatch(row->"PICKED".equals(row.getStatus()))) {
|
||||
requireResources(trip);trip.setStatus("IN_TRANSIT");trip.setDepartedAt(now());
|
||||
}
|
||||
trips.recordExecution(trip,request.recordVersion(),"PICKUP_PICKED","确认原单取货",operator);
|
||||
repository.command(number,request.idempotencyKey(),hash);return tasks(number);
|
||||
}
|
||||
/** 到仓确认只开放 PDA 清点,不覆盖预报箱数和 SKU 件数。 */
|
||||
/** 到仓照片独立归档,全部送仓后自动完成本次取货派送并开放PDA清点。 */
|
||||
@Transactional
|
||||
public List<FleetPickupModels.Task> arrive(String number,String customer,String transferNo,String operator,FleetPickupModels.Command request) {
|
||||
var trip=pickupTrip(number,true);String hash=commandHash("ARRIVED:"+customer+":"+transferNo,request);
|
||||
if(replayed(trip,request.idempotencyKey(),hash))return tasks(number);
|
||||
version(trip,request.recordVersion());var task=member(trip,customer,transferNo);
|
||||
if(!"IN_TRANSIT".equals(trip.getStatus()) || !"PICKED".equals(task.getStatus()))throw conflict("请先确认该单已取货");
|
||||
source(customer,transferNo,true);task.setStatus("ARRIVED");task.setArrivedAt(now());task.setUpdatedAt(now());repository.update(task);
|
||||
if(request.evidenceFileIds()==null || request.evidenceFileIds().isEmpty() || request.evidenceFileIds().size()>10)throw bad("请上传到仓照片");
|
||||
var old=evidence(task);var arrival=new ArrayList<FleetPickupModels.Evidence>(old);
|
||||
for(long id:request.evidenceFileIds().stream().distinct().toList()) {
|
||||
if(!repository.ownsUpload(task.getTransferOrderId(),number,id) || old.stream().anyMatch(file->file.fileId().equals(String.valueOf(id))))throw bad("请上传本票新的到仓照片,不能复用取货照片");
|
||||
var file=files.metadata(trip.getId(),id);
|
||||
if(file.contentType()==null || !file.contentType().startsWith("image/"))throw bad("请上传有效到仓照片");
|
||||
arrival.add(new FleetPickupModels.Evidence(String.valueOf(file.fileId()),file.fileName(),file.contentType(),file.sizeBytes(),"ARRIVAL"));
|
||||
}
|
||||
source(customer,transferNo,true);task.setStatus("ARRIVED");task.setArrivedAt(now());task.setEvidenceJson(write(arrival));task.setUpdatedAt(now());repository.update(task);
|
||||
repository.history(task,"ARRIVED",operator,request.reason());
|
||||
if(repository.tasks(number).stream().allMatch(row->"ARRIVED".equals(row.getStatus()))) {
|
||||
trip.setStatus("COMPLETED");trip.setCompletedAt(now());
|
||||
trip.setCompletionJson(write(new FleetCompletionModels.Completed(request.idempotencyKey(),hash,trip.getCompletedAt().toInstant(ZoneOffset.UTC),Instant.now(),operator,List.of())));
|
||||
}
|
||||
trips.recordExecution(trip,request.recordVersion(),"PICKUP_ARRIVED",request.reason(),operator);
|
||||
repository.command(number,request.idempotencyKey(),hash);return tasks(number);
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.baiyue.lastleg.fleet.pickup;
|
||||
|
||||
/** 取货发车必须通过排车主单的本轮两级审核。 */
|
||||
public interface PickupDepartureReview {
|
||||
/** 调用方已持车次锁,校验并锁定对应排车审核事实。 */
|
||||
void requireApproved(long tripId);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.lastleg.fleet.pickup;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 原单所属域负责取消未执行取货的转运业务,失败时整车取消回滚。 */
|
||||
public interface PickupOrderCancellation {
|
||||
/** 调用方持有车次锁,按稳定来源顺序取消全部成员。 */
|
||||
void cancel(List<FleetPickupModels.Source> sources,String tripNo,String operator,String reason);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ public class FleetTripService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort appointmentAllocations;
|
||||
private final FleetTripRepository repository;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.fleet.pickup.FleetPickupRepository pickups;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.fleet.pickup.PickupOrderCancellation pickupCancellation;
|
||||
private final FleetDispatchGateway dispatches;
|
||||
private final VehicleRepository vehicles;
|
||||
private final DriverService drivers;
|
||||
@@ -467,18 +468,20 @@ public class FleetTripService {
|
||||
|
||||
|
||||
|
||||
/** 取消未执行车次,释放资源并将取货原单退回待安排。 */
|
||||
/** 取消未执行车次,取货同时取消全部转运原单并释放资源。 */
|
||||
@Transactional
|
||||
public FleetTripModels.Detail cancel(String tripNo, String operator, FleetTripModels.Cancel request) {
|
||||
FleetTripEntity trip = planned(tripNo, request.recordVersion());
|
||||
requireReason(request.reason());
|
||||
if ("PICKUP".equals(trip.getTripPurpose())) {
|
||||
// 取消车次与释放原单关联同事务,失败时全部回滚。
|
||||
for (var task : pickups.tasks(tripNo)) {
|
||||
var tasks=pickups.tasks(tripNo);
|
||||
for (var task : tasks) {
|
||||
if (!"ASSIGNED".equals(task.getStatus())) throw conflict("已开始取货,不能取消车次");
|
||||
pickups.history(task,"UNASSIGN",operator,request.reason());
|
||||
}
|
||||
pickups.unassign(tripNo);
|
||||
pickupCancellation.cancel(tasks.stream().map(task->new com.baiyue.lastleg.fleet.pickup.FleetPickupModels.Source(task.getCustomerId(),task.getTransferNo())).toList(),tripNo,operator,request.reason());
|
||||
for(var task:tasks)pickups.history(task,"CANCEL",operator,"取消排车及转运单:"+request.reason());
|
||||
if(pickups.cancelTasks(tripNo)!=tasks.size())throw conflict("取货关联已变化,取消未完成");
|
||||
}
|
||||
if (!repository.listBindings(trip.getId()).isEmpty()) throw conflict("请先解除客户排车单关联,再取消车次");
|
||||
if (bookingReservations.hasConfirmed(trip.getId())) throw conflict("请先取消已确认的外接预订,再取消车次");
|
||||
@@ -554,6 +557,7 @@ public class FleetTripService {
|
||||
if (trip == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "车次不存在");
|
||||
if (trip.getRecordVersion() != version) throw conflict("车次已被修改,请刷新后重试");
|
||||
if (!"PLANNED".equals(trip.getStatus())) throw conflict("仅计划中的车次允许此操作");
|
||||
if("PICKUP".equals(trip.getTripPurpose()) && pickups.tasks(number).stream().anyMatch(task->!"ASSIGNED".equals(task.getStatus())))throw conflict("已开始取货,不能修改资源、计划或取消排车");
|
||||
return trip;
|
||||
}
|
||||
/** 复核创建请求摘要,同键不同内容不允许复用。 */
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.baiyue.lastleg.transfer.mapper;
|
||||
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
/** 仅取消本客户外来取货的待执行原单,不释放或回滚任何已入库库存。 */
|
||||
@Mapper
|
||||
public interface TransferPickupCancellationMapper {
|
||||
/** 与PDA共用转运原单锁,串行核验收货事实。 */
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_order WHERE customer_id=#{customer} AND transfer_no=#{number} FOR UPDATE")
|
||||
WarehouseTransferOrder lock(@Param("customer") String customer,@Param("number") String number);
|
||||
/** 任何收货、箱清点或托盘测量开始后均禁止普通取消。 */
|
||||
@Select("SELECT (SELECT COUNT(*) FROM ll_warehouse_transfer_receipt WHERE customer_id=#{customer} AND transfer_order_id=#{id}) + (SELECT COUNT(*) FROM ll_warehouse_transfer_carton WHERE customer_id=#{customer} AND transfer_order_id=#{id} AND counted_at IS NOT NULL) + (SELECT COUNT(*) FROM ll_warehouse_transfer_pallet WHERE customer_id=#{customer} AND transfer_order_id=#{id})")
|
||||
int started(@Param("customer") String customer,@Param("id") long id);
|
||||
/** 同一取消事务只接受待PDA收货的转运单。 */
|
||||
@Update("UPDATE ll_warehouse_transfer_order SET status='CANCELLED',updated_at=CURRENT_TIMESTAMP WHERE customer_id=#{customer} AND id=#{id} AND status='WAIT_PDA_RECEIVING'")
|
||||
int cancelTransfer(@Param("customer") String customer,@Param("id") long id);
|
||||
/** 写入主单取消事实并递增版本,防止旧页面继续写入。 */
|
||||
@Update("UPDATE ll_order SET current_status='CANCELLED',sub_order_status='WAREHOUSE_TRANSFER:CANCELLED',version=version+1,updated_at=CURRENT_TIMESTAMP WHERE customer_id=#{customer} AND id=#{id} AND current_status='INBOUND_MEASURING'")
|
||||
int cancelOrder(@Param("customer") String customer,@Param("id") long id);
|
||||
/** 只取消尚未接货的外来入仓单。 */
|
||||
@Update("UPDATE ll_inbound_order SET status='CANCELLED',updated_at=CURRENT_TIMESTAMP WHERE customer_id=#{customer} AND id=#{id} AND status='PENDING_INBOUND'")
|
||||
int cancelInbound(@Param("customer") String customer,@Param("id") long id);
|
||||
/** 只取消原转运生成且仍待PDA的唯一计划。 */
|
||||
@Update("UPDATE ll_inbound_plan SET status='CANCELLED',version=version+1,updated_at=CURRENT_TIMESTAMP WHERE customer_id=#{customer} AND source_order_id=#{order} AND creation_mode='WAREHOUSE_TRANSFER' AND status='PDA_PENDING'")
|
||||
int cancelPlan(@Param("customer") String customer,@Param("order") long order);
|
||||
/** 不可变历史保留取消前状态、原车次与操作者。 */
|
||||
@Insert("INSERT INTO ll_loading_order_status_history(customer_id,order_id,from_status,to_status,event_id,remark,created_at) SELECT customer_id,id,current_status,'CANCELLED',#{trip},CONCAT('取消取货排车及转运单;操作人:',#{operator},';原因:',#{reason}),CURRENT_TIMESTAMP FROM ll_order WHERE customer_id=#{customer} AND id=#{id}")
|
||||
int history(@Param("customer") String customer,@Param("id") long id,@Param("trip") String trip,@Param("operator") String operator,@Param("reason") String reason);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.transfer.repository;
|
||||
|
||||
import com.baiyue.lastleg.transfer.mapper.TransferPickupCancellationMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/** 未执行取货取消的原单、入仓与历史持久化边界。 */
|
||||
@Repository
|
||||
public class TransferPickupCancellationRepository {
|
||||
private final TransferPickupCancellationMapper mapper;
|
||||
/** 注入本业务取消映射器。 */
|
||||
public TransferPickupCancellationRepository(TransferPickupCancellationMapper mapper){this.mapper=mapper;}
|
||||
/** 锁定真实客户所属转运来源。 */
|
||||
public com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder lock(String customer,String number){return mapper.lock(customer,number);}
|
||||
/** 已存在清点、托盘或收货事实时禁止普通取消。 */
|
||||
public boolean started(String customer,long id){return mapper.started(customer,id)>0;}
|
||||
/** 取消转运并检查当前状态。 */
|
||||
public int cancelTransfer(String customer,long id){return mapper.cancelTransfer(customer,id);}
|
||||
/** 取消尾程主单并记录当前状态来源。 */
|
||||
public int cancelOrder(String customer,long id,String trip,String operator,String reason){mapper.history(customer,id,trip,operator,reason);return mapper.cancelOrder(customer,id);}
|
||||
/** 取消原入仓单,保留不可删除的业务关联。 */
|
||||
public int cancelInbound(String customer,long id){return mapper.cancelInbound(customer,id);}
|
||||
/** 取消待收货计划,已执行的计划导致整车取消失败。 */
|
||||
public int cancelPlan(String customer,long order){return mapper.cancelPlan(customer,order);}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.lastleg.transfer.service;
|
||||
|
||||
import com.baiyue.lastleg.fleet.pickup.*;
|
||||
import com.baiyue.lastleg.transfer.repository.TransferPickupCancellationRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.*;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.List;
|
||||
|
||||
/** 取货排车取消时原子取消其全部转运、尾程、入仓单及待执行入仓计划。 */
|
||||
@Service
|
||||
public class TransferPickupCancellationService implements PickupOrderCancellation {
|
||||
private final TransferPickupCancellationRepository repository;
|
||||
/** 取消只访问本域持久化边界,不反向调用排车服务。 */
|
||||
public TransferPickupCancellationService(TransferPickupCancellationRepository repository){this.repository=repository;}
|
||||
/** 任一来源已经执行或归属不符时抛错,使外层整车取消全部回滚。 */
|
||||
@Override @Transactional(propagation=Propagation.MANDATORY)
|
||||
public void cancel(List<FleetPickupModels.Source> sources,String tripNo,String operator,String reason) {
|
||||
if(sources==null || sources.isEmpty())throw conflict();
|
||||
for(var source:sources) {
|
||||
var row=repository.lock(source.customerId(),source.transferNo());
|
||||
if(row==null || !"WAREHOUSE_PICKUP".equals(row.getDeliveryMethod()) || !row.getTransferNo().equals(row.getInboundCode()) || !"WAIT_PDA_RECEIVING".equals(row.getStatus()) || repository.started(source.customerId(),row.getId()))throw conflict();
|
||||
if(repository.cancelTransfer(source.customerId(),row.getId())!=1
|
||||
|| repository.cancelOrder(source.customerId(),row.getLastlegOrderId(),tripNo,operator,reason)!=1
|
||||
|| repository.cancelInbound(source.customerId(),row.getInboundOrderId())!=1
|
||||
|| repository.cancelPlan(source.customerId(),row.getLastlegOrderId())!=1)throw conflict();
|
||||
}
|
||||
}
|
||||
/** 保留用户上下文,不把部分取消作为成功返回。 */
|
||||
private static ResponseStatusException conflict(){return new ResponseStatusException(HttpStatus.CONFLICT,"转运单已开始收货或关联状态变化,不能取消排车及转运单,请刷新核对");}
|
||||
}
|
||||
+6
@@ -94,6 +94,7 @@ import java.util.stream.Collectors;
|
||||
public class WarehouseTransferService {
|
||||
@Resource private TransferQuantityService transferQuantityService;
|
||||
@Resource private com.baiyue.lastleg.fleet.pickup.FleetPickupService pickupService;
|
||||
@Resource private com.baiyue.lastleg.dispatch.transport.TransportTaskService transportTaskService;
|
||||
/** 退货单在转运来源中的展示编码,避免把退货单号误当成普通入仓码查询。 */
|
||||
public static final String RETURN_ORDER_PREFIX = "RETURN_ORDER:";
|
||||
@Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator compactNumbers;
|
||||
@@ -814,6 +815,7 @@ public class WarehouseTransferService {
|
||||
return new WarehouseTransferResponses.Label(transferNo, cartons, html);
|
||||
}
|
||||
|
||||
/** 审核与入仓计划、上门取货排车原子提交,重放复用原业务关联。 */
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail review(String customerId, String transferNo, String operatorId,
|
||||
WarehouseTransferRequests.Review request) {
|
||||
@@ -836,6 +838,7 @@ public class WarehouseTransferService {
|
||||
.contains(transfer.getStatus())) {
|
||||
if (APPROVED.equals(next) && transfer.getTransferNo().equals(transfer.getInboundCode())) {
|
||||
activateTransferInbound(customerId, transfer, LocalDateTime.now());
|
||||
transportTaskService.ensureApprovedPickup(customerId, transferNo, operatorId);
|
||||
}
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
@@ -865,6 +868,9 @@ public class WarehouseTransferService {
|
||||
"Warehouse transfer reviewed: " + next
|
||||
+ (StringUtils.hasText(request.remark()) ? "; " + request.remark().trim() : ""), now);
|
||||
}
|
||||
if (APPROVED.equals(next) && incomingTransit) {
|
||||
transportTaskService.ensureApprovedPickup(customerId, transferNo, operatorId);
|
||||
}
|
||||
return detail(customerId, transferNo);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user