From 0785c783822c7dcc299fac0fcf55941958ceae2c Mon Sep 17 00:00:00 2001 From: kkfluous Date: Thu, 16 Apr 2026 21:41:08 +0800 Subject: [PATCH] fix(scheduling): only show candidates that can actually qualify after swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit replace_qualified (换下): - Exclude already-qualified inventory (totalMileage >= yearTarget, gap=0) - Only keep candidates where canQualifyAfterSwap=true - Skip suggestions with no qualifiable candidates (e.g., too few days left) - Reason text now shows customer's remaining capacity: "日均 318km × 53天 ≈ 1.7万km" Before: showed 粤AGP9738 (缺口 0, already at target) — pointless After: shows 粤AGQ5808 (缺口 1.7万, 换后 3.0万, 可达标) — meaningful All replace_qualified candidates now guaranteed canQualifyAfterSwap=true. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/server/routes/scheduling/algorithm.ts | 28 +++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/server/routes/scheduling/algorithm.ts b/src/server/routes/scheduling/algorithm.ts index e43669e..16c6fdb 100644 --- a/src/server/routes/scheduling/algorithm.ts +++ b/src/server/routes/scheduling/algorithm.ts @@ -144,15 +144,21 @@ export function generateSuggestions( } // --- replace_qualified (medium priority) --- + // Swap out the qualified car, swap in a car that NEEDS mileage. + // The high-mileage customer will drive it hard → helps it reach target. + // Exclude candidates already at target (gap <= 0) — swapping those in is pointless. for (const vehicle of qualified) { if (vehicle.customerAvgDaily <= vehicle.dailyRequiredMileage) continue; 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; + // Must still need mileage — exclude already-qualified inventory + const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget; + if (effectiveTarget > 0 && inv.totalMileage >= effectiveTarget) return false; + return true; + }) .map((inv) => { const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget; const mileageGap = Math.max(0, effectiveTarget - inv.totalMileage); @@ -175,15 +181,23 @@ export function generateSuggestions( }; }) .sort((a, b) => { + // 1. canQualifyAfterSwap first if (a.canQualifyAfterSwap !== b.canQualifyAfterSwap) return a.canQualifyAfterSwap ? -1 : 1; + // 2. Among qualifiable: biggest gap first (most value from the swap) return b.mileageGap - a.mileageGap; }) + // Only keep candidates that can actually qualify at this customer + .filter(c => c.canQualifyAfterSwap) .slice(0, 5); + // Skip if no candidate can reach target — swap would be pointless + if (candidates.length === 0) continue; + const yearRate = vehicle.yearTarget > 0 ? Math.round((vehicle.currentYearMileage / vehicle.yearTarget) * 100) : 0; - const reason = `该车在客户「${vehicle.customer}」处已达标(完成率 ${yearRate}%),客户日均 ${Math.round(vehicle.customerAvgDaily)} km,属于高里程客户。` - + `\n建议:将此车换下,换上一辆里程少的车,利用该客户的高日均里程帮助新车快速达标。`; + const canAddKm = vehicle.customerAvgDaily * vehicle.daysLeft; + const reason = `该车在客户「${vehicle.customer}」处已达标(完成率 ${yearRate}%),客户日均 ${Math.round(vehicle.customerAvgDaily)} km × ${vehicle.daysLeft} 天 ≈ ${fmtKmSimple(canAddKm)} km。` + + `\n建议:换上里程未达标的车,利用该客户的高日均帮新车快速冲线。`; suggestions.push({ id: `qualified-${vehicle.plateNumber}`,