fix(shadow): repair field routing and harden evidence collection

This commit is contained in:
zgw
2026-09-07 12:42:17 +08:00
parent 9829b9c0fa
commit 414b3bf4a9
37 changed files with 1751 additions and 103 deletions

View File

@@ -14,6 +14,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
@@ -114,27 +115,9 @@ public final class DefaultVehicleAssignmentPolicy implements VehicleAssignmentPo
List<String> resources = new ArrayList<String>(approach.getResourceKeys());
resources.addAll(execution.getResourceKeys().subList(
1, execution.getResourceKeys().size()));
return new RoutePlan(nodes, resources,
approach.getDistanceCost() + execution.getDistanceCost(),
countTurns(nodes, topology), topology.version());
}
private static int countTurns(List<String> nodes, EffectiveTopology topology) {
int turns = 0;
int previousX = 0;
int previousY = 0;
for (int i = 1; i < nodes.size(); i++) {
TopologyNode from = topology.node(nodes.get(i - 1));
TopologyNode to = topology.node(nodes.get(i));
int directionX = Integer.compare(to.getX(), from.getX());
int directionY = Integer.compare(to.getY(), from.getY());
if (i > 1 && (previousX != directionX || previousY != directionY)) {
turns++;
}
previousX = directionX;
previousY = directionY;
}
return turns;
// Returning over the approach is valid geometry, but reserves each resource once.
return RoutePlan.fromTopology(nodes, new ArrayList<String>(new LinkedHashSet<String>(resources)),
approach.getDistanceCost() + execution.getDistanceCost(), topology);
}
private static final class ScoredVehicle {

View File

@@ -163,8 +163,7 @@ public final class AStarRoutePlanner implements RoutePlanner {
}
private RoutePlan toRoutePlan(EffectiveTopology topology) {
return new RoutePlan(nodeIds, resourceKeys, distanceCost,
turnCount, topology.version());
return RoutePlan.fromTopology(nodeIds, resourceKeys, distanceCost, topology);
}
private int getEstimatedTotalCost() {

View File

@@ -1,5 +1,7 @@
package com.ruoyi.wcs.domain.traffic;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
import com.ruoyi.wcs.domain.topology.TopologyNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -10,9 +12,18 @@ public final class RoutePlan {
private final int distanceCost;
private final int turnCount;
private final long topologyVersion;
private final List<String> turnNodeIds;
public RoutePlan(List<String> nodeIds, List<String> resourceKeys,
int distanceCost, int turnCount, long topologyVersion) {
this(nodeIds, resourceKeys, distanceCost, turnCount, topologyVersion,
nodeIds != null && nodeIds.size() <= 2 && turnCount == 0
? Collections.<String>emptyList() : null);
}
private RoutePlan(List<String> nodeIds, List<String> resourceKeys,
int distanceCost, int turnCount, long topologyVersion,
List<String> turnNodeIds) {
this.nodeIds = immutable(nodeIds, "nodeIds");
this.resourceKeys = immutable(resourceKeys, "resourceKeys");
if (this.nodeIds.isEmpty()) {
@@ -24,6 +35,34 @@ public final class RoutePlan {
this.distanceCost = distanceCost;
this.turnCount = turnCount;
this.topologyVersion = topologyVersion;
this.turnNodeIds = turnNodeIds == null ? null : immutable(turnNodeIds, "turnNodeIds");
}
public static RoutePlan fromTopology(List<String> nodes, List<String> resources,
int distanceCost, EffectiveTopology topology) {
List<String> turns = new ArrayList<String>();
int previousX = 0, previousY = 0;
for (int i = 1; i < nodes.size(); i++) {
TopologyNode from = topology.node(nodes.get(i - 1));
TopologyNode to = topology.node(nodes.get(i));
int directionX = Integer.compare(to.getX(), from.getX());
int directionY = Integer.compare(to.getY(), from.getY());
if (i > 1 && (previousX != directionX || previousY != directionY)) {
turns.add(nodes.get(i - 1));
}
previousX = directionX;
previousY = directionY;
}
return new RoutePlan(nodes, resources, distanceCost, turns.size(), topology.version(), turns);
}
public boolean hasTurnGeometry() {
return turnNodeIds != null;
}
public List<String> getTurnNodeIds() {
if (turnNodeIds == null) throw new IllegalStateException("route turn geometry is unavailable");
return turnNodeIds;
}
public List<String> getNodeIds() {

View File

@@ -28,6 +28,21 @@ class DefaultVehicleAssignmentPolicyTest {
private final VehicleAssignmentPolicy policy = new DefaultVehicleAssignmentPolicy(
new AStarRoutePlanner(), DispatchPolicy.NONE);
@Test
void returningOverApproachRouteDoesNotDuplicateReservationResources() {
DispatchTask task = new DispatchTask("O1", "S1", "A", "B", 10, 60L, 30,
Collections.<DeviceCapability>emptySet());
VehicleAssignmentDecision assigned = policy.assign(task,
Collections.singletonList(vehicle("V1", "C", 90, true,
VehicleSnapshot.ConnectionState.ONLINE)), topology);
assertEquals(Arrays.asList("C", "B", "A", "B"), assigned.getRoute().getNodeIds());
assertEquals(5, assigned.getRoute().getResourceKeys().size());
com.ruoyi.wcs.domain.traffic.ReservationRequest request =
new com.ruoyi.wcs.domain.traffic.ReservationRequest("R1", "O1", "S1", "V1",
assigned.getRoute().getResourceKeys());
assertTrue(new com.ruoyi.wcs.domain.traffic.ResourceLedger(topology).reserve(request).isGranted());
}
@Test
void rejectsCloserVehicleWithoutRequiredCapability() {
VehicleAssignmentDecision decision = policy.assign(taskRequiringLoad(),