22 KiB
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 oncom.ruoyi.schedule..,com.ruoyi.api..,HslCommunication.., ororg.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>andclearUncommittedEvents(). -
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/FAILEDonly after application-level command and resource convergence checks. -
REJECTED/COMPLETED/CANCELLED/FAILEDare 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>andonStepSucceeded(String stepId). -
TaskStepexposesmarkReady,block(reasonCode),unblock,assign(vehicleId,reservationIds),start,suspend(reason),resumeAssigned,resumeRunning,succeed,skip,cancel, andfail(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:
WorkflowDefinitionProviderandDomainEventPublisher.publish(List<DomainEvent>). -
Produces outbound port
WmsNotificationGateway.notify(WmsNotification): WmsNotificationReceipt; later plans deliver it through a core-owned Outbox. -
WmsNotificationcontainseventId,externalRequestId,orderId,occurredAt,eventType,resultCode,resultMessage, and typed context fields forOrderAccepted,OrderStarted,OrderCompleted,OrderFailed,OrderCancelled, andOrderSuspended. -
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:
TransportOrderRepositoryfrom Task 5. -
Produces: unique constraint
uk_wcs_transport_order_external_request. -
Produces: optimistic
state_versionon 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, andIdGenerator. -
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.., andcom.ruoyi.wcs.query.., but may not depend oncom.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"