feat(javaTest): model source-aligned task phases

This commit is contained in:
zgw
2026-08-07 14:07:54 +08:00
parent 93ef274b63
commit 48f0cad832
7 changed files with 323 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
package com.ft546.wcs.shadow.history.simulation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** 一条父任务不可变的移动阶段顺序,以及没有生成通道阶段的明确原因。 */
public final class BusinessPhasePlan {
private final List<BusinessTaskLeg> legs;
private final List<String> skippedStageReasons;
public BusinessPhasePlan(List<BusinessTaskLeg> legs) {
this(legs, Collections.<String>emptyList());
}
public BusinessPhasePlan(List<BusinessTaskLeg> legs, List<String> skippedStageReasons) {
if (legs == null || legs.isEmpty() || skippedStageReasons == null) {
throw new IllegalArgumentException("business legs and skipped reasons are required");
}
List<BusinessTaskLeg> copy = new ArrayList<BusinessTaskLeg>(legs);
for (int i = 0; i < copy.size(); i++) {
if (copy.get(i) == null) {
throw new IllegalArgumentException("business leg is required");
}
// 相邻阶段到同一站点不会产生真实移动,通常说明错误地把业务目标当成通道前端。
if (i > 0 && copy.get(i - 1).getTargetSite()
.equals(copy.get(i).getTargetSite())) {
throw new IllegalArgumentException("ZERO_LENGTH_PHASE:"
+ copy.get(i).getPhase());
}
}
this.legs = Collections.unmodifiableList(copy);
this.skippedStageReasons = Collections.unmodifiableList(
new ArrayList<String>(skippedStageReasons));
}
/** 返回中心调度必须逐段规划和执行的业务顺序。 */
public List<BusinessTaskLeg> getLegs() { return legs; }
/** 返回指定阶段;阶段不适用于当前业务类型时明确报错,避免调用方误取其他路段。 */
public BusinessTaskLeg leg(BusinessTaskPhase phase) {
for (BusinessTaskLeg leg : legs) {
if (leg.getPhase() == phase) return leg;
}
throw new IllegalArgumentException("phase is not planned: " + phase);
}
/** 返回因货位不在通道组或端点本身就是前端而跳过的阶段原因。 */
public List<String> getSkippedStageReasons() { return skippedStageReasons; }
}

View File

@@ -0,0 +1,67 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.model.DispatchTask;
import java.util.ArrayList;
import java.util.List;
/** 根据父任务业务类型和权威通道前端,生成需要独立规划的移动阶段。 */
public final class BusinessPhasePlanner {
public BusinessPhasePlan plan(HistoricalSimulationTask task) {
if (task == null || blank(task.getPhysicalSource()) || blank(task.getPhysicalTarget())) {
throw new IllegalArgumentException("business task and endpoints are required");
}
if (task.getPhysicalSource().equals(task.getPhysicalTarget())) {
throw new IllegalArgumentException("business source and target must differ");
}
List<BusinessTaskLeg> legs = new ArrayList<BusinessTaskLeg>();
List<String> skipped = new ArrayList<String>();
// 所有业务都先让中心调度选车,并把空车送到真实取货点;到达 ACK 后货物才转为 LOADED。
legs.add(BusinessTaskLeg.emptyToPickup(task.getPhysicalSource()));
boolean sourceOperation = task.getTaskType() == DispatchTask.TaskType.OUTBOUND
|| task.getTaskType() == DispatchTask.TaskType.RELOCATION;
boolean targetOperation = task.getTaskType() == DispatchTask.TaskType.INBOUND
|| task.getTaskType() == DispatchTask.TaskType.RELOCATION;
boolean returnToTargetFront = targetOperation && task.requiresTargetFrontMovement();
// 出库和移库必须载货退出源货道;没有源前端时保留为普通点对点作业而不猜测站点。
if (sourceOperation) {
if (task.requiresSourceFrontMovement()) {
legs.add(BusinessTaskLeg.loadedReturnToSourceFront(task.getSourceFrontSite()));
} else if (task.getSourceFrontSite() == null) {
skipped.add("SOURCE_NOT_IN_CARGO_GROUP");
} else {
skipped.add("SOURCE_IS_FRONT");
}
}
// 放货是唯一业务完成点;若之后还要空车回目标前端,此时只归档业务而不释放车辆。
legs.add(BusinessTaskLeg.loadedToPlace(
task.getPhysicalTarget(), !returnToTargetFront));
// 入库和移库在放货后自动回目标通道前端,完成后才允许车辆接收下一条任务。
if (targetOperation) {
if (returnToTargetFront) {
legs.add(BusinessTaskLeg.emptyReturnToTargetFront(task.getTargetFrontSite()));
} else if (task.getTargetFrontSite() == null) {
skipped.add("TARGET_NOT_IN_CARGO_GROUP");
} else {
skipped.add("TARGET_IS_FRONT");
}
}
// A/B 两端都没有生产货位分组时,用一个稳定原因替代两个实现细节原因,便于 UI 审计。
if (task.getSourceFrontSite() == null && task.getTargetFrontSite() == null) {
skipped.clear();
skipped.add("NO_CHANNEL_OPERATION");
}
return new BusinessPhasePlan(legs, skipped);
}
private boolean blank(String value) {
return value == null || value.trim().isEmpty();
}
}

View File

@@ -0,0 +1,64 @@
package com.ft546.wcs.shadow.history.simulation;
/**
* 业务父任务中一次需要真实规划和移动的路段。
*
* <p>路段结束语义随目标一起固定,模拟器到达目标并收到 PLC ACK 后,才能据此改变货物状态、
* 归档业务或释放车辆,避免仅凭历史完成状态跳过真实移动。
*/
public final class BusinessTaskLeg {
private final BusinessTaskPhase phase;
private final String targetSite;
private final CargoState cargoStateOnArrival;
private final boolean completesBusiness;
private final boolean releasesVehicle;
private BusinessTaskLeg(BusinessTaskPhase phase, String targetSite,
CargoState cargoStateOnArrival,
boolean completesBusiness, boolean releasesVehicle) {
if (phase == null || cargoStateOnArrival == null || blank(targetSite)) {
throw new IllegalArgumentException(
"phase, target site and arrival cargo state are required");
}
this.phase = phase;
this.targetSite = targetSite.trim();
this.cargoStateOnArrival = cargoStateOnArrival;
this.completesBusiness = completesBusiness;
this.releasesVehicle = releasesVehicle;
}
/** 空车到取货点并完成取货确认,因此下一段开始时货物已经绑定到该车。 */
public static BusinessTaskLeg emptyToPickup(String targetSite) {
return new BusinessTaskLeg(BusinessTaskPhase.EMPTY_TO_PICKUP,
targetSite, CargoState.LOADED, false, false);
}
/** 出库或移库取货后,载货车辆先退出源货道到通道前端。 */
public static BusinessTaskLeg loadedReturnToSourceFront(String targetSite) {
return new BusinessTaskLeg(BusinessTaskPhase.LOADED_RETURN_TO_SOURCE_FRONT,
targetSite, CargoState.LOADED, false, false);
}
/** 载货到业务目标并完成放货;是否立即释放取决于后面是否还有空车回前端。 */
public static BusinessTaskLeg loadedToPlace(String targetSite,
boolean releasesVehicle) {
return new BusinessTaskLeg(BusinessTaskPhase.LOADED_TO_PLACE,
targetSite, CargoState.DEPOSITED, true, releasesVehicle);
}
/** 入库或移库放货后,车辆空车回目标货道前端,此时才解除父任务占车关系。 */
public static BusinessTaskLeg emptyReturnToTargetFront(String targetSite) {
return new BusinessTaskLeg(BusinessTaskPhase.EMPTY_RETURN_TO_TARGET_FRONT,
targetSite, CargoState.DEPOSITED, false, true);
}
public BusinessTaskPhase getPhase() { return phase; }
public String getTargetSite() { return targetSite; }
public CargoState getCargoStateOnArrival() { return cargoStateOnArrival; }
public boolean completesBusiness() { return completesBusiness; }
public boolean releasesVehicle() { return releasesVehicle; }
private static boolean blank(String value) {
return value == null || value.trim().isEmpty();
}
}

View File

@@ -0,0 +1,21 @@
package com.ft546.wcs.shadow.history.simulation;
/**
* 一条业务父任务在中心调度中的阶段名称。
*
* <p>其中移动阶段会进入 {@link BusinessTaskLeg};其余名称用于后续 PLC、换层、归档和故障证据,
* 使报告能区分“正在去取货”“货物已完成”和“车辆已释放”。
*/
public enum BusinessTaskPhase {
WAITING_ASSIGNMENT,
FLOOR_TRANSFER_TO_PICKUP,
EMPTY_TO_PICKUP,
PICKUP_CONFIRMED,
LOADED_RETURN_TO_SOURCE_FRONT,
LOADED_TO_PLACE,
CARGO_DEPOSITED,
EMPTY_RETURN_TO_TARGET_FRONT,
BUSINESS_COMPLETED,
VEHICLE_RELEASED,
FAILED
}

View File

@@ -0,0 +1,8 @@
package com.ft546.wcs.shadow.history.simulation;
/** 记录父任务货物是尚未取走、正在车上,还是已经放到业务目标。 */
public enum CargoState {
EMPTY,
LOADED,
DEPOSITED
}

View File

@@ -0,0 +1,9 @@
package com.ft546.wcs.shadow.history.simulation;
/** 区分业务完成后是否还需要车辆回前端,以及该清理动作的最终结果。 */
public enum VehicleReleaseStatus {
NOT_REQUIRED,
PENDING,
COMPLETED,
FAILED
}