Files
FT546-WCS/docs/superpowers/plans/2026-08-04-standard-wcs-ft546-adapters.md

24 KiB
Raw Blame History

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

@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:

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
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

@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
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

@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
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

@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
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

@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
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

@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
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
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

@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
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

@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
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"