feat(javaTest): execute source-aligned business phases

This commit is contained in:
zgw
2026-08-10 09:45:59 +08:00
parent 6169fbcb46
commit 1c73fda672
4 changed files with 652 additions and 36 deletions

View File

@@ -92,7 +92,8 @@ public final class HistoricalDispatchSimulator {
for (HistoricalSimulationTask task : input.getTasks()) {
long arrival = clock.toSimulationMillis(task.getCreatedAt());
queue.add(task, arrival);
executions.put(task.getSimulationTaskId(), new Execution(task, arrival));
executions.put(task.getSimulationTaskId(), new Execution(
task, arrival, input.getExecutionMode()));
}
List<SimulationTickEvent> events = new ArrayList<SimulationTickEvent>();
@@ -450,9 +451,11 @@ public final class HistoricalDispatchSimulator {
task.getSimulationTaskId(), vehicleId);
List<String> postTransferRoute = new ArrayList<String>(
postTransferApproach.getSelectedPath());
for (int index = 1;
index < postTransferService.getSelectedPath().size(); index++) {
postTransferRoute.add(postTransferService.getSelectedPath().get(index));
if (!execution.isBusinessMode()) {
for (int index = 1;
index < postTransferService.getSelectedPath().size(); index++) {
postTransferRoute.add(postTransferService.getSelectedPath().get(index));
}
}
String destinationHold = destinationCommitmentReason(
vehicleId, postTransferRoute.get(postTransferRoute.size() - 1),
@@ -547,10 +550,12 @@ public final class HistoricalDispatchSimulator {
RoutePlanDecision serviceDecision = routeEvaluator.select(input.getGridMap(),
serviceCandidates, traffic, task.getSimulationTaskId(), vehicleId);
// 车辆先走到业务源点,再执行服务段;拼接时去掉重复源点。
// 原始模式仍一次执行“接近 + 服务”;业务模式首段只让空车到取货点,后续载货段必须重新规划。
List<String> route = new ArrayList<String>(approachDecision.getSelectedPath());
List<String> servicePath = serviceDecision.getSelectedPath();
for (int i = 1; i < servicePath.size(); i++) route.add(servicePath.get(i));
if (!execution.isBusinessMode()) {
for (int i = 1; i < servicePath.size(); i++) route.add(servicePath.get(i));
}
String destinationHold = destinationCommitmentReason(
vehicleId, route.get(route.size() - 1), vehicles, active,
floorTransfers, input.getGridMap());
@@ -582,18 +587,27 @@ public final class HistoricalDispatchSimulator {
// 预约成功才正式交接给活动任务;这些字段同时构成“历史方案与新方案”的可审计差异。
recordAssignment(execution, vehicleId, now);
if (execution.assignmentMode == null) execution.assignmentMode = AssignmentMode.DIRECT;
execution.plannedPath = new ArrayList<String>(servicePath);
execution.pathVersion++;
execution.planningEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.APPROACH,
execution.pathVersion, now, false, approachDecision));
execution.planningEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.SERVICE,
execution.pathVersion, now, false, serviceDecision,
historicalReferenceQuality(task, input, traffic, routePlanner,
routeEvaluator, vehicleId)));
active.put(vehicleId, new ActiveTask(execution, route,
approachDecision.getSelectedPath().size() - 1));
if (execution.isBusinessMode()) {
// 第一阶段路径就是所选车辆从当前位置到真实取货点的接近路径。
execution.plannedPath = new ArrayList<String>();
BusinessTaskLeg leg = execution.currentLeg();
execution.beginPhase(leg, route.get(0), route, now);
active.put(vehicleId, new ActiveTask(execution, route, 0,
leg.getPhase(), execution.pathVersion));
} else {
execution.plannedPath = new ArrayList<String>(servicePath);
execution.planningEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.SERVICE,
execution.pathVersion, now, false, serviceDecision,
historicalReferenceQuality(task, input, traffic, routePlanner,
routeEvaluator, vehicleId)));
active.put(vehicleId, new ActiveTask(execution, route,
approachDecision.getSelectedPath().size() - 1));
}
queue.remove(task.getSimulationTaskId());
events.add(event(tick, now, "ASSIGN", task.getSimulationTaskId(), vehicleId,
decision.getReason(), route.get(0), route.get(route.size() - 1)));
@@ -1017,8 +1031,10 @@ public final class HistoricalDispatchSimulator {
serviceCandidates, traffic, task.getSimulationTaskId(),
vehicle.getVehicleId());
List<String> route = new ArrayList<String>(approachDecision.getSelectedPath());
for (int index = 1; index < serviceDecision.getSelectedPath().size(); index++) {
route.add(serviceDecision.getSelectedPath().get(index));
if (!execution.isBusinessMode()) {
for (int index = 1; index < serviceDecision.getSelectedPath().size(); index++) {
route.add(serviceDecision.getSelectedPath().get(index));
}
}
CorridorReservationManager.ReservationDecision reservation = reservations.replaceRoute(
@@ -1033,18 +1049,26 @@ public final class HistoricalDispatchSimulator {
}
// 预约成功后才提交新路径版本;证据中的接近段必定从设备完成事实返回的实际出口开始。
execution.plannedPath = new ArrayList<String>(serviceDecision.getSelectedPath());
execution.pathVersion++;
execution.planningEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.APPROACH,
execution.pathVersion, now, false, approachDecision));
execution.planningEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.SERVICE,
execution.pathVersion, now, false, serviceDecision,
historicalReferenceQuality(task, input, traffic, routePlanner,
routeEvaluator, vehicle.getVehicleId())));
active.put(vehicle.getVehicleId(), new ActiveTask(execution, route,
approachDecision.getSelectedPath().size() - 1));
if (execution.isBusinessMode()) {
execution.plannedPath = new ArrayList<String>();
BusinessTaskLeg leg = execution.currentLeg();
execution.beginPhase(leg, route.get(0), route, now);
active.put(vehicle.getVehicleId(), new ActiveTask(execution, route, 0,
leg.getPhase(), execution.pathVersion));
} else {
execution.plannedPath = new ArrayList<String>(serviceDecision.getSelectedPath());
execution.planningEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.SERVICE,
execution.pathVersion, now, false, serviceDecision,
historicalReferenceQuality(task, input, traffic, routePlanner,
routeEvaluator, vehicle.getVehicleId())));
active.put(vehicle.getVehicleId(), new ActiveTask(execution, route,
approachDecision.getSelectedPath().size() - 1));
}
floorTransfers.remove(vehicle.getVehicleId());
events.add(event(tick, now, "POST_TRANSFER_REPLAN",
task.getSimulationTaskId(), vehicle.getVehicleId(),
@@ -1311,9 +1335,11 @@ public final class HistoricalDispatchSimulator {
SimulatedVehicleRuntime vehicle = vehicles.get(vehicleId);
if (!vehicle.isAssignmentEligible()) continue;
if (current.index >= current.route.size() - 1) {
// 路线已经走完时不再申请资源,直接进入归档和 WMS 回调闭环。
complete(tick, now, current, vehicle, active, terminal, plc,
archive, wms, invariants, reservations, events);
// 原始路线直接归档;业务路线只关闭当前阶段,并从车辆事实位置规划下一阶段。
finishReachedRoute(tick, now, current, vehicle, input, config, queue,
vehicles, active, terminal, routePlanner, routeEvaluator,
ledger, plc, archive, wms, invariants, reservations,
clearanceMoves, floorTransfers, clearancePlanner, events);
progress = true;
continue;
}
@@ -1392,8 +1418,11 @@ public final class HistoricalDispatchSimulator {
continue;
}
// 资源已预占后才下发 PLC;只有确认 ACK 才能提交车辆位置变化。
SimulatedPlcGateway.PlcResponse sent = plc.send(
current.execution.task.getSimulationTaskId(), vehicle, from, to);
String parentTaskId = current.execution.task.getSimulationTaskId();
String commandId = current.phase == null ? parentTaskId : current.commandId();
SimulatedPlcGateway.PlcResponse sent = current.phase == null
? plc.send(parentTaskId, vehicle, from, to)
: plc.send(parentTaskId, commandId, vehicle, from, to);
SimulatedPlcGateway.PlcResponse accepted = plc.accept(sent, vehicle);
if (accepted.getReason() != SimulatedPlcGateway.PlcReason.ACK) {
// 命令未被确认时撤销本步新增占用,但保留当前位置和路线索引以便安全重试。
@@ -1402,8 +1431,10 @@ public final class HistoricalDispatchSimulator {
events.add(event(tick, now, "PLC_RETRY",
current.execution.task.getSimulationTaskId(), vehicleId,
accepted.getReason().name(), from, to));
if (plc.attemptsFor(current.execution.task.getSimulationTaskId())
% config.getRetryLimit() == 0) {
int commandAttempts = current.phase == null
? plc.attemptsForTask(parentTaskId)
: plc.attemptsForCommand(commandId);
if (commandAttempts % config.getRetryLimit() == 0) {
// 重试额度耗尽后释放未来走廊并回队,避免单车协议异常无限阻塞全局交通。
active.remove(vehicleId);
reservations.releaseTask(current.execution.task.getSimulationTaskId());
@@ -1429,14 +1460,235 @@ public final class HistoricalDispatchSimulator {
"ACK", from, to));
progress = true;
if (current.index >= current.route.size() - 1) {
// 最后一条边确认后立即关闭任务,避免下一个 tick 才释放剩余走廊和归档。
complete(tick, now, current, vehicle, active, terminal, plc,
archive, wms, invariants, reservations, events);
// 最后一条边 ACK 后立即提交当前阶段,业务下一段从这个真实位置重新规划。
finishReachedRoute(tick, now, current, vehicle, input, config, queue,
vehicles, active, terminal, routePlanner, routeEvaluator,
ledger, plc, archive, wms, invariants, reservations,
clearanceMoves, floorTransfers, clearancePlanner, events);
}
}
return progress;
}
/**
* 处理一条活动路线的终点:旧回放直接结束任务,业务回放只提交当前阶段并继续下一段。
*/
private void finishReachedRoute(
long tick, long now, ActiveTask current, SimulatedVehicleRuntime vehicle,
HistoricalSimulationInput input, SimulationConfig config,
SimulationTaskQueue queue,
Map<String, SimulatedVehicleRuntime> vehicles,
Map<String, ActiveTask> active,
Map<String, SimulationTaskResult> terminal,
RouteCandidatePlanner routePlanner, RouteQualityEvaluator routeEvaluator,
ResourceLedger ledger, SimulatedPlcGateway plc,
SimulationArchive archive, IdempotentWmsCompletionReceiver wms,
SimulationInvariantSummary invariants,
CorridorReservationManager reservations,
Map<String, ClearanceMove> clearanceMoves,
Map<String, FloorTransferExecution> floorTransfers,
ClearanceMovePlanner clearancePlanner,
List<SimulationTickEvent> events) {
if (!current.execution.isBusinessMode()) {
complete(tick, now, current, vehicle, active, terminal, plc,
archive, wms, invariants, reservations, events);
return;
}
completeBusinessPhase(tick, now, current, vehicle, input, config, queue,
vehicles, active, terminal, routePlanner, routeEvaluator,
ledger, plc, archive, wms, invariants, reservations,
clearanceMoves, floorTransfers, clearancePlanner, events);
}
/**
* 在路线终点 ACK 后提交业务阶段;放货只归档一次,最后一段才解除车辆与父任务的绑定。
*/
private void completeBusinessPhase(
long tick, long now, ActiveTask current, SimulatedVehicleRuntime vehicle,
HistoricalSimulationInput input, SimulationConfig config,
SimulationTaskQueue queue,
Map<String, SimulatedVehicleRuntime> vehicles,
Map<String, ActiveTask> active,
Map<String, SimulationTaskResult> terminal,
RouteCandidatePlanner routePlanner, RouteQualityEvaluator routeEvaluator,
ResourceLedger ledger, SimulatedPlcGateway plc,
SimulationArchive archive, IdempotentWmsCompletionReceiver wms,
SimulationInvariantSummary invariants,
CorridorReservationManager reservations,
Map<String, ClearanceMove> clearanceMoves,
Map<String, FloorTransferExecution> floorTransfers,
ClearanceMovePlanner clearancePlanner,
List<SimulationTickEvent> events) {
Execution execution = current.execution;
String taskId = execution.task.getSimulationTaskId();
if (!current.phaseCompletionApplied) {
BusinessTaskLeg completedLeg = execution.currentLeg();
reservations.releaseTask(taskId);
PhaseProgress phase = execution.currentPhaseProgress();
if (phase == null || phase.phase != completedLeg.getPhase()) {
throw new IllegalStateException("missing active phase evidence for " + taskId);
}
String completionReason = phaseCompletionReason(completedLeg.getPhase());
phase.complete(now, completionReason);
execution.cargoState = completedLeg.getCargoStateOnArrival();
current.phaseCompletionApplied = true;
events.add(event(tick, now, completionReason, taskId,
vehicle.getVehicleId(), "ACK", phase.fromSite, phase.toSite));
// 货物送达业务目标是唯一归档和 WMS 回调点;后续空车回前端不会重复业务副作用。
if (completedLeg.completesBusiness()
&& execution.businessCompletedMillis == null) {
if (!archive.complete(taskId, vehicle.getVehicleId(), now)) {
invariants.duplicateArchiveAttempt();
}
boolean first = wms.receive(taskId);
boolean duplicate = wms.receive(taskId);
if (!first || duplicate) invariants.duplicateWmsBusinessEffect();
execution.businessCompletedMillis = now;
events.add(event(tick, now, "BUSINESS_ARCHIVED", taskId,
vehicle.getVehicleId(), "CARGO_DEPOSITED",
vehicle.getCurrentSite(), vehicle.getCurrentSite()));
events.add(event(tick, now, "WMS_CALLBACK", taskId,
vehicle.getVehicleId(), "IDEMPOTENT_EFFECT=1;DELIVERIES=2",
vehicle.getCurrentSite(), vehicle.getCurrentSite()));
}
if (completedLeg.releasesVehicle()) {
execution.vehicleReleasedMillis = now;
execution.vehicleReleaseStatus = VehicleReleaseStatus.COMPLETED;
terminal.put(taskId, result(execution,
SimulationTaskStatus.COMPLETED, "COMPLETED", plc, now));
active.remove(vehicle.getVehicleId());
events.add(event(tick, now, "VEHICLE_RELEASED", taskId,
vehicle.getVehicleId(), "BUSINESS_PHASES_COMPLETED",
vehicle.getCurrentSite(), vehicle.getCurrentSite()));
events.add(event(tick, now, "COMPLETE", taskId,
vehicle.getVehicleId(), "ARCHIVED_WMS_ACK",
vehicle.getCurrentSite(), vehicle.getCurrentSite()));
return;
}
execution.phaseIndex++;
}
// 阶段切换不复用上一段路径,从车辆已确认的实际站点重新执行候选规划和走廊预约。
activateNextBusinessPhase(tick, now, current, vehicle, input, config, queue,
vehicles, active, terminal, routePlanner, routeEvaluator,
ledger, plc, reservations, clearanceMoves, floorTransfers,
clearancePlanner, events);
}
/** 从车辆当前位置规划父任务下一业务阶段,并保持同一车辆绑定。 */
private boolean activateNextBusinessPhase(
long tick, long now, ActiveTask completedRoute,
SimulatedVehicleRuntime vehicle, HistoricalSimulationInput input,
SimulationConfig config, SimulationTaskQueue queue,
Map<String, SimulatedVehicleRuntime> vehicles,
Map<String, ActiveTask> active,
Map<String, SimulationTaskResult> terminal,
RouteCandidatePlanner routePlanner, RouteQualityEvaluator routeEvaluator,
ResourceLedger ledger, SimulatedPlcGateway plc,
CorridorReservationManager reservations,
Map<String, ClearanceMove> clearanceMoves,
Map<String, FloorTransferExecution> floorTransfers,
ClearanceMovePlanner clearancePlanner,
List<SimulationTickEvent> events) {
Execution execution = completedRoute.execution;
BusinessTaskLeg leg = execution.currentLeg();
String taskId = execution.task.getSimulationTaskId();
String fromSite = vehicle.getCurrentSite();
// 先建立阶段槽位,预约或终点承诺产生的 HOLD 才能准确归入即将执行的阶段。
PhaseProgress phase = execution.beginPhase(leg, fromSite,
Collections.singletonList(fromSite), now);
RouteCandidateSet candidates = routePlanner.plan(input.getGridMap(),
fromSite, leg.getTargetSite(), Collections.<String>emptySet(),
RoutePlanningPolicy.defaultPolicy());
if (candidates.getCandidates().isEmpty()) {
String reason = "NO_ROUTE_" + leg.getPhase();
phase.complete(now, reason);
reservations.releaseTask(taskId);
active.remove(vehicle.getVehicleId());
execution.vehicleReleaseStatus = execution.businessCompletedMillis == null
? VehicleReleaseStatus.PENDING : VehicleReleaseStatus.FAILED;
terminal.put(taskId, result(execution,
SimulationTaskStatus.FAILED, reason, plc, now));
events.add(event(tick, now, "PHASE_FAILED", taskId,
vehicle.getVehicleId(), reason, fromSite, leg.getTargetSite()));
return true;
}
TrafficRouteSnapshot traffic = trafficSnapshot(vehicles, active, ledger, reservations);
RoutePlanDecision decision = routeEvaluator.select(input.getGridMap(), candidates,
traffic, taskId, vehicle.getVehicleId());
List<String> route = new ArrayList<String>(decision.getSelectedPath());
String destinationHold = destinationCommitmentReason(
vehicle.getVehicleId(), leg.getTargetSite(), vehicles,
active, floorTransfers, input.getGridMap());
if (destinationHold != null) {
if ("DESTINATION_OCCUPIED".equals(destinationHold)) {
planDestinationClearance(tick, now, taskId, vehicle.getVehicleId(),
route, vehicles, active, clearanceMoves, floorTransfers,
clearancePlanner, ledger, input.getGridMap(), events);
}
hold(execution, destinationHold);
events.add(event(tick, now, "PHASE_HOLD", taskId,
vehicle.getVehicleId(), destinationHold,
fromSite, leg.getTargetSite()));
return false;
}
CorridorReservationManager.ReservationDecision reservation = reservations.reserve(
taskId, route, queuePriority(execution, now, config),
execution.arrivalMillis);
if (!reservation.isGranted()) {
hold(execution, reservation.getReason());
events.add(event(tick, now, "PHASE_HOLD", taskId,
vehicle.getVehicleId(), reservation.getReason(),
fromSite, leg.getTargetSite()));
return false;
}
execution.pathVersion++;
execution.planningEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.SERVICE,
execution.pathVersion, now, false, decision));
phase.plannedPath = new ArrayList<String>(route);
if (leg.getPhase() == BusinessTaskPhase.LOADED_RETURN_TO_SOURCE_FRONT
|| leg.getPhase() == BusinessTaskPhase.LOADED_TO_PLACE) {
appendBusinessPath(execution.plannedPath, route);
}
active.put(vehicle.getVehicleId(), new ActiveTask(execution, route, 0,
leg.getPhase(), execution.pathVersion));
events.add(event(tick, now, "PHASE_ASSIGN", taskId,
vehicle.getVehicleId(), leg.getPhase().name(),
fromSite, leg.getTargetSite()));
return true;
}
/** 把阶段路径拼成兼容旧报告的总路线,同时保留 TaskPhaseEvidence 中的边界。 */
private void appendBusinessPath(List<String> completePath, List<String> phasePath) {
if (phasePath.isEmpty()) return;
int start = !completePath.isEmpty()
&& completePath.get(completePath.size() - 1).equals(phasePath.get(0)) ? 1 : 0;
for (int index = start; index < phasePath.size(); index++) {
completePath.add(phasePath.get(index));
}
}
/** 将移动阶段转换成用户可识别的业务事实事件。 */
private String phaseCompletionReason(BusinessTaskPhase phase) {
if (phase == BusinessTaskPhase.EMPTY_TO_PICKUP) return "PICKUP_CONFIRMED";
if (phase == BusinessTaskPhase.LOADED_RETURN_TO_SOURCE_FRONT) {
return "SOURCE_FRONT_REACHED";
}
if (phase == BusinessTaskPhase.LOADED_TO_PLACE) return "CARGO_DEPOSITED";
if (phase == BusinessTaskPhase.EMPTY_RETURN_TO_TARGET_FRONT) {
return "TARGET_FRONT_REACHED";
}
return "PHASE_COMPLETED";
}
/**
* 逐段执行为主任务让路的清障移动,并施加与正常任务相同的方向、交通、资源和 PLC 约束。
*
@@ -1946,7 +2198,10 @@ public final class HistoricalDispatchSimulator {
plc.attemptsFor(execution.task.getSimulationTaskId()),
execution.retryCount, execution.holdReasons, execution.retryReasons,
execution.planningEvidence, execution.rerouteCount,
execution.assignmentMode, execution.floorTransferJobs);
execution.assignmentMode, execution.floorTransferJobs,
execution.phaseEvidence(), execution.cargoState,
execution.businessCompletedMillis, execution.vehicleReleasedMillis,
execution.vehicleReleaseStatus, execution.skippedStageReasons());
}
/**
@@ -1976,6 +2231,8 @@ public final class HistoricalDispatchSimulator {
private static void hold(Execution execution, String reason) {
execution.holdCount++;
increment(execution.holdReasons, reason);
PhaseProgress phase = execution.currentPhaseProgress();
if (phase != null) phase.holdCount++;
}
/**
@@ -1986,6 +2243,8 @@ public final class HistoricalDispatchSimulator {
private static void retry(Execution execution, String reason) {
execution.retryCount++;
increment(execution.retryReasons, reason);
PhaseProgress phase = execution.currentPhaseProgress();
if (phase != null) phase.retryCount++;
}
/**
@@ -2020,6 +2279,15 @@ public final class HistoricalDispatchSimulator {
private static final class Execution {
private final HistoricalSimulationTask task;
private final long arrivalMillis;
private final SimulationExecutionMode executionMode;
private final BusinessPhasePlan phasePlan;
private int phaseIndex;
private CargoState cargoState = CargoState.EMPTY;
private Long businessCompletedMillis;
private Long vehicleReleasedMillis;
private VehicleReleaseStatus vehicleReleaseStatus;
private final List<PhaseProgress> phaseProgress =
new ArrayList<PhaseProgress>();
private Long firstAssignedMillis;
private String lastVehicleId;
private List<String> plannedPath = new ArrayList<String>();
@@ -2045,9 +2313,90 @@ public final class HistoricalDispatchSimulator {
* @param task 执行该处理所需的 task 输入。
* @param arrivalMillis 执行该处理所需的 arrival millis 输入。
*/
private Execution(HistoricalSimulationTask task, long arrivalMillis) {
private Execution(HistoricalSimulationTask task, long arrivalMillis,
SimulationExecutionMode executionMode) {
this.task = task;
this.arrivalMillis = arrivalMillis;
this.executionMode = executionMode;
if (executionMode == SimulationExecutionMode.SOURCE_ALIGNED_BUSINESS) {
this.phasePlan = new BusinessPhasePlanner().plan(task);
this.vehicleReleaseStatus = VehicleReleaseStatus.PENDING;
} else {
this.phasePlan = null;
this.vehicleReleaseStatus = VehicleReleaseStatus.NOT_REQUIRED;
}
}
private boolean isBusinessMode() {
return executionMode == SimulationExecutionMode.SOURCE_ALIGNED_BUSINESS;
}
private BusinessTaskLeg currentLeg() {
return phasePlan.getLegs().get(phaseIndex);
}
private PhaseProgress beginPhase(BusinessTaskLeg leg, String fromSite,
List<String> path, long now) {
PhaseProgress existing = currentPhaseProgress();
if (existing != null && existing.phase == leg.getPhase()) {
existing.plannedPath = new ArrayList<String>(path);
return existing;
}
PhaseProgress created = new PhaseProgress(
leg.getPhase(), fromSite, leg.getTargetSite(), path, now);
phaseProgress.add(created);
return created;
}
private PhaseProgress currentPhaseProgress() {
if (phaseProgress.isEmpty()) return null;
PhaseProgress current = phaseProgress.get(phaseProgress.size() - 1);
return current.completedMillis == null ? current : null;
}
private List<TaskPhaseEvidence> phaseEvidence() {
List<TaskPhaseEvidence> evidence = new ArrayList<TaskPhaseEvidence>();
for (PhaseProgress phase : phaseProgress) evidence.add(phase.freeze());
return evidence;
}
private List<String> skippedStageReasons() {
return phasePlan == null ? Collections.<String>emptyList()
: phasePlan.getSkippedStageReasons();
}
}
/** 模拟器内部的可变阶段记录,只在生成结果时冻结为 TaskPhaseEvidence。 */
private static final class PhaseProgress {
private final BusinessTaskPhase phase;
private final String fromSite;
private final String toSite;
private List<String> plannedPath;
private final long startedMillis;
private Long completedMillis;
private int holdCount;
private int retryCount;
private String completionReason;
private PhaseProgress(BusinessTaskPhase phase, String fromSite, String toSite,
List<String> plannedPath, long startedMillis) {
this.phase = phase;
this.fromSite = fromSite;
this.toSite = toSite;
this.plannedPath = new ArrayList<String>(plannedPath);
this.startedMillis = startedMillis;
}
private void complete(long now, String reason) {
if (completedMillis != null) return;
completedMillis = now;
completionReason = reason;
}
private TaskPhaseEvidence freeze() {
return new TaskPhaseEvidence(phase, fromSite, toSite, plannedPath,
startedMillis, completedMillis, holdCount, retryCount,
completionReason);
}
}
@@ -2093,6 +2442,8 @@ public final class HistoricalDispatchSimulator {
private static final class ActiveTask {
private final Execution execution;
private final BusinessTaskPhase phase;
private final int routeVersion;
private List<String> route;
private int index;
private int serviceStartIndex;
@@ -2100,6 +2451,7 @@ public final class HistoricalDispatchSimulator {
private final TrafficHoldTracker trafficHolds =
new TrafficHoldTracker(TRAFFIC_HOLD_REROUTE_THRESHOLD);
private boolean movedSinceReroute = true;
private boolean phaseCompletionApplied;
/**
* 创建 {@code ActiveTask} 实例并初始化其内部状态。
@@ -2110,12 +2462,26 @@ public final class HistoricalDispatchSimulator {
* @param serviceStartIndex 服务段源点在完整执行路线中的下标。
*/
private ActiveTask(Execution execution, List<String> route, int serviceStartIndex) {
this(execution, route, serviceStartIndex, null,
execution.pathVersion);
}
/** 创建一段绑定父任务和业务阶段的活动路线,供 PLC 命令生成稳定幂等键。 */
private ActiveTask(Execution execution, List<String> route, int serviceStartIndex,
BusinessTaskPhase phase, int routeVersion) {
this.execution = execution;
this.phase = phase;
this.routeVersion = routeVersion;
this.route = Collections.unmodifiableList(new ArrayList<String>(route));
this.serviceStartIndex = serviceStartIndex;
if (serviceStartIndex == 0) traversedServicePath.add(route.get(0));
}
private String commandId() {
return execution.task.getSimulationTaskId() + ":" + phase
+ ":v" + routeVersion + ":e" + index;
}
/** 返回车辆是否已经到达业务源点并开始执行服务段。 */
private boolean hasReachedServiceSource() {
return index >= serviceStartIndex || !traversedServicePath.isEmpty();

View File

@@ -28,6 +28,12 @@ public final class SimulationTaskResult {
private final int rerouteCount;
private final AssignmentMode assignmentMode;
private final List<FloorTransferJob> floorTransferJobs;
private final List<TaskPhaseEvidence> phaseEvidence;
private final CargoState cargoState;
private final Long businessCompletedMillis;
private final Long vehicleReleasedMillis;
private final VehicleReleaseStatus vehicleReleaseStatus;
private final List<String> skippedStageReasons;
/**
* 创建 {@code SimulationTaskResult} 实例并初始化其内部状态。
@@ -64,6 +70,33 @@ public final class SimulationTaskResult {
List<RoutePlanningEvidence> planningEvidence,
int rerouteCount, AssignmentMode assignmentMode,
List<FloorTransferJob> floorTransferJobs) {
this(simulationTaskId, record, status, reason, simulatedVehicleId,
plannedPath, assignedVehicles, arrivalMillis, firstAssignedMillis,
completedMillis, holdCount, protocolAttempts, retryCount,
holdReasons, retryReasons, planningEvidence, rerouteCount,
assignmentMode, floorTransferJobs,
Collections.<TaskPhaseEvidence>emptyList(), CargoState.EMPTY,
null, null, VehicleReleaseStatus.NOT_REQUIRED,
Collections.<String>emptyList());
}
/** 创建同时表达业务完成、车辆释放和逐阶段路径证据的完整结果。 */
SimulationTaskResult(String simulationTaskId, HistoricalQueueRecord record,
SimulationTaskStatus status, String reason,
String simulatedVehicleId, List<String> plannedPath,
List<String> assignedVehicles, long arrivalMillis,
Long firstAssignedMillis, Long completedMillis,
int holdCount, int protocolAttempts, int retryCount,
Map<String, Integer> holdReasons,
Map<String, Integer> retryReasons,
List<RoutePlanningEvidence> planningEvidence,
int rerouteCount, AssignmentMode assignmentMode,
List<FloorTransferJob> floorTransferJobs,
List<TaskPhaseEvidence> phaseEvidence,
CargoState cargoState, Long businessCompletedMillis,
Long vehicleReleasedMillis,
VehicleReleaseStatus vehicleReleaseStatus,
List<String> skippedStageReasons) {
this.simulationTaskId = simulationTaskId;
this.record = record;
this.status = status;
@@ -83,6 +116,12 @@ public final class SimulationTaskResult {
this.rerouteCount = rerouteCount;
this.assignmentMode = assignmentMode;
this.floorTransferJobs = immutable(floorTransferJobs);
this.phaseEvidence = immutable(phaseEvidence);
this.cargoState = cargoState;
this.businessCompletedMillis = businessCompletedMillis;
this.vehicleReleasedMillis = vehicleReleasedMillis;
this.vehicleReleaseStatus = vehicleReleaseStatus;
this.skippedStageReasons = immutable(skippedStageReasons);
}
/**
@@ -205,6 +244,24 @@ public final class SimulationTaskResult {
/** 返回该父任务产生的换层业务效果数;重复提交不会增加该计数。 */
public int getFloorTransferCount() { return floorTransferJobs.size(); }
/** 返回按真实执行顺序冻结的每阶段路线、时间、HOLD 和重试证据。 */
public List<TaskPhaseEvidence> getPhaseEvidence() { return phaseEvidence; }
/** 返回父任务货物的最终状态;业务模式完成时应为 {@code DEPOSITED}。 */
public CargoState getCargoState() { return cargoState; }
/** 返回货物入位并完成归档/WMS 回调的仿真时间。 */
public Long getBusinessCompletedMillis() { return businessCompletedMillis; }
/** 返回同一车辆完成必要回前端动作并解除占车关系的仿真时间。 */
public Long getVehicleReleasedMillis() { return vehicleReleasedMillis; }
/** 返回业务完成后的车辆清理动作是否仍待执行、已完成或失败。 */
public VehicleReleaseStatus getVehicleReleaseStatus() { return vehicleReleaseStatus; }
/** 返回未生成通道阶段的明确原因,不允许前端自行猜测货位分组。 */
public List<String> getSkippedStageReasons() { return skippedStageReasons; }
/**
* 返回当前对象保存的“waiting millis”信息。
*

View File

@@ -0,0 +1,59 @@
package com.ft546.wcs.shadow.history.simulation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 一次业务移动阶段实际采用的路径和执行结果。
*
* <p>每个阶段单独保存起终点、规划路径、时间、HOLD 和 PLC 重试数,页面无需把多段路线猜成一条;
* 未完成阶段允许 {@code completedMillis} 和 {@code completionReason} 为空,供失败/等待快照诊断。
*/
public final class TaskPhaseEvidence {
private final BusinessTaskPhase phase;
private final String fromSite;
private final String toSite;
private final List<String> plannedPath;
private final long startedMillis;
private final Long completedMillis;
private final int holdCount;
private final int retryCount;
private final String completionReason;
public TaskPhaseEvidence(BusinessTaskPhase phase, String fromSite, String toSite,
List<String> plannedPath, long startedMillis,
Long completedMillis, int holdCount, int retryCount,
String completionReason) {
if (phase == null || blank(fromSite) || blank(toSite) || plannedPath == null
|| plannedPath.isEmpty() || startedMillis < 0L
|| (completedMillis != null && completedMillis < startedMillis)
|| holdCount < 0 || retryCount < 0) {
throw new IllegalArgumentException("valid task phase evidence is required");
}
this.phase = phase;
this.fromSite = fromSite;
this.toSite = toSite;
this.plannedPath = Collections.unmodifiableList(
new ArrayList<String>(plannedPath));
this.startedMillis = startedMillis;
this.completedMillis = completedMillis;
this.holdCount = holdCount;
this.retryCount = retryCount;
this.completionReason = completionReason;
}
public BusinessTaskPhase getPhase() { return phase; }
public String getFromSite() { return fromSite; }
public String getToSite() { return toSite; }
public List<String> getPlannedPath() { return plannedPath; }
public long getStartedMillis() { return startedMillis; }
public Long getCompletedMillis() { return completedMillis; }
public int getHoldCount() { return holdCount; }
public int getRetryCount() { return retryCount; }
public String getCompletionReason() { return completionReason; }
private static boolean blank(String value) {
return value == null || value.trim().isEmpty();
}
}

View File

@@ -0,0 +1,134 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.demo.model.Site;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import com.ft546.wcs.demo.traffic.GridMap;
import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
import com.ft546.wcs.shadow.map.EffectiveTrafficMap;
import com.ft546.wcs.shadow.map.TrafficMapCompiler;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Collectors;
import static com.ft546.wcs.shadow.history.simulation.BusinessTaskPhase.EMPTY_RETURN_TO_TARGET_FRONT;
import static com.ft546.wcs.shadow.history.simulation.BusinessTaskPhase.EMPTY_TO_PICKUP;
import static com.ft546.wcs.shadow.history.simulation.BusinessTaskPhase.LOADED_RETURN_TO_SOURCE_FRONT;
import static com.ft546.wcs.shadow.history.simulation.BusinessTaskPhase.LOADED_TO_PLACE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HistoricalDispatchSimulatorTwoStageTest {
/** 入库在货物送入货位时归档和回调,随后同一车辆空车回前端才释放。 */
@Test
void inboundCallbacksAtDepositThenReturnsTheSameVehicleEmptyToFront() {
HistoricalDispatchSimulationResult result = run(task(
DispatchTask.TaskType.INBOUND, "PICK", "BIN", null, "FRONT"));
SimulationTaskResult task = result.getTaskResults().get(0);
assertEquals(Arrays.asList(EMPTY_TO_PICKUP, LOADED_TO_PLACE,
EMPTY_RETURN_TO_TARGET_FRONT), phases(task));
assertEquals(Arrays.asList("PICK", "BIN", "FRONT"), targets(task));
assertTrue(task.getBusinessCompletedMillis() < task.getVehicleReleasedMillis());
assertEquals(CargoState.DEPOSITED, task.getCargoState());
assertEquals(VehicleReleaseStatus.COMPLETED, task.getVehicleReleaseStatus());
assertEquals(Collections.singletonList("1"), task.getAssignedVehicles());
assertEquals(1, result.getArchiveCount());
assertEquals(2, result.getWmsDeliveryAttempts());
assertEquals(1, result.getWmsBusinessEffects());
assertTrue(result.getEvents().stream().anyMatch(event ->
"VEHICLE_RELEASED".equals(event.getType())
&& "FRONT".equals(event.getToSite())));
}
/** 出库取货后先载货退出源货道前端,再到业务目标,送达即归档并释放车辆。 */
@Test
void outboundCarriesCargoToSourceFrontBeforeBusinessDestination() {
HistoricalDispatchSimulationResult result = run(task(
DispatchTask.TaskType.OUTBOUND, "BIN", "EXIT", "MID", null));
SimulationTaskResult task = result.getTaskResults().get(0);
assertEquals(Arrays.asList(EMPTY_TO_PICKUP,
LOADED_RETURN_TO_SOURCE_FRONT, LOADED_TO_PLACE), phases(task));
assertEquals(Arrays.asList("BIN", "MID", "EXIT"), targets(task));
assertEquals(task.getBusinessCompletedMillis(), task.getVehicleReleasedMillis());
assertEquals(VehicleReleaseStatus.COMPLETED, task.getVehicleReleaseStatus());
assertEquals(1, result.getArchiveCount());
assertEquals(1, result.getWmsBusinessEffects());
}
private HistoricalDispatchSimulationResult run(HistoricalSimulationTask task) {
GridMap grid = grid();
HistoricalSimulationInput input = new HistoricalSimulationInput(
Collections.singletonList(task), Collections.emptyList(),
grid, traffic(), Collections.singletonList(
new VehicleSnapshot("1", 1, "V", 5, 1,
100, true, true, false)),
new FloorTransferTopology(Collections.<Integer, String>emptyMap()),
SimulationExecutionMode.SOURCE_ALIGNED_BUSINESS,
new HistoricalInputAudit(1, 1, 0, 1, 0),
ChannelFrontTopology.from(Collections.emptyList()));
return new HistoricalDispatchSimulator().simulate(input,
new SimulationConfig(100, 30_000L, 3, 100, 100),
DeterministicFaultPlan.none());
}
private HistoricalSimulationTask task(DispatchTask.TaskType type,
String source, String target,
String sourceFront, String targetFront) {
LocalDateTime created = LocalDateTime.of(2026, 8, 10, 8, 0);
HistoricalQueueRecord record = new HistoricalQueueRecord(
"GUID", "BUSINESS-1", type.name(), 2,
source, target, "HISTORY_ONLY", "2026-08-10 08:00:00",
"2026-08-10 08:01:00", "HISTORY-VEHICLE");
return new HistoricalSimulationTask("BUSINESS-1", record, type, 2,
created, created.plusMinutes(1), source, target,
Collections.singletonList("HISTORY_ONLY"), 0,
sourceFront, targetFront,
Collections.<HistoricalTaskSnapshotEvidence>emptyList());
}
private GridMap grid() {
GridMap grid = new GridMap();
for (String id : Arrays.asList("V", "PICK", "MID", "BIN", "FRONT", "EXIT")) {
grid.addSite(new Site(id, 1, id.hashCode(), 1, "TEST"));
}
grid.connect("V", "PICK");
grid.connect("PICK", "MID");
grid.connect("MID", "BIN");
grid.connect("MID", "FRONT");
grid.connect("FRONT", "EXIT");
return grid;
}
private EffectiveTrafficMap traffic() {
LinkedHashSet<String> nodes = new LinkedHashSet<String>(
Arrays.asList("V", "PICK", "MID", "BIN", "FRONT", "EXIT"));
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
edges.add(new TrafficMapCompiler.EdgeDefinition("V", "PICK"));
edges.add(new TrafficMapCompiler.EdgeDefinition("PICK", "MID"));
edges.add(new TrafficMapCompiler.EdgeDefinition("MID", "BIN"));
edges.add(new TrafficMapCompiler.EdgeDefinition("MID", "FRONT"));
edges.add(new TrafficMapCompiler.EdgeDefinition("FRONT", "EXIT"));
return new TrafficMapCompiler().compile(new TrafficMapCompiler.MapDefinition(
"TWO-STAGE", 1L, "TEST", true, "test", "test",
nodes, edges, Collections.emptyMap(),
Collections.emptySet(), Collections.emptySet()));
}
private List<BusinessTaskPhase> phases(SimulationTaskResult task) {
return task.getPhaseEvidence().stream().map(TaskPhaseEvidence::getPhase)
.collect(Collectors.toList());
}
private List<String> targets(SimulationTaskResult task) {
return task.getPhaseEvidence().stream().map(TaskPhaseEvidence::getToSite)
.collect(Collectors.toList());
}
}