fix(mileage): totalKm 当日为空时回填该车 ln_vehicle_day_total_pg 最近一条
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

视图 v_vehicle_daily_stats.total_km 仅当日有 TBOX 记录才有值,
否则原样为 NULL。新增 fetchLatestPgTotalMileageMap 在应用层按车牌
取最近一条非空 total_mileage(queryDateMileage 限定 dates <= 查询日,
保证历史日不取未来值),插入 gpsTotal → latestPgTotal → bizTotal 的
回退链,让 totalKm 显示连续。

MySQL 5.7 无窗口函数,用 INNER JOIN + GROUP BY MAX(dates) 取每车最近一条;
本地 dev 实测 1004 辆车 cache 刷新 ~16s,不再有 SQL 解析错。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-05-07 15:17:16 +08:00
parent 05c99fc57a
commit 331ad1a1da

View File

@@ -80,11 +80,41 @@ async function fetchBizTotalMileageMap(): Promise<Map<string, number>> {
return map; return map;
} }
async function fetchLatestPgTotalMileageMap(asOf?: string): Promise<Map<string, number>> {
// 当日 ln_vehicle_day_total_pg 无记录或 total_mileage 为 NULL 时,
// 回填该车 dates <= asOf 的最近一条非空 total_mileage÷1000 转 km
// 让视图 total_km 为 NULL 的车也能显示历史累计。
// MySQL 5.7 无窗口函数,用 GROUP BY MAX(dates) + JOIN 取每车最近一条。
const sql = `
SELECT t.plate_number, t.total_mileage
FROM ln_vehicle_day_total_pg t
INNER JOIN (
SELECT plate_number, MAX(dates) AS max_dates
FROM ln_vehicle_day_total_pg
WHERE total_mileage IS NOT NULL
${asOf ? 'AND dates <= ?' : ''}
GROUP BY plate_number
) m ON m.plate_number = t.plate_number AND m.max_dates = t.dates
WHERE t.total_mileage IS NOT NULL`;
const params = asOf ? [asOf] : [];
const [rows] = await mileagePool.execute(sql, params) as [
{ plate_number: string; total_mileage: string | number | null }[],
unknown,
];
const map = new Map<string, number>();
for (const r of rows) {
const km = Number(r.total_mileage) / 1000;
if (Number.isFinite(km) && km > 0) map.set(r.plate_number, km);
}
return map;
}
function mergeVehicles( function mergeVehicles(
mileageRows: MileageRow[], mileageRows: MileageRow[],
infoMap: Map<string, VehicleInfoRow>, infoMap: Map<string, VehicleInfoRow>,
yesterdayMap: Map<string, number>, yesterdayMap: Map<string, number>,
bizTotalMap: Map<string, number>, bizTotalMap: Map<string, number>,
latestPgTotalMap: Map<string, number>,
): CachedVehicle[] { ): CachedVehicle[] {
const mileageMap = new Map<string, MileageRow>(); const mileageMap = new Map<string, MileageRow>();
for (const row of mileageRows) { for (const row of mileageRows) {
@@ -99,12 +129,13 @@ function mergeVehicles(
const dailyKm = Number(m.daily_km) || 0; const dailyKm = Number(m.daily_km) || 0;
const source = m.source || 'NONE'; const source = m.source || 'NONE';
const gpsTotal = m.total_km !== null ? Number(m.total_km) : null; const gpsTotal = m.total_km !== null ? Number(m.total_km) : null;
const latestPgTotal = latestPgTotalMap.get(m.plate);
const bizTotal = bizTotalMap.get(m.plate); const bizTotal = bizTotalMap.get(m.plate);
return { return {
plate: m.plate, plate: m.plate,
vin: m.vin, vin: m.vin,
dailyKm, dailyKm,
totalKm: gpsTotal !== null ? gpsTotal : (bizTotal ?? null), totalKm: gpsTotal !== null ? gpsTotal : (latestPgTotal ?? bizTotal ?? null),
source, source,
isOnline: source !== 'NONE' && dailyKm > 0, isOnline: source !== 'NONE' && dailyKm > 0,
isDataSynced: source !== 'NONE', isDataSynced: source !== 'NONE',
@@ -126,7 +157,7 @@ export async function refreshMonitoringCache(): Promise<void> {
console.log('[mileage] refreshing monitoring cache...'); console.log('[mileage] refreshing monitoring cache...');
const start = Date.now(); const start = Date.now();
const [mileageRows, yesterdayMap, infoMap, targetRows, bizTotalMap] = await Promise.all([ const [mileageRows, yesterdayMap, infoMap, targetRows, bizTotalMap, latestPgTotalMap] = await Promise.all([
(async () => { (async () => {
const [dateRows] = await mileagePool.execute( const [dateRows] = await mileagePool.execute(
'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats' 'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats'
@@ -160,6 +191,7 @@ export async function refreshMonitoringCache(): Promise<void> {
WHERE t.is_deleted = 0` WHERE t.is_deleted = 0`
).then(([rows]) => rows as { id: number; target_name: string; plate_number: string }[]), ).then(([rows]) => rows as { id: number; target_name: string; plate_number: string }[]),
fetchBizTotalMileageMap(), fetchBizTotalMileageMap(),
fetchLatestPgTotalMileageMap(),
]); ]);
const targetPlatesMap = new Map<string, Set<string>>(); const targetPlatesMap = new Map<string, Set<string>>();
@@ -170,7 +202,7 @@ export async function refreshMonitoringCache(): Promise<void> {
} }
const targetNames = Array.from(targetPlatesMap.keys()); const targetNames = Array.from(targetPlatesMap.keys());
const vehicles = mergeVehicles(mileageRows, infoMap, yesterdayMap, bizTotalMap); const vehicles = mergeVehicles(mileageRows, infoMap, yesterdayMap, bizTotalMap, latestPgTotalMap);
const totalToday = vehicles.reduce((sum, v) => sum + v.dailyKm, 0); const totalToday = vehicles.reduce((sum, v) => sum + v.dailyKm, 0);
const totalAll = vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0); const totalAll = vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0);
@@ -189,7 +221,7 @@ export async function refreshMonitoringCache(): Promise<void> {
} }
export async function queryDateMileage(dateStr: string): Promise<CachedVehicle[]> { export async function queryDateMileage(dateStr: string): Promise<CachedVehicle[]> {
const [mileageRows, yesterdayRows, infoMap, bizTotalMap] = await Promise.all([ const [mileageRows, yesterdayRows, infoMap, bizTotalMap, latestPgTotalMap] = await Promise.all([
mileagePool.execute( mileagePool.execute(
'SELECT plate, vin, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ?', 'SELECT plate, vin, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ?',
[dateStr] [dateStr]
@@ -200,6 +232,7 @@ export async function queryDateMileage(dateStr: string): Promise<CachedVehicle[]
).then(([r]) => r as { plate: string; daily_km: string }[]), ).then(([r]) => r as { plate: string; daily_km: string }[]),
fetchVehicleInfoMap(), fetchVehicleInfoMap(),
fetchBizTotalMileageMap(), fetchBizTotalMileageMap(),
fetchLatestPgTotalMileageMap(dateStr),
]); ]);
const yesterdayMap = new Map<string, number>(); const yesterdayMap = new Map<string, number>();
@@ -209,7 +242,7 @@ export async function queryDateMileage(dateStr: string): Promise<CachedVehicle[]
if (km > existing) yesterdayMap.set(r.plate, km); if (km > existing) yesterdayMap.set(r.plate, km);
} }
return mergeVehicles(mileageRows, infoMap, yesterdayMap, bizTotalMap); return mergeVehicles(mileageRows, infoMap, yesterdayMap, bizTotalMap, latestPgTotalMap);
} }
export function buildDateFilters(vehicles: CachedVehicle[]): MonitoringFilters { export function buildDateFilters(vehicles: CachedVehicle[]): MonitoringFilters {