diff --git a/src/server/routes/mileage/cache.ts b/src/server/routes/mileage/cache.ts new file mode 100644 index 0000000..e22ec20 --- /dev/null +++ b/src/server/routes/mileage/cache.ts @@ -0,0 +1,179 @@ +import pool from '../../db.js'; +import mileagePool from '../../mileage-db.js'; +import { fetchVehicleInfoMap } from './vehicle-info.js'; +import type { CachedVehicle, MonitoringCache, MonitoringFilters, PlatePrefix, VehicleInfoRow } 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 }; +} + +interface MileageRow { + plate: string; + vin: string; + daily_km: string; + total_km: string | null; + source: string; +} + +function mergeVehicles( + mileageRows: MileageRow[], + infoMap: Map, + yesterdayMap: Map, +): CachedVehicle[] { + 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 [MileageRow[], 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 [{ plate: string; daily_km: string }[], 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 MileageRow[]), + mileagePool.execute( + 'SELECT plate, daily_km FROM v_vehicle_daily_stats WHERE stat_date = DATE_SUB(?, INTERVAL 1 DAY)', + [dateStr] + ).then(([r]) => r as { plate: string; daily_km: string }[]), + 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 || []); +}