refactor: modularize application domains
This commit is contained in:
@@ -1,9 +1,19 @@
|
||||
import dotenv from 'dotenv';
|
||||
import {
|
||||
DEFAULT_ONEOS_PROTOCOL_PRIORITY,
|
||||
normalizeOneOsProtocol,
|
||||
type OneOsProtocol,
|
||||
} from './source-policy.js';
|
||||
import {
|
||||
assertMileageDate,
|
||||
groupContiguousMileageDates,
|
||||
normalizeOneOsRangeRows,
|
||||
normalizeOneOsRows,
|
||||
normalizeSelectedPlates,
|
||||
oneOsRangeRequestKey,
|
||||
type OneOsDailyMileage,
|
||||
} from './oneos-model.js';
|
||||
|
||||
export type { OneOsDailyMileage } from './oneos-model.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -15,19 +25,6 @@ const HISTORICAL_TTL_MS = 6 * 60 * 60 * 1000;
|
||||
const MAX_CACHE_DATES = 400;
|
||||
const RANGE_PAGE_SIZE = 5000;
|
||||
|
||||
export interface OneOsDailyMileage {
|
||||
vin: string;
|
||||
plateNumber: string;
|
||||
date: string;
|
||||
dailyMileageKm: number | null;
|
||||
totalMileageKm: number | null;
|
||||
status: 'NORMAL' | 'NO_DATA';
|
||||
dataTime: string | null;
|
||||
calculatedAt: string | null;
|
||||
updatedAt: string | null;
|
||||
sourceProtocol: OneOsProtocol | null;
|
||||
}
|
||||
|
||||
interface OneOsMileageResponse {
|
||||
code?: string;
|
||||
message?: string;
|
||||
@@ -78,80 +75,6 @@ function shanghaiDate(): string {
|
||||
}).format(new Date());
|
||||
}
|
||||
|
||||
function normalizeRows(value: unknown, requestedDate: string): OneOsDailyMileage[] {
|
||||
if (!Array.isArray(value)) throw new Error('OneOS mileage API returned a non-array data field');
|
||||
|
||||
const best = new Map<string, OneOsDailyMileage>();
|
||||
for (const item of value) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const row = item as Record<string, unknown>;
|
||||
const vin = typeof row.vin === 'string' ? row.vin.trim() : '';
|
||||
const plateNumber = typeof row.plateNumber === 'string' ? row.plateNumber.trim() : '';
|
||||
const date = typeof row.date === 'string' ? row.date : requestedDate;
|
||||
const status = row.status === 'NORMAL' ? 'NORMAL' : 'NO_DATA';
|
||||
const rawKm = row.dailyMileageKm;
|
||||
const numericKm = rawKm === null || rawKm === undefined ? null : Number(rawKm);
|
||||
const dailyMileageKm = status === 'NORMAL' && numericKm !== null && Number.isFinite(numericKm)
|
||||
? Math.max(0, numericKm)
|
||||
: null;
|
||||
const rawTotalKm = row.totalMileageKm;
|
||||
const numericTotalKm = rawTotalKm === null || rawTotalKm === undefined ? null : Number(rawTotalKm);
|
||||
const totalMileageKm = status === 'NORMAL' && numericTotalKm !== null && Number.isFinite(numericTotalKm)
|
||||
? Math.max(0, numericTotalKm)
|
||||
: null;
|
||||
const stringField = (...names: string[]): string | null => {
|
||||
for (const name of names) {
|
||||
const field = row[name];
|
||||
if (typeof field === 'string' && field.trim()) return field.trim();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// dataTime is the preferred contract. Aliases keep the BI forward-compatible
|
||||
// while the Open API rolls out the formal field name.
|
||||
const dataTime = stringField('dataTime', 'statisticTime', 'recordTime');
|
||||
const calculatedAt = stringField('calculatedAt', 'calculationTime');
|
||||
const updatedAt = stringField('updatedAt');
|
||||
const sourceProtocol = normalizeOneOsProtocol(
|
||||
stringField('sourceProtocol', 'protocol', 'dataSource'),
|
||||
);
|
||||
if (!plateNumber || date !== requestedDate) continue;
|
||||
|
||||
const normalized: OneOsDailyMileage = {
|
||||
vin,
|
||||
plateNumber,
|
||||
date,
|
||||
dailyMileageKm,
|
||||
totalMileageKm,
|
||||
status,
|
||||
dataTime,
|
||||
calculatedAt,
|
||||
updatedAt,
|
||||
sourceProtocol,
|
||||
};
|
||||
const existing = best.get(plateNumber);
|
||||
if (!existing || (dailyMileageKm ?? -1) > (existing.dailyMileageKm ?? -1)) {
|
||||
best.set(plateNumber, normalized);
|
||||
}
|
||||
}
|
||||
return Array.from(best.values());
|
||||
}
|
||||
|
||||
function normalizeRangeRows(value: unknown, startDate: string, endDate: string): Map<string, OneOsDailyMileage[]> {
|
||||
if (!Array.isArray(value)) throw new Error('OneOS mileage range API returned a non-array data field');
|
||||
const rawByDate = new Map<string, unknown[]>();
|
||||
for (const item of value) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const date = (item as Record<string, unknown>).date;
|
||||
if (typeof date !== 'string' || date < startDate || date > endDate) continue;
|
||||
const rows = rawByDate.get(date) || [];
|
||||
rows.push(item);
|
||||
rawByDate.set(date, rows);
|
||||
}
|
||||
const result = new Map<string, OneOsDailyMileage[]>();
|
||||
for (const [date, rows] of rawByDate) result.set(date, normalizeRows(rows, date));
|
||||
return result;
|
||||
}
|
||||
|
||||
async function requestDate(
|
||||
date: string,
|
||||
protocolPriority: OneOsProtocol[],
|
||||
@@ -193,7 +116,7 @@ async function requestDate(
|
||||
lastError = error;
|
||||
} else {
|
||||
if (includeProtocolPriority) protocolPrioritySupported = true;
|
||||
return normalizeRows(payload.data, date);
|
||||
return normalizeOneOsRows(payload.data, date);
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
@@ -280,7 +203,7 @@ async function requestRange(
|
||||
snapshotId = payload.snapshotId || snapshotId;
|
||||
allRows.push(...payload.data);
|
||||
const nextCursor = payload.nextCursor || null;
|
||||
if (!nextCursor) return normalizeRangeRows(allRows, startDate, endDate);
|
||||
if (!nextCursor) return normalizeOneOsRangeRows(allRows, startDate, endDate);
|
||||
if (seenCursors.has(nextCursor)) throw new Error('OneOS mileage range API returned a repeated cursor');
|
||||
seenCursors.add(nextCursor);
|
||||
cursor = nextCursor;
|
||||
@@ -302,7 +225,7 @@ export async function fetchOneOsDailyMileage(
|
||||
plateNumbers?: string[],
|
||||
protocolPriority: OneOsProtocol[] = DEFAULT_ONEOS_PROTOCOL_PRIORITY,
|
||||
): Promise<OneOsDailyMileage[]> {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new Error(`Invalid mileage date: ${date}`);
|
||||
assertMileageDate(date);
|
||||
|
||||
const policyKey = protocolPriority.join('>');
|
||||
const cacheKey = `${date}:${policyKey}`;
|
||||
@@ -338,38 +261,17 @@ export async function fetchOneOsMileageDates(
|
||||
protocolPriority: OneOsProtocol[] = DEFAULT_ONEOS_PROTOCOL_PRIORITY,
|
||||
): Promise<Map<string, OneOsDailyMileage[]>> {
|
||||
const result = new Map<string, OneOsDailyMileage[]>();
|
||||
const uniqueDates = Array.from(new Set(dates)).sort();
|
||||
const selectedPlates = Array.from(new Set(
|
||||
(plateNumbers || []).map(plate => plate.trim()).filter(Boolean),
|
||||
)).sort();
|
||||
if (uniqueDates.length === 0) return result;
|
||||
for (const date of uniqueDates) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new Error(`Invalid mileage date: ${date}`);
|
||||
result.set(date, []);
|
||||
}
|
||||
|
||||
const groups: string[][] = [];
|
||||
for (const date of uniqueDates) {
|
||||
const current = groups[groups.length - 1];
|
||||
if (!current) {
|
||||
groups.push([date]);
|
||||
continue;
|
||||
}
|
||||
const previous = new Date(`${current[current.length - 1]}T00:00:00Z`);
|
||||
previous.setUTCDate(previous.getUTCDate() + 1);
|
||||
if (previous.toISOString().slice(0, 10) === date && current.length < 366) current.push(date);
|
||||
else groups.push([date]);
|
||||
const selectedPlates = normalizeSelectedPlates(plateNumbers);
|
||||
if (dates.length === 0) return result;
|
||||
const groups = groupContiguousMileageDates(dates);
|
||||
for (const group of groups) {
|
||||
for (const date of group) result.set(date, []);
|
||||
}
|
||||
|
||||
await Promise.all(groups.map(async group => {
|
||||
const startDate = group[0];
|
||||
const endDate = group[group.length - 1];
|
||||
const key = [
|
||||
startDate,
|
||||
endDate,
|
||||
selectedPlates.length > 0 ? selectedPlates.join('\u0000') : '*',
|
||||
protocolPriority.join('>'),
|
||||
].join(':');
|
||||
const key = oneOsRangeRequestKey(startDate, endDate, selectedPlates, protocolPriority);
|
||||
let pending = rangeInflight.get(key);
|
||||
if (!pending) {
|
||||
pending = requestRange(
|
||||
|
||||
Reference in New Issue
Block a user