批量导入功能设计(账号 + 试题 双模块)¶
目标:在现有"账号 CSV/Excel 两步导入"基础之上,补齐账号字段(角色/部门),并新增试题批量导入模块。 两个模块共享同一套文件解析 / 行校验 / 批次生命周期 / 结果反馈机制(需求 8),各自保留独立字段规则。 本文档先评审、后编码。现状以
accounts.py/qbank.py/sitedb.py为准。
0. 现状与差距¶
| 能力 | 现状 | 差距(本次补齐) |
|---|---|---|
| 账号 CSV/Excel 解析 | 有(_read_grid 自动编码/分隔符/xlsx) |
复用,抽为共享层 |
| 账号两步导入(预览→确认) | 有(/api/import/preview|commit|cancel + import_batches) |
保留,接入共享层 |
| 账号字段 | 姓名/用户名/手机/邮箱/初始密码/状态/备注 | 缺 角色、部门 两列(需求 2) |
| 密码规则 | 批量 6-20 位无空格 | 维持现状(已拍板,不收紧,与单条新增规则的差异留待以后统一) |
| 失败明细导出 | /batches/{bid}/fails 只输出占位说明(错误行未持久化) |
preview 错误行持久化进批次 raw,导出真实行级明细(需求 5) |
| 题目字段落点 | qb_questions 无 难度/分值 列;知识点=标签(tags) |
加 difficulty、points 列;知识点并入 tags(需求 3) |
| 试题批量导入 | 无 | 新增整模块(5 种题型全支持,material 以父子行块表达) |
| 权限 | 管理接口无鉴权(局域网信任模型) | 本轮暂缓:设计保留于 §6,待后续单独叠加 |
| 批量写库 | 逐行独立 commit(每行一个事务) |
单批次单事务 + executemany,行级失败记录原因(需求 7) |
1. 总体设计¶
┌─────────────────────────────┐
上传文件 ───▶ │ importkit(共享校验/导入框架) │
(csv/xlsx) │ · 解析网格(编码/分隔符/xlsx) │
│ · 表头归一(别名表) │
│ · 行校验管线 → 收集错误/冲突 │
│ · 批次存根(import_batches) │
│ · commit:单事务批量写库 │
│ · 结果/失败明细/模板 CSV 导出 │
└──────┬──────────┬────────────┘
│ │
USERS_SCHEMA QS_SCHEMA
(账号字段+规则) (试题字段+规则)
│ │
accounts.py qbank.py
/api/import/* /api/qbank/import/*
- 共享框架
importkit.py:与业务无关,只定义"文件→网格→表头→行记录→按字段定义逐行校验→统计→批次持久化→导入执行"的管线,字段规则以参数(schema 对象)注入。 - 模块自有规则:账号 schema 校验用户名格式/密码复杂度/撞库冲突;试题 schema 校验题型/答案格式/选项完整性/难度枚举。二者各自实现"与数据库交互"的校验闭包,注入框架。
- 两模块共用同一张
import_batches表(新增kind列区分),但路由彼此独立,互不耦合。
1.1 共享数据结构¶
# schema 描述(模块注入)
Field = dict(
key="username", title="用户名", aliases=("账号","学号"), # 表头别名
required=False, # 是否本行必填(部分依赖题型等条件用可调用)
max_len=…,
pattern=…, err=…, # 正则/格式校验
cn_map={"正常":"normal", …}, # 中文值归一
apply_fn=callable, # 复杂校验+转换(库查重/答案归一等,返回 (值, 错误))
)
# 每条数据行
Row = dict(row_no=2, cells=[…原始列…], data={field_key: 值}, errors=[错误文案])
1.2 批次生命周期(两模块一致)¶
上传解析(preview) ─▶ pending批次(仅存 raw JSON,不碰业务表) ─▶ commit ─▶ done
│ 原始行 + 校验结果(ok/conflict/err) │ 单事务写库 + 更新计数
└─ cancel ─▶ cancelled └─ 失败行原因与整行内容持久化
- 预览阶段只读不写业务表,大文件内存友好:错误行/冲突行整行持久化到
raw,保证事后可导出失败明细; total_rows / ok_rows / fail_rows计数随 commit 刷新,与现有导入历史 UI 兼容。
2. 数据模型变更(幂等)¶
-- users:启用角色值域,新增部门列
ensure_col(users, "department", "department TEXT DEFAULT ''"); -- 部门/院系(导入列)
ensure_col(users, "manage_depts", "manage_depts TEXT DEFAULT '[]'");
-- ^ teacher 角色可导入部门白名单 JSON,[] 表示不限;仅 teacher 使用
-- role 列已存在(DEFAULT 'student'):本次启用值域 student/teacher/admin
-- import_batches:区分导入对象
ensure_col(import_batches, "kind", "kind TEXT NOT NULL DEFAULT 'users'"); -- users | questions
-- qb_questions:难度/分值
ensure_col(qb_questions, "difficulty", "difficulty TEXT NOT NULL DEFAULT 'medium'"); -- easy|medium|hard
ensure_col(qb_questions, "points", "points REAL NOT NULL DEFAULT 10");
3. 账号批量导入规格(增强)¶
3.1 模板列(v2)¶
姓名*, 用户名, 手机号, 邮箱, 初始密码, 账号状态, 角色, 部门, 备注
| 列 | 必填 | 校验 / 规则 |
|---|---|---|
| 姓名 | 是 | 2-30 字符 |
| 用户名 | 条件 | 至少填 用户名/邮箱 之一;格式 ^[A-Za-z][A-Za-z0-9_]{2,19}$;文件内重复→错误;库内重复→冲突行(可勾选覆盖更新) |
| 手机号 | 否 | ^1\d{10}$;文件内/库内撞→冲突 |
| 邮箱 | 条件 | 格式校验;撞→冲突 |
| 初始密码 | 否 | 填:6-20 位且不含空格(维持现状,与历史导入模板兼容);空:系统生成随机 8 位 |
| 账号状态 | 否 | 归一:正常/启用/active→normal;停用/禁用→disabled;默认 normal |
| 角色 | 否 | 归一:学生/student→student(默认);教师/teacher→teacher;管理员/admin→admin |
| 部门 | 否 | 长度≤50;在操作者可导入范围内(§6) |
| 备注 | 否 | 自由文本(班级等),与“部门”解耦 |
表头别名沿用现有 HEAD_ALIAS 并扩展 role/department;旧模板(无角色/部门列)继续可用。
3.2 用户名唯一性¶
- 文件内重复:记为错误行(不导入);
- 与库冲突:进入“冲突”桶——默认跳过,勾选“覆盖更新”后刷新资料/状态、不改原密码(现有行为保留);
- 覆盖更新同样校验:不降级 admin 已有账号、不把已有账号改成与导入行 scope 冲突的部门(§6)。
3.3 事务与回滚策略¶
账号行彼此独立:单批次 commit = 单个 SQLite 事务(BEGIN…COMMIT + executemany)。
- 规则校验失败的行在事务前剔除,按行记原因;
- 入库阶段的偶发失败(主键竞态等)捕获后计入失败明细、不中断事务中其余行;
- 因此已成功行保留、失败行有明细可下载修正后重导——与现有 UI 文案“新增/更新/跳过”一致。不做整批回滚(覆盖更新场景整批回滚代价高且无益)。
4. 试题批量导入规格(新增)¶
4.1 模板列¶
题型*, 题干*, 选项A, 选项B, 选项C, 选项D, 选项E, 选项F, 答案*, 难度, 分值, 知识点, 解析
示例:
题型,题干,选项A,选项B,选项C,选项D,答案,难度,分值,知识点,解析
单选题,2+2=?,2,3,4,5,B,易,5,四则运算;加法,2+2=4
多选题,以下哪些是编程语言?,Python,C++,HTML,英语,ACD,中,10,计算机基础,
判断题,地球是圆的?,,,,,对,易,10,常识,常识
编程题,用 Python 求两数之和,,,,,print(a+b),难,20,编程;算法,
材料题,阅读材料:植物生长需要光照与水分。,A,B,C,,,,,10,生物,
,,,,(1)以下哪项是光合作用必要条件?,光照,水分,土壤,空气,A,,2,,光合作用产物为有机物
,,,,(2)判断:没有光植物也能长期存活。,,,,,错,,2,,
4.2 支持题型(合法性校验 → 与题库 TYPES 一致,5 型全支持)¶
| 题型列值 | 选项/答案 | 说明 |
|---|---|---|
| 单选题/single | 选项≥2 且≤6;答案必须命中选项字母 | |
| 多选题/multi | 同上;答案如 ACD 或 A,C,D,需≥1 个且 ⊆ 选项集 |
|
| 判断题/judge | 选项留空;答案归一:对/正确/√/T→T,错/错误/×/F→F |
|
| 编程题/code | 选项留空;答案填代码文本 | 落库 {"lang":"","text":…},语言后续可在出题页补充 |
| 材料题/material | 自身只填题干;紧跟其后的若干“题型留空”行为子题 | 见下方父子行规则 |
父子行规则(material):
1. 题型=材料题 的行开启一个大题块,该行题干即材料正文;
2. 其后紧邻的题型列留空的行自动归为该大题的子题(可多行),直到出现下一个“题型非空”的行结束本块;
3. 子题行类型按该行形态自动推断:有选项且答案为单字母→single;有选项且答案为多字母组合→multi;无选项且答案为 对/错→judge;无选项且答案为其它文本→code;
4. 约束:大题块至少 1 个子题;子题行不能以空题型出现在非大题之后(无归属→错误“题型不能为空”);不支持大题嵌套大题;
5. 材料题自身不入卷,分值以子题计(同现有组卷/计分逻辑);难度/知识点/解析 对材料题与子题均可填。
4.3 逐字段校验¶
| 字段 | 必填 | 规则 |
|---|---|---|
| 题型 | 是 | 合法值见 §4.2(支持中文/英文别名);未在集合内→“题型不合法”;非大题上下文留空→错误 |
| 题干 | 是 | 去首尾空白后非空,长度≤1000 |
| 选项 | 条件 | single/multi ≥2 且≤6;选项内容去重;judge/code 必须留空 |
| 答案 | 是 | 按题型归一与校验(§4.2);single/multi 答案键必须落在选项键集合内 |
| 难度 | 否 | 归一:易/简单/easy/1→easy、中/普通/medium/2→medium(默认)、难/困难/hard/3→hard;非法→错误行 |
| 分值 | 否 | 数字 >0 且≤100(允许小数,如 0.5);默认 10 |
| 知识点 | 否 | 以 , 、 ; ; / 分隔拆分,自动建立/关联 qb_tags(知识点并入标签体系,与现有“标签”列合并去重) |
| 解析 | 否 | 纯文本落库(渲染端兼容 HTML) |
题干、选项、代码答案均按纯文本入库(富文本/插图请走出题页)。
4.4 试题行查重(防呆,非阻断)¶
文件内“题干完全相同”的行:提示但不拦截,全部导入(题库允许多道相似题;如需严格去重可在预览区人工核对)。库内已存在相同题干:不校验(题库本来就允许多条同题干)。
4.5 默认分值贯通(附带小改)¶
qb_questions.points 落库后,组卷把题目加入试卷时若未单独设置分值,使用题目默认分值替代固定 10(qb_paper_items.points 仍允许按卷覆盖;旧题 points=10 行为不变)。
5. 结果反馈规格(需求 5)¶
5.1 preview 响应(两模块同构)¶
{
"batch_id": 12, "kind": "questions", "filename": "q.csv",
"total_rows": 50, "ok_rows": 45, "conflict_rows": 0, "err_rows": 5,
"cols": ["行号","题型",…], // 展示用表头
"preview": [ {row_no, cells}…], // 最多 20 行
"conflicts": [ {row_no, why, cells}…],// 仅账号模块会有
"errors": [ {row_no, why}…] // 仅前 N 条,完整明细已入批次
}
5.2 commit 响应¶
{ "ok": true, "insert": 45, "update": 0, "skip": 5,
"fails": [ {row_no, why}…], // 入库阶段失败(通常为空)
"pwd_csv": "…" } // 仅账号模块:新增账号初始密码清单
5.3 成功/失败条数¶
- 预览与历史列表实时可见(现有
pill绿/橙/红 样式复用); import_batches.ok_rows/fail_rows即“成功/失败条数”权威来源。
5.4 失败原因明细导出(修复现有缺口)¶
GET /api/import/batches/{bid}/fails(账号)与/api/qbank/import/batches/{bid}/fails(试题): 输出真实 CSV:行号, 原始单元格…, 原因,含被拒行的整行内容,便于修正后重导;- 账号模块既有“初始密码清单”下载路径不变。
6. 权限与范围控制(需求 6)¶
状态:本轮暂缓实现(已拍板)。 设计保留备档;实现阶段两模块接口维持现状(无鉴权),后续叠加时按 6.2 执行,不影响本轮导入功能本身。
6.1 现状约束¶
全部管理接口(/api/users、/api/import/*、/api/qbank/* 等)与 /admin* 页面当前无鉴权(局域网信任模型,历史设计文档 Q1 遗留)。本项目已有完整账号会话(users.session_token + qt Cookie)。
6.2 落地方案(批量写接口收权限)¶
为不破坏“主持人现场开赛无需登录”的现有工作流,本次只给批量导入这类敏感批量写操作收权限,不做全站强制登录改造。
- 角色准入:
users.role ∈ {admin, teacher}才允许执行批量导入(含 preview / commit / 模板下载 / 历史导出)。 - 新增
accounts.require_staff(request) → user:无会话→401;role 不足→403,前端弹引导到登录页。 - 登录来源:沿用现有
POST /api/auth/login;管理员/教师账号在【账号管理】里创建(角色=教师/管理员,同本次账号导入可建)。 - 部门/角色范围限制:
- 操作者
teacher受manage_depts白名单约束:被导入行的“部门”不在白名单 → 该行按错误行处理(原因“超出你的可导入范围:仅限 {白名单}”); teacher不可导入admin角色行(角色越权→错误行);admin不受部门/角色限制;- 白名单为空
[]表示不限部门(教师默认值,避免未配置时误伤)。 - 兼容开关:
cfg("imp_require_staff"),默认 on;现场若必须免登录使用,可置off回到现状(记录于文档,不做 UI 开关)。
说明:若需求方希望“整个管理端必须登录”,属于更大范围加固,可在本方案评审后追加独立任务(涉及
/admin*页面鉴权与登录跳转),本设计不展开。
7. API 契约¶
7.1 账号模块(路径不变,向后兼容)¶
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/import/preview |
上传解析+预览(multipart file) |
| POST | /api/import/commit |
{batch_id, overwrite?} 确认导入 |
| POST | /api/import/cancel |
{batch_id} 取消 |
| GET | /api/import/batches?kind=users |
历史(前端改传 kind;默认 users,向后兼容) |
| GET | /api/import/template |
v2 模板(增角色/部门列) |
| GET | /api/import/batches/{bid}/pwd |
初始密码清单(不变) |
| GET | /api/import/batches/{bid}/fails |
升级为真实行级失败明细 |
| POST | /api/auth/login |
登录(不变,作为 staff 会话来源) |
7.2 试题模块(新路由,同构)¶
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/qbank/import/preview |
上传解析+预览 |
| POST | /api/qbank/import/commit |
{batch_id} 确认导入 |
| POST | /api/qbank/import/cancel |
{batch_id} |
| GET | /api/qbank/import/batches?kind=questions |
试题导入历史 |
| GET | /api/qbank/import/template |
模板下载 |
| GET | /api/qbank/import/batches/{bid}/fails |
失败明细 CSV |
两模块 commit 均要求 batch_id 的 kind 与路由匹配(跨 kind 提交→400)。
7.3 权限中间件¶
preview / commit / cancel / template / fails / pwd / batches 全部过 require_staff(受 §6.4 开关控制)。
8. 前端交互¶
8.1 账号页(admin_users.html / admin_users.js)¶
- 导入区样式与交互不变,仅历史接口加
kind=users、模板换 v2; - 用户列表表头与“新增账号”弹窗同步支持 角色/部门(新增下拉/输入,列表加列展示
pill); - commit 后仍展示“新增/更新/跳过 + 下载密码清单 + 失败明细下载”。
8.2 题库页(qbank.html / qbank.js)¶
在【题目管理】工具栏增加「批量导入题目」按钮 → 弹层(复用现有 .m-mask/.m-box 弹窗 CSS 与 qbank 调色):
1. 下载模板 / 选择文件(CSV、xlsx);
2. 解析预览:统计 pill + 表格(前 20 行)+ 错误清单(行号+原因,逐条,可滚动);
3. 确认导入 → 结果:新增 N / 跳过 M + 下载失败明细;
4. 导入历史(该 kind)小列表:批次、行数、成功/失败、状态、失败明细下载、模板再次下载。
8.3 交互约定¶
- 上传后若校验全失败,确认按钮禁用;错误清单含中文“第 X 行:原因”;
- 题库导入无 conflict/覆盖概念,交互比账号导入更简(无“覆盖更新”复选框)。
9. 性能与事务(需求 7)¶
| 项 | 设计 |
|---|---|
| 解析 | preview 只解析校验,业务表零写入;import_batches.raw 存整行,历史/明细由服务端生成 CSV,避免前端拉大 JSON |
| 预览上限 | 响应只回传前 20 行预览 + 计数(保持现有行为) |
| 提交 | 单批次单事务:BEGIN → 业务行 executemany(一次性预编译)→ COMMIT;规则失败行不入库 |
| 分片 | 每 500 行内建子检查点(进度可续);SQLite 万行级 executemany 提交通常 <1s |
| 锁 | 单事务窗口尽量短,避免与正在进行的答题场次写并发冲突;冲突时走 SQLite 忙碌重试(timeout=10 已配置) |
| 失败策略 | 整批事务内逐行 try:行级异常捕获记录(row_no+原因),不回滚其余成功行;异常原因与整行内容持久化 |
| 并发 | 同一 kind 同时只允许 1 个 pending 批次?(现有结构允许多个,保持现状:commit 按 batch_id 幂等) |
10. 实现拆分与验证¶
| 阶段 | 内容 | 验证 |
|---|---|---|
| P0 基础设施 | importkit.py 抽取 + import_batches.kind/users.department/qb_questions 加列 |
现有账号导入冒烟回归 |
| P1 账号增强 | role/department 列、密码规则统一、错误行持久化、失败明细真实导出、require_staff | 账号导入冒烟扩充(角色/部门/越权用例) |
| P2 试题导入 | QS_SCHEMA + /api/qbank/import/* + 批量写库 + 组卷默认分贯通 |
新写题库导入冒烟(合法/非法行样例、xlsx、失败明细) |
| P3 前端 | 题库页导入弹层、账号页角色/部门展示与新增表单、两页历史 kind 过滤 | 手工走查 + 截图核对 |
| P4 权限联调 | staff 登录态、teacher 部门/角色越权拦截、cfg 开关 | 暂缓(见 §6 状态标注) |
11. 拍板结论(2026-09 评审)¶
- 权限档位:本轮不做权限(§6 设计保留,后续单独叠加)。
- 题型范围:5 型全支持,material 以父子行块表达(§4.2);code 选项留空、答案填代码。
- 题干完全重复:提示不阻断(§4.4)。
- 密码规则:维持 6-20 位(现状,不收紧;与单条新增的差异留待后续统一)。
- 实施范围:P0-P4 全量(P4 权限除外),含前端与回归冒烟。