diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/PointToPointSimulationInputFactory.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/PointToPointSimulationInputFactory.java new file mode 100644 index 0000000..511fe1b --- /dev/null +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/PointToPointSimulationInputFactory.java @@ -0,0 +1,65 @@ +package com.ft546.wcs.shadow.history.simulation; + +import com.ft546.wcs.demo.model.DispatchTask; +import com.ft546.wcs.shadow.history.HistoricalQueueRecord; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; + +/** + * 将审计台的一次 A 到 B 请求转换为仿真器可执行的单任务输入。 + * + *

该工厂位于 simulation 包中,只负责构建领域输入,不执行选车或路径规划。 + */ +public final class PointToPointSimulationInputFactory { + public static final String TASK_ID = "VISUALIZER-TASK-1"; + private static final LocalDateTime CREATED_AT = LocalDateTime.of(2026, 8, 5, 0, 0); + private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 基于权威地图和初始车队创建唯一的审计任务。 + */ + public HistoricalSimulationInput create( + HistoricalSimulationInput base, String sourceSite, String targetSite, + DispatchTask.TaskType taskType, int priority, String historicalVehicleId, + List historicalPath) { + if (base == null || taskType == null) { + throw new IllegalArgumentException("BAD_REQUEST: base input and task type are required"); + } + if (base.getGridMap().getSite(sourceSite) == null) { + throw new IllegalArgumentException("UNKNOWN_SITE: source " + sourceSite); + } + if (base.getGridMap().getSite(targetSite) == null) { + throw new IllegalArgumentException("UNKNOWN_SITE: target " + targetSite); + } + if (sourceSite.equals(targetSite)) { + throw new IllegalArgumentException("SAME_ENDPOINT: source and target must differ"); + } + + List history = historicalPath == null + ? Collections.emptyList() : historicalPath; + String pathInfo = String.join(",", history); + HistoricalQueueRecord record = new HistoricalQueueRecord( + TASK_ID, TASK_ID, taskTypeCode(taskType), priority, + sourceSite, targetSite, pathInfo, + TIME.format(CREATED_AT), TIME.format(CREATED_AT.plusMinutes(1)), + historicalVehicleId); + HistoricalSimulationTask task = new HistoricalSimulationTask( + TASK_ID, record, taskType, priority, CREATED_AT, + CREATED_AT.plusMinutes(1), sourceSite, targetSite, history, 0); + return new HistoricalSimulationInput( + Collections.singletonList(task), Collections.emptyList(), + base.getGridMap(), base.getTrafficMap(), base.getInitialVehicles()); + } + + private String taskTypeCode(DispatchTask.TaskType taskType) { + switch (taskType) { + case INBOUND: return "1"; + case OUTBOUND: return "2"; + case RELOCATION: return "3"; + default: throw new IllegalArgumentException("INVALID_TASK_TYPE: " + taskType); + } + } +} diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/PointToPointSimulationRequest.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/PointToPointSimulationRequest.java new file mode 100644 index 0000000..33a54c0 --- /dev/null +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/PointToPointSimulationRequest.java @@ -0,0 +1,128 @@ +package com.ft546.wcs.shadow.history.visualizer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ft546.wcs.demo.model.DispatchTask; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 描述审计台发起的一次 A 到 B 单任务仿真请求。 + * + *

请求中的历史车辆和历史路径只用于结果对照;调度中心仍会基于当前车辆快照重新选车, + * 并在完整地图上重新规划路径。 + */ +public final class PointToPointSimulationRequest { + /** 审计台支持的确定性故障场景。 */ + public enum FaultPreset { + NONE, + PLC_FIRST_SEND_FAILURE, + VEHICLE_RESTART, + LOW_BATTERY, + DISCONNECT_RECONNECT + } + + @JsonProperty("sourceSite") + private String sourceSite; + @JsonProperty("targetSite") + private String targetSite; + @JsonProperty("taskType") + private String taskType; + @JsonProperty("priority") + private Integer priority; + @JsonProperty("acceleration") + private Integer acceleration; + @JsonProperty("historicalVehicleId") + private String historicalVehicleId; + @JsonProperty("historicalPath") + private List historicalPath; + @JsonProperty("faultPreset") + private String faultPreset; + + /** 供 Jackson 反序列化请求体使用。 */ + public PointToPointSimulationRequest() { + } + + /** + * 创建并立即校验一个请求,供测试和非 HTTP 调用方使用。 + */ + public static PointToPointSimulationRequest valid( + String sourceSite, String targetSite, String taskType, + int priority, int acceleration, String historicalVehicleId, + List historicalPath, String faultPreset) { + PointToPointSimulationRequest request = new PointToPointSimulationRequest(); + request.sourceSite = sourceSite; + request.targetSite = targetSite; + request.taskType = taskType; + request.priority = priority; + request.acceleration = acceleration; + request.historicalVehicleId = historicalVehicleId; + request.historicalPath = historicalPath; + request.faultPreset = faultPreset; + request.validate(); + return request; + } + + /** + * 校验并规范化来自页面的业务输入,避免非法参数进入调度器。 + */ + public void validate() { + sourceSite = required(sourceSite, "sourceSite"); + targetSite = required(targetSite, "targetSite"); + taskType = required(taskType, "taskType").toUpperCase(); + faultPreset = faultPreset == null || faultPreset.trim().isEmpty() + ? FaultPreset.NONE.name() : faultPreset.trim().toUpperCase(); + if (priority == null || priority < 0 || priority > 9) { + throw new IllegalArgumentException("INVALID_PRIORITY: priority must be between 0 and 9"); + } + if (acceleration == null || (acceleration != 10 && acceleration != 100)) { + throw new IllegalArgumentException("INVALID_ACCELERATION: acceleration must be 10 or 100"); + } + try { + DispatchTask.TaskType.valueOf(taskType); + } catch (IllegalArgumentException exception) { + throw new IllegalArgumentException("INVALID_TASK_TYPE: " + taskType, exception); + } + try { + FaultPreset.valueOf(faultPreset); + } catch (IllegalArgumentException exception) { + throw new IllegalArgumentException("INVALID_FAULT_PRESET: " + faultPreset, exception); + } + historicalVehicleId = trimToNull(historicalVehicleId); + historicalPath = immutablePath(historicalPath); + } + + private static String required(String value, String name) { + String normalized = trimToNull(value); + if (normalized == null) { + throw new IllegalArgumentException("BAD_REQUEST: " + name + " is required"); + } + return normalized; + } + + private static String trimToNull(String value) { + if (value == null) return null; + String normalized = value.trim(); + return normalized.isEmpty() ? null : normalized; + } + + private static List immutablePath(List source) { + if (source == null || source.isEmpty()) return Collections.emptyList(); + List result = new ArrayList(); + for (String node : source) { + String normalized = trimToNull(node); + if (normalized != null) result.add(normalized); + } + return Collections.unmodifiableList(result); + } + + public String getSourceSite() { return sourceSite; } + public String getTargetSite() { return targetSite; } + public DispatchTask.TaskType getTaskType() { return DispatchTask.TaskType.valueOf(taskType); } + public int getPriority() { return priority; } + public int getAcceleration() { return acceleration; } + public String getHistoricalVehicleId() { return historicalVehicleId; } + public List getHistoricalPath() { return historicalPath; } + public FaultPreset getFaultPreset() { return FaultPreset.valueOf(faultPreset); } +} diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/PointToPointSimulationService.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/PointToPointSimulationService.java new file mode 100644 index 0000000..7e46fa4 --- /dev/null +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/PointToPointSimulationService.java @@ -0,0 +1,73 @@ +package com.ft546.wcs.shadow.history.visualizer; + +import com.ft546.wcs.demo.model.VehicleSnapshot; +import com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan; +import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResult; +import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulator; +import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput; +import com.ft546.wcs.shadow.history.simulation.PointToPointSimulationInputFactory; +import com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway; +import com.ft546.wcs.shadow.history.simulation.SimulationConfig; + +import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.CONNECT; +import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.DISCONNECT; +import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.LOW_BATTERY; +import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.RESTART; +import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.RESTORE_BATTERY; +import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.SYNCHRONIZE; + +/** 调用现有中心调度器执行审计台单任务仿真。 */ +public final class PointToPointSimulationService { + private final HistoricalSimulationInput baseInput; + private final PointToPointSimulationInputFactory inputFactory; + private final HistoricalDispatchSimulator simulator; + + public PointToPointSimulationService(HistoricalSimulationInput baseInput) { + if (baseInput == null) { + throw new IllegalArgumentException("base input is required"); + } + this.baseInput = baseInput; + this.inputFactory = new PointToPointSimulationInputFactory(); + this.simulator = new HistoricalDispatchSimulator(); + } + + /** + * 校验页面输入,构造一个任务,并完整执行选车、规划、资源、PLC、移动和归档流程。 + */ + public HistoricalDispatchSimulationResult run(PointToPointSimulationRequest request) { + if (request == null) throw new IllegalArgumentException("BAD_REQUEST: request is required"); + request.validate(); + HistoricalSimulationInput input = inputFactory.create( + baseInput, request.getSourceSite(), request.getTargetSite(), + request.getTaskType(), request.getPriority(), + request.getHistoricalVehicleId(), request.getHistoricalPath()); + SimulationConfig config = new SimulationConfig( + request.getAcceleration(), 30_000L, 3, 500, 20_000); + return simulator.simulate(input, config, faultPlan(request.getFaultPreset())); + } + + private DeterministicFaultPlan faultPlan(PointToPointSimulationRequest.FaultPreset preset) { + if (preset == PointToPointSimulationRequest.FaultPreset.NONE) { + return DeterministicFaultPlan.none(); + } + DeterministicFaultPlan.Builder builder = DeterministicFaultPlan.builder(); + if (preset == PointToPointSimulationRequest.FaultPreset.PLC_FIRST_SEND_FAILURE) { + return builder.plcFailure(PointToPointSimulationInputFactory.TASK_ID, 1, + SimulatedPlcGateway.PlcReason.SEND_FAILURE).build(); + } + for (VehicleSnapshot vehicle : baseInput.getInitialVehicles()) { + String vehicleId = vehicle.getVehicleId(); + if (preset == PointToPointSimulationRequest.FaultPreset.VEHICLE_RESTART) { + builder.vehicleEvent(0, vehicleId, RESTART) + .vehicleEvent(2, vehicleId, SYNCHRONIZE); + } else if (preset == PointToPointSimulationRequest.FaultPreset.LOW_BATTERY) { + builder.vehicleEvent(0, vehicleId, LOW_BATTERY) + .vehicleEvent(2, vehicleId, RESTORE_BATTERY); + } else if (preset == PointToPointSimulationRequest.FaultPreset.DISCONNECT_RECONNECT) { + builder.vehicleEvent(0, vehicleId, DISCONNECT) + .vehicleEvent(2, vehicleId, CONNECT); + } + } + return builder.build(); + } +} diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/PointToPointSimulationServiceTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/PointToPointSimulationServiceTest.java new file mode 100644 index 0000000..8befa3b --- /dev/null +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/PointToPointSimulationServiceTest.java @@ -0,0 +1,109 @@ +package com.ft546.wcs.shadow.history.visualizer; + +import com.ft546.wcs.shadow.history.HistoricalQueueDataset; +import com.ft546.wcs.shadow.history.HistoricalQueueLoader; +import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput; +import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInputFactory; +import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationTask; +import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResult; +import com.ft546.wcs.shadow.history.simulation.SimulationTaskResult; +import com.ft546.wcs.shadow.history.simulation.SimulationTaskStatus; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PointToPointSimulationServiceTest { + private static HistoricalSimulationInput baseInput; + private static HistoricalSimulationTask sampleTask; + + @BeforeAll + static void loadBaseInput() { + HistoricalQueueDataset dataset = new HistoricalQueueLoader().load( + PointToPointSimulationServiceTest.class.getClassLoader(), "shadow/history"); + baseInput = new HistoricalSimulationInputFactory().create(dataset); + sampleTask = baseInput.getTasks().stream() + .filter(task -> !task.getPhysicalSource().equals(task.getPhysicalTarget())) + .findFirst() + .orElseThrow(() -> new AssertionError("a point-to-point sample is required")); + } + + @Test + void runsOneTaskWithTheRealPlannerAndIgnoresHistoricalPath() { + PointToPointSimulationRequest request = PointToPointSimulationRequest.valid( + sampleTask.getPhysicalSource(), sampleTask.getPhysicalTarget(), + "INBOUND", 1, 100, "HISTORY-CAR", + Arrays.asList("WRONG", "REFERENCE"), "NONE"); + + HistoricalDispatchSimulationResult result = service().run(request); + SimulationTaskResult task = result.getTaskResults().get(0); + + assertEquals(1, result.getTaskResults().size()); + assertEquals(SimulationTaskStatus.COMPLETED, task.getStatus()); + assertEquals(sampleTask.getPhysicalSource(), task.getPlannedPath().get(0)); + assertEquals(sampleTask.getPhysicalTarget(), + task.getPlannedPath().get(task.getPlannedPath().size() - 1)); + assertFalse(task.getPlannedPath().equals(Arrays.asList("WRONG", "REFERENCE"))); + assertEquals("HISTORY-CAR", task.getRecord().getAgvId()); + assertEquals(0, result.getInvariants().totalSafetyViolations()); + } + + @Test + void rejectsUnknownAndIdenticalEndpointsBeforeSimulation() { + PointToPointSimulationRequest unknown = PointToPointSimulationRequest.valid( + "UNKNOWN-SITE", sampleTask.getPhysicalTarget(), "INBOUND", 1, + 100, null, Collections.emptyList(), "NONE"); + PointToPointSimulationRequest identical = PointToPointSimulationRequest.valid( + sampleTask.getPhysicalSource(), sampleTask.getPhysicalSource(), "INBOUND", 1, + 100, null, Collections.emptyList(), "NONE"); + + IllegalArgumentException unknownError = assertThrows( + IllegalArgumentException.class, () -> service().run(unknown)); + IllegalArgumentException identicalError = assertThrows( + IllegalArgumentException.class, () -> service().run(identical)); + + assertTrue(unknownError.getMessage().contains("UNKNOWN_SITE")); + assertTrue(identicalError.getMessage().contains("SAME_ENDPOINT")); + } + + @Test + void plcFirstSendFailureProducesRetryEvidenceAndStillCompletes() { + PointToPointSimulationRequest request = PointToPointSimulationRequest.valid( + sampleTask.getPhysicalSource(), sampleTask.getPhysicalTarget(), + "OUTBOUND", 2, 100, null, Collections.emptyList(), + "PLC_FIRST_SEND_FAILURE"); + + HistoricalDispatchSimulationResult result = service().run(request); + + assertEquals(1, result.getCompletedCount()); + assertTrue(result.getRetryCount() > 0); + assertTrue(result.getEvents().stream() + .anyMatch(event -> "PLC_RETRY".equals(event.getType()))); + } + + @Test + void validatesPriorityAccelerationTaskTypeAndFaultPreset() { + assertThrows(IllegalArgumentException.class, () -> PointToPointSimulationRequest.valid( + "A", "B", "INBOUND", 10, 100, null, + Collections.emptyList(), "NONE")); + assertThrows(IllegalArgumentException.class, () -> PointToPointSimulationRequest.valid( + "A", "B", "INBOUND", 1, 20, null, + Collections.emptyList(), "NONE")); + assertThrows(IllegalArgumentException.class, () -> PointToPointSimulationRequest.valid( + "A", "B", "UNKNOWN", 1, 100, null, + Collections.emptyList(), "NONE")); + assertThrows(IllegalArgumentException.class, () -> PointToPointSimulationRequest.valid( + "A", "B", "INBOUND", 1, 100, null, + Collections.emptyList(), "UNKNOWN")); + } + + private PointToPointSimulationService service() { + return new PointToPointSimulationService(baseInput); + } +}