Files
gongxue-base/apps/server/src/imports/imports.workbook.ts
wangziqi ab4765cee5 feat: Excel 导入预检与动态问答,AI 聊天/文件解析体验修复
- imports: 新增 preflight_import 预检报告(判定/分阶段统计/阻断归因/问题/下一步/错误示例),导入任务 settings 落库(映射/校区/更新/重复/未匹配策略),预览应用策略,向导提交写操作日志
- ai-chat: 新增 excel_analyze(ExcelJS)工具,移除附件/上下文截断,start_import_wizard 支持确认参数,ui.import_preflight SSE,预览确认写操作日志
- admin: ImportPreflightCard 渲染与持久化,聊天抽屉布局/侧边栏修复,考勤页 CSS 引入,费用/学生页接口 schema 校验修复
2026-08-05 21:12:50 +08:00

89 lines
3.0 KiB
TypeScript
Raw 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 { 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<ImportSheetData[]> {
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 文件解析失败,请检查文件格式');
}
}