feat(imports): 成绩/名单表格导入 + ClassIsland 时间配置导入 + 安装版增量迁移

服务端:
- 新增 imports 模块: 模板下载(csv/xlsx)、表格解析预览、字段映射导入(append/upsert)、
  错误行号与结果统计; ClassIsland 时间配置容错解析 + replace/merge 导入 + 失败回滚(400 不写库)
- TimeLayout 模型 + 迁移; 修复 contentBase64 空值被 422 拦截, 改由 service 返回 IMPORT_EMPTY_FILE
- merge 语义改为按开始时间槽位覆盖, 同开始时间的旧节次不再被静默丢弃
- db-bootstrap 增加 _ch_migrations 账本: 覆盖安装升级也能自动补跑新增迁移(旧库实测通过)

Web 管理端:
- TableImportDialog(成绩/名单): 模板下载、预览、字段映射、重复处理模式、结果统计与错误行
- TimeLayoutImportDialog: 粘贴/选 .json、解析预览、覆盖/合并、已导入列表与删除
- 成绩录入/学生管理/课表管理接入入口(按权限矩阵显示)

验证: verify:e2e 107/107(新增导入 22 项) verify:web 18/18(新增时间配置导入弹窗真实点击)
      verify:desktop 40/40
文档: README 新增导入章节、增量迁移说明、API 表与验收对照更新
This commit is contained in:
LoveChengke committed 2026-09-12 15:35:01 +08:00
1 parent 0dcab6f369
commit 8b85079d81
23 files changed
+2735 -108

No files matched your search

+2
View File
@@ -21,6 +21,8 @@ export const API_PATHS = {
grades: '/grades',
students: '/students',
teachers: '/teachers',
/** 导入:模板下载 / 表格导入 / ClassIsland 课表时间配置 */
imports: '/imports',
dashboard: '/dashboard/summary',
} as const;
+71
View File
@@ -458,6 +458,77 @@ export interface ServerToClientEvents {
connected: (payload: { userId: string; role: UserRole; rooms: string[] }) => void;
}
/* ------------------------------------------------------------------ 导入(模板 / 表格 / 课表时间配置) */
/** 表格导入预览:列名、前若干行、建议字段映射与校验问题 */
export interface TableImportPreview {
kind: 'grades' | 'students';
columns: string[];
rows: string[][];
totalRows: number;
suggestedMapping: Record<string, string>;
errors: string[];
warnings: string[];
templateCsv: string;
}
/** 导入时某一行的具体错误(row 为 Excel 视角的行号,含表头) */
export interface TableImportRowError {
row: number;
message: string;
}
/** 导入结果统计 */
export interface TableImportResult {
kind: 'grades' | 'students';
total: number;
inserted: number;
updated: number;
skipped: number;
failed: number;
errors: TableImportRowError[];
warnings: string[];
}
/** ClassIsland 时间配置条目 */
export interface TimeLayoutEntry {
index: number;
name: string;
startTime: string;
endTime: string;
type: 'class' | 'break' | 'divider';
skipped: boolean;
}
export interface TimeLayoutDto {
id: string;
classId: string;
name: string;
source: string;
items: TimeLayoutEntry[];
createdAt: string;
updatedAt: string;
}
/** ClassIsland JSON 解析结果(预览用:只解析不落库) */
export interface TimeLayoutParsePreview {
items: TimeLayoutEntry[];
errors: string[];
warnings: string[];
shape: string;
}
/** 课表时间配置导入结果 */
export interface TimeLayoutImportResult {
layout: TimeLayoutDto;
mode: 'replace' | 'merge';
warnings: string[];
/** merge 时:被此次导入覆盖的旧节次数 */
merged: number;
/** merge 时:新增节次数;replace 时为总节次数 */
replaced: number;
}
/** 客户端 -> 服务端事件名 */
export interface ClientToServerEvents {
'class:join': (classId: string, ack?: (result: { ok: boolean; message?: string }) => void) => void;