Files
lingniu-vehicle-ingest/tmp/mileage-appeal-20260810/build_appeal.mjs

447 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from "node:fs/promises";
import path from "node:path";
import { SpreadsheetFile, Workbook } from "@oai/artifact-tool";
const evidencePath = "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/tmp/mileage-appeal-20260810/evidence.json";
const outputDir = "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/outputs/019fe9b9-eb60-7d22-8c0a-c70851a48083";
const outputPath = path.join(outputDir, "15辆车里程核验申诉材料_GB32960日里程及原始报文.xlsx");
const previewDir = path.join(outputDir, "preview");
const data = JSON.parse(await fs.readFile(evidencePath, "utf8"));
const workbook = Workbook.create();
const summary = workbook.worksheets.add("申诉总览");
const notes = workbook.worksheets.add("口径与证据说明");
const vehicleSheets = new Map();
for (const vehicle of data.vehicles) {
const sheetName = `${String(vehicle.seq).padStart(2, "0")}-${vehicle.plate}`;
vehicleSheets.set(vehicle.vin, { sheetName, sheet: workbook.worksheets.add(sheetName) });
}
const palette = {
navy: "#1F4E78",
blue: "#D9EAF7",
blue2: "#EAF3F8",
yellow: "#FFF2CC",
amber: "#FCE4D6",
mileageHeader: "#C65911",
mileageLight: "#FFF2CC",
mileageStrong: "#FFD966",
green: "#E2F0D9",
gray: "#E7E6E6",
dark: "#1F2937",
muted: "#5B6573",
border: "#B7C9D6",
white: "#FFFFFF",
};
const thinBorders = { preset: "all", style: "thin", color: palette.border };
const titleFormat = {
fill: palette.navy,
font: { bold: true, color: palette.white, fontSize: 16, typeface: "Microsoft YaHei" },
horizontalAlignment: "left",
verticalAlignment: "center",
};
const sectionFormat = {
fill: palette.blue,
font: { bold: true, color: palette.dark, fontSize: 11, typeface: "Microsoft YaHei" },
horizontalAlignment: "left",
verticalAlignment: "center",
borders: thinBorders,
};
const headerFormat = {
fill: palette.navy,
font: { bold: true, color: palette.white, fontSize: 9, typeface: "Microsoft YaHei" },
horizontalAlignment: "center",
verticalAlignment: "center",
wrapText: true,
borders: thinBorders,
};
const labelFormat = {
fill: palette.blue2,
font: { bold: true, color: palette.dark, fontSize: 9, typeface: "Microsoft YaHei" },
horizontalAlignment: "left",
verticalAlignment: "center",
wrapText: true,
borders: thinBorders,
};
const valueFormat = {
font: { color: palette.dark, fontSize: 9, typeface: "Microsoft YaHei" },
verticalAlignment: "center",
wrapText: true,
borders: thinBorders,
};
function asDate(day) {
return new Date(`${day}T00:00:00Z`);
}
function addDays(day, count) {
const date = asDate(day);
date.setUTCDate(date.getUTCDate() + count);
return date.toISOString().slice(0, 10);
}
function calendarDays(start, end) {
const days = [];
for (let day = start; day <= end; day = addDays(day, 1)) days.push(day);
return days;
}
function absDelta(a, b) {
if (a == null || b == null || Number.isNaN(Number(a)) || Number.isNaN(Number(b))) return null;
return Math.abs(Number(a) - Number(b));
}
function rawRange(vehicle) {
const rows = vehicle.rawEvidence.filter((row) => row.status === "OK");
if (rows.length === 0) return "无";
return `${rows[0].day}${rows.at(-1).day}`;
}
function evidenceLevel(metric, raw) {
if (raw?.status === "OK") return "A-原始32960报文";
if (metric?.source_code === "legacy_lingniu_prod") return "C-历史日统计迁移(无RAW";
if (metric) return "B-中台32960日统计(无RAW";
return "D-无中台日记录";
}
function rowNote(metric, raw) {
const notes = [];
if (metric?.source_code === "legacy_lingniu_prod") notes.push("历史生产库 ln_vehicle_day_mileage 迁移记录,不是原始帧");
if (raw?.status === "OK" && !metric) notes.push("有原始报文,但中台日统计无记录");
if (raw?.status === "OK" && metric && absDelta(metric.latest_total_mileage_km, raw.parsedTotalMileageKm) > 0.1) {
notes.push("日统计日末值与本日最后可得RAW读数不一致,详见数据源/时间;可能涉及人工源选举或跨日延迟");
}
if (raw?.status !== "OK" && metric && metric.source_code !== "legacy_lingniu_prod") notes.push("有中台日统计,但当日未检出含仪表总里程的RAW帧");
if (!metric && raw?.status !== "OK") notes.push("当日无中台日记录,且现行RAW历史不可用或未检出");
return notes.join("");
}
function setStandardSheet(sheet) {
sheet.showGridLines = false;
sheet.freezePanes.freezeRows(13);
sheet.freezePanes.freezeColumns(1);
}
for (const vehicle of data.vehicles) {
const { sheetName, sheet } = vehicleSheets.get(vehicle.vin);
setStandardSheet(sheet);
sheet.getRange("A1:P1").merge();
sheet.getRange("A1").values = [[`${vehicle.plate} 里程核验申诉明细`]];
sheet.getRange("A1:P1").format = titleFormat;
sheet.getRange("A1:P1").format.rowHeight = 30;
sheet.getRange("A2:P2").merge();
sheet.getRange("A2").values = [["核验口径:GB/T 32960 车辆仪表累计总里程;原始报文列保存完整HEX,点击单元格可在公式栏复制。相邻自然日均有RAW读数时才计算RAW日差。"]];
sheet.getRange("A2:P2").format = { fill: palette.yellow, font: { color: palette.dark, fontSize: 9, typeface: "Microsoft YaHei" }, wrapText: true, verticalAlignment: "center", borders: thinBorders };
sheet.getRange("A2:P2").format.rowHeight = 30;
sheet.getRange("A4:P6").format = valueFormat;
sheet.getRange("A4").values = [["车牌"]];
sheet.getRange("B4:C4").merge(); sheet.getRange("B4").values = [[vehicle.plate]];
sheet.getRange("D4").values = [["VIN"]];
sheet.getRange("E4:H4").merge(); sheet.getRange("E4").values = [[vehicle.vin]];
sheet.getRange("I4").values = [["车型"]];
sheet.getRange("J4:K4").merge(); sheet.getRange("J4").values = [[vehicle.model]];
sheet.getRange("L4").values = [["协议"]];
sheet.getRange("M4:P4").merge(); sheet.getRange("M4").values = [["GB32960(仪表累计总里程)"]];
sheet.getRange("A5").values = [["核验区间"]];
sheet.getRange("B5:C5").merge(); sheet.getRange("B5").values = [["2025-11-28 至 2026-07-31"]];
sheet.getRange("D5").values = [["原TBOX合计(km)"]];
sheet.getRange("E5:F5").merge(); sheet.getRange("E5").values = [[vehicle.sourceTboxKm]];
sheet.getRange("G5").values = [["第三方平台合计(km)"]];
sheet.getRange("H5:I5").merge(); sheet.getRange("H5").values = [[vehicle.platformKm]];
sheet.getRange("J5").values = [["原表损耗(km)"]];
sheet.getRange("K5:L5").merge(); sheet.getRange("K5").values = [[vehicle.lossKm]];
sheet.getRange("M5").values = [["原申诉里程(km)"]];
sheet.getRange("N5:P5").merge(); sheet.getRange("N5").values = [[vehicle.appealKm]];
sheet.getRange("E5:F5").format.numberFormat = "#,##0.0";
sheet.getRange("H5:I5").format.numberFormat = "#,##0.0";
sheet.getRange("K5:L5").format.numberFormat = "#,##0.0";
sheet.getRange("N5:P5").format.numberFormat = "#,##0.0";
sheet.getRange("K5:L5").format.fill = palette.yellow;
sheet.getRange("A6").values = [["异常标记"]];
sheet.getRange("B6:D6").merge(); sheet.getRange("B6").values = [[vehicle.anomaly]];
sheet.getRange("E6").values = [["原排查结论"]];
sheet.getRange("F6:P6").merge(); sheet.getRange("F6").values = [[vehicle.note]];
for (const address of ["A4", "A5", "A6", "D4", "D5", "E6", "G5", "I4", "J5", "L4", "M5"]) sheet.getRange(address).format = labelFormat;
sheet.getRange("A8:P8").format = valueFormat;
sheet.getRange("A8").values = [["原车辆统计合计(km)"]];
sheet.getRange("B8:C8").merge(); sheet.getRange("B8").values = [[vehicle.sourceTboxKm]];
sheet.getRange("D8").values = [["车辆数据中台统计合计(km)"]];
sheet.getRange("E8:F8").merge(); sheet.getRange("E8").formulas = [["=SUM(B14:B259)"]];
sheet.getRange("G8").values = [["中台日统计有值天数"]];
sheet.getRange("H8:I8").merge(); sheet.getRange("H8").formulas = [["=COUNT(B14:B259)"]];
sheet.getRange("J8").values = [["RAW仪表证据天数"]];
sheet.getRange("K8:L8").merge(); sheet.getRange("K8").formulas = [["=COUNT(F14:F259)"]];
sheet.getRange("M8").values = [["RAW证据日期范围"]];
sheet.getRange("N8:P8").merge(); sheet.getRange("N8").values = [[rawRange(vehicle)]];
for (const address of ["A8", "D8", "G8", "J8", "M8"]) sheet.getRange(address).format = labelFormat;
sheet.getRange("B8:C8").format.numberFormat = "#,##0.0";
sheet.getRange("E8:F8").format.numberFormat = "#,##0.0";
sheet.getRange("B8:C8").format.fill = palette.blue2;
sheet.getRange("E8:F8").format.fill = palette.mileageStrong;
sheet.getRange("E8:F8").format.font = { bold: true, color: "#7F6000", fontSize: 10, typeface: "Microsoft YaHei" };
sheet.getRange("A8:P8").format.rowHeight = 38;
sheet.getRange("A9").values = [["证据限制"]];
sheet.getRange("B9:P9").merge();
sheet.getRange("B9").values = [["现行TDengine RAW历史自2026-07-01起可查询;此前仅有历史生产库日统计迁移记录。材料已按A/B/C/D分级,未将统计记录伪称为原始报文。"]];
sheet.getRange("A9").format = labelFormat;
sheet.getRange("B9:P9").format = { ...valueFormat, fill: palette.amber };
sheet.getRange("A9:P9").format.rowHeight = 30;
sheet.getRange("A11:P11").merge();
sheet.getRange("A11").values = [["每日核验明细(完整日历)"]];
sheet.getRange("A11:P11").format = sectionFormat;
sheet.getRange("A12:D12").merge();
sheet.getRange("A12").values = [["车辆数据中台区间里程总计"]];
sheet.getRange("E12:F12").merge();
sheet.getRange("E12").formulas = [["=SUM(B14:B259)"]];
sheet.getRange("G12:P12").merge();
sheet.getRange("G12").values = [["计算范围:B14:B259(下方“中台当日里程(km)”列)"]];
sheet.getRange("A12:D12").format = {
...headerFormat,
fill: palette.mileageHeader,
horizontalAlignment: "left",
};
sheet.getRange("E12:F12").format = {
...valueFormat,
fill: palette.mileageStrong,
font: { bold: true, color: "#7F6000", fontSize: 11, typeface: "Microsoft YaHei" },
numberFormat: "#,##0.0",
horizontalAlignment: "right",
};
sheet.getRange("G12:P12").format = {
...valueFormat,
fill: palette.mileageLight,
font: { color: palette.textMuted, fontSize: 9, typeface: "Microsoft YaHei" },
};
sheet.getRange("A12:P12").format.rowHeight = 30;
const headers = ["日期", "中台当日里程(km)", "中台日末仪表总里程(km)", "日统计数据源", "日统计来源端点", "RAW解析仪表总里程(km)", "RAW相邻日差(km)", "日统计-RAW差(km)", "证据等级", "报文事件时间", "中台接收时间", "RAW来源端点", "Event ID", "Frame ID", "原始报文大小(bytes)", "原始GB32960报文HEX(完整)"];
sheet.getRange("A13:P13").values = [headers];
sheet.getRange("A13:P13").format = headerFormat;
sheet.getRange("A13:P13").format.rowHeight = 42;
const metricByDay = new Map(vehicle.dailyMetrics.map((row) => [row.stat_date, row]));
const rawByDay = new Map(vehicle.rawEvidence.map((row) => [row.day, row]));
const days = calendarDays(data.period.start, data.period.end);
const rows = days.map((day) => {
const metric = metricByDay.get(day);
const raw = rawByDay.get(day);
return [
asDate(day),
metric?.daily_mileage_km ?? null,
metric?.latest_total_mileage_km ?? null,
metric ? `${metric.platform_name || ""} / ${metric.source_code || ""}` : "",
metric?.latest_source_endpoint ?? "",
raw?.status === "OK" ? raw.parsedTotalMileageKm : null,
null,
null,
evidenceLevel(metric, raw),
raw?.status === "OK" ? raw.eventTime : "",
raw?.status === "OK" ? raw.receivedAt : "",
raw?.status === "OK" ? raw.sourceEndpoint : "",
raw?.status === "OK" ? raw.eventId : "",
raw?.status === "OK" ? raw.frameId : "",
raw?.status === "OK" ? raw.rawSizeBytes : null,
raw?.status === "OK" ? raw.rawHex : "",
];
});
sheet.getRange("A14:P259").values = rows;
sheet.getRange("A14:A259").format.numberFormat = "yyyy-mm-dd";
sheet.getRange("B14:C259").format.numberFormat = "#,##0.0";
sheet.getRange("F14:H259").format.numberFormat = "#,##0.0";
sheet.getRange("O14:O259").format.numberFormat = "#,##0";
sheet.getRange("A14:P259").format = {
font: { color: palette.dark, fontSize: 8, typeface: "Microsoft YaHei" },
verticalAlignment: "center",
borders: { insideHorizontal: { style: "thin", color: "#D9E2E8" }, bottom: { style: "thin", color: palette.border } },
};
sheet.getRange("A14:A259").format.horizontalAlignment = "center";
sheet.getRange("B14:C259").format.horizontalAlignment = "right";
sheet.getRange("F14:H259").format.horizontalAlignment = "right";
sheet.getRange("I14:O259").format.wrapText = true;
sheet.getRange("P14:P259").format = { font: { color: palette.muted, fontSize: 7, typeface: "Consolas" }, wrapText: true, verticalAlignment: "top" };
sheet.getRange("A14:P259").format.rowHeight = 34;
sheet.getRange("G14").values = [[null]];
for (let row = 15; row <= 259; row += 1) {
sheet.getRange(`G${row}`).formulas = [[`=IF(OR(F${row}="",F${row - 1}=""),"",F${row}-F${row - 1})`]];
}
for (let row = 14; row <= 259; row += 1) {
sheet.getRange(`H${row}`).formulas = [[`=IF(OR(C${row}="",F${row}=""),"",C${row}-F${row})`]];
}
sheet.getRange("I14:I259").conditionalFormats.add("containsText", { text: "A-", format: { fill: palette.green, font: { color: "#375623", bold: true } } });
sheet.getRange("I14:I259").conditionalFormats.add("containsText", { text: "C-", format: { fill: palette.yellow, font: { color: "#7F6000" } } });
sheet.getRange("I14:I259").conditionalFormats.add("containsText", { text: "D-", format: { fill: palette.gray, font: { color: palette.muted } } });
sheet.getRange("H14:H259").conditionalFormats.addCustom("=ABS($H14)>0.1", { fill: palette.amber, font: { color: "#9C0006", bold: true } });
const table = sheet.tables.add("A13:P259", true, `Vehicle${String(vehicle.seq).padStart(2, "0")}Evidence`);
table.style = "TableStyleMedium2";
table.showBandedColumns = false;
table.showFilterButton = true;
sheet.getRange("B13").format = { ...headerFormat, fill: palette.mileageHeader };
sheet.getRange("B14:B259").format.fill = palette.mileageLight;
sheet.getRange("B14:B259").conditionalFormats.add("cellIs", {
operator: "greaterThan",
formula: 0,
format: { fill: palette.mileageStrong, font: { bold: true, color: "#7F6000" } },
});
const widths = [12, 17, 20, 24, 26, 20, 17, 18, 25, 20, 20, 22, 34, 34, 14, 72];
for (let col = 0; col < widths.length; col += 1) sheet.getRangeByIndexes(0, col, 259, 1).format.columnWidth = widths[col];
}
summary.showGridLines = false;
summary.freezePanes.freezeRows(4);
summary.getRange("A1:P1").merge();
summary.getRange("A1").values = [["15辆车里程核验申诉总览"]];
summary.getRange("A1:P1").format = titleFormat;
summary.getRange("A1:P1").format.rowHeight = 32;
summary.getRange("A2:P2").merge();
summary.getRange("A2").values = [["结论:车辆数据中台GB32960日里程复核合计与原异常表TBOX区间里程逐车一致;第三方平台合计显著偏低。RAW原始帧按现有可查历史提供,早期记录已明确标注证据限制。"]];
summary.getRange("A2:P2").format = { fill: palette.green, font: { bold: true, color: "#375623", fontSize: 10, typeface: "Microsoft YaHei" }, wrapText: true, verticalAlignment: "center", borders: thinBorders };
summary.getRange("A2:P2").format.rowHeight = 34;
summary.getRange("A3:B3").merge(); summary.getRange("A3").values = [["全部15辆车·原车辆统计(km)"]];
summary.getRange("C3").formulas = [["=F20"]];
summary.getRange("D3:E3").merge(); summary.getRange("D3").values = [["车辆数据中台统计(km)"]];
summary.getRange("F3").formulas = [["=G20"]];
summary.getRange("G3:H3").merge(); summary.getRange("G3").values = [["第三方平台统计(km)"]];
summary.getRange("I3").formulas = [["=H20"]];
summary.getRange("J3:K3").merge(); summary.getRange("J3").values = [["平台漏计差异(km)"]];
summary.getRange("L3").formulas = [["=I20"]];
summary.getRange("M3:N3").merge(); summary.getRange("M3").values = [["RAW仪表证据(天)"]];
summary.getRange("O3:P3").merge(); summary.getRange("O3").formulas = [["=M20"]];
for (const address of ["A3:B3", "D3:E3", "G3:H3", "J3:K3", "M3:N3"]) summary.getRange(address).format = labelFormat;
for (const address of ["C3", "F3", "I3", "L3", "O3:P3"]) summary.getRange(address).format = { ...valueFormat, fill: palette.mileageLight, font: { bold: true, color: "#7F6000", fontSize: 10, typeface: "Microsoft YaHei" }, horizontalAlignment: "right" };
summary.getRange("C3:L3").format.numberFormat = "#,##0.0";
summary.getRange("O3:P3").format.numberFormat = "#,##0";
summary.getRange("A3:P3").format.rowHeight = 34;
const summaryHeaders = ["序号", "车型", "车牌", "VIN", "核验区间", "原TBOX合计(km)", "中台复核合计(km)", "第三方平台合计(km)", "损耗(km)", "损耗率", "申诉里程(km)", "日统计天数", "RAW证据天数", "RAW证据范围", "核验结论", "原排查结论"];
summary.getRange("A4:P4").values = [summaryHeaders];
summary.getRange("A4:P4").format = headerFormat;
summary.getRange("A4:P4").format.rowHeight = 40;
const summaryRows = data.vehicles.map((vehicle) => [
vehicle.seq, vehicle.model, vehicle.plate, vehicle.vin, "2025-11-28 至 2026-07-31", vehicle.sourceTboxKm, null, vehicle.platformKm, null, null, vehicle.appealKm, null, null, rawRange(vehicle), null, vehicle.note,
]);
summary.getRange("A5:P19").values = summaryRows;
for (let index = 0; index < data.vehicles.length; index += 1) {
const row = 5 + index;
const { sheetName } = vehicleSheets.get(data.vehicles[index].vin);
summary.getRange(`G${row}`).formulas = [[`='${sheetName}'!E8`]];
summary.getRange(`I${row}`).formulas = [[`=F${row}-H${row}`]];
summary.getRange(`J${row}`).formulas = [[`=IF(F${row}=0,"",I${row}/F${row})`]];
summary.getRange(`L${row}`).formulas = [[`='${sheetName}'!H8`]];
summary.getRange(`M${row}`).formulas = [[`='${sheetName}'!K8`]];
summary.getRange(`O${row}`).formulas = [[`=IF(ABS(G${row}-F${row})<0.1,"中台复核合计与原TBOX一致","需复核中台合计")`]];
}
summary.getRange("A20:E20").merge(); summary.getRange("A20").values = [["合计"]];
summary.getRange("F20").formulas = [["=SUM(F5:F19)"]];
summary.getRange("G20").formulas = [["=SUM(G5:G19)"]];
summary.getRange("H20").formulas = [["=SUM(H5:H19)"]];
summary.getRange("I20").formulas = [["=SUM(I5:I19)"]];
summary.getRange("J20").formulas = [["=IF(F20=0,\"\",I20/F20)"]];
summary.getRange("K20").formulas = [["=SUM(K5:K19)"]];
summary.getRange("L20").formulas = [["=SUM(L5:L19)"]];
summary.getRange("M20").formulas = [["=SUM(M5:M19)"]];
summary.getRange("N20:P20").merge(); summary.getRange("N20").values = [["15车合计;RAW证据为现行历史可查询范围"]];
summary.getRange("A20:P20").format = { fill: palette.blue, font: { bold: true, color: palette.dark, fontSize: 9, typeface: "Microsoft YaHei" }, borders: thinBorders, verticalAlignment: "center", wrapText: true };
summary.getRange("A5:P19").format = { font: { color: palette.dark, fontSize: 9, typeface: "Microsoft YaHei" }, verticalAlignment: "center", wrapText: true, borders: { insideHorizontal: { style: "thin", color: "#D9E2E8" }, bottom: { style: "thin", color: palette.border } } };
summary.getRange("F5:I20").format.numberFormat = "#,##0.0";
summary.getRange("J5:J20").format.numberFormat = "0.00%";
summary.getRange("K5:K20").format.numberFormat = "#,##0.0";
summary.getRange("L5:M20").format.numberFormat = "#,##0";
summary.getRange("I5:I19").format.fill = palette.yellow;
summary.getRange("O5:O19").conditionalFormats.add("containsText", { text: "一致", format: { fill: palette.green, font: { color: "#375623", bold: true } } });
const summaryTable = summary.tables.add("A4:P19", true, "AppealSummary");
summaryTable.style = "TableStyleMedium2";
summaryTable.showFilterButton = true;
const summaryWidths = [8, 9, 14, 22, 22, 16, 18, 18, 15, 12, 16, 13, 14, 24, 26, 54];
for (let col = 0; col < summaryWidths.length; col += 1) summary.getRangeByIndexes(0, col, 20, 1).format.columnWidth = summaryWidths[col];
summary.getRange("A5:P19").format.rowHeight = 42;
notes.showGridLines = false;
notes.getRange("A1:H1").merge();
notes.getRange("A1").values = [["核验口径与证据说明"]];
notes.getRange("A1:H1").format = titleFormat;
notes.getRange("A1:H1").format.rowHeight = 32;
notes.getRange("A3:H3").merge(); notes.getRange("A3").values = [["1. 核验口径"]]; notes.getRange("A3:H3").format = sectionFormat;
notes.getRange("A4:B8").values = [
["协议", "GB/T 32960"],
["仪表总里程字段", "gb32960.vehicle.total_mileage_km"],
["仪表总里程含义", "车辆仪表盘累计总里程,单位 km;不同协议累计值不可拼接。"],
["中台当日里程", "生产 vehicle_daily_mileage 的 GB32960 日统计值;本材料逐车求和后与原异常表TBOX合计核对。"],
["RAW相邻日差", "仅相邻两个自然日都存在RAW仪表读数时计算;缺任一日则留空,不把跨日缺口跳变塞进单日。"],
];
notes.getRange("A4:A8").format = labelFormat;
notes.getRange("B4:H8").merge(true);
notes.getRange("B4:H8").format = valueFormat;
notes.getRange("A10:H10").merge(); notes.getRange("A10").values = [["2. 证据分级"]]; notes.getRange("A10:H10").format = sectionFormat;
notes.getRange("A11:B14").values = [
["A-原始32960报文", "中台TDengine保存的完整GB32960十六进制报文,且解析结果明确包含仪表总里程字段。"],
["B-中台32960日统计", "中台日统计存在,但当日未检出含仪表总里程的RAW帧。"],
["C-历史日统计迁移", "来自历史生产库 lingniu_prod.ln_vehicle_day_mileage 的迁移记录,不是原始帧。"],
["D-无中台日记录", "当日中台日统计无记录,且现行RAW历史不可用或未检出。"],
];
notes.getRange("A11:A14").format = labelFormat;
notes.getRange("B11:H14").merge(true);
notes.getRange("B11:H14").format = valueFormat;
notes.getRange("A11:H11").format.fill = palette.green;
notes.getRange("A13:H13").format.fill = palette.yellow;
notes.getRange("A14:H14").format.fill = palette.gray;
notes.getRange("A16:H16").merge(); notes.getRange("A16").values = [["3. 数据来源与限制"]]; notes.getRange("A16:H16").format = sectionFormat;
notes.getRange("A17:B21").values = [
["原异常表", "/Users/lingniu/Documents/里程异常统计表_20251128-20260731-回复.xlsx"],
["中台日统计API", "http://115.29.187.205:20200/api/stats/daily-metrics"],
["中台RAW历史API", "http://115.29.187.205:20200/api/history/raw-frames"],
["RAW可查范围", "本次提取范围2026-07-01至2026-07-3115车共取得334个含仪表总里程字段的完整原始帧。"],
["重要限制", "2026-07-01之前现行TDengine没有可查询RAW历史;只能提供历史生产库日统计迁移记录。对外申诉时应如实说明,并向原TSP/车厂补调更早期原始报文归档。"],
];
notes.getRange("A17:A21").format = labelFormat;
notes.getRange("B17:H21").merge(true);
notes.getRange("B17:H21").format = valueFormat;
notes.getRange("A21:H21").format.fill = palette.amber;
notes.getRange("A23:H23").merge(); notes.getRange("A23").values = [["4. 使用建议"]]; notes.getRange("A23:H23").format = sectionFormat;
notes.getRange("A24:H27").values = [
["① 先提交“申诉总览”,证明15车中台GB32960日里程合计与原TBOX合计一致。", null, null, null, null, null, null, null],
["② 每车附对应Sheet,重点引用A类RAW证据、事件时间、接收时间和完整HEX。", null, null, null, null, null, null, null],
["③ 对第三方缺失日,用完整日历中的D级空缺、前后仪表累计变化和原排查结论说明漏传。", null, null, null, null, null, null, null],
["④ 申诉若要求全区间逐日原始报文,需由原TSP/车厂补调2026-07-01之前归档;本材料未虚构缺失证据。", null, null, null, null, null, null, null],
];
for (let row = 24; row <= 27; row += 1) notes.getRange(`A${row}:H${row}`).merge();
notes.getRange("A24:H27").format = { ...valueFormat, fill: palette.blue2 };
notes.getRange("A4:H27").format.rowHeight = 30;
notes.getRange("A1:A27").format.columnWidth = 22;
for (let col = 1; col < 8; col += 1) notes.getRangeByIndexes(0, col, 27, 1).format.columnWidth = 18;
await fs.mkdir(outputDir, { recursive: true });
await fs.mkdir(previewDir, { recursive: true });
const inspection = {};
inspection.summary = (await workbook.inspect({ kind: "table", range: "申诉总览!A1:P20", include: "values,formulas", tableMaxRows: 24, tableMaxCols: 16, maxChars: 22000 })).ndjson;
inspection.formulaErrors = (await workbook.inspect({ kind: "match", searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A", options: { useRegex: true, maxResults: 300 }, summary: "final formula error scan" })).ndjson;
await fs.writeFile(path.join(outputDir, "verification.ndjson"), `${inspection.summary}\n${inspection.formulaErrors}\n`, "utf8");
const previewSheets = ["申诉总览", "口径与证据说明", ...data.vehicles.map((vehicle) => vehicleSheets.get(vehicle.vin).sheetName)];
for (let index = 0; index < previewSheets.length; index += 1) {
const sheetName = previewSheets[index];
const range = sheetName === "申诉总览" ? "A1:P20" : sheetName === "口径与证据说明" ? "A1:H27" : "A1:P24";
const preview = await workbook.render({ sheetName, range, scale: 1, format: "png" });
const safeName = sheetName.replaceAll(/[\\/:*?\[\]]/g, "_");
await fs.writeFile(path.join(previewDir, `${String(index + 1).padStart(2, "0")}-${safeName}.png`), new Uint8Array(await preview.arrayBuffer()));
}
const output = await SpreadsheetFile.exportXlsx(workbook);
await output.save(outputPath);
console.log(JSON.stringify({ outputPath, sheets: previewSheets.length, previewDir }, null, 2));