feat(wcs): persist and query execution recovery state

This commit is contained in:
zgw
2026-08-11 11:27:31 +08:00
parent 9782800b0b
commit 434cbe57b9
10 changed files with 858 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
package com.ruoyi.wcs.infrastructure.persistence.recovery;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ExecutionRecoveryMigrationTest {
@Test
void packagesExecutionRecoveryTablesAndIdempotencyIndexes() throws Exception {
String migration = readSql("V003__wcs_execution_and_recovery.sql");
assertContains(migration, "wcs_device_command");
assertContains(migration, "wcs_device_execution");
assertContains(migration, "wcs_command_outbox");
assertContains(migration, "wcs_wms_notification_outbox");
assertContains(migration, "wcs_device_snapshot");
assertContains(migration, "wcs_device_event");
assertContains(migration, "wcs_alarm");
assertContains(migration, "wcs_recovery_case");
assertContains(migration, "wcs_manual_action_audit");
assertContains(migration, "wcs_control_lease");
assertContains(migration, "uk_wcs_recovery_case_open");
assertContains(migration, "WHERE recovery_state = 'OPEN'");
assertContains(migration, "uk_wcs_wms_outbox_event");
assertContains(migration, "ix_wcs_device_command_order_step");
assertContains(migration, "ix_wcs_resource_reservation_resource_state");
}
@Test
void packagesTheExecutionRecoveryVerificationScript() throws Exception {
String verification = readSql(
"verify_V003__wcs_execution_and_recovery.sql");
assertContains(verification, "wcs_device_command");
assertContains(verification, "uk_wcs_recovery_case_open");
assertContains(verification, "uk_wcs_wms_outbox_event");
}
private static void assertContains(String sql, String token) {
assertTrue(sql.contains(token), "missing SQL token: " + token);
}
private static String readSql(String filename) throws Exception {
Path current = Paths.get(System.getProperty(
"maven.multiModuleProjectDirectory", "."))
.toAbsolutePath().normalize();
while (current != null) {
Path file = current.resolve(Paths.get("sql", "wcs", filename));
if (Files.isRegularFile(file)) {
return new String(Files.readAllBytes(file),
StandardCharsets.UTF_8);
}
current = current.getParent();
}
throw new java.nio.file.NoSuchFileException(filename);
}
}

View File

@@ -0,0 +1,114 @@
package com.ruoyi.wcs.query;
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class WcsOperationsQueryServiceTest {
@Test
void returnsCommandAndRecoveryCorrelationForAnOrder() {
WcsOperationsQueryService queries = new WcsOperationsQueryService(
new FakeReadRepository());
OrderExecutionView view = queries.order("ORDER-1");
assertEquals("CMD-1", view.getCurrentCommandKey());
assertEquals("RC-1", view.getRecoveryCaseId());
assertEquals("UNKNOWN", view.getCommandState());
assertEquals("VEH-1", view.getDeviceId());
assertEquals("ORDER-1", queries.command("CMD-1").getOrderId());
}
@Test
void exposesStableDeviceResourceAndRecoveryViews() {
WcsOperationsQueryService queries = new WcsOperationsQueryService(
new FakeReadRepository());
assertEquals("ALARM-1", queries.device("VEH-1").getActiveAlarmCode());
assertEquals(77L, queries.resource("NODE:A").getFencingToken());
assertEquals("TASK_NUMBER_MISMATCH",
queries.recoveryCase("RC-1").getReason());
}
@Test
void pagesRecoveryCasesByExplicitStateWithABoundedLimit() {
WcsOperationsQueryService queries = new WcsOperationsQueryService(
new FakeReadRepository());
List<RecoveryCaseView> page = queries.recoveryCases(
"OPEN", 0, 50);
assertEquals(Arrays.asList("RC-1", "RC-2"), Arrays.asList(
page.get(0).getCaseId(), page.get(1).getCaseId()));
assertThrows(UnsupportedOperationException.class,
() -> page.add(page.get(0)));
assertThrows(IllegalArgumentException.class,
() -> queries.recoveryCases("OPEN", 0, 501));
}
@Test
void missingOrBlankOperationsLookupFailsExplicitly() {
WcsOperationsQueryService queries = new WcsOperationsQueryService(
new FakeReadRepository());
assertThrows(IllegalArgumentException.class, () -> queries.order(" "));
assertThrows(DomainRuleViolation.class,
() -> queries.order("ORDER-MISSING"));
}
private static final class FakeReadRepository
implements WcsOperationsReadRepository {
@Override
public OrderExecutionView findOrder(String orderId) {
if (!"ORDER-1".equals(orderId)) {
return null;
}
return new OrderExecutionView("ORDER-1", "WF-1", "STEP-1",
"VEH-1", "CMD-1", "UNKNOWN", "RC-1", Instant.EPOCH);
}
@Override
public OrderExecutionView findCommand(String commandKey) {
return "CMD-1".equals(commandKey) ? findOrder("ORDER-1") : null;
}
@Override
public DeviceOperationsView findDevice(String deviceId) {
return new DeviceOperationsView("VEH-1", "RGV", "CMD-1",
"UNKNOWN", "NODE:A", true, "ALARM-1", "RC-1",
Instant.EPOCH);
}
@Override
public ResourceOperationsView findResource(String resourceKey) {
return new ResourceOperationsView("NODE:A", "OCCUPIED",
"ORDER-1", "STEP-1", "VEH-1", 77L, null,
Instant.EPOCH);
}
@Override
public RecoveryCaseView findRecoveryCase(String caseId) {
return recovery(caseId);
}
@Override
public List<RecoveryCaseView> findRecoveryCases(String state,
int offset,
int limit) {
return Arrays.asList(recovery("RC-1"), recovery("RC-2"));
}
private static RecoveryCaseView recovery(String caseId) {
return new RecoveryCaseView(caseId, "VEH-1", "CMD-1", "OPEN",
"TASK_NUMBER_MISMATCH", Instant.EPOCH, null, null);
}
}
}