feat: Excel 导入预检与动态问答,AI 聊天/文件解析体验修复

- imports: 新增 preflight_import 预检报告(判定/分阶段统计/阻断归因/问题/下一步/错误示例),导入任务 settings 落库(映射/校区/更新/重复/未匹配策略),预览应用策略,向导提交写操作日志
- ai-chat: 新增 excel_analyze(ExcelJS)工具,移除附件/上下文截断,start_import_wizard 支持确认参数,ui.import_preflight SSE,预览确认写操作日志
- admin: ImportPreflightCard 渲染与持久化,聊天抽屉布局/侧边栏修复,考勤页 CSS 引入,费用/学生页接口 schema 校验修复
This commit is contained in:
2026-08-05 21:12:50 +08:00
parent b70f45fb04
commit ab4765cee5
44 changed files with 2449 additions and 166 deletions

View File

@@ -1,4 +1,6 @@
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';
@@ -37,3 +39,50 @@ export function extractSheets(workbook: ExcelJS.Workbook): ImportSheetData[] {
}
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 文件解析失败,请检查文件格式');
}
}