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,54 @@
package com.ruoyi.wcs.query;
import java.time.Instant;
import java.util.Objects;
public final class DeviceOperationsView {
private final String deviceId;
private final String deviceType;
private final String currentCommandKey;
private final String commandState;
private final String currentSite;
private final boolean connected;
private final String activeAlarmCode;
private final String recoveryCaseId;
private final Instant updatedAt;
public DeviceOperationsView(String deviceId, String deviceType,
String currentCommandKey, String commandState,
String currentSite, boolean connected,
String activeAlarmCode, String recoveryCaseId,
Instant updatedAt) {
this.deviceId = requireText(deviceId, "deviceId");
this.deviceType = requireText(deviceType, "deviceType");
this.currentCommandKey = optional(currentCommandKey);
this.commandState = optional(commandState);
this.currentSite = optional(currentSite);
this.connected = connected;
this.activeAlarmCode = optional(activeAlarmCode);
this.recoveryCaseId = optional(recoveryCaseId);
this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt");
}
public String getDeviceId() { return deviceId; }
public String getDeviceType() { return deviceType; }
public String getCurrentCommandKey() { return currentCommandKey; }
public String getCommandState() { return commandState; }
public String getCurrentSite() { return currentSite; }
public boolean isConnected() { return connected; }
public String getActiveAlarmCode() { return activeAlarmCode; }
public String getRecoveryCaseId() { return recoveryCaseId; }
public Instant getUpdatedAt() { return updatedAt; }
private static String optional(String value) {
return value == null || value.trim().isEmpty() ? null : value.trim();
}
private static String requireText(String value, String field) {
String text = optional(value);
if (text == null) {
throw new IllegalArgumentException(field + " must not be blank");
}
return text;
}
}

View File

@@ -0,0 +1,50 @@
package com.ruoyi.wcs.query;
import java.time.Instant;
import java.util.Objects;
public final class OrderExecutionView {
private final String orderId;
private final String workflowId;
private final String stepId;
private final String deviceId;
private final String currentCommandKey;
private final String commandState;
private final String recoveryCaseId;
private final Instant updatedAt;
public OrderExecutionView(String orderId, String workflowId, String stepId,
String deviceId, String currentCommandKey,
String commandState, String recoveryCaseId,
Instant updatedAt) {
this.orderId = requireText(orderId, "orderId");
this.workflowId = optional(workflowId);
this.stepId = optional(stepId);
this.deviceId = optional(deviceId);
this.currentCommandKey = optional(currentCommandKey);
this.commandState = optional(commandState);
this.recoveryCaseId = optional(recoveryCaseId);
this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt");
}
public String getOrderId() { return orderId; }
public String getWorkflowId() { return workflowId; }
public String getStepId() { return stepId; }
public String getDeviceId() { return deviceId; }
public String getCurrentCommandKey() { return currentCommandKey; }
public String getCommandState() { return commandState; }
public String getRecoveryCaseId() { return recoveryCaseId; }
public Instant getUpdatedAt() { return updatedAt; }
private static String optional(String value) {
return value == null || value.trim().isEmpty() ? null : value.trim();
}
private static String requireText(String value, String field) {
String text = optional(value);
if (text == null) {
throw new IllegalArgumentException(field + " must not be blank");
}
return text;
}
}

View File

@@ -0,0 +1,49 @@
package com.ruoyi.wcs.query;
import java.time.Instant;
import java.util.Objects;
public final class RecoveryCaseView {
private final String caseId;
private final String deviceId;
private final String commandKey;
private final String state;
private final String reason;
private final Instant createdAt;
private final String resolvedBy;
private final Instant resolvedAt;
public RecoveryCaseView(String caseId, String deviceId, String commandKey,
String state, String reason, Instant createdAt,
String resolvedBy, Instant resolvedAt) {
this.caseId = requireText(caseId, "caseId");
this.deviceId = requireText(deviceId, "deviceId");
this.commandKey = requireText(commandKey, "commandKey");
this.state = requireText(state, "state");
this.reason = requireText(reason, "reason");
this.createdAt = Objects.requireNonNull(createdAt, "createdAt");
this.resolvedBy = optional(resolvedBy);
this.resolvedAt = resolvedAt;
}
public String getCaseId() { return caseId; }
public String getDeviceId() { return deviceId; }
public String getCommandKey() { return commandKey; }
public String getState() { return state; }
public String getReason() { return reason; }
public Instant getCreatedAt() { return createdAt; }
public String getResolvedBy() { return resolvedBy; }
public Instant getResolvedAt() { return resolvedAt; }
private static String optional(String value) {
return value == null || value.trim().isEmpty() ? null : value.trim();
}
private static String requireText(String value, String field) {
String text = optional(value);
if (text == null) {
throw new IllegalArgumentException(field + " must not be blank");
}
return text;
}
}

View File

@@ -0,0 +1,54 @@
package com.ruoyi.wcs.query;
import java.time.Instant;
import java.util.Objects;
public final class ResourceOperationsView {
private final String resourceKey;
private final String reservationState;
private final String orderId;
private final String stepId;
private final String vehicleId;
private final long fencingToken;
private final String releaseEvidenceId;
private final Instant updatedAt;
public ResourceOperationsView(String resourceKey, String reservationState,
String orderId, String stepId,
String vehicleId, long fencingToken,
String releaseEvidenceId, Instant updatedAt) {
this.resourceKey = requireText(resourceKey, "resourceKey");
this.reservationState = requireText(reservationState,
"reservationState");
this.orderId = requireText(orderId, "orderId");
this.stepId = requireText(stepId, "stepId");
this.vehicleId = requireText(vehicleId, "vehicleId");
if (fencingToken <= 0L) {
throw new IllegalArgumentException("fencingToken must be positive");
}
this.fencingToken = fencingToken;
this.releaseEvidenceId = optional(releaseEvidenceId);
this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt");
}
public String getResourceKey() { return resourceKey; }
public String getReservationState() { return reservationState; }
public String getOrderId() { return orderId; }
public String getStepId() { return stepId; }
public String getVehicleId() { return vehicleId; }
public long getFencingToken() { return fencingToken; }
public String getReleaseEvidenceId() { return releaseEvidenceId; }
public Instant getUpdatedAt() { return updatedAt; }
private static String optional(String value) {
return value == null || value.trim().isEmpty() ? null : value.trim();
}
private static String requireText(String value, String field) {
String text = optional(value);
if (text == null) {
throw new IllegalArgumentException(field + " must not be blank");
}
return text;
}
}

View File

@@ -0,0 +1,75 @@
package com.ruoyi.wcs.query;
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public final class WcsOperationsQueryService {
private static final int MAX_PAGE_SIZE = 500;
private final WcsOperationsReadRepository repository;
public WcsOperationsQueryService(WcsOperationsReadRepository repository) {
this.repository = Objects.requireNonNull(repository, "repository");
}
public OrderExecutionView order(String orderId) {
String id = requireText(orderId, "orderId");
return requireResult(repository.findOrder(id), "order", id);
}
public OrderExecutionView command(String commandKey) {
String key = requireText(commandKey, "commandKey");
return requireResult(repository.findCommand(key), "command", key);
}
public DeviceOperationsView device(String deviceId) {
String id = requireText(deviceId, "deviceId");
return requireResult(repository.findDevice(id), "device", id);
}
public ResourceOperationsView resource(String resourceKey) {
String key = requireText(resourceKey, "resourceKey");
return requireResult(repository.findResource(key), "resource", key);
}
public RecoveryCaseView recoveryCase(String caseId) {
String id = requireText(caseId, "caseId");
return requireResult(repository.findRecoveryCase(id),
"recovery case", id);
}
public List<RecoveryCaseView> recoveryCases(String state, int offset,
int limit) {
String recoveryState = requireText(state, "state");
if (offset < 0) {
throw new IllegalArgumentException("offset must not be negative");
}
if (limit <= 0 || limit > MAX_PAGE_SIZE) {
throw new IllegalArgumentException(
"limit must be between 1 and " + MAX_PAGE_SIZE);
}
List<RecoveryCaseView> views = Objects.requireNonNull(
repository.findRecoveryCases(recoveryState, offset, limit),
"recovery case page");
return Collections.unmodifiableList(
new ArrayList<RecoveryCaseView>(views));
}
private static <T> T requireResult(T value, String type, String key) {
if (value == null) {
throw new DomainRuleViolation(type + " not found: " + key);
}
return value;
}
private static String requireText(String value, String field) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return value.trim();
}
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.wcs.query;
import java.util.List;
public interface WcsOperationsReadRepository {
OrderExecutionView findOrder(String orderId);
OrderExecutionView findCommand(String commandKey);
DeviceOperationsView findDevice(String deviceId);
ResourceOperationsView findResource(String resourceKey);
RecoveryCaseView findRecoveryCase(String caseId);
List<RecoveryCaseView> findRecoveryCases(String state, int offset,
int limit);
}

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);
}
}
}

View File

@@ -0,0 +1,341 @@
SET XACT_ABORT ON;
BEGIN TRANSACTION;
IF OBJECT_ID('dbo.wcs_control_lease', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_control_lease (
lease_name varchar(64) NOT NULL PRIMARY KEY,
owner_id varchar(128) NULL,
fencing_token bigint NOT NULL,
lease_until datetime2(3) NULL,
state_version bigint NOT NULL,
CONSTRAINT ck_wcs_control_lease_fencing CHECK (fencing_token >= 0),
CONSTRAINT ck_wcs_control_lease_version CHECK (state_version >= 0)
);
END;
IF OBJECT_ID('dbo.wcs_device_command', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_device_command (
command_id varchar(128) NOT NULL PRIMARY KEY,
command_key varchar(128) NOT NULL,
order_id varchar(64) NULL,
step_id varchar(128) NULL,
device_id varchar(64) NOT NULL,
command_type varchar(64) NOT NULL,
payload_json nvarchar(max) NOT NULL,
transport_attempt int NOT NULL,
command_state varchar(32) NOT NULL,
send_receipt_id varchar(128) NULL,
acknowledge_evidence_id varchar(128) NULL,
completion_evidence_id varchar(128) NULL,
status_reason nvarchar(512) NULL,
created_time datetime2(3) NOT NULL,
updated_time datetime2(3) NOT NULL,
state_version bigint NOT NULL,
CONSTRAINT uk_wcs_device_command_key UNIQUE (command_key),
CONSTRAINT ck_wcs_device_command_attempt CHECK (transport_attempt >= 0),
CONSTRAINT ck_wcs_device_command_version CHECK (state_version >= 0)
);
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_device_command')
AND name = 'ix_wcs_device_command_device_state')
BEGIN
CREATE INDEX ix_wcs_device_command_device_state
ON dbo.wcs_device_command(device_id, command_state);
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_device_command')
AND name = 'ix_wcs_device_command_order_step')
BEGIN
CREATE INDEX ix_wcs_device_command_order_step
ON dbo.wcs_device_command(order_id, step_id);
END;
IF OBJECT_ID('dbo.wcs_device_execution', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_device_execution (
command_id varchar(128) NOT NULL PRIMARY KEY,
command_key varchar(128) NOT NULL,
device_id varchar(64) NOT NULL,
task_number varchar(128) NOT NULL,
route_nodes_json nvarchar(max) NOT NULL,
route_resources_json nvarchar(max) NOT NULL,
topology_version bigint NOT NULL,
fencing_token bigint NOT NULL,
expected_boot_id varchar(128) NOT NULL,
current_node_index int NOT NULL,
released_resource_count int NOT NULL,
last_device_sequence bigint NOT NULL,
active_flag bit NOT NULL
CONSTRAINT df_wcs_device_execution_active DEFAULT (1),
updated_time datetime2(3) NOT NULL,
state_version bigint NOT NULL,
CONSTRAINT fk_wcs_device_execution_command FOREIGN KEY (command_id)
REFERENCES dbo.wcs_device_command(command_id),
CONSTRAINT uk_wcs_device_execution_key UNIQUE (command_key),
CONSTRAINT ck_wcs_device_execution_topology CHECK (topology_version > 0),
CONSTRAINT ck_wcs_device_execution_fencing CHECK (fencing_token > 0),
CONSTRAINT ck_wcs_device_execution_indexes CHECK (
current_node_index >= 0 AND released_resource_count >= 0),
CONSTRAINT ck_wcs_device_execution_version CHECK (state_version >= 0)
);
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_device_execution')
AND name = 'uk_wcs_device_execution_active_device')
BEGIN
CREATE UNIQUE INDEX uk_wcs_device_execution_active_device
ON dbo.wcs_device_execution(device_id)
WHERE active_flag = 1;
END;
IF OBJECT_ID('dbo.wcs_command_outbox', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_command_outbox (
command_id varchar(128) NOT NULL PRIMARY KEY,
command_key varchar(128) NOT NULL,
payload_json nvarchar(max) NOT NULL,
topology_version bigint NOT NULL,
fencing_token bigint NOT NULL,
delivery_state varchar(32) NOT NULL,
attempt_count int NOT NULL,
next_attempt_time datetime2(3) NOT NULL,
claimed_by varchar(128) NULL,
claimed_time datetime2(3) NULL,
last_error nvarchar(1024) NULL,
delivered_time datetime2(3) NULL,
state_version bigint NOT NULL,
CONSTRAINT fk_wcs_command_outbox_command FOREIGN KEY (command_id)
REFERENCES dbo.wcs_device_command(command_id),
CONSTRAINT uk_wcs_command_outbox_key UNIQUE (command_key),
CONSTRAINT ck_wcs_command_outbox_attempt CHECK (attempt_count >= 0),
CONSTRAINT ck_wcs_command_outbox_topology CHECK (topology_version > 0),
CONSTRAINT ck_wcs_command_outbox_fencing CHECK (fencing_token > 0),
CONSTRAINT ck_wcs_command_outbox_version CHECK (state_version >= 0)
);
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_command_outbox')
AND name = 'ix_wcs_command_outbox_due')
BEGIN
CREATE INDEX ix_wcs_command_outbox_due
ON dbo.wcs_command_outbox(delivery_state, next_attempt_time);
END;
IF OBJECT_ID('dbo.wcs_wms_notification_outbox', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_wms_notification_outbox (
outbox_id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY,
event_id varchar(128) NOT NULL,
notification_type varchar(64) NOT NULL,
payload_json nvarchar(max) NOT NULL,
delivery_state varchar(32) NOT NULL,
attempt_count int NOT NULL,
next_attempt_time datetime2(3) NOT NULL,
claimed_by varchar(128) NULL,
claimed_time datetime2(3) NULL,
external_receipt varchar(256) NULL,
last_error nvarchar(1024) NULL,
created_time datetime2(3) NOT NULL,
delivered_time datetime2(3) NULL,
state_version bigint NOT NULL,
CONSTRAINT uk_wcs_wms_outbox_event UNIQUE (event_id),
CONSTRAINT ck_wcs_wms_outbox_attempt CHECK (attempt_count >= 0),
CONSTRAINT ck_wcs_wms_outbox_version CHECK (state_version >= 0)
);
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_wms_notification_outbox')
AND name = 'ix_wcs_wms_outbox_due')
BEGIN
CREATE INDEX ix_wcs_wms_outbox_due
ON dbo.wcs_wms_notification_outbox(
delivery_state, next_attempt_time);
END;
IF OBJECT_ID('dbo.wcs_device_snapshot', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_device_snapshot (
snapshot_id varchar(128) NOT NULL PRIMARY KEY,
device_id varchar(64) NOT NULL,
command_key varchar(128) NOT NULL,
task_number varchar(128) NOT NULL,
device_sequence bigint NOT NULL,
observed_time datetime2(3) NOT NULL,
connected_flag bit NOT NULL,
device_boot_id varchar(128) NOT NULL,
topology_version bigint NOT NULL,
fencing_token bigint NOT NULL,
current_node_id varchar(128) NOT NULL,
remaining_route_json nvarchar(max) NOT NULL,
command_completed_flag bit NOT NULL,
facts_json nvarchar(max) NULL,
received_time datetime2(3) NOT NULL,
CONSTRAINT ck_wcs_device_snapshot_sequence CHECK (device_sequence >= 0),
CONSTRAINT ck_wcs_device_snapshot_topology CHECK (topology_version > 0),
CONSTRAINT ck_wcs_device_snapshot_fencing CHECK (fencing_token > 0)
);
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_device_snapshot')
AND name = 'ix_wcs_device_snapshot_device_time')
BEGIN
CREATE INDEX ix_wcs_device_snapshot_device_time
ON dbo.wcs_device_snapshot(device_id, observed_time DESC);
END;
IF OBJECT_ID('dbo.wcs_device_event', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_device_event (
event_id varchar(128) NOT NULL PRIMARY KEY,
device_id varchar(64) NOT NULL,
command_key varchar(128) NOT NULL,
task_number varchar(128) NOT NULL,
device_sequence bigint NOT NULL,
event_type varchar(32) NOT NULL,
current_node_id varchar(128) NULL,
remaining_route_json nvarchar(max) NULL,
facts_json nvarchar(max) NULL,
device_time datetime2(3) NOT NULL,
received_time datetime2(3) NOT NULL,
CONSTRAINT ck_wcs_device_event_sequence CHECK (device_sequence >= 0)
);
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_device_event')
AND name = 'ix_wcs_device_event_device_sequence')
BEGIN
CREATE INDEX ix_wcs_device_event_device_sequence
ON dbo.wcs_device_event(device_id, device_sequence);
END;
IF OBJECT_ID('dbo.wcs_alarm', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_alarm (
alarm_id varchar(128) NOT NULL PRIMARY KEY,
device_id varchar(64) NULL,
command_key varchar(128) NULL,
recovery_case_id varchar(128) NULL,
alarm_code varchar(128) NOT NULL,
severity varchar(16) NOT NULL,
alarm_state varchar(32) NOT NULL,
detail nvarchar(1024) NULL,
source_event_id varchar(128) NULL,
opened_time datetime2(3) NOT NULL,
acknowledged_by varchar(128) NULL,
acknowledged_time datetime2(3) NULL,
closed_time datetime2(3) NULL,
state_version bigint NOT NULL,
CONSTRAINT ck_wcs_alarm_version CHECK (state_version >= 0)
);
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_alarm')
AND name = 'ix_wcs_alarm_device_state')
BEGIN
CREATE INDEX ix_wcs_alarm_device_state
ON dbo.wcs_alarm(device_id, alarm_state, opened_time DESC);
END;
IF OBJECT_ID('dbo.wcs_recovery_case', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_recovery_case (
case_id varchar(128) NOT NULL PRIMARY KEY,
device_id varchar(64) NOT NULL,
command_key varchar(128) NOT NULL,
reason_code varchar(128) NOT NULL,
evidence_ids_json nvarchar(max) NOT NULL,
expected_task_number varchar(128) NULL,
expected_site varchar(128) NULL,
expected_load_state varchar(32) NULL,
recovery_state varchar(32) NOT NULL,
created_time datetime2(3) NOT NULL,
resolved_by varchar(128) NULL,
resolved_time datetime2(3) NULL,
resolution_evidence_json nvarchar(max) NULL,
resolution_reason nvarchar(512) NULL,
state_version bigint NOT NULL,
CONSTRAINT ck_wcs_recovery_case_version CHECK (state_version >= 0),
CONSTRAINT ck_wcs_recovery_case_expected_facts CHECK (
(expected_task_number IS NULL AND expected_site IS NULL
AND expected_load_state IS NULL)
OR
(expected_task_number IS NOT NULL AND expected_site IS NOT NULL
AND expected_load_state IS NOT NULL))
);
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_recovery_case')
AND name = 'uk_wcs_recovery_case_open')
BEGIN
CREATE UNIQUE INDEX uk_wcs_recovery_case_open
ON dbo.wcs_recovery_case(device_id, command_key)
WHERE recovery_state = 'OPEN';
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_recovery_case')
AND name = 'ix_wcs_recovery_case_state_created')
BEGIN
CREATE INDEX ix_wcs_recovery_case_state_created
ON dbo.wcs_recovery_case(recovery_state, created_time);
END;
IF OBJECT_ID('dbo.wcs_manual_action_audit', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_manual_action_audit (
audit_id varchar(128) NOT NULL PRIMARY KEY,
case_id varchar(128) NOT NULL,
action_type varchar(32) NOT NULL,
operator_id varchar(128) NOT NULL,
action_reason nvarchar(512) NOT NULL,
before_state varchar(32) NOT NULL,
after_state varchar(32) NOT NULL,
evidence_json nvarchar(max) NULL,
occurred_time datetime2(3) NOT NULL,
CONSTRAINT fk_wcs_manual_action_case FOREIGN KEY (case_id)
REFERENCES dbo.wcs_recovery_case(case_id)
);
END;
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_manual_action_audit')
AND name = 'ix_wcs_manual_action_case_time')
BEGIN
CREATE INDEX ix_wcs_manual_action_case_time
ON dbo.wcs_manual_action_audit(case_id, occurred_time);
END;
IF OBJECT_ID('dbo.wcs_resource_reservation', 'U') IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE object_id = OBJECT_ID('dbo.wcs_resource_reservation')
AND name = 'ix_wcs_resource_reservation_resource_state')
BEGIN
CREATE INDEX ix_wcs_resource_reservation_resource_state
ON dbo.wcs_resource_reservation(resource_key, reservation_state);
END;
COMMIT TRANSACTION;

View File

@@ -0,0 +1,40 @@
SET NOCOUNT ON;
SELECT name AS table_name
FROM sys.tables
WHERE name IN (
'wcs_control_lease',
'wcs_device_command',
'wcs_device_execution',
'wcs_command_outbox',
'wcs_wms_notification_outbox',
'wcs_device_snapshot',
'wcs_device_event',
'wcs_alarm',
'wcs_recovery_case',
'wcs_manual_action_audit'
)
ORDER BY name;
SELECT i.name AS index_name, t.name AS table_name, i.is_unique
FROM sys.indexes i
JOIN sys.tables t ON t.object_id = i.object_id
WHERE i.name IN (
'uk_wcs_recovery_case_open',
'uk_wcs_wms_outbox_event',
'ix_wcs_device_command_device_state',
'ix_wcs_device_command_order_step',
'ix_wcs_resource_reservation_resource_state',
'ix_wcs_recovery_case_state_created'
)
ORDER BY i.name;
SELECT c.name AS constraint_name
FROM sys.check_constraints c
WHERE c.name IN (
'ck_wcs_control_lease_fencing',
'ck_wcs_device_execution_fencing',
'ck_wcs_command_outbox_fencing',
'ck_wcs_recovery_case_expected_facts'
)
ORDER BY c.name;