445 lines
22 KiB
Markdown
445 lines
22 KiB
Markdown
# Standard WCS Topology Dispatch Traffic 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:** 运行时只读取编译后的不可变 `EffectiveTopology`;车辆选择和路径规划产生候选,`ResourceLedger` 与 `DeadlockGuard` 在同一调度快照中决定 GRANT/HOLD/YIELD。项目只提供定义和评分权重,不直接持有资源或修改步骤状态。
|
||
|
||
**Tech Stack:** Java 8, JUnit 5, MyBatis, SQL Server, deterministic graph search.
|
||
|
||
## Global Constraints
|
||
|
||
- 依赖 `2026-08-04-standard-wcs-core-foundation.md` 已交付的 `TransportOrder`、`TaskStep`、共享事件和 Repository 边界。
|
||
- 交通区域是版本与协调边界,不默认是容量 1 的资源。
|
||
- 节点和无向边默认容量为 1;容量大于 1 必须显式配置。
|
||
- 地图加载后不可变;运行循环不得临时查询多张地图表或解释 `remark`。
|
||
- 车辆筛选先执行硬条件,再执行可配置评分。
|
||
- 已占资源只有匹配物理事实或恢复裁定才能释放。
|
||
- 等待图新增授权后必须无环。
|
||
- 本计划只生成设备无关的调度决定,不调用 PLC。
|
||
|
||
---
|
||
|
||
### Task 1: Define, validate, publish, and compile versioned warehouse configuration
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/TopologyDefinition.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/TopologyNode.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/TopologyEdge.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/ResourceDefinition.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/EffectiveTopology.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/TopologyCompiler.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/in/ConfigurationUseCase.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/TopologyDefinitionProvider.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/ConfigurationRepository.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/configuration/PublishConfigurationCommand.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/configuration/ConfigurationPublicationResult.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/configuration/PublishedConfigurationView.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/configuration/ConfigurationApplicationService.java`
|
||
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/topology/TopologyCompilerTest.java`
|
||
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/configuration/ConfigurationApplicationServiceTest.java`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `TopologyDefinitionProvider.load(String warehouseCode, long version)`.
|
||
- Produces: `TopologyCompiler.compile(TopologyDefinition): EffectiveTopology`.
|
||
- Produces: `EffectiveTopology.neighbors(String nodeId)`, `edge(String sourceNodeId,String targetNodeId)`, `resource(String resourceKey)`, `containsNode(String nodeId)`, `version()`.
|
||
- Produces: `ConfigurationUseCase.validate(PublishConfigurationCommand)`, `publish(PublishConfigurationCommand): ConfigurationPublicationResult`, `read(String warehouseCode,long version): PublishedConfigurationView`, and `readActive(String warehouseCode): PublishedConfigurationView`.
|
||
- `publish` atomically stores the immutable warehouse, floor, topology, traffic-zone, resource, device, capability, port, charger, and workflow version after all cross-reference checks pass.
|
||
- Rejects publication while an active order references the version being replaced; running orders remain pinned to their original topology/configuration version.
|
||
|
||
- [ ] **Step 1: Write failing topology invariant tests**
|
||
|
||
```java
|
||
@Test
|
||
void canonicalizesUndirectedEdgeResources() {
|
||
EffectiveTopology topology = compiler.compile(definition(
|
||
node("A", 1), node("B", 1), edge("A", "B")));
|
||
assertEquals("EDGE:A:B", topology.edge("A", "B").getResourceKey());
|
||
assertEquals("EDGE:A:B", topology.edge("B", "A").getResourceKey());
|
||
}
|
||
|
||
@Test
|
||
void rejectsDisconnectedLiftPortReference() {
|
||
assertThrows(DomainRuleViolation.class, () ->
|
||
compiler.compile(definitionWithLiftPort("MISSING")));
|
||
}
|
||
|
||
@Test
|
||
void publishesOneImmutableConfigurationVersionAtomically() {
|
||
ConfigurationPublicationResult result = service.publish(validConfiguration(7L));
|
||
assertEquals(7L, result.getPublishedVersion());
|
||
assertTrue(repository.isPublished("FT546", 7L));
|
||
assertEquals(0, repository.partialWriteCount());
|
||
}
|
||
|
||
@Test
|
||
void readsOneInternallyConsistentPublishedVersion() {
|
||
service.publish(validConfiguration(7L));
|
||
PublishedConfigurationView view = service.read("FT546", 7L);
|
||
assertEquals(7L, view.getTopologyVersion());
|
||
assertEquals(7L, view.getDeviceConfigurationVersion());
|
||
assertEquals(7L, view.getWorkflowConfigurationVersion());
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=TopologyCompilerTest,ConfigurationApplicationServiceTest test`
|
||
|
||
- [ ] **Step 3: Implement immutable compilation**
|
||
|
||
```java
|
||
public EffectiveTopology compile(TopologyDefinition definition) {
|
||
requirePublishedVersion(definition);
|
||
Map<String, TopologyNode> nodes = uniqueNodes(definition.getNodes());
|
||
Map<String, TopologyEdge> edges = canonicalEdges(definition.getEdges(), nodes);
|
||
validatePortsAndResources(definition, nodes);
|
||
return new EffectiveTopology(definition.getWarehouseCode(),
|
||
definition.getVersion(), nodes, edges,
|
||
immutableResources(definition, nodes, edges));
|
||
}
|
||
```
|
||
|
||
Reject duplicate IDs, duplicate coordinates on one floor, missing endpoints, nonpositive capacity, unconfirmed traffic zones, and lift/conveyor ports without nodes.
|
||
|
||
`ConfigurationApplicationService.publish` first compiles topology, validates every device floor/capability/port/charger reference and every workflow step type, then calls one optimistic `ConfigurationRepository.publish(command, effectiveTopology)` transaction. It returns stable validation codes and never exposes partially published versions.
|
||
|
||
`read` and `readActive` return immutable DTOs through the inbound use case. Admin configuration pages may call these methods but may not query the core configuration Mapper or tables directly.
|
||
|
||
- [ ] **Step 4: Run GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=TopologyCompilerTest,ConfigurationApplicationServiceTest test`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/configuration ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/in/ConfigurationUseCase.java ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/TopologyDefinitionProvider.java ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/ConfigurationRepository.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/topology ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/configuration
|
||
git commit -m "feat(wcs): publish immutable warehouse configuration"
|
||
```
|
||
|
||
### Task 2: Add device registry and capability matching
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceType.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceCapability.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceDescriptor.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/VehicleSnapshot.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceRegistry.java`
|
||
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/equipment/DeviceCapabilityTest.java`
|
||
|
||
**Interfaces:**
|
||
- Produces: `DeviceRegistry.vehicles(): List<DeviceDescriptor>`.
|
||
- Produces: `DeviceDescriptor.supports(DeviceCapability): boolean`.
|
||
- Produces: `VehicleSnapshot.isDispatchable(int minimumBattery)`.
|
||
|
||
- [ ] **Step 1: Write failing dispatchability tests**
|
||
|
||
```java
|
||
@ParameterizedTest
|
||
@CsvSource({
|
||
"ONLINE,AUTO,NORMAL,IDLE,80,true",
|
||
"OFFLINE,AUTO,NORMAL,IDLE,80,false",
|
||
"ONLINE,MANUAL,NORMAL,IDLE,80,false",
|
||
"ONLINE,AUTO,FAULT,IDLE,80,false",
|
||
"ONLINE,AUTO,NORMAL,BUSY,80,false",
|
||
"ONLINE,AUTO,NORMAL,IDLE,20,false"
|
||
})
|
||
void dispatchabilityUsesOrthogonalStateDimensions(
|
||
String connection, String mode, String health,
|
||
String occupation, int battery, boolean expected) {
|
||
assertEquals(expected, snapshot(connection, mode, health, occupation, battery)
|
||
.isDispatchable(30));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=DeviceCapabilityTest test`
|
||
|
||
- [ ] **Step 3: Implement descriptors and snapshots**
|
||
|
||
Keep connection, mode, health, occupation, location, load, battery, current command, floor, and snapshot time as separate immutable fields. `supports` checks an immutable `EnumSet<DeviceCapability>`.
|
||
|
||
- [ ] **Step 4: Run GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=DeviceCapabilityTest test`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceRegistry.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/equipment
|
||
git commit -m "feat(wcs): add device capabilities and snapshots"
|
||
```
|
||
|
||
### Task 3: Implement deterministic route planning
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/RoutePlan.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/RoutePlanner.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/AStarRoutePlanner.java`
|
||
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic/AStarRoutePlannerTest.java`
|
||
|
||
**Interfaces:**
|
||
- Produces: `RoutePlanner.plan(EffectiveTopology,String source,String destination,Set<String> blockedResources): RoutePlan`.
|
||
- Produces: ordered node IDs, ordered resource keys, distance cost, turn count, and topology version.
|
||
|
||
- [ ] **Step 1: Write failing shortest-safe-route tests**
|
||
|
||
```java
|
||
@Test
|
||
void avoidsBlockedEdgeAndUsesStableNodeIdTieBreak() {
|
||
RoutePlan route = planner.plan(grid(), "A", "D",
|
||
Collections.singleton("EDGE:B:D"));
|
||
assertEquals(Arrays.asList("A", "C", "D"), route.getNodeIds());
|
||
}
|
||
|
||
@Test
|
||
void rejectsUnknownDestination() {
|
||
assertThrows(DomainRuleViolation.class, () ->
|
||
planner.plan(grid(), "A", "UNKNOWN", Collections.<String>emptySet()));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=AStarRoutePlannerTest test`
|
||
|
||
- [ ] **Step 3: Implement A* with deterministic ordering**
|
||
|
||
Use Manhattan distance only when coordinates exist; otherwise use zero heuristic. Sort equal-cost open nodes by total cost, path cost, turn count, then node ID. Never read historical `path_info`.
|
||
|
||
- [ ] **Step 4: Run GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=AStarRoutePlannerTest,TopologyCompilerTest test`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic
|
||
git commit -m "feat(wcs): add deterministic route planning"
|
||
```
|
||
|
||
### Task 4: Implement vehicle assignment policy
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/dispatch/DispatchTask.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/dispatch/VehicleAssignmentDecision.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/dispatch/VehicleAssignmentPolicy.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/dispatch/DefaultVehicleAssignmentPolicy.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DispatchPolicy.java`
|
||
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/dispatch/DefaultVehicleAssignmentPolicyTest.java`
|
||
|
||
**Interfaces:**
|
||
- Produces: `assign(DispatchTask,List<VehicleSnapshot>,EffectiveTopology): VehicleAssignmentDecision`.
|
||
- Produces: selected vehicle ID, route, total score, and ordered reason codes.
|
||
- Consumes: optional `DispatchPolicy.adjustment(DispatchTask,VehicleSnapshot): int` with no side effects.
|
||
|
||
- [ ] **Step 1: Write failing hard-filter and stable-score tests**
|
||
|
||
```java
|
||
@Test
|
||
void rejectsCloserVehicleWithoutRequiredCapability() {
|
||
VehicleAssignmentDecision decision = policy.assign(taskRequiringLoad(),
|
||
Arrays.asList(vehicle("V1", 1, 90, false), vehicle("V2", 2, 70, true)), topology);
|
||
assertEquals("V2", decision.getVehicleId());
|
||
assertTrue(decision.getReasonCodes().contains("CAPABILITY_MATCH"));
|
||
}
|
||
|
||
@Test
|
||
void breaksEqualScoresByVehicleId() {
|
||
assertEquals("V1", policy.assign(task(), Arrays.asList(vehicle("V2"), vehicle("V1")), topology)
|
||
.getVehicleId());
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=DefaultVehicleAssignmentPolicyTest test`
|
||
|
||
- [ ] **Step 3: Implement filtering and scoring**
|
||
|
||
Apply online/auto/healthy/idle/battery/capability/reachability filters before scoring. Score business priority, waiting age, same floor, path cost, battery margin, current workload, policy adjustment, then stable vehicle ID.
|
||
|
||
- [ ] **Step 4: Run GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest='*VehicleAssignment*Test,*RoutePlannerTest' test`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/dispatch ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DispatchPolicy.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/dispatch
|
||
git commit -m "feat(wcs): add deterministic vehicle assignment"
|
||
```
|
||
|
||
### Task 5: Implement the resource ledger and reservation lifecycle
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/ReservationState.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/ResourceReservation.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/ReservationRequest.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/ReservationDecision.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/ResourceLedger.java`
|
||
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic/ResourceLedgerTest.java`
|
||
|
||
**Interfaces:**
|
||
- Produces: `reserve(ReservationRequest): ReservationDecision`.
|
||
- Produces: `occupy(String reservationId,long fencingToken)`.
|
||
- Produces: `release(String reservationId,String releaseEvidenceId,long fencingToken)`.
|
||
- Produces: `abortUnoccupied(String reservationId,String reason)`.
|
||
- `ReservationState` values are `REQUESTED`, `GRANTED`, `OCCUPIED`, `RELEASED`, `REJECTED`, `EXPIRED`, and `ABORTED`.
|
||
|
||
- [ ] **Step 1: Write failing capacity and UNKNOWN tests**
|
||
|
||
```java
|
||
@Test
|
||
void grantsOnlyOneReservationForCapacityOneResource() {
|
||
assertTrue(ledger.reserve(request("R1", "NODE:A")).isGranted());
|
||
assertFalse(ledger.reserve(request("R2", "NODE:A")).isGranted());
|
||
}
|
||
|
||
@Test
|
||
void occupiedReservationCannotExpireByTime() {
|
||
ResourceReservation reservation = occupiedReservation();
|
||
assertThrows(DomainRuleViolation.class, reservation::expire);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=ResourceLedgerTest test`
|
||
|
||
- [ ] **Step 3: Implement atomic in-memory ledger semantics**
|
||
|
||
Sort requested resource keys lexicographically, verify all capacities before changing any count, assign one fencing token to the decision, and roll back the whole request on any conflict. Permit only `REQUESTED -> GRANTED/REJECTED/EXPIRED/ABORTED`, `GRANTED -> OCCUPIED/EXPIRED/ABORTED`, and `OCCUPIED -> RELEASED`. `EXPIRED` is legal only before physical occupation; `OCCUPIED` can be released only with matching evidence/fencing token and must remain protected when a linked command is `UNKNOWN` or a RecoveryCase is unresolved.
|
||
|
||
- [ ] **Step 4: Run GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=ResourceLedgerTest test`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic/ResourceLedgerTest.java
|
||
git commit -m "feat(wcs): add resource reservation ledger"
|
||
```
|
||
|
||
### Task 6: Add deadlock prevention, HOLD, and YIELD decisions
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/WaitForGraph.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/TrafficDecisionType.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/TrafficDecision.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/DeadlockGuard.java`
|
||
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic/DeadlockGuardTest.java`
|
||
|
||
**Interfaces:**
|
||
- Produces: `wouldCreateCycle(String waiter,Set<String> holders): boolean`.
|
||
- Produces: `decide(candidate,ledger,topology): TrafficDecision` with `GRANT/HOLD/YIELD` and reason codes.
|
||
|
||
- [ ] **Step 1: Write failing cycle and safe-yield tests**
|
||
|
||
```java
|
||
@Test
|
||
void rejectsGrantThatClosesAWaitCycle() {
|
||
graph.addWait("V1", "V2");
|
||
graph.addWait("V2", "V3");
|
||
assertTrue(graph.wouldCreateCycle("V3", Collections.singleton("V1")));
|
||
}
|
||
|
||
@Test
|
||
void doesNotInventYieldPointWhenTopologyHasNone() {
|
||
TrafficDecision decision = guard.decide(blockedCandidate(), ledger, topologyWithoutSafeWait());
|
||
assertEquals(TrafficDecisionType.HOLD, decision.getType());
|
||
assertEquals("NO_SAFE_YIELD_POINT", decision.getReasonCode());
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=DeadlockGuardTest test`
|
||
|
||
- [ ] **Step 3: Implement deterministic wait-graph traversal**
|
||
|
||
Use stable vehicle-ID traversal. YIELD may target only topology nodes flagged `safeWait=true`, must not cross occupied resources, and must never alter the business destination.
|
||
|
||
- [ ] **Step 4: Run GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=DeadlockGuardTest,ResourceLedgerTest test`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic/DeadlockGuardTest.java
|
||
git commit -m "feat(wcs): prevent traffic deadlocks"
|
||
```
|
||
|
||
### Task 7: Orchestrate dispatch decisions and persist traffic state
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/dispatch/DispatchCoordinator.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/dispatch/DispatchCycleResult.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/dispatch/DispatchSnapshot.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/dispatch/GrantedDispatch.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DispatchSnapshotProvider.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/TrafficStateRepository.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DecisionEventRepository.java`
|
||
- Create: `sql/wcs/V002__wcs_topology_and_traffic.sql`
|
||
- Create: `sql/wcs/verify_V002__wcs_topology_and_traffic.sql`
|
||
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/dispatch/DispatchCoordinatorTest.java`
|
||
- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/traffic/TrafficConcurrencyIT.java`
|
||
|
||
**Interfaces:**
|
||
- Produces: `DispatchSnapshotProvider.load(): DispatchSnapshot`; the immutable snapshot contains ready steps, current vehicle snapshots, effective topology, and one optimistic `snapshotVersion`.
|
||
- Produces: `dispatch(DispatchSnapshot): DispatchCycleResult`.
|
||
- `DispatchCycleResult.getGranted()` returns `List<GrantedDispatch>`; each item contains order ID, step ID, vehicle ID, protected route, reservation IDs, topology version, and fencing token.
|
||
- Persists: vehicle assignment, route plan, reservations, resource counts, and decision event in one transaction.
|
||
- Produces: no device command; Plan 3 consumes granted assignments.
|
||
|
||
- [ ] **Step 1: Write a failing two-vehicle arbitration test**
|
||
|
||
```java
|
||
@Test
|
||
void grantsOneVehicleAndHoldsTheOtherForTheSameIntersection() {
|
||
DispatchCycleResult result = coordinator.dispatch(dispatchSnapshot(
|
||
Arrays.asList(step("S1", 1), step("S2", 2)),
|
||
Arrays.asList(vehicle("V1"), vehicle("V2")), topology));
|
||
assertEquals(1, result.getGranted().size());
|
||
assertEquals(1, result.getHeld().size());
|
||
assertEquals(0, result.getInvariantViolations().size());
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest=DispatchCoordinatorTest test`
|
||
|
||
- [ ] **Step 3: Implement the coordinator transaction boundary**
|
||
|
||
Load one immutable `DispatchSnapshot`, sort ready steps by priority/age/order ID, assign vehicles, plan routes, call `DeadlockGuard`, then call one repository method:
|
||
|
||
```java
|
||
trafficStateRepository.commit(new DispatchCommit(
|
||
topology.version(), snapshotVersion, assignments,
|
||
routePlans, reservations, decisionEvents));
|
||
```
|
||
|
||
If optimistic versions changed, return `RETRY_SNAPSHOT_CHANGED` and persist no partial assignment.
|
||
|
||
- [ ] **Step 4: Add SQL Server tables and locking test**
|
||
|
||
Create `wcs_warehouse`, `wcs_floor`, `wcs_topology_version`, `wcs_topology_node`, `wcs_topology_edge`, `wcs_traffic_zone`, `wcs_resource_definition`, `wcs_device`, `wcs_device_capability`, `wcs_device_port`, `wcs_charger`, `wcs_vehicle_assignment`, `wcs_route_plan`, `wcs_resource_state`, `wcs_resource_reservation`, and `wcs_traffic_decision_event`. The integration test runs two concurrent capacity-1 reservations and asserts exactly one commit succeeds.
|
||
|
||
- [ ] **Step 5: Run full Plan 2 verification**
|
||
|
||
Run: `mvn -pl ruoyi-wcs -am -Dtest='*Topology*,*Capability*,*RoutePlanner*,*VehicleAssignment*,*ResourceLedger*,*DeadlockGuard*,*DispatchCoordinator*' test`
|
||
|
||
Run SQL-tagged tests against the isolated SQL Server: `mvn -pl ruoyi-wcs -Dgroups=sqlserver -Dtest=TrafficConcurrencyIT test`.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/dispatch ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/dispatch ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/traffic sql/wcs
|
||
git commit -m "feat(wcs): coordinate and persist traffic decisions"
|
||
```
|