# 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" ```