From 9bf9bdd8ffb817c428928246ebc8692abcf88c2b Mon Sep 17 00:00:00 2001 From: kkfluous Date: Thu, 16 Apr 2026 20:11:51 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=99=BA=E8=83=BD=E8=B0=83=E5=BA=A6?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E8=AE=BE=E8=AE=A1=E8=A7=84=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-04-16-smart-scheduling-design.md | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-16-smart-scheduling-design.md diff --git a/docs/superpowers/specs/2026-04-16-smart-scheduling-design.md b/docs/superpowers/specs/2026-04-16-smart-scheduling-design.md new file mode 100644 index 0000000..208a447 --- /dev/null +++ b/docs/superpowers/specs/2026-04-16-smart-scheduling-design.md @@ -0,0 +1,224 @@ +# 智能调度模块设计 + +基于里程考核数据,通过贪心优先级匹配算法,生成车辆替换建议,帮助调度员优化车队里程分布,最大化达标车辆数。 + +## 业务背景 + +公司有多批次考核车辆(40台普货、190台冷藏车等),每批次有年度里程考核目标。车辆租赁给不同客户,客户实际使用强度差异大。考核的是**车辆本身的里程**,因此需要通过替换车辆来均衡里程: +- 高里程客户的已达标车换下来,换上里程缺口大的车(让新车追赶) +- 低里程客户的无望达标车换下来给高里程客户(抢救),给低里程客户换上已达标的车 + +## 核心算法 + +### 车辆分类 + +从 `tab_mileage_assessment_vehicle` 获取所有考核车辆,按客户聚合计算**客户日均里程**(客户下所有车辆近 30 天日均里程的平均值),然后对每辆车计算: + +``` +预测年终里程 = 当前累计里程 + 客户日均里程 × 剩余天数 +达标概率 = 预测年终里程 / 年度目标里程 +``` + +分为三类: + +| 类型 | 条件 | 含义 | +|------|------|------| +| qualified | `currentYearIsQualified = true` 或 达标概率 ≥ 120% | 已完成或铁定完成 | +| hopeless | 达标概率 < 60% | 按当前客户使用强度,年底肯定完不成 | +| normal | 60% ≤ 达标概率 < 120% | 有希望但不确定,暂不干预 | + +### 替换建议生成 + +**场景 A:replace_qualified(高里程客户的已达标车辆)** +- 目标:把已达标的车换下来,换上里程缺口大的库存车 +- 候选池:库存车(rent_status='在库')+ 同车型 + 同区域 +- 排序:优先选剩余缺口最大但换后仍可达标的车 +- 校验:`候选车当前累计 + 客户日均 × 剩余天数 ≥ 年度目标` 才推荐 + +**场景 B:rescue_hopeless(低里程客户的无望达标车辆)** +- 目标:把无望车换给高里程客户抢救,给低里程客户换上已达标/库存车 +- 候选池:库存中已达标或将达标的同车型同区域车辆 +- 排序:优先选已达标且里程最高的车(对低里程客户无影响) + +### 车型匹配规则 + +| 源车型 | 可替换为 | 说明 | +|--------|---------|------| +| 4.5T冷链 | 4.5T冷链、4.5T普货 | 冷链不开空调可当普货用 | +| 4.5T普货 | 4.5T普货 | 不能反向替换冷链 | +| 18T | 18T | 同型号互换 | +| 49T | 49T | 同型号互换 | +| 挂车 | 挂车 | 同型号互换 | + +### 区域匹配规则 + +复用已有 `mapRegion()` 函数,将 province/city 映射到大区(嘉兴/广东/北京/新疆/其他)。同一大区内可替换,跨大区不推荐。 + +### 优先级排序 + +干预清单排序: +1. **hopeless + 有可行替换方案** → priority: high(最紧急,还能抢救) +2. **qualified + 高里程客户 + 有库存可换** → priority: medium(释放达标车,让新车追赶) + +## 后端 API + +### GET /api/scheduling/suggestions + +获取调度建议列表。每次请求实时计算(不使用定时缓存),因为用户操作后需要立即看到最新结果。 + +**请求参数**: + +| 参数 | 类型 | 说明 | +|------|------|------| +| targetId | number (可选) | 按批次筛选,不传则全部 | + +**响应**: + +```typescript +{ + summary: { + qualifiedCount: number; // 已达标车辆数 + hopelessCount: number; // 无望达标车辆数 + suggestionCount: number; // 可干预建议数 + estimatedGain: number; // 预计干预后可新增达标数 + }; + suggestions: SchedulingSuggestion[]; + targets: { id: number; name: string; vehicleCount: number }[]; +} +``` + +`SchedulingSuggestion` 结构: + +```typescript +{ + id: string; // 建议唯一ID(如 "s-{plate}-{timestamp}") + priority: 'high' | 'medium'; + type: 'replace_qualified' | 'rescue_hopeless'; + + currentVehicle: { + plateNumber: string; + targetId: number; + targetName: string; // 所属批次 + vehicleType: string; // "4.5T冷链" / "18T" 等 + totalMileage: number; + completionRate: number; // 0-1 + yearTarget: number; // 年度目标里程 + region: string; // 大区(嘉兴/广东等) + province: string; // 原始省份 + customer: string; + customerAvgDaily: number; // 客户日均里程 + predictedYearEnd: number; // 预测年终里程 + daysLeft: number; + }; + + candidates: { + plateNumber: string; + targetId: number | null; // 库存车可能无批次 + targetName: string | null; + vehicleType: string; + totalMileage: number; + completionRate: number; + yearTarget: number | null; + region: string; + province: string; + mileageGap: number; // 剩余缺口 + predictedAfterSwap: number; // 换到该客户后预测年终里程 + canQualifyAfterSwap: boolean; + }[]; + + reason: string; // 建议原因文案 +} +``` + +### POST /api/scheduling/notify + +发送替换通知。成功后前端立即重新拉取 suggestions。 + +**请求体**: + +```typescript +{ + suggestionId: string; + currentPlate: string; + candidatePlate: string; +} +``` + +操作人从 JWT auth 中获取。 + +**响应**:`{ success: boolean; message: string }` + +**行为**:调用外部回调接口发送通知(具体回调 URL 后续配置)。成功后在本地记录已操作状态,后续 GET suggestions 时排除已操作的建议。 + +### 数据查询流程 + +后端一次请求聚合以下数据: +1. 所有考核车辆 — `tab_mileage_assessment_vehicle`(里程进度、达标状态) +2. 所有考核目标 — `tab_mileage_assessment_target`(批次名称、年度目标) +3. 库存车辆 — `tab_truck WHERE truck_rent_status = 0`(在库)+ 同表获取车型 +4. 车辆实时位置 — `tab_truck_remote_sync_realtime_info`(province, city) +5. 合同/客户信息 — 复用 `vehicle-info.ts` 已有的 JOIN 查询 +6. 客户日均里程 — 按客户聚合 `v_vehicle_daily_stats` 近 30 天均值 + +## 前端结构 + +### 文件组织 + +``` +src/modules/scheduling/ +├── SchedulingModule.tsx // 主入口,状态管理和数据加载 +├── SuggestionList.tsx // 干预建议清单列表 +├── SuggestionDetail.tsx // 单条建议展开详情(含替换车辆对比) +├── api.ts // fetchSuggestions(), sendNotify() +└── types.ts // SchedulingSuggestion 等类型定义 +``` + +后端: +``` +src/server/routes/scheduling/ +├── index.ts // 路由注册 +├── suggestions.ts // GET /suggestions 算法核心 +└── notify.ts // POST /notify 回调通知 +``` + +### 页面层级 + +``` +智能调度 Tab +├── 顶部:批次选择器(复用里程统计的批次 tabs,默认"全部") +├── 统计卡片区(3 个) +│ ├── 已达标车辆数(绿色) +│ ├── 无望达标车辆数(红色) +│ └── 可干预建议数 + 预计可新增达标数(蓝色) +├── 干预建议清单(主列表,按优先级排序) +│ ├── 每条:车牌、批次、客户、客户日均、完成率、区域、类型标签(已达标/无望) +│ └── 点击 → 展开干预详情 +└── 干预详情(弹窗) + ├── 当前车辆信息卡片 + ├── 推荐替换车辆列表(最多 5 辆) + │ └── 每辆显示对比:替换前后的区域、车型、里程、预测达标 + ├── 建议原因说明 + └── 「发送替换通知」按钮 → notify 接口 → 成功后刷新列表 +``` + +### UI 设计要求 + +- 以原型 `SmartSchedulingView` 组件为基础风格 +- 使用 ui-ux-pro-max 优化视觉质量 +- 适配移动端(竖屏卡片流)和 Web 端(landscape 横屏大表格) +- 干预详情弹窗需截图友好:完整卡片布局、替换前后对比一屏可见、关键数据醒目 +- 统计卡片区保持与原型一致的三列 grid 布局 +- 批次选择器横向滚动 pill 按钮样式 + +### 技术栈 + +复用项目已有:React 19 + Tailwind CSS + motion/react(动画)+ recharts(图表)+ lucide-react(图标) + +## 约束与边界 + +- 替换仅为建议,不直接操作数据库修改车辆归属 +- 不能推荐已租赁给其他客户的车辆,只从库存(在库)中推荐 +- 跨批次可替换,但车型必须匹配(含冷链→普货单向规则) +- 同大区内替换,不跨大区 +- notify 操作后数据立即更新(不使用定时缓存) +- 客户名称展示需使用已有的脱敏/Blur 组件