fix(shadow): protect observed vehicle traffic

This commit is contained in:
zgw
2026-09-09 15:42:09 +08:00
parent edb13450a6
commit 4408958443
12 changed files with 545 additions and 4 deletions

View File

@@ -41,6 +41,7 @@ public final class ShadowDispatchSnapshotAssembler {
private final Clock clock;
private final java.util.Set<String> safeWaitSites;
private final java.util.Set<String> safeYieldSites;
private final ShadowObservedTrafficProjector observedTraffic = new ShadowObservedTrafficProjector();
public ShadowDispatchSnapshotAssembler(
String warehouseCode,
@@ -72,7 +73,7 @@ public final class ShadowDispatchSnapshotAssembler {
stateLoader.apply(topology), "stateLoader result");
return new DispatchSnapshot(tasks(batch, topology),
vehicles(batch, topology), topology, state.getLedger(),
state.getWaitForGraph(), state.getSnapshotVersion());
state.getWaitForGraph(), state.getSnapshotVersion(), observedTraffic.project(batch, topology));
}
private EffectiveTopology topology(List<ShadowMapFact> facts) {

View File

@@ -0,0 +1,121 @@
package com.ruoyi.shadow.kernel;
import com.ruoyi.shadow.facts.ShadowFactBatch;
import com.ruoyi.shadow.facts.ShadowQueueFact;
import com.ruoyi.shadow.facts.ShadowVehicleFact;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
import com.ruoyi.wcs.domain.topology.TopologyEdge;
import com.ruoyi.wcs.domain.topology.TopologyNode;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Pure read-only observations; never creates or releases persisted reservations. */
final class ShadowObservedTrafficProjector {
private final Set<String> observedVehicleIds = new LinkedHashSet<String>();
synchronized Map<String, Set<String>> project(ShadowFactBatch batch, EffectiveTopology topology) {
Map<String, Set<String>> owners = new LinkedHashMap<String, Set<String>>();
Map<String, ShadowVehicleFact> vehicles = new LinkedHashMap<String, ShadowVehicleFact>();
for (ShadowVehicleFact vehicle : batch.getVehicles()) {
if (vehicles.put(vehicle.getDeviceId(), vehicle) != null) {
throw new IllegalArgumentException("OBSERVED_VEHICLE_DUPLICATE:" + vehicle.getDeviceId());
}
}
if (vehicles.isEmpty()) throw new IllegalArgumentException("OBSERVED_VEHICLES_EMPTY");
observedVehicleIds.addAll(vehicles.keySet());
Set<String> missing = new LinkedHashSet<String>(observedVehicleIds);
missing.removeAll(vehicles.keySet());
if (!missing.isEmpty()) throw new IllegalArgumentException("OBSERVED_VEHICLE_MISSING:" + missing);
Map<String, String> coordinates = new LinkedHashMap<String, String>();
for (String id : topology.getNodeIds()) {
TopologyNode node = topology.node(id);
coordinates.put(coordinate(node.getFloorCode(), node.getX(), node.getY()), id);
}
for (ShadowVehicleFact vehicle : batch.getVehicles()) {
TopologyNode current = topology.node(vehicle.getCurrentSite());
if (current == null) throw new IllegalArgumentException("OBSERVED_POSITION_UNKNOWN:" + vehicle.getDeviceId());
if (vehicle.getCurrentFloor() != null
&& !current.getFloorCode().equals("F" + vehicle.getCurrentFloor())) {
throw new IllegalArgumentException("OBSERVED_FLOOR_CONFLICT:" + vehicle.getDeviceId());
}
add(owners, "NODE:" + current.getNodeId(), vehicle.getDeviceId());
boolean confirmedIdle = Integer.valueOf(1).equals(vehicle.getStatus())
&& Integer.valueOf(1).equals(vehicle.getAllowSend());
if (!confirmedIdle) {
if (!addPath(owners, vehicle.getPath(), vehicle.getDeviceId(), current, topology, coordinates)) {
blockFloor(owners, current.getFloorCode(), vehicle.getDeviceId(), topology);
}
}
}
for (ShadowQueueFact queue : batch.getQueues()) {
if (!queue.isActive() || (!Integer.valueOf(1).equals(queue.getIssued())
&& !Integer.valueOf(3).equals(queue.getExecutionStatus()))) continue;
String owner = queue.getAgvId() == null ? "" : queue.getAgvId().trim();
ShadowVehicleFact vehicle = vehicles.get(owner);
if (vehicle == null) throw new IllegalArgumentException("OBSERVED_QUEUE_VEHICLE_UNKNOWN:" + queue.getGuid());
if (!addPath(owners, queue.getPathInfo(), owner, topology.node(vehicle.getCurrentSite()), topology, coordinates)) {
blockFloor(owners, topology.node(vehicle.getCurrentSite()).getFloorCode(), owner, topology);
}
}
return owners;
}
// path/path_info are movement-ordered, not the PLC [start,end,turns] encoding.
// Expand straight segments through topology coordinates, including WY/TD nodes.
private static boolean addPath(Map<String, Set<String>> owners, String path, String owner,
TopologyNode current, EffectiveTopology topology, Map<String, String> coordinates) {
if (path == null || path.trim().isEmpty()) return false;
List<String> anchors = new ArrayList<String>();
for (String token : path.split(",")) {
String id = token.trim();
if (id.isEmpty() || !topology.containsNode(id)) return false;
if (!topology.node(id).getFloorCode().equals(current.getFloorCode())) {
throw new IllegalArgumentException("OBSERVED_PATH_CROSS_FLOOR:" + owner);
}
anchors.add(id);
}
if (anchors.isEmpty()) return false;
Set<String> resources = new LinkedHashSet<String>();
resources.add("NODE:" + anchors.get(0));
for (int i = 1; i < anchors.size(); i++) {
TopologyNode from = topology.node(anchors.get(i - 1));
TopologyNode to = topology.node(anchors.get(i));
if (!from.getFloorCode().equals(to.getFloorCode())
|| (from.getX() != to.getX() && from.getY() != to.getY())) return false;
int dx = Integer.compare(to.getX(), from.getX());
int dy = Integer.compare(to.getY(), from.getY());
int x = from.getX(), y = from.getY();
String previous = from.getNodeId();
while (x != to.getX() || y != to.getY()) {
x += dx;
y += dy;
String next = coordinates.get(coordinate(from.getFloorCode(), x, y));
if (next == null || topology.edge(previous, next) == null) return false;
resources.add("NODE:" + next);
resources.add(TopologyEdge.canonicalResourceKey(previous, next));
previous = next;
}
}
if (!resources.contains("NODE:" + current.getNodeId())) return false;
for (String key : resources) add(owners, key, owner);
return true;
}
private static void blockFloor(Map<String, Set<String>> owners, String floor,
String owner, EffectiveTopology topology) {
for (String id : topology.getNodeIds()) {
if (topology.node(id).getFloorCode().equals(floor)) add(owners, "NODE:" + id, owner);
}
}
private static void add(Map<String, Set<String>> owners, String resource, String owner) {
owners.computeIfAbsent(resource, key -> new LinkedHashSet<String>()).add(owner);
}
private static String coordinate(String floor, int x, int y) { return floor + ':' + x + ':' + y; }
}