Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/domain/profileSync.ts

71 lines
3.4 KiB
TypeScript

import type { VehicleProfileSyncItem } from '../../api/types';
const headers = ['vin', 'modelName', 'vehicleType', 'companyName', 'operationStatus', 'accessProvider', 'firstAccessAt', 'runtimeSeconds'] as const;
const allowedStatuses = new Set(['', 'unknown', 'active', 'inactive', 'maintenance', 'retired']);
export const vehicleProfileSyncCSVHeader = headers.join(',');
export function parseVehicleProfileSyncCSV(text: string): VehicleProfileSyncItem[] {
const rows = parseCSVRows(text.replace(/^\uFEFF/, ''));
if (rows.length < 2) throw new Error('CSV 至少需要表头和一行车辆数据');
const actualHeaders = rows[0].map((value) => value.trim());
if (actualHeaders.length !== headers.length || actualHeaders.some((value, index) => value !== headers[index])) {
throw new Error(`CSV 表头必须为:${vehicleProfileSyncCSVHeader}`);
}
const dataRows = rows.slice(1).filter((row) => row.some((value) => value.trim() !== ''));
if (dataRows.length === 0 || dataRows.length > 500) throw new Error('单个 CSV 必须包含 1 至 500 辆车');
const seen = new Set<string>();
return dataRows.map((row, index) => {
const line = index + 2;
if (row.length !== headers.length) throw new Error(`CSV 第 ${line} 行列数不正确`);
const [rawVIN, modelName, vehicleType, companyName, rawStatus, accessProvider, firstAccessAt, rawRuntime] = row.map((value) => value.trim());
const vin = rawVIN.toUpperCase();
if (!vin || vin.length > 32) throw new Error(`CSV 第 ${line} 行 VIN 无效`);
if (seen.has(vin)) throw new Error(`CSV 第 ${line} 行 VIN 重复:${vin}`);
seen.add(vin);
const operationStatus = rawStatus.toLowerCase();
if (!allowedStatuses.has(operationStatus)) throw new Error(`CSV 第 ${line} 行运营状态无效`);
const runtimeSeconds = rawRuntime === '' ? null : Number(rawRuntime);
if (runtimeSeconds !== null && (!Number.isSafeInteger(runtimeSeconds) || runtimeSeconds < 0)) throw new Error(`CSV 第 ${line} 行累计运行秒数无效`);
return {
vin, modelName, vehicleType, companyName,
operationStatus: (operationStatus || 'unknown') as VehicleProfileSyncItem['operationStatus'],
accessProvider, firstAccessAt, runtimeSeconds
};
});
}
function parseCSVRows(text: string): string[][] {
const rows: string[][] = [];
let row: string[] = [];
let field = '';
let quoted = false;
let closedQuote = false;
for (let index = 0; index < text.length; index += 1) {
const char = text[index];
if (quoted) {
if (char === '"') {
if (text[index + 1] === '"') { field += '"'; index += 1; } else { quoted = false; closedQuote = true; }
} else {
field += char;
}
continue;
}
if (closedQuote && char !== ',' && char !== '\n' && char !== '\r') throw new Error('CSV 引号结束后存在无效字符');
if (char === '"') {
if (field !== '') throw new Error('CSV 引号格式无效');
quoted = true;
} else if (char === ',') {
row.push(field); field = ''; closedQuote = false;
} else if (char === '\n' || char === '\r') {
if (char === '\r' && text[index + 1] === '\n') index += 1;
row.push(field); rows.push(row); row = []; field = ''; closedQuote = false;
} else {
field += char;
}
}
if (quoted) throw new Error('CSV 存在未闭合的引号');
if (field !== '' || row.length > 0) { row.push(field); rows.push(row); }
return rows;
}