Files
FT546-WCS/docs/superpowers/plans/2026-08-04-standard-wcs-core-foundation.md

22 KiB
Raw Blame History

Standard WCS Core Foundation 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: 建立可被 ruoyi-admin 依赖的 ruoyi-wcs 模块,并交付订单、流程、步骤、幂等和核心持久化边界。

Architecture: ruoyi-wcs 只暴露入站用例、出站端口和查询 DTO;任务状态迁移保存在领域对象中,应用服务负责事务和幂等。Admin 只增加模块依赖,本计划不接 PLC,也不替换现有调度线程。

Tech Stack: Java 8, Spring Boot 2.5.15, Maven, JUnit 5, ArchUnit 1.2.1, MyBatis, SQL Server.

Global Constraints

  • 根 Reactor 新增且只新增 ruoyi-wcs 产品模块。
  • ruoyi-wcs 只依赖 ruoyi-common、Spring 事务、MyBatis 和测试依赖;不依赖 ruoyi-admin、PLC 库或项目包。
  • 所有公共接口使用 Java 8 类型,不使用 record、sealed class 或高版本 API。
  • 订单、步骤和事件 ID 使用非空 String,时间使用 java.time.Instant。
  • 外部请求以 externalRequestId 幂等;重复请求返回同一 orderId。
  • 终态订单和步骤不可反向迁移。
  • 本计划不修改 BaseSRgvThread、PlcSrgvRead、现有 WMS Controller 或旧业务表。

Task 1: Add the ruoyi-wcs Maven module and architecture test harness

Files:

  • Modify: pom.xml
  • Modify: ruoyi-admin/pom.xml
  • Create: ruoyi-wcs/pom.xml
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/WcsModule.java
  • Create: ruoyi-wcs/src/test/java/com/ruoyi/wcs/architecture/WcsModuleArchitectureTest.java

Interfaces:

  • Produces: Maven artifact com.ruoyi:ruoyi-wcs:${ruoyi.version}.

  • Produces: marker type com.ruoyi.wcs.WcsModule.

  • Enforces: no class under com.ruoyi.wcs.. depends on com.ruoyi.schedule.., com.ruoyi.api.., HslCommunication.., or org.apache.plc4x...

  • Step 1: Add the module skeleton and a deliberately failing boundary test

Add <module>ruoyi-wcs</module> after ruoyi-common, add the managed dependency, add the Admin dependency, and create this test before WcsModule exists:

@Test
void exposesAStableModuleMarker() throws Exception {
    assertEquals("com.ruoyi.wcs.WcsModule",
            Class.forName("com.ruoyi.wcs.WcsModule").getName());
}

Configure spring-boot-starter-test and archunit-junit5:1.2.1 as test dependencies in ruoyi-wcs/pom.xml; configure Surefire 3.2.5 in the root build.

  • Step 2: Run the module test and observe RED

Run: mvn -pl ruoyi-wcs -am -Dtest=WcsModuleArchitectureTest test

Expected: FAIL because com.ruoyi.wcs.WcsModule does not exist.

  • Step 3: Add the marker and dependency rules
package com.ruoyi.wcs;

public final class WcsModule {
    private WcsModule() {
    }
}

Add ArchUnit rules:

@Test
void coreDoesNotDependOnProjectOrPlcPackages() {
    noClasses().that().resideInAPackage("com.ruoyi.wcs..")
        .should().dependOnClassesThat()
        .resideInAnyPackage("com.ruoyi.schedule..", "com.ruoyi.api..",
                "HslCommunication..", "org.apache.plc4x..")
        .check(new ClassFileImporter().importPackages("com.ruoyi.wcs"));
}
  • Step 4: Run module and Admin reactor tests GREEN

Run: mvn -pl ruoyi-wcs,ruoyi-admin -am test

Expected: Reactor succeeds and Surefire reports at least one ruoyi-wcs test.

  • Step 5: Commit
git add pom.xml ruoyi-admin/pom.xml ruoyi-wcs
git commit -m "build: add standard WCS core module"

Task 2: Define shared time, ID, event, and failure contracts

Files:

  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/WcsClock.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/IdGenerator.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/DomainEvent.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/DomainRuleViolation.java
  • Create: ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/shared/SharedContractsTest.java

Interfaces:

  • Produces: WcsClock.now(): Instant.

  • Produces: IdGenerator.nextId(): String.

  • Produces: DomainEvent.getEventId/getAggregateId/getSequence/getOccurredAt/getType.

  • Step 1: Write failing immutable event tests

@Test
void rejectsBlankEventIdentity() {
    assertThrows(IllegalArgumentException.class, () ->
        new DomainEvent("", "ORDER-1", 1L, Instant.EPOCH, "OrderAccepted"));
}

@Test
void exposesDeterministicEventFields() {
    DomainEvent event = new DomainEvent("E-1", "ORDER-1", 2L,
            Instant.parse("2026-08-04T00:00:00Z"), "OrderPlanned");
    assertEquals(2L, event.getSequence());
    assertEquals("OrderPlanned", event.getType());
}
  • Step 2: Run RED

Run: mvn -pl ruoyi-wcs -am -Dtest=SharedContractsTest test

Expected: compilation fails because the shared contracts do not exist.

  • Step 3: Implement the minimal contracts
public interface WcsClock {
    Instant now();
}

public interface IdGenerator {
    String nextId();
}

public final class DomainRuleViolation extends RuntimeException {
    public DomainRuleViolation(String message) {
        super(message);
    }
}

Implement DomainEvent as a final immutable class with constructor validation and getters.

  • Step 4: Run GREEN

Run: mvn -pl ruoyi-wcs -am -Dtest=SharedContractsTest test

  • Step 5: Commit
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/shared
git commit -m "feat(wcs): add shared domain contracts"

Task 3: Implement the TransportOrder state machine

Files:

  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrderState.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrderType.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrder.java
  • Create: ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task/TransportOrderTest.java

Interfaces:

  • Produces: TransportOrder.accept(...) factory.

  • Produces: plan(String workflowId), reject(String code,String reason), start(), suspend(String reason), resumeToPlanned(), resumeToExecuting(), requestCancel(), cancel(), complete(), fail(String reason).

  • Produces: getUncommittedEvents(): List<DomainEvent> and clearUncommittedEvents().

  • Step 1: Write failing legal and illegal transition tests

@Test
void completesOnlyThroughPlannedAndExecuting() {
    TransportOrder order = order();
    order.plan("WF-1");
    order.start();
    order.complete();
    assertEquals(TransportOrderState.COMPLETED, order.getState());
}

@Test
void terminalOrderCannotResume() {
    TransportOrder order = order();
    order.plan("WF-1");
    order.cancel();
    assertThrows(DomainRuleViolation.class, order::resumeToPlanned);
}
  • Step 2: Run RED

Run: mvn -pl ruoyi-wcs -am -Dtest=TransportOrderTest test

  • Step 3: Implement explicit transition guards
public void start() {
    requireState(TransportOrderState.PLANNED);
    transitionTo(TransportOrderState.EXECUTING, "OrderStarted");
}

public void requestCancel() {
    if (state != TransportOrderState.EXECUTING
            && state != TransportOrderState.SUSPENDED) {
        throw new DomainRuleViolation("only executing or suspended order can request cancellation");
    }
    transitionTo(TransportOrderState.CANCEL_REQUESTED, "OrderCancelRequested");
}

Implement this exact transition set and increment event sequence exactly once per successful transition:

  • ACCEPTED -> PLANNED/REJECTED/SUSPENDED/CANCELLED/FAILED.

  • PLANNED -> EXECUTING/SUSPENDED/CANCELLED/FAILED.

  • EXECUTING -> COMPLETED/SUSPENDED/CANCEL_REQUESTED/FAILED.

  • SUSPENDED -> PLANNED/EXECUTING/CANCEL_REQUESTED/FAILED.

  • CANCEL_REQUESTED -> CANCELLED/FAILED only after application-level command and resource convergence checks.

  • REJECTED/COMPLETED/CANCELLED/FAILED are mutually exclusive terminal states and reject every further transition.

  • Step 4: Run state tests GREEN

Run: mvn -pl ruoyi-wcs -am -Dtest=TransportOrderTest test

  • Step 5: Commit
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task
git commit -m "feat(wcs): add transport order state machine"

Task 4: Implement WorkflowInstance and TaskStep dependencies

Files:

  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TaskStepState.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TaskStepType.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TaskStep.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowInstance.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowDefinition.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowStepDefinition.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WorkflowDefinitionProvider.java
  • Create: ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task/WorkflowInstanceTest.java

Interfaces:

  • Produces: WorkflowDefinitionProvider.definitionFor(TransportOrderType): WorkflowDefinition.

  • Produces: WorkflowInstance.create(String workflowId, String orderId, WorkflowDefinition).

  • Produces: readySteps(): List<TaskStep> and onStepSucceeded(String stepId).

  • TaskStep exposes markReady, block(reasonCode), unblock, assign(vehicleId,reservationIds), start, suspend(reason), resumeAssigned, resumeRunning, succeed, skip, cancel, and fail(reason).

  • Step 1: Write failing dependency and optional-step tests

@Test
void exposesOnlyStepsWhoseMandatoryDependenciesSucceeded() {
    WorkflowInstance workflow = workflow("MOVE", "DROP");
    assertEquals(Collections.singletonList("MOVE"), ids(workflow.readySteps()));
    workflow.step("MOVE").assign("VEH-1");
    workflow.step("MOVE").start();
    workflow.step("MOVE").succeed();
    workflow.onStepSucceeded("MOVE");
    assertEquals(Collections.singletonList("DROP"), ids(workflow.readySteps()));
}
  • Step 2: Run RED

Run: mvn -pl ruoyi-wcs -am -Dtest=WorkflowInstanceTest test

  • Step 3: Implement dependency evaluation and transition methods
public List<TaskStep> readySteps() {
    List<TaskStep> result = new ArrayList<TaskStep>();
    for (TaskStep step : steps.values()) {
        if (step.isWaiting() && mandatoryDependenciesSucceeded(step)) {
            step.markReady();
            result.add(step);
        }
    }
    return Collections.unmodifiableList(result);
}

Implement mandatory dependency failure as FAILED, optional branch exclusion as SKIPPED, and these exact transitions: WAITING -> READY/BLOCKED/SKIPPED/CANCELLED/FAILED; BLOCKED -> READY/CANCELLED/FAILED; READY -> ASSIGNED/BLOCKED/CANCELLED/FAILED; ASSIGNED -> RUNNING/SUSPENDED/CANCELLED/FAILED; RUNNING -> SUCCEEDED/SUSPENDED/CANCELLED/FAILED; SUSPENDED -> ASSIGNED/RUNNING/CANCELLED/FAILED. SKIPPED/CANCELLED/FAILED/SUCCEEDED are terminal.

  • Step 4: Run GREEN

Run: mvn -pl ruoyi-wcs -am -Dtest=WorkflowInstanceTest,TransportOrderTest test

  • Step 5: Commit
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WorkflowDefinitionProvider.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task
git commit -m "feat(wcs): add workflow and task step model"

Task 5: Add standard order use cases and idempotent application service

Files:

  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/in/TransportOrderUseCase.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task/CreateTransportOrderCommand.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task/TransportOrderResult.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task/TransportOrderApplicationService.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/TransportOrderRepository.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DomainEventPublisher.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WmsNotificationGateway.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WmsNotification.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WmsNotificationReceipt.java
  • Create: ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/task/TransportOrderApplicationServiceTest.java

Interfaces:

  • Produces: create(CreateTransportOrderCommand), cancel(String externalRequestId), suspend(String orderId,String reason), resume(String orderId).

  • Consumes: TransportOrderRepository.findByExternalRequestId/save.

  • Consumes: WorkflowDefinitionProvider and DomainEventPublisher.publish(List<DomainEvent>).

  • Produces outbound port WmsNotificationGateway.notify(WmsNotification): WmsNotificationReceipt; later plans deliver it through a core-owned Outbox.

  • WmsNotification contains eventId, externalRequestId, orderId, occurredAt, eventType, resultCode, resultMessage, and typed context fields for OrderAccepted, OrderStarted, OrderCompleted, OrderFailed, OrderCancelled, and OrderSuspended.

  • Step 1: Write failing duplicate-request test

@Test
void returnsTheExistingOrderForTheSameExternalRequest() {
    TransportOrderResult first = service.create(command("REQ-1"));
    TransportOrderResult second = service.create(command("REQ-1"));
    assertEquals(first.getOrderId(), second.getOrderId());
    assertEquals(1, repository.count());
}
  • Step 2: Run RED

Run: mvn -pl ruoyi-wcs -am -Dtest=TransportOrderApplicationServiceTest test

  • Step 3: Implement transactional orchestration
@Transactional
public TransportOrderResult create(CreateTransportOrderCommand command) {
    TransportOrder existing = repository.findByExternalRequestId(command.getExternalRequestId());
    if (existing != null) {
        return TransportOrderResult.from(existing);
    }
    TransportOrder order = TransportOrder.accept(command, idGenerator.nextId(), clock.now());
    WorkflowDefinition definition = workflowDefinitions.definitionFor(order.getType());
    WorkflowInstance workflow = WorkflowInstance.create(idGenerator.nextId(), order.getOrderId(), definition);
    order.plan(workflow.getWorkflowId());
    repository.save(order, workflow);
    eventPublisher.publish(order.getUncommittedEvents());
    order.clearUncommittedEvents();
    return TransportOrderResult.from(order);
}

Implement cancel/suspend/resume using domain methods; do not expose repository entities in results.

Map order domain events to immutable WmsNotification values without calling the gateway in the order transaction. WmsNotificationReceipt exposes DELIVERED, RETRYABLE_FAILURE, and PERMANENT_FAILURE; the notification eventId is always the external idempotency key.

  • Step 4: Run GREEN

Run: mvn -pl ruoyi-wcs -am -Dtest='TransportOrder*Test,WorkflowInstanceTest' test

  • Step 5: Commit
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task ruoyi-wcs/src/main/java/com/ruoyi/wcs/port ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/task
git commit -m "feat(wcs): add idempotent transport order use cases"

Task 6: Add the foundation SQL schema and MyBatis repository

Files:

  • Create: sql/wcs/V001__wcs_task_foundation.sql
  • Create: sql/wcs/verify_V001__wcs_task_foundation.sql
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRow.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/WorkflowRow.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskStepRow.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapper.java
  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepository.java
  • Create: ruoyi-wcs/src/main/resources/mapper/wcs/task/TransportOrderMapper.xml
  • Create: ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRowMapperTest.java

Interfaces:

  • Implements: TransportOrderRepository from Task 5.

  • Produces: unique constraint uk_wcs_transport_order_external_request.

  • Produces: optimistic state_version on order, workflow, and step rows.

  • Step 1: Write a failing domain-row round-trip test

@Test
void preservesOrderStateAndVersionAcrossPersistenceMapping() {
    TransportOrder source = executingOrder("ORDER-1", 4L);
    TransportOrderRow row = TransportOrderRow.fromDomain(source);
    TransportOrder restored = row.toDomain();
    assertEquals(source.getState(), restored.getState());
    assertEquals(4L, restored.getStateVersion());
}
  • Step 2: Run RED

Run: mvn -pl ruoyi-wcs -am -Dtest=TransportOrderRowMapperTest test

  • Step 3: Create idempotent SQL Server DDL

The first table must include this exact invariant:

IF OBJECT_ID('dbo.wcs_transport_order', 'U') IS NULL
BEGIN
    CREATE TABLE dbo.wcs_transport_order (
        order_id varchar(64) NOT NULL PRIMARY KEY,
        external_request_id varchar(128) NOT NULL,
        order_type varchar(32) NOT NULL,
        load_id varchar(128) NULL,
        source_code varchar(128) NOT NULL,
        destination_code varchar(128) NOT NULL,
        priority int NOT NULL,
        order_state varchar(32) NOT NULL,
        workflow_id varchar(64) NULL,
        state_version bigint NOT NULL,
        created_time datetime2(3) NOT NULL,
        updated_time datetime2(3) NOT NULL,
        CONSTRAINT uk_wcs_transport_order_external_request UNIQUE (external_request_id)
    );
END;

Add wcs_workflow_instance, wcs_task_step, wcs_task_dependency, and wcs_domain_event with named constraints and no default02-default08 columns.

  • Step 4: Implement mapping and optimistic updates

TransportOrderMapper.updateState must include WHERE order_id = #{orderId} AND state_version = #{expectedVersion} and increment state_version; repository throws OptimisticLockingFailureException when update count is 0.

  • Step 5: Run mapping and schema text tests GREEN

Run: mvn -pl ruoyi-wcs -am -Dtest=TransportOrderRowMapperTest test

Run the read-only verification script against the isolated WCS test database and expect one row per required table and unique constraint.

  • Step 6: Commit
git add sql/wcs ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task ruoyi-wcs/src/main/resources/mapper/wcs/task ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task
git commit -m "feat(wcs): persist transport orders and workflows"

Task 7: Wire the core module into RuoYi without changing legacy behavior

Files:

  • Create: ruoyi-wcs/src/main/java/com/ruoyi/wcs/WcsCoreConfiguration.java
  • Create: ruoyi-admin/src/test/java/com/ruoyi/wcs/WcsCoreWiringTest.java
  • Create: ruoyi-admin/src/test/java/com/ruoyi/wcs/TestPersistenceConfiguration.java
  • Create: ruoyi-admin/src/test/java/com/ruoyi/wcs/AdminWcsBoundaryArchitectureTest.java
  • Verify: ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java
  • Verify: ruoyi-admin/src/main/resources/application.yml

Interfaces:

  • Produces: Spring beans TransportOrderUseCase, TransportOrderRepository, WcsClock, and IdGenerator.

  • Does not change: ScheduleThread, BaseSRgvThread, Wcs2WmsController, or PLC writes.

  • Enforces: project/Admin packages may use com.ruoyi.wcs.port.in.., com.ruoyi.wcs.port.out.., and com.ruoyi.wcs.query.., but may not depend on com.ruoyi.wcs.infrastructure.persistence.. or concrete Repository implementations.

  • Step 1: Write a failing Spring wiring test

@Test
void exposesTheTransportOrderUseCaseFromTheWcsModule() {
    try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
        context.register(WcsCoreConfiguration.class, TestPersistenceConfiguration.class);
        context.refresh();
        assertNotNull(context.getBean(TransportOrderUseCase.class));
    }
}

Create the test-only dependency configuration explicitly:

@TestConfiguration
public class TestPersistenceConfiguration {
    @Bean
    public TransportOrderRepository transportOrderRepository() {
        return Mockito.mock(TransportOrderRepository.class);
    }

    @Bean
    public WorkflowDefinitionProvider workflowDefinitionProvider() {
        return Mockito.mock(WorkflowDefinitionProvider.class);
    }

    @Bean
    public DomainEventPublisher domainEventPublisher() {
        return Mockito.mock(DomainEventPublisher.class);
    }
}
  • Step 2: Run RED

Run: mvn -pl ruoyi-admin -am -Dtest=WcsCoreWiringTest test

  • Step 3: Add explicit core configuration
@Configuration
@ComponentScan(basePackages = "com.ruoyi.wcs")
public class WcsCoreConfiguration {
}

Do not alter @MapperScan("com.ruoyi.**.mapper"); add a test assertion that TransportOrderMapper is found under the existing wildcard scan.

Add an ArchUnit rule importing com.ruoyi.project.. and com.ruoyi.api.. that fails on dependencies to com.ruoyi.wcs.infrastructure.persistence..; allow only the public ports, application use cases, domain value types required by those ports, and query DTOs.

  • Step 4: Run the full foundation verification

Run: mvn -pl ruoyi-wcs,ruoyi-admin -am test

Run: git diff --check

Expected: tests pass, no PLC or scheduler source file is modified, and the module boundary test passes.

  • Step 5: Commit
git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/WcsCoreConfiguration.java ruoyi-admin/src/test/java/com/ruoyi/wcs
git commit -m "feat(wcs): wire core module into RuoYi"