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

@@ -8,6 +8,7 @@ import { IMPORT_STEP_LABELS } from './imports.types';
import type {
CellValue,
ColumnMapping,
ImportRunSettings,
ImportStepKey,
StepPreviewSummary,
} from './imports.types';
@@ -16,6 +17,7 @@ import { assertMapping, suggestMapping } from './imports.mapping';
import { buildLookups } from './imports.lookups';
import { validateRow } from './imports.rows';
import type { ImportBatchState } from './imports.rows';
import { applyPreviewPolicies } from './imports.policies';
import { findOwnedRun, findStep } from './imports.access';
import type { ImportPrincipal } from './imports.access';
@@ -55,6 +57,7 @@ export class ImportPreviewService {
const sheetsData =
parseJson<Array<{ name: string; headers: string[]; rows: CellValue[][] }>>(run.sheetsJson) ??
[];
const settings = parseJson<ImportRunSettings>(run.settingsJson) ?? {};
const sheetNames = body.sheets?.length
? body.sheets
: (parseJson<string[]>(step.sheetsJson) ?? []);
@@ -106,20 +109,22 @@ export class ImportPreviewService {
fields[field] = rawValues[sheet.headers.indexOf(header)] ?? null;
}
const result = validateRow(stepKey, fields, lookups, batchState);
const policy = applyPreviewPolicies(result, settings);
const normalized = { ...result.normalized, ...result.resolvedIds };
summary.total += 1;
if (result.errors.length > 0) {
if (policy.status === 'error') {
summary.error += 1;
} else {
summary.valid += 1;
if (result.action === 'create') summary.create += 1;
if (result.action === 'update') summary.update += 1;
if (result.action === 'create') {
const studentId = result.resolvedIds._studentId;
if (studentId !== undefined) {
if (stepKey === 'checkins') batchState.checkinStudentIds.add(studentId);
if (stepKey === 'transfers') batchState.transferStudentIds.add(studentId);
}
if (policy.action === 'create') summary.create += 1;
if (policy.action === 'update') summary.update += 1;
if (policy.action === 'skip') summary.skip += 1;
}
if (policy.status === 'valid' && policy.action === 'create') {
const studentId = result.resolvedIds._studentId;
if (studentId !== undefined) {
if (stepKey === 'checkins') batchState.checkinStudentIds.add(studentId);
if (stepKey === 'transfers') batchState.transferStudentIds.add(studentId);
}
}
rowEntities.push(
@@ -131,9 +136,9 @@ export class ImportPreviewService {
rawJson: JSON.stringify(raw),
normalizedJson: JSON.stringify(normalized),
matchKey: result.matchKey,
action: result.action,
status: result.errors.length > 0 ? 'error' : 'valid',
errorsJson: result.errors.length > 0 ? JSON.stringify(result.errors) : null,
action: policy.action,
status: policy.status,
errorsJson: policy.errors.length > 0 ? JSON.stringify(policy.errors) : null,
targetId: result.targetId ?? null,
}),
);