docs(javaTest): design floor transfer orchestration

This commit is contained in:
zgw
2026-08-07 09:50:29 +08:00
parent 2e7ef72f38
commit 97e60087b0
2 changed files with 326 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
# javaTest Floor Transfer Dispatch Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让中心调度把跨层车辆作为可行候选,幂等创建并等待换层作业,完成后从实际出口重新规划并执行父业务任务。
**Architecture:** 提升机保持在普通 `GridMap` 之外。`FloorTransferGateway` 是中心调度与现有 WCS 的出站端口,`SimulatedFloorTransferGateway` 提供确定性离线实现;`HistoricalDispatchSimulator` 保存换层中的父任务和车辆锁,完成事实后再创建普通 `ActiveTask`。
**Tech Stack:** Java 8, JUnit 5, Maven Surefire, Java `HttpServer`, vanilla HTML/CSS/JavaScript.
## Global Constraints
- 历史 `agv_id` 与 `path_info` 只用于对照,不参与强制选车和路径结果。
- 提升机不能编译成 `GridMap` 普通边。
- 同一车辆不能同时绑定换层作业、业务任务或清障任务。
- 相同幂等键只能创建一个换层作业;重复完成不能重复移动、归档或回调。
- 换层完成后必须从完成事实的实际出口重新规划。
- 现有 240 条同层历史负载结果必须保持 240/0/0 和零安全违规。
---
### Task 1: Floor transfer domain and deterministic gateway
**Files:**
- Create: `javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/AssignmentMode.java`
- Create: `javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/FloorTransferJob.java`
- Create: `javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/FloorTransferGateway.java`
- Create: `javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/SimulatedFloorTransferGateway.java`
- Create: `javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/FloorTransferTopology.java`
- Test: `javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/SimulatedFloorTransferGatewayTest.java`
**Interfaces:**
- Produces: `FloorTransferGateway.submit(FloorTransferJob)`, `advance(String)`, `snapshot(String)` and immutable completion facts.
- Produces: `FloorTransferTopology.plan(vehicleSite, vehicleFloor, targetFloor, targetSite, GridMap, RouteCandidatePlanner)` returning a feasible source/target port pair.
- [ ] **Step 1: Write failing gateway idempotency and lifecycle tests**
```java
FloorTransferGateway.Submission first = gateway.submit(job);
FloorTransferGateway.Submission duplicate = gateway.submit(job);
assertEquals(first.getJob().getIdempotencyKey(), duplicate.getJob().getIdempotencyKey());
assertEquals(1, gateway.createdJobCount());
assertEquals(FloorTransferStatus.COMPLETED, gateway.advance(job.getIdempotencyKey()).getStatus());
```
- [ ] **Step 2: Run the focused test and verify RED**
Run: `mvn -Dtest=SimulatedFloorTransferGatewayTest test`
Expected: compilation failure because the floor-transfer types do not exist.
- [ ] **Step 3: Implement the minimal strong types and in-memory gateway**
The gateway stores jobs by idempotency key, rejects a different active job when its single simulated lift is occupied, advances deterministically from `SUBMITTED` to `RUNNING` to `COMPLETED`, and returns the configured actual exit site. Every public method receives business-oriented Chinese Javadoc and non-obvious state transitions receive inline comments.
- [ ] **Step 4: Run focused tests and verify GREEN**
Run: `mvn -Dtest=SimulatedFloorTransferGatewayTest test`
Expected: all gateway lifecycle, duplicate submit and duplicate completion assertions pass.
### Task 2: Center dispatch orchestration
**Files:**
- Modify: `javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/HistoricalSimulationInput.java`
- Modify: `javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/HistoricalSimulationInputFactory.java`
- Modify: `javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/PointToPointSimulationInputFactory.java`
- Modify: `javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/HistoricalDispatchSimulator.java`
- Modify: `javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/SimulationTaskResult.java`
- Test: `javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/HistoricalDispatchSimulatorTest.java`
**Interfaces:**
- Consumes: Task 1 gateway and topology.
- Produces: task result fields `AssignmentMode getAssignmentMode()`, `int getFloorTransferCount()`, `List<FloorTransferJob> getFloorTransferJobs()`.
- Produces: audit events `FLOOR_TRANSFER_SUBMITTED`, `FLOOR_TRANSFER_RUNNING`, `FLOOR_TRANSFER_HOLD`, `FLOOR_TRANSFER_COMPLETED`, `POST_TRANSFER_REPLAN`.
- [ ] **Step 1: Write failing same-floor and cross-floor orchestration tests**
```java
assertEquals(AssignmentMode.DIRECT, direct.getAssignmentMode());
assertEquals(AssignmentMode.FLOOR_TRANSFER, crossFloor.getAssignmentMode());
assertEquals(1, crossFloor.getFloorTransferCount());
assertTrue(events.stream().anyMatch(e -> "POST_TRANSFER_REPLAN".equals(e.getType())));
```
The cross-floor fixture has no eligible vehicle on the task floor, uses separate floor-local graph components, and configures `LIFT-1/LIFT-2` outside ordinary edges.
- [ ] **Step 2: Run the focused simulator test and verify RED**
Run: `mvn -Dtest=HistoricalDispatchSimulatorTest test`
Expected: compilation failure for missing assignment mode and transfer result fields.
- [ ] **Step 3: Add candidate feasibility without bypassing VehicleAssignmentService**
Build direct candidates from ordinary approach paths. Build cross-floor candidates only when the topology can reach the source port and target port can reach the task source. Pass both sets to the existing `VehicleAssignmentService`, preserving its stable same-floor preference.
- [ ] **Step 4: Add transfer lock and completion orchestration**
On a cross-floor decision, submit one idempotent job, remove the parent from the normal queue, lock the vehicle in `floorTransfers`, and emit submission evidence. Each tick advances the gateway. A submitted job surviving a vehicle fault/restart remains locked; once the vehicle recovers and the gateway reports completion, update the vehicle from the actual exit, atomically occupy the exit node, replan approach/service paths, reserve the route, create `ActiveTask`, and emit completion/replan events.
- [ ] **Step 5: Cover missing topology, lock, duplicate completion and recovery**
Add tests proving no second assignment uses a transfer-locked vehicle, missing ports close as `NO_ROUTE`, duplicate gateway facts have one effect, and disconnect/restart during transfer resumes the original job rather than selecting another vehicle.
- [ ] **Step 6: Run simulator and fault tests and verify GREEN**
Run: `mvn -Dtest=HistoricalDispatchSimulatorTest,HistoricalDispatchFaultMatrixTest,SimulationAdaptersTest test`
Expected: all direct, cross-floor, recovery, resource, archive and WMS assertions pass.
### Task 3: Visualization and report evidence
**Files:**
- Modify: `javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/SimulationVisualizationMapper.java`
- Modify: `javaTest/src/main/resources/visualizer/index.html`
- Modify: `javaTest/src/main/resources/visualizer/assets/app.js`
- Modify: `javaTest/src/main/resources/visualizer/assets/app.css`
- Test: `javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/SimulationVisualizationMapperTest.java`
- Test: `javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/PointToPointSimulationServiceTest.java`
**Interfaces:**
- Consumes: Task 2 task result and event types.
- Produces: JSON task fields `assignmentMode`, `floorTransferCount`, `floorTransferJobs`; event rows retain source/target transfer ports.
- [ ] **Step 1: Write failing mapper assertions**
```java
assertEquals("FLOOR_TRANSFER", taskPayload.get("assignmentMode"));
assertEquals(1, taskPayload.get("floorTransferCount"));
assertFalse(((List<?>) taskPayload.get("floorTransferJobs")).isEmpty());
```
- [ ] **Step 2: Run mapper/service tests and verify RED**
Run: `mvn -Dtest=SimulationVisualizationMapperTest,PointToPointSimulationServiceTest test`
Expected: new JSON fields are absent.
- [ ] **Step 3: Map and render transfer evidence**
Expose the new fields through `SimulationVisualizationMapper`. Add a compact assignment-mode and floor-transfer section to the task inspector. Render transfer events as a cross-floor operation with source and destination floor labels; do not append the lift transition to the normal route polyline.
- [ ] **Step 4: Run mapper/server tests and verify GREEN**
Run: `mvn -Dtest=SimulationVisualizationMapperTest,PointToPointSimulationServiceTest,HistoricalDispatchVisualizerServerTest test`
Expected: API payloads contain the transfer evidence and all HTTP smoke tests pass.
### Task 4: Full regression and audit
**Files:**
- Modify only if a verified failure requires a scoped correction.
**Interfaces:**
- Consumes: all prior tasks.
- Produces: fresh Maven, 240-task replay, browser and independent review evidence.
- [ ] **Step 1: Run the complete Maven suite**
Run: `mvn clean test`
Expected: all discovered tests pass with zero failures, errors and skips.
- [ ] **Step 2: Run the 240-task deterministic replay twice**
Run the existing real-data simulation acceptance test twice and compare `canonicalView`/core evidence. Expected: 240 completed, 0 waiting, 0 failed, no safety violations, no duplicate archive and no duplicate WMS business effect.
- [ ] **Step 3: Start the visualizer and perform browser QA**
Start on an available localhost port. Verify desktop `1440x1000` and mobile `390x844`, run a forced cross-floor fixture/request, inspect transfer events, confirm the normal route line does not draw a fake cross-floor edge, and check the browser console for errors.
- [ ] **Step 4: Request independent code review and resolve findings**
Provide the reviewer this spec, plan, base SHA and final diff. Fix all Critical and Important findings, rerun the affected focused tests and the complete suite, then record final evidence in the implementation commit.

View File

@@ -0,0 +1,158 @@
# javaTest 中心调度换层编排设计
> 状态:已确认
> 确认日期:2026-08-07
> 实施边界:`javaTest` 离线仿真与审计台
> 生产衔接:现有 FT546 WCS 提升机队列和完成事实
## 1. 目标
中心调度不能因为车辆与任务源点不在同一层,就把该车辆当成永久不可达。调度中心需要继续使用现有选车算法比较全部可行车辆;选中跨层车辆后,生成一个独立换层作业,锁定车辆和父任务,等待现有 WCS 完成换层,再根据车辆实际到达的目标层出口重新规划到任务源点的接近路径和业务服务路径。
本次改造只在 `javaTest` 中实现中心编排、适配器契约、确定性模拟和可视化证据。它不重写提升机 PLC 握手,也不修改生产 `TsjThread` 的现场控制流程。
## 2. 现状依据
- 完整历史地图只连接同层相邻网格,不包含跨层普通边。
- `VehicleAssignmentService` 已经能够按同层优先、楼层距离、坐标距离和车辆号稳定选车。
- `HistoricalDispatchSimulator` 当前在调用选车服务前,先要求车辆当前位置能够沿普通地图路径抵达任务源点,因此跨层车辆被提前排除。
- 生产 FT546 已有 `tsj_device_execution_queue`、`TsjThread.createQueue/sendTsj/finishTask` 和提升机 PLC 状态轮询,可以执行实际换层作业。
- 生产入口目前是旧代码内部流程,不适合作为算法内核依赖;`javaTest` 应依赖稳定的领域端口,迁移时再由项目适配器映射到旧队列和回调。
## 3. 方案比较
### 3.1 采用:显式换层作业编排
新增强类型 `FloorTransferJob`、`FloorTransferGateway` 和确定性模拟实现。中心调度负责选车、创建换层作业、锁车、消费完成事实和重新规划;网关负责把标准换层意图交给具体 WCS。
优点:设备作业与地图路径边界清楚;可以测试幂等、忙碌、故障、重启和重复完成;后续替换为生产适配器时不修改选车和交通算法。
### 3.2 不采用:把提升机连接成普通地图边
普通边只有逐节点移动和节点/边容量语义,无法表达提升机任务号、源层/目标层、设备 ACK、门和端口事实,也会让车辆在页面上看起来沿不存在的边瞬移。
### 3.3 不采用:中心调度直接调用 `TsjThread` 私有方法
`TsjThread` 同时承担 PLC 轮询、旧表读写和车辆队列恢复,接口依赖静态状态且幂等边界不足。直接依赖会让离线算法绑定生产线程,并阻碍独立测试。
## 4. 领域模型
### 4.1 分配模式
每条任务结果保存:
- `DIRECT`:选中车辆与任务源点同层,直接规划接近段和服务段。
- `FLOOR_TRANSFER`:选中车辆需要先换层,再执行接近段和服务段。
同一任务发生重派时保存所有实际选择过的车辆和最终生效模式;历史 `agv_id` 仍只作对照。
### 4.2 FloorTransferJob
换层作业至少包含:
```text
idempotencyKey
parentTaskId
vehicleId
sourceFloor
targetFloor
sourceSite
targetSite
priority
```
`idempotencyKey` 由父任务、车辆、源层和目标层稳定生成。相同键重复提交只能返回同一作业,不能创建第二个物理换层意图。
### 4.3 换层状态
```text
SUBMITTED -> RUNNING -> COMPLETED
-> REJECTED
-> UNKNOWN
```
- `SUBMITTED`:网关已接受作业,车辆立即锁定给父任务。
- `RUNNING`:现有 WCS 已开始执行换层。
- `COMPLETED`:完成事实包含车辆实际出口站点和实际目标层。
- `REJECTED`:能够证明作业未开始,中心释放锁并让父任务重新参与选车。
- `UNKNOWN`:发送后无法判断是否执行,必须保留车辆锁和作业,不能盲目重建或改派。
离线模拟首版产生 `SUBMITTED/RUNNING/COMPLETED`,并提供重复提交、重复完成、车辆故障和重启后的对账行为测试。生产拒绝和未知状态保留在端口契约中,不伪造现场语义。
## 5. 换层拓扑
提升机端口不是 `GridMap` 普通边。`HistoricalSimulationInput` 单独携带每层换层端口目录:FT546 默认使用当前历史解析已经采用的 `07050<floor>`。
跨层车辆成为可行候选必须同时满足:
1. 车辆当前位置到本层换层入口存在同层普通路径;
2. 目标层换层出口到任务源点存在同层普通路径;
3. 任务源点到业务终点存在普通服务路径;
4. 换层网关配置支持源层到目标层。
缺少端口或任一永久路径不存在时,该车辆不是候选。全车队都既不能直接到达、也不能通过换层到达时,任务关闭为 `FAILED/NO_ROUTE`。
## 6. 调度数据流
```text
任务按 create_date 到达
-> 规划服务段
-> 收集 DIRECT 与 FLOOR_TRANSFER 可行车辆
-> VehicleAssignmentService 统一选车
-> DIRECT: 预约接近段+服务段并执行
-> FLOOR_TRANSFER: 幂等创建换层作业并锁车
-> 等待现有 WCS 完成事实
-> 用实际出口更新车辆楼层和站点
-> 从实际出口重新规划接近段和服务段
-> 原子申请走廊预约
-> 逐节点执行、释放资源、归档和 WMS 幂等回调
```
换层中的父任务不留在普通等待队列中,避免同一任务重复选车;被锁车辆也不能接收业务任务或清障任务。换层完成但后续走廊暂时冲突时,保持车辆与父任务绑定并记录 HOLD,不重新创建换层作业。
## 7. 资源和故障规则
- 换层作业期间,车辆原节点资源在提交后由模拟网关接管;完成事实到达时原节点释放、实际出口节点占用。
- 实际出口若被其他车辆占用,完成事实不能乐观提交;任务保持等待,页面显示目标出口阻塞。
- 车辆断线、低电量、故障或重启发生在换层前,作业未提交时可以重新选车。
- 作业提交后发生上述事件时,不释放车辆锁、不重建作业;等待恢复和同步后消费原作业状态。
- 重启后的旧完成事件必须按作业键和车辆绑定校验,重复完成只产生一次车辆位置变更和一次后续任务激活。
- 服务段仍由现有节点、边、对向冲突、走廊预约和 PLC ACK 规则控制。
## 8. 输出和可视化
任务结果新增:
- `assignmentMode`
- `floorTransferCount`
- `floorTransferJobs`
审计事件新增:
- `FLOOR_TRANSFER_SUBMITTED`
- `FLOOR_TRANSFER_RUNNING`
- `FLOOR_TRANSFER_HOLD`
- `FLOOR_TRANSFER_COMPLETED`
- `POST_TRANSFER_REPLAN`
事件的 `fromSite/toSite` 表示换层源入口和实际目标出口。前端在地图上把它显示为跨楼层作业,不画成普通路线边;任务检查器显示选车模式、源层、目标层、作业键和状态。楼层选择器提示路径涉及的所有楼层。
## 9. 测试与验收
必须先写失败测试并观察失败,再实现:
1. 同层车辆可用时仍按现有算法选择 `DIRECT`。
2. 只有跨层车辆可行时,生成一个幂等换层作业并最终完成父任务。
3. 换层作业提交期间车辆被锁,不能接收第二个任务或清障任务。
4. 重复提交和重复完成不重复创建作业、不重复移动车辆、不重复归档或回调 WMS。
5. 换层完成后从实际出口重新规划,结果证据不复用提交前估算路径。
6. 换层期间断线、故障和重启不会错误改派;恢复同步后继续原作业。
7. 缺少换层端口且没有直接路径时任务关闭为 `NO_ROUTE`。
8. 可视化 API 和页面能区分 `DIRECT/FLOOR_TRANSFER` 并展示换层事件。
9. 现有 240 条历史任务继续满足 240 完成、0 等待、0 失败,安全不变量、重复归档和重复 WMS 业务效果均为 0。
## 10. 明确边界
- `javaTest` 的换层网关是确定性离线模拟,不连接 SQL Server、PLC 或生产 WMS。
- 本次实现不声明生产旧表已经具备事务幂等,也不把模拟时序当成现场提升机时序证明。
- 生产迁移需另写 FT546 适配器,把 `FloorTransferJob` 映射到提升机执行队列,并补足数据库唯一键、事务完成和启动对账;该适配器不改变本设计的中心调度接口。