chore: snapshot production code before Apple Design UI refinement

This commit is contained in:
lingniu
2026-09-17 18:05:57 +08:00
parent 9f0a87f49e
commit f7e7176f88
90 changed files with 8990 additions and 328 deletions
@@ -0,0 +1,94 @@
import fs from "node:fs/promises";
import path from "node:path";
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
const sourceDir =
"/Users/lingniu/Library/Mobile Documents/com~apple~CloudDocs/rsync/2026/07/27";
const workDir =
"/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/tmp/gps-mileage-import-019fa1ac-db39-7933-bb0e-30dd63cc25bd";
const names = (await fs.readdir(sourceDir))
.filter((name) => name.endsWith(".xlsx"))
.sort((a, b) => a.localeCompare(b, "zh-CN"));
const manifest = [];
for (const name of names) {
const filePath = path.join(sourceDir, name);
const input = await FileBlob.load(filePath);
const workbook = await SpreadsheetFile.importXlsx(input);
const sheet = workbook.worksheets.getItemAt(0);
const usedRange = sheet.getUsedRange(true);
const values = usedRange?.values ?? [];
const rowCount = values.length;
const colCount = values.reduce((max, row) => Math.max(max, row.length), 0);
const topRows = values.slice(0, 8).map((row) =>
row.slice(0, Math.min(colCount, 40)).map((value) => {
if (value instanceof Date) return value.toISOString();
return value ?? null;
}),
);
const preview = await workbook.render({
sheetName: sheet.name,
range: `A1:${columnName(Math.min(Math.max(colCount, 1), 40))}${Math.min(
Math.max(rowCount, 1),
15,
)}`,
scale: 1,
format: "png",
});
const previewName = `${String(manifest.length + 1).padStart(2, "0")}-${safeName(
name,
)}.png`;
await fs.writeFile(
path.join(workDir, previewName),
new Uint8Array(await preview.arrayBuffer()),
);
manifest.push({
name,
sheet: sheet.name,
rowCount,
colCount,
firstRow: topRows[0] ?? [],
topRows,
preview: previewName,
});
}
await fs.writeFile(
path.join(workDir, "source-manifest.json"),
`${JSON.stringify(manifest, null, 2)}\n`,
"utf8",
);
console.log(
JSON.stringify(
manifest.map(({ name, sheet, rowCount, colCount, firstRow, preview }) => ({
name,
sheet,
rowCount,
colCount,
firstRow,
preview,
})),
null,
2,
),
);
function safeName(name) {
return name
.replace(/\.xlsx$/i, "")
.replace(/[^\p{L}\p{N}._-]+/gu, "_")
.slice(0, 100);
}
function columnName(index) {
let value = index;
let result = "";
while (value > 0) {
value -= 1;
result = String.fromCharCode(65 + (value % 26)) + result;
value = Math.floor(value / 26);
}
return result || "A";
}
@@ -0,0 +1,378 @@
import fs from "node:fs/promises";
import fsSync from "node:fs";
import path from "node:path";
import readline from "node:readline";
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
const sourceDir =
"/Users/lingniu/Library/Mobile Documents/com~apple~CloudDocs/rsync/2026/07/27";
const workDir =
"/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/tmp/gps-mileage-import-019fa1ac-db39-7933-bb0e-30dd63cc25bd";
const priorCsv =
"/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/outputs/g7-mileage-history-20260724/merged/G7车辆每日里程_20220101-20260720.csv";
const normalizedCsv = path.join(
workDir,
"G7_GPS车辆每日里程_20251231-20260630.csv",
);
const combinedCsv = path.join(
workDir,
"G7_GPS车辆每日里程_合并优先季度统计_20251231-20260630.csv",
);
const statisticCsv = path.join(
workDir,
"G7_GPS季度统计每日里程_20260101-20260630.csv",
);
const dailyNames = [
"智能管车_车辆里程日报-091555330.xlsx",
"智能管车_车辆里程日报-090938059.xlsx",
"智能管车_车辆里程日报-091002580.xlsx",
"智能管车_车辆里程日报-091020404.xlsx",
"智能管车_车辆里程日报-091030619.xlsx",
"智能管车_车辆里程日报-091046118.xlsx",
"智能管车_车辆里程日报-091058580.xlsx",
];
const statisticNames = [
"里程统计[天][2026-01-01至2026-03-31] (1).xlsx",
"里程统计[天][2026-01-01至2026-03-31].xlsx",
"里程统计[天][2026-04-01至2026-06-30] (1).xlsx",
"里程统计[天][2026-04-01至2026-06-30].xlsx",
];
const rows = [];
const newByKey = new Map();
const dailyFiles = [];
for (const name of dailyNames) {
const values = await readFirstSheetValues(path.join(sourceDir, name));
const headers = values[0] ?? [];
const runtimeIndex = headers.indexOf("运行时长");
assert(runtimeIndex > 3, `${name} 缺少运行时长列`);
const dateHeaders = headers.slice(3, runtimeIndex);
const dates = dateHeaders.map((header) => parseHeaderDate(header));
const seenPlates = new Set();
let statedTotalKm = 0;
let calculatedTotalKm = 0;
let mismatchRows = 0;
for (const row of values.slice(1)) {
const plate = cleanText(row?.[0]);
if (!plate) continue;
assert(!seenPlates.has(plate), `${name} 存在重复车牌 ${plate}`);
seenPlates.add(plate);
const stated = numberValue(row?.[2], 10_000_000);
const dailyValues = row
.slice(3, runtimeIndex)
.map((value) => numberValue(value));
const calculated = dailyValues.reduce((sum, value) => sum + value, 0);
statedTotalKm += stated;
calculatedTotalKm += calculated;
if (Math.abs(stated - calculated) > 0.011) mismatchRows += 1;
for (let index = 0; index < dates.length; index += 1) {
const date = dates[index];
const mileage = dailyValues[index];
const key = `${plate}|${date}`;
assert(!newByKey.has(key), `日报重复车辆日 ${key}`);
const normalized = { plate, date, mileage };
newByKey.set(key, normalized);
rows.push(normalized);
}
}
dailyFiles.push({
name,
vehicleCount: seenPlates.size,
dateFrom: dates[0],
dateTo: dates.at(-1),
dateCount: dates.length,
rowCount: seenPlates.size * dates.length,
statedTotalKm: round(statedTotalKm),
calculatedTotalKm: round(calculatedTotalKm),
mismatchRows,
});
}
rows.sort(
(a, b) =>
a.plate.localeCompare(b.plate, "zh-CN") || a.date.localeCompare(b.date),
);
await fs.writeFile(
normalizedCsv,
`\uFEFFplate,date,daily_mileage_km\n${rows
.map((row) => `${row.plate},${row.date},${formatNumber(row.mileage)}`)
.join("\n")}\n`,
"utf8",
);
const statisticByKey = new Map();
const statisticFiles = [];
for (const name of statisticNames) {
const values = await readFirstSheetValues(path.join(sourceDir, name));
const headers = (values[1] ?? []).map(cleanText);
const plateIndex = headers.indexOf("车牌号码");
const methodIndex = headers.indexOf("里程计算方式");
const dateIndex = headers.indexOf("日期");
const mileageIndex = headers.indexOf("里程(km");
assert(
[plateIndex, methodIndex, dateIndex, mileageIndex].every((index) => index >= 0),
`${name} 表头不完整`,
);
const fileKeys = new Set();
const methods = new Set();
let totalKm = 0;
for (const row of values.slice(2)) {
const plate = cleanText(row?.[plateIndex]);
const date = isoDate(row?.[dateIndex]);
if (!plate || !date) continue;
const method = cleanText(row?.[methodIndex]);
const mileage = numberValue(row?.[mileageIndex]);
const key = `${plate}|${date}`;
assert(!fileKeys.has(key), `${name} 文件内重复车辆日 ${key}`);
assert(!statisticByKey.has(key), `季度统计跨文件重复车辆日 ${key}`);
fileKeys.add(key);
statisticByKey.set(key, { plate, date, mileage, method, name });
methods.add(method);
totalKm += mileage;
}
statisticFiles.push({
name,
rowCount: fileKeys.size,
vehicleCount: new Set([...fileKeys].map((key) => key.split("|")[0])).size,
dateCount: new Set([...fileKeys].map((key) => key.split("|")[1])).size,
methods: [...methods].sort(),
totalKm: round(totalKm),
});
}
let statisticMatched = 0;
let statisticMissingInDaily = 0;
let statisticChanged = 0;
let statisticAbsoluteDifferenceKm = 0;
let statisticMaxDifferenceKm = 0;
const statisticExamples = [];
for (const [key, statistic] of statisticByKey) {
const daily = newByKey.get(key);
if (!daily) {
statisticMissingInDaily += 1;
continue;
}
statisticMatched += 1;
const difference = daily.mileage - statistic.mileage;
const absolute = Math.abs(difference);
statisticAbsoluteDifferenceKm += absolute;
statisticMaxDifferenceKm = Math.max(statisticMaxDifferenceKm, absolute);
if (absolute > 0.011) {
statisticChanged += 1;
if (statisticExamples.length < 20) {
statisticExamples.push({
key,
dailyKm: daily.mileage,
statisticKm: statistic.mileage,
differenceKm: round(difference),
});
}
}
}
const statisticRows = [...statisticByKey.values()].sort(
(a, b) =>
a.plate.localeCompare(b.plate, "zh-CN") || a.date.localeCompare(b.date),
);
await fs.writeFile(
statisticCsv,
`\uFEFFplate,date,daily_mileage_km\n${statisticRows
.map((row) => `${row.plate},${row.date},${formatNumber(row.mileage)}`)
.join("\n")}\n`,
"utf8",
);
const combinedByKey = new Map(newByKey);
for (const [key, statistic] of statisticByKey) {
combinedByKey.set(key, {
plate: statistic.plate,
date: statistic.date,
mileage: statistic.mileage,
});
}
const combinedRows = [...combinedByKey.values()].sort(
(a, b) =>
a.plate.localeCompare(b.plate, "zh-CN") || a.date.localeCompare(b.date),
);
await fs.writeFile(
combinedCsv,
`\uFEFFplate,date,daily_mileage_km\n${combinedRows
.map((row) => `${row.plate},${row.date},${formatNumber(row.mileage)}`)
.join("\n")}\n`,
"utf8",
);
const priorByKey = await loadPriorRange(
priorCsv,
"2025-12-31",
"2026-06-30",
);
let priorUnchanged = 0;
let priorChanged = 0;
let priorMissing = 0;
let priorExtra = 0;
let priorTotalDifferenceKm = 0;
let priorMaxDifferenceKm = 0;
const priorExamples = [];
for (const [key, row] of newByKey) {
const prior = priorByKey.get(key);
if (!prior) {
priorMissing += 1;
continue;
}
const difference = row.mileage - prior.mileage;
const absolute = Math.abs(difference);
priorTotalDifferenceKm += difference;
priorMaxDifferenceKm = Math.max(priorMaxDifferenceKm, absolute);
if (absolute <= 0.0005) {
priorUnchanged += 1;
} else {
priorChanged += 1;
if (priorExamples.length < 20) {
priorExamples.push({
key,
newKm: row.mileage,
priorKm: prior.mileage,
differenceKm: round(difference),
});
}
}
}
for (const key of priorByKey.keys()) {
if (!newByKey.has(key)) priorExtra += 1;
}
const result = {
normalizedCsv,
combinedCsv,
statisticCsv,
source: {
rowCount: rows.length,
vehicleCount: new Set(rows.map((row) => row.plate)).size,
dateCount: new Set(rows.map((row) => row.date)).size,
dateFrom: rows.reduce(
(min, row) => (!min || row.date < min ? row.date : min),
"",
),
dateTo: rows.reduce((max, row) => (row.date > max ? row.date : max), ""),
positiveRows: rows.filter((row) => row.mileage > 0).length,
zeroRows: rows.filter((row) => row.mileage === 0).length,
totalKm: round(rows.reduce((sum, row) => sum + row.mileage, 0)),
files: dailyFiles,
},
quarterlyReconciliation: {
rowCount: statisticByKey.size,
matchedRows: statisticMatched,
missingInDailyRows: statisticMissingInDaily,
changedRowsOver0011Km: statisticChanged,
absoluteDifferenceKm: round(statisticAbsoluteDifferenceKm),
maxDifferenceKm: round(statisticMaxDifferenceKm),
examples: statisticExamples,
files: statisticFiles,
},
combinedImport: {
rowCount: combinedRows.length,
vehicleCount: new Set(combinedRows.map((row) => row.plate)).size,
dateCount: new Set(combinedRows.map((row) => row.date)).size,
dateFrom: combinedRows.reduce(
(min, row) => (!min || row.date < min ? row.date : min),
"",
),
dateTo: combinedRows.reduce(
(max, row) => (row.date > max ? row.date : max),
"",
),
positiveRows: combinedRows.filter((row) => row.mileage > 0).length,
zeroRows: combinedRows.filter((row) => row.mileage === 0).length,
totalKm: round(
combinedRows.reduce((sum, row) => sum + row.mileage, 0),
),
precedence:
"季度里程统计(明确标注终端里程/经纬度)优先;无季度记录时使用车辆里程日报",
},
priorImportComparison: {
priorRangeRows: priorByKey.size,
unchangedRows: priorUnchanged,
changedRows: priorChanged,
missingInPriorRows: priorMissing,
extraPriorRows: priorExtra,
totalDifferenceKm: round(priorTotalDifferenceKm),
maxDifferenceKm: round(priorMaxDifferenceKm),
examples: priorExamples,
},
};
await fs.writeFile(
path.join(workDir, "normalization-manifest.json"),
`${JSON.stringify(result, null, 2)}\n`,
"utf8",
);
console.log(JSON.stringify(result, null, 2));
async function readFirstSheetValues(file) {
const input = await FileBlob.load(file);
const workbook = await SpreadsheetFile.importXlsx(input);
return workbook.worksheets.getItemAt(0).getUsedRange(true)?.values ?? [];
}
async function loadPriorRange(file, dateFrom, dateTo) {
const result = new Map();
const stream = fsSync.createReadStream(file, { encoding: "utf8" });
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
let first = true;
for await (const line of lines) {
if (first) {
first = false;
continue;
}
if (!line) continue;
const [plate, date, rawMileage] = line.split(",");
if (date < dateFrom || date > dateTo) continue;
result.set(`${plate}|${date}`, {
plate,
date,
mileage: numberValue(rawMileage),
});
}
return result;
}
function parseHeaderDate(value) {
const text = cleanText(value);
const match = text.match(/^(\d{2})月(\d{2})日$/);
assert(match, `无法解析日报日期表头 ${text}`);
const month = Number(match[1]);
const year = month === 12 ? 2025 : 2026;
return `${year}-${match[1]}-${match[2]}`;
}
function isoDate(value) {
if (value instanceof Date) return value.toISOString().slice(0, 10);
const text = cleanText(value);
const match = text.match(/^(\d{4})-(\d{2})-(\d{2})/);
return match ? `${match[1]}-${match[2]}-${match[3]}` : "";
}
function cleanText(value) {
return String(value ?? "").trim();
}
function numberValue(value, max = 2500) {
if (value === null || value === undefined || value === "") return 0;
const number = Number(String(value).replace(/,/g, ""));
assert(Number.isFinite(number) && number >= 0 && number <= max, `非法里程 ${value}`);
return number;
}
function formatNumber(value) {
if (Number.isInteger(value)) return String(value);
return String(Number(value.toFixed(3)));
}
function round(value) {
return Math.round((value + Number.EPSILON) * 1000) / 1000;
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}