fix(scheduling): fix vehicle type classification and algorithm candidate matching
- classifyVehicleType now parses dic_type.dic_name (e.g. "4.5吨冷链车") instead of raw model code - Remove overly strict completionRate >= 0.8 filter for hopeless candidates - Use vehicle's yearTarget as fallback when inventory has no assessment target - Filter out suggestions with no candidates (not actionable) - estimatedGain counts rescue_hopeless suggestions as potential gains Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,15 +71,14 @@ export function generateSuggestions(
|
||||
.filter(
|
||||
(inv) =>
|
||||
isTypeCompatible(vehicle.vehicleType, inv.vehicleType) &&
|
||||
inv.region === vehicle.region &&
|
||||
inv.completionRate >= 0.8,
|
||||
inv.region === vehicle.region,
|
||||
)
|
||||
.map((inv) => {
|
||||
const mileageGap = (inv.yearTarget ?? 0) - inv.totalMileage;
|
||||
const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget;
|
||||
const mileageGap = Math.max(0, effectiveTarget - inv.totalMileage);
|
||||
const predictedAfterSwap =
|
||||
inv.totalMileage + vehicle.customerAvgDaily * vehicle.daysLeft;
|
||||
const canQualifyAfterSwap =
|
||||
inv.yearTarget != null && predictedAfterSwap >= inv.yearTarget;
|
||||
const canQualifyAfterSwap = predictedAfterSwap >= effectiveTarget;
|
||||
return {
|
||||
plateNumber: inv.plateNumber,
|
||||
targetId: inv.targetId,
|
||||
@@ -87,7 +86,7 @@ export function generateSuggestions(
|
||||
vehicleType: inv.vehicleType,
|
||||
totalMileage: inv.totalMileage,
|
||||
completionRate: inv.completionRate,
|
||||
yearTarget: inv.yearTarget,
|
||||
yearTarget: inv.yearTarget ?? vehicle.yearTarget,
|
||||
region: inv.region,
|
||||
province: inv.province,
|
||||
mileageGap,
|
||||
@@ -98,6 +97,7 @@ 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;
|
||||
})
|
||||
.slice(0, 5);
|
||||
@@ -125,11 +125,11 @@ export function generateSuggestions(
|
||||
inv.region === vehicle.region,
|
||||
)
|
||||
.map((inv) => {
|
||||
const mileageGap = (inv.yearTarget ?? 0) - inv.totalMileage;
|
||||
const effectiveTarget = inv.yearTarget ?? vehicle.yearTarget;
|
||||
const mileageGap = Math.max(0, effectiveTarget - inv.totalMileage);
|
||||
const predictedAfterSwap =
|
||||
inv.totalMileage + vehicle.customerAvgDaily * vehicle.daysLeft;
|
||||
const canQualifyAfterSwap =
|
||||
inv.yearTarget != null && predictedAfterSwap >= inv.yearTarget;
|
||||
const canQualifyAfterSwap = predictedAfterSwap >= effectiveTarget;
|
||||
return {
|
||||
plateNumber: inv.plateNumber,
|
||||
targetId: inv.targetId,
|
||||
@@ -137,7 +137,7 @@ export function generateSuggestions(
|
||||
vehicleType: inv.vehicleType,
|
||||
totalMileage: inv.totalMileage,
|
||||
completionRate: inv.completionRate,
|
||||
yearTarget: inv.yearTarget,
|
||||
yearTarget: inv.yearTarget ?? vehicle.yearTarget,
|
||||
region: inv.region,
|
||||
province: inv.province,
|
||||
mileageGap,
|
||||
@@ -164,22 +164,27 @@ export function generateSuggestions(
|
||||
});
|
||||
}
|
||||
|
||||
// Remove suggestions with no candidates
|
||||
const filteredSuggestions = suggestions.filter((s) => s.candidates.length > 0);
|
||||
|
||||
// Sort: high priority first
|
||||
suggestions.sort((a, b) => {
|
||||
filteredSuggestions.sort((a, b) => {
|
||||
if (a.priority === b.priority) return 0;
|
||||
return a.priority === 'high' ? -1 : 1;
|
||||
});
|
||||
|
||||
const estimatedGain = suggestions.filter((s) =>
|
||||
s.candidates.some((c) => c.canQualifyAfterSwap),
|
||||
// estimatedGain: count suggestions where at least one candidate canQualifyAfterSwap,
|
||||
// plus rescue_hopeless suggestions (each rescued car can potentially qualify at a new customer)
|
||||
const estimatedGain = filteredSuggestions.filter((s) =>
|
||||
s.candidates.some((c) => c.canQualifyAfterSwap) || s.type === 'rescue_hopeless',
|
||||
).length;
|
||||
|
||||
const summary: SchedulingSummary = {
|
||||
qualifiedCount: qualified.length,
|
||||
hopelessCount: hopeless.length,
|
||||
suggestionCount: suggestions.length,
|
||||
suggestionCount: filteredSuggestions.length,
|
||||
estimatedGain,
|
||||
};
|
||||
|
||||
return { suggestions, summary };
|
||||
return { suggestions: filteredSuggestions, summary };
|
||||
}
|
||||
|
||||
@@ -12,13 +12,18 @@ import type { AuthUser } from '../../auth/types.js';
|
||||
// Helper: vehicle type classification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function classifyVehicleType(type: string, model: string): string {
|
||||
if (type === '4.5T' && model.includes('冷链')) return '4.5T冷链';
|
||||
if (type === '4.5T') return '4.5T普货';
|
||||
if (type === '18T') return '18T';
|
||||
if (type === '49T') return '49T';
|
||||
if (type === '挂车' || model.includes('挂车')) return '挂车';
|
||||
return type || '其他';
|
||||
/**
|
||||
* Classify vehicle type from dic_type.dic_name (e.g. "4.5吨冷链车", "4.5吨货车", "18吨双飞翼货车").
|
||||
* The typeName is the full label from the dictionary, modelRaw is the numeric dic_code.
|
||||
*/
|
||||
function classifyVehicleType(typeName: string, _modelRaw: string): string {
|
||||
const t = (typeName || '').trim();
|
||||
if (t.includes('4.5') && t.includes('冷链')) return '4.5T冷链';
|
||||
if (t.includes('4.5')) return '4.5T普货';
|
||||
if (t.includes('18')) return '18T';
|
||||
if (t.includes('49') || t.includes('牵引')) return '49T';
|
||||
if (t.includes('挂车')) return '挂车';
|
||||
return t || '其他';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user