516 lines
24 KiB
Markdown
516 lines
24 KiB
Markdown
# Standard WCS FT546 Adapters 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:** 用 FT546 Admin 适配器把现有 WMS URL、旧 PLC 寄存器、设备清单和 RuoYi 页面接入标准 `ruoyi-wcs`,并让标准运行入口取代旧调度线程的业务所有权。
|
||
|
||
**Architecture:** `ruoyi-admin` 下新增 `com.ruoyi.project.ft546`,保存 WMS、设备协议、配置、运行 wiring 和页面 Controller。现有 `PlcSrgvRead/PlcTsjRead/PlcSsjRead/PlcCdzRead` 只保留通信与事实采集职责;核心端口实现调用它们,但状态裁决全部回到 `ruoyi-wcs`。
|
||
|
||
**Tech Stack:** Java 8, RuoYi, Spring MVC, Spring scheduling, HslCommunication Modbus, PLC4J, MyBatis, Vue 2, Element UI.
|
||
|
||
## Global Constraints
|
||
|
||
- 保留 `/wcs2Wms/api/receiveOutMydoJob`、`receiveInMydoJob`、`cancelMydoJob` 和 `tidyVehicleSnapshot` URL。
|
||
- 保留 FT546 现有 PLC 地址和寄存器语义,不在核心出现 `4000/4400/4068` 等地址。
|
||
- `Ft546LegacySrgvAdapter` 必须按能力 Profile 限制命令;旧 PLC 不支持命令幂等和 Prepare/Commit。
|
||
- 旧队列、`default02-default08` 和 `mydo_job_wcs` 只作为旧接口证据,不成为新核心状态。
|
||
- 一个设备同一时刻只允许一个运动命令;`UNKNOWN` 时只读快照。
|
||
- 项目配置在启动时校验;有活动命令时禁止切换 Profile。
|
||
- `BaseSRgvThread`、`TsjThread` 和 `CdzThread` 不再创建、调度或完成标准任务。
|
||
- UI 只调用 QueryService 和 RecoveryUseCase,不直接编辑核心表。
|
||
|
||
---
|
||
|
||
### Task 1: Add FT546 project configuration and capability profiles
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546WcsProperties.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546DeviceProfile.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546ConfigurationValidator.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546CapabilityGate.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546CapabilityEvidence.java`
|
||
- Create: `ruoyi-admin/src/main/resources/ft546/wcs-device-profiles.yml`
|
||
- Create: `sql/wcs/V004__ft546_project_adapter_config.sql`
|
||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/config/Ft546ConfigurationValidatorTest.java`
|
||
|
||
**Interfaces:**
|
||
- Produces: `Ft546WcsProperties.getRuntimeMode/getDispatchIntervalMs/getCommandPollIntervalMs`.
|
||
- Produces: device profile fields `deviceId`, `deviceType`, `adapterType`, `ip`, `port`, `station`, `capabilities`, `registers`, `profileVersion`.
|
||
- Persists Admin-owned `project_device_endpoint`, `project_plc_address`, `project_protocol_profile`, `project_protocol_capability_evidence`, `project_wms_mapping`.
|
||
- `Ft546CapabilityGate.evaluate(profile,evidence): CapabilityGateResult` defaults `TWO_STAGE_ATOMIC_TASK` to disabled and permits it only when evidence names the exact profile version, corpus manifest hash, passing test run ID, Git commit, and approval time.
|
||
|
||
- [ ] **Step 1: Write failing profile-validation tests**
|
||
|
||
```java
|
||
@Test
|
||
void rejectsLegacyProfileThatClaimsPrepareCommit() {
|
||
Ft546DeviceProfile profile = legacySrgvProfile();
|
||
profile.getCapabilities().add("PREPARE_COMMIT_SEGMENT");
|
||
assertThrows(IllegalArgumentException.class, () -> validator.validate(profile));
|
||
}
|
||
|
||
@Test
|
||
void rejectsDuplicateRegisterOwnershipWithinOneDevice() {
|
||
assertThrows(IllegalArgumentException.class, () ->
|
||
validator.validate(profileWithDuplicateAddress("4000")));
|
||
}
|
||
|
||
@Test
|
||
void rejectsTwoStageCapabilityWithoutPassingEvidenceForTheExactProfile() {
|
||
Ft546DeviceProfile profile = legacySrgvProfileWith("TWO_STAGE_ATOMIC_TASK");
|
||
assertFalse(capabilityGate.evaluate(profile, noEvidence()).isPublishable());
|
||
assertEquals("TWO_STAGE_EVIDENCE_REQUIRED",
|
||
capabilityGate.evaluate(profile, noEvidence()).getReasonCode());
|
||
}
|
||
|
||
@Test
|
||
void legacyProfileKeepsUnsupportedCapabilitiesDisabled() {
|
||
CapabilityGateResult result = capabilityGate.evaluate(legacySrgvProfile(), noEvidence());
|
||
assertFalse(result.isEnabled("COMMAND_RESULT_QUERY"));
|
||
assertFalse(result.isEnabled("ORDERED_DEVICE_SNAPSHOT"));
|
||
assertFalse(result.isEnabled("CANDIDATE_BUFFER"));
|
||
assertFalse(result.isEnabled("PREPARE_COMMIT_SEGMENT"));
|
||
assertFalse(result.isEnabled("IDEMPOTENT_COMMAND_KEY"));
|
||
assertFalse(result.isEnabled("BOOT_SESSION_ID"));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546ConfigurationValidatorTest test`
|
||
|
||
- [ ] **Step 3: Implement strongly typed configuration**
|
||
|
||
The checked-in profile contains no credentials and declares:
|
||
|
||
```yaml
|
||
devices:
|
||
- deviceId: "1"
|
||
deviceType: SHUTTLE
|
||
adapterType: FT546_LEGACY_SRGV
|
||
profileVersion: 1
|
||
capabilities: [POINT_TO_POINT_COMMAND]
|
||
registers:
|
||
taskNo: "4000"
|
||
taskType: "4002"
|
||
path: "4004"
|
||
trigger: "4021"
|
||
status: "4068"
|
||
execution: "4060"
|
||
```
|
||
|
||
IP, port, station and WMS URLs come from database/environment configuration, not the checked-in YAML.
|
||
|
||
Persist capability evidence separately from the profile. Publishing or activating a profile fails closed when evidence is absent, expired, belongs to another profile version/hash, or reports any protocol regression failure. Active-command devices cannot change profile or evidence binding.
|
||
|
||
- [ ] **Step 4: Run GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546ConfigurationValidatorTest test`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config ruoyi-admin/src/main/resources/ft546 sql/wcs/V004__ft546_project_adapter_config.sql ruoyi-admin/src/test/java/com/ruoyi/project/ft546/config
|
||
git commit -m "feat(ft546): add WCS adapter configuration profiles"
|
||
```
|
||
|
||
### Task 2: Replace fixed shuttle readers with a device runtime registry
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/Ft546DeviceRuntimeRegistry.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvRuntimeFactory.java`
|
||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java`
|
||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/srgv/PlcSrgvRead.java`
|
||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546DeviceRuntimeRegistryTest.java`
|
||
|
||
**Interfaces:**
|
||
- Produces: `register(String deviceId, PlcSrgvRead runtime)`, `requireSrgv(String deviceId)`, `allSrgv()`.
|
||
- Produces: factory `create(DeviceSrgv,Ft546DeviceProfile): PlcSrgvRead`.
|
||
- Removes new-code dependence on fixed `srgv1Read/srgv2Read/srgv3Read` switches.
|
||
|
||
- [ ] **Step 1: Write a failing configurable-count test**
|
||
|
||
```java
|
||
@Test
|
||
void registersAnyConfiguredNumberOfShuttleRuntimes() {
|
||
registry.register("1", runtime("1"));
|
||
registry.register("2", runtime("2"));
|
||
registry.register("9", runtime("9"));
|
||
assertEquals(3, registry.allSrgv().size());
|
||
assertSame(runtime("9").getClass(), registry.requireSrgv("9").getClass());
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546DeviceRuntimeRegistryTest test`
|
||
|
||
- [ ] **Step 3: Implement registry-backed startup**
|
||
|
||
`ScheduleThread.startAllThread()` loads enabled devices, creates readers through the factory, registers them, and starts only fact-collection threads. It must not start `SRgv1Thread` or any task-creation thread in `STANDARD` mode.
|
||
|
||
- [ ] **Step 4: Run GREEN and verify no three-vehicle switch remains in new paths**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546DeviceRuntimeRegistryTest test`
|
||
|
||
Run: `rg "case 1|case 2|case 3|srgv1Read|srgv2Read|srgv3Read" ruoyi-admin/src/main/java/com/ruoyi/project/ft546 ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java`
|
||
|
||
Expected: no fixed vehicle selection in the new project package or Standard startup branch.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/srgv/PlcSrgvRead.java ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device
|
||
git commit -m "refactor(ft546): register shuttle runtimes from configuration"
|
||
```
|
||
|
||
### Task 3: Implement the legacy shuttle Adapter and snapshot mapper
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapter.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvCommandEncoder.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvSnapshotMapper.java`
|
||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapterTest.java`
|
||
- Create: `ruoyi-admin/src/test/resources/ft546/plc/srgv-point-to-point.json`
|
||
- Create: `ruoyi-admin/src/test/resources/ft546/plc/srgv-two-stage-gate.json`
|
||
|
||
**Interfaces:**
|
||
- Implements: `DeviceCommandGateway` and `DeviceSnapshotGateway` for `SHUTTLE` devices.
|
||
- Consumes: `Ft546DeviceRuntimeRegistry` and validated profile.
|
||
- Produces: existing write sequence `4004 -> 4000 -> 4002 -> 4021`, plus configured optional fields.
|
||
|
||
- [ ] **Step 1: Write failing protocol sequence and UNKNOWN tests**
|
||
|
||
```java
|
||
@Test
|
||
void encodesPointToPointWritesInTheExistingOrder() {
|
||
SendReceipt receipt = adapter.send(pointToPoint("CMD-1", "1", route("A", "B")));
|
||
assertEquals(SendReceipt.Status.ACCEPTED, receipt.getStatus());
|
||
assertEquals(Arrays.asList("4004", "4000", "4002", "4021"), fakeRuntime.writtenAddresses());
|
||
}
|
||
|
||
@Test
|
||
void writeAmbiguityReturnsAcceptedUnknownInsteadOfRetryableNotSent() {
|
||
fakeRuntime.throwAfterWriting("4000");
|
||
assertEquals(SendReceipt.Status.ACCEPTED_UNKNOWN,
|
||
adapter.send(pointToPoint("CMD-1", "1", route("A", "B"))).getStatus());
|
||
}
|
||
|
||
@Test
|
||
void twoStageCommandIsRejectedUntilTheProfileGateEvidencePasses() {
|
||
assertThrows(IllegalStateException.class, () ->
|
||
adapterFor(twoStageProfile(), noEvidence()));
|
||
assertEquals(0, fakeRuntime.writtenAddresses().size());
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546LegacySrgvAdapterTest test`
|
||
|
||
- [ ] **Step 3: Implement profile-limited command encoding**
|
||
|
||
Reject commands other than enabled `POINT_TO_POINT_COMMAND` and evidence-gated `TWO_STAGE_ATOMIC_TASK`. The two-stage gate fixture records the exact legacy write order, trigger semantics, disconnect boundaries, expected certainty, profile version, and manifest hash; it does not claim Prepare/Commit semantics. Never send a new command when the core reports an active `UNKNOWN` command for the device. Map `DeviceSrgv` connection, mode, status, battery, current site, task number, source, target, floor and load facts into the standard snapshot without inventing device sequence or boot ID.
|
||
|
||
- [ ] **Step 4: Run protocol regression GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546LegacySrgvAdapterTest test`
|
||
|
||
Expected: JSON sample and register sequence match, and no command completes merely because writes returned success.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle ruoyi-admin/src/test/resources/ft546/plc/srgv-point-to-point.json
|
||
git commit -m "feat(ft546): adapt legacy shuttle PLC protocol"
|
||
```
|
||
|
||
### Task 4: Implement lift, conveyor, and charger Adapters
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/lift/Ft546LiftAdapter.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/conveyor/Ft546ConveyorAdapter.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/charger/Ft546ChargerAdapter.java`
|
||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/tsj/PlcTsjRead.java`
|
||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/ssj/PlcSsjRead.java`
|
||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/cdz/PlcCdzRead.java`
|
||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546EquipmentAdaptersTest.java`
|
||
|
||
**Interfaces:**
|
||
- Implements standard `DeviceCommandGateway`/`DeviceSnapshotGateway` dispatch by device type.
|
||
- Maps lift floor/door/port facts, conveyor goods/mode/ready facts, and charger occupancy/charging/fault facts.
|
||
|
||
- [ ] **Step 1: Write failing fact-mapping and no-business-decision tests**
|
||
|
||
```java
|
||
@Test
|
||
void liftAdapterReportsFactsWithoutReleasingCoreResources() {
|
||
DeviceSnapshot snapshot = liftAdapter.readSnapshot("TSJ-1");
|
||
assertEquals("3", snapshot.fact("floor"));
|
||
assertEquals("OPEN", snapshot.fact("doorState"));
|
||
assertFalse(snapshot.getFacts().containsKey("releaseLiftResource"));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546EquipmentAdaptersTest test`
|
||
|
||
- [ ] **Step 3: Implement typed protocol translation**
|
||
|
||
Each adapter validates device type and enabled command, performs only configured writes/reads, and emits standard facts. Remove business entry/exit decisions from new calls to `TsjThread`, `SsjThread`, and `CdzThread`; keep those classes unused in Standard mode.
|
||
|
||
- [ ] **Step 4: Run GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546EquipmentAdaptersTest,Ft546LegacySrgvAdapterTest test`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device
|
||
git commit -m "feat(ft546): adapt lift conveyor and charger protocols"
|
||
```
|
||
|
||
### Task 5: Adapt existing WMS endpoints to standard order use cases
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsOrderMapper.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterService.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsNotificationGateway.java`
|
||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/controller/Wcs2WmsController.java`
|
||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ApiServiceImpl.java`
|
||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterServiceTest.java`
|
||
|
||
**Interfaces:**
|
||
- Maps existing `ReceiveJob` to `CreateTransportOrderCommand`.
|
||
- Keeps existing response envelope expected by WMS.
|
||
- Implements `WmsNotificationGateway.notify(WmsNotification): WmsNotificationReceipt` by calling the existing `RequestLogService` callback transport.
|
||
|
||
- [ ] **Step 1: Write failing endpoint mapping and idempotency tests**
|
||
|
||
```java
|
||
@Test
|
||
void mapsExistingInboundRequestToCanonicalOrder() {
|
||
adapter.receiveInMydoJob(receiveJob("TASK-1", "PORT-1", "SPACE-9"));
|
||
CreateTransportOrderCommand command = useCase.lastCreateCommand();
|
||
assertEquals("TASK-1", command.getExternalRequestId());
|
||
assertEquals(TransportOrderType.INBOUND, command.getOrderType());
|
||
assertEquals("PORT-1", command.getSource());
|
||
assertEquals("SPACE-9", command.getDestination());
|
||
}
|
||
|
||
@Test
|
||
void duplicateWmsRequestReturnsTheSameOrder() {
|
||
assertEquals(adapter.receiveOutMydoJob(job).getOrderId(),
|
||
adapter.receiveOutMydoJob(job).getOrderId());
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546WmsAdapterServiceTest test`
|
||
|
||
- [ ] **Step 3: Delegate the four preserved endpoints**
|
||
|
||
`Wcs2WmsController` routes `receiveOutMydoJob`, `receiveInMydoJob`, `cancelMydoJob`, and `tidyVehicleSnapshot` to `Ft546WmsAdapterService`. The new path must not insert `mydo_job_wcs` or `wcs_srgv_queue`; unrelated legacy endpoints remain untouched.
|
||
|
||
- [ ] **Step 4: Implement idempotent completion callback**
|
||
|
||
Map `OrderCompleted/Failed/Cancelled` to existing callback payloads and use WCS `eventId` as the callback idempotency key. A callback transport failure leaves Outbox pending and never recreates an order.
|
||
|
||
- [ ] **Step 5: Run GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546WmsAdapterServiceTest test`
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms ruoyi-admin/src/main/java/com/ruoyi/api/controller/Wcs2WmsController.java ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ApiServiceImpl.java ruoyi-admin/src/test/java/com/ruoyi/project/ft546/wms
|
||
git commit -m "feat(ft546): adapt existing WMS contract to WCS core"
|
||
```
|
||
|
||
### Task 6: Wire the Standard runtime and remove legacy scheduling ownership
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/WcsRuntimeCoordinator.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/RuntimeTickResult.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/SnapshotCollector.java`
|
||
- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/recovery/RecoveryGate.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime/Ft546WcsScheduler.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime/Ft546AdapterConfiguration.java`
|
||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java`
|
||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/scheduleTask/srgv/BaseSRgvThread.java`
|
||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/runtime/Ft546StandardRuntimeTest.java`
|
||
|
||
**Interfaces:**
|
||
- Produces: `WcsRuntimeCoordinator.tick(): RuntimeTickResult`.
|
||
- Scheduler calls snapshot ingest, recovery gate, dispatch, command creation, and Outbox delivery in that order.
|
||
- Standard mode has exactly one business scheduler and one Outbox sender.
|
||
- Consumes: `DispatchSnapshotProvider.load()` and `DispatchCoordinator.dispatch(DispatchSnapshot)` from Plan 2.
|
||
|
||
- [ ] **Step 1: Write a failing single-writer test**
|
||
|
||
```java
|
||
@Test
|
||
void standardModeStartsCoreRuntimeButNotLegacyBusinessThreads() {
|
||
runtime.start();
|
||
assertTrue(runtime.isCoreSchedulerStarted());
|
||
assertFalse(runtime.isBaseSrgvThreadStarted());
|
||
assertFalse(runtime.isTsjBusinessThreadStarted());
|
||
assertFalse(runtime.isCdzBusinessThreadStarted());
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546StandardRuntimeTest test`
|
||
|
||
- [ ] **Step 3: Implement the runtime tick**
|
||
|
||
```java
|
||
public RuntimeTickResult tick() {
|
||
snapshotCollector.collectAndAccept();
|
||
if (!recoveryGate.allowsDispatch()) {
|
||
return RuntimeTickResult.recoveryBlocked();
|
||
}
|
||
DispatchCycleResult dispatch = dispatchCoordinator.dispatch(dispatchSnapshotProvider.load());
|
||
for (GrantedDispatch granted : dispatch.getGranted()) {
|
||
executionService.createCommand(granted);
|
||
}
|
||
OutboxDispatchResult outbox = outboxDispatcher.dispatchDue(100, clock.now());
|
||
return RuntimeTickResult.of(dispatch, outbox);
|
||
}
|
||
```
|
||
|
||
`ScheduleThread` in Standard mode starts PLC fact readers only. Remove standard-mode calls that instantiate or start `SRgv1Thread`, `TsjThread`, `SsjThread`, or `CdzThread`. Keep legacy code compilable but unreachable from Standard startup.
|
||
|
||
- [ ] **Step 4: Run single-writer and reactor tests GREEN**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546StandardRuntimeTest,Ft546WmsAdapterServiceTest,Ft546LegacySrgvAdapterTest test`
|
||
|
||
Run: `rg "new SRgv1Thread|new TsjThread|new CdzThread" ruoyi-admin/src/main/java/com/ruoyi/project/ft546 ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java`
|
||
|
||
Expected: Standard branch contains no legacy business thread construction.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/WcsRuntimeCoordinator.java ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java ruoyi-admin/src/main/java/com/ruoyi/schedule/scheduleTask/srgv/BaseSRgvThread.java ruoyi-admin/src/test/java/com/ruoyi/project/ft546/runtime
|
||
git commit -m "feat(ft546): run standard WCS as the single scheduler"
|
||
```
|
||
|
||
### Task 7: Add operations controllers and Vue monitoring pages
|
||
|
||
**Files:**
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsOrderController.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsDeviceController.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsTrafficController.java`
|
||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsRecoveryController.java`
|
||
- Create: `ruoyi-ui/src/api/wcs/order.js`
|
||
- Create: `ruoyi-ui/src/api/wcs/device.js`
|
||
- Create: `ruoyi-ui/src/api/wcs/traffic.js`
|
||
- Create: `ruoyi-ui/src/api/wcs/recovery.js`
|
||
- Create: `ruoyi-ui/src/views/wcs/order/index.vue`
|
||
- Create: `ruoyi-ui/src/views/wcs/device/index.vue`
|
||
- Create: `ruoyi-ui/src/views/wcs/traffic/index.vue`
|
||
- Create: `ruoyi-ui/src/views/wcs/recovery/index.vue`
|
||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/web/WcsOperationsControllerTest.java`
|
||
|
||
**Interfaces:**
|
||
- GET `/wcs/orders`, `/wcs/devices`, `/wcs/traffic/resources`, `/wcs/recovery/cases`.
|
||
- POST `/wcs/orders/{id}/suspend`, `/resume`, `/cancel`.
|
||
- POST `/wcs/recovery/cases/{id}/confirm` and `/abort` with operator reason/evidence.
|
||
|
||
- [ ] **Step 1: Write failing controller authorization and DTO tests**
|
||
|
||
```java
|
||
@Test
|
||
@WithMockUser(authorities = "wcs:recovery:confirm")
|
||
void confirmsRecoveryThroughTheUseCase() throws Exception {
|
||
mockMvc.perform(post("/wcs/recovery/cases/RC-1/confirm")
|
||
.contentType(MediaType.APPLICATION_JSON)
|
||
.content("{\"reason\":\"现场确认车辆位于070501\",\"currentSite\":\"070501\",\"loaded\":false}"))
|
||
.andExpect(status().isOk());
|
||
verify(recoveryUseCase).confirmFacts(eq("RC-1"), anyString(), any(RecoveryEvidence.class));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=WcsOperationsControllerTest test`
|
||
|
||
- [ ] **Step 3: Implement controllers and API clients**
|
||
|
||
Controllers return stable QueryService DTOs and use RuoYi permissions. Vue pages provide filters, tables, state tags, detail drawers, pagination and explicit recovery dialogs; they do not include generic CRUD edit/delete for core records.
|
||
|
||
- [ ] **Step 4: Implement the traffic view without editing core state**
|
||
|
||
Show resource key, type, capacity, protected count, holder, reservation state and evidence ID. Only Recovery page exposes controlled confirm/abort commands.
|
||
|
||
- [ ] **Step 5: Run backend and frontend verification**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=WcsOperationsControllerTest test`
|
||
|
||
Run: `cd ruoyi-ui && npm run build:prod`
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web ruoyi-admin/src/test/java/com/ruoyi/project/ft546/web ruoyi-ui/src/api/wcs ruoyi-ui/src/views/wcs
|
||
git commit -m "feat(ft546): add WCS operations console"
|
||
```
|
||
|
||
### Task 8: Add menus, permissions, and the FT546 end-to-end contract test
|
||
|
||
**Files:**
|
||
- Create: `sql/wcs/V005__ft546_wcs_menus_and_permissions.sql`
|
||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/Ft546StandardWcsEndToEndTest.java`
|
||
- Create: `ruoyi-admin/src/test/resources/ft546/scenarios/inbound-same-floor.json`
|
||
- Create: `ruoyi-admin/src/test/resources/ft546/scenarios/outbound-cross-floor.json`
|
||
|
||
**Interfaces:**
|
||
- Permissions: `wcs:order:list`, `wcs:order:control`, `wcs:device:list`, `wcs:traffic:list`, `wcs:recovery:list`, `wcs:recovery:confirm`.
|
||
- End-to-end test uses fake PLC runtimes and fake WMS callback transport through real Spring wiring.
|
||
|
||
- [ ] **Step 1: Write the failing same-floor scenario**
|
||
|
||
```java
|
||
@Test
|
||
void completesInboundThroughExistingWmsUrlAndLegacyShuttleAdapter() throws Exception {
|
||
String orderId = postInboundScenario("inbound-same-floor.json");
|
||
runTicksUntilTerminal(orderId, 200);
|
||
assertEquals("COMPLETED", operations.order(orderId).getOrderState());
|
||
assertEquals(1, callbackTransport.businessEffectCount(orderId));
|
||
assertEquals(0, invariantMonitor.totalViolations());
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run RED**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546StandardWcsEndToEndTest test`
|
||
|
||
- [ ] **Step 3: Add permission SQL and scenario fixtures**
|
||
|
||
Insert one WCS menu group and four child pages using existing RuoYi `sys_menu` conventions. Use idempotent `IF NOT EXISTS` checks keyed by permission string.
|
||
|
||
- [ ] **Step 4: Make same-floor and cross-floor scenarios GREEN**
|
||
|
||
The cross-floor test must prove source port, lift and target port joint reservation; WMS receives one completion event; no legacy queue row is created.
|
||
|
||
Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546StandardWcsEndToEndTest test`
|
||
|
||
- [ ] **Step 5: Run full Plan 4 verification**
|
||
|
||
Run: `mvn -pl ruoyi-admin -am test`
|
||
|
||
Run: `cd ruoyi-ui && npm run build:prod`
|
||
|
||
Run: `git diff --check`
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add sql/wcs/V005__ft546_wcs_menus_and_permissions.sql ruoyi-admin/src/test/java/com/ruoyi/project/ft546/Ft546StandardWcsEndToEndTest.java ruoyi-admin/src/test/resources/ft546/scenarios
|
||
git commit -m "test(ft546): verify standard WCS end to end"
|
||
```
|