From 4696cdb950c8b5c792baec25dc27a8c511ce6289 Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Mon, 10 Aug 2026 15:24:11 +0800 Subject: [PATCH] feat(wcs): persist transport orders and workflows --- .../ruoyi/wcs/domain/task/TransportOrder.java | 23 +++ .../wcs/domain/task/WorkflowInstance.java | 9 + .../task/MyBatisTransportOrderRepository.java | 76 ++++++++ .../persistence/task/TaskDependencyRow.java | 41 +++++ .../persistence/task/TaskStepRow.java | 101 ++++++++++ .../task/TransportOrderMapper.java | 23 +++ .../persistence/task/TransportOrderRow.java | 173 ++++++++++++++++++ .../persistence/task/WorkflowRow.java | 51 ++++++ .../mapper/wcs/task/TransportOrderMapper.xml | 91 +++++++++ .../MyBatisTransportOrderRepositoryTest.java | 116 ++++++++++++ .../task/TransportOrderMapperXmlTest.java | 33 ++++ .../task/TransportOrderRowMapperTest.java | 45 +++++ sql/wcs/V001__wcs_task_foundation.sql | 85 +++++++++ sql/wcs/verify_V001__wcs_task_foundation.sql | 19 ++ 14 files changed, 886 insertions(+) create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepository.java create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskDependencyRow.java create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskStepRow.java create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapper.java create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRow.java create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/WorkflowRow.java create mode 100644 ruoyi-wcs/src/main/resources/mapper/wcs/task/TransportOrderMapper.xml create mode 100644 ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepositoryTest.java create mode 100644 ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapperXmlTest.java create mode 100644 ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRowMapperTest.java create mode 100644 sql/wcs/V001__wcs_task_foundation.sql create mode 100644 sql/wcs/verify_V001__wcs_task_foundation.sql diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrder.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrder.java index 8833161..77b1452 100644 --- a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrder.java +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrder.java @@ -53,6 +53,29 @@ public final class TransportOrder { sourceCode, destinationCode, priority, createdAt); } + public static TransportOrder restore(String orderId, String externalRequestId, + TransportOrderType type, String loadId, + String sourceCode, String destinationCode, + int priority, TransportOrderState state, + String workflowId, String resultCode, + String stateReason, long stateVersion, + Instant createdAt, Instant updatedAt) { + if (stateVersion < 0L) { + throw new IllegalArgumentException("stateVersion must not be negative"); + } + TransportOrder order = new TransportOrder(orderId, externalRequestId, type, + loadId, sourceCode, destinationCode, priority, createdAt); + order.state = Objects.requireNonNull(state, "state"); + order.workflowId = workflowId; + order.resultCode = resultCode; + order.stateReason = stateReason; + order.stateVersion = stateVersion; + order.eventSequence = stateVersion + 1L; + order.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt"); + order.uncommittedEvents.clear(); + return order; + } + public void plan(String workflowId) { requireState(TransportOrderState.ACCEPTED); this.workflowId = requireText(workflowId, "workflowId"); diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowInstance.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowInstance.java index 45f029e..40fbc8c 100644 --- a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowInstance.java +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowInstance.java @@ -85,6 +85,15 @@ public final class WorkflowInstance { return Collections.unmodifiableList(new ArrayList(steps.values())); } + public Map> getMandatoryDependencies() { + Map> copy = new LinkedHashMap>(); + for (Map.Entry> entry : mandatoryDependencies.entrySet()) { + copy.put(entry.getKey(), Collections.unmodifiableList( + new ArrayList(entry.getValue()))); + } + return Collections.unmodifiableMap(copy); + } + private DependencyState dependencyState(String stepKey) { boolean allSucceeded = true; for (String dependencyKey : mandatoryDependencies.get(stepKey)) { diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepository.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepository.java new file mode 100644 index 0000000..e1e9e29 --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepository.java @@ -0,0 +1,76 @@ +package com.ruoyi.wcs.infrastructure.persistence.task; + +import com.ruoyi.wcs.domain.task.TaskStep; +import com.ruoyi.wcs.domain.task.TransportOrder; +import com.ruoyi.wcs.domain.task.WorkflowInstance; +import com.ruoyi.wcs.port.out.TransportOrderRepository; +import org.springframework.dao.OptimisticLockingFailureException; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public final class MyBatisTransportOrderRepository implements TransportOrderRepository { + private final TransportOrderMapper mapper; + + public MyBatisTransportOrderRepository(TransportOrderMapper mapper) { + this.mapper = Objects.requireNonNull(mapper, "mapper"); + } + + @Override + public TransportOrder findByExternalRequestId(String externalRequestId) { + TransportOrderRow row = mapper.selectByExternalRequestId(externalRequestId); + return row == null ? null : row.toDomain(); + } + + @Override + public TransportOrder findByOrderId(String orderId) { + TransportOrderRow row = mapper.selectByOrderId(orderId); + return row == null ? null : row.toDomain(); + } + + @Override + public void save(TransportOrder order, WorkflowInstance workflow) { + Objects.requireNonNull(order, "order"); + Objects.requireNonNull(workflow, "workflow"); + if (mapper.insertOrder(TransportOrderRow.fromDomain(order)) != 1) { + throw new IllegalStateException("transport order insert did not affect one row"); + } + if (mapper.insertWorkflow(WorkflowRow.fromDomain(workflow)) != 1) { + throw new IllegalStateException("workflow insert did not affect one row"); + } + for (TaskStep step : workflow.getSteps()) { + if (mapper.insertTaskStep(TaskStepRow.fromDomain( + workflow.getWorkflowId(), step)) != 1) { + throw new IllegalStateException("task step insert did not affect one row"); + } + } + for (Map.Entry> entry + : workflow.getMandatoryDependencies().entrySet()) { + for (String dependency : entry.getValue()) { + if (mapper.insertTaskDependency(new TaskDependencyRow( + workflow.getWorkflowId(), entry.getKey(), dependency)) != 1) { + throw new IllegalStateException( + "task dependency insert did not affect one row"); + } + } + } + } + + @Override + public void save(TransportOrder order) { + Objects.requireNonNull(order, "order"); + TransportOrderRow existing = mapper.selectByOrderId(order.getOrderId()); + if (existing == null) { + if (mapper.insertOrder(TransportOrderRow.fromDomain(order)) != 1) { + throw new IllegalStateException("transport order insert did not affect one row"); + } + return; + } + long expectedVersion = order.getStateVersion() - 1L; + if (mapper.updateState(TransportOrderRow.fromDomain(order), expectedVersion) != 1) { + throw new OptimisticLockingFailureException( + "transport order state changed concurrently: " + order.getOrderId()); + } + } +} diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskDependencyRow.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskDependencyRow.java new file mode 100644 index 0000000..e63bf81 --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskDependencyRow.java @@ -0,0 +1,41 @@ +package com.ruoyi.wcs.infrastructure.persistence.task; + +public final class TaskDependencyRow { + private String workflowId; + private String stepKey; + private String dependsOnStepKey; + + public TaskDependencyRow() { + } + + public TaskDependencyRow(String workflowId, String stepKey, + String dependsOnStepKey) { + this.workflowId = workflowId; + this.stepKey = stepKey; + this.dependsOnStepKey = dependsOnStepKey; + } + + public String getWorkflowId() { + return workflowId; + } + + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; + } + + public String getStepKey() { + return stepKey; + } + + public void setStepKey(String stepKey) { + this.stepKey = stepKey; + } + + public String getDependsOnStepKey() { + return dependsOnStepKey; + } + + public void setDependsOnStepKey(String dependsOnStepKey) { + this.dependsOnStepKey = dependsOnStepKey; + } +} diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskStepRow.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskStepRow.java new file mode 100644 index 0000000..220531f --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskStepRow.java @@ -0,0 +1,101 @@ +package com.ruoyi.wcs.infrastructure.persistence.task; + +import com.ruoyi.wcs.domain.task.TaskStep; + +public final class TaskStepRow { + private String stepId; + private String workflowId; + private String stepKey; + private String stepType; + private boolean required; + private String stepState; + private String reasonCode; + private String vehicleId; + private long stateVersion; + + public static TaskStepRow fromDomain(String workflowId, TaskStep step) { + TaskStepRow row = new TaskStepRow(); + row.stepId = step.getStepId(); + row.workflowId = workflowId; + row.stepKey = step.getStepKey(); + row.stepType = step.getType().name(); + row.required = step.isRequired(); + row.stepState = step.getState().name(); + row.reasonCode = step.getReasonCode(); + row.vehicleId = step.getVehicleId(); + row.stateVersion = step.getStateVersion(); + return row; + } + + public String getStepId() { + return stepId; + } + + public void setStepId(String stepId) { + this.stepId = stepId; + } + + public String getWorkflowId() { + return workflowId; + } + + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; + } + + public String getStepKey() { + return stepKey; + } + + public void setStepKey(String stepKey) { + this.stepKey = stepKey; + } + + public String getStepType() { + return stepType; + } + + public void setStepType(String stepType) { + this.stepType = stepType; + } + + public boolean isRequired() { + return required; + } + + public void setRequired(boolean required) { + this.required = required; + } + + public String getStepState() { + return stepState; + } + + public void setStepState(String stepState) { + this.stepState = stepState; + } + + public String getReasonCode() { + return reasonCode; + } + + public void setReasonCode(String reasonCode) { + this.reasonCode = reasonCode; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + public long getStateVersion() { + return stateVersion; + } + + public void setStateVersion(long stateVersion) { + this.stateVersion = stateVersion; + } +} diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapper.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapper.java new file mode 100644 index 0000000..ff45c14 --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapper.java @@ -0,0 +1,23 @@ +package com.ruoyi.wcs.infrastructure.persistence.task; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface TransportOrderMapper { + TransportOrderRow selectByExternalRequestId( + @Param("externalRequestId") String externalRequestId); + + TransportOrderRow selectByOrderId(@Param("orderId") String orderId); + + int insertOrder(TransportOrderRow row); + + int updateState(@Param("row") TransportOrderRow row, + @Param("expectedVersion") long expectedVersion); + + int insertWorkflow(WorkflowRow row); + + int insertTaskStep(TaskStepRow row); + + int insertTaskDependency(TaskDependencyRow row); +} diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRow.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRow.java new file mode 100644 index 0000000..37ac85a --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRow.java @@ -0,0 +1,173 @@ +package com.ruoyi.wcs.infrastructure.persistence.task; + +import com.ruoyi.wcs.domain.task.TransportOrder; +import com.ruoyi.wcs.domain.task.TransportOrderState; +import com.ruoyi.wcs.domain.task.TransportOrderType; + +import java.time.Instant; + +public final class TransportOrderRow { + private String orderId; + private String externalRequestId; + private String orderType; + private String loadId; + private String sourceCode; + private String destinationCode; + private int priority; + private String orderState; + private String workflowId; + private String resultCode; + private String stateReason; + private long stateVersion; + private Instant createdTime; + private Instant updatedTime; + + public static TransportOrderRow fromDomain(TransportOrder order) { + TransportOrderRow row = new TransportOrderRow(); + row.orderId = order.getOrderId(); + row.externalRequestId = order.getExternalRequestId(); + row.orderType = order.getType().name(); + row.loadId = order.getLoadId(); + row.sourceCode = order.getSourceCode(); + row.destinationCode = order.getDestinationCode(); + row.priority = order.getPriority(); + row.orderState = order.getState().name(); + row.workflowId = order.getWorkflowId(); + row.resultCode = order.getResultCode(); + row.stateReason = order.getStateReason(); + row.stateVersion = order.getStateVersion(); + row.createdTime = order.getCreatedAt(); + row.updatedTime = order.getUpdatedAt(); + return row; + } + + public TransportOrder toDomain() { + return TransportOrder.restore( + orderId, + externalRequestId, + TransportOrderType.valueOf(orderType), + loadId, + sourceCode, + destinationCode, + priority, + TransportOrderState.valueOf(orderState), + workflowId, + resultCode, + stateReason, + stateVersion, + createdTime, + updatedTime); + } + + public String getOrderId() { + return orderId; + } + + public void setOrderId(String orderId) { + this.orderId = orderId; + } + + public String getExternalRequestId() { + return externalRequestId; + } + + public void setExternalRequestId(String externalRequestId) { + this.externalRequestId = externalRequestId; + } + + public String getOrderType() { + return orderType; + } + + public void setOrderType(String orderType) { + this.orderType = orderType; + } + + public String getLoadId() { + return loadId; + } + + public void setLoadId(String loadId) { + this.loadId = loadId; + } + + public String getSourceCode() { + return sourceCode; + } + + public void setSourceCode(String sourceCode) { + this.sourceCode = sourceCode; + } + + public String getDestinationCode() { + return destinationCode; + } + + public void setDestinationCode(String destinationCode) { + this.destinationCode = destinationCode; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public String getOrderState() { + return orderState; + } + + public void setOrderState(String orderState) { + this.orderState = orderState; + } + + public String getWorkflowId() { + return workflowId; + } + + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; + } + + public String getResultCode() { + return resultCode; + } + + public void setResultCode(String resultCode) { + this.resultCode = resultCode; + } + + public String getStateReason() { + return stateReason; + } + + public void setStateReason(String stateReason) { + this.stateReason = stateReason; + } + + public long getStateVersion() { + return stateVersion; + } + + public void setStateVersion(long stateVersion) { + this.stateVersion = stateVersion; + } + + public Instant getCreatedTime() { + return createdTime; + } + + public void setCreatedTime(Instant createdTime) { + this.createdTime = createdTime; + } + + public Instant getUpdatedTime() { + return updatedTime; + } + + public void setUpdatedTime(Instant updatedTime) { + this.updatedTime = updatedTime; + } +} diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/WorkflowRow.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/WorkflowRow.java new file mode 100644 index 0000000..8640e64 --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/WorkflowRow.java @@ -0,0 +1,51 @@ +package com.ruoyi.wcs.infrastructure.persistence.task; + +import com.ruoyi.wcs.domain.task.WorkflowInstance; + +public final class WorkflowRow { + private String workflowId; + private String orderId; + private String workflowState; + private long stateVersion; + + public static WorkflowRow fromDomain(WorkflowInstance workflow) { + WorkflowRow row = new WorkflowRow(); + row.workflowId = workflow.getWorkflowId(); + row.orderId = workflow.getOrderId(); + row.workflowState = "ACTIVE"; + row.stateVersion = 0L; + return row; + } + + public String getWorkflowId() { + return workflowId; + } + + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; + } + + public String getOrderId() { + return orderId; + } + + public void setOrderId(String orderId) { + this.orderId = orderId; + } + + public String getWorkflowState() { + return workflowState; + } + + public void setWorkflowState(String workflowState) { + this.workflowState = workflowState; + } + + public long getStateVersion() { + return stateVersion; + } + + public void setStateVersion(long stateVersion) { + this.stateVersion = stateVersion; + } +} diff --git a/ruoyi-wcs/src/main/resources/mapper/wcs/task/TransportOrderMapper.xml b/ruoyi-wcs/src/main/resources/mapper/wcs/task/TransportOrderMapper.xml new file mode 100644 index 0000000..105dc9c --- /dev/null +++ b/ruoyi-wcs/src/main/resources/mapper/wcs/task/TransportOrderMapper.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + order_id, external_request_id, order_type, load_id, source_code, + destination_code, priority, order_state, workflow_id, result_code, + state_reason, state_version, created_time, updated_time + + + + + + + + INSERT INTO dbo.wcs_transport_order ( + order_id, external_request_id, order_type, load_id, source_code, + destination_code, priority, order_state, workflow_id, result_code, + state_reason, state_version, created_time, updated_time + ) VALUES ( + #{orderId}, #{externalRequestId}, #{orderType}, #{loadId}, #{sourceCode}, + #{destinationCode}, #{priority}, #{orderState}, #{workflowId}, #{resultCode}, + #{stateReason}, #{stateVersion}, #{createdTime}, #{updatedTime} + ) + + + + UPDATE dbo.wcs_transport_order + SET order_state = #{row.orderState}, + workflow_id = #{row.workflowId}, + result_code = #{row.resultCode}, + state_reason = #{row.stateReason}, + state_version = #{row.stateVersion}, + updated_time = #{row.updatedTime} + WHERE order_id = #{row.orderId} + AND state_version = #{expectedVersion} + + + + INSERT INTO dbo.wcs_workflow_instance ( + workflow_id, order_id, workflow_state, state_version + ) VALUES ( + #{workflowId}, #{orderId}, #{workflowState}, #{stateVersion} + ) + + + + INSERT INTO dbo.wcs_task_step ( + step_id, workflow_id, step_key, step_type, required_flag, + step_state, reason_code, vehicle_id, state_version + ) VALUES ( + #{stepId}, #{workflowId}, #{stepKey}, #{stepType}, #{required}, + #{stepState}, #{reasonCode}, #{vehicleId}, #{stateVersion} + ) + + + + INSERT INTO dbo.wcs_task_dependency ( + workflow_id, step_key, depends_on_step_key + ) VALUES ( + #{workflowId}, #{stepKey}, #{dependsOnStepKey} + ) + + diff --git a/ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepositoryTest.java b/ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepositoryTest.java new file mode 100644 index 0000000..40dbf17 --- /dev/null +++ b/ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepositoryTest.java @@ -0,0 +1,116 @@ +package com.ruoyi.wcs.infrastructure.persistence.task; + +import com.ruoyi.wcs.domain.task.TaskStepType; +import com.ruoyi.wcs.domain.task.TransportOrder; +import com.ruoyi.wcs.domain.task.TransportOrderType; +import com.ruoyi.wcs.domain.task.WorkflowDefinition; +import com.ruoyi.wcs.domain.task.WorkflowInstance; +import com.ruoyi.wcs.domain.task.WorkflowStepDefinition; +import org.junit.jupiter.api.Test; +import org.springframework.dao.OptimisticLockingFailureException; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MyBatisTransportOrderRepositoryTest { + + @Test + void rejectsAStateUpdateWhenThePersistedVersionChanged() { + RecordingMapper mapper = new RecordingMapper(); + TransportOrder order = order(); + order.plan("WF-1"); + mapper.selected = TransportOrderRow.fromDomain(order); + order.start(); + mapper.updateCount = 0; + + MyBatisTransportOrderRepository repository = + new MyBatisTransportOrderRepository(mapper); + + assertThrows(OptimisticLockingFailureException.class, + () -> repository.save(order)); + assertEquals(1L, mapper.expectedVersion); + } + + @Test + void insertsANewOrderWorkflowAndItsInitialSteps() { + RecordingMapper mapper = new RecordingMapper(); + TransportOrder order = order(); + WorkflowDefinition definition = new WorkflowDefinition( + TransportOrderType.INBOUND, + Arrays.asList( + new WorkflowStepDefinition("MOVE", TaskStepType.MOVE, + true, true, Collections.emptyList()), + new WorkflowStepDefinition("DROP", TaskStepType.UNLOAD, + true, true, Collections.singletonList("MOVE")))); + WorkflowInstance workflow = WorkflowInstance.create("WF-1", "ORDER-1", definition); + order.plan(workflow.getWorkflowId()); + + new MyBatisTransportOrderRepository(mapper).save(order, workflow); + + assertEquals(1, mapper.orderInserts); + assertEquals(1, mapper.workflowInserts); + assertEquals(2, mapper.stepInserts); + assertEquals(1, mapper.dependencyInserts); + } + + private TransportOrder order() { + return TransportOrder.accept( + "ORDER-1", "REQ-1", TransportOrderType.INBOUND, + "LOAD-1", "PORT-1", "SPACE-1", 50, + Instant.parse("2026-08-04T00:00:00Z")); + } + + private static final class RecordingMapper implements TransportOrderMapper { + private TransportOrderRow selected; + private int updateCount = 1; + private long expectedVersion = -1L; + private int orderInserts; + private int workflowInserts; + private int stepInserts; + private int dependencyInserts; + + @Override + public TransportOrderRow selectByExternalRequestId(String externalRequestId) { + return selected; + } + + @Override + public TransportOrderRow selectByOrderId(String orderId) { + return selected; + } + + @Override + public int insertOrder(TransportOrderRow row) { + orderInserts++; + return 1; + } + + @Override + public int updateState(TransportOrderRow row, long expectedVersion) { + this.expectedVersion = expectedVersion; + return updateCount; + } + + @Override + public int insertWorkflow(WorkflowRow row) { + workflowInserts++; + return 1; + } + + @Override + public int insertTaskStep(TaskStepRow row) { + stepInserts++; + return 1; + } + + @Override + public int insertTaskDependency(TaskDependencyRow row) { + dependencyInserts++; + return 1; + } + } +} diff --git a/ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapperXmlTest.java b/ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapperXmlTest.java new file mode 100644 index 0000000..ded8c18 --- /dev/null +++ b/ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapperXmlTest.java @@ -0,0 +1,33 @@ +package com.ruoyi.wcs.infrastructure.persistence.task; + +import org.apache.ibatis.builder.xml.XMLMapperBuilder; +import org.apache.ibatis.session.Configuration; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TransportOrderMapperXmlTest { + + @Test + void registersAllFoundationPersistenceStatements() { + String resource = "mapper/wcs/task/TransportOrderMapper.xml"; + InputStream input = getClass().getClassLoader().getResourceAsStream(resource); + assertNotNull(input, "missing " + resource); + + Configuration configuration = new Configuration(); + XMLMapperBuilder builder = new XMLMapperBuilder( + input, configuration, resource, configuration.getSqlFragments()); + builder.parse(); + + String namespace = TransportOrderMapper.class.getName() + "."; + assertTrue(configuration.hasStatement(namespace + "selectByExternalRequestId")); + assertTrue(configuration.hasStatement(namespace + "insertOrder")); + assertTrue(configuration.hasStatement(namespace + "updateState")); + assertTrue(configuration.hasStatement(namespace + "insertWorkflow")); + assertTrue(configuration.hasStatement(namespace + "insertTaskStep")); + assertTrue(configuration.hasStatement(namespace + "insertTaskDependency")); + } +} diff --git a/ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRowMapperTest.java b/ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRowMapperTest.java new file mode 100644 index 0000000..d20eb2d --- /dev/null +++ b/ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRowMapperTest.java @@ -0,0 +1,45 @@ +package com.ruoyi.wcs.infrastructure.persistence.task; + +import com.ruoyi.wcs.domain.task.TransportOrder; +import com.ruoyi.wcs.domain.task.TransportOrderType; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TransportOrderRowMapperTest { + + @Test + void preservesOrderStateAndVersionAcrossPersistenceMapping() { + TransportOrder source = TransportOrder.accept( + "ORDER-1", "REQ-1", TransportOrderType.OUTBOUND, + "LOAD-1", "SPACE-1", "PORT-1", 80, + Instant.parse("2026-08-04T00:00:00Z")); + source.plan("WF-1"); + source.start(); + source.clearUncommittedEvents(); + + TransportOrderRow row = TransportOrderRow.fromDomain(source); + TransportOrder restored = row.toDomain(); + + assertEquals(source.getState(), restored.getState()); + assertEquals(2L, restored.getStateVersion()); + assertEquals("WF-1", restored.getWorkflowId()); + assertTrue(restored.getUncommittedEvents().isEmpty()); + } + + @Test + void mapsNullableLoadAndResultFieldsWithoutInventingValues() { + TransportOrder source = TransportOrder.accept( + "ORDER-2", "REQ-2", TransportOrderType.INVENTORY, + null, "SPACE-2", "SPACE-2", 10, + Instant.parse("2026-08-04T00:00:00Z")); + + TransportOrder restored = TransportOrderRow.fromDomain(source).toDomain(); + + assertEquals(null, restored.getLoadId()); + assertEquals(null, restored.getResultCode()); + } +} diff --git a/sql/wcs/V001__wcs_task_foundation.sql b/sql/wcs/V001__wcs_task_foundation.sql new file mode 100644 index 0000000..148deb3 --- /dev/null +++ b/sql/wcs/V001__wcs_task_foundation.sql @@ -0,0 +1,85 @@ +SET XACT_ABORT ON; +BEGIN TRANSACTION; + +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, + result_code varchar(64) NULL, + state_reason nvarchar(512) 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; + +IF OBJECT_ID('dbo.wcs_workflow_instance', 'U') IS NULL +BEGIN + CREATE TABLE dbo.wcs_workflow_instance ( + workflow_id varchar(64) NOT NULL PRIMARY KEY, + order_id varchar(64) NOT NULL, + workflow_state varchar(32) NOT NULL, + state_version bigint NOT NULL, + CONSTRAINT fk_wcs_workflow_order FOREIGN KEY (order_id) + REFERENCES dbo.wcs_transport_order(order_id), + CONSTRAINT uk_wcs_workflow_order UNIQUE (order_id) + ); +END; + +IF OBJECT_ID('dbo.wcs_task_step', 'U') IS NULL +BEGIN + CREATE TABLE dbo.wcs_task_step ( + step_id varchar(128) NOT NULL PRIMARY KEY, + workflow_id varchar(64) NOT NULL, + step_key varchar(64) NOT NULL, + step_type varchar(32) NOT NULL, + required_flag bit NOT NULL, + step_state varchar(32) NOT NULL, + reason_code varchar(128) NULL, + vehicle_id varchar(64) NULL, + state_version bigint NOT NULL, + CONSTRAINT fk_wcs_task_step_workflow FOREIGN KEY (workflow_id) + REFERENCES dbo.wcs_workflow_instance(workflow_id), + CONSTRAINT uk_wcs_task_step_key UNIQUE (workflow_id, step_key) + ); +END; + +IF OBJECT_ID('dbo.wcs_task_dependency', 'U') IS NULL +BEGIN + CREATE TABLE dbo.wcs_task_dependency ( + workflow_id varchar(64) NOT NULL, + step_key varchar(64) NOT NULL, + depends_on_step_key varchar(64) NOT NULL, + CONSTRAINT pk_wcs_task_dependency PRIMARY KEY ( + workflow_id, step_key, depends_on_step_key), + CONSTRAINT fk_wcs_task_dependency_workflow FOREIGN KEY (workflow_id) + REFERENCES dbo.wcs_workflow_instance(workflow_id) + ); +END; + +IF OBJECT_ID('dbo.wcs_domain_event', 'U') IS NULL +BEGIN + CREATE TABLE dbo.wcs_domain_event ( + event_id varchar(128) NOT NULL PRIMARY KEY, + aggregate_id varchar(64) NOT NULL, + event_sequence bigint NOT NULL, + event_type varchar(64) NOT NULL, + occurred_time datetime2(3) NOT NULL, + payload_json nvarchar(max) NULL, + published_flag bit NOT NULL CONSTRAINT df_wcs_domain_event_published DEFAULT (0), + CONSTRAINT uk_wcs_domain_event_sequence + UNIQUE (aggregate_id, event_sequence) + ); +END; + +COMMIT TRANSACTION; diff --git a/sql/wcs/verify_V001__wcs_task_foundation.sql b/sql/wcs/verify_V001__wcs_task_foundation.sql new file mode 100644 index 0000000..7ab3f6b --- /dev/null +++ b/sql/wcs/verify_V001__wcs_task_foundation.sql @@ -0,0 +1,19 @@ +SET NOCOUNT ON; + +SELECT name AS table_name +FROM sys.tables +WHERE name IN ( + 'wcs_transport_order', + 'wcs_workflow_instance', + 'wcs_task_step', + 'wcs_task_dependency', + 'wcs_domain_event' +) +ORDER BY name; + +SELECT i.name AS unique_constraint_name +FROM sys.indexes i +JOIN sys.tables t ON t.object_id = i.object_id +WHERE t.name = 'wcs_transport_order' + AND i.name = 'uk_wcs_transport_order_external_request' + AND i.is_unique = 1;