fix(scheduling): exclude near-qualified vehicles from rescue candidates

For rescue_hopeless (换走) scenario, filter out inventory candidates
where totalMileage/yearTarget >= 80%. These are already near target
and swapping them in adds no value.

Instead, prioritize candidates with biggest mileage gaps — they benefit
most from accumulating any mileage, even at a low-mileage customer.

Before: showed 粤AGR6869 (93% done, 缺口 1990) as "可达标" — pointless
After:  shows 浙FF58720 (0% done, 缺口 60000) — genuinely needs mileage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-16 21:34:05 +08:00
parent 81305be2df
commit 1d1f8901aa

View File

@@ -76,13 +76,21 @@ export function generateSuggestions(
const suggestions: SchedulingSuggestion[] = [];
// --- rescue_hopeless (high priority) ---
// For this scenario: take the hopeless car away to a high-mileage customer,
// and give the low-mileage customer a replacement from inventory.
// Exclude near-qualified candidates (completionRate >= 80%) — no point swapping
// in a car that's basically already at target.
// Instead, pick cars with BIG gaps: they benefit from any mileage, even at a low customer.
for (const vehicle of hopeless) {
const candidates: CandidateVehicle[] = inventoryVehicles
.filter(
(inv) =>
isTypeCompatible(vehicle.vehicleType, inv.vehicleType) &&
inv.region === vehicle.region,
)
.filter((inv) => {
if (!isTypeCompatible(vehicle.vehicleType, inv.vehicleType)) return false;
if (inv.region !== vehicle.region) return false;
// Exclude near-qualified: yearTarget known and already >= 80% done
const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget;
if (effectiveTarget > 0 && inv.totalMileage / effectiveTarget >= 0.8) return false;
return true;
})
.map((inv) => {
const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget;
const mileageGap = Math.max(0, effectiveTarget - inv.totalMileage);
@@ -105,10 +113,8 @@ export function generateSuggestions(
};
})
.sort((a, b) => {
if (a.canQualifyAfterSwap !== b.canQualifyAfterSwap)
return a.canQualifyAfterSwap ? -1 : 1;
// For hopeless: prefer already-qualified inventory, then highest completion
return b.completionRate - a.completionRate;
// Prefer biggest gap first — these benefit most from any mileage
return b.mileageGap - a.mileageGap;
})
.slice(0, 5);