跳转至

重大变更

1. 高层摘要 (TL;DR)

  • 影响等级: 🔴 高 (High) — 改动横跨 Python 后端 / 浏览器 JS / 数据库 Schema / 前端模板 / 打包系统,涉及全新子模块引入、计分模型重构、全栈性能优化。
  • 关键变更:
    • 全新 Excel/WPS 操作题判分引擎 — 浏览器内执行 EG 引擎(ExcelJS + HyperFormula + JSZip + SheetJS,全本地无 CDN),后端只存文件与分数
    • 📊 计分模型重构 — 从「按步骤累计」改为「按合并小题计分」(全对才满分,任一错即 0)
    • SQLite + 静态资源 + GZip 多重性能优化 — WAL、mmap、线程局部连接、缓存压缩、版本号击穿
    • 🔀 题库导入/导出 + 多库合并工具/api/qbank/export_pack/api/qbank/import_packdb_merge_gui.py
    • 🚀 自动升级系统exepack/updatecore.py 实现检查→下载→MD5 校验→替换重启全链路
    • 📝 题项支持 ≥4 选项(最多 15 个 A–O)+ 选项随机打乱(防邻座互抄)

2. 可视化总览(架构与数据流)

graph TD
    subgraph 客户端["前端 (浏览器)"]
        STUDENT["学生端<br/>static/index.js<br/>static/exam.js"]
        TEACHER["教师/场控端<br/>static/admin.js<br/>static/admin_update.js"]
        EXCELJS["Excel/WPS 引擎<br/>static/excel-wps.js<br/>excelgrade/web/js/*"]
    end

    subgraph 后端["后端 (FastAPI)"]
        MAIN["main.py<br/>路由/WS/管理面中间件"]
        QBANK["qbank.py<br/>题库 CRUD"]
        EXCELAPI["excelgrade/api.py<br/>Excel/WPS 接口"]
        RECORDS["records.py<br/>整卷/统计/报告"]
        DBCORE["dbcore.py<br/>SQLite/MySQL 抽象 + WAL"]
        UPDATE["exepack/updatecore.py<br/>自动升级"]
        ACCOUNTS["accounts.py<br/>账号/解锁"]
    end

    subgraph 数据层
        DB[("quiz.db<br/>SQLite WAL<br/>+索引")]
        EXCEL_DIR["uploads/excel/<br/>题目三件套+作答文件"]
    end

    subgraph 性能优化层
        CACHED["CachedStaticFiles<br/>GZip + 版本号缓存击穿"]
        GZIP["GZipMiddleware<br/>全站文本压缩"]
 end

    STUDENT -->|"答题/提交"| MAIN
    TEACHER -->|"出题/控制"| MAIN
    EXCELJS -->|"上传 subs分数"| EXCELAPI
    MAIN --> QBANK
    MAIN --> RECORDS
    MAIN --> EXCELAPI
    MAIN --> UPDATE
    QBANK --> DBCORE
    RECORDS --> DBCORE
    EXCELAPI --> QBANK
    EXCELAPI --> DBCORE
    UPDATE --> DBCORE
    EXCELAPI --> EXCEL_DIR
    MAIN -.->|"static/*"| CACHED
    MAIN -.->|"HTML/JSON"| GZIP

    style EXCELJS fill:#fff3e0,color:#e65100
    style EXCELAPI fill:#fff3e0,color:#e65100
    style CACHED fill:#c8e6c9,color:#1a5e20
    style GZIP fill:#c8e6c9,color:#1a5e20
    style UPDATE fill:#f3e5f5,color:#7b1fa2
    style MAIN fill:#bbdefb,color:#0d47a1
    style DBCORE fill:#c8e6c9,color:#1a5e20

Excel/WPS 操作题提交流程

sequenceDiagram
    participant S as 学生
    participant JS as excel-wps.js
    participant EG as EG引擎(浏览器)
    participant API as /api/excel
    participant DB as 数据库

    S->>JS: 选择作答文件(.xlsx)
    S->>JS: 点"提交"
    JS->>JS: 二次确认(含文件名)
    JS->>EG: ensureEngine() 单例懒加载
    EG-->>JS: libs + core 就绪
    JS->>EG: gradeFile(file)
    EG->>EG: reader.readWorkbook
    EG->>EG: recalc (HyperFormula)
    EG->>EG: grouper.viewByQuestion<br/>按 qgroups 合并为 subs
    EG-->>JS: {score, score_max, subs}
    JS->>API: POST /api/excel/exam/{sid}/answer
    API->>DB: INSERT excel_submission<br/>(含 subs JSON)
    DB-->>API: ok
    API-->>JS: 回执

3. 详细变更分析

🚀 模块 A:Excel/WPS 操作题判分引擎(全新)

核心架构:判分在浏览器(学生端 EG 引擎 + 教师上传的 judge.js),后端只存文件与分数。

维度 内容
新增文件 excelgrade/api.py(815行)、excelgrade/grader.pyexcelgrade/extractor.pyexcelgrade/grouper.pyexcelgrade/reader.pyexcelgrade/store.pyexcelgrade/compare.pyexcelgrade/model.pyexcelgrade/config.pyexcelgrade/__init__.pyexcelgrade/cli.pyexcelgrade/selftest.pyexcelgrade/batch.pyexcelgrade/report.pyexcelgrade/report_html.pyexcelgrade/pairing.pyexcelgrade/web/*(20+ 文件)、static/excel-wps.js(490行)、db_merge_core.pydb_merge_gui.py
数据库 Schema 变更 qb_questions / questions 新增 template_file_urljson_standard_urljudge_js_urloptions(JSON 数组)
计分口径变更 🔥 旧 detail[](逐步骤) → 新 subs[](逐小题);全对才给满分(任一错即 0)
关键模型 EG.grouper.viewByQuestion(result, qgroups) 按 qgroups 编排把上百条规则聚合成 N 个「小题」
文件存储 uploads/excel/ 下分 q/(题目三件套)、tmp/(草稿)、answers/(场次作答)、live/(实时作答)

新增 API 端点

方法 路径 用途
GET /api/excel/meta/{qid} 编辑弹窗回填(文件元数据)
POST /api/excel/q/{qid}/files 上传 template/standard/judge(已有题目)
POST /api/excel/tmp/{key}/files 上传草稿文件(新建题目时无 id)
POST /api/excel/q/{qid}/attach 草稿迁移到正式题目目录并写库字段
DELETE /api/excel/q/{qid}/files/{kind} 移除某个判分文件
GET /api/excel/res/{qid}/{kind} 资源下载(鉴权)
GET /api/excel/res/tmp/{key}/{kind} 草稿资源读取(教职工预览)
POST /api/excel/exam/{sid}/answer 整卷 Excel 作答(前端已判分,回传分数)
POST /api/excel/live/{code}/{qid}/answer 实时场 Excel 作答

数据库表新增

表名 字段 用途
excel_submission id/session_id/user_id/qn_id/qb_id/file_path/file_name/score/score_max/passed/failed/unknown/total_rules/detail/subs/created_at/updated_at 整卷/练习:场次作答留痕(UNIQUE(session,user,qn) 重传覆盖)
excel_live_sub id/room_code/player_id/qid/qb_src/.../score_added 实时答题:按房间+选手+实时题号独立表
attempt_items excel_sub_id 新增列,关联场次作答到文件留痕

⚡ 模块 B:性能优化(P0/P1 — 2026-09-15/16)

子项 来源文件 优化手段 收益
P0-1 SQLite WAL dbcore.py journal_mode=WAL / synchronous=NORMAL / cache_size=-4000 / mmap_size=64MB / temp_store=MEMORY / busy_timeout=30s 机房 Win7/机械盘并发解锁,写入延迟降;不再 database is locked
P0-4 线程局部连接 dbcore.py 每线程复用 SQLite 连接(带 PRAGMA 缓存),_Conn.close() 自动清缓存 实测每次查询 ~0.93ms 建连 → ~0.001ms 复用(6 条 PRAGMA 占 83%)
P1-1 静态资源缓存 main.py CachedStaticFiles 继承 Starlette:max-age=86400 + GZip(≥1KB) wangEditor/qbank.js 等大文件传输量降 ~70%
P1-2 全站 GZip main.py GZipMiddleware(minimum_size=1024, compresslevel=6);已带 content-encoding 跳过避免重复 HTML/JSON 文本类响应压缩
P1-3 资源版本号 main.py _static_ver()static/ 目录 mtime;模板 {{ sv('/x.js') }}?v=…;JS 也能读 window.STATIC_VER 改文件重启后 URL 变 → 旧缓存自动失效
数据库索引补齐 sitedb.py _perf_indexes() 添加6 个高频索引:users(session_token) / sessions(school_id,id) / qb_questions(parent_id) / qb_questions(school_id,visibility,id) / attempt_items(session_id,qn_id) / session_users(session_id,score) 千人库登录态解析 0.53ms → 走索引查找
列裁剪 sitedb.py get_user_by_token()SELECT * 改为 11 列 19 列 → 11 列,每 /api 请求省回传
N+1 查询消除 records.py user_sessions() 原每行一次 SELECT quiz_plan 改为一次 IN 批量;plan_from_paper() 子题从「逐材料一次查询」改为一次 IN 批量 用户场次列表页性能提升显著
WAL Checkpoint dbcore.py 新增 checkpoint():合并 -wal 回主库 防止 db_merge 时漏数据;服务退出前自动调用
心跳键防膨胀 main.py _hb_cache 超 2000 时清理 5 分钟未心跳的旧键 长期运行/换人时无界增长问题修复
前端轮询降频 static/index.js WS 离线兜底轮询:800ms → 5000ms(请求量降 ~6 倍);vizInterval() 后台标签页暂停轮询 多班级大并发明显

关键 PRAGMA 设置

PRAGMA journal_mode=WAL              -- 读写不互锁
PRAGMA synchronous=NORMAL            -- 单条提交不 fsync
PRAGMA temp_store=MEMORY             -- GROUP BY/ORDER BY 不落盘
PRAGMA cache_size=-4000              -- 4MB 缓存
PRAGMA mmap_size=67108864            -- 64MB 内存映射
PRAGMA busy_timeout=30000            -- 30s 锁等待

🚀 模块 C:自动升级系统(全新 — 2026-09-16)

维度 内容
新增文件 exepack/updatecore.py(280行)、static/admin_update.js(80行)
核心流程 启动后台检查 → 通知前端 → 老师点「立即升级」 → 下载(带进度)→ MD5 校验 → 生成 bat → 退出进程 → bat 覆盖 exe →拉起新版 → 自删
更新源优先级 exe 同目录 更新地址.txt > 内置常量 https://img.sakaay.com/p/img/quiz/latest.json
安全设计 MD5 校验失败自动删下载文件;进程退出前 dbcore.checkpoint() 合并 WAL;bat 失败留 update_fail.txt 痕迹
打包产物 exepack/build.py 新增 publish_update():复制产物到 dist_update/ 并合并 latest.json(win7/win10 两条目分别打包累积),文件名用纯 ASCII 避免图床改名
管理员界面 顶部悬浮条:「发现新版本 vX.X.X(当前 vY.Y.Y)」→「立即升级」→「下载中 XX%」→「新版本已下载完成」→「重启应用」

自动升级 API

方法 路径 权限 用途
GET /api/update/check 教职工 主动触发检查,返回 {has_update, latest, cur, notes, size, packaged}
POST /api/update/start 教职工 开始下载(后台线程 +进度状态)
GET /api/update/status 教职工 轮询下载进度(每 800ms)
POST /api/update/apply 教职工 触发替换 +退出进程

📚 模块 D:题库导入/导出 + 多库合并

维度 内容
Web端 API GET /api/qbank/export_pack 下载 JSON;POST /api/qbank/import_pack?strategy=xxx 增量合并(导入前自动备份 .bak_import_时间戳
GUI 工具 db_merge_gui.py(tkinter,零三方依赖)选择目标库 + 多源库 + 策略,预览/执行
核心引擎 db_merge_core.py(242 行),GUI 与 Web 共用同一套逻辑
策略 keep_all 全部保留(重映射 id,推荐);② overwrite 源覆盖目标;③ skip 跳过冲突
表范围 qb_categories / qb_tags / qb_papers / qb_questions / qb_paper_items / qb_question_tags(不碰 users/sessions/players/questions隐私与运行期数据)
自动加列 源库有新字段(如升级后的 Excel 题 3 字段)→目标库自动 ALTER TABLE 加列
外键级联 parent_id / qid / tid / paper_id 在重映射后自动级联修正

📚 模块 E:题型与题项扩展

变更 内容
新增题型 excel_wps(Excel/WPS 操作题)加入 TYPES 元组
字段扩展 QIn 模型新增 template_file_url / json_standard_url / judge_js_url(None 表示不覆盖)
选项上限 MAX_OPTIONS 4 → 6,配合 DB options JSON 列扩展最多 15 个 A–O(_live_opts() 函数优先 options,回退 optA..optD
选项随机打乱 教师可设 shuffle_options=1,每位学生选项顺序随机但内容一致(防邻座互抄)
Python 3.8 兼容 int \| NoneOptional[int]list[dict]List[Dict](PEP 604 需 3.10+;Win7 打包要求3.8)
校验调整 type=excel_wps 时跳过传统答案校验(return {"keys": [], "ans": None}
题组成组 r["type"] in ("material", "excel_wps") 同样触发子题下发;WPS 题组保留 t=excel_wps;父题不限 material
子题独立出题 parent_id 单独出材料子题时自动附上材料正文 question["material"]
题型识别 q_type_of() 增加 excel_wps 分支,避免被「答案长度推断」误判为单选

🔒 模块 F:报告权限模型重构(2026-09-15)

变更 内容
新增 require_staff() 统计报告等教职工接口统一闸:登录 + 状态正常 + 教师/管理员角色
新增 school_scoped() 教师/学校管理员可看本校任意场次(不再按房间隔离);超管不限校;满足「本校成绩统一查阅」
room_scoped() 抽提 写操作仍保持房间级(编辑/删除/收卷),与查看类区分
_room_manageable() 房间管理判定:超管/房间创建者/本校管理员;与列表的 can_manage 同源
报告列表 ?school= 超管可 ?school=<id|code> 切校查看;普通教师恒为本校
/api/stats 纳入守卫 /api/stats 不在管理面中间件前缀内,接口内必须强制身份

操作题满分口径修正

场景
整卷组卷时操作题满分 套用组卷默认分(10 分) excelgrade.api._excel_standard_max(qid) 解析考点 JSON 取 Σ规则分(manualMax 优先)
实时场结算满分 默认 10 分/题 同上,按判分文件满分
排行榜「全对」 仅客观题可比对 操作题以「达满分」计(_excel_full()

🔒 模块 G:账号体系增强

变更 内容
新增 POST /api/users/{uid}/unlock 解除账号「登录失败临时锁定」(清 lock_until + fail_count=0),幂等;教师仅可解本校学生,校管理员可解本校教师/学生,超管不限;不能解超管
list_users 返回 locked 服务端按当前时间计算,前端列表/解锁按钮统一依据本字段,避免各处自行比较 lock_until 造成口径不一致
锁字段暴露 user_pub 等消费点需要时同步扩充

🎨 模块 H:前端 UI 改进

改动 内容
抽屉响应式自适应 templates/admin.html 把排行榜/现场动态从悬浮面板改为可拖动抽屉;CSS 容器查询(container-type: inline-size)+ clamp(...cqi...) 让内部字号/图标等比缩放;删除 @media 硬切宽度跳变
悬浮面板瘦身 主控按钮改为图标(▶ 开跑/继续、⏸ 暂停、↻ 新一轮),含义放 title悬浮提示
题目列表分布懒加载 static/admin.jsIntersectionObserver 仅对视口内题目请求分布;名单悬停时按题单取并缓存;题目数据没变化不重建列表 DOM(解决 600 题卡顿)
WebSocket + 轮询降频 WS 在线时由服务端每 2s 推送;离线时才用轮询兜底(请求量降 6 倍)
心跳离开判定 切走标签页 = 离开本页超 3 秒;已离开 = 关闭/失联超时;学生回本页自动转「在线」
admin.html 模板 引入 {{ sv('/static/style.css') }} 版本号;新增答题现场入口链接(新窗口打开)
模板公共函数 templates.env.globals["sv"] + static_ver(供 JS 动态加载资源时拼版本号)

🧹 模块 I:API 接口清理

状态 接口 原因
❌ 已删除 GET /api/portal 兼容遗留,前端只从 /api/room/me.portal
❌ 已删除 GET /api/admin/dist dist_batch + dist_names 取代
❌ 已删除 POST /api/admin/add_q 旧「手动新增单选题」,无前端入口
❌ 已删除 POST /api/admin/q_ana 旧「单独编辑解析」,无调用方

📦 模块 J:打包系统升级(Win7 兼容)

改动 内容
PyInstaller 版本策略 --target auto:Python 3.8 → win7(PyInstaller 5.13.2,最后支持 Win7/8 的 bootloader);Python 3.9+ → win10(PyInstaller 6.x)
Win7 UCRT 剔除 spec 中 strip_ucrt=True 时在 Analysis 之后剔除 ucrtbase.dllapi-ms-win-*(Win7 SP1+KB2999226 自带 UCRT)
pycryptodome 瘦身 显式过滤 Crypto/PublicKey/*Crypto/Signature/*Crypto/IO/*Crypto/Protocol/*(仅 _ec_ws.pyd 单项省0.7MB)
uvicorn 父包补全 HIDDEN 加 uvicorn.loops/uvicorn.protocols/uvicorn.lifespan(PyInstaller 5.x 报 cannot handle module 'uvicorn.loops'
低配机瘦身 EXCLUDES 增加 unittest/pydoc_data/lib2to3
自动升级模块显式收集 HIDDEN 加 exepack.updatecore(main.py 顶层 import)
版本号文件随包 version.txt 随打包发出(updatecore.read_version 从 RES_DIR 读取)
exepack 退出 checkpoint exepack/svc.py 退出前 dbcore.checkpoint() 合并 WAL,老师随后拷贝 quiz.db 不漏数据

🆕 模块 K:excel_submission 数据迁移

变更 内容
attempt_items 新列 excel_sub_id 指向本次提交的作答明细(普通题为 NULL)
subs 字段 小题结果(计分/展示口径);detail 仅排查用
重传覆盖 UNIQUE(session_id, user_id, qn_id) 约束保证同一场同一考生同一题只保留最后一次提交
结算转存 settle_live 把实时留痕 excel_live_sub 转存到场次留痕 excel_submission,并 UPDATE attempt_items.excel_sub_id
本轮清空 实时场结束后 DELETE FROM excel_live_sub WHERE room_code=?(否则同一房间下一轮再答同一题时 score_added 仍是上一轮的值)

4. 影响 & 风险评估

⚠️ Breaking Changes(破坏性变更)

变更 影响面 迁移建议
数据库 Schema 新增 4 列 所有环境 init_db() 幂等 ALTER 自动兼容,旧库无感升级
excel_submission/excel_live_sub 新表 所有环境 自动建表,无需手动
索引补齐 6 个 所有环境 自动 CREATE INDEX IF NOT EXISTS
Python 3.8 兼容性 全部 Python 代码 已统一改为 Optional[int] / List[Dict]
路径 /api/portal 删除 仅学生端前端 前端早已不调用
路径 /api/admin/dist 等删除 教师端 已被新接口替代
题型 excel_wps 进入 TYPES 题库编辑 UI 已支持

⚠️ 风险点

风险 触发条件 建议测试
WAL + 线程局部连接 打包到只读介质(精简 Windows) 启动日志观察 PRAGMA 失败是否回退
GZip 与 CachedStaticFiles 双重压缩 极端中间件链路 浏览器 F12确认 Content-Encoding: gzip 仅出现一次
Excel 引擎 2.6MB 懒加载 弱网学生首次答题 实测首次判分耗时,预热降级路径
自动升级 MD5 校验失败 CDN/图床改名 失败时留 update_fail.txt 不影响旧 exe 运行
bat 替换死锁 旧进程未释放 exe APPLY_WAIT_TRIES=120 秒重试 + 超时落盘
题库合并 id 重映射 他校题 id 与本校冲突 默认 keep_all 策略;预览后再执行
报告 school_scoped 他校管理员误看本校成绩 require_staff() + school_id 比对已加

🧪 测试建议

场景 验证点
Excel 操作题提交 整卷+实时双路径;上传 .xlsx / .xls;多次重传;非 Excel 类型拒绝
计分口径 subs 全对→满分;任一错→0;detail 仅排查不展示
SQLite 并发 8 教师机同时刷报告页,确认无 database is locked
静态资源缓存 改文件重启后 URL 变;GZip 命中 ≥1KB 文本
自动升级全链路 故意改 MD5 看失败重试;中途断网看进度;bat 替换成功后旧 exe 自删
题库合并 ① 三个源库合并,id 重映射;② 同主键字段差异;③ 源库有 Excel 题3 字段时目标自动加列
账号解锁 连续输错密码被锁 → 教师解锁;幂等;权限边界(教师不能解校管理员)
Python 3.8 打包 python exepack/build.py --target win7 产出在 win7/;UCRT 剔除
抽屉响应式 三档实测(桌面/平板/移动);拖动调节宽度后内部字号跟随

✅ 已通过的回归

  • python -m py_compile main.py 通过
  • _e2e_smoke.py 51/51 通过
  • 浏览器三档实测抽屉缩放(桌面/平板/移动)通过

5. 总结

本次变更是一次全栈级别的版本升级,核心价值:

价值 体现
判分能力扩展 新增 Excel/WPS 操作题(浏览器内 EG 引擎),突破传统客观题边界
计分精细化 从「按步骤」到「按合并小题」,贴近实考判分习惯
运维自动化 自动升级、题库合并 GUI、WAL checkpoint,机房老师几乎无感升级
性能翻倍 SQLite 并发解锁、静态资源降 70%传输量、WebSocket 推送替代高频轮询
权限精细化 报告「查看」放开为校域,「写操作」仍房间级;账号解锁不再依赖重置密码

建议重点回归: Excel 操作题提交链路 + 自动升级全链路 + SQLite 并发 + 题库合并 + 抽屉响应式。