refactor: extract monitoring cache module

This commit is contained in:
kkfluous
2026-04-02 13:31:07 +08:00
parent 1a169feaa6
commit 459b0400b4

View File

@@ -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<string, number>();
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<string, VehicleInfoRow>,
yesterdayMap: Map<string, number>,
): CachedVehicle[] {
const mileageMap = new Map<string, MileageRow>();
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<void> {
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<string, number>();
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<string, Set<string>>();
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<CachedVehicle[]> {
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<string, number>();
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 || []);
}