159 lines
10 KiB
JavaScript
159 lines
10 KiB
JavaScript
import fs from "node:fs/promises";
|
|
|
|
const baseUrl = "http://115.29.187.205:20200";
|
|
const outputPath = "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/tmp/mileage-appeal-20260810/evidence.json";
|
|
const vehicles = [
|
|
{ seq: 1, model: "18T", plate: "粤A05995F", vin: "LNXNEGRR7SR321370", sourceTboxKm: 26180.6, platformKm: 4208.9, lossKm: 21971.7, lossRatePct: 83.92, anomaly: "里程损失,tbox异常", appealKm: 41497.1, note: "区间内仪表盘开始里程是576.6,结束里程是41497.1,差值40920.5,小于申诉里程;2026-05-11到2026-07-28间的数据没上传,仪表盘里程差了36306.6,2025-12-25到2026-01-01间还存在里程跳变导致里程缺失" },
|
|
{ seq: 2, model: "18T", plate: "粤A05839F", vin: "LNXNEGRR8SR321376", sourceTboxKm: 41030.3, platformKm: 19763.6, lossKm: 21266.7, lossRatePct: 51.83, anomaly: "里程损失", appealKm: 41030.3, note: "存在很多数据缺失导致的里程跳变,统计里程缺失,特别是7月份仪表里程从7月1号的16341.2到7月31号的41848.9,相差25507.7,计算的总里程只有8470" },
|
|
{ seq: 3, model: "18T", plate: "粤A09369F", vin: "LNXNEGRR6SR321473", sourceTboxKm: 40381.5, platformKm: 24994.0, lossKm: 15387.5, lossRatePct: 38.11, anomaly: "里程损失", appealKm: 40381.5, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
|
{ seq: 4, model: "18T", plate: "粤A06658F", vin: "LNXNEGRR5SR321397", sourceTboxKm: 52386.5, platformKm: 33064.5, lossKm: 19322.0, lossRatePct: 36.88, anomaly: "里程损失", appealKm: 52386.5, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
|
{ seq: 5, model: "18T", plate: "粤A05700F", vin: "LNXNEGRR8SR321393", sourceTboxKm: 60054.3, platformKm: 41352.9, lossKm: 18701.4, lossRatePct: 31.14, anomaly: "里程损失", appealKm: 60054.3, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
|
{ seq: 6, model: "18T", plate: "粤A05906F", vin: "LNXNEGRR2SR319462", sourceTboxKm: 37216.8, platformKm: 26711.4, lossKm: 10505.4, lossRatePct: 28.23, anomaly: "里程损失", appealKm: 37216.8, note: "最新数据只到2026-06-30,仪表盘最后里程为28638.5,没有7月份的数据;也存在很多数据缺失导致的里程缺失,例如2026-03-26到2026-04-01,2026-04-21到2026-05-01间没上传数据" },
|
|
{ seq: 7, model: "4.5T", plate: "粤AGP9346", vin: "LB9A32A28R0LS1722", sourceTboxKm: 32337.2, platformKm: 28685.4, lossKm: 3651.8, lossRatePct: 11.29, anomaly: "里程损失", appealKm: 32337.2, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
|
{ seq: 8, model: "4.5T", plate: "粤AGP2017", vin: "LB9A32A28R0LS1445", sourceTboxKm: 48164.9, platformKm: 44357.1, lossKm: 3807.8, lossRatePct: 7.91, anomaly: "里程损失", appealKm: 48164.9, note: "存在很多数据缺失导致的里程跳变,统计里程缺失,而且区间内最后的仪表里程是2026-07-31的47337.5,小于申诉里程" },
|
|
{ seq: 9, model: "4.5T", plate: "粤AGP5165", vin: "LB9A32A25R0LS1385", sourceTboxKm: 32906.4, platformKm: 30302.3, lossKm: 2604.1, lossRatePct: 7.91, anomaly: "里程损失", appealKm: 32906.4, note: "存在很多数据缺失导致的里程跳变,统计里程缺失,而且区间内最后的仪表里程是2026-07-30的31994.6,2026-07-31没有数据上传" },
|
|
{ seq: 10, model: "4.5T", plate: "粤AGP4355", vin: "LB9A32A26R0LS1606", sourceTboxKm: 55151.0, platformKm: 51525.8, lossKm: 3625.2, lossRatePct: 6.57, anomaly: "里程损失", appealKm: 55151.0, note: "区间内仪表盘开始里程是1898.6,结束里程是55229.6,差值53331,小于申诉里程,也存在很多数据缺失导致的里程缺失" },
|
|
{ seq: 11, model: "4.5T", plate: "粤AGP5646", vin: "LB9A32A22R0LS1702", sourceTboxKm: 39935.0, platformKm: 37319.8, lossKm: 2615.2, lossRatePct: 6.55, anomaly: "里程损失", appealKm: 39935.0, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
|
{ seq: 12, model: "4.5T", plate: "粤AGP3692", vin: "LB9A32A27R0LS1727", sourceTboxKm: 64195.0, platformKm: 60242.6, lossKm: 3952.4, lossRatePct: 6.16, anomaly: "里程损失", appealKm: 64195.0, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
|
{ seq: 13, model: "4.5T", plate: "粤AGP9782", vin: "LB9A32A28R0LS1705", sourceTboxKm: 50216.4, platformKm: 47123.8, lossKm: 3092.6, lossRatePct: 6.16, anomaly: "里程损失", appealKm: 50216.4, note: "存在很多数据缺失导致的里程跳变,统计里程缺失,如:2026-05-21数据缺失导致里程少了1300多" },
|
|
{ seq: 14, model: "4.5T", plate: "粤AGP3027", vin: "LB9A32A22R0LS1599", sourceTboxKm: 31180.9, platformKm: 29260.1, lossKm: 1920.8, lossRatePct: 6.16, anomaly: "里程损失", appealKm: 31180.9, note: "区间内仪表盘开始里程是933.5,结束里程是31245.3,差值30311.8,小于申诉里程,也存在一些数据缺失导致的里程跳变,统计里程缺失" },
|
|
{ seq: 15, model: "4.5T", plate: "粤AGP9751", vin: "LB9A32A28R0LS1574", sourceTboxKm: 17944.6, platformKm: 16861.0, lossKm: 1083.6, lossRatePct: 6.04, anomaly: "里程损失", appealKm: 17944.6, note: "区间内仪表盘开始里程是205.2,结束里程是17796.8,差值17591.6,小于申诉里程,也存在一些数据缺失导致的里程缺失" },
|
|
];
|
|
|
|
function dateString(date) {
|
|
return date.toISOString().slice(0, 10);
|
|
}
|
|
|
|
function addDays(day, count) {
|
|
const date = new Date(`${day}T00:00:00Z`);
|
|
date.setUTCDate(date.getUTCDate() + count);
|
|
return dateString(date);
|
|
}
|
|
|
|
function dateRange(start, end) {
|
|
const out = [];
|
|
for (let day = start; day <= end; day = addDays(day, 1)) out.push(day);
|
|
return out;
|
|
}
|
|
|
|
async function fetchJson(url, attempts = 4) {
|
|
let lastError;
|
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
try {
|
|
const response = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
return await response.json();
|
|
} catch (error) {
|
|
lastError = error;
|
|
if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
|
|
}
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
async function mapConcurrent(items, concurrency, worker) {
|
|
const results = new Array(items.length);
|
|
let cursor = 0;
|
|
async function run() {
|
|
while (true) {
|
|
const index = cursor;
|
|
cursor += 1;
|
|
if (index >= items.length) return;
|
|
results[index] = await worker(items[index], index);
|
|
}
|
|
}
|
|
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, run));
|
|
return results;
|
|
}
|
|
|
|
async function fetchDaily(vehicle) {
|
|
const params = new URLSearchParams({
|
|
vin: vehicle.vin,
|
|
protocol: "GB32960",
|
|
dateFrom: "2025-11-28",
|
|
dateTo: "2026-07-31",
|
|
limit: "500",
|
|
includeTotal: "true",
|
|
});
|
|
const data = await fetchJson(`${baseUrl}/api/stats/daily-metrics?${params}`);
|
|
return data.items ?? [];
|
|
}
|
|
|
|
async function fetchRawEvidence({ vehicle, day }) {
|
|
const params = new URLSearchParams({
|
|
protocol: "GB32960",
|
|
vin: vehicle.vin,
|
|
dateFrom: `${day} 00:00:00`,
|
|
dateTo: `${addDays(day, 1)} 00:00:00`,
|
|
orderBy: "eventTime",
|
|
limit: "20",
|
|
includeFields: "true",
|
|
includePayload: "true",
|
|
fields: "gb32960.vehicle.total_mileage_km",
|
|
});
|
|
let data = await fetchJson(`${baseUrl}/api/history/raw-frames?${params}`);
|
|
let evidence = (data.items ?? []).find((item) => item?.parsed_fields?.["gb32960.vehicle.total_mileage_km"] != null && String(item.event_time ?? "").startsWith(day));
|
|
if (!evidence && (data.items ?? []).length === 20) {
|
|
params.set("limit", "100");
|
|
data = await fetchJson(`${baseUrl}/api/history/raw-frames?${params}`);
|
|
evidence = (data.items ?? []).find((item) => item?.parsed_fields?.["gb32960.vehicle.total_mileage_km"] != null && String(item.event_time ?? "").startsWith(day));
|
|
}
|
|
if (!evidence) return { day, status: "NO_RAW_ODOMETER_FRAME" };
|
|
return {
|
|
day,
|
|
status: "OK",
|
|
ts: evidence.ts ?? "",
|
|
eventTime: evidence.event_time ?? "",
|
|
receivedAt: evidence.received_at ?? "",
|
|
frameId: evidence.frame_id ?? "",
|
|
eventId: evidence.event_id ?? "",
|
|
messageIdHex: evidence.message_id_hex ?? "",
|
|
rawSizeBytes: evidence.raw_size_bytes ?? null,
|
|
rawHex: evidence.raw_hex ?? "",
|
|
parseStatus: evidence.parse_status ?? "",
|
|
parseError: evidence.parse_error ?? "",
|
|
sourceEndpoint: evidence.source_endpoint ?? "",
|
|
parsedTotalMileageKm: Number(evidence.parsed_fields["gb32960.vehicle.total_mileage_km"]),
|
|
};
|
|
}
|
|
|
|
const dailyLists = await mapConcurrent(vehicles, 6, fetchDaily);
|
|
const rawDays = dateRange("2026-07-01", "2026-07-31");
|
|
const rawTasks = vehicles.flatMap((vehicle) => rawDays.map((day) => ({ vehicle, day })));
|
|
let completed = 0;
|
|
const rawResults = await mapConcurrent(rawTasks, 10, async (task) => {
|
|
const result = await fetchRawEvidence(task);
|
|
completed += 1;
|
|
if (completed % 50 === 0 || completed === rawTasks.length) console.log(`raw evidence ${completed}/${rawTasks.length}`);
|
|
return { vin: task.vehicle.vin, ...result };
|
|
});
|
|
|
|
const byVinRaw = new Map();
|
|
for (const item of rawResults) {
|
|
if (!byVinRaw.has(item.vin)) byVinRaw.set(item.vin, []);
|
|
byVinRaw.get(item.vin).push(item);
|
|
}
|
|
|
|
const result = {
|
|
generatedAt: new Date().toISOString(),
|
|
baseUrl,
|
|
period: { start: "2025-11-28", end: "2026-07-31" },
|
|
protocol: "GB32960",
|
|
parsedMileageField: "gb32960.vehicle.total_mileage_km",
|
|
rawEvidenceAvailability: { start: "2026-07-01", end: "2026-07-31", note: "现行TDengine RAW历史自2026-07-01起;此前中台仅保留历史生产库日统计迁移记录,不能伪称原始帧。" },
|
|
vehicles: vehicles.map((vehicle, index) => ({
|
|
...vehicle,
|
|
dailyMetrics: dailyLists[index],
|
|
rawEvidence: (byVinRaw.get(vehicle.vin) ?? []).sort((a, b) => a.day.localeCompare(b.day)),
|
|
})),
|
|
};
|
|
|
|
await fs.writeFile(outputPath, JSON.stringify(result, null, 2), "utf8");
|
|
console.log(JSON.stringify({
|
|
outputPath,
|
|
vehicles: result.vehicles.length,
|
|
dailyRows: result.vehicles.reduce((sum, item) => sum + item.dailyMetrics.length, 0),
|
|
rawEvidenceRows: result.vehicles.reduce((sum, item) => sum + item.rawEvidence.filter((row) => row.status === "OK").length, 0),
|
|
rawMissingDays: result.vehicles.reduce((sum, item) => sum + item.rawEvidence.filter((row) => row.status !== "OK").length, 0),
|
|
}, null, 2));
|