import { BadRequestException } from '@nestjs/common'; import * as ExcelJS from 'exceljs'; import { Readable } from 'node:stream'; import { cellValue, textValue } from './imports.helpers'; import type { CellValue } from './imports.types'; const MAX_SHEETS = 30; const MAX_ROWS_PER_SHEET = 3000; const MAX_COLS_PER_SHEET = 60; export interface ImportSheetData { name: string; headers: string[]; rows: CellValue[][]; } export function extractSheets(workbook: ExcelJS.Workbook): ImportSheetData[] { const sheets: ImportSheetData[] = []; for (const worksheet of workbook.worksheets) { if (sheets.length >= MAX_SHEETS) break; const headers: string[] = []; const rows: CellValue[][] = []; const firstRow = worksheet.getRow(1); for (let col = 1; col <= Math.min(firstRow.cellCount, MAX_COLS_PER_SHEET); col += 1) { const header = textValue(cellValue(firstRow.getCell(col))); headers.push(header); } if (!headers.some(Boolean)) continue; worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => { if (rowNumber === 1 || rows.length >= MAX_ROWS_PER_SHEET) return; const values: CellValue[] = []; for (let col = 1; col <= headers.length; col += 1) { values.push(cellValue(row.getCell(col))); } if (values.every((v) => v === null || textValue(v) === '')) return; rows.push(values); }); if (rows.length > 0) sheets.push({ name: worksheet.name, headers, rows }); } return sheets; } export type WorkbookKind = 'csv' | 'xlsx'; export function detectWorkbookKind(originalName: string, mimeType: string): WorkbookKind | null { const isCsv = /\.csv$/i.test(originalName) || /csv/i.test(mimeType) || /text\/(csv|plain)/i.test(mimeType); const isXlsx = /\.xlsx$/i.test(originalName) || /spreadsheetml/i.test(mimeType) || /excel/i.test(mimeType); if (isCsv) return 'csv'; if (isXlsx) return 'xlsx'; return null; } /** 校验文件类型并解析为工作表数据;解析失败抛出可读错误。 */ export async function parseSheets( buffer: Buffer, originalName: string, mimeType: string, ): Promise { const kind = detectWorkbookKind(originalName, mimeType); if (!kind) { throw new BadRequestException('仅支持 .xlsx / .csv 文件'); } if (/\.xls$/i.test(originalName) && !/\.xlsx$/i.test(originalName)) { throw new BadRequestException('暂不支持 .xls,请另存为 .xlsx 或 .csv 后重试'); } try { const workbook = new ExcelJS.Workbook(); if (kind === 'csv') { await workbook.csv.read(Readable.from(Buffer.from(buffer))); } else { await workbook.xlsx.load(buffer.buffer as ArrayBuffer); } const sheets = extractSheets(workbook); if (sheets.length === 0) { throw new BadRequestException('文件中没有可用的工作表数据'); } return sheets; } catch (error) { if (error instanceof BadRequestException) throw error; throw new BadRequestException('Excel 文件解析失败,请检查文件格式'); } }