# Mileage Backend Refactor Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Refactor `src/server/routes/mileage.ts` (569 lines) into well-typed, modular files with clear responsibilities, eliminating duplicate logic and `as any` casts. **Architecture:** Split the monolithic route file into: shared types, a reusable vehicle-info query module, a monitoring cache module, and focused route handlers. The API contract (request/response shapes) stays identical — this is a pure internal refactor with zero frontend changes. **Tech Stack:** Hono, mysql2/promise, TypeScript strict types --- ## File Structure | File | Responsibility | |------|---------------| | `src/server/routes/mileage/types.ts` | All interfaces for mileage domain (cache, vehicles, filters, API responses) | | `src/server/routes/mileage/vehicle-info.ts` | Shared SQL + helper to build plate→info Map from `lingniu_prod` | | `src/server/routes/mileage/cache.ts` | Monitoring cache: refresh logic, data merging, filter precomputation, target mapping | | `src/server/routes/mileage/monitoring.ts` | `GET /monitoring` route handler | | `src/server/routes/mileage/targets.ts` | `GET /targets`, `GET /target/:id/vehicles` route handlers | | `src/server/routes/mileage/trend.ts` | `GET /trend` route handler | | `src/server/routes/mileage/index.ts` | Hono app assembly: imports routes, starts cache timer, exports app | After refactor, delete: `src/server/routes/mileage.ts` (the old monolith). ## Constraints - **Zero API changes** — all request params and response JSON shapes must remain identical - **Zero frontend changes** — `src/modules/mileage/api.ts` and `types.ts` stay untouched - **Preserve all existing behavior** including cache refresh interval, date queries, filter logic --- ### Task 1: Create shared types **Files:** - Create: `src/server/routes/mileage/types.ts` - [ ] **Step 1: Create the types file** ```typescript // src/server/routes/mileage/types.ts /** 缓存中的单辆车数据 */ export interface CachedVehicle { plate: string; vin: string; dailyKm: number; totalKm: number | null; source: string; isOnline: boolean; isDataSynced: boolean; customer: string | null; department: string | null; manager: string | null; rentStatus: string | null; entity: string | null; project: string | null; yesterdayKm: number; } /** 车牌前缀统计 */ export interface PlatePrefix { prefix: string; count: number; } /** 筛选选项(前端下拉) */ export interface MonitoringFilters { departments: string[]; customers: string[]; plates: string[]; projects: string[]; entities: string[]; rentStatuses: string[]; platePrefixes: PlatePrefix[]; targetNames: string[]; } /** 监控缓存 */ export interface MonitoringCache { vehicles: CachedVehicle[]; stats: { totalToday: number; totalAll: number; vehicleCount: number }; filters: MonitoringFilters; targetPlatesMap: Map>; updatedAt: string; } /** /monitoring 响应中的统计 */ export interface MonitoringStats { totalToday: number; totalAll: number; vehicleCount: number; yesterdayTotal: number; } /** /monitoring 完整响应 */ export interface MonitoringResponse { vehicles: CachedVehicle[]; stats: MonitoringStats; filters: MonitoringFilters; total: number; page: number; totalPages: number; updatedAt: string; } /** 车辆关联信息(从 lingniu_prod 查出的原始行) */ export interface VehicleInfoRow { plate: string; customer: string | null; department: string | null; manager: string | null; rent_status: string | null; entity: string | null; project: string | null; } ``` - [ ] **Step 2: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors (new file has no imports/consumers yet) - [ ] **Step 3: Commit** ```bash git add src/server/routes/mileage/types.ts git commit -m "refactor: extract mileage shared types" ``` --- ### Task 2: Extract vehicle-info query module **Files:** - Create: `src/server/routes/mileage/vehicle-info.ts` - [ ] **Step 1: Create the vehicle-info module** This extracts the `VEHICLE_INFO_SQL` constant and a helper function to build the info Map. Both the cache builder and the `/target/:id/vehicles` route reuse this. ```typescript // src/server/routes/mileage/vehicle-info.ts import pool from '../../db.js'; import type { VehicleInfoRow } from './types.js'; /** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目) */ export const VEHICLE_INFO_SQL = `SELECT truck.plate_number AS plate, cus.customer_name AS customer, dep.dep_name AS department, u.user_name AS manager, dic_status.dic_name AS rent_status, org_truck.org_name AS entity, c.project_name AS project FROM tab_truck truck LEFT JOIN tab_truck_status_info si ON si.truck_id = truck.id AND si.is_deleted = 0 LEFT JOIN tab_contract c ON c.id = si.contract_id AND c.is_deleted = 0 LEFT JOIN tab_customer cus ON cus.id = c.customer_id AND cus.is_deleted = 0 LEFT JOIN tab_user u ON u.id = c.bd AND u.is_deleted = 0 LEFT JOIN tab_department dep ON dep.id = u.dep_id AND dep.is_deleted = 0 LEFT JOIN tab_dic dic_status ON dic_status.parent_code = 'dic_truck_rent_status' AND dic_status.dic_code = truck.truck_rent_status AND dic_status.is_deleted = 0 LEFT JOIN tab_org org_truck ON org_truck.id = truck.org_id AND org_truck.is_deleted = 0 WHERE truck.is_deleted = 0 AND truck.is_operation = 1`; /** 查询所有车辆关联信息,返回 plate→info 的 Map */ export async function fetchVehicleInfoMap(): Promise> { const [rows] = await pool.execute(VEHICLE_INFO_SQL) as [VehicleInfoRow[], unknown]; const map = new Map(); for (const row of rows) { map.set(row.plate, row); } return map; } /** 查询指定车牌的关联信息 */ export async function fetchVehicleInfoByPlates(plates: string[]): Promise> { if (plates.length === 0) return new Map(); const [rows] = await pool.execute( `${VEHICLE_INFO_SQL} AND truck.plate_number IN (${plates.map(() => '?').join(',')})`, plates ) as [VehicleInfoRow[], unknown]; const map = new Map(); for (const row of rows) { map.set(row.plate, row); } return map; } ``` - [ ] **Step 2: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 3: Commit** ```bash git add src/server/routes/mileage/vehicle-info.ts git commit -m "refactor: extract vehicle-info query module" ``` --- ### Task 3: Extract monitoring cache module **Files:** - Create: `src/server/routes/mileage/cache.ts` - [ ] **Step 1: Create the cache module** This contains the cache singleton, refresh logic, and the `queryDateMileage` function. Both used to live in the monolith. ```typescript // src/server/routes/mileage/cache.ts import pool from '../../db.js'; import mileagePool from '../../mileage-db.js'; import { fetchVehicleInfoMap } from './vehicle-info.js'; import type { CachedVehicle, MonitoringCache, MonitoringFilters, PlatePrefix } from './types.js'; let monitoringCache: MonitoringCache | null = null; export function getCache(): MonitoringCache | null { return monitoringCache; } /** 部门排序顺序 */ const DEPT_ORDER = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; function sortDepartments(departments: string[]): string[] { return departments.sort((a, b) => { const ai = DEPT_ORDER.findIndex(d => a.includes(d)); const bi = DEPT_ORDER.findIndex(d => b.includes(d)); return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi); }); } /** 从车辆列表计算筛选选项 */ function buildFilters(vehicles: CachedVehicle[], targetNames: string[]): MonitoringFilters { const departments = sortDepartments( Array.from(new Set(vehicles.map(v => v.department).filter((d): d is string => d !== null))) ); const customers = Array.from(new Set(vehicles.map(v => v.customer).filter((c): c is string => c !== null))); const plates = vehicles.map(v => v.plate); const projects = Array.from(new Set(vehicles.map(v => v.project).filter((p): p is string => p !== null))); const entities = Array.from(new Set(vehicles.map(v => v.entity).filter((e): e is string => e !== null))); const rentStatuses = Array.from(new Set(vehicles.map(v => v.rentStatus).filter((r): r is string => r !== null))); const prefixCount = new Map(); for (const v of vehicles) { const p = v.plate.charAt(0); prefixCount.set(p, (prefixCount.get(p) || 0) + 1); } const platePrefixes: PlatePrefix[] = Array.from(prefixCount.entries()) .map(([prefix, count]) => ({ prefix, count })) .sort((a, b) => b.count - a.count); return { departments, customers, plates, projects, entities, rentStatuses, platePrefixes, targetNames }; } /** 将里程原始行 + 车辆信息合并为 CachedVehicle 列表 */ function mergeVehicles( mileageRows: { plate: string; vin: string; daily_km: string; total_km: string | null; source: string }[], infoMap: Map, yesterdayMap: Map, ): CachedVehicle[] { // 去重:同一 plate 取 daily_km 最大的 const mileageMap = new Map(); for (const row of mileageRows) { const existing = mileageMap.get(row.plate); if (!existing || Number(row.daily_km) > Number(existing.daily_km)) { mileageMap.set(row.plate, row); } } return Array.from(mileageMap.values()).map(m => { const info = infoMap.get(m.plate); const dailyKm = Number(m.daily_km) || 0; const source = m.source || 'NONE'; return { plate: m.plate, vin: m.vin, dailyKm, totalKm: m.total_km !== null ? Number(m.total_km) : null, source, isOnline: source !== 'NONE' && dailyKm > 0, isDataSynced: source !== 'NONE', customer: info?.customer || null, department: info?.department || null, manager: info?.manager || null, rentStatus: info?.rent_status || null, entity: info?.entity || null, project: info?.project || null, yesterdayKm: yesterdayMap.get(m.plate) || 0, }; }); } /** 刷新监控缓存(从两个数据库并行查询) */ export async function refreshMonitoringCache(): Promise { try { console.log('[mileage] refreshing monitoring cache...'); const start = Date.now(); const [mileageRows, yesterdayMap, infoMap, targetRows] = await Promise.all([ // 最新日期的里程数据 (async () => { const [dateRows] = await mileagePool.execute( 'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats' ) as [{ latest: string | null }[], unknown]; const latestDate = dateRows[0]?.latest; if (!latestDate) return []; const [rows] = await mileagePool.execute( 'SELECT plate, vin, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ?', [latestDate] ) as [any[], unknown]; return rows; })(), // 昨日里程(用于环比) (async () => { const [rows] = await mileagePool.execute( `SELECT plate, daily_km FROM v_vehicle_daily_stats WHERE stat_date = DATE_SUB((SELECT MAX(stat_date) FROM v_vehicle_daily_stats), INTERVAL 1 DAY)` ) as [any[], unknown]; const map = new Map(); for (const r of rows) { const km = Number(r.daily_km) || 0; const existing = map.get(r.plate) || 0; if (km > existing) map.set(r.plate, km); } return map; })(), // 车辆关联信息 fetchVehicleInfoMap(), // 考核批次→车牌映射 pool.execute( `SELECT t.id, t.target_name, v.plate_number FROM tab_mileage_assessment_target t JOIN tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0 WHERE t.is_deleted = 0` ).then(([rows]) => rows as { id: number; target_name: string; plate_number: string }[]), ]); // 构建批次映射 const targetPlatesMap = new Map>(); for (const r of targetRows) { const set = targetPlatesMap.get(r.target_name) || new Set(); set.add(r.plate_number); targetPlatesMap.set(r.target_name, set); } const targetNames = Array.from(targetPlatesMap.keys()); const vehicles = mergeVehicles(mileageRows, infoMap, yesterdayMap); const totalToday = vehicles.reduce((sum, v) => sum + v.dailyKm, 0); const totalAll = vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0); monitoringCache = { vehicles, stats: { totalToday, totalAll, vehicleCount: vehicles.length }, filters: buildFilters(vehicles, targetNames), targetPlatesMap, updatedAt: new Date().toISOString(), }; console.log(`[mileage] cache refreshed: ${vehicles.length} vehicles in ${Date.now() - start}ms`); } catch (e: unknown) { console.error('[mileage] cache refresh error:', e); } } /** 查询指定日期的里程数据(不使用缓存) */ export async function queryDateMileage(dateStr: string): Promise { const [mileageRows, yesterdayRows, infoMap] = await Promise.all([ mileagePool.execute( 'SELECT plate, vin, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ?', [dateStr] ).then(([r]) => r as any[]), mileagePool.execute( 'SELECT plate, daily_km FROM v_vehicle_daily_stats WHERE stat_date = DATE_SUB(?, INTERVAL 1 DAY)', [dateStr] ).then(([r]) => r as any[]), fetchVehicleInfoMap(), ]); const yesterdayMap = new Map(); for (const r of yesterdayRows) { const km = Number(r.daily_km) || 0; const existing = yesterdayMap.get(r.plate) || 0; if (km > existing) yesterdayMap.set(r.plate, km); } return mergeVehicles(mileageRows, infoMap, yesterdayMap); } /** 构建指定日期数据的筛选选项 */ export function buildDateFilters(vehicles: CachedVehicle[]): MonitoringFilters { return buildFilters(vehicles, monitoringCache?.filters.targetNames || []); } ``` - [ ] **Step 2: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 3: Commit** ```bash git add src/server/routes/mileage/cache.ts git commit -m "refactor: extract monitoring cache module" ``` --- ### Task 4: Create monitoring route handler **Files:** - Create: `src/server/routes/mileage/monitoring.ts` - [ ] **Step 1: Create the monitoring route** ```typescript // src/server/routes/mileage/monitoring.ts import { Hono } from 'hono'; import { getCache, queryDateMileage, buildDateFilters } from './cache.js'; import type { CachedVehicle, MonitoringFilters, MonitoringResponse } from './types.js'; const app = new Hono(); const EMPTY_RESPONSE: MonitoringResponse = { vehicles: [], stats: { totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }, filters: { departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [], platePrefixes: [], targetNames: [] }, total: 0, page: 1, totalPages: 1, updatedAt: new Date().toISOString(), }; /** 应用筛选条件 */ function applyFilters(vehicles: CachedVehicle[], params: { search: string; dept: string; customer: string; project: string; entity: string; rentStatus: string; plate: string; platePrefix: string; targetName: string; mileageMin: string; mileageMax: string; }): CachedVehicle[] { let result = vehicles; if (params.search) { const q = params.search.toLowerCase(); result = result.filter(v => v.plate.toLowerCase().includes(q) || (v.customer || '').toLowerCase().includes(q) || (v.project || '').toLowerCase().includes(q) ); } if (params.dept) result = result.filter(v => params.dept === '__EMPTY__' ? !v.department : v.department === params.dept); if (params.customer) result = result.filter(v => params.customer === '__EMPTY__' ? !v.customer : v.customer === params.customer); if (params.project) result = result.filter(v => v.project === params.project); if (params.entity) result = result.filter(v => v.entity === params.entity); if (params.rentStatus) result = result.filter(v => v.rentStatus === params.rentStatus); if (params.plate) result = result.filter(v => v.plate === params.plate); if (params.platePrefix) result = result.filter(v => v.plate.startsWith(params.platePrefix)); if (params.targetName) { const cache = getCache(); const tPlates = cache?.targetPlatesMap.get(params.targetName); result = tPlates ? result.filter(v => tPlates.has(v.plate)) : []; } if (params.mileageMin) result = result.filter(v => v.dailyKm >= Number(params.mileageMin)); if (params.mileageMax) result = result.filter(v => v.dailyKm <= Number(params.mileageMax)); return result; } app.get('/', async (c) => { const sortBy = c.req.query('sortBy') || 'today'; const sortOrder = c.req.query('sortOrder') || 'desc'; const limit = Number(c.req.query('limit')) || 50; const page = Number(c.req.query('page')) || 1; const date = c.req.query('date') || ''; const filterParams = { search: c.req.query('search') || '', dept: c.req.query('dept') || '', customer: c.req.query('customer') || '', project: c.req.query('project') || '', entity: c.req.query('entity') || '', rentStatus: c.req.query('rentStatus') || '', plate: c.req.query('plate') || '', platePrefix: c.req.query('platePrefix') || '', targetName: c.req.query('targetName') || '', mileageMin: c.req.query('mileageMin') || '', mileageMax: c.req.query('mileageMax') || '', }; // 获取数据源 let allVehicles: CachedVehicle[]; let filters: MonitoringFilters; if (date) { try { allVehicles = await queryDateMileage(date); filters = buildDateFilters(allVehicles); } catch (e: unknown) { console.error('monitoring date query error:', e); return c.json(EMPTY_RESPONSE, 500); } } else { const cache = getCache(); if (!cache) return c.json(EMPTY_RESPONSE); allVehicles = cache.vehicles; filters = cache.filters; } // 筛选 const filtered = applyFilters(allVehicles, filterParams); // 统计 const stats = { totalToday: filtered.reduce((sum, v) => sum + v.dailyKm, 0), totalAll: filtered.reduce((sum, v) => sum + (v.totalKm || 0), 0), vehicleCount: filtered.length, yesterdayTotal: filtered.reduce((sum, v) => sum + v.yesterdayKm, 0), }; // 排序 const sorted = [...filtered].sort((a, b) => { const valA = sortBy === 'today' ? a.dailyKm : (a.totalKm || 0); const valB = sortBy === 'today' ? b.dailyKm : (b.totalKm || 0); return sortOrder === 'desc' ? valB - valA : valA - valB; }); // 分页 const offset = (page - 1) * limit; const paged = sorted.slice(offset, offset + limit); const total = filtered.length; return c.json({ vehicles: paged, stats, filters, total, page, totalPages: Math.ceil(total / limit), updatedAt: date || getCache()?.updatedAt || new Date().toISOString(), }); }); export default app; ``` - [ ] **Step 2: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 3: Commit** ```bash git add src/server/routes/mileage/monitoring.ts git commit -m "refactor: create monitoring route handler" ``` --- ### Task 5: Create targets route handler **Files:** - Create: `src/server/routes/mileage/targets.ts` - [ ] **Step 1: Create the targets route** ```typescript // src/server/routes/mileage/targets.ts import { Hono } from 'hono'; import pool from '../../db.js'; import mileagePool from '../../mileage-db.js'; import { getCache } from './cache.js'; import { fetchVehicleInfoByPlates } from './vehicle-info.js'; const app = new Hono(); // GET /targets — 考核项目列表 + 汇总 app.get('/', async (c) => { try { const [targets] = await pool.execute( 'SELECT * FROM tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id' ) as [any[], unknown]; const [vehicleStats] = await pool.execute(` SELECT target_id, COUNT(*) as total, SUM(today_mileage) as today_total, SUM(current_mileage) as cumulative_total, AVG(current_year_completion_rate) as avg_completion, SUM(CASE WHEN is_qualified = 1 THEN 1 ELSE 0 END) as qualified_count, SUM(CASE WHEN current_year_is_qualified = 1 THEN 1 ELSE 0 END) as year_qualified_count, SUM(CASE WHEN current_year_completion_rate >= 0.5 THEN 1 ELSE 0 END) as half_qualified_count, SUM(current_year_mileage_task) as current_year_target, SUM(current_year_mileage) as current_year_completed, MAX(current_year_assessment_end_date) as year_end_date FROM tab_mileage_assessment_vehicle WHERE is_deleted = 0 GROUP BY target_id `) as [any[], unknown]; const statsMap = new Map(); for (const s of vehicleStats) statsMap.set(s.target_id, s); const [periodRows] = await pool.execute(` SELECT target_id, DATE_FORMAT(assessment_start_date, '%Y-%m-%d') as start_date, DATE_FORMAT(assessment_end_date, '%Y-%m-%d') as end_date, COUNT(*) as cnt FROM tab_mileage_assessment_vehicle WHERE is_deleted = 0 GROUP BY target_id, assessment_start_date, assessment_end_date ORDER BY target_id, assessment_start_date `) as [any[], unknown]; const periodsMap = new Map(); for (const p of periodRows) { const list = periodsMap.get(p.target_id) || []; list.push(`${p.start_date} ~ ${p.end_date} (${p.cnt}台)`); periodsMap.set(p.target_id, list); } // 使用监控缓存里程数据(与里程看板一致) const cache = getCache(); const cacheVehicleMap = new Map(); if (cache) { for (const v of cache.vehicles) { cacheVehicleMap.set(v.plate, Math.max(0, v.dailyKm || 0)); } } const [targetVehicleRows] = await pool.execute( 'SELECT target_id, plate_number FROM tab_mileage_assessment_vehicle WHERE is_deleted = 0' ) as [{ target_id: number; plate_number: string }[], unknown]; const targetIdPlatesMap = new Map(); for (const r of targetVehicleRows) { const list = targetIdPlatesMap.get(r.target_id) || []; list.push(r.plate_number); targetIdPlatesMap.set(r.target_id, list); } const now = new Date(); const result = targets.map((t: any) => { const s = statsMap.get(t.id) || {}; const currentYearTarget = Number(s.current_year_target) || 0; const currentYearCompleted = Number(s.current_year_completed) || 0; const remaining = Math.max(0, currentYearTarget - currentYearCompleted); const yearEnd = s.year_end_date ? new Date(s.year_end_date) : now; const daysLeft = Math.max(1, Math.ceil((yearEnd.getTime() - now.getTime()) / 86400000)); const dailyTarget = remaining / daysLeft; const periods = periodsMap.get(t.id) || []; if (periods.length === 0) { const startDate = t.default_start_date ? new Date(t.default_start_date).toISOString().split('T')[0] : ''; const endDate = t.default_end_date ? new Date(t.default_end_date).toISOString().split('T')[0] : ''; if (startDate || endDate) periods.push(`${startDate} ~ ${endDate}`); } return { id: t.id, targetName: t.target_name, vehicleCount: Number(s.total) || t.vehicle_count, totalMileagePerVehicle: Number(t.total_mileage_per_vehicle), annualMileagePerVehicle: Number(t.annual_mileage_per_vehicle), assessmentYears: t.assessment_years, periods, todayTotal: (targetIdPlatesMap.get(t.id) || []).reduce((sum, plate) => sum + (cacheVehicleMap.get(plate) || 0), 0), cumulativeTotal: Number(s.cumulative_total) || 0, avgCompletion: (Number(s.avg_completion) || 0) * 100, qualifiedCount: Number(s.qualified_count) || 0, yearQualifiedCount: Number(s.year_qualified_count) || 0, halfQualifiedCount: Number(s.half_qualified_count) || 0, currentYearTarget, currentYearCompleted, remaining, daysLeft, dailyTarget: Math.round(dailyTarget * 10) / 10, }; }); return c.json(result); } catch (e: unknown) { console.error('targets error:', e); return c.json([], 500); } }); // GET /target/:id/vehicles — 某项目的车辆明细 app.get('/:id/vehicles', async (c) => { const targetId = c.req.param('id'); const date = c.req.query('date') || ''; try { const [rows] = await pool.execute( `SELECT plate_number, today_mileage, vehicle_total_mileage, completion_rate, is_qualified, current_year_is_qualified, daily_required_mileage FROM tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0 ORDER BY today_mileage DESC`, [targetId] ) as [any[], unknown]; const plates: string[] = rows.map((r: any) => r.plate_number); const infoMap = await fetchVehicleInfoByPlates(plates); // 指定日期时,从里程库查该日里程 const dateMileageMap = new Map(); if (date && plates.length > 0) { const [mileageRows] = await mileagePool.execute( `SELECT plate, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ? AND plate IN (${plates.map(() => '?').join(',')})`, [date, ...plates] ) as [any[], unknown]; for (const m of mileageRows) { const existing = dateMileageMap.get(m.plate); const dailyKm = Number(m.daily_km) || 0; if (!existing || dailyKm > existing.dailyKm) { const source = m.source || 'NONE'; dateMileageMap.set(m.plate, { dailyKm, totalKm: m.total_km !== null ? Number(m.total_km) : null, isOnline: source !== 'NONE' && dailyKm > 0, }); } } } const result = rows.map((r: any) => { const info = infoMap.get(r.plate_number); const dateMileage = date ? dateMileageMap.get(r.plate_number) : null; return { plateNumber: r.plate_number, todayMileage: dateMileage ? dateMileage.dailyKm : (Number(r.today_mileage) || 0), totalMileage: dateMileage?.totalKm ?? (Number(r.vehicle_total_mileage) || 0), completionRate: Number(r.completion_rate) || 0, isQualified: r.is_qualified === 1, currentYearIsQualified: r.current_year_is_qualified === 1, dailyRequiredMileage: Number(r.daily_required_mileage) || 0, rentStatus: info?.rent_status || null, department: info?.department || null, customer: info?.customer || null, isOnline: dateMileage ? dateMileage.isOnline : true, }; }); return c.json(result); } catch (e: unknown) { console.error('target vehicles error:', e); return c.json([], 500); } }); export default app; ``` - [ ] **Step 2: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 3: Commit** ```bash git add src/server/routes/mileage/targets.ts git commit -m "refactor: create targets route handler" ``` --- ### Task 6: Create trend route handler **Files:** - Create: `src/server/routes/mileage/trend.ts` - [ ] **Step 1: Create the trend route** ```typescript // src/server/routes/mileage/trend.ts import { Hono } from 'hono'; import pool from '../../db.js'; import mileagePool from '../../mileage-db.js'; const app = new Hono(); app.get('/', async (c) => { const targetId = c.req.query('targetId'); const days = Number(c.req.query('days')) || 7; try { let plates: string[] = []; if (targetId) { const [vehicleRows] = await pool.execute( 'SELECT plate_number FROM tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0', [targetId] ) as [{ plate_number: string }[], unknown]; plates = vehicleRows.map(r => r.plate_number); if (plates.length === 0) return c.json([]); } let sql = ` SELECT DATE_FORMAT(stat_date, '%m-%d') as date, SUM(daily_km) as mileage FROM v_vehicle_daily_stats WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) AND stat_date < CURDATE() `; const params: (string | number)[] = [days]; if (plates.length > 0) { sql += ` AND plate IN (${plates.map(() => '?').join(',')})`; params.push(...plates); } sql += ' GROUP BY stat_date ORDER BY stat_date'; const [rows] = await mileagePool.execute(sql, params) as [any[], unknown]; return c.json(rows.map((r: any) => ({ date: r.date, mileage: Math.round(Number(r.mileage) || 0), }))); } catch (e: unknown) { console.error('trend error:', e); return c.json([], 500); } }); export default app; ``` - [ ] **Step 2: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 3: Commit** ```bash git add src/server/routes/mileage/trend.ts git commit -m "refactor: create trend route handler" ``` --- ### Task 7: Assemble new index and swap in **Files:** - Create: `src/server/routes/mileage/index.ts` - Delete: `src/server/routes/mileage.ts` - [ ] **Step 1: Create the new index** ```typescript // src/server/routes/mileage/index.ts import { Hono } from 'hono'; import { refreshMonitoringCache } from './cache.js'; import monitoringRouter from './monitoring.js'; import targetsRouter from './targets.js'; import trendRouter from './trend.js'; const app = new Hono(); app.route('/monitoring', monitoringRouter); app.route('/targets', targetsRouter); app.route('/target', targetsRouter); app.route('/trend', trendRouter); // 启动时立即刷新缓存,之后每分钟刷新 refreshMonitoringCache(); setInterval(refreshMonitoringCache, 60 * 1000); export default app; ``` - [ ] **Step 2: Delete the old monolith** ```bash rm src/server/routes/mileage.ts ``` - [ ] **Step 3: Verify TypeScript compiles** Run: `npx tsc --noEmit` Expected: no errors - [ ] **Step 4: Verify the server starts and API works** Run: `npm run dev` and test: - `curl http://localhost:3001/api/mileage/monitoring?limit=2` — should return vehicles - `curl http://localhost:3001/api/mileage/targets` — should return target list - `curl http://localhost:3001/api/mileage/trend?days=7` — should return trend data - [ ] **Step 5: Commit** ```bash git add src/server/routes/mileage/ && git add -u src/server/routes/mileage.ts git commit -m "refactor: replace mileage monolith with modular route files" ``` --- ### Task 8: Fix the stale comment and final cleanup **Files:** - Modify: `src/server/routes/mileage/cache.ts` - [ ] **Step 1: Verify no leftover references to old file** Run: `grep -r "routes/mileage.js" src/` — should only find `src/server/index.ts` which imports `./routes/mileage.js`. Since we moved to `mileage/index.ts`, the import path `./routes/mileage.js` resolves to `./routes/mileage/index.js` automatically. No change needed. - [ ] **Step 2: Verify full build** Run: `npx tsc --noEmit && npm run build` Expected: no errors - [ ] **Step 3: Final commit** ```bash git commit --allow-empty -m "refactor: mileage backend refactor complete — verified build" ```