跳转至

批量导入功能设计(账号 + 试题 双模块)

目标:在现有"账号 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) difficultypoints 列;知识点并入 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 同上;答案如 ACDA,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 落地方案(批量写接口收权限)

为不破坏“主持人现场开赛无需登录”的现有工作流,本次只给批量导入这类敏感批量写操作收权限,不做全站强制登录改造。

  1. 角色准入users.role ∈ {admin, teacher} 才允许执行批量导入(含 preview / commit / 模板下载 / 历史导出)。
  2. 新增 accounts.require_staff(request) → user:无会话→401;role 不足→403,前端弹引导到登录页。
  3. 登录来源:沿用现有 POST /api/auth/login;管理员/教师账号在【账号管理】里创建(角色=教师/管理员,同本次账号导入可建)。
  4. 部门/角色范围限制
  5. 操作者 teachermanage_depts 白名单约束:被导入行的“部门”不在白名单 → 该行按错误行处理(原因“超出你的可导入范围:仅限 {白名单}”);
  6. teacher 不可导入 admin 角色行(角色越权→错误行);
  7. admin 不受部门/角色限制;
  8. 白名单为空 [] 表示不限部门(教师默认值,避免未配置时误伤)。
  9. 兼容开关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_idkind 与路由匹配(跨 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 评审)

  1. 权限档位:本轮不做权限(§6 设计保留,后续单独叠加)。
  2. 题型范围5 型全支持,material 以父子行块表达(§4.2);code 选项留空、答案填代码。
  3. 题干完全重复:提示不阻断(§4.4)。
  4. 密码规则维持 6-20 位(现状,不收紧;与单条新增的差异留待后续统一)。
  5. 实施范围:P0-P4 全量(P4 权限除外),含前端与回归冒烟。