feat(wcs): coordinate and persist traffic decisions

This commit is contained in:
zgw
2026-08-11 09:54:26 +08:00
parent 8b3713d182
commit 74f2de59cd
17 changed files with 1369 additions and 0 deletions

View File

@@ -34,5 +34,10 @@
<version>1.2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,82 @@
package com.ruoyi.wcs.application.dispatch;
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
import com.ruoyi.wcs.domain.traffic.WaitForGraph;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class DispatchCommit {
private final String warehouseCode;
private final long topologyVersion;
private final long snapshotVersion;
private final List<GrantedDispatch> granted;
private final List<HeldDispatch> held;
private final List<HeldDispatch> yielded;
private final List<DispatchDecisionEvent> decisionEvents;
private final ResourceLedger resultingLedger;
private final WaitForGraph resultingWaitForGraph;
public DispatchCommit(String warehouseCode,
long topologyVersion, long snapshotVersion,
List<GrantedDispatch> granted,
List<HeldDispatch> held,
List<HeldDispatch> yielded,
List<DispatchDecisionEvent> decisionEvents,
ResourceLedger resultingLedger,
WaitForGraph resultingWaitForGraph) {
if (warehouseCode == null || warehouseCode.trim().isEmpty()) {
throw new IllegalArgumentException("warehouseCode must not be blank");
}
this.warehouseCode = warehouseCode.trim();
this.topologyVersion = topologyVersion;
this.snapshotVersion = snapshotVersion;
this.granted = immutable(granted);
this.held = immutable(held);
this.yielded = immutable(yielded);
this.decisionEvents = immutable(decisionEvents);
this.resultingLedger = resultingLedger.copy();
this.resultingWaitForGraph = resultingWaitForGraph.copy();
}
public String getWarehouseCode() {
return warehouseCode;
}
public long getTopologyVersion() {
return topologyVersion;
}
public long getSnapshotVersion() {
return snapshotVersion;
}
public List<GrantedDispatch> getGranted() {
return granted;
}
public List<HeldDispatch> getHeld() {
return held;
}
public List<HeldDispatch> getYielded() {
return yielded;
}
public List<DispatchDecisionEvent> getDecisionEvents() {
return decisionEvents;
}
public ResourceLedger getResultingLedger() {
return resultingLedger.copy();
}
public WaitForGraph getResultingWaitForGraph() {
return resultingWaitForGraph.copy();
}
private static <T> List<T> immutable(List<T> source) {
return Collections.unmodifiableList(new ArrayList<T>(source));
}
}

View File

@@ -0,0 +1,187 @@
package com.ruoyi.wcs.application.dispatch;
import com.ruoyi.wcs.domain.dispatch.DispatchTask;
import com.ruoyi.wcs.domain.dispatch.VehicleAssignmentDecision;
import com.ruoyi.wcs.domain.dispatch.VehicleAssignmentPolicy;
import com.ruoyi.wcs.domain.equipment.VehicleSnapshot;
import com.ruoyi.wcs.domain.shared.IdGenerator;
import com.ruoyi.wcs.domain.shared.WcsClock;
import com.ruoyi.wcs.domain.traffic.DeadlockGuard;
import com.ruoyi.wcs.domain.traffic.ReservationDecision;
import com.ruoyi.wcs.domain.traffic.ReservationRequest;
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
import com.ruoyi.wcs.domain.traffic.TrafficCandidate;
import com.ruoyi.wcs.domain.traffic.TrafficDecision;
import com.ruoyi.wcs.domain.traffic.TrafficDecisionType;
import com.ruoyi.wcs.port.out.DispatchSnapshotProvider;
import com.ruoyi.wcs.port.out.TrafficStateRepository;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public final class DispatchCoordinator {
private final DispatchSnapshotProvider snapshotProvider;
private final VehicleAssignmentPolicy assignmentPolicy;
private final DeadlockGuard deadlockGuard;
private final TrafficStateRepository trafficStateRepository;
private final IdGenerator idGenerator;
private final WcsClock clock;
public DispatchCoordinator(VehicleAssignmentPolicy assignmentPolicy,
DeadlockGuard deadlockGuard,
TrafficStateRepository trafficStateRepository,
IdGenerator idGenerator) {
this(null, assignmentPolicy, deadlockGuard, trafficStateRepository,
idGenerator, Instant::now);
}
public DispatchCoordinator(DispatchSnapshotProvider snapshotProvider,
VehicleAssignmentPolicy assignmentPolicy,
DeadlockGuard deadlockGuard,
TrafficStateRepository trafficStateRepository,
IdGenerator idGenerator,
WcsClock clock) {
this.snapshotProvider = snapshotProvider;
this.assignmentPolicy = Objects.requireNonNull(assignmentPolicy,
"assignmentPolicy");
this.deadlockGuard = Objects.requireNonNull(deadlockGuard, "deadlockGuard");
this.trafficStateRepository = Objects.requireNonNull(
trafficStateRepository, "trafficStateRepository");
this.idGenerator = Objects.requireNonNull(idGenerator, "idGenerator");
this.clock = Objects.requireNonNull(clock, "clock");
}
public DispatchCycleResult dispatch() {
if (snapshotProvider == null) {
throw new IllegalStateException("dispatch snapshot provider is not configured");
}
return dispatch(snapshotProvider.load());
}
public DispatchCycleResult dispatch(DispatchSnapshot snapshot) {
Objects.requireNonNull(snapshot, "snapshot");
List<DispatchTask> tasks = new ArrayList<DispatchTask>(snapshot.getReadySteps());
Collections.sort(tasks, Comparator
.comparingInt(DispatchTask::getPriority).reversed()
.thenComparing(Comparator
.comparingLong(DispatchTask::getWaitingAgeSeconds)
.reversed())
.thenComparing(DispatchTask::getOrderId)
.thenComparing(DispatchTask::getStepId));
List<VehicleSnapshot> available = new ArrayList<VehicleSnapshot>(
snapshot.getVehicleSnapshots());
ResourceLedger ledger = snapshot.workingLedger();
DeadlockGuard cycleGuard = deadlockGuard.withGraph(
snapshot.workingWaitForGraph());
List<GrantedDispatch> granted = new ArrayList<GrantedDispatch>();
List<HeldDispatch> held = new ArrayList<HeldDispatch>();
List<HeldDispatch> yielded = new ArrayList<HeldDispatch>();
List<DispatchDecisionEvent> events = new ArrayList<DispatchDecisionEvent>();
List<String> violations = detectInputViolations(tasks, available);
for (DispatchTask task : tasks) {
VehicleAssignmentDecision assignment = assignmentPolicy.assign(
task, available, snapshot.getTopology());
if (!assignment.isAssigned()) {
HeldDispatch noVehicle = new HeldDispatch(task.getOrderId(),
task.getStepId(), null, TrafficDecisionType.HOLD,
"NO_ELIGIBLE_VEHICLE", Collections.<String>emptySet(),
null, null);
held.add(noVehicle);
events.add(event(task, null, TrafficDecisionType.HOLD,
noVehicle.getReasonCode(), snapshot));
continue;
}
VehicleSnapshot vehicle = removeVehicle(
available, assignment.getVehicleId());
String reservationId = requireGeneratedId(idGenerator.nextId());
ReservationRequest request = new ReservationRequest(reservationId,
task.getOrderId(), task.getStepId(), assignment.getVehicleId(),
assignment.getRoute().getResourceKeys());
TrafficDecision traffic = cycleGuard.decide(new TrafficCandidate(
request, vehicle.getLocationNodeId(), assignment.getRoute()),
ledger, snapshot.getTopology());
if (traffic.getType() == TrafficDecisionType.GRANT) {
ReservationDecision reservation = traffic.getReservationDecision();
granted.add(new GrantedDispatch(task.getOrderId(), task.getStepId(),
assignment.getVehicleId(), assignment.getRoute(),
Collections.singletonList(reservation.getReservationId()),
snapshot.getTopology().version(), reservation.getFencingToken()));
} else {
HeldDispatch blocked = new HeldDispatch(task.getOrderId(),
task.getStepId(), assignment.getVehicleId(), traffic.getType(),
traffic.getReasonCode(), traffic.getBlockingVehicleIds(),
traffic.getYieldNodeId(), traffic.getYieldRoute());
if (traffic.getType() == TrafficDecisionType.YIELD) {
yielded.add(blocked);
} else {
held.add(blocked);
}
}
events.add(event(task, assignment.getVehicleId(), traffic.getType(),
traffic.getReasonCode(), snapshot));
}
DispatchCommit commit = new DispatchCommit(
snapshot.getTopology().getWarehouseCode(),
snapshot.getTopology().version(),
snapshot.getSnapshotVersion(), granted, held, yielded, events,
ledger, cycleGuard.snapshotWaitForGraph());
if (!trafficStateRepository.commit(commit)) {
return DispatchCycleResult.retrySnapshotChanged();
}
return DispatchCycleResult.committed(granted, held, yielded, violations);
}
private DispatchDecisionEvent event(DispatchTask task, String vehicleId,
TrafficDecisionType type, String reason,
DispatchSnapshot snapshot) {
return new DispatchDecisionEvent(requireGeneratedId(idGenerator.nextId()),
task.getOrderId(), task.getStepId(), vehicleId, type, reason,
snapshot.getTopology().version(), snapshot.getSnapshotVersion(),
clock.now());
}
private static VehicleSnapshot removeVehicle(List<VehicleSnapshot> vehicles,
String vehicleId) {
for (int i = 0; i < vehicles.size(); i++) {
if (vehicleId.equals(vehicles.get(i).getVehicleId())) {
return vehicles.remove(i);
}
}
throw new IllegalStateException("assigned vehicle missing from snapshot: " + vehicleId);
}
private static List<String> detectInputViolations(
List<DispatchTask> tasks, List<VehicleSnapshot> vehicles) {
List<String> violations = new ArrayList<String>();
Set<String> stepIds = new HashSet<String>();
for (DispatchTask task : tasks) {
if (!stepIds.add(task.getStepId())) {
violations.add("DUPLICATE_READY_STEP:" + task.getStepId());
}
}
Set<String> vehicleIds = new HashSet<String>();
for (VehicleSnapshot vehicle : vehicles) {
if (!vehicleIds.add(vehicle.getVehicleId())) {
violations.add("DUPLICATE_VEHICLE_SNAPSHOT:" + vehicle.getVehicleId());
}
}
return violations;
}
private static String requireGeneratedId(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalStateException("id generator returned a blank id");
}
return value.trim();
}
}

View File

@@ -0,0 +1,68 @@
package com.ruoyi.wcs.application.dispatch;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class DispatchCycleResult {
private final String statusCode;
private final List<GrantedDispatch> granted;
private final List<HeldDispatch> held;
private final List<HeldDispatch> yielded;
private final List<String> invariantViolations;
private DispatchCycleResult(String statusCode,
List<GrantedDispatch> granted,
List<HeldDispatch> held,
List<HeldDispatch> yielded,
List<String> invariantViolations) {
this.statusCode = statusCode;
this.granted = immutable(granted);
this.held = immutable(held);
this.yielded = immutable(yielded);
this.invariantViolations = immutable(invariantViolations);
}
public static DispatchCycleResult committed(List<GrantedDispatch> granted,
List<HeldDispatch> held,
List<HeldDispatch> yielded,
List<String> violations) {
return new DispatchCycleResult("COMMITTED", granted, held, yielded, violations);
}
public static DispatchCycleResult retrySnapshotChanged() {
return new DispatchCycleResult("RETRY_SNAPSHOT_CHANGED",
Collections.<GrantedDispatch>emptyList(),
Collections.<HeldDispatch>emptyList(),
Collections.<HeldDispatch>emptyList(),
Collections.<String>emptyList());
}
public String getStatusCode() {
return statusCode;
}
public boolean isRetrySnapshotChanged() {
return "RETRY_SNAPSHOT_CHANGED".equals(statusCode);
}
public List<GrantedDispatch> getGranted() {
return granted;
}
public List<HeldDispatch> getHeld() {
return held;
}
public List<HeldDispatch> getYielded() {
return yielded;
}
public List<String> getInvariantViolations() {
return invariantViolations;
}
private static <T> List<T> immutable(List<T> source) {
return Collections.unmodifiableList(new ArrayList<T>(source));
}
}

View File

@@ -0,0 +1,68 @@
package com.ruoyi.wcs.application.dispatch;
import com.ruoyi.wcs.domain.traffic.TrafficDecisionType;
import java.time.Instant;
public final class DispatchDecisionEvent {
private final String eventId;
private final String orderId;
private final String stepId;
private final String vehicleId;
private final TrafficDecisionType decisionType;
private final String reasonCode;
private final long topologyVersion;
private final long snapshotVersion;
private final Instant occurredAt;
public DispatchDecisionEvent(String eventId, String orderId, String stepId,
String vehicleId, TrafficDecisionType decisionType,
String reasonCode, long topologyVersion,
long snapshotVersion, Instant occurredAt) {
this.eventId = eventId;
this.orderId = orderId;
this.stepId = stepId;
this.vehicleId = vehicleId;
this.decisionType = decisionType;
this.reasonCode = reasonCode;
this.topologyVersion = topologyVersion;
this.snapshotVersion = snapshotVersion;
this.occurredAt = occurredAt;
}
public String getEventId() {
return eventId;
}
public String getOrderId() {
return orderId;
}
public String getStepId() {
return stepId;
}
public String getVehicleId() {
return vehicleId;
}
public TrafficDecisionType getDecisionType() {
return decisionType;
}
public String getReasonCode() {
return reasonCode;
}
public long getTopologyVersion() {
return topologyVersion;
}
public long getSnapshotVersion() {
return snapshotVersion;
}
public Instant getOccurredAt() {
return occurredAt;
}
}

View File

@@ -0,0 +1,79 @@
package com.ruoyi.wcs.application.dispatch;
import com.ruoyi.wcs.domain.dispatch.DispatchTask;
import com.ruoyi.wcs.domain.equipment.VehicleSnapshot;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
import com.ruoyi.wcs.domain.traffic.WaitForGraph;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public final class DispatchSnapshot {
private final List<DispatchTask> readySteps;
private final List<VehicleSnapshot> vehicleSnapshots;
private final EffectiveTopology topology;
private final ResourceLedger resourceLedger;
private final WaitForGraph waitForGraph;
private final long snapshotVersion;
public DispatchSnapshot(List<DispatchTask> readySteps,
List<VehicleSnapshot> vehicleSnapshots,
EffectiveTopology topology,
ResourceLedger resourceLedger,
long snapshotVersion) {
this(readySteps, vehicleSnapshots, topology, resourceLedger,
new WaitForGraph(), snapshotVersion);
}
public DispatchSnapshot(List<DispatchTask> readySteps,
List<VehicleSnapshot> vehicleSnapshots,
EffectiveTopology topology,
ResourceLedger resourceLedger,
WaitForGraph waitForGraph,
long snapshotVersion) {
this.readySteps = immutable(readySteps, "readySteps");
this.vehicleSnapshots = immutable(vehicleSnapshots, "vehicleSnapshots");
this.topology = Objects.requireNonNull(topology, "topology");
this.resourceLedger = Objects.requireNonNull(resourceLedger,
"resourceLedger").copy();
this.waitForGraph = Objects.requireNonNull(waitForGraph, "waitForGraph").copy();
if (snapshotVersion < 0L) {
throw new IllegalArgumentException("snapshotVersion must not be negative");
}
this.snapshotVersion = snapshotVersion;
}
public List<DispatchTask> getReadySteps() {
return readySteps;
}
public List<VehicleSnapshot> getVehicleSnapshots() {
return vehicleSnapshots;
}
public EffectiveTopology getTopology() {
return topology;
}
public ResourceLedger workingLedger() {
return resourceLedger.copy();
}
public WaitForGraph workingWaitForGraph() {
return waitForGraph.copy();
}
public long getSnapshotVersion() {
return snapshotVersion;
}
private static <T> List<T> immutable(List<T> source, String field) {
if (source == null) {
throw new IllegalArgumentException(field + " must not be null");
}
return Collections.unmodifiableList(new ArrayList<T>(source));
}
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.wcs.application.dispatch;
import com.ruoyi.wcs.domain.traffic.RoutePlan;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class GrantedDispatch {
private final String orderId;
private final String stepId;
private final String vehicleId;
private final RoutePlan protectedRoute;
private final List<String> reservationIds;
private final long topologyVersion;
private final long fencingToken;
public GrantedDispatch(String orderId, String stepId, String vehicleId,
RoutePlan protectedRoute, List<String> reservationIds,
long topologyVersion, long fencingToken) {
this.orderId = orderId;
this.stepId = stepId;
this.vehicleId = vehicleId;
this.protectedRoute = protectedRoute;
this.reservationIds = Collections.unmodifiableList(
new ArrayList<String>(reservationIds));
this.topologyVersion = topologyVersion;
this.fencingToken = fencingToken;
}
public String getOrderId() {
return orderId;
}
public String getStepId() {
return stepId;
}
public String getVehicleId() {
return vehicleId;
}
public RoutePlan getProtectedRoute() {
return protectedRoute;
}
public List<String> getReservationIds() {
return reservationIds;
}
public long getTopologyVersion() {
return topologyVersion;
}
public long getFencingToken() {
return fencingToken;
}
}

View File

@@ -0,0 +1,66 @@
package com.ruoyi.wcs.application.dispatch;
import com.ruoyi.wcs.domain.traffic.RoutePlan;
import com.ruoyi.wcs.domain.traffic.TrafficDecisionType;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
public final class HeldDispatch {
private final String orderId;
private final String stepId;
private final String vehicleId;
private final TrafficDecisionType decisionType;
private final String reasonCode;
private final Set<String> blockingVehicleIds;
private final String yieldNodeId;
private final RoutePlan yieldRoute;
public HeldDispatch(String orderId, String stepId, String vehicleId,
TrafficDecisionType decisionType, String reasonCode,
Set<String> blockingVehicleIds,
String yieldNodeId, RoutePlan yieldRoute) {
this.orderId = orderId;
this.stepId = stepId;
this.vehicleId = vehicleId;
this.decisionType = decisionType;
this.reasonCode = reasonCode;
this.blockingVehicleIds = Collections.unmodifiableSet(
new LinkedHashSet<String>(blockingVehicleIds));
this.yieldNodeId = yieldNodeId;
this.yieldRoute = yieldRoute;
}
public String getOrderId() {
return orderId;
}
public String getStepId() {
return stepId;
}
public String getVehicleId() {
return vehicleId;
}
public TrafficDecisionType getDecisionType() {
return decisionType;
}
public String getReasonCode() {
return reasonCode;
}
public Set<String> getBlockingVehicleIds() {
return blockingVehicleIds;
}
public String getYieldNodeId() {
return yieldNodeId;
}
public RoutePlan getYieldRoute() {
return yieldRoute;
}
}

View File

@@ -21,6 +21,14 @@ public final class DeadlockGuard {
this.routePlanner = Objects.requireNonNull(routePlanner, "routePlanner");
}
public DeadlockGuard withGraph(WaitForGraph graph) {
return new DeadlockGuard(graph, routePlanner);
}
public WaitForGraph snapshotWaitForGraph() {
return waitForGraph.copy();
}
public TrafficDecision decide(TrafficCandidate candidate,
ResourceLedger ledger,
EffectiveTopology topology) {

View File

@@ -57,6 +57,14 @@ public final class WaitForGraph {
return Collections.unmodifiableSet(new LinkedHashSet<String>(holders));
}
public synchronized WaitForGraph copy() {
WaitForGraph copy = new WaitForGraph();
for (Map.Entry<String, Set<String>> entry : edges.entrySet()) {
copy.edges.put(entry.getKey(), new TreeSet<String>(entry.getValue()));
}
return copy;
}
private Map<String, Set<String>> copyEdges() {
Map<String, Set<String>> copy = new LinkedHashMap<String, Set<String>>();
for (Map.Entry<String, Set<String>> entry : edges.entrySet()) {

View File

@@ -0,0 +1,9 @@
package com.ruoyi.wcs.port.out;
import com.ruoyi.wcs.application.dispatch.DispatchDecisionEvent;
import java.util.List;
public interface DecisionEventRepository {
void saveAllWithinTrafficCommit(List<DispatchDecisionEvent> events);
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.wcs.port.out;
import com.ruoyi.wcs.application.dispatch.DispatchSnapshot;
public interface DispatchSnapshotProvider {
DispatchSnapshot load();
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.wcs.port.out;
import com.ruoyi.wcs.application.dispatch.DispatchCommit;
public interface TrafficStateRepository {
boolean commit(DispatchCommit commit);
}

View File

@@ -0,0 +1,140 @@
package com.ruoyi.wcs.application.dispatch;
import com.ruoyi.wcs.domain.dispatch.DefaultVehicleAssignmentPolicy;
import com.ruoyi.wcs.domain.dispatch.DispatchTask;
import com.ruoyi.wcs.domain.equipment.DeviceCapability;
import com.ruoyi.wcs.domain.equipment.DeviceDescriptor;
import com.ruoyi.wcs.domain.equipment.DeviceType;
import com.ruoyi.wcs.domain.equipment.VehicleSnapshot;
import com.ruoyi.wcs.domain.shared.IdGenerator;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
import com.ruoyi.wcs.domain.topology.ResourceDefinition;
import com.ruoyi.wcs.domain.topology.TopologyCompiler;
import com.ruoyi.wcs.domain.topology.TopologyDefinition;
import com.ruoyi.wcs.domain.topology.TopologyEdge;
import com.ruoyi.wcs.domain.topology.TopologyNode;
import com.ruoyi.wcs.domain.traffic.AStarRoutePlanner;
import com.ruoyi.wcs.domain.traffic.DeadlockGuard;
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
import com.ruoyi.wcs.domain.traffic.WaitForGraph;
import com.ruoyi.wcs.port.out.DispatchPolicy;
import com.ruoyi.wcs.port.out.TrafficStateRepository;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class DispatchCoordinatorTest {
@Test
void grantsOneVehicleAndHoldsTheOtherForTheSameIntersection() {
EffectiveTopology topology = topology();
RecordingTrafficRepository repository = new RecordingTrafficRepository(true);
DispatchCoordinator coordinator = coordinator(repository);
DispatchCycleResult result = coordinator.dispatch(new DispatchSnapshot(
Arrays.asList(task("O1", "S1", "A", 2),
task("O2", "S2", "B", 1)),
Arrays.asList(vehicle("V1", "A"), vehicle("V2", "B")),
topology, new ResourceLedger(topology), 11L));
assertEquals(1, result.getGranted().size());
assertEquals("S1", result.getGranted().get(0).getStepId());
assertEquals("V1", result.getGranted().get(0).getVehicleId());
assertEquals(Collections.singletonList("R-1"),
result.getGranted().get(0).getReservationIds());
assertEquals(1, result.getHeld().size());
assertEquals("NO_SAFE_YIELD_POINT", result.getHeld().get(0).getReasonCode());
assertEquals(0, result.getInvariantViolations().size());
assertEquals(1, repository.commitCount);
assertEquals("FT546", repository.lastCommit.getWarehouseCode());
assertEquals(2, repository.lastCommit.getDecisionEvents().size());
assertFalse(result.isRetrySnapshotChanged());
}
@Test
void returnsRetryWithoutExposingPartialGrantWhenSnapshotChanged() {
EffectiveTopology topology = topology();
RecordingTrafficRepository repository = new RecordingTrafficRepository(false);
DispatchCoordinator coordinator = coordinator(repository);
DispatchCycleResult result = coordinator.dispatch(new DispatchSnapshot(
Collections.singletonList(task("O1", "S1", "A", 1)),
Collections.singletonList(vehicle("V1", "A")),
topology, new ResourceLedger(topology), 11L));
assertTrue(result.isRetrySnapshotChanged());
assertEquals("RETRY_SNAPSHOT_CHANGED", result.getStatusCode());
assertEquals(0, result.getGranted().size());
assertEquals(0, result.getHeld().size());
assertEquals(1, repository.commitCount);
}
private static DispatchCoordinator coordinator(TrafficStateRepository repository) {
AStarRoutePlanner routePlanner = new AStarRoutePlanner();
AtomicInteger sequence = new AtomicInteger();
IdGenerator ids = () -> "R-" + sequence.incrementAndGet();
return new DispatchCoordinator(
new DefaultVehicleAssignmentPolicy(routePlanner, DispatchPolicy.NONE),
new DeadlockGuard(new WaitForGraph(), routePlanner),
repository, ids);
}
private static DispatchTask task(String orderId, String stepId,
String source, int priority) {
return new DispatchTask(orderId, stepId, source, "X", priority,
60L, 30, Collections.<DeviceCapability>emptySet());
}
private static VehicleSnapshot vehicle(String id, String location) {
DeviceDescriptor descriptor = new DeviceDescriptor(id,
DeviceType.FOUR_WAY_SHUTTLE, "F1",
EnumSet.of(DeviceCapability.FOUR_WAY_MOVE));
return new VehicleSnapshot(descriptor,
VehicleSnapshot.ConnectionState.ONLINE,
VehicleSnapshot.OperatingMode.AUTO,
VehicleSnapshot.HealthState.NORMAL,
VehicleSnapshot.OccupationState.IDLE,
location, null, 80, null, "F1",
Instant.parse("2026-08-04T00:00:00Z"));
}
private static EffectiveTopology topology() {
TopologyDefinition definition = new TopologyDefinition("FT546", 7L, true,
Arrays.asList(
new TopologyNode("A", "F1", 0, 0, 1, false, false),
new TopologyNode("X", "F1", 1, 0, 1, false, false),
new TopologyNode("B", "F1", 2, 0, 1, false, false)),
Arrays.asList(
new TopologyEdge("A", "X", 1, 1),
new TopologyEdge("B", "X", 1, 1)),
Collections.<ResourceDefinition>emptyList(),
Collections.<String>emptyList(), Collections.<String>emptyList());
return new TopologyCompiler().compile(definition);
}
private static final class RecordingTrafficRepository
implements TrafficStateRepository {
private final boolean commitResult;
private int commitCount;
private DispatchCommit lastCommit;
private RecordingTrafficRepository(boolean commitResult) {
this.commitResult = commitResult;
}
@Override
public boolean commit(DispatchCommit commit) {
commitCount++;
lastCommit = commit;
return commitResult;
}
}
}

View File

@@ -0,0 +1,248 @@
package com.ruoyi.wcs.infrastructure.persistence.traffic;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Tag;
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 java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@Tag("sqlserver")
class TrafficConcurrencyIT {
private String jdbcUrl;
private String username;
private String password;
private String warehouseCode;
private String floorId;
private String resourceKey;
@Test
void packagesTheTrafficMigrationUsedByTheSqlServerVerification() throws Exception {
String root = System.getProperty("maven.multiModuleProjectDirectory", ".");
String migration = readMigration(root, "V002__wcs_topology_and_traffic.sql");
assertFalse(migration.trim().isEmpty());
}
@AfterEach
void cleanFixture() throws Exception {
if (jdbcUrl == null || resourceKey == null) {
return;
}
try (Connection connection = connect()) {
delete(connection, "DELETE FROM dbo.wcs_resource_reservation WHERE resource_key = ?", resourceKey);
delete(connection, "DELETE FROM dbo.wcs_resource_state WHERE resource_key = ?", resourceKey);
delete(connection, "DELETE FROM dbo.wcs_resource_definition WHERE resource_key = ?", resourceKey);
delete(connection, "DELETE FROM dbo.wcs_topology_version WHERE warehouse_code = ?", warehouseCode);
delete(connection, "DELETE FROM dbo.wcs_floor WHERE floor_id = ?", floorId);
delete(connection, "DELETE FROM dbo.wcs_warehouse WHERE warehouse_code = ?", warehouseCode);
}
}
@Test
void onlyOneConcurrentReservationCommitsForCapacityOneResource() throws Exception {
configureIsolatedDatabase();
applyMigration();
createCapacityOneFixture();
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch ready = new CountDownLatch(2);
CountDownLatch start = new CountDownLatch(1);
try {
Future<Boolean> first = executor.submit(() -> reserve("IT-R1", "IT-V1", ready, start));
Future<Boolean> second = executor.submit(() -> reserve("IT-R2", "IT-V2", ready, start));
ready.await();
start.countDown();
int committed = (first.get() ? 1 : 0) + (second.get() ? 1 : 0);
assertEquals(1, committed);
assertEquals(1, scalarInt(
"SELECT reserved_count FROM dbo.wcs_resource_state WHERE resource_key = ?",
resourceKey));
assertEquals(1, scalarInt(
"SELECT COUNT(*) FROM dbo.wcs_resource_reservation "
+ "WHERE resource_key = ? AND reservation_state = 'GRANTED'",
resourceKey));
} finally {
executor.shutdownNow();
}
}
private void configureIsolatedDatabase() {
Assumptions.assumeTrue("true".equalsIgnoreCase(
System.getenv("WCS_SQLSERVER_TEST_ENABLED")),
"set WCS_SQLSERVER_TEST_ENABLED=true only for an isolated SQL Server database");
jdbcUrl = requireEnvironment("WCS_SQLSERVER_TEST_URL");
username = requireEnvironment("WCS_SQLSERVER_TEST_USER");
password = requireEnvironment("WCS_SQLSERVER_TEST_PASSWORD");
}
private void applyMigration() throws Exception {
String root = System.getProperty("maven.multiModuleProjectDirectory", ".");
try (Connection connection = connect(); Statement statement = connection.createStatement()) {
statement.execute(readMigration(root, "V001__wcs_task_foundation.sql"));
statement.execute(readMigration(root, "V002__wcs_topology_and_traffic.sql"));
}
}
private static String readMigration(String root, String filename) throws Exception {
Path current = Paths.get(root).toAbsolutePath().normalize();
while (current != null) {
Path migration = current.resolve(Paths.get("sql", "wcs", filename));
if (Files.isRegularFile(migration)) {
return new String(Files.readAllBytes(migration), StandardCharsets.UTF_8);
}
current = current.getParent();
}
throw new java.nio.file.NoSuchFileException(filename);
}
private void createCapacityOneFixture() throws Exception {
String suffix = UUID.randomUUID().toString().replace("-", "");
warehouseCode = "IT" + suffix;
floorId = "IT-F-" + suffix;
resourceKey = "IT:RESOURCE:" + suffix;
try (Connection connection = connect()) {
insert(connection,
"INSERT INTO dbo.wcs_warehouse "
+ "(warehouse_code, warehouse_name, active_configuration_version, state_version) "
+ "VALUES (?, ?, NULL, 0)",
warehouseCode, "traffic concurrency test");
insert(connection,
"INSERT INTO dbo.wcs_floor "
+ "(floor_id, warehouse_code, floor_code, floor_level) VALUES (?, ?, ?, 1)",
floorId, warehouseCode, "IT-F1");
insert(connection,
"INSERT INTO dbo.wcs_topology_version "
+ "(warehouse_code, topology_version, publication_state, published_time) "
+ "VALUES (?, 1, 'PUBLISHED', ?)",
warehouseCode, Instant.now());
insert(connection,
"INSERT INTO dbo.wcs_resource_definition "
+ "(resource_key, warehouse_code, topology_version, resource_type, capacity) "
+ "VALUES (?, ?, 1, 'NODE', 1)",
resourceKey, warehouseCode);
insert(connection,
"INSERT INTO dbo.wcs_resource_state "
+ "(warehouse_code, topology_version, resource_key, capacity, "
+ "reserved_count, occupied_count, fencing_token, state_version) "
+ "VALUES (?, 1, ?, 1, 0, 0, 0, 0)",
warehouseCode, resourceKey);
}
}
private boolean reserve(String reservationId, String vehicleId,
CountDownLatch ready, CountDownLatch start) throws Exception {
try (Connection connection = connect()) {
connection.setAutoCommit(false);
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
ready.countDown();
start.await();
try {
int claimed;
try (PreparedStatement statement = connection.prepareStatement(
"UPDATE dbo.wcs_resource_state WITH (UPDLOCK, ROWLOCK) "
+ "SET reserved_count = reserved_count + 1, "
+ "fencing_token = fencing_token + 1, state_version = state_version + 1 "
+ "WHERE warehouse_code = ? AND topology_version = 1 "
+ "AND resource_key = ? "
+ "AND reserved_count + occupied_count < capacity")) {
statement.setString(1, warehouseCode);
statement.setString(2, resourceKey);
claimed = statement.executeUpdate();
}
if (claimed == 0) {
connection.rollback();
return false;
}
try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO dbo.wcs_resource_reservation "
+ "(reservation_id, warehouse_code, topology_version, resource_key, "
+ "order_id, step_id, vehicle_id, reservation_state, fencing_token, "
+ "created_time, updated_time) "
+ "SELECT ?, ?, 1, ?, 'IT-O', 'IT-S', ?, 'GRANTED', fencing_token, ?, ? "
+ "FROM dbo.wcs_resource_state WHERE warehouse_code = ? "
+ "AND topology_version = 1 AND resource_key = ?")) {
Instant now = Instant.now();
statement.setString(1, reservationId + '-' + warehouseCode);
statement.setString(2, warehouseCode);
statement.setString(3, resourceKey);
statement.setString(4, vehicleId);
statement.setTimestamp(5, Timestamp.from(now));
statement.setTimestamp(6, Timestamp.from(now));
statement.setString(7, warehouseCode);
statement.setString(8, resourceKey);
statement.executeUpdate();
}
connection.commit();
return true;
} catch (Exception error) {
connection.rollback();
throw error;
}
}
}
private int scalarInt(String sql, String value) throws Exception {
try (Connection connection = connect();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, value);
try (ResultSet result = statement.executeQuery()) {
if (!result.next()) {
throw new AssertionError("query returned no rows: " + sql);
}
return result.getInt(1);
}
}
}
private Connection connect() throws SQLException {
return DriverManager.getConnection(jdbcUrl, username, password);
}
private static void insert(Connection connection, String sql,
Object... values) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement(sql)) {
for (int index = 0; index < values.length; index++) {
Object value = values[index];
statement.setObject(index + 1,
value instanceof Instant ? Timestamp.from((Instant) value) : value);
}
statement.executeUpdate();
}
}
private static void delete(Connection connection, String sql,
String value) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, value);
statement.executeUpdate();
}
}
private static String requireEnvironment(String name) {
String value = System.getenv(name);
Assumptions.assumeTrue(value != null && !value.trim().isEmpty(),
name + " must point to the isolated SQL Server test database");
return value.trim();
}
}

View File

@@ -0,0 +1,297 @@
SET XACT_ABORT ON;
BEGIN TRANSACTION;
IF OBJECT_ID('dbo.wcs_warehouse', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_warehouse (
warehouse_code varchar(64) NOT NULL PRIMARY KEY,
warehouse_name nvarchar(128) NOT NULL,
active_configuration_version bigint NULL,
state_version bigint NOT NULL CONSTRAINT df_wcs_warehouse_state_version DEFAULT (0)
);
END;
IF OBJECT_ID('dbo.wcs_floor', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_floor (
floor_id varchar(64) NOT NULL PRIMARY KEY,
warehouse_code varchar(64) NOT NULL,
floor_code varchar(64) NOT NULL,
floor_level int NOT NULL,
CONSTRAINT fk_wcs_floor_warehouse FOREIGN KEY (warehouse_code)
REFERENCES dbo.wcs_warehouse(warehouse_code),
CONSTRAINT uk_wcs_floor_code UNIQUE (warehouse_code, floor_code),
CONSTRAINT uk_wcs_floor_level UNIQUE (warehouse_code, floor_level)
);
END;
IF OBJECT_ID('dbo.wcs_topology_version', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_topology_version (
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
publication_state varchar(32) NOT NULL,
published_time datetime2(3) NULL,
snapshot_version bigint NOT NULL
CONSTRAINT df_wcs_topology_snapshot_version DEFAULT (0),
CONSTRAINT pk_wcs_topology_version PRIMARY KEY (
warehouse_code, topology_version),
CONSTRAINT fk_wcs_topology_version_warehouse FOREIGN KEY (warehouse_code)
REFERENCES dbo.wcs_warehouse(warehouse_code)
);
END;
IF OBJECT_ID('dbo.wcs_topology_node', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_topology_node (
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
node_id varchar(128) NOT NULL,
floor_id varchar(64) NOT NULL,
coordinate_x int NOT NULL,
coordinate_y int NOT NULL,
capacity int NOT NULL,
safe_wait_flag bit NOT NULL CONSTRAINT df_wcs_node_safe_wait DEFAULT (0),
safe_yield_flag bit NOT NULL CONSTRAINT df_wcs_node_safe_yield DEFAULT (0),
CONSTRAINT pk_wcs_topology_node PRIMARY KEY (
warehouse_code, topology_version, node_id),
CONSTRAINT fk_wcs_topology_node_version FOREIGN KEY (
warehouse_code, topology_version)
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version),
CONSTRAINT fk_wcs_topology_node_floor FOREIGN KEY (floor_id)
REFERENCES dbo.wcs_floor(floor_id),
CONSTRAINT ck_wcs_topology_node_capacity CHECK (capacity > 0),
CONSTRAINT uk_wcs_topology_node_coordinate UNIQUE (
warehouse_code, topology_version, floor_id, coordinate_x, coordinate_y)
);
END;
IF OBJECT_ID('dbo.wcs_topology_edge', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_topology_edge (
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
resource_key varchar(320) NOT NULL,
source_node_id varchar(128) NOT NULL,
target_node_id varchar(128) NOT NULL,
distance_cost int NOT NULL,
capacity int NOT NULL,
CONSTRAINT pk_wcs_topology_edge PRIMARY KEY (
warehouse_code, topology_version, resource_key),
CONSTRAINT fk_wcs_topology_edge_version FOREIGN KEY (
warehouse_code, topology_version)
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version),
CONSTRAINT ck_wcs_topology_edge_distance CHECK (distance_cost >= 0),
CONSTRAINT ck_wcs_topology_edge_capacity CHECK (capacity > 0),
CONSTRAINT ck_wcs_topology_edge_direction CHECK (
source_node_id <> target_node_id)
);
END;
IF OBJECT_ID('dbo.wcs_traffic_zone', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_traffic_zone (
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
zone_id varchar(128) NOT NULL,
zone_name nvarchar(128) NOT NULL,
coordination_mode varchar(32) NOT NULL,
CONSTRAINT pk_wcs_traffic_zone PRIMARY KEY (
warehouse_code, topology_version, zone_id),
CONSTRAINT fk_wcs_traffic_zone_version FOREIGN KEY (
warehouse_code, topology_version)
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version)
);
END;
IF OBJECT_ID('dbo.wcs_resource_definition', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_resource_definition (
resource_key varchar(320) NOT NULL,
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
resource_type varchar(32) NOT NULL,
capacity int NOT NULL,
zone_id varchar(128) NULL,
CONSTRAINT pk_wcs_resource_definition PRIMARY KEY (
warehouse_code, topology_version, resource_key),
CONSTRAINT fk_wcs_resource_definition_version FOREIGN KEY (
warehouse_code, topology_version)
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version),
CONSTRAINT ck_wcs_resource_definition_capacity CHECK (capacity > 0)
);
END;
IF OBJECT_ID('dbo.wcs_device', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_device (
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
device_id varchar(64) NOT NULL,
device_type varchar(32) NOT NULL,
floor_id varchar(64) NULL,
enabled_flag bit NOT NULL CONSTRAINT df_wcs_device_enabled DEFAULT (1),
CONSTRAINT pk_wcs_device PRIMARY KEY (
warehouse_code, topology_version, device_id),
CONSTRAINT fk_wcs_device_version FOREIGN KEY (
warehouse_code, topology_version)
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version),
CONSTRAINT fk_wcs_device_floor FOREIGN KEY (floor_id)
REFERENCES dbo.wcs_floor(floor_id)
);
END;
IF OBJECT_ID('dbo.wcs_device_capability', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_device_capability (
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
device_id varchar(64) NOT NULL,
capability_code varchar(64) NOT NULL,
CONSTRAINT pk_wcs_device_capability PRIMARY KEY (
warehouse_code, topology_version, device_id, capability_code),
CONSTRAINT fk_wcs_device_capability_device FOREIGN KEY (
warehouse_code, topology_version, device_id)
REFERENCES dbo.wcs_device(warehouse_code, topology_version, device_id)
);
END;
IF OBJECT_ID('dbo.wcs_device_port', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_device_port (
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
port_id varchar(64) NOT NULL,
device_id varchar(64) NOT NULL,
node_id varchar(128) NOT NULL,
port_role varchar(32) NOT NULL,
CONSTRAINT pk_wcs_device_port PRIMARY KEY (
warehouse_code, topology_version, port_id),
CONSTRAINT fk_wcs_device_port_device FOREIGN KEY (
warehouse_code, topology_version, device_id)
REFERENCES dbo.wcs_device(warehouse_code, topology_version, device_id)
);
END;
IF OBJECT_ID('dbo.wcs_charger', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_charger (
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
charger_id varchar(64) NOT NULL,
node_id varchar(128) NOT NULL,
enabled_flag bit NOT NULL CONSTRAINT df_wcs_charger_enabled DEFAULT (1),
CONSTRAINT pk_wcs_charger PRIMARY KEY (
warehouse_code, topology_version, charger_id),
CONSTRAINT fk_wcs_charger_version FOREIGN KEY (
warehouse_code, topology_version)
REFERENCES dbo.wcs_topology_version(warehouse_code, topology_version)
);
END;
IF OBJECT_ID('dbo.wcs_vehicle_assignment', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_vehicle_assignment (
assignment_id varchar(128) NOT NULL PRIMARY KEY,
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
snapshot_version bigint NOT NULL,
order_id varchar(64) NOT NULL,
step_id varchar(128) NOT NULL,
vehicle_id varchar(64) NOT NULL,
fencing_token bigint NOT NULL,
assignment_state varchar(32) NOT NULL,
created_time datetime2(3) NOT NULL,
CONSTRAINT uk_wcs_vehicle_assignment_step UNIQUE (step_id),
CONSTRAINT ck_wcs_vehicle_assignment_fencing CHECK (fencing_token > 0)
);
END;
IF OBJECT_ID('dbo.wcs_route_plan', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_route_plan (
route_plan_id varchar(128) NOT NULL PRIMARY KEY,
assignment_id varchar(128) NOT NULL,
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
node_path_json nvarchar(max) NOT NULL,
resource_path_json nvarchar(max) NOT NULL,
distance_cost int NOT NULL,
turn_count int NOT NULL,
route_state varchar(32) NOT NULL,
created_time datetime2(3) NOT NULL,
CONSTRAINT fk_wcs_route_plan_assignment FOREIGN KEY (assignment_id)
REFERENCES dbo.wcs_vehicle_assignment(assignment_id),
CONSTRAINT uk_wcs_route_plan_assignment UNIQUE (assignment_id),
CONSTRAINT ck_wcs_route_plan_distance CHECK (distance_cost >= 0),
CONSTRAINT ck_wcs_route_plan_turns CHECK (turn_count >= 0)
);
END;
IF OBJECT_ID('dbo.wcs_resource_state', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_resource_state (
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
resource_key varchar(320) NOT NULL,
capacity int NOT NULL,
reserved_count int NOT NULL,
occupied_count int NOT NULL,
fencing_token bigint NOT NULL,
state_version bigint NOT NULL,
CONSTRAINT pk_wcs_resource_state PRIMARY KEY (
warehouse_code, topology_version, resource_key),
CONSTRAINT fk_wcs_resource_state_definition FOREIGN KEY (
warehouse_code, topology_version, resource_key)
REFERENCES dbo.wcs_resource_definition(
warehouse_code, topology_version, resource_key),
CONSTRAINT ck_wcs_resource_state_counts CHECK (
capacity > 0 AND reserved_count >= 0 AND occupied_count >= 0
AND reserved_count + occupied_count <= capacity),
CONSTRAINT ck_wcs_resource_state_fencing CHECK (fencing_token >= 0),
CONSTRAINT ck_wcs_resource_state_version CHECK (state_version >= 0)
);
END;
IF OBJECT_ID('dbo.wcs_resource_reservation', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_resource_reservation (
reservation_id varchar(128) NOT NULL,
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
resource_key varchar(320) NOT NULL,
order_id varchar(64) NOT NULL,
step_id varchar(128) NOT NULL,
vehicle_id varchar(64) NOT NULL,
reservation_state varchar(32) NOT NULL,
fencing_token bigint NOT NULL,
release_evidence_id varchar(128) NULL,
created_time datetime2(3) NOT NULL,
updated_time datetime2(3) NOT NULL,
CONSTRAINT pk_wcs_resource_reservation PRIMARY KEY (
reservation_id, resource_key),
CONSTRAINT fk_wcs_resource_reservation_state FOREIGN KEY (
warehouse_code, topology_version, resource_key)
REFERENCES dbo.wcs_resource_state(
warehouse_code, topology_version, resource_key),
CONSTRAINT ck_wcs_resource_reservation_fencing CHECK (fencing_token > 0)
);
END;
IF OBJECT_ID('dbo.wcs_traffic_decision_event', 'U') IS NULL
BEGIN
CREATE TABLE dbo.wcs_traffic_decision_event (
event_id varchar(128) NOT NULL PRIMARY KEY,
warehouse_code varchar(64) NOT NULL,
topology_version bigint NOT NULL,
snapshot_version bigint NOT NULL,
order_id varchar(64) NOT NULL,
step_id varchar(128) NOT NULL,
vehicle_id varchar(64) NULL,
decision_type varchar(16) NOT NULL,
reason_code varchar(128) NOT NULL,
occurred_time datetime2(3) NOT NULL
);
END;
COMMIT TRANSACTION;

View File

@@ -0,0 +1,32 @@
SET NOCOUNT ON;
SELECT name AS table_name
FROM sys.tables
WHERE name IN (
'wcs_warehouse',
'wcs_floor',
'wcs_topology_version',
'wcs_topology_node',
'wcs_topology_edge',
'wcs_traffic_zone',
'wcs_resource_definition',
'wcs_device',
'wcs_device_capability',
'wcs_device_port',
'wcs_charger',
'wcs_vehicle_assignment',
'wcs_route_plan',
'wcs_resource_state',
'wcs_resource_reservation',
'wcs_traffic_decision_event'
)
ORDER BY name;
SELECT c.name AS constraint_name
FROM sys.check_constraints c
WHERE c.name IN (
'ck_wcs_resource_definition_capacity',
'ck_wcs_resource_state_counts',
'ck_wcs_resource_reservation_fencing'
)
ORDER BY c.name;