feat: add deterministic vehicle assignment demo

This commit is contained in:
zgw
2026-07-30 11:19:36 +08:00
parent 010bea9e5e
commit 828a68aa6c
4 changed files with 299 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
package com.ft546.wcs.demo.center;
import com.ft546.wcs.demo.model.DecisionCode;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import java.util.Optional;
public final class VehicleAssignmentDecision {
private final DecisionCode code;
private final VehicleSnapshot vehicle;
private final String reason;
private VehicleAssignmentDecision(
DecisionCode code,
VehicleSnapshot vehicle,
String reason
) {
this.code = code;
this.vehicle = vehicle;
this.reason = reason;
}
public static VehicleAssignmentDecision assigned(
VehicleSnapshot vehicle,
String reason
) {
return new VehicleAssignmentDecision(DecisionCode.ASSIGNED, vehicle, reason);
}
public static VehicleAssignmentDecision rejected(
DecisionCode code,
String reason
) {
if (code == DecisionCode.ASSIGNED) {
throw new IllegalArgumentException("rejected decision cannot be ASSIGNED");
}
return new VehicleAssignmentDecision(code, null, reason);
}
public DecisionCode getCode() {
return code;
}
public Optional<VehicleSnapshot> getVehicle() {
return Optional.ofNullable(vehicle);
}
public String getReason() {
return reason;
}
}

View File

@@ -0,0 +1,121 @@
package com.ft546.wcs.demo.center;
import com.ft546.wcs.demo.model.DecisionCode;
import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.demo.model.Site;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
public final class VehicleAssignmentService {
public VehicleAssignmentDecision select(
DispatchTask task,
List<VehicleSnapshot> vehicles,
Map<String, Site> sites
) {
if (task == null || vehicles == null || sites == null) {
throw new IllegalArgumentException("task, vehicles and sites are required");
}
Site source = sites.get(task.getSourceSite());
if (source == null) {
return VehicleAssignmentDecision.rejected(
DecisionCode.REJECT_INVALID_SITE,
"unknown task source site: " + task.getSourceSite()
);
}
List<Candidate> candidates = new ArrayList<Candidate>();
boolean eligibleWithUnknownSite = false;
for (VehicleSnapshot vehicle : vehicles) {
if (!isEligible(vehicle)) {
continue;
}
Site current = sites.get(vehicle.getCurrentSite());
if (current == null) {
eligibleWithUnknownSite = true;
continue;
}
candidates.add(new Candidate(vehicle, current, source));
}
if (candidates.isEmpty()) {
if (eligibleWithUnknownSite) {
return VehicleAssignmentDecision.rejected(
DecisionCode.REJECT_INVALID_SITE,
"eligible vehicle references an unknown current site"
);
}
return VehicleAssignmentDecision.rejected(
DecisionCode.HOLD_NO_ELIGIBLE_VEHICLE,
"no connected automatic idle vehicle with battery > 30 is available"
);
}
Collections.sort(candidates, Candidate.ORDER);
Candidate selected = candidates.get(0);
return VehicleAssignmentDecision.assigned(
selected.vehicle,
"vehicle=" + selected.vehicle.getVehicleId()
+ ",sameFloorPenalty=" + selected.sameFloorPenalty
+ ",floorDistance=" + selected.floorDistance
+ ",yDistance=" + selected.yDistance
+ ",xDistance=" + selected.xDistance
);
}
private boolean isEligible(VehicleSnapshot vehicle) {
return vehicle != null
&& vehicle.isConnected()
&& vehicle.getWorkMode() == 5
&& vehicle.getStatus() == 1
&& vehicle.isAllowSend()
&& vehicle.getBattery() > 30;
}
private static final class Candidate {
private static final Comparator<Candidate> ORDER = new Comparator<Candidate>() {
@Override
public int compare(Candidate left, Candidate right) {
int result = Integer.compare(
left.sameFloorPenalty,
right.sameFloorPenalty
);
if (result != 0) {
return result;
}
result = Integer.compare(left.floorDistance, right.floorDistance);
if (result != 0) {
return result;
}
result = Integer.compare(left.yDistance, right.yDistance);
if (result != 0) {
return result;
}
result = Integer.compare(left.xDistance, right.xDistance);
if (result != 0) {
return result;
}
return left.vehicle.getVehicleId().compareTo(right.vehicle.getVehicleId());
}
};
private final VehicleSnapshot vehicle;
private final int sameFloorPenalty;
private final int floorDistance;
private final int yDistance;
private final int xDistance;
private Candidate(VehicleSnapshot vehicle, Site current, Site source) {
this.vehicle = vehicle;
this.sameFloorPenalty = vehicle.getFloor() == source.getFloor() ? 0 : 1;
this.floorDistance = Math.abs(vehicle.getFloor() - source.getFloor());
this.yDistance = Math.abs(current.getY() - source.getY());
this.xDistance = Math.abs(current.getX() - source.getX());
}
}
}

View File

@@ -0,0 +1,14 @@
package com.ft546.wcs.demo.model;
public enum DecisionCode {
ASSIGNED,
PATH_READY,
AVOIDANCE_CREATED,
HOLD_NO_ELIGIBLE_VEHICLE,
REJECT_INVALID_SITE,
HOLD_NO_PATH,
HOLD_CHANNEL_BLOCKED,
HOLD_PROTOCOL_FAILURE,
SENT,
COMPLETED
}