refactor: modularize application domains

This commit is contained in:
kkfluous
2026-08-13 12:03:34 +08:00
parent d610e4b841
commit 06fe75b4c7
142 changed files with 14645 additions and 8885 deletions
+33 -212
View File
@@ -3,111 +3,37 @@ import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import pool from '../../db.js';
import { fetchVehicleInfoMap } from './vehicle-info.js';
import { fetchOneOsDailyMileage, fetchOneOsMileageDates, type OneOsDailyMileage } from './oneos-api.js';
import { fetchOneOsDailyMileage, fetchOneOsMileageDates } from './oneos-api.js';
import {
sourceCategoryFromProtocol,
type MileageSourceCategory,
type OneOsProtocol,
} from './source-policy.js';
import type { CachedVehicle, MonitoringCache, MonitoringFilters, PlatePrefix, VehicleInfoRow } from './types.js';
import {
buildMonitoringFilters,
buildPlateTargetNamesMap,
buildTargetPlatesMap,
dailyMileageMap,
mergeMonitoringVehicles,
mileageDatesBetween,
previousMileageDate,
toMileageRows,
type DailyMileageRow,
type MileageRow,
type TargetRow,
} from './cache-model.js';
import type { CachedVehicle, MonitoringCache, MonitoringFilters } from './types.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const regionMap: Record<string, string> = JSON.parse(
readFileSync(join(__dirname, 'region-map.json'), 'utf8')
);
const REGION_ORDER = ['华东区域', '华南区域', '西南区域', '西北区域', '华北区域', '华中区域', '东北区域'];
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 brands = Array.from(new Set(vehicles.map(v => v.brand).filter((b): b is string => b !== null))).sort();
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);
const regionSet = new Set(vehicles.map(v => v.region).filter((r): r is string => r !== null));
const regions = Array.from(regionSet).sort((a, b) => {
const ai = REGION_ORDER.indexOf(a);
const bi = REGION_ORDER.indexOf(b);
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
});
return { departments, customers, plates, projects, entities, rentStatuses, platePrefixes, targetNames, regions, brands };
}
interface MileageRow {
plate: string;
vin: string;
daily_km: string;
total_km: string | null;
source: string;
source_protocol: OneOsProtocol | null;
data_time: string | null;
calculated_at: string | null;
updated_at: string | null;
}
interface DailyMileageRow {
plate: string;
vin: string | null;
date: string;
daily_km: string | number | null;
source: string | null;
source_protocol: OneOsProtocol | null;
data_time: string | null;
calculated_at: string | null;
updated_at: string | null;
}
function toMileageRows(rows: OneOsDailyMileage[]): MileageRow[] {
return rows.map(row => ({
plate: row.plateNumber,
vin: row.vin,
daily_km: String(row.dailyMileageKm ?? 0),
total_km: row.totalMileageKm == null ? null : String(row.totalMileageKm),
source: row.status === 'NORMAL' ? 'ONEOS_API' : 'NONE',
source_protocol: row.sourceProtocol,
data_time: row.dataTime,
calculated_at: row.calculatedAt,
updated_at: row.updatedAt,
}));
}
function previousDate(date: string): string {
const [year, month, day] = date.split('-').map(Number);
const value = new Date(Date.UTC(year, month - 1, day));
value.setUTCDate(value.getUTCDate() - 1);
return value.toISOString().slice(0, 10);
}
function shanghaiDate(): string {
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
@@ -117,17 +43,6 @@ function shanghaiDate(): string {
}).format(new Date());
}
function dailyMileageMap(rows: OneOsDailyMileage[]): Map<string, number> {
const map = new Map<string, number>();
for (const row of rows) {
if (row.status !== 'NORMAL') continue;
const km = Math.max(0, Number(row.dailyMileageKm) || 0);
const existing = map.get(row.plateNumber) || 0;
if (km > existing) map.set(row.plateNumber, km);
}
return map;
}
export interface RangeMileageResult {
vehicles: CachedVehicle[];
dailyTotals: { date: string; totalKm: number }[];
@@ -135,12 +50,6 @@ export interface RangeMileageResult {
end: string;
}
interface TargetRow {
id: number;
target_name: string;
plate_number: string;
}
async function fetchTargetRows(): Promise<TargetRow[]> {
return pool.execute(
`SELECT t.id, t.target_name, v.plate_number
@@ -150,77 +59,6 @@ async function fetchTargetRows(): Promise<TargetRow[]> {
).then(([rows]) => rows as TargetRow[]);
}
function buildTargetPlatesMap(targetRows: TargetRow[]): Map<string, Set<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);
}
return targetPlatesMap;
}
function buildPlateTargetNamesMap(targetRows: TargetRow[]): Map<string, string[]> {
const map = new Map<string, string[]>();
for (const r of targetRows) {
const list = map.get(r.plate_number) || [];
list.push(r.target_name);
map.set(r.plate_number, list);
}
return map;
}
function mergeVehicles(
mileageRows: MileageRow[],
infoMap: Map<string, VehicleInfoRow>,
yesterdayMap: Map<string, number>,
targetNamesByPlate: Map<string, string[]>,
): 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(infoMap.values()).map(info => {
const m = mileageMap.get(info.plate);
const plate = info.plate;
const dailyKm = Number(m?.daily_km) || 0;
const source = m?.source || 'NONE';
const gpsTotal = m?.total_km != null ? Number(m.total_km) : null;
return {
plate,
vin: m?.vin || info.vin || '',
dailyKm,
// The OneOS daily mileage API does not currently return cumulative mileage.
// Never backfill it from another mileage source outside the assessment page.
totalKm: gpsTotal,
source,
sourceProtocol: m?.source_protocol || null,
sourceCategory: sourceCategoryFromProtocol(m?.source_protocol || null),
dataTime: m?.data_time || null,
calculatedAt: m?.calculated_at || null,
updatedAt: m?.updated_at || null,
isOnline: source !== 'NONE' && dailyKm > 0,
isDataSynced: source !== 'NONE',
customer: info.customer || null,
department: info.department || null,
manager: info.manager || null,
managerId: info.manager_id || null,
rentStatus: info.rent_status || null,
entity: info.entity || null,
project: info.project || null,
region: regionMap[plate] || null,
inventoryLocation: info.inventory_location || null,
brand: info.brand_label || null,
targetNames: targetNamesByPlate.get(plate) || [],
yesterdayKm: yesterdayMap.get(plate) || 0,
};
});
}
export async function refreshMonitoringCache(): Promise<void> {
try {
console.log('[mileage] refreshing monitoring cache...');
@@ -229,7 +67,7 @@ export async function refreshMonitoringCache(): Promise<void> {
const date = shanghaiDate();
const [apiRows, yesterdayRows, infoMap, targetRows] = await Promise.all([
fetchOneOsDailyMileage(date),
fetchOneOsDailyMileage(previousDate(date)),
fetchOneOsDailyMileage(previousMileageDate(date)),
fetchVehicleInfoMap(),
fetchTargetRows(),
]);
@@ -241,14 +79,20 @@ export async function refreshMonitoringCache(): Promise<void> {
const targetNamesByPlate = buildPlateTargetNamesMap(targetRows);
const targetNames = Array.from(targetPlatesMap.keys());
const vehicles = mergeVehicles(mileageRows, infoMap, yesterdayMap, targetNamesByPlate);
const vehicles = mergeMonitoringVehicles(
mileageRows,
infoMap,
yesterdayMap,
targetNamesByPlate,
regionMap,
);
const totalToday = Math.round(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),
filters: buildMonitoringFilters(vehicles, targetNames),
targetPlatesMap,
updatedAt: new Date().toISOString(),
};
@@ -265,7 +109,7 @@ export async function queryDateMileage(
): Promise<CachedVehicle[]> {
const [apiRows, yesterdayRows, infoMap, targetRows] = await Promise.all([
fetchOneOsDailyMileage(dateStr, undefined, protocolPriority),
fetchOneOsDailyMileage(previousDate(dateStr), undefined, protocolPriority),
fetchOneOsDailyMileage(previousMileageDate(dateStr), undefined, protocolPriority),
fetchVehicleInfoMap(),
fetchTargetRows(),
]);
@@ -273,29 +117,15 @@ export async function queryDateMileage(
const mileageRows = toMileageRows(apiRows);
const yesterdayMap = dailyMileageMap(yesterdayRows);
return mergeVehicles(
return mergeMonitoringVehicles(
mileageRows,
infoMap,
yesterdayMap,
buildPlateTargetNamesMap(targetRows),
regionMap,
);
}
function datesBetween(start: string, end: string): string[] {
const result: string[] = [];
const [sy, sm, sd] = start.split('-').map(Number);
const [ey, em, ed] = end.split('-').map(Number);
const cursor = new Date(sy, sm - 1, sd);
const last = new Date(ey, em - 1, ed);
cursor.setHours(0, 0, 0, 0);
last.setHours(0, 0, 0, 0);
while (cursor <= last) {
result.push(`${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, '0')}-${String(cursor.getDate()).padStart(2, '0')}`);
cursor.setDate(cursor.getDate() + 1);
}
return result;
}
export async function queryRangeMileage(
startDate: string,
endDate: string,
@@ -318,11 +148,11 @@ export async function queryRangeMileage(
};
}
const days = datesBetween(startDate, endDate);
const days = mileageDatesBetween(startDate, endDate);
const [apiRowsByDate, endDateRows, yesterdayRows, infoMap, targetRows] = await Promise.all([
fetchOneOsMileageDates(days, undefined, protocolPriority),
fetchOneOsDailyMileage(endDate, undefined, protocolPriority),
fetchOneOsDailyMileage(previousDate(startDate), undefined, protocolPriority),
fetchOneOsDailyMileage(previousMileageDate(startDate), undefined, protocolPriority),
fetchVehicleInfoMap(),
fetchTargetRows(),
]);
@@ -346,17 +176,7 @@ export async function queryRangeMileage(
const perVehicleDaily = new Map<string, Record<string, number>>();
const perVehicleDailySources = new Map<string, Record<string, OneOsProtocol | null>>();
const perVehicleSourceCategories = new Map<string, Set<MileageSourceCategory>>();
const perVehicleSum = new Map<string, {
plate: string;
vin: string;
daily_km: string;
total_km: string | null;
source: string;
source_protocol: OneOsProtocol | null;
data_time: string | null;
calculated_at: string | null;
updated_at: string | null;
}>();
const perVehicleSum = new Map<string, MileageRow>();
const dailyTotals = new Map<string, number>();
const bestDailyRows = new Map<string, DailyMileageRow>();
@@ -420,11 +240,12 @@ export async function queryRangeMileage(
const yesterdayMap = dailyMileageMap(yesterdayRows);
const vehicles = mergeVehicles(
const vehicles = mergeMonitoringVehicles(
Array.from(perVehicleSum.values()),
infoMap,
yesterdayMap,
buildPlateTargetNamesMap(targetRows),
regionMap,
).map(vehicle => {
const dailyMileage = perVehicleDaily.get(vehicle.plate) || {};
const dailySources = perVehicleDailySources.get(vehicle.plate) || {};
@@ -452,5 +273,5 @@ export async function queryRangeMileage(
}
export function buildDateFilters(vehicles: CachedVehicle[]): MonitoringFilters {
return buildFilters(vehicles, monitoringCache?.filters.targetNames || []);
return buildMonitoringFilters(vehicles, monitoringCache?.filters.targetNames || []);
}