71 lines
3.4 KiB
TypeScript
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;
|
|
}
|