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));