Kaynağa Gözat

移除文档构建与评审脚本,仓库仅保留方案文档与检索证据包

19323826807 2 gün önce
ebeveyn
işleme
f2778b42ec
14 değiştirilmiş dosya ile 3 ekleme ve 1902 silme
  1. 3 0
      .gitignore
  2. 0 107
      build_doc.py
  3. 0 382
      build_optimized_plan.py
  4. 0 249
      build_ref_doc.py
  5. 0 165
      build_review_report.py
  6. 0 313
      docx_helpers.py
  7. 0 17
      dump_docx.py
  8. 0 10
      dump_heads.py
  9. 0 31
      extract_plan_sources.py
  10. 0 135
      optimized_plan_content.py
  11. 0 196
      sections_a.py
  12. 0 227
      sections_b.py
  13. 0 19
      verify_doc.py
  14. 0 51
      verify_optimized_plan.py

+ 3 - 0
.gitignore

@@ -2,6 +2,9 @@
 __pycache__/
 __pycache__/
 *.pyc
 *.pyc
 
 
+# 文档构建与评审脚本(本地保留,不入库)
+*.py
+
 # 会话临时文件(评审过程中的中间产物)
 # 会话临时文件(评审过程中的中间产物)
 _*.py
 _*.py
 _*.json
 _*.json

+ 0 - 107
build_doc.py

@@ -1,107 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Part C: appendices and entry point."""
-import sys
-from docx.shared import Pt, Cm
-from docx.enum.text import WD_ALIGN_PARAGRAPH
-from docx_helpers import (init_doc, heading, para, rich, bullet, bullet_rich,
-                          make_table, caption, note, quote, link_para, set_cn_font)
-from sections_a import cover, abstract, toc_page, chapter1, chapter2
-from sections_b import chapter3, chapter4, chapter5
-
-GH = "https://github.com/"
-
-REPOS = [
- ("mindskip/xzs-mysql", "3604", "AGPL-3.0", "2026-11-09", "学之思考试系统,Java+Vue,Web 与小程序双端"),
- ("mindskip/xzs", "3914", "AGPL-3.0", "2026-11-09", "同上,SQL Server 分支,文档更完整"),
- ("Alanosy/exam-platform", "886", "MIT", "2026-11-05", "「砚考」,Spring Boot 3.5 + Vue3,微服务形态"),
- ("lsgwr/spring-boot-online-exam", "1692", "MIT", "2026-10-10", "Spring Boot 在线考试,含在线预览"),
- ("YXJ2018/SpringBoot-Vue-OnlineExam", "2279", "无", "2025-04-03", "经典前后端分离考试系统"),
- ("shuzhao11/quiz", "393", "无", "2024-08-01", "Java 毕设在线测试系统"),
- ("cppcpp/exam-system", "87", "无", "2022-12-16", "南京晓庄学院自动组卷,遗传算法"),
- ("wulivictor/ExamOnline", "739", "无", "2025-10-09", "答题小程序,移动端答题场景"),
- ("xingxingzaixian/django-vue3.2-online-exam", "184", "MIT", "2022-12-01", "Django + Vue3 在线考试"),
- ("google/or-tools", "14348", "Apache-2.0", "2026-11-09", "CP-SAT 约束求解,组卷对照方法"),
- ("TimefoldAI/timefold-solver", "1859", "Apache-2.0", "2026-11-09", "Java 局部搜索框架,原 OptaPlanner 团队"),
- ("pubkey/rxdb", "23421", "Apache-2.0", "2026-11-09", "本地优先数据库,冲突解决策略族"),
- ("pouchdb/pouchdb", "17627", "Apache-2.0", "2026-11-05", "CouchDB 复制协议,修订号版本链"),
- ("dexie/Dexie.js", "14673", "Apache-2.0", "2026-11-09", "IndexedDB 封装,方案 B 客户端存储层"),
- ("lukas-krecan/ShedLock", "4235", "Apache-2.0", "2026-11-07", "分布式定时任务锁"),
- ("testcontainers/testcontainers-java", "8758", "MIT", "2026-11-02", "集成测试与故障注入基础设施"),
- ("vardanagarwal/Proctoring-AI", "633", "MIT", "2024-12-10", "视线、人脸、手机、多人检测"),
- ("narender-rk10/MyProctor.ai", "236", "无", "2026-10-30", "Flask + YOLOv4 全栈监考示例"),
- ("tusharnankani/Aankh", "86", "GPL-3.0", "2024-12-18", "浏览器端实时监考"),
- ("meefik/ITMOproctor", "40", "GPL-3.0", "2023-03-04", "WebRTC 人工监考(已归档)"),
- ("eLearning-BS23/moodle-quizaccess_proctoring", "30", "未声明 SPDX", "2026-08-03", "Moodle 考试过程检测插件"),
- ("aesophor/hacklympics", "69", "MPL-2.0", "2021-02-17", "竞赛场景机器侧管控原型"),
- ("SafeExamBrowser/seb-win-refactoring", "366", "MPL-2.0", "2026-11-02", "Windows 客户端锁机"),
- ("SafeExamBrowser/seb-server", "97", "MPL-2.0", "2026-11-06", "服务端会话编排与监控"),
- ("moodle/moodle", "7594", "GPL-3.0", "2026-10-30", "mod_quiz,question_attempts 状态机"),
- ("openedx/edx-platform", "8220", "AGPL-3.0", "2026-11-08", "ProctoringProvider 抽象"),
- ("oat-sa/tao-core", "64", "GPL-2.0", "2026-11-06", "基于 QTI 的测评引擎"),
- ("mawoka-myblock/ClassQuiz", "737", "MPL-2.0", "2026-11-02", "Kahoot 式实时答题,Socket.IO 广播"),
- ("tomwalker/django_quiz", "628", "无", "2024-04-23", "可配置 quiz 应用,题型与计分规则简洁"),
-]
-
-URLS = {
- "narender-rk10/MyProctor.ai": GH + "narender-rk10/MyProctor.ai-AI-BASED-SMART-ONLINE-EXAMINATION-PROCTORING-SYSYTEM",
-}
-
-def appendix_a(doc):
-    heading(doc, "附录 A 开源仓库完整清单", 1)
-    para(doc, "表 A-1 为逐仓核验过的全部仓库,按类别排列。星标数与最后提交时间均为 2026-11-09 通过 GitHub REST API 逐仓复核所得,仅在需要精确引用时使用;日常阅读可直接点击仓库名跳转。")
-    caption(doc, "表 A-1 开源仓库清单(按类别)")
-    rows = []
-    for i, (name, stars, lic, pushed, desc) in enumerate(REPOS):
-        url = URLS.get(name, GH + name)
-        rows.append([(name.split("/")[-1] if name in URLS else name, url), stars, lic, pushed, desc])
-    make_table(doc,
-        ["仓库(点击进入)", "星标", "许可证", "最后提交", "说明"],
-        rows,
-        [5.9, 1.2, 1.9, 1.9, 4.5], link_col=0, font=7.5, head_font=8.0)
-    note(doc, "说明:星标数为仓库累计星标,非活跃度指标;「最后提交」取默认分支最近一次 push 时间。许可证一栏的「无」表示仓库未声明许可证,法律状态不明。")
-
-def appendix_b(doc):
-    heading(doc, "附录 B 检索记录与方法说明", 1)
-    heading(doc, "B.1 检索执行说明", 2)
-    para(doc, "本次调研以 GitHub 官方 REST API 为主要数据来源,辅以仓库 README 原文核对功能描述。执行中有三点需要说明,供复现参考。")
-    bullet_rich(doc, [("检索语法的选择。", True), ("中文关键词(如「在线考试」)在 GitHub 搜索中会被分词并命中大量无关仓库,因此改用英文名称匹配(online exam in:name)与 topic 标签(topic:online-exam)两类精确语法。", False)])
-    bullet_rich(doc, [("数据获取方式。", True), ("GitHub 搜索接口对匿名请求限速 10 次/分钟,且响应长度存在上限。本文档改为逐仓调用仓库详情接口,以固定间隔串行查询,从响应中提取仓库全名、星标数、语言、最后提交时间与许可证字段。", False)])
-    bullet_rich(doc, [("能力描述的来源。", True), ("功能能力以仓库 README 与官方文档为准,未对每个项目做源码级审计。因此表 2-2 中标注为「无」的项,含义是「公开代码与文档中未见对应实现」,而非断言其内部不存在。", False)])
-
-    heading(doc, "B.2 主题分布", 2)
-    caption(doc, "表 B-1 调研仓库的主题分布")
-    make_table(doc,
-        ["主题", "数量", "代表项目", "与本研究的关系"],
-        [["国产考试系统", "9", "xzs-mysql、exam-platform、lsgwr 版", "直接参照:业务闭环、表结构、三端功能"],
-         ["约束求解与优化", "2", "or-tools、timefold-solver", "问题一的方法来源与对照"],
-         ["离线同步与一致性", "5", "rxdb、pouchdb、Dexie.js、ShedLock、Testcontainers", "问题二的理论参照与工程支撑"],
-         ["在线监考", "8", "Proctoring-AI、Aankh、SEB 系列", "问题三的信号清单与方法边界"],
-         ["国际平台与标准", "3", "Moodle、Open edX、TAO(另含 QTI 标准 1 项)", "概念模型与标准实践"],
-         ["轻量 quiz 应用", "2", "ClassQuiz、django_quiz", "实时答题与题型设计的补充参考"]],
-        [3.4, 1.6, 6.0, 4.4])
-
-    heading(doc, "B.3 引用时的注意事项", 2)
-    bullet_rich(doc, [("注明查询日期。", True), ("星标数与提交时间会变化,若在论文中引用具体数值,应在脚注中写明「数据查询于 2026 年 11 月 9 日」,避免评审按定稿时的数值质疑。", False)])
-    bullet_rich(doc, [("区分「未见」与「没有」。", True), ("本文档的空白判定基于检索覆盖范围,表述上应使用「在本次检索范围内未见」而非「业界没有」,这是学术表述的严谨性要求。", False)])
-    bullet_rich(doc, [("功能描述以文档为准。", True), ("表 2-2 的能力对照来自官方文档,若论文需要更强的论断(如「该系统不支持冲突合并」),应补充源码核查并说明核查范围。", False)])
-    bullet_rich(doc, [("链接的可访问性。", True), ("部分国内项目在 GitHub 与 Gitee 双托管,若 GitHub 访问受限,可用 Gitee 镜像,但引用时应标注原始仓库地址。", False)])
-
-def main():
-    doc = init_doc()
-    cover(doc)
-    abstract(doc)
-    toc_page(doc)
-    chapter1(doc)
-    chapter2(doc)
-    doc.add_page_break()
-    chapter3(doc)
-    chapter4(doc)
-    chapter5(doc)
-    appendix_a(doc)
-    appendix_b(doc)
-    out = sys.argv[1]
-    doc.save(out)
-    print("saved", out)
-
-if __name__ == "__main__":
-    main()

+ 0 - 382
build_optimized_plan.py

@@ -1,382 +0,0 @@
-# -*- coding: utf-8 -*-
-from pathlib import Path
-from docx.shared import Pt, Cm
-from docx.enum.text import WD_ALIGN_PARAGRAPH
-from docx.oxml import OxmlElement
-from docx.oxml.ns import qn
-from docx_helpers import init_doc, para, rich, bullet, bullet_rich, heading, caption, note, make_table, toc, add_hyperlink, set_cn_font, link_para
-import optimized_plan_content as C
-
-ROOT = Path(__file__).resolve().parent
-OUT = ROOT / "在线考试系统实现方案(优化定稿版).docx"
-doc = init_doc()
-props = doc.core_properties
-props.title = "在线考试系统实现方案(优化定稿版)"
-props.subject = "组卷优化、断网续考答案一致性与作弊行为识别"
-props.author = "项目实现方案"
-props.keywords = "在线考试系统, Spring Boot, 模块化单体, 组卷, 答案一致性, 作弊识别"
-
-
-def code_block(lines):
-    for line in lines:
-        p = doc.add_paragraph()
-        p.paragraph_format.left_indent = Cm(0.65)
-        p.paragraph_format.right_indent = Cm(0.25)
-        p.paragraph_format.space_after = Pt(2)
-        p.paragraph_format.line_spacing = 1.15
-        r = p.add_run(line)
-        set_cn_font(r, "宋体", "Consolas", 9)
-
-
-def link_list(items):
-    for label, url in items:
-        p = doc.add_paragraph()
-        p.paragraph_format.left_indent = Cm(0.75)
-        p.paragraph_format.first_line_indent = Cm(-0.45)
-        p.paragraph_format.space_after = Pt(5)
-        r = p.add_run("· " + label + ":")
-        set_cn_font(r, "宋体", "Times New Roman", 9.5)
-        add_hyperlink(p, url, url, size=7.5)
-
-
-def decision_box(title, text):
-    t = doc.add_table(rows=1, cols=1)
-    t.style = "Table Grid"
-    t.autofit = False
-    cell = t.cell(0, 0)
-    cell.width = Cm(15.4)
-    shd = OxmlElement("w:shd")
-    shd.set(qn("w:fill"), "EAF2F8")
-    cell._tc.get_or_add_tcPr().append(shd)
-    cell.text = ""
-    p = cell.paragraphs[0]
-    p.paragraph_format.space_after = Pt(3)
-    r = p.add_run(title)
-    set_cn_font(r, "黑体", "Times New Roman", 11, True)
-    p2 = cell.add_paragraph()
-    p2.paragraph_format.space_after = Pt(3)
-    r2 = p2.add_run(text)
-    set_cn_font(r2, "宋体", "Times New Roman", 10.5)
-    para(doc, "", size=2, first_indent=False, space_after=0)
-
-
-# Cover
-para(doc, "在线考试系统", size=24, bold=True, first_indent=False, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=16, font_cn="黑体")
-para(doc, "实现方案(优化定稿版)", size=22, bold=True, first_indent=False, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=24, font_cn="黑体")
-para(doc, "——组卷优化、答案一致性与作弊识别方法研究", size=14, first_indent=False, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=30)
-decision_box("最优路线:方案 B + 评审整改 + 协议补强", "保留 Spring Boot 模块化单体与三项研究内容;不更换微服务,不引入视觉 AI。将断网续考升级为“操作日志 + 当前答案 + 乐观并发 + 幂等交卷”协议,优先完成可验证的核心闭环。")
-para(doc, "版本:2026 年 11 月 10 日", size=11, first_indent=False, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=8)
-para(doc, "依据:原方案 B、两方案可行性评审报告、开源调研及补充源码核验", size=10.5, first_indent=False, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=8)
-para(doc, "文档属性:实施定稿建议,不代表系统已实现或实验已完成", size=10.5, first_indent=False, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=8)
-doc.add_page_break()
-
-heading(doc, "执行摘要", 1)
-rich(doc, [("一、选择结论。", True), ("评审报告所指“两方案”并非互相竞争的技术路线,而是方案 B 与配套开源调研。最优解不是二选一,也不是另起炉灶,而是以方案 B 为主线、调研为证据支撑,逐项完成评审整改。", False)])
-rich(doc, [("二、主攻顺序。", True), ("问题二(断网续考答案一致性)优先保证协议正确与故障证据,问题一(多约束组卷)保证对比实验完整,问题三(作弊识别)限定为轻量行为信号的有效性探索。交卷—判分—成绩一致性是低成本支撑点。", False)])
-rich(doc, [("三、关键修订。", True), ("原方案把客户端版本号、服务端同步游标和幂等键混为一体,且 (session_id,question_id,version) 不能约束一道题仅有一个当前答案。本版明确拆分当前答案与追加操作日志,用 op_id 识别操作、base_version 防止过期写覆盖、server_seq 提供同步顺序,交卷通过 session 唯一约束与 submit_token 幂等。", False)])
-rich(doc, [("四、交付纪律。", True), ("P0 只做能形成论文证据的完整闭环;第 3 周必须跑通薄链路。降级时砍大屏、主观双评、视觉增强和非关键压测,不砍 E1/E2 的基线、原始数据和失败场景。", False)])
-rich(doc, [("五、证据状态。", True), ("截至 2026-11-10,已完成原评审整改设计及关键开源实体的源码核对;系统、实验、真实正常样本和完整学术检索尚待实施。文中“应”“计划”“目标”均是实施要求,不是既有结果。", False)])
-caption(doc, "表 0-1 方案范围与裁剪优先级")
-make_table(doc, ["级别", "纳入范围", "完成定义"], C.SCOPE_ROWS, [2.1, 9.1, 4.2], font=8.3)
-doc.add_page_break()
-heading(doc, "目录", 1)
-toc(doc)
-doc.add_page_break()
-
-# 1
-heading(doc, "一、最优解选择与建设边界", 1)
-heading(doc, "1.1 路线选择", 2)
-para(doc, "题目保持《在线考试系统中的组卷优化、答案一致性与作弊识别方法研究》。系统是为三项研究提供可运行载体与数据来源,而不是为了覆盖通用考试平台的所有功能。最优路线为“成熟业务流程借鉴 + 核心一致性协议自研 + 可复现实验优先”。")
-caption(doc, "表 1-1 五项关键取舍")
-make_table(doc, ["决策", "理由", "落地约束"], C.DECISION_ROWS, [3.4, 7.8, 4.2], font=8.4)
-heading(doc, "1.2 用户、终端与功能边界", 2)
-bullet_rich(doc, [("角色:", True), ("考生、教师、管理员。教师维护题库/组卷/阅卷,管理员编排考试/监控/复核;不另拆三个前端工程,使用同一 Vue 3 工程按角色路由。", False)])
-bullet_rich(doc, [("终端:", True), ("考生端只承诺桌面 Chromium 浏览器。IndexedDB、失焦与可见性事件是核心依赖;不支持存储、存储配额不足或隐私模式无法持久化时,阻止进入离线模式并提示,不宣称可在所有浏览器一致工作。", False)])
-bullet_rich(doc, [("必做业务:", True), ("题库与知识点、约束组卷、考试场次、答题与本地恢复、幂等交卷、客观判分、成绩查询、最小审计。", False)])
-bullet_rich(doc, [("可选业务:", True), ("主观题双评仲裁、监控大屏、复杂统计导出、界面美化。小程序、移动 App、视觉监考、公网生产级运维不在本期范围。", False)])
-bullet_rich(doc, [("防作弊定位:", True), ("风险分只触发人工复核,不自动认定作弊、不自动零分;风险事件是提示信号而非事实裁判。", False)])
-heading(doc, "1.3 非功能目标与表述纪律", 2)
-para(doc, "系统正确性优先于并发数字。目标是以单库约束与自动化故障注入证明核心协议正确,再在明确硬件、数据和脚本条件下测量性能。P95<500 ms、500 并发、核心覆盖率不低于 50% 是原方案工程目标,不是本文已验证结论;未满足时必须报告实测值与瓶颈。")
-
-# 2
-heading(doc, "二、评审整改闭环与证据状态", 1)
-para(doc, "评审 R1—R10 已转化为范围、模型、实验或检索任务。完成源码阅读不等于完成系统验证;完成方案修改也不等于得到实验结论。")
-caption(doc, "表 2-1 R1—R10 整改映射")
-make_table(doc, ["风险", "本版整改", "当前状态/后续"], C.REVIEW_ROWS, [2.25, 9.1, 4.05], font=8.0)
-heading(doc, "2.1 源码核验后收缩开源结论", 2)
-para(doc, "2026-11-10 的补充核验表明:学之思答卷表 t_exam_paper_question_customer_answer 的主键为 id,保存服务提供常规 insertList 等操作;在所读表和保存路径中未见按题版本、操作幂等键或冲突合并字段。因此不能直接复用为本设计的冲突协议,但不能由此推断整个系统没有任何断点续答能力。")
-para(doc, "砚考 ExamAnswer/ExamRecord 体现答卷、场次状态与交卷信息;ProctorEvent、ProctorRule、ProctorSession 包含事件分类、风险分和切屏/复制/失焦等计数。这些可作为事件清单与复核界面参考,不能证明其已经实现本设计的断网冲突合并。论文应写“所核验文件未见该机制”,而非写“开源界均没有”。")
-heading(doc, "2.2 文献论证仍需独立完成", 2)
-para(doc, "本次曾按 offline e-assessment answer synchronization 方向检索,但结果不足以支撑学术空白结论,且部分索引出现限流或验证页。因此 R8 未关闭。开题/论文中不得把 GitHub 未见实现直接写成“学术界无人研究”。")
-bullet(doc, "组卷:automated test assembly、constraint-based item selection、mixed integer/constraint programming、genetic algorithm examination paper generation。")
-bullet(doc, "一致性:offline-first e-assessment、optimistic replication、offline data synchronization、idempotent command、conflict resolution。")
-bullet(doc, "风险识别:e-assessment academic integrity、online proctoring behavior analytics、aberrant response behavior;区分视觉监考与轻量行为日志。")
-note(doc, "文献检索应保存数据库、完整检索式、日期、筛选标准与排除原因;正式参考文献须从学术工具导出并逐条核验,不以仓库 README 代替学术引用。")
-doc.add_page_break()
-
-# 3
-heading(doc, "三、总体架构与模块边界", 1)
-heading(doc, "3.1 分层与数据流", 2)
-para(doc, "浏览器负责交互、试卷缓存、答案本地持久化和待同步队列;Spring Boot 单应用承载全部业务规则;MySQL 是答案、交卷与成绩的权威存储;Redis 只用于可重建的限流、心跳/在线状态或短期缓存,不作为唯一答案来源。定时任务负责 outbox 重试、对账与到时收卷。")
-code_block([
-    "桌面 Vue 3 应用",
-    "  ├─ IndexedDB:exam_snapshot / answer_op / outbox_local / sync_state / conflict",
-    "  └─ HTTPS JSON(核心同步/交卷);WebSocket 仅监控通知",
-    "             ↓",
-    "Spring Boot 3 模块化单体:user / question / paper / exam / grading / score / monitor",
-    "  ├─ exam:会话锁、op 幂等、按题版本检查、交卷冻结",
-    "  ├─ grading + score:事务内 outbox、重试、对账",
-    "  └─ monitor:行为事件、固定规则、人工复核",
-    "             ↓",
-    "MySQL 8(权威状态)  +  Redis 7(可重建状态)  +  Python 实验脚本",
-])
-para(doc, "跨模块只调用 api 包的 Service/DTO 或消费领域事件,不直接访问他模块 Mapper。可以用 ArchUnit 或等价测试固定依赖方向;单体不是跨表任意调用。未来即使拆分,也需重新处理分布式事务,不能宣称仅凭分包即可无缝微服务化。")
-heading(doc, "3.2 技术栈与替代边界", 2)
-make_table(doc, ["层次", "确定选型", "使用边界"], [
-    ["后端", "Spring Boot 3.x、JDK 17+、Spring MVC、Jakarta Validation", "首日锁定依赖版本;显式统一异常与请求校验"],
-    ["持久化", "MyBatis-Plus Boot3 starter、MySQL 8.0", "并发/幂等用显式 SQL、事务与唯一索引,不只靠 ORM save"],
-    ["安全", "Spring Security、JWT、BCrypt/Argon2", "短期访问令牌;角色+资源归属双重校验"],
-    ["浏览器", "Vue 3、Vite、Pinia、Element Plus、Dexie.js", "IndexedDB 事务与同步状态机是核心,不只保存页面 state"],
-    ["缓存/通知", "Redis 7、WebSocket/STOMP", "缓存可失效重建;长连接不是答案可靠性保障"],
-    ["任务/一致性", "Spring Scheduling、ShedLock、outbox", "多实例防重锁;失败重试与对账"],
-    ["测试/实验", "JUnit 5、Mockito、Testcontainers、JMeter、Python", "真实 MySQL 并发测试;脚本保存配置和原始数据"],
-    ["部署", "Docker Compose、Nginx", "本地/教学演示部署;不等同生产高可用"],
-], [2.2, 6.0, 7.2], font=8.2)
-caption(doc, "表 3-1 模块职责与依赖边界")
-make_table(doc, ["模块", "职责", "关键约束"], C.MODULE_ROWS, [2.0, 6.4, 7.0], font=8.3)
-
-# 4
-heading(doc, "四、关键方法一:多约束组卷", 1)
-heading(doc, "4.1 形式化与约束分类", 2)
-para(doc, "设候选题为 Q={q₁,…,qₙ},二元变量 xᵢ∈{0,1} 表示是否选择题 i。每题具有分值 sᵢ、难度 dᵢ、题型 tᵢ、知识点集合 Kᵢ 和历史使用信息。组卷结果必须满足硬约束,并在可行解中优化覆盖均衡与难度偏差。")
-bullet(doc, "硬约束:题量、总分、题型数量、题目不重复、同卷互斥题、指定必选题;任何违反都判为不可行,不能用惩罚分掩盖。")
-bullet(doc, "分布约束:各难度档比例与知识点覆盖的目标区间;须区分“可在区间内”与“尽量接近”的语义。")
-bullet(doc, "软目标:历史避重、知识点覆盖偏差、难度分布偏差等加权目标;权重在实验前固定并随配置归档。")
-bullet(doc, "预检:候选总量、题型供给、知识点供给、总分上下界与互斥冲突;供给不足时返回 INFEASIBLE_INPUT,而不是把它计为算法求解失败。")
-heading(doc, "4.2 统一求解器与输出契约", 2)
-para(doc, "PaperSolver 接收不可变的题库快照、paper_rule 与随机种子,返回 status、selectedQuestionIds、score、constraintViolations、metrics、seed、elapsedMs。三种算法共享校验器和指标计算器,避免不同算法采用不同成功定义。")
-make_table(doc, ["求解器", "实现重点", "对照意义"], [
-    ["随机回溯", "随机选题+约束剪枝+有限重试;达到时间/重试上限返回失败", "工程基线,观察强约束下的退化"],
-    ["贪心+局部修补", "先满足最稀缺知识点/题型,再处理总分;交换/增补/删除局部修复", "低成本启发式,记录修复次数与残留违反"],
-    ["遗传算法", "固定种群、代数、交叉、变异与修复;硬约束不可行个体不得冒充成功", "同行级启发式对照,不无限调参"],
-    ["CP-SAT(可选)", "JDK17环境时间盒验证;可行时对小规模实例求最优/界限", "用于最优性差距;失败则只讨论不实测"],
-], [2.5, 8.1, 4.8], font=8.3)
-para(doc, "难度偏差以目标与实际五档分布的卡方统计量为主,并同时报告各档计数,避免小样本只看一个汇总数。KL 散度仅在统一平滑、明确 epsilon 时作为辅助,不能在零频档位直接计算。相同题库、规则、算法参数和种子必须可复跑。")
-doc.add_page_break()
-
-# 5
-heading(doc, "五、关键方法二:断网续考与答案一致性", 1)
-heading(doc, "5.1 一致性目标与威胁模型", 2)
-para(doc, "核心目标不是“断网时还能点击页面”,而是网络中断、浏览器刷新、应用重启、请求重试和多端并发后,答案与交卷语义仍然可解释。系统设计应保证:单个操作不重复生效、旧操作不覆盖新答案、同一考生同场次只有一个权威交卷结果、任何已受理操作可追踪。")
-note(doc, "客户端时间戳、客户端 last_seq 和客户端自增版本都不是权威依据;时钟可能回拨,重试可能重复,两个设备可能同时产生相同版本。权威顺序来自服务端事务和数据库约束。")
-heading(doc, "5.2 本地持久化模型", 2)
-make_table(doc, ["本地对象", "关键字段", "职责"], [
-    ["exam_snapshot", "sessionId、paperVersion、questions、rules、savedAt", "支持断网展示;开考在线时获取,敏感内容只存必要数据"],
-    ["answer_op", "opId、clientSeq、questionId、baseVersion、answer、localState", "每次有效编辑生成追加操作;本地仅在未同步区保留原文"],
-    ["outbox_local", "opId、attempts、nextRetryAt、lastError", "持久重试队列;页面刷新不能丢"],
-    ["sync_state", "sessionId、actorId、nextClientSeq、lastServerSeq、authoritativeStatus", "客户端序号本地唯一;server_seq仅作拉取游标"],
-    ["conflict", "opId、clientAnswer、serverVersion、serverAnswerRef、resolutionState", "保留冲突原文;未经规则或人工确认不得静默覆盖"],
-    ["submission_intent", "sessionId、submitToken、requestHash、status", "用户点击交卷时一次生成,断线重试沿用"],
-], [3.0, 6.2, 6.2], font=8.2)
-para(doc, "Dexie 事务同时写入 answer_op、outbox_local 和 sync_state,避免只写一半。页面内存中的 Pinia 状态不是持久化。浏览器存储被用户清除、设备丢失或浏览器更换不属于可无条件恢复范围;需在说明中明确这一局限。")
-heading(doc, "5.3 操作模型与服务端游标", 2)
-para(doc, "客户端 actorId 表示同一浏览器安装/会话内持久身份,clientSeq 在该 actor 内严格递增;opId 使用 UUID。服务端为每个已受理事件分配会话内 server_seq。op_id 全局唯一解决重试幂等,(session_id,actor_id,client_seq) 唯一防止同设备序号复用;两者职责不可互相替代。")
-code_block([
-    "答案操作(逻辑结构)",
-    "{ opId, actorId, clientSeq, questionId, baseVersion, answer,",
-    "  type: SET_ANSWER | CLEAR_ANSWER }",
-    "",
-    "服务端返回(逐项)",
-    "{ opId, status: APPLIED | DUPLICATE | STALE_VERSION | SESSION_CLOSED | REJECTED,",
-    "  questionId, resultVersion, serverSeq, authoritativeAnswer?, errorCode? }",
-])
-para(doc, "server_seq 可以排序“服务端已受理事件”,但不回答“哪个离线编辑应当获胜”。STALE_VERSION 表示同一题在客户端编辑后又有另一来源写入;它不是自动丢弃离线草稿的理由。排序、幂等与冲突解决是三个不同问题。")
-heading(doc, "5.4 单操作事务与批量同步", 2)
-code_block([
-    "对批次内每个 op(会话锁串行化,独立小事务,失败不回滚整个批次):",
-    "1. 校验本人会话、题目归属、载荷大小和考试状态。",
-    "2. 按 opId 查询:同 payloadHash → 返回原结果;不同请求体 → REJECTED。",
-    "3. 读取 answer_item:",
-    "   a. 首次写入 version=1,依赖 (session_id,question_id) 唯一;冲突后按并发分支重读。",
-    "   b. 后续写入 UPDATE ... SET version=version+1",
-    "      WHERE session_id=? AND question_id=? AND version=baseVersion。",
-    "4. 影响行数为0:记录 STALE_VERSION,保留客户端原文,返回权威版本。",
-    "5. 成功:写 answer_operation,next_seq=next_seq+1,server_seq=原next_seq;提交。",
-    "6. 客户端仅将 APPLIED/DUPLICATE 标记 synced;STALE_VERSION 进入 conflict,不无限自动重试。",
-])
-para(doc, "正式实现必须先固定锁与事务策略:一种简单策略是在每个 op 事务开头 SELECT ... FOR UPDATE 锁定 exam_session,再执行上述检查。它牺牲部分并发换取清晰语义,适合作为正确性基线。性能优化不得绕过幂等检查和状态检查。批量响应失败时客户端可查询同步结果后再重试,而不是直接扩大重试风暴。")
-heading(doc, "5.5 冲突处理:不做盲目“大者胜出”", 2)
-make_table(doc, ["场景", "判定", "处理"], [
-    ["同 op 重试,载荷相同", "opId+payloadHash一致", "返回首次结果,不新增版本/成绩"],
-    ["同 actor 的 clientSeq 复用", "(session,actor,clientSeq)冲突", "载荷相同按重复,不同则拒绝并告警"],
-    ["无其他设备编辑,仅本地连续编辑", "baseVersion与当前版本一致", "按服务端受理顺序应用;逐op更新版本"],
-    ["另一设备已修改同题", "baseVersion小于当前版本", "STALE_VERSION;本地草稿保留,显示冲突"],
-    ["不同题并行编辑", "互不共享答案版本", "可分别受理,不要求全卷全局版本锁"],
-    ["会话已交卷/冻结", "服务端状态非可写", "SESSION_CLOSED;返回交卷结果,迟到答案不覆盖"],
-    ["答案完全相同但版本落后", "payload与权威值相同,baseVersion过期", "返回STALE_VERSION,errorCode=CONTENT_ALREADY_EQUAL;不新增答案版本,保留可审计记录"],
-], [4.0, 5.1, 6.3], font=8.1)
-para(doc, "默认冲突策略为“权威值不被隐式覆盖 + 冲突显式呈现”。若考生在交卷前选择保留本地版本,客户端以最新 serverVersion 作为 baseVersion 发起一个新的 opId;该决定本身留痕。若产品要求自动合并,仅可对明确可合并的文本增量另行设计,不将“版本大者胜出”作为通用答案合并算法。")
-heading(doc, "5.6 交卷幂等、截止与迟到答案", 2)
-para(doc, "交卷请求在用户首次确认时生成 submitToken,requestHash 由会话、试卷版本、已同步游标和客户端声明信息计算。服务端以 session_id 唯一约束保证只有一次有效交卷,(session_id,submit_token) 唯一处理网络重试;同 token 不同请求体必须拒绝,不能返回貌似成功的旧结果。")
-para(doc, "交卷事务锁定会话,检查状态与 token,冻结当前权威答案并写 submission 与 outbox,最后置 SUBMITTED 并提交。答案写事务与交卷事务必须通过同一把会话锁串行化,以确定“受理在前还是交卷在前”。未同步草稿在断网交卷场景下不能凭空到达服务端,界面应明确“已交卷的是服务端已受理答案”,保留本地冲突/未同步记录供后续申诉,不承诺自动补交。")
-para(doc, "若交卷响应丢失,客户端进入 SUBMIT_UNKNOWN,使用同 token 重试或按会话查询;不得创建新 token 重复点击。到时收卷也复用同一状态机,服务端时间与 received_at 共同决定截止,客户端时间只作显示。")
-heading(doc, "5.7 恢复与快照优化", 2)
-para(doc, "最小 P0 以 answer_item + answer_operation 为恢复依据,不必另建快照表。需要优化时,脏标记队列按会话合并,每 30 秒批量写派生快照;关闭快照不应改变答案正确性。考试结束后按规则清理本地敏感数据,服务端冲突日志保留期限与访问权限须可配置。")
-doc.add_page_break()
-
-# 6
-heading(doc, "六、关键方法三:轻量作弊风险识别", 1)
-heading(doc, "6.1 信号与事件契约", 2)
-para(doc, "仅采集可解释且与考试相关的信号,不采集摄像头、人脸、屏幕内容或通讯录。每条事件至少包含 session_id、event_id、type、observed_at、received_at、payload 和 client_version;event_id 幂等,浏览器提供的事件与服务器观测到的登录/请求事件分开标识来源。")
-make_table(doc, ["信号", "计算方式", "限制与防误判"], [
-    ["切屏/失焦", "次数、累计时长、连续异常窗口", "系统弹窗/辅助技术可触发;浏览器不能证明考生在看什么"],
-    ["多端登录", "同账号有效设备数、会话切换与互斥策略", "区分重连/换设备与共享账号;不依据IP单独定性"],
-    ["IP/设备聚集", "同IP或设备关联的会话簇", "校园NAT/机房天然聚集;只作辅助"],
-    ["答题节奏", "答题间隔、修改频率、极短作答比例", "按题型与个人能力分层;不使用客户端时钟作唯一证据"],
-    ["答案相似度", "同场考生答案序列异常一致及共同错误", "标准客观题本来相似;须对照随机/同能力基线"],
-], [2.4, 6.0, 7.0], font=8.2)
-heading(doc, "6.2 固定规则与评价隔离", 2)
-para(doc, "首选先验固定权重或等权模型:riskScore=Σwᵢ·normalizedSignalᵢ,权重和阈值在评估集揭晓前冻结,保存 ruleVersion。若确需调参,则按考生划分调参与评估集合,同一考生的轨迹不能跨集合;不能在全部样本调权后再报告同一集合的召回率/误报率。")
-para(doc, "原计划 200 条标注样本可保留为上限目标,但不把它写成既定事实。正常轨迹应来自知情自愿的模拟考,争取 15—20 名参与者;脚本只构造异常模式。资源不足时,减少信号数量并报告小样本置信区间,结论降级为“信号有效性探索”。")
-heading(doc, "6.3 隐私、申诉与人工复核", 2)
-bullet(doc, "采集前说明目的、字段、保存期限和退出方式;使用虚构试题与模拟成绩,避免以真实成绩迫使参与。")
-bullet(doc, "设备标识优先使用随机安装ID;IP/User-Agent 按最小必要保存,报表聚合/脱敏,限制原始事件访问。")
-bullet(doc, "复核界面展示触发规则、原始事件时间线和相反证据;人工处置记录原因、操作者和时间。")
-bullet(doc, "浏览器失焦检测不能覆盖真实世界作弊;本研究不声称替代人工监考或视觉监考。")
-
-# 7
-heading(doc, "七、数据模型、接口与状态机", 1)
-para(doc, "核心表由原 15 张调整为 17 张:将当前答案与操作日志分开,并新增 submission 固化幂等交卷。以下字段为逻辑设计,建表时补齐主外键、时间戳、JSON类型、字符集和必要索引。")
-caption(doc, "表 7-1 核心数据表及约束")
-make_table(doc, ["表", "关键字段", "约束/用途"], C.TABLE_ROWS, [2.65, 7.4, 5.35], font=7.6)
-heading(doc, "7.1 必须落到数据库的约束", 2)
-code_block([
-    "UNIQUE exam_session(exam_id, user_id)",
-    "UNIQUE answer_item(session_id, question_id)",
-    "UNIQUE answer_operation(session_id, server_seq)",
-    "UNIQUE answer_operation(op_id)",
-    "UNIQUE answer_operation(session_id, actor_id, client_seq)",
-    "UNIQUE submission(session_id)",
-    "UNIQUE submission(session_id, submit_token)",
-    "UNIQUE score(session_id, grading_version)",
-    "INDEX  answer_operation(session_id, server_seq)",
-    "INDEX  outbox(status, next_retry_at)",
-])
-para(doc, "唯一索引只是最后防线,不能替代事务逻辑。opId 的全局唯一性意味着重试必须携带同一 ID;载荷摘要不保存为索引并不等于可省略校验。答案操作的状态枚举固定为 APPLIED、DUPLICATE、STALE_VERSION、SESSION_CLOSED、REJECTED:APPLIED 表示本次已生效,DUPLICATE 表示同 op 重试并返回原结果,STALE_VERSION 表示版本过期(内容相同也不自动覆盖),SESSION_CLOSED 表示会话不可写,REJECTED 表示校验失败。server_seq 只对已受理且需要同步的事件分配,被拒绝记录不占用游标;状态迁移通过会话行锁串行化,不另设悬空的会话级 version。")
-heading(doc, "7.2 核心接口契约", 2)
-make_table(doc, ["接口", "用途", "关键约束"], C.API_ROWS, [4.7, 3.6, 7.1], font=7.8)
-para(doc, "错误响应至少区分 SESSION_NOT_WRITABLE、DUPLICATE_PAYLOAD_MISMATCH、STALE_VERSION、INFEASIBLE_RULE、RATE_LIMITED、FORBIDDEN、STORAGE_UNAVAILABLE。客户端不得把所有非 2xx 都当作网络失败重试;限流与冲突采用不同退避/人工处理策略。")
-heading(doc, "7.3 状态迁移", 2)
-make_table(doc, ["状态/迁移", "触发", "不变量/动作"], C.STATE_ROWS, [4.4, 5.0, 6.0], font=8.0)
-
-# 8
-heading(doc, "八、交卷—判分—成绩一致性支撑", 1)
-para(doc, "交卷事务同时写 submission 和 outbox,消费者读取 SUBMITTED 事件后按 session_id+grading_version 幂等判分。客观题 P0 先实现;主观题双评仲裁是 P2,不作为核心链路的前置条件。")
-make_table(doc, ["环节", "实现", "异常处理"], [
-    ["交卷", "锁会话、冻结答案、写submission+outbox、提交事务", "事务回滚则没有半个交卷;同token可重试"],
-    ["事件分发", "本地调度扫描outbox;ShedLock防多实例并发", "状态NEW/RETRY/DEAD;指数退避与最大重试"],
-    ["客观判分", "读取冻结试卷和答案快照,按版本生成score", "同事件重复投递不重复加分;先查已有结果"],
-    ["成绩发布", "状态迁移与权限检查;复核生成新grading_version", "不覆盖历史版本;审计每次人工改分"],
-    ["对账", "扫描SUBMITTED无score、score与答案版本不一致、长时间RETRY", "可重放的自动修复;不能判定的进入人工队列"],
-], [2.2, 7.4, 5.8], font=8.2)
-para(doc, "Redis 或 WebSocket 不可用不应阻塞已经写入 MySQL 的交卷与判分。通知可延迟,最终状态可通过查询恢复。对账修复率是 E5 指标,而不是用“最终一致”四个字替代验证。")
-
-# 9
-heading(doc, "九、验证与实验设计", 1)
-para(doc, "所有实验保存 Git 提交号、配置文件、随机种子、原始 CSV、运行脚本、环境信息和失败日志。E1/E2 为不可砍核心;E3/E6 可以缩小,但必须保留最小工程验证。")
-caption(doc, "表 9-1 E1—E6 修订后的实验矩阵")
-make_table(doc, ["实验", "变量与步骤", "指标", "对照", "优先级/降级"], C.EXPERIMENT_ROWS, [1.5, 4.8, 3.8, 2.8, 2.5], font=7.3)
-heading(doc, "9.1 E2 核心测试用例(必须先于性能优化)", 2)
-make_table(doc, ["编号", "场景", "必须断言"], [
-    ["C01", "同一op同步两次/响应丢失重试", "当前答案只前进一次;仅一条有效操作;两次返回语义一致"],
-    ["C02", "同一批操作乱序/部分成功", "不覆盖已确认新值;服务端游标与逐op状态可恢复"],
-    ["C03", "两设备修改同题", "旧baseVersion不覆盖新值;本地原文保留;冲突可见"],
-    ["C04", "两设备修改不同题", "两题均正确保存;无全局版本误伤"],
-    ["C05", "浏览器刷新/断网30秒/5分钟", "本地op、nextClientSeq、outbox与提交意图可恢复"],
-    ["C06", "服务重启/请求超时", "重连查询后可确定每个op和交卷的真实状态"],
-    ["C07", "交卷与最后答案并发", "锁顺序确定;交卷后任何写不修改冻结答案"],
-    ["C08", "重复交卷/不同token/同token不同载荷", "仅一个有效交卷;错误请求明确拒绝;重复返回稳定结果"],
-    ["C09", "到时收卷与迟到答案", "received_at和服务端状态决定受理;不依赖客户端时钟"],
-    ["C10", "Redis/通知不可用", "MySQL权威答案、交卷、判分与对账仍可完成"],
-], [1.2, 5.4, 8.8], font=8.0)
-heading(doc, "9.2 指标定义", 2)
-bullet(doc, "操作可追踪率 = 可关联到唯一 op 结果的有效客户端操作数 / 客户端有效操作总数。它衡量审计完整性,不代替答案正确率。")
-bullet(doc, "答案语义正确率 = 故障结束后,服务端权威答案与测试预言机逐题一致的题目数 / 应核对题目总数。冲突待处理项单列,不能默认为正确。")
-bullet(doc, "会话语义正确率 = 故障结束后,答案版本、会话状态、交卷结果及待处理冲突均符合测试预言机的会话数 / 受测会话总数;仅全部不变量满足才计为正确,冲突未处置不得计为通过。")
-bullet(doc, "非预期重复数 = 同一 op 导致的重复状态推进、重复交卷或重复成绩记录数;目标为 0,重复请求总数本身不是错误。")
-bullet(doc, "恢复时长 = 网络/服务恢复到客户端获得权威一致状态的时间;区分连接恢复与冲突解决耗时。")
-bullet(doc, "误报率 = 被规则判为可疑的正常轨迹数 / 正常轨迹总数;必须同时报告样本构成、置信区间与按人划分方式。")
-
-# 10
-heading(doc, "十、实施排期与验收", 1)
-caption(doc, "表 10-1 13 周实施计划")
-make_table(doc, ["周次", "阶段", "任务", "可检查交付/止损点"], C.SCHEDULE_ROWS, [1.2, 2.6, 6.2, 5.4], font=8.0)
-heading(doc, "10.1 降级顺序", 2)
-para(doc, "当里程碑延期时按以下顺序裁剪:先取消监控大屏和主观双评,再缩小 E3 并发规模与非关键安全用例,再把问题三降为少量固定信号的单变量分析。不得裁掉答案唯一约束、幂等交卷、E1/E2 基线、真实 MySQL 故障测试和原始实验记录。")
-heading(doc, "10.2 最终验收标准", 2)
-make_table(doc, ["方面", "通过条件", "证据"], C.ACCEPT_ROWS, [2.0, 9.0, 4.4], font=8.2)
-
-# 11
-heading(doc, "十一、安全、隐私、许可与运行保障", 1)
-heading(doc, "11.1 安全与隐私清单", 2)
-bullet(doc, "所有会话/答案/成绩接口同时检查角色与资源归属,防止仅修改 sessionId 的水平越权;教师和管理员权限按动作细分。")
-bullet(doc, "SQL 使用参数绑定;题干/富文本白名单过滤;密码哈希保存;JWT 设置过期与签发校验,日志不记录令牌、完整答案或密码。")
-bullet(doc, "核心请求设置长度、批量大小、频率与超时限制;同步重试采用指数退避+抖动,服务端幂等而非客户端限流兜底。")
-bullet(doc, "HTTPS 部署;本地 IndexedDB 仅保存必要试卷与操作数据,离场清理;备份文件加密/限权并测试恢复。")
-bullet(doc, "参与者知情自愿;模拟考不用真实成绩;行为数据按期限删除;公开实验数据仅保留脱敏/聚合结果。")
-heading(doc, "11.2 开源许可", 2)
-para(doc, "学之思相关仓库为 AGPL-3.0,砚考为 MIT,其他仓库也应逐仓核验。无论许可证是否宽松,都应保留来源与许可文本;AGPL/无许可证仓库坚持“阅读原理,不复制代码”, MIT/Apache-2.0 依赖也保留 NOTICE 与版权信息。论文引用的“参考”不能代替软件分发的许可证合规。")
-heading(doc, "11.3 风险处置", 2)
-make_table(doc, ["风险", "预防", "触发后的止损"], C.RISK_ROWS, [3.0, 8.0, 4.4], font=8.2)
-
-# 12
-heading(doc, "十二、论文落点与待完成事项", 1)
-heading(doc, "12.1 论文结构对应", 2)
-make_table(doc, ["章节", "本方案可提供的材料", "禁止写法"], [
-    ["绪论/研究现状", "开源检索记录、源码级能力对照、定向学术检索", "GitHub未见⇒学术无人研究"],
-    ["需求分析", "三角色用例、桌面端假设、P0/P1/P2与隐私边界", "手机、小程序、视觉监考全支持"],
-    ["系统设计", "模块化架构、17表约束、接口与状态机", "唯一索引本身就解决所有冲突"],
-    ["关键方法", "约束模型、三求解器、op/版本/游标协议、固定风险规则", "客户端版本大者胜出即可保证一致"],
-    ["系统实现与实验", "E1/E2原始数据、故障日志;按资源补E3—E6", "把目标指标写成已达到,或只展示成功截图"],
-    ["总结与展望", "真实负结果、样本/终端/单实例边界、后续优化", "没有实测却声称生产高可用或通用反作弊"],
-], [3.0, 7.8, 4.6], font=8.3)
-heading(doc, "12.2 实施前仍需确认", 2)
-bullet(doc, "按学校模板确认开题字数、参考文献格式和 13 周起止日期;本方案不替代学校官方模板。")
-bullet(doc, "完成 R8 定向学术检索并保存检索证据,再确定“创新点”措辞;当前只能确认工程差异化设计。")
-bullet(doc, "半天验证 JDK17 + or-tools CP-SAT;失败即启用两级对照,不消耗核心开发时间。")
-bullet(doc, "确认模拟考参与者、知情说明和样本量;未落实前不承诺“真实误报率”或固定 200 条样本结论。")
-bullet(doc, "将 op 状态语义、会话锁、截止时间、交卷边界先写成自动化测试,再编码性能优化。")
-heading(doc, "12.3 一句话执行准则", 2)
-decision_box("先证明“不会错”,再测量“跑多快”;先保住 E1/E2,再扩展功能。", "本题最强的交付不是功能数量,而是:组卷实验可复跑,断网与重放可证明,交卷与成绩可追踪,风险识别有边界。任何扩展都不能破坏这四条。")
-
-# Appendix
-heading(doc, "附录 A 已核验源码与工具链接", 1)
-para(doc, "以下为 2026-11-10 补充源码核验涉及的链接。链接用于追溯所读文件,不是完整的学术参考文献表;论文定稿应固定仓库提交哈希并再次复核。")
-link_list([
-    ("学之思答卷表结构(t_exam_paper_question_customer_answer)", C.XZS_SQL),
-    ("学之思答卷保存服务", C.XZS_SERVICE),
-    ("砚考 ExamAnswer 实体", C.EXAM_ANSWER),
-    ("砚考 ProctorEvent 实体", C.PROCTOR_EVENT),
-    ("砚考 ProctorSession 实体", C.PROCTOR_SESSION),
-    ("Dexie.js(IndexedDB 封装)", C.DEXIE),
-    ("ShedLock(定时任务锁)", C.SHEDLOCK),
-    ("Testcontainers Java(真实依赖测试)", C.TESTCONTAINERS),
-    ("Google OR-Tools(可选 CP-SAT 对照)", C.ORTOOLS),
-])
-heading(doc, "附录 B 最小目录与实验归档建议", 1)
-code_block([
-    "exam-system/",
-    "  backend/  (模块 api/internal;事务与并发测试)",
-    "  frontend/ (Dexie schema、同步状态机、冲突与交卷界面)",
-    "  deploy/   (Compose、Nginx、备份与恢复说明)",
-    "  experiments/",
-    "    E1-paper/ E2-consistency/ E3-performance/",
-    "    E4-risk/ E5-score/ E6-security/",
-    "    各目录保存 README、配置、原始数据、脚本、图表和环境记录",
-    "  docs/     (接口、数据字典、开源核验、检索记录、许可证清单)",
-])
-note(doc, "文档结束。版本:2026-11-10。原方案 B、评审报告和开源调研均保留,本版用于统一后续实现与实验口径。")
-
-doc.save(OUT)
-print(OUT)
-print(f"paragraphs={len(doc.paragraphs)}, tables={len(doc.tables)}")

+ 0 - 249
build_ref_doc.py

@@ -1,249 +0,0 @@
-# -*- coding: utf-8 -*-
-import sys
-from docx import Document
-from docx.shared import Pt, Cm, RGBColor
-from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
-from docx.enum.table import WD_TABLE_ALIGNMENT
-from docx.enum.section import WD_SECTION
-from docx.oxml.ns import qn
-from docx.oxml import OxmlElement
-import docx.opc.constants
-
-sys.stdout.reconfigure(encoding="utf-8")
-
-OUT = r"C:\Users\aroun\Desktop\在线考试系统\在线考试系统开源方案调研与论文写作参考.docx"
-
-doc = Document()
-
-# ---------- page setup ----------
-sec = doc.sections[0]
-sec.page_width = Cm(21.0)
-sec.page_height = Cm(29.7)
-sec.top_margin = Cm(2.54)
-sec.bottom_margin = Cm(2.54)
-sec.left_margin = Cm(2.8)
-sec.right_margin = Cm(2.8)
-TEXT_W = 21.0 - 2.8 * 2  # 15.4 cm
-
-# ---------- styles ----------
-def set_cn_font(style_or_run, name_cn, name_ascii=None, size=None, bold=None, color=None):
-    f = style_or_run.font
-    f.name = name_ascii or name_cn
-    if size is not None:
-        f.size = Pt(size)
-    if bold is not None:
-        f.bold = bold
-    if color is not None:
-        f.color.rgb = color
-    el = style_or_run.element if hasattr(style_or_run, "element") else style_or_run._element
-    rPr = el.get_or_add_rPr()
-    rFonts = rPr.find(qn("w:rFonts"))
-    if rFonts is None:
-        rFonts = OxmlElement("w:rFonts")
-        rPr.append(rFonts)
-    rFonts.set(qn("w:ascii"), name_ascii or name_cn)
-    rFonts.set(qn("w:hAnsi"), name_ascii or name_cn)
-    rFonts.set(qn("w:eastAsia"), name_cn)
-
-normal = doc.styles["Normal"]
-set_cn_font(normal, "宋体", "Times New Roman", 10.5)
-normal.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
-normal.paragraph_format.space_before = Pt(0)
-normal.paragraph_format.space_after = Pt(4)
-
-BLACK = RGBColor(0, 0, 0)
-
-h1 = doc.styles["Heading 1"]
-set_cn_font(h1, "黑体", "Times New Roman", 16, True, BLACK)
-h1.paragraph_format.space_before = Pt(18)
-h1.paragraph_format.space_after = Pt(8)
-h2 = doc.styles["Heading 2"]
-set_cn_font(h2, "黑体", "Times New Roman", 13, True, BLACK)
-h2.paragraph_format.space_before = Pt(14)
-h2.paragraph_format.space_after = Pt(6)
-h3 = doc.styles["Heading 3"]
-set_cn_font(h3, "黑体", "Times New Roman", 11, True, BLACK)
-h3.paragraph_format.space_before = Pt(10)
-h3.paragraph_format.space_after = Pt(4)
-
-# ---------- helpers ----------
-def add_hyperlink(paragraph, url, text, size=9, bold=False):
-    part = paragraph.part
-    r_id = part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True)
-    hyperlink = OxmlElement("w:hyperlink")
-    hyperlink.set(qn("r:id"), r_id)
-    new_run = OxmlElement("w:r")
-    rPr = OxmlElement("w:rPr")
-    rFonts = OxmlElement("w:rFonts")
-    rFonts.set(qn("w:ascii"), "Consolas")
-    rFonts.set(qn("w:hAnsi"), "Consolas")
-    rFonts.set(qn("w:eastAsia"), "宋体")
-    rPr.append(rFonts)
-    sz = OxmlElement("w:sz"); sz.set(qn("w:val"), str(int(size * 2))); rPr.append(sz)
-    szcs = OxmlElement("w:szCs"); szcs.set(qn("w:val"), str(int(size * 2))); rPr.append(szcs)
-    if bold:
-        b = OxmlElement("w:b"); rPr.append(b)
-    color = OxmlElement("w:color"); color.set(qn("w:val"), "1F4E79"); rPr.append(color)
-    u = OxmlElement("w:u"); u.set(qn("w:val"), "single"); rPr.append(u)
-    new_run.append(rPr)
-    t = OxmlElement("w:t")
-    t.text = text
-    t.set(qn("xml:space"), "preserve")
-    new_run.append(t)
-    hyperlink.append(new_run)
-    paragraph._p.append(hyperlink)
-    return hyperlink
-
-def shade(cell, hexcolor):
-    tcPr = cell._tc.get_or_add_tcPr()
-    shd = OxmlElement("w:shd")
-    shd.set(qn("w:val"), "clear")
-    shd.set(qn("w:color"), "auto")
-    shd.set(qn("w:fill"), hexcolor)
-    tcPr.append(shd)
-
-def cell_margins(table, top=60, bottom=60, left=90, right=90):
-    tblPr = table._tbl.tblPr
-    mar = OxmlElement("w:tblCellMar")
-    for tag, val in (("top", top), ("left", left), ("bottom", bottom), ("right", right)):
-        el = OxmlElement("w:" + tag)
-        el.set(qn("w:w"), str(val))
-        el.set(qn("w:type"), "dxa")
-        mar.append(el)
-    tblPr.append(mar)
-
-def repeat_header(row):
-    trPr = row._tr.get_or_add_trPr()
-    el = OxmlElement("w:tblHeader")
-    el.set(qn("w:val"), "true")
-    trPr.append(el)
-
-def cant_split(row):
-    trPr = row._tr.get_or_add_trPr()
-    el = OxmlElement("w:cantSplit")
-    el.set(qn("w:val"), "true")
-    trPr.append(el)
-
-def set_cell_text(cell, text, size=9, bold=False, align=None, color=None):
-    cell.text = ""
-    p = cell.paragraphs[0]
-    p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
-    p.paragraph_format.space_before = Pt(0)
-    p.paragraph_format.space_after = Pt(0)
-    if align is not None:
-        p.alignment = align
-    run = p.add_run(text)
-    set_cn_font(run, "宋体", "Times New Roman", size, bold, color)
-
-def make_table(headers, rows, widths, link_col=None, font=8.5, head_font=8.5):
-    table = doc.add_table(rows=1, cols=len(headers))
-    table.style = "Table Grid"
-    table.alignment = WD_TABLE_ALIGNMENT.CENTER
-    table.autofit = False
-    cell_margins(table)
-    hdr = table.rows[0]
-    repeat_header(hdr)
-    cant_split(hdr)
-    for i, h in enumerate(headers):
-        c = hdr.cells[i]
-        c.width = Cm(widths[i])
-        shade(c, "D9D9D9")
-        set_cell_text(c, h, head_font, True, WD_ALIGN_PARAGRAPH.CENTER)
-    for r in rows:
-        row = table.add_row()
-        cant_split(row)
-        for i, val in enumerate(r):
-            c = row.cells[i]
-            c.width = Cm(widths[i])
-            if link_col is not None and i == link_col:
-                c.text = ""
-                p = c.paragraphs[0]
-                p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
-                p.paragraph_format.space_before = Pt(0)
-                p.paragraph_format.space_after = Pt(0)
-                add_hyperlink(p, val[1], val[0], size=font - 0.5)
-            else:
-                set_cell_text(c, val, font)
-    # force column widths on every cell (Word honours cell-level w)
-    for row in table.rows:
-        for i, c in enumerate(row.cells):
-            c.width = Cm(widths[i])
-    return table
-
-def para(text, size=10.5, bold=False, indent=True, space_after=4, align=None, color=None, first_indent=True):
-    p = doc.add_paragraph()
-    p.paragraph_format.space_after = Pt(space_after)
-    if align is not None:
-        p.alignment = align
-    if indent and first_indent:
-        p.paragraph_format.first_line_indent = Pt(21)
-    run = p.add_run(text)
-    set_cn_font(run, "宋体", "Times New Roman", size, bold, color)
-    return p
-
-def bullet(text, size=10.5, level=0):
-    p = doc.add_paragraph(style="List Bullet")
-    p.paragraph_format.left_indent = Cm(0.75 + 0.6 * level)
-    p.paragraph_format.first_line_indent = Cm(-0.4)
-    p.paragraph_format.space_after = Pt(2)
-    run = p.add_run(text)
-    set_cn_font(run, "宋体", "Times New Roman", size)
-    return p
-
-def numitem(n, title, text, size=10.5):
-    p = doc.add_paragraph()
-    p.paragraph_format.space_after = Pt(4)
-    p.paragraph_format.left_indent = Cm(0.75)
-    p.paragraph_format.first_line_indent = Cm(-0.75)
-    r1 = p.add_run("%s " % n)
-    set_cn_font(r1, "宋体", "Times New Roman", size, True)
-    r2 = p.add_run(title)
-    set_cn_font(r2, "黑体", "Times New Roman", size, True)
-    if text:
-        r3 = p.add_run(text)
-        set_cn_font(r3, "宋体", "Times New Roman", size)
-    return p
-
-def caption(text):
-    p = doc.add_paragraph()
-    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-    p.paragraph_format.space_before = Pt(6)
-    p.paragraph_format.space_after = Pt(3)
-    run = p.add_run(text)
-    set_cn_font(run, "黑体", "Times New Roman", 9, True)
-    return p
-
-def note(text):
-    p = doc.add_paragraph()
-    p.paragraph_format.space_before = Pt(2)
-    p.paragraph_format.space_after = Pt(10)
-    p.paragraph_format.left_indent = Cm(0.2)
-    run = p.add_run(text)
-    set_cn_font(run, "楷体", "Times New Roman", 9.5, False, RGBColor(0x40, 0x40, 0x40))
-    return p
-
-def quote_block(lines):
-    for ln in lines:
-        p = doc.add_paragraph()
-        p.paragraph_format.left_indent = Cm(0.9)
-        p.paragraph_format.right_indent = Cm(0.4)
-        p.paragraph_format.space_after = Pt(4)
-        p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
-        run = p.add_run(ln)
-        set_cn_font(run, "楷体", "Times New Roman", 10)
-    return
-
-# footer page number
-footer = sec.footer
-fp = footer.paragraphs[0]
-fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
-run = fp.add_run()
-fld1 = OxmlElement("w:fldChar"); fld1.set(qn("w:fldCharType"), "begin")
-instr = OxmlElement("w:instrText"); instr.set(qn("xml:space"), "preserve"); instr.text = " PAGE "
-fld2 = OxmlElement("w:fldChar"); fld2.set(qn("w:fldCharType"), "end")
-run._r.append(fld1); run._r.append(instr); run._r.append(fld2)
-set_cn_font(run, "宋体", "Times New Roman", 9)
-
-print("setup ok")
-doc.save(OUT)
-print("saved skeleton")

+ 0 - 165
build_review_report.py

@@ -1,165 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Build 在线考试系统方案评审报告.docx from docx_helpers."""
-import docx_helpers as H
-from docx.enum.text import WD_ALIGN_PARAGRAPH
-from docx.shared import Pt
-
-doc = H.init_doc()
-
-# ============ 封面区(非独立页,正式报告头) ============
-p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-r = p.add_run("在线考试系统两方案可行性评审报告")
-H.set_cn_font(r, "黑体", "Times New Roman", 22, True, H.BLACK)
-
-p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-p.paragraph_format.space_after = Pt(2)
-r = p.add_run("评审对象:《在线考试系统技术文案(方案B)》《在线考试系统开源方案调研与论文写作参考》")
-H.set_cn_font(r, "楷体", "Times New Roman", 11)
-
-p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-p.paragraph_format.space_after = Pt(18)
-r = p.add_run("版本:2026 年 11 月 9 日 | 评审结论:两方案合并可行,附 10 项风险与整改优先级")
-H.set_cn_font(r, "楷体", "Times New Roman", 10, False, H.GREY)
-
-from docx.shared import Pt
-
-# ============ 摘要 ============
-H.heading(doc, "评审摘要", 1)
-H.para(doc, "本报告受委托对两份文档进行可行性评审:《在线考试系统技术文案(方案B)》(Spring Boot 模块化单体技术方案)与《在线考试系统开源方案调研与论文写作参考》(GitHub 生态调研)。评审范围覆盖定位正确性、技术可行性、工作量可行性、论证严谨性四个维度,并对两份文档做了交叉一致性检查。")
-H.para(doc, "评审首先澄清一个关键前提:两份文档不是二选一的竞争关系。调研文档在开篇明确自我定位为方案 B 的配套材料,服务于同一篇论文《在线考试系统中的组卷优化、答案一致性与作弊识别方法研究》。因此本报告按一份技术方案加一份立论支撑材料分别评估,并检查其内部一致性。")
-H.rich(doc, [("总体结论:", True),
-    ("两份文档合并构成一个可行的本科毕业设计方案,且具备产出一篇有真实增量论文的潜力。方案 B 的技术路线无不可验证的技术赌注;调研文档把创新性论证从「我觉得没人做过」升级为「检索范围内可复现的空白」。可行性瓶颈不在技术,而在执行时间与两处尚未闭环的证据链。", False)])
-
-# 总评表
-H.caption(doc, "表 0-1 两份文档分维度评审总览")
-H.make_table(doc,
-    ["维度", "方案B(技术文案)", "开源调研文档"],
-    [
-        ["定位正确性", "与本科毕设体量匹配", "与方案B逐章对应"],
-        ["技术可行性", "高:栈成熟、无未验证依赖", "高:纯文档工作"],
-        ["工作量可行性", "警示:处于上限边缘", "通过:余量大"],
-        ["论证严谨性", "有基线、有指标、有风险预案", "高于本科水准,一处硬伤(R7)"],
-        ["总体结论", "可行,需预设降级路径", "可行,是论文的护城河而非附件"],
-    ],
-    [3.0, 5.6, 5.6])
-
-H.note(doc, "评审基准:两份文档正文与全部表格已完整读取;星标数等时效数据以文档标注的 2026-11-09 复核值为准,本报告不另行复核。")
-
-# ============ 目录 ============
-H.toc(doc)
-
-# ============ 第一章 ============
-H.heading(doc, "一、评审范围与对象", 1)
-H.para(doc, "评审对象为工作区内的两份 Word 文档。为使后续章节的风险编号可追溯,此处先固定指代:")
-H.bullet(doc, "《在线考试系统技术文案(方案B)》,下称「方案B」:给出题目定位、技术栈、三个研究内容、数据模型、实验方案 E1—E6、论文结构、12 周排期、风险预案、答辩预案与文献策略。")
-H.bullet(doc, "《在线考试系统开源方案调研与论文写作参考》,下称「调研文档」:覆盖 60 余个候选仓库、逐仓核验 29 个、重点精读 6 个,按同类系统—组卷求解—离线同步—作弊识别—平台与标准五条线索组织。")
-H.para(doc, "评审方法:逐节通读两份文档正文与表格,核对交叉引用关系(如方案B的 E1—E6 与调研文档表 5-2 实验组织建议的对应),按「技术可行性—工作量—论证严谨性—一致性」的顺序形成结论。风险编号 R1—R10 在全文中保持唯一。")
-
-# ============ 第二章 方案B ============
-H.heading(doc, "二、方案B(技术文案)可行性评审", 1)
-
-H.heading(doc, "2.1 做出正确判断的部分", 2)
-H.para(doc, "以下六项判断直接决定了方案的可行性,评审予以确认:")
-H.bullet_rich(doc, [("架构降级判断正确。", True),
-    ("从 Spring Cloud 微服务改为 Spring Boot 模块化单体,三条理由(三个研究点均在单业务边界内闭环、断网续考一致性本质是单库唯一索引、跨服务一致性挤占实验时间)成立,且已写进论文第 4 章论证——答辩最常被攻击的点已提前布防。", False)])
-H.bullet_rich(doc, [("去掉 RocketMQ 换 outbox 加对账是聪明的范围裁剪。", True),
-    ("少一个中间件约等于环境故障率减半,且对账修复率本身成为实验 E5 的硬指标——范围收缩反而产出了实验素材。", False)])
-H.bullet_rich(doc, [("三个研究点全部可量化、有基线。", True),
-    ("E1—E6 每个实验都写明了基线(随机抽题重试、无本地暂存、无缓存、单一切屏规则、无对账),方案自身定的纪律「没有基线的指标等于自夸」被贯彻。", False)])
-H.bullet_rich(doc, [("数据模型落到字段级。", True),
-    ("answer_item 建唯一索引 (session_id, question_id, version) 是问题二幂等的物理保障,该设计在 MySQL 8.0 上零验证成本。", False)])
-H.bullet_rich(doc, [("工程细节真实。", True),
-    ("「mybatis-plus-spring-boot3-starter 是本项目最常见的启动失败原因」这类表述说明作者踩过坑;ShedLock 防多实例快照重复执行、Testcontainers 做真实重启注入,选型均无争议。", False)])
-H.bullet_rich(doc, [("排期纪律明确。", True),
-    ("「第 3 周跑通一条完整链路,跑不通砍功能不砍实验」是本科毕设最关键的生存规则。", False)])
-
-H.heading(doc, "2.2 风险与必须补齐的缺口", 2)
-H.para(doc, "以下按严重度排列,编号 R1—R6。每项给出问题描述、影响与具体补法。")
-
-H.heading(doc, "R1(最大风险)总工作量贴着上限", 3)
-H.para(doc, "8 个模块、15 张表、40+ 接口、三端前端、6 组实验、500 并发压测、核心模块 50% 测试覆盖率——方案自身承认「三个都做就是一篇有分量的论文」,但没有回答「做不完时先砍哪个」。建议现在就预设降级顺序:")
-H.bullet(doc, "问题二(一致性)不可砍:成本最低、指标最硬,且是开源调研证实的空白点。")
-H.bullet(doc, "问题一(组卷)不可砍:最有演示效果,E1 计算成本近乎为零(900 次求解器运行是毫秒级)。")
-H.bullet(doc, "问题三(作弊)第一个降级:证据价值最弱(样本自造),降到「单信号 AUC + 简单加权」仍可成章。")
-H.bullet(doc, "E3 压测与 E6 安全测试降为「记录机器规格 + 外推」,不作为硬门槛。")
-
-H.heading(doc, "R2 E4 存在方法论漏洞:权重设定与评估同源", 3)
-H.para(doc, "规则引擎的权重如果在这 200 条样本上调出来、又在同 200 条上算召回与误报,就是过拟合,答辩一问即穿。补法二选一:其一,200 条拆成调参集与评估集各半;其二,权重先验固定(依据信号文献),评估集只测不调。调研文档表 5-2 已把「等权求和」列为对照物,方向正确,但方案B正文应补一句权重确定协议。")
-
-H.heading(doc, "R3 作弊样本的「正常类」必须来自真实使用", 3)
-H.para(doc, "若 100 条正常轨迹也是脚本造的,E4 就成了循环论证——自己构造信号模式、再验证规则能识别该模式。最低成本补法:请 15—20 名同学各做一场模拟考,正常样本即真实数据,构造脚本只负责作弊类。方案已预判此质疑(风险表「主动说明构造方法」),但「正常样本真实化」比「说明构造方法」的防御力强一个量级。")
-
-H.heading(doc, "R4 客户端范围假设未显式声明", 3)
-H.para(doc, "IndexedDB、visibilitychange、失焦事件只在桌面浏览器完整可用;微信小程序端(学之思有)无法实现断网续考与切屏检测。需在需求分析中明确「考生端仅支持桌面浏览器」为硬性假设,否则被问「手机上怎么办」时无解。")
-
-H.heading(doc, "R5 两处小硬伤,交付导师前修掉", 3)
-H.bullet(doc, "章节标题「八、12 周排期」,但排期表实际覆盖第 1—13 周。")
-H.bullet(doc, "难度分布偏差以 KL 散度为主指标不稳:5 个难度档、某档计数为 0 时 KL 发散。建议卡方值为主、KL 为辅。")
-
-H.heading(doc, "R6 快照写入在压测场景的自扰", 3)
-H.para(doc, "500 并发 × 每 30 秒快照 ≈ 每分钟 1000 次快照写。量级上 MySQL 扛得住,但 E3 若测出 P95 超标,先排查的应是快照任务本身(改为脏标记 + 批量落盘)而不是业务 SQL。建议把这条写进实验预案。")
-
-# ============ 第三章 调研文档 ============
-H.heading(doc, "三、开源调研文档可行性评审", 1)
-
-H.heading(doc, "3.1 做出正确判断的部分", 2)
-H.bullet_rich(doc, [("方法论超出本科水准。", True),
-    ("60 余候选 → 逐仓核验 29 → 精读 6;检索式、命中规模、数据口径(2026-11-09 复核)全部留痕;附录 B 明确区分「未见」与「没有」。评审无法从数据时效性与断言严谨性上发难。", False)])
-H.bullet_rich(doc, [("空白判定是问题二的立论支柱。", True),
-    ("topic:online-exam(119 仓)、topic:exam(1744 仓)、topic:proctoring(155 仓)逐一检视后确认无任何以「客户端—服务端答案冲突合并」为核心的系统——证据链完整,答辩可守。", False)])
-H.bullet_rich(doc, [("三级实验对照设计(表 5-2)是全文档最有价值的一张表。", True),
-    ("基线级(随机重试)、同行级(cppcpp 遗传算法)、上界级(or-tools CP-SAT 精确解)。有上界才能算「最优性差距」,这让 E1 从自证升级为定位。", False)])
-H.bullet_rich(doc, [("许可证风险表(表 4-3)避免了一个真实事故。", True),
-    ("学之思 AGPL-3.0 传染性、无许可证仓库「只读不抄」的处置都是正确的。", False)])
-H.bullet_rich(doc, [("5.2 节三段可直接改写的研究现状素材质量高。", True),
-    ("语体和分寸已经是可以进论文的状态。", False)])
-
-H.heading(doc, "3.2 风险与必须补齐的缺口", 2)
-
-H.heading(doc, "R7(文档自身的硬伤)能力对照表是 README 级而非源码级", 3)
-H.para(doc, "表 2-2 中所有「无」的判定均基于 README 与文档描述。这张表会进论文第 1 章,是整个创新性论证的地基。必须执行 §5.5 列出的「半天通读学之思答卷表结构 + 砚考防作弊事件表」,把字段级结论落进笔记;否则「砚考其实做了冲突处理」这类反例会让核心论证翻车。这是两份文档中唯一「不补就致命」的缺口。")
-
-H.heading(doc, "R8 学术文献侧仍是空白", 3)
-H.para(doc, "开源空白不等于学术空白。e-assessment 领域离线考试客户端、optimistic replication 在考试场景的应用是否有论文,决定了「空白性」论证能否同时通过 GitHub 和知网/DBLP 两道检验。方案B第十一章的文献策略(每研究点至少一篇核心期刊)必须落地,且应在开题前完成检索——万一学术侧已有密集工作,问题二的表述需从「无人做过」调整为「开源工程界未落地」。")
-
-H.heading(doc, "R9 or-tools Java 绑定验证建议时间盒化", 3)
-H.para(doc, "半天内跑不通 JDK 17 + CP-SAT 的 hello world,就降级为「论文中作为对照讨论、不做实测上界」,E1 改用「随机基线 + cppcpp 同行 + 本文三求解器」两级对照,依然成立。")
-
-H.heading(doc, "R10 引用时效", 3)
-H.para(doc, "星标数与提交时间已注明查询日期且建议定稿前复核——已闭环,无风险,仅提醒执行。")
-
-# ============ 第四章 交叉一致性 ============
-H.heading(doc, "四、交叉一致性检查", 1)
-H.para(doc, "逐项核对两份文档的核心主张,未发现实质矛盾:")
-H.bullet(doc, "三研究点定义、五条一致性协议设计(IndexedDB、唯一索引、last_seq、快照、submit_token)、5 类作弊信号、outbox 加对账——两文完全一致。")
-H.bullet(doc, "调研文档表 2-2 的「本研究(方案B)」列与方案B的 E1—E6 指标一一对应。")
-H.bullet(doc, "文献策略与表 4-2 检索方向互为补充,无重复无冲突。")
-H.para(doc, "仅两处轻微不齐(均属方案B侧,已列入 R5):12 周 vs 13 周;数据模型「表不少于 15」而自身恰好列了 15 张(达标但零冗余,加 1 张余量更稳,如 exam_record 归档表)。")
-
-# ============ 第五章 行动清单 ============
-H.heading(doc, "五、整改优先级与行动清单", 1)
-H.para(doc, "按优先级排序的完整行动清单如下。P0 为开题前必须完成,P1 为开题后两周内写入方案,P2 为实验设计阶段落实,P3 为条件允许时优化。")
-H.caption(doc, "表 5-1 整改优先级清单")
-H.make_table(doc,
-    ["优先级", "行动", "对应风险", "建议时限"],
-    [
-        ["P0", "源码级通读学之思答卷表与砚考防作弊事件表", "R7", "半天,开题前"],
-        ["P0", "完成问题二的学术文献检索(知网 + DBLP)", "R8", "开题前"],
-        ["P1", "预设降级顺序写入排期:问题三→E3/E6 降级路径", "R1", "开题后两周内"],
-        ["P1", "E4 权重确定协议(调参/评估分离或先验固定)写进 3.3 节", "R2", "开题后两周内"],
-        ["P2", "正常类作弊样本改为真实模拟考采集", "R3", "实验设计阶段"],
-        ["P2", "显式声明考生端仅桌面浏览器;修 12/13 周与 KL 散度两处", "R4/R5", "交付导师前"],
-        ["P3", "or-tools 验证时间盒半天;快照批量写入预案", "R9/R6", "条件允许时"],
-    ],
-    [1.6, 6.4, 1.8, 3.4])
-
-H.para(doc, "以上清单执行完毕后,方案B与调研文档即可作为开题材料提交。若时间仅够完成 P0 两项,也应优先完成——它们直接决定创新性论证能否通过评审。")
-
-# ============ 第六章 结论 ============
-H.heading(doc, "六、评审结论", 1)
-H.para(doc, "方案B:可行。技术选型无不可验证的赌注,实验设计有基线有指标,风险预案覆盖了答辩高频问题。主要隐患是总工作量贴着上限,需按 R1 预设降级顺序,并将 R2、R3 两个方法论缺口在实验设计阶段补齐。")
-H.para(doc, "调研文档:可行且价值高于预期。其空白判定与三级对照设计是论文创新性论证的支柱;但能力对照表停留在 README 级(R7)是唯一致命缺口,必须在开题前完成源码级核验。")
-H.para(doc, "综合结论:两份文档合并构成一个可行的本科毕业设计方案,且是一篇有真实增量的论文。可行性瓶颈不在技术,而在执行时间与两处未闭环的证据链。按表 5-1 的优先级完成整改后,本评审认为该方案达到可提交开题的水平。")
-
-doc.save("在线考试系统两方案可行性评审报告.docx")
-print("saved")

+ 0 - 313
docx_helpers.py

@@ -1,313 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Shared DOCX helpers for the reference document."""
-import docx.opc.constants
-from docx import Document
-from docx.shared import Pt, Cm, RGBColor
-from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
-from docx.enum.table import WD_TABLE_ALIGNMENT
-from docx.oxml.ns import qn
-from docx.oxml import OxmlElement
-
-BLACK = RGBColor(0, 0, 0)
-GREY = RGBColor(0x40, 0x40, 0x40)
-
-def set_cn_font(target, name_cn, name_ascii=None, size=None, bold=None, color=None):
-    f = target.font
-    f.name = name_ascii or name_cn
-    if size is not None:
-        f.size = Pt(size)
-    if bold is not None:
-        f.bold = bold
-    if color is not None:
-        f.color.rgb = color
-    el = target.element
-    rPr = el.get_or_add_rPr()
-    rFonts = rPr.find(qn("w:rFonts"))
-    if rFonts is None:
-        rFonts = OxmlElement("w:rFonts")
-        rPr.append(rFonts)
-    rFonts.set(qn("w:ascii"), name_ascii or name_cn)
-    rFonts.set(qn("w:hAnsi"), name_ascii or name_cn)
-    rFonts.set(qn("w:eastAsia"), name_cn)
-
-def init_doc():
-    doc = Document()
-    sec = doc.sections[0]
-    sec.page_width = Cm(21.0)
-    sec.page_height = Cm(29.7)
-    sec.top_margin = Cm(2.54)
-    sec.bottom_margin = Cm(2.54)
-    sec.left_margin = Cm(2.8)
-    sec.right_margin = Cm(2.8)
-
-    normal = doc.styles["Normal"]
-    set_cn_font(normal, "宋体", "Times New Roman", 10.5)
-    pf = normal.paragraph_format
-    pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
-    pf.space_before = Pt(0)
-    pf.space_after = Pt(4)
-
-    for name, size, before, after in (("Heading 1", 16, 18, 8),
-                                      ("Heading 2", 13, 14, 6),
-                                      ("Heading 3", 11, 10, 4)):
-        st = doc.styles[name]
-        set_cn_font(st, "黑体", "Times New Roman", size, True, BLACK)
-        st.paragraph_format.space_before = Pt(before)
-        st.paragraph_format.space_after = Pt(after)
-        st.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
-
-    # let Word refresh the TOC field when the document is opened
-    settings = doc.settings.element
-    uf = OxmlElement("w:updateFields")
-    uf.set(qn("w:val"), "true")
-    settings.append(uf)
-
-    footer = sec.footer
-    fp = footer.paragraphs[0]
-    fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
-    run = fp.add_run()
-    f1 = OxmlElement("w:fldChar"); f1.set(qn("w:fldCharType"), "begin")
-    it = OxmlElement("w:instrText"); it.set(qn("xml:space"), "preserve"); it.text = " PAGE "
-    f2 = OxmlElement("w:fldChar"); f2.set(qn("w:fldCharType"), "end")
-    run._r.append(f1); run._r.append(it); run._r.append(f2)
-    set_cn_font(run, "宋体", "Times New Roman", 9)
-    return doc
-
-def add_hyperlink(paragraph, url, text, size=8.5, bold=False):
-    part = paragraph.part
-    r_id = part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True)
-    link = OxmlElement("w:hyperlink")
-    link.set(qn("r:id"), r_id)
-    r = OxmlElement("w:r")
-    rPr = OxmlElement("w:rPr")
-    rFonts = OxmlElement("w:rFonts")
-    rFonts.set(qn("w:ascii"), "Consolas")
-    rFonts.set(qn("w:hAnsi"), "Consolas")
-    rFonts.set(qn("w:eastAsia"), "宋体")
-    rPr.append(rFonts)
-    if bold:
-        rPr.append(OxmlElement("w:b"))
-    color = OxmlElement("w:color"); color.set(qn("w:val"), "1F4E79"); rPr.append(color)
-    u = OxmlElement("w:u"); u.set(qn("w:val"), "single"); rPr.append(u)
-    sz = OxmlElement("w:sz"); sz.set(qn("w:val"), str(int(size * 2))); rPr.append(sz)
-    r.append(rPr)
-    t = OxmlElement("w:t"); t.text = text; t.set(qn("xml:space"), "preserve")
-    r.append(t)
-    link.append(r)
-    paragraph._p.append(link)
-
-def shade(cell, hexcolor):
-    tcPr = cell._tc.get_or_add_tcPr()
-    shd = OxmlElement("w:shd")
-    shd.set(qn("w:val"), "clear")
-    shd.set(qn("w:color"), "auto")
-    shd.set(qn("w:fill"), hexcolor)
-    tcPr.append(shd)
-
-def cell_margins(table, top=55, bottom=55, left=85, right=85):
-    tblPr = table._tbl.tblPr
-    mar = OxmlElement("w:tblCellMar")
-    for tag, val in (("top", top), ("left", left), ("bottom", bottom), ("right", right)):
-        el = OxmlElement("w:" + tag)
-        el.set(qn("w:w"), str(val))
-        el.set(qn("w:type"), "dxa")
-        mar.append(el)
-    tblPr.append(mar)
-
-def repeat_header(row):
-    trPr = row._tr.get_or_add_trPr()
-    el = OxmlElement("w:tblHeader"); el.set(qn("w:val"), "true")
-    trPr.append(el)
-
-def cant_split(row):
-    trPr = row._tr.get_or_add_trPr()
-    el = OxmlElement("w:cantSplit"); el.set(qn("w:val"), "true")
-    trPr.append(el)
-
-def set_cell_text(cell, text, size=8.5, bold=False, align=None, color=None):
-    cell.text = ""
-    p = cell.paragraphs[0]
-    p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
-    p.paragraph_format.space_before = Pt(1)
-    p.paragraph_format.space_after = Pt(1)
-    if align is not None:
-        p.alignment = align
-    run = p.add_run(text)
-    set_cn_font(run, "宋体", "Times New Roman", size, bold, color)
-
-def make_table(doc, headers, rows, widths, link_col=None, font=8.5, head_font=8.5):
-    table = doc.add_table(rows=1, cols=len(headers))
-    table.style = "Table Grid"
-    table.alignment = WD_TABLE_ALIGNMENT.CENTER
-    table.autofit = False
-    cell_margins(table)
-    hdr = table.rows[0]
-    repeat_header(hdr)
-    cant_split(hdr)
-    for i, h in enumerate(headers):
-        c = hdr.cells[i]
-        c.width = Cm(widths[i])
-        shade(c, "D9E2F3")
-        set_cell_text(c, h, head_font, True, WD_ALIGN_PARAGRAPH.CENTER)
-    for r in rows:
-        row = table.add_row()
-        cant_split(row)
-        for i, val in enumerate(r):
-            c = row.cells[i]
-            c.width = Cm(widths[i])
-            if i == 0:
-                cell_no_shade = c
-            if link_col is not None and i == link_col:
-                c.text = ""
-                p = c.paragraphs[0]
-                p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
-                p.paragraph_format.space_before = Pt(1)
-                p.paragraph_format.space_after = Pt(1)
-                add_hyperlink(p, val[1], val[0], size=font)
-            else:
-                set_cell_text(c, val, font)
-    for row in table.rows:
-        for i, c in enumerate(row.cells):
-            c.width = Cm(widths[i])
-    return table
-
-def para(doc, text, size=10.5, bold=False, first_indent=True, space_after=4,
-         align=None, color=None, left=0.0, line=1.5, font_cn="宋体"):
-    p = doc.add_paragraph()
-    pf = p.paragraph_format
-    pf.space_after = Pt(space_after)
-    if left:
-        pf.left_indent = Cm(left)
-    if line == 1.5:
-        pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
-    else:
-        pf.line_spacing = line
-    if align is not None:
-        p.alignment = align
-    if first_indent:
-        pf.first_line_indent = Pt(21)
-    run = p.add_run(text)
-    set_cn_font(run, font_cn, "Times New Roman", size, bold, color)
-    return p
-
-def rich(doc, chunks, size=10.5, first_indent=True, space_after=4, left=0.0):
-    """chunks: list of (text, bold) tuples, all one paragraph."""
-    p = doc.add_paragraph()
-    pf = p.paragraph_format
-    pf.space_after = Pt(space_after)
-    pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
-    if left:
-        pf.left_indent = Cm(left)
-    if first_indent:
-        pf.first_line_indent = Pt(21)
-    for text, bold in chunks:
-        run = p.add_run(text)
-        set_cn_font(run, "宋体" if not bold else "黑体", "Times New Roman", size, bold, None)
-    return p
-
-def bullet(doc, text, size=10.5, level=0, space_after=2):
-    p = doc.add_paragraph()
-    pf = p.paragraph_format
-    pf.left_indent = Cm(0.75 + 0.6 * level)
-    pf.first_line_indent = Cm(-0.45)
-    pf.space_after = Pt(space_after)
-    pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
-    run = p.add_run("· " if level == 0 else "– ")
-    set_cn_font(run, "宋体", "Times New Roman", size)
-    run2 = p.add_run(text)
-    set_cn_font(run2, "宋体", "Times New Roman", size)
-    return p
-
-def bullet_rich(doc, chunks, size=10.5, level=0, space_after=2):
-    p = doc.add_paragraph()
-    pf = p.paragraph_format
-    pf.left_indent = Cm(0.75 + 0.6 * level)
-    pf.first_line_indent = Cm(-0.45)
-    pf.space_after = Pt(space_after)
-    pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
-    run = p.add_run("· " if level == 0 else "– ")
-    set_cn_font(run, "宋体", "Times New Roman", size)
-    for text, bold in chunks:
-        r = p.add_run(text)
-        set_cn_font(r, "宋体" if not bold else "黑体", "Times New Roman", size, bold, None)
-    return p
-
-def heading(doc, text, level=1):
-    h = doc.add_heading(level=level)
-    h.paragraph_format.space_before = Pt({1: 18, 2: 14, 3: 10}[level])
-    h.paragraph_format.space_after = Pt({1: 8, 2: 6, 3: 4}[level])
-    run = h.add_run(text)
-    set_cn_font(run, "黑体", "Times New Roman", {1: 16, 2: 13, 3: 11}[level], True, BLACK)
-    return h
-
-def caption(doc, text):
-    p = doc.add_paragraph()
-    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-    p.paragraph_format.space_before = Pt(8)
-    p.paragraph_format.space_after = Pt(3)
-    p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
-    run = p.add_run(text)
-    set_cn_font(run, "黑体", "Times New Roman", 9, True)
-    return p
-
-def note(doc, text):
-    p = doc.add_paragraph()
-    pf = p.paragraph_format
-    pf.space_before = Pt(4)
-    pf.space_after = Pt(10)
-    pf.left_indent = Cm(0.2)
-    pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
-    run = p.add_run(text)
-    set_cn_font(run, "楷体", "Times New Roman", 9.5, False, GREY)
-    return p
-
-def quote(doc, lines):
-    for ln in lines:
-        p = doc.add_paragraph()
-        pf = p.paragraph_format
-        pf.left_indent = Cm(0.9)
-        pf.right_indent = Cm(0.4)
-        pf.space_after = Pt(3)
-        pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
-        run = p.add_run(ln)
-        set_cn_font(run, "楷体", "Times New Roman", 10)
-    return
-
-def link_para(doc, pairs, size=9.5, left=0.75, space_after=2):
-    """pairs: list of (label, url) rendered as 'label  url' with url clickable."""
-    p = doc.add_paragraph()
-    pf = p.paragraph_format
-    pf.left_indent = Cm(left)
-    pf.first_line_indent = Cm(-0.45)
-    pf.space_after = Pt(space_after)
-    pf.line_spacing_rule = WD_LINE_SPACING.SINGLE
-    r = p.add_run("· ")
-    set_cn_font(r, "宋体", "Times New Roman", size)
-    for label, url in pairs:
-        rl = p.add_run(label + "  ")
-        set_cn_font(rl, "宋体", "Times New Roman", size)
-        add_hyperlink(p, url, url.replace("https://", "").replace("http://", ""), size=size - 0.5)
-    return p
-
-def toc(doc):
-    hint = doc.add_paragraph()
-    hint.paragraph_format.space_after = Pt(6)
-    hr = hint.add_run("(本页为自动目录域。用 Word 打开时若提示更新域,选择「是」即可生成带页码的目录;也可按 Ctrl+A 后按 F9 手动更新。)")
-    set_cn_font(hr, "楷体", "Times New Roman", 9, False, GREY)
-
-    p = doc.add_paragraph()
-    run = p.add_run()
-    f1 = OxmlElement("w:fldChar"); f1.set(qn("w:fldCharType"), "begin"); f1.set(qn("w:dirty"), "true")
-    it = OxmlElement("w:instrText"); it.set(qn("xml:space"), "preserve")
-    it.text = r' TOC \o "1-2" \h \z \u '
-    f2 = OxmlElement("w:fldChar"); f2.set(qn("w:fldCharType"), "separate")
-    f3 = OxmlElement("w:fldChar"); f3.set(qn("w:fldCharType"), "end")
-    run._r.append(f1); run._r.append(it); run._r.append(f2)
-    ph = OxmlElement("w:r")
-    pt = OxmlElement("w:t")
-    pt.text = "目录将在此处生成"
-    pt.set(qn("xml:space"), "preserve")
-    ph.append(pt)
-    run._r.append(ph)
-    run._r.append(f3)
-    return p

+ 0 - 17
dump_docx.py

@@ -1,17 +0,0 @@
-# -*- coding: utf-8 -*-
-import zipfile, re, json, sys, io
-sys.stdout.reconfigure(encoding="utf-8")
-p = r"C:\Users\aroun\Desktop\在线考试系统\在线考试系统技术文案(方案B).docx"
-z = zipfile.ZipFile(p)
-xml = z.read("word/document.xml").decode("utf-8")
-paras = re.findall(r"<w:p[ >].*?</w:p>", xml, re.S)
-out = []
-for pa in paras:
-    texts = re.findall(r"<w:t[^>]*>(.*?)</w:t>", pa, re.S)
-    t = "".join(texts)
-    t = t.replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", '"')
-    m = re.search(r'<w:pStyle w:val="([^"]+)"', pa)
-    out.append({"style": m.group(1) if m else "", "text": t})
-with open(r"C:\Users\aroun\Desktop\在线考试系统\_paras.json", "w", encoding="utf-8") as f:
-    json.dump(out, f, ensure_ascii=False, indent=0)
-print("paras", len(out))

+ 0 - 10
dump_heads.py

@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-import json, re, sys
-sys.stdout.reconfigure(encoding="utf-8")
-paras = json.load(open(r"C:\Users\aroun\Desktop\在线考试系统\_paras.json", encoding="utf-8"))
-pat = re.compile(r"^([一二三四五六七八九十]+、|第[一二三四五六七八九十\d]+[章节]|\d+(\.\d+)*\s|([一二三四五六七八九十]+))")
-for i,p in enumerate(paras):
-    t = p["text"].strip()
-    if not t: continue
-    if pat.match(t) and len(t) < 60:
-        print(i, "|", t)

+ 0 - 31
extract_plan_sources.py

@@ -1,31 +0,0 @@
-# -*- coding: utf-8 -*-
-from pathlib import Path
-from docx import Document
-
-ROOT = Path(__file__).resolve().parent
-FILES = [
-    "在线考试系统技术文案(方案B).docx",
-    "在线考试系统两方案可行性评审报告.docx",
-]
-for name in FILES:
-    doc = Document(ROOT / name)
-    out = []
-    out.append(f"SOURCE: {name}")
-    for block in doc.element.body.iterchildren():
-        if block.tag.endswith("}p"):
-            for p in doc.paragraphs:
-                if p._p is block:
-                    if p.text.strip():
-                        out.append(f"[{p.style.name}] {p.text}")
-                    break
-        elif block.tag.endswith("}tbl"):
-            for table in doc.tables:
-                if table._tbl is block:
-                    out.append("[TABLE]")
-                    for row in table.rows:
-                        out.append(" | ".join(cell.text.replace("\n", " / ") for cell in row.cells))
-                    out.append("[/TABLE]")
-                    break
-    target = ROOT / (Path(name).stem + "_提取.txt")
-    target.write_text("\n".join(out), encoding="utf-8")
-    print(f"{target.name}: {len(out)} blocks")

+ 0 - 135
optimized_plan_content.py

@@ -1,135 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Content model for the revised online examination implementation plan."""
-
-XZS_SQL = "https://github.com/mindskip/xzs-mysql/blob/master/sql/xzs-mysql.sql"
-XZS_SERVICE = "https://github.com/mindskip/xzs-mysql/blob/master/source/xzs/src/main/java/com/mindskip/xzs/service/impl/ExamPaperQuestionCustomerAnswerServiceImpl.java"
-EXAM_ANSWER = "https://github.com/Alanosy/exam-platform/blob/master/exam-back/ruoyi-modules/ruoyi-exam-answer/src/main/java/org/dromara/exam/answer/domain/ExamAnswer.java"
-PROCTOR_EVENT = "https://github.com/Alanosy/exam-platform/blob/master/exam-back/ruoyi-modules/ruoyi-exam-proctor/src/main/java/org/dromara/exam/proctor/domain/ProctorEvent.java"
-PROCTOR_SESSION = "https://github.com/Alanosy/exam-platform/blob/master/exam-back/ruoyi-modules/ruoyi-exam-proctor/src/main/java/org/dromara/exam/proctor/domain/ProctorSession.java"
-DEXIE = "https://github.com/dexie/Dexie.js"
-SHEDLOCK = "https://github.com/lukas-krecan/ShedLock"
-TESTCONTAINERS = "https://github.com/testcontainers/testcontainers-java"
-ORTOOLS = "https://github.com/google/or-tools"
-
-REVIEW_ROWS = [
-    ["R1 工作量", "锁定 P0/P1/P2 边界;问题一、二及 E1/E2 为不可砍核心,问题三和 E3/E6 设降级路径。", "已修正方案;里程碑须执行"],
-    ["R2 权重同源", "先验权重+独立评估;若调权,只用训练集并冻结后评估;不调测试标签。", "已修正设计;待实验"],
-    ["R3 正常样本", "正常轨迹来自知情自愿的模拟考试;脚本只构造作弊/异常,不作为正常类的唯一来源。", "待组织采集;按资源条件执行"],
-    ["R4 客户端", "仅支持桌面 Chromium 浏览器;IndexedDB 不可用或容量不足时禁止开始离线考试,不承诺自动降级。", "已修正范围;待兼容测试"],
-    ["R5 排期/指标", "统一 13 周;难度偏差以卡方为主,KL 仅在平滑处理并明确 epsilon 时辅助报告。", "已修正"],
-    ["R6 快照自扰", "仅写脏会话,合并队列,批量落盘;与无快照、每会话直写分组比较。", "已修正设计;待压测"],
-    ["R7 源码证据", "已核对学之思答卷表/保存服务、砚考答卷/监考实体;将结论限定为所读实体和保存路径。", "关键源码核验已完成"],
-    ["R8 学术检索", "把 GitHub 未见实现与学术空白严格区分;按三条研究线完成定向检索后再写创新范围。", "待完成,不作为已实现结论"],
-    ["R9 求解上界", "JDK 17 + or-tools CP-SAT 验证限半天;失败则保留随机/同行/本文算法对照。", "待时间盒验证"],
-    ["R10 数据时效", "引用仓库与开源结论均标注检索/核验日期;论文定稿前复核链接与仓库版本。", "本文已标日期;定稿前复核"],
-]
-
-SCOPE_ROWS = [
-    ["P0 必交付", "模块化单体;题库/考试/答题/交卷/客观判分;三种组卷算法;本地草稿、重放、幂等、冲突处理;E1/E2。", "从开考到成绩的可复现闭环;核心故障测试通过"],
-    ["P1 应交付", "轻量行为事件、规则风险分与人工复核;outbox+对账;教师配置与基础审计;E4/E5。", "与 P0 不耦合;样本和评价协议完整"],
-    ["P2 可裁剪", "主观题双评仲裁、监控大屏、500 并发实测、完整安全测试、or-tools 实测、界面美化。", "降级仅缩减范围,不伪造指标或数据"],
-]
-
-DECISION_ROWS = [
-    ["模块化单体而非微服务", "三个核心问题可单库闭环;减少部署/分布式一致性成本,把时间留给可复现实验。", "保留api/internal边界;不宣称可零成本拆分"],
-    ["当前答案+追加操作日志", "唯一当前答案防并发分叉;操作日志提供幂等、恢复、冲突证据,不混用版本链与主记录。", "answer_item与answer_operation分工"],
-    ["服务端同步游标+独立op_id", "op_id识别操作,服务端游标只排序已受理事件;按题base_version防止过期写覆盖新值。", "不是客户端时间戳或大者胜出"],
-    ["先验固定风险权重", "避免在同一批样本调参又评估;以信号有效性为目标而非产品级识别率。", "样本少时只作探索性结论"],
-    ["不引入视觉AI/RocketMQ", "与轻量规则、可解释、低部署成本的目标一致;砍掉无关依赖而不砍核心问题。", "后续扩展不影响论文核心闭环"],
-]
-
-MODULE_ROWS = [
-    ["user", "账号、角色、机构、JWT 会话", "权限校验;不暴露密码/令牌"],
-    ["question", "题目、知识点、难度与题目标签", "只读供给组卷;保留题干/答案来源与版本"],
-    ["paper", "组卷规则、求解、试题快照与发布", "paper_rule JSON 经 Schema 校验;发布后不可回改已开考卷"],
-    ["exam", "场次、考生会话、答案同步、交卷", "唯一拥有当前答案写入口;游标分配与状态迁移"],
-    ["grading", "客观判分、主观评分(P2)、重试", "读取交卷快照;不直接修改答案"],
-    ["score", "成绩、发布、复核、对账", "通过事件/API 获取判分结果;版本化成绩"],
-    ["monitor", "行为事件、风险评分、复核与审计", "可疑提示,不作自动作弊定性或自动零分"],
-    ["common", "错误码、审计接口、幂等组件、事件封装", "不含业务规则;不形成跨模块公共数据访问"],
-]
-
-TABLE_ROWS = [
-    ["user", "id、account、role、org_id、password_hash", "密码仅保存哈希;按账号唯一"],
-    ["question", "id、type、stem、difficulty、answer、version", "教师维护;组卷读取发布版本"],
-    ["knowledge_point", "id、name、parent_id", "树形知识点"],
-    ["question_knowledge", "question_id、knowledge_point_id", "联合唯一;多对多关联"],
-    ["paper", "id、name、total_score、status、version", "发布后形成试卷快照"],
-    ["paper_rule", "paper_id、constraint_json、strength、schema_version", "配置版本与校验结果可追溯"],
-    ["paper_question", "paper_id、question_id、score、sequence", "联合唯一;快照分值/题序"],
-    ["exam", "id、paper_id、start_at、end_at、policy_json", "考试策略随场次固化"],
-    ["exam_session", "id、exam_id、user_id、status、next_seq", "(exam_id,user_id) 唯一;next_seq 分配服务端游标;状态迁移用会话行锁,不另设version"],
-    ["answer_item", "session_id、question_id、answer_json、version、last_op_id", "(session_id,question_id) 唯一,仅存当前答案"],
-    ["answer_operation", "session_id、server_seq、op_id、actor_id、client_seq、question_id、base_version、result_version、status、payload_hash", "(session_id,server_seq)、op_id、(session_id,actor_id,client_seq) 唯一;server_seq可空"],
-    ["answer_draft_log", "session_id、op_id、conflict_type、client_version、server_version、payload_hash", "保留脱敏冲突证据;原文留在客户端待处置"],
-    ["submission", "id、session_id、submit_token、request_hash、result_json、created_at", "session_id与(session_id,submit_token)均唯一;同token不同请求体拒绝"],
-    ["score", "id、session_id、grading_version、total_score、status", "(session_id,grading_version)唯一;复核生成新版本"],
-    ["cheat_event", "id、session_id、type、evidence_ref、observed_at、received_at", "接收时间独立;原始事件追加不可改"],
-    ["outbox", "id、event_type、aggregate_id、payload、status、retry_count、next_retry_at", "业务事务内写入;支持重试/死信状态"],
-    ["audit_log", "id、actor_id、action、object_id、detail_json、created_at", "处置与敏感操作留痕"],
-]
-
-API_ROWS = [
-    ["POST /api/exam-sessions", "创建/恢复本人场次", "校验报名与时间;返回session_id、当前游标、规则快照"],
-    ["POST /api/exam-sessions/{id}/operations:batch", "幂等批量重放答案操作", "返回逐op状态、最新版本与当前同步游标"],
-    ["GET /api/exam-sessions/{id}/sync?afterSeq=n", "拉取缺失的服务端操作与状态", "只返回本人数据;含权威next_seq和考试状态"],
-    ["POST /api/exam-sessions/{id}/submit", "幂等交卷并冻结答案", "(session_id,submit_token)+request_hash;返回稳定submission_id/状态"],
-    ["GET /api/exam-sessions/{id}/submission", "恢复交卷结果", "断线后按会话查询,不新生成token"],
-    ["POST /api/exam-sessions/{id}/proctor-events", "接收行为事件", "限制频率/大小;校验类型、时间、来源;事件与风险分分离"],
-    ["POST /api/papers:solve", "按约束求解组卷", "记录rule/schema/version、seed、参数、状态与耗时"],
-]
-
-STATE_ROWS = [
-    ["服务端:NOT_STARTED→ANSWERING", "身份、报名、时间、存储检查通过", "创建/恢复会话;同步游标从0开始"],
-    ["服务端:ANSWERING→SUBMITTING", "考生交卷或到时收卷", "将受理截止时间固定到事务开始时刻;拒绝新到达答案"],
-    ["服务端:SUBMITTING→SUBMITTED", "截止时间前已受理事务提交;答案与试卷快照冻结成功", "同事务写submission与outbox;返回稳定凭证"],
-    ["服务端:SUBMITTING→SUBMIT_FAILED_RETRYABLE", "暂时性资源/网络失败", "回滚本次交卷写入;原token重试或查询,不丢本地草稿"],
-    ["服务端:SUBMITTED→GRADING→SCORED", "判分与成绩生成", "失败可重试;同一事件不重复产生成绩版本"],
-    ["服务端:SCORED→REVIEWING→SCORED", "有权限的人工复核", "记录原因、成绩版本与审计链"],
-    ["客户端:ONLINE↔OFFLINE_DIRTY→REPLAYING", "网络变化;本地有待同步操作", "仅是同步状态,不直接改服务端考试状态"],
-    ["客户端:REPLAYING→ONLINE/CONFLICT_REQUIRED", "全部受理或存在同题版本冲突", "冲突原文保留本地,须考生/教师按权限处置"],
-    ["客户端:SUBMIT_UNKNOWN→SUBMITTED/ANSWERING", "交卷响应丢失后查询权威结果", "同token重试或查询,不盲目再次交卷"],
-]
-
-EXPERIMENT_ROWS = [
-    ["E1 组卷", "固定题库/约束/种子;3算法×3强度×各100次;预检区分无可行解与求解失败。", "成功率、耗时、总分误差、知识点覆盖偏差、难度卡方;KL仅平滑辅助。", "随机重试、同行遗传算法;可选CP-SAT上界", "P0;先小样本打通,参数一次冻结"],
-    ["E2 一致性", "断网30秒/5分钟、重启、重放同批、乱序/重复seq、交卷竞争;记录全量操作日志。", "操作可追踪率、答案语义正确率、会话语义正确率、非预期重复数、恢复时长。", "无IndexedDB草稿;无幂等保护(仅隔离测试库)", "P0;自动化断言,不以录屏代替"],
-    ["E3 性能", "记录CPU/内存/JDK/数据库/浏览器;按100→250→500逐步加压,读写混合。", "P50/P95/P99、错误率、TPS、数据库连接/锁等待;快照写次数。", "无缓存、逐会话同步快照、批量脏快照", "P2;资源不足则报告实测点,不外推为已达标"],
-    ["E4 风险识别", "真实正常轨迹+脚本异常;先验等权/固定权重;按考生划分训练与评估,避免同人泄漏。", "召回率、误报率、PR-AUC/单信号AUC、置信区间;阈值与混淆矩阵。", "仅切屏规则、等权基线", "P1;资源不足降级为信号有效性探索,不称实用产品"],
-    ["E5 成绩一致性", "测试环境注入判分异常、重复事件、延迟/丢失投递;事务与对账日志关联。", "不一致检出率、修复率、端到端时延、重复处理结果一致性。", "关闭对账;同事件重放", "P1;不得破坏真实成绩数据"],
-    ["E6 安全", "水平/垂直越权、伪造op/session/token、参数绑定、登出与令牌过期、存储失败。", "用例通过率;敏感数据暴露清单;异常后可恢复性。", "按OWASP基础用例", "P2;最低保留越权与伪造请求用例"],
-]
-
-SCHEDULE_ROWS = [
-    ["1", "证据与范围冻结", "文献检索计划、源码笔记、P0接口/数据字典", "评审风险责任到人;不扩需求"],
-    ["2", "工程基线", "JDK17/Boot3/MyBatis-Plus适配;Compose与CI;模块边界检查", "环境一键启动"],
-    ["3", "薄链路", "登录→建卷→考试→答题→交卷→客观成绩", "必须端到端可跑;未通则停止扩展"],
-    ["4–5", "问题一", "约束Schema、预检、三算法、规则配置与实验记录", "E1数据与图;固定种子可复跑"],
-    ["6–7", "问题二", "Dexie操作日志、批量同步、服务端唯一约束、幂等交卷", "E2核心场景通过;先正确再优化"],
-    ["8", "业务闭环", "outbox、客观判分、成绩与对账", "E5最小实验;无中间件扩展"],
-    ["9", "风险识别(可降级)", "事件采集、固定规则、复核;招募与知情流程", "仅采集获准数据;先冻结评价协议"],
-    ["10", "核心补测", "E1/E2/E5复测;故障与接口回归", "不允许只保留成功截图"],
-    ["11", "P1/P2实验", "E4;按余量做E3/E6", "按实测报告,不预设500并发达标"],
-    ["12", "论文与清理", "初稿、实验归档、许可证/隐私/局限说明", "正文结论能追溯数据与脚本"],
-    ["13", "验收与答辩", "回归、录屏、一键演示、论文定稿", "离线演示与故障脚本均可运行"],
-]
-
-RISK_ROWS = [
-    ["答案冲突协议错误", "唯一约束+事务+状态机先行;Testcontainers覆盖重放/乱序/交卷竞争", "任何核心用例失败即停止性能优化"],
-    ["约束无可行解", "供给量与总分/题型/知识点预检;记录不可行而非记为算法超时", "无解用例单列"],
-    ["算法调参失控", "一组固定参数+一组消融;隐藏最终评估用例", "时间耗尽则减少算法参数,不删基线"],
-    ["正常样本招募不足", "提前招募;脚本与真实轨迹分标签", "不足则缩小E4结论,不冒充真实误报率"],
-    ["断网演示失败", "DevTools Offline、服务重启脚本与已保存数据快照", "先保证自动化结果,再准备录屏"],
-    ["性能资源不足", "逐级加压、限制测试数据量、分离快照与业务写", "报告环境与瓶颈;不外推未测并发"],
-    ["隐私或许可证风险", "最小采集、知情同意、哈希/脱敏;AGPL/无许可代码只读不复制", "复核清单不通过则不发布"],
-]
-
-ACCEPT_ROWS = [
-    ["架构", "单应用可启动;模块依赖可检查;无跨模块Mapper;核心业务不依赖Redis可用", "启动日志、依赖检查、模块测试"],
-    ["组卷", "三算法同接口;规则与种子可复现;不可行输入有明确输出;E1可复跑", "脚本、配置、原始CSV、图表"],
-    ["答案一致性", "一道题一个当前答案;重复op不重复生效;过期写不覆盖新值;交卷后拒绝写入", "自动化断言+数据库查询+故障日志"],
-    ["成绩", "交卷/判分/成绩状态可追踪;重复事件不重复算分;对账能发现并修复测试异常", "E5记录与审计链"],
-    ["风险识别", "风险分可解释、事件可复核;评估与调参隔离;无自动作弊定性", "规则版本、样本划分、混淆矩阵"],
-    ["工程", "桌面浏览器流程可用;异常有错误码;核心路径自动化;部署和备份说明完整", "测试报告、Compose、README"],
-    ["论文", "目标/结果分开;报告负结果;限制与数据来源明确;链接、日期、许可证可追溯", "文稿、数据清单、来源清单"],
-]

+ 0 - 196
sections_a.py

@@ -1,196 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Part A: cover, abstract, TOC, chapter 1, chapter 2."""
-from docx.shared import Pt, Cm, RGBColor
-from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
-from docx_helpers import (init_doc, set_cn_font, make_table, para, rich, bullet,
-                          bullet_rich, heading, caption, note, quote, link_para,
-                          toc, add_hyperlink, GREY, BLACK)
-from docx.oxml.ns import qn
-from docx.oxml import OxmlElement
-
-GH = "https://github.com/"
-
-def cover(doc):
-    doc.add_paragraph()
-    p = doc.add_paragraph()
-    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-    r = p.add_run("在线考试系统技术文案(方案 B)配套材料")
-    set_cn_font(r, "楷体", "Times New Roman", 12, False, GREY)
-
-    doc.add_paragraph()
-    p = doc.add_paragraph()
-    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-    p.paragraph_format.space_after = Pt(6)
-    r = p.add_run("在线考试系统开源方案调研")
-    set_cn_font(r, "黑体", "Times New Roman", 26, True, BLACK)
-
-    p = doc.add_paragraph()
-    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-    p.paragraph_format.space_after = Pt(18)
-    r = p.add_run("与论文写作参考")
-    set_cn_font(r, "黑体", "Times New Roman", 26, True, BLACK)
-
-    p = doc.add_paragraph()
-    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-    p.paragraph_format.space_after = Pt(26)
-    r = p.add_run("——面向「多约束组卷、断网续考答案一致性、作弊识别」三个研究点的 GitHub 生态梳理")
-    set_cn_font(r, "楷体", "Times New Roman", 12, False, GREY)
-
-    rows = [("对应方案", "《在线考试系统中的组卷优化、答案一致性与作弊识别方法研究》(方案 B:Spring Boot 模块化单体)"),
-            ("调研对象", "GitHub 公开仓库(含同类考试系统、组卷求解器、离线同步引擎、在线监考项目、测评平台与标准)"),
-            ("数据口径", "GitHub REST API 实时查询;星标数与最后提交时间以 2026-11-09 复核值为准"),
-            ("文档性质", "文献与开源现状综述素材;可直接裁剪进论文第 1 章、第 2 章、第 4 章与第 5 章"),
-            ("统计规模", "候选仓库 60 余个,逐仓核验 29 个,重点精读 6 个")]
-    t = doc.add_table(rows=0, cols=2)
-    t.autofit = False
-    for k, v in rows:
-        row = t.add_row()
-        c0, c1 = row.cells
-        c0.width = Cm(3.0)
-        c1.width = Cm(12.2)
-        c0.text = ""
-        c1.text = ""
-        pa0 = c0.paragraphs[0]
-        pa0.paragraph_format.space_after = Pt(3)
-        r0 = pa0.add_run(k)
-        set_cn_font(r0, "黑体", "Times New Roman", 10, True, BLACK)
-        pa1 = c1.paragraphs[0]
-        pa1.paragraph_format.space_after = Pt(3)
-        r1 = pa1.add_run(v)
-        set_cn_font(r1, "宋体", "Times New Roman", 10)
-    doc.add_paragraph()
-    p = doc.add_paragraph()
-    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-    r = p.add_run("2026 年 11 月")
-    set_cn_font(r, "宋体", "Times New Roman", 12)
-
-def abstract(doc):
-    doc.add_page_break()
-    p = doc.add_paragraph()
-    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-    p.paragraph_format.space_after = Pt(14)
-    r = p.add_run("摘 要")
-    set_cn_font(r, "黑体", "Times New Roman", 16, True, BLACK)
-
-    para(doc, "本文档是对 GitHub 开源生态中与在线考试系统相关项目的系统性梳理,服务于本科毕业论文《在线考试系统中的组卷优化、答案一致性与作弊识别方法研究》(方案 B:Spring Boot 模块化单体)的开题论证、相关工作撰写与技术选型。调研覆盖 60 余个候选仓库,逐仓核验 29 个,重点精读 6 个,按「同类系统—组卷求解—离线同步—作弊识别—平台与标准」五条线索组织,并给出每个项目与三个研究点的对应关系。", first_indent=True)
-
-    para(doc, "调研得到三点关键结论。第一,国内可用的同类系统已相当成熟:学之思(xzs-mysql,3604 星)与「砚考」(exam-platform,886 星)分别在业务闭环完整度与工程现代化程度(Spring Boot 3.5 + JDK 17/21 + Vue 3)上处于领先,但两者的组卷都停留在「按题型、难度、分值随机抽题」的规则筛选层面,不具备约束满足求解能力;砚考的防作弊以 13 类行为的计数与阈值强制交卷为主,未建立风险分模型。第二,针对断网续考的检索在所考察的 topic 范围内未发现任何以离线答题、断网续答或冲突合并为核心的开源考试系统,现有实现普遍止步于「逐题自动保存 + 断点续答」,客户端与服务端之间的版本冲突没有公开的通用方案,本研究的唯一索引幂等写入、last_seq 增量同步与交卷令牌三项设计因此具备可论证的空白性。第三,三个研究点在开源生态中呈现明显的不均衡:作弊识别方向项目数量最多但几乎全部采用摄像头视觉 AI 路线,与本研究「不做深度模型、验证轻量行为信号有效性」的定位差异显著;组卷方向可复用的仅是通用求解器(or-tools 的 CP-SAT、Timefold 的局部搜索),没有开箱可用的考试组卷引擎。", first_indent=True)
-
-    para(doc, "基于以上结论,文档给出了分研究点的引用建议、可作为实验基线的实现清单、许可证风险提示,以及标准与学术检索的补充方向。总体判断是:本研究的技术栈选择已被成熟项目验证,无需自证可行性;而三个研究点在开源生态中的空白与不均衡,恰好构成本文创新性论证的事实基础。", first_indent=True)
-
-    p = doc.add_paragraph()
-    p.paragraph_format.space_before = Pt(14)
-    r = p.add_run("关键词:")
-    set_cn_font(r, "黑体", "Times New Roman", 10.5, True, BLACK)
-    r2 = p.add_run("在线考试系统;开源调研;多约束组卷;断网续考;最终一致性;作弊识别;模块化单体")
-    set_cn_font(r2, "宋体", "Times New Roman", 10.5)
-
-def toc_page(doc):
-    doc.add_page_break()
-    p = doc.add_paragraph()
-    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
-    p.paragraph_format.space_after = Pt(12)
-    r = p.add_run("目 录")
-    set_cn_font(r, "黑体", "Times New Roman", 16, True, BLACK)
-    toc(doc)
-    doc.add_page_break()
-
-def chapter1(doc):
-    heading(doc, "第一章 调研概述", 1)
-
-    heading(doc, "1.1 调研背景", 2)
-    para(doc, "在线考试系统在高校课程考核、职业资格认证、企业内训测评等场景中已成为基础设施级应用。与一般的业务信息系统相比,它天然叠加了三类硬约束:试卷必须同时满足知识点覆盖、难度分布、题型配比与总分等多重约束,属于典型的组合优化问题;考试过程对网络连续性高度敏感,一次断网就可能造成答案丢失,后果等同于教学事故;考场缺乏现场监考,只能依靠行为信号推断作弊意图,而误判又会产生申诉与复核成本。")
-    para(doc, "本研究采用的方案 B 将系统定位为 Spring Boot 3 模块化单体(modular monolith),围绕上述三个问题分别提出方法,并用对比实验与故障注入验证其有效性。在正式展开设计之前,需要回答一个前置问题:这些工作是否已经被开源社区做过?如果已有成熟实现,本研究的价值应转向工程整合与场景适配;如果存在明显空白,则应把空白本身写进创新性论证。本文档即为回答这一问题而编写。")
-
-    heading(doc, "1.2 调研目的与范围", 2)
-    para(doc, "本次调研设定三个目的。")
-    bullet_rich(doc, [("摸清实现边界。", True), ("确认同类系统在组卷、断网续考、作弊识别三方面究竟做到了哪一步,避免在论文中重复叙述已被普遍实现的功能。", False)])
-    bullet_rich(doc, [("判断创新性。", True), ("研究点的价值取决于其稀缺程度。若某个问题在开源生态中存在大量成熟方案,则应重新表述研究角度;若普遍缺失,则该缺失可作为创新性的事实依据。", False)])
-    bullet_rich(doc, [("寻找可复用资产。", True), ("包括可直接引用的求解器、同步引擎、定时任务锁、测试工具,以及可作为实验对照基线的现成实现。", False)])
-    para(doc, "调研范围限定为 GitHub 上的公开仓库,检索截止时间为 2026 年 11 月 5 日,数据于 2026 年 11 月 9 日逐仓复核。Gitee 镜像、商业产品(如超星学习通、雨课堂)仅作为功能对照,不纳入代码级分析;学术论文的检索策略由方案 B 第十一章文献策略另行规定,本文档只在必要处给出衔接建议。")
-
-    heading(doc, "1.3 检索方法与数据口径", 2)
-    para(doc, "检索分三条路径并行:以名称关键词匹配同类系统,以 topic 标签聚合垂直领域,以技术关键词定位可复用的通用组件。主要检索式与命中情况如表 1-1 所示。")
-    caption(doc, "表 1-1 检索式与命中情况")
-    make_table(doc,
-        ["检索路径", "检索式", "命中规模", "用途"],
-        [["名称匹配", "online exam in:name / exam system in:name", "数十至数百", "定位同类系统"],
-         ["topic 聚合", "topic:online-exam", "119 个仓库", "垂直领域全景"],
-         ["topic 聚合", "topic:exam", "1744 个仓库", "含大量非考试业务噪声"],
-         ["topic 聚合", "topic:proctoring", "155 个仓库", "在线监考方向"],
-         ["技术定位", "automatic paper generation / constraint solver", "少量", "组卷算法基线"],
-         ["技术定位", "offline first sync / conflict resolution", "少量", "断网续考参照实现"]],
-        [2.8, 6.2, 3.0, 3.4])
-    note(doc, "说明:GitHub 搜索 API 对匿名请求限速 10 次/分钟,且单次响应存在长度上限,本文档的数据通过逐仓调用仓库详情接口获得;星标数与最后提交时间均以查询当日为准,引用时须注明查询日期,避免评审质疑数据时效。")
-
-    heading(doc, "1.4 主要结论", 2)
-    para(doc, "调研结论可归纳为五点,其中第三点最关键,因为它同时影响了论文的选题表述与章节重心。")
-    rich(doc, [("结论一:同类系统成熟,但成熟的是「业务流程」,不是「方法」。", True)])
-    para(doc, "学之思与砚考已经覆盖了题库管理、组卷、考试、判分、统计的完整闭环,界面与工程结构均达到可用水平。但它们解决的是「功能有没有」,而不是「方法好不好」:组卷仍是规则筛选,续考仍是逐题保存,防作弊仍是行为计数。这恰恰说明本研究的三个研究点没有被同类系统覆盖。")
-    rich(doc, [("结论二:组卷方向缺少开箱可用的实现,通用求解器是唯一可靠的可复用资产。", True)])
-    para(doc, "在检索到的自动组卷项目中,绝大多数采用遗传算法或随机回溯的简化实现,且多为 SSM 时代的老技术栈,缺少对约束强度、退化曲线的系统性度量。工业级的约束求解能力集中在 or-tools 与 Timefold 这类通用求解器中,需要自行建模。这意味着本研究的三求解器对比实验既有基线可依,又不至于与既有工作重复。")
-    rich(doc, [("结论三:断网续考是本轮调研中最明确的空白。", True)])
-    para(doc, "在所考察的 topic:online-exam(119 个仓库)、topic:exam(1744 个仓库)、topic:proctoring(155 个仓库)范围内,没有任何一个开源考试系统以离线答题、断网续答或客户端—服务端冲突合并为核心特性。商业系统的宣传口径也普遍停留在「逐题自动保存 + 断点续答」,即假设网络短暂中断后重新拉取即可,未处理同一题在两端被并发修改的情形。这一空白构成本研究问题二最有力的立论依据。")
-    rich(doc, [("结论四:作弊识别项目数量最多,但路线高度同质。", True)])
-    para(doc, "开源监考项目几乎全部采用摄像头视觉方案(人脸、视线、手机检测、多人检测),依赖 OpenCV、dlib 或 YOLO 系列模型;另有少数走客户端锁机路线(Safe Exam Browser 生态)。与本研究「规则引擎 + 风险分 + 人工复核、不做深度模型」的定位相比,二者不是同一路线的优劣,而是成本结构的不同:视觉方案需要摄像头、算力与隐私合规成本,规则方案则以浏览器事件与日志分析为主。论文中应把这一差异写成方法选择的理由,而不是技术先进性的比较。")
-    rich(doc, [("结论五:技术栈选择无需自证,架构选择需要论证。", True)])
-    para(doc, "Spring Boot 3 + Vue 3 + MyBatis-Plus 的组合在砚考等项目中已有完整落地,因此第 2 章的技术选型部分可以直接引用既有实践,不必展开论证;真正需要论证的是「为什么是模块化单体而不是微服务」,而砚考恰好提供了微服务形态的完整样本,可作为第 4 章的正面对照材料。")
-
-    heading(doc, "1.5 文档结构", 2)
-    para(doc, "第二章梳理同类在线考试系统,给出逐项能力对照,并说明可供本研究借鉴的设计;第三章按三个研究点分别整理可复用的开源资产,并给出研究空白的判定依据;第四章汇总平台、标准与学术检索方向;第五章给出与论文各章节对应的引用建议、实验基线与写作要点;附录 A 为完整仓库清单,附录 B 为按主题整理的检索记录。")
-
-def chapter2(doc):
-    heading(doc, "第二章 同类在线考试系统调研", 1)
-
-    heading(doc, "2.1 总体格局", 2)
-    para(doc, "按技术栈与定位,可将检索到的同类系统分为三类。第一类是国产 Java 全栈系统,以 Spring Boot + Vue 前后端分离为主,代表是学之思系列与砚考,功能覆盖最完整、社区最活跃,也是本研究最直接的参照对象。第二类是轻量毕设型系统,代码量小、结构简单,适合快速理解业务流程,但工程规范性不足。第三类是以 Moodle、Open edX、TAO 为代表的国际测评平台,功能极其完备但体量庞大,更适合作为标准与概念参照,而非代码参照。")
-    para(doc, "从活跃度看,学之思两个仓库在 2026 年 11 月仍有提交,砚考同日也有推送,说明国产考试系统生态仍在演进;而多数学术型自动组卷项目已停止维护(如 cppcpp/exam-system 最后提交为 2022 年 12 月),其价值主要在算法思路而非工程质量。")
-
-    heading(doc, "2.2 重点系统清单", 2)
-    para(doc, "表 2-1 列出逐仓核验后与本研究相关性最高的八个系统。星标数为 2026-11-09 的复核值,许可证一栏为空表示仓库未声明许可证,此类项目只能参考设计,不能复制代码。")
-    caption(doc, "表 2-1 同类在线考试系统清单(星标与提交时间为 2026-11-09 复核)")
-    make_table(doc,
-        ["项目(点击进入仓库)", "技术栈", "星标 / 许可", "覆盖范围", "对本研究的参考价值"],
-        [
-         [("mindskip/xzs-mysql", GH + "mindskip/xzs-mysql"), "Java + Vue,Web 与微信小程序", "3604 / AGPL-3.0", "题库、组卷、考试、判分、统计全流程", "业务闭环与表结构设计的首选参照;断点续答与随机组卷逻辑可直接对照"],
-         [("mindskip/xzs", GH + "mindskip/xzs"), "Java + Vue(SQL Server 版)", "3914 / AGPL-3.0", "同 xzs-mysql,文档与前端更完整", "前端工程组织与部署脚本"],
-         [("Alanosy/exam-platform", GH + "Alanosy/exam-platform"), "Spring Boot 3.5 + JDK17/21 + Vue3 + TS + Element Plus;微服务(RuoYi-Cloud-Plus 2.6.2)", "886 / MIT", "出题、组卷、考试、监考、阅卷、统计、发证、错题本闭环,9 种题型", "与本方案技术栈逐项对应;13 类防作弊计数、服务端强制交卷、交卷幂等可直接对照;其微服务形态可作第 4 章架构取舍的反例"],
-         [("lsgwr/spring-boot-online-exam", GH + "lsgwr/spring-boot-online-exam"), "Spring Boot + Vue", "1692 / MIT", "题库、考试、成绩,含在线预览", "代码量适中、许可证宽松,适合作为实现级参考"],
-         [("YXJ2018/SpringBoot-Vue-OnlineExam", GH + "YXJ2018/SpringBoot-Vue-OnlineExam"), "Spring Boot + Vue", "2279 / 无", "经典前后端分离考试系统", "模块划分与论文章节结构易被评审接受;无许可证,仅参考"],
-         [("shuzhao11/quiz", GH + "shuzhao11/quiz"), "Spring Boot + Thymeleaf", "393 / 无", "毕设定位的在线测试系统", "论文结构参照,技术深度不足"],
-         [("cppcpp/exam-system", GH + "cppcpp/exam-system"), "SSM(Java)+ 遗传算法", "87 / 无", "南京晓庄学院自动组卷,遗传算法生成试卷", "问题一最贴合的基线实现;技术栈老旧,仅借鉴算法与约束表达"],
-         [("xingxingzaixian/django-vue3.2-online-exam", GH + "xingxingzaixian/django-vue3.2-online-exam"), "Django + DRF + Vue3 + Element Plus", "184 / MIT", "在线考试,开发中", "Vue3 + Element Plus 的前端交互参考(题库、答题页)"]],
-        [3.5, 3.6, 2.2, 3.1, 3.0], link_col=0)
-    note(doc, "注:wulivictor/ExamOnline(739 星,答题/考试小程序)虽未列入上表,但其切屏与退后台处理对移动端答题场景有参考价值;xzs 系列的 AGPL-3.0 许可具有传染性,仅可借鉴设计,不可复制代码进入以其他许可证发布的仓库。")
-
-    heading(doc, "2.3 逐项能力对照", 2)
-    para(doc, "为把「已有 vs 未有」说清楚,表 2-2 将四个代表性系统与本研究方案逐项对齐。该表可直接改造为论文第 1 章国内外研究现状中的对照表,也可作为第 4 章设计取舍的论据。")
-    caption(doc, "表 2-2 代表性系统与本研究方案的能力对照")
-    make_table(doc,
-        ["能力维度", "学之思 xzs-mysql", "砚考 exam-platform", "lsgwr 版", "本研究(方案 B)"],
-        [["组卷方式", "按题型/难度/分值随机抽题,规则可配置", "手工选题 + 规则随机抽题,候选不足时提示", "随机抽题", "约束三元组建模,三求解器(回溯/贪心/遗传)"],
-         ["约束满足能力", "规则筛选,无求解保证", "规则筛选,无求解保证", "无", "带成功率与退化曲线度量"],
-         ["断点续答", "支持(逐题自动保存)", "支持(中途退出可续答)", "部分支持", "支持"],
-         ["客户端离线缓存", "无(依赖实时提交)", "无", "无", "IndexedDB 本地草稿 + last_seq 游标"],
-         ["答案冲突合并", "无", "无", "无", "版本号比较,服务端拒绝过期版本"],
-         ["交卷幂等", "未见显式设计", "支持(重复交卷幂等)", "无", "唯一索引 (session_id, submit_token)"],
-         ["防作弊信号", "切屏、复制等基础检测", "13 类行为计数,达阈值服务端强制交卷", "切屏检测", "5 类信号 → 加权风险分 → 分级复核"],
-         ["风险分级模型", "无", "无(仅阈值强制交卷)", "无", "连续风险分 + 人工复核队列"],
-         ["主观题阅卷", "人工阅卷", "三级人工阅卷,改分留痕;双评已预留", "人工阅卷", "双评 + 仲裁,与判分链路一致性绑定"],
-         ["成绩一致性保障", "同步判分", "汇总表 10 分钟定时重算兜底", "同步判分", "outbox + 定时对账,故障注入验证"],
-         ["架构形态", "单体(前后端分离)", "微服务(Nacos + Dubbo,10 个服务)", "单体", "模块化单体,显式模块边界"],
-         ["许可证", "AGPL-3.0", "MIT", "MIT", "—"]],
-        [2.6, 3.4, 3.6, 2.0, 3.8])
-    note(doc, "注:本表中「无」指该仓库公开代码与文档中未见对应实现,不等同于项目缺陷;砚考的部分能力(双评、AI 阅卷)在其 README 中标注为「已预留接口、尚未接入」。")
-
-    heading(doc, "2.4 可借鉴的设计与明确的差距", 2)
-    heading(doc, "2.4.1 可直接借鉴的设计", 3)
-    bullet_rich(doc, [("表结构草案。", True), ("学之思的题库、试卷、考试、答卷四组表与本研究的数据模型高度同构,可作为 question、paper、exam、exam_session、answer_item 的字段起点,减少建模范式上的返工。", False)])
-    bullet_rich(doc, [("交卷幂等的落地方式。", True), ("砚考以「前端主动提交 + 后端定时兜底」双保险处理倒计时归零场景,与本研究 (session_id, submit_token) 方案可以互相印证:前者保证「一定会交」,后者保证「只生效一次」,二者正交互补。", False)])
-    bullet_rich(doc, [("统计表的预计算 + 定时重算。", True), ("砚考将考试统计落到预计算汇总表,并以每 10 分钟定时重算兜底。这一结构与本研究第 3.4 节「交卷—判分—成绩」的 outbox + 对账思路一致,可作为该支撑研究点的工程依据。", False)])
-    bullet_rich(doc, [("前端三端同工程的组织方式。", True), ("砚考与学生端、教师端、管理端的路由拆分方式,对应本研究 4.2 节的三端功能划分。", False)])
-    heading(doc, "2.4.2 需要在本研究中补足的差距", 3)
-    bullet_rich(doc, [("组卷从「筛选」升级为「求解」。", True), ("既有系统的组卷本质是对候选集按条件过滤后随机抽取,约束一多就会出现抽不满或凑不齐总分的情况,且没有任何成功率度量。本研究把它表述为约束满足问题并给出三档强度下的退化曲线,这是方法层面的增量。", False)])
-    bullet_rich(doc, [("续考从「保存」升级为「一致性协议」。", True), ("既有实现的断点续答建立在「网络恢复后以服务端为准」的隐含假设上,未定义同一题在客户端与服务端被并发修改时的裁决规则。本研究引入客户端版本号、服务端唯一索引与增量同步三者配合,形成可证明的收敛协议。", False)])
-    bullet_rich(doc, [("防作弊从「计数」升级为「风险度量」。", True), ("砚考的 13 类行为计数达到上限即强制交卷,是二值判定:在阈值以下完全无害,越过阈值直接终止。本研究的加权风险分允许中间状态进入人工复核,兼顾召回率与误报成本,并可用单信号 AUC 说明各信号的贡献。", False)])
-    bullet_rich(doc, [("架构从「微服务堆叠」回归「模块化单体」。", True), ("砚考用 Nacos + Dubbo 拆分 10 个服务,对一个课程设计规模的系统而言,分布式基础设施带来的运维与调试成本高于收益。本研究以模块边界纪律替代物理拆分,并用同一边界说明未来演进路径,这一点在第 4 章需要正面论证。", False)])

+ 0 - 227
sections_b.py

@@ -1,227 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Part B: chapters 3, 4, 5."""
-from docx.shared import Pt, Cm
-from docx.enum.text import WD_ALIGN_PARAGRAPH
-from docx_helpers import (make_table, para, rich, bullet, bullet_rich, heading,
-                          caption, note, quote, link_para)
-
-GH = "https://github.com/"
-
-def chapter3(doc):
-    heading(doc, "第三章 三个研究点的开源现状", 1)
-    para(doc, "本章按方案 B 的三个研究点分别展开。每一节的结构一致:先给出检索过程与命中情况,再评估既有系统的实际能力,然后列出可复用资产,最后给出空白判定。判定标准有两条:其一,检索范围内是否存在以该问题为核心特性的开源项目;其二,既有系统的实现是否停留在工程惯例层面而缺少方法层面的设计与度量。只有两条同时成立,才可称为「空白」。")
-
-    # ---------------- 3.1 ----------------
-    heading(doc, "3.1 问题一:多约束组卷", 2)
-    heading(doc, "3.1.1 开源现状", 3)
-    para(doc, "自动组卷在开源生态中的存在形式有三种。第一种是考试系统内置的「随机抽题」功能,按题型、难度、分值等条件从题库过滤后随机选取,学之思与砚考都属此类;这种方式实现简单,但本质是筛选而非求解,约束一多就会出现候选不足或凑不齐总分,且系统只能提示「候选不足」,无法给出最优或次优方案。第二种是学术型自动组卷项目,多采用遗传算法或模拟退火,代码量小、技术栈老旧,代表性的是 cppcpp/exam-system(南京晓庄学院自动组卷系统,遗传算法生成试卷,SSM 技术栈,最后提交于 2022 年 12 月)。第三种是通用约束求解器,本身不是考试系统,但提供了完整的建模与求解能力。")
-    para(doc, "值得注意的是,第三种才是工业界真正在用的路径。Google 的 or-tools 提供 CP-SAT 求解器,可把「知识点覆盖、难度分布、题型配比、总分、去重避重」直接表达为约束与目标;Timefold(原 OptaPlanner 团队)提供 Java/Kotlin 原生的启发式与局部搜索框架,其领域建模方式与排课、排班问题同源。二者都被大量生产系统使用,但对考试组卷这一具体场景没有现成模型,需要研究者自行完成变量定义、约束表达与目标函数设计。这恰恰说明:组卷的难点不在求解器本身,而在问题建模。")
-
-    heading(doc, "3.1.2 可复用资产", 3)
-    caption(doc, "表 3-1 组卷方向可复用仓库")
-    make_table(doc,
-        ["项目(点击进入仓库)", "语言", "星标 / 许可", "在本研究中的用途"],
-        [[("cppcpp/exam-system", GH + "cppcpp/exam-system"), "Java(SSM)", "87 / 无", "遗传算法组卷的基线实现:编码方式、适应度函数、约束惩罚项可直接对照,用于验证本研究遗传求解器的合理性"],
-         [("google/or-tools", GH + "google/or-tools"), "C++(Java 绑定)", "14348 / Apache-2.0", "CP-SAT 建模多约束组卷,作为「精确求解 vs 启发式」的对照方法;论文中可论证其建模成本与求解质量的权衡"],
-         [("TimefoldAI/timefold-solver", GH + "TimefoldAI/timefold-solver"), "Java / Kotlin", "1859 / Apache-2.0", "局部搜索框架,与本研究「贪心 + 局部修补」求解器同源,可引用其评分函数与邻域算子设计"],
-         [("mindskip/xzs-mysql", GH + "mindskip/xzs-mysql"), "Java", "3604 / AGPL-3.0", "paper_rule 表的字段设计可参考其组卷规则的持久化方式(仅借鉴结构)"]],
-        [4.0, 2.4, 2.6, 6.4], link_col=0)
-    note(doc, "注:or-tools 的 Java 绑定虽可用,但官方文档以 Python/C++ 为主,建模示例需要自行转写;若时间紧张,建议仅在论文中作为对照方法讨论,不作为主力实现。")
-
-    heading(doc, "3.1.3 空白判定与立项依据", 3)
-    para(doc, "按前述两条判定标准逐项检验。第一,检索范围内不存在以「多约束组卷求解」为核心特性的开源考试系统:所有被检视的系统都把组卷实现为一个带随机性的筛选函数,没有把约束集形式化为可配置模型,也没有提供求解器抽象。第二,既有实现停留在工程惯例层面:没有成功率度量,没有约束强度分级,没有退化曲线,也没有对难度分布偏差或知识点覆盖偏差的量化。两条同时成立,可以判定该方向存在方法层面的空白。")
-    para(doc, "因此本研究把组卷表述为「题库、约束集、目标」三元组,约束集以可配置 JSON 落入 paper_rule 表,并实现随机回溯、贪心加局部修补、遗传算法三个求解器、统一接口、三档约束强度,测度成功率、单次耗时、难度分布偏差(KL 散度或卡方值)、知识点覆盖偏差与总分误差。这一设计的价值不在于「用了遗传算法」,而在于给出了约束强度与求解质量之间的定量关系,使「什么时候该用哪个求解器」成为一个可回答的问题。")
-
-    heading(doc, "3.1.4 实验基线建议", 3)
-    para(doc, "实验设计建议采用三级对照。基线级为「随机抽题加重试」,即既有系统的做法,用于说明朴素方法的失效边界;中间级为 cppcpp/exam-system 的遗传算法实现,用于说明同类学术工作的水平;对照级为 or-tools CP-SAT 的精确求解,用于给出质量上界(在小规模题库上可求得最优解,从而计算本研究启发式解的最优性差距)。这样组织,实验既能自证改进,又能说明与工业级方法的距离。")
-
-    # ---------------- 3.2 ----------------
-    heading(doc, "3.2 问题二:断网续考的答案一致性", 2)
-    heading(doc, "3.2.1 检索过程与结果", 3)
-    para(doc, "本方向的检索最为彻底,也最令人意外。检索沿 topic 标签展开,逐一检视了三个标签下的仓库列表,并按关键词过滤离线、同步、续答、冲突等语义。检索记录如表 3-2。")
-    caption(doc, "表 3-2 断网续考方向的检索记录")
-    make_table(doc,
-        ["检索式", "命中规模", "检视方式", "结果"],
-        [["topic:online-exam", "119 个仓库", "全部检视仓库名与简介", "无任何以离线答题或断网续答为核心特性的项目"],
-         ["topic:exam", "1744 个仓库", "按关键词过滤后检视", "含大量非考试业务仓库,过滤后无相关项目"],
-         ["topic:proctoring", "155 个仓库", "全部检视仓库名与简介", "集中于人脸、视线、屏幕共享等监考能力,与续考无关"],
-         ["offline first / local-first sync", "少量", "检视通用同步引擎", "存在成熟引擎(如 rxdb、pouchdb),但无考试场景应用"],
-         ["commercial systems(功能对照)", "—", "查阅产品说明与帮助文档", "普遍宣传「逐题自动保存 + 断点续答」,未提及冲突裁决规则"]],
-        [4.4, 2.6, 4.0, 5.4])
-    para(doc, "结论很明确:在本次检索覆盖的范围内,没有任何一个开源考试系统把「客户端与服务端答案的并发修改」作为需要解决的问题。既有系统的断点续答建立在一个隐含假设之上——网络恢复后以服务端数据为准,客户端本地内容只是尚未送达的暂存。这个假设在「断网期间考生继续作答、同时服务端因超时收卷或另一终端登录而产生写入」的情形下并不成立,而方案 B 的问题二正是针对这一情形。")
-
-    heading(doc, "3.2.2 既有系统的实际能力", 3)
-    para(doc, "把三个代表性系统的续考能力拆开看,可以更清楚地定位差距。")
-    bullet_rich(doc, [("学之思(xzs-mysql):", True), ("答题过程逐题提交,服务端保存作答记录,考生中途退出后可恢复。其前提是每次作答都能即时联网送达,因此本质是「在线保存 + 断点恢复」,不含离线缓存。", False)])
-    bullet_rich(doc, [("砚考(exam-platform):", True), ("逐题自动保存、中途退出可断点续答、倒计时归零自动交卷(前端主动提交 + 后端定时兜底双保险)、重复交卷幂等。这是目前能力最完整的一组,但同样没有客户端离线缓存与版本合并。", False)])
-    bullet_rich(doc, [("Moodle(mod_quiz):", True), ("以 question_attempts 状态机记录每一次作答步骤,支持分步保存与草稿态恢复,是理论上最接近「可恢复」的模型。但它是服务端状态机,不涉及客户端离线副本的冲突合并。", False)])
-    para(doc, "三者的共同点是:把「断网」处理为传输层问题(重发、重试、恢复),而不是数据处理问题(多副本、版本、收敛)。这正是本研究可以切入的缝隙。")
-
-    heading(doc, "3.2.3 可复用的同步与一致性组件", 3)
-    para(doc, "虽然没有考试场景的实现可参照,但「本地优先 + 增量同步 + 冲突解决」在其他领域已有成熟工程方案。这些方案不直接可用,但其数据模型与协议设计可作为本研究的理论来源。")
-    caption(doc, "表 3-3 断网续考方向可复用仓库")
-    make_table(doc,
-        ["项目(点击进入仓库)", "语言", "星标 / 许可", "可借鉴的机制"],
-        [[("pubkey/rxdb", GH + "pubkey/rxdb"), "TypeScript", "23421 / Apache-2.0", "本地优先数据库的冲突解决策略族(主从、最后写入胜出、自定义合并),其策略枚举可直接映射到本研究「版本大者胜出 + 服务端仲裁」的设计"],
-         [("pouchdb/pouchdb", GH + "pouchdb/pouchdb"), "JavaScript", "17627 / Apache-2.0", "CouchDB 复制协议:以修订号(rev)链表达版本,冲突时保留多个叶子节点供上层裁决,是「不丢写」的经典做法"],
-         [("dexie/Dexie.js", GH + "dexie/Dexie.js"), "TypeScript", "14673 / Apache-2.0", "IndexedDB 的封装库,方案 B 已选定为客户端存储层;其事务与索引 API 适合按 (examId, questionId) 建草稿表"],
-         [("lukas-krecan/ShedLock", GH + "lukas-krecan/ShedLock"), "Java", "4235 / Apache-2.0", "基于 Redis 的分布式定时任务锁,保证 30 秒快照任务在多实例下不重复执行"],
-         [("testcontainers/testcontainers-java", GH + "testcontainers/testcontainers-java"), "Java", "8758 / MIT", "故障注入的工程基础:可在测试中真实重启 MySQL/Redis、断开连接,用于验证恢复语义"]],
-        [4.0, 2.4, 2.6, 6.4], link_col=0)
-    note(doc, "注:rxdb 与 pouchdb 的冲突模型建立在对等副本(peer-to-peer)假设上,而考试场景是「一个考生对应一个会话」的中心化结构,因此不能照搬;本研究的做法是保留其版本比较思想,但把裁决权收敛到服务端,从而避免复杂的多副本协商。")
-
-    heading(doc, "3.2.4 空白判定与本研究定位", 3)
-    para(doc, "两条判定标准中,第一条成立得很彻底:检索范围内无任何以该问题为核心的开源项目。第二条同样成立:既有实现没有任何显式的冲突裁决规则,也没有对丢答案、重复提交、恢复时长等指标的度量。因此可以判定为空白。")
-    para(doc, "本研究据此提出的五项设计构成一套完整协议:客户端 IndexedDB 即时写入并维护 last_seq 游标;服务端以 (session_id, question_id, version) 唯一索引保证同一版本的重复写入只生效一次;重连后携带 last_seq 拉取差异并按版本号合并,版本大者胜出、服务端拒绝过期版本;服务端每 30 秒落一次会话快照,重启后按快照加增量日志恢复;交卷以 (session_id, submit_token) 幂等。五项设计分别对应「写不丢、写不重、能收敛、能恢复、交不重」,指标的选取(答案完整率、重复提交次数、断网恢复时长、草稿丢失条数、重启后会话语义正确率)也一一对应。")
-    para(doc, "在论文写作上,这一节的价值不仅是「别人没做」,更在于可以论证「为什么别人没做」:考试系统的答题负载是低频小数据,多数实现者认为重试足够;只有把断网、重启、多端登录三种故障叠加,才会暴露一致性缺口。这一论证可以显著提升第 1 章问题陈述的说服力。")
-
-    # ---------------- 3.3 ----------------
-    heading(doc, "3.3 问题三:作弊行为识别", 2)
-    heading(doc, "3.3.1 两条技术路线", 3)
-    para(doc, "开源监考项目的数量明显多于前两个方向,但路线高度集中,可归为两类。")
-    para(doc, "第一类是视觉 AI 路线,以摄像头画面为输入,检测人脸数量、视线方向、是否出现手机、是否离开画面。这是开源生态的主流,代表项目见表 3-4。")
-    caption(doc, "表 3-4 视觉 AI 路线代表项目")
-    make_table(doc,
-        ["项目(点击进入仓库)", "技术栈", "星标 / 许可", "检测能力"],
-        [[("vardanagarwal/Proctoring-AI", GH + "vardanagarwal/Proctoring-AI"), "Python,OpenCV + dlib", "633 / MIT", "视线估计、人脸识别、手机检测、多人检测、离开检测,五类信号齐全,是本路线最经典的起点"],
-         [("narender-rk10/MyProctor.ai", GH + "narender-rk10/MyProctor.ai-AI-BASED-SMART-ONLINE-EXAMINATION-PROCTORING-SYSYTEM"), "Flask + MySQL + YOLOv4", "236 / 无", "目标检测为核心的全栈示例,含考生端与监考端界面"],
-         [("tusharnankani/Aankh", GH + "tusharnankani/Aankh"), "JavaScript", "86 / GPL-3.0", "浏览器端实时监考:标签页切换、离开屏幕、多人脸检测,代码短、易读"],
-         [("meefik/ITMOproctor", GH + "meefik/ITMOproctor"), "WebRTC", "40 / GPL-3.0", "远程人工监考:多路视频 + 监考端坐席,已在 2023 年归档"],
-         [("eLearning-BS23/moodle-quizaccess_proctoring", GH + "eLearning-BS23/moodle-quizaccess_proctoring"), "PHP(Moodle 插件)", "30 / 未声明 SPDX", "在 LMS 考试过程中嵌入浏览器侧检测,可参考其与宿主系统的集成边界"]],
-        [4.0, 3.0, 2.6, 5.8], link_col=0)
-    para(doc, "第二类是客户端锁机路线,不以检测为目标,而是直接限制考试期间的设备行为。代表是 Safe Exam Browser 生态:客户端把机器锁定为考试专用环境,服务端负责会话编排与实时监控。")
-    caption(doc, "表 3-5 客户端锁机路线代表项目")
-    make_table(doc,
-        ["项目(点击进入仓库)", "语言", "星标 / 许可", "定位"],
-        [[("SafeExamBrowser/seb-win-refactoring", GH + "SafeExamBrowser/seb-win-refactoring"), "C#", "366 / MPL-2.0", "Windows 客户端,锁机、禁用切换、限制外设"],
-         [("SafeExamBrowser/seb-server", GH + "SafeExamBrowser/seb-server"), "Java", "97 / MPL-2.0", "服务端,管理考试会话与在线的 SEB 客户端,欧洲高校事实标准"],
-         [("aesophor/hacklympics", GH + "aesophor/hacklympics"), "Java", "69 / MPL-2.0", "竞赛场景原型:键盘记录、进程检测,思路偏机器侧管控"]],
-        [5.0, 2.4, 2.6, 5.4], link_col=0)
-
-    heading(doc, "3.3.2 可借鉴的信号清单", 3)
-    para(doc, "本研究不做深度模型,因此表 3-4 中的视觉能力不直接采用,但其中的信号划分极有参考价值。把开源项目实际检测的维度与方案 B 的五类信号对照,可以得到表 3-6。")
-    caption(doc, "表 3-6 开源检测维度与本研究信号的对应关系")
-    make_table(doc,
-        ["开源项目检测的行为", "典型来源", "本研究对应信号", "采集方式"],
-        [["标签页切换、窗口失焦", "Aankh、砚考(13 类行为)", "切屏与失焦次数、累计时长", "visibilitychange、blur/focus 事件"],
-         ["多人脸、无人脸、离开画面", "Proctoring-AI、MyProctor.ai", "(不采用,改为)多端登录与会话并发", "会话表与登录日志"],
-         ["手机出现、外设接入", "Proctoring-AI", "(不采用)", "—"],
-         ["复制、剪切、粘贴、右键、开发者工具", "砚考", "答题节奏异常(修改频次、停留时间)", "前端事件埋点"],
-         ["多标签页、退出全屏", "砚考", "切屏与失焦", "全屏状态监听"],
-         ["IP 与设备聚集", "SEB Server 会话编排", "IP 与设备指纹聚集", "登录与心跳元数据"],
-         ["答案雷同", "(开源中普遍缺失)", "相邻考生答案相似度", "交卷后批量比对"]],
-        [3.6, 3.6, 4.6, 3.6])
-    note(doc, "注:表中「不采用」的两行并非否定其有效性,而是出于成本与合规考虑——摄像头方案需要终端设备、算力与隐私授权,与本研究「仅依赖浏览器事件与日志」的定位冲突。论文中应把这一取舍写成显式的方法边界,而非能力缺失。")
-
-    heading(doc, "3.3.3 与本研究定位的差异", 3)
-    para(doc, "本研究的做法是规则引擎输出连续风险分(加权求和加阈值分级),超阈值进入人工复核队列。与既有方案相比,差异有三点,每一点都可以写成论文中的设计理由。")
-    bullet_rich(doc, [("从二值判定到连续度量。", True), ("砚考的 13 类行为采用计数加阈值的方式,达到上限即由服务端强制交卷。这种设计对明显违规有效,但无法表达「轻度可疑、需要人工看一眼」的中间状态。连续风险分把判定推迟到复核环节,降低了误伤成本。", False)])
-    bullet_rich(doc, [("从单一强信号到多信号融合。", True), ("视觉方案依赖摄像头这一强信号,一旦设备缺失或考生遮挡即失效;本研究采用五个弱信号加权,单个信号失效不影响整体可用性,代价是必须在标注样本上验证各信号的区分度(AUC)。", False)])
-    bullet_rich(doc, [("从追求准确率到验证信号有效性。", True), ("本研究的定位是信号有效性验证,而不是追求绝对准确率。这一定位既符合本科论文的可行性,也避开了与商业 AI 监考产品在模型能力上的正面比较。", False)])
-    para(doc, "在论文表述上,建议明确写出:本研究的贡献不是更高的识别准确率,而是给出一套低成本、可解释、可复核的风险度量方法,并用 200 条标注样本量化各信号的单变量区分度。这样的表述既诚实又有边界,评审难以从「效果不如商业产品」的角度发难。")
-
-    # ---------------- 3.4 ----------------
-    heading(doc, "3.4 支撑研究点:交卷—判分—成绩的一致性", 2)
-    para(doc, "方案 B 把「交卷、客观题判分、成绩发布」三步的一致性作为支撑研究点,采用本地消息表 outbox 加定时对账,主观题采用双评加仲裁。这一设计在开源生态中同样有对应物:砚考的统计汇总表采用预计算加每 10 分钟定时重算兜底,本质上就是「先算后校」的对账思想;其主观题按「考试、答卷、逐题」三级人工阅卷并记录改分日志,与双评仲裁的审计需求同构。")
-    para(doc, "这一研究点的价值在于实验成本低、指标硬:注入异常后的不一致记录检出率、对账修复率、端到端时延都是可直接测量的量,且 Testcontainers 能在测试中真实重启数据库,构造出「消息已发、下游未处理」的经典故障。建议在论文第 6 章保留这组实验,它与问题二共享同一套故障注入设施,边际成本很低。")
-
-def chapter4(doc):
-    heading(doc, "第四章 平台、标准与学术检索", 1)
-
-    heading(doc, "4.1 国际测评平台", 2)
-    para(doc, "国际平台的价值不在于代码复用(体量与栈差异过大),而在于概念模型与标准实践。Moodle 的 question_attempts 状态机是「可恢复答题过程」最成熟的公开设计;Open edX 的 ProctoringProvider 接口把监考能力抽象为可替换的服务提供者,对本研究「作弊识别模块对外只暴露 api 包」的设计有直接启发;TAO 则以 QTI 标准为核心,试卷结构与判分模型最为规范。")
-    caption(doc, "表 4-1 国际测评平台")
-    make_table(doc,
-        ["平台(点击进入仓库)", "语言", "星标 / 许可", "可借鉴之处"],
-        [[("moodle/moodle", GH + "moodle/moodle"), "PHP", "7594 / GPL-3.0", "mod_quiz 的 question_attempts 状态机与分步保存,是断网续考最好的理论参照;题库分类与随机题机制可对照 paper_rule 设计"],
-         [("openedx/edx-platform", GH + "openedx/edx-platform"), "Python", "8220 / AGPL-3.0", "ProctoringProvider 抽象:把监考做成可插拔提供者,对应本研究 monitor 模块的边界设计"],
-         [("oat-sa/tao-core", GH + "oat-sa/tao-core"), "PHP", "64 / GPL-2.0", "基于 QTI 的测评引擎,试题与试卷的数据结构最规范,适合引用其模型"],
-         [("mawoka-myblock/ClassQuiz", GH + "mawoka-myblock/ClassQuiz"), "Svelte + FastAPI", "737 / MPL-2.0", "Kahoot 式实时答题,可参考开考瞬间的高并发广播(Socket.IO)与房间模型"],
-         [("tomwalker/django_quiz", GH + "tomwalker/django_quiz"), "Python", "628 / 无", "题型与计分规则的简洁设计,适合作为需求分析阶段的题型枚举参照"]],
-        [4.4, 2.6, 2.6, 5.8], link_col=0)
-
-    heading(doc, "4.2 标准", 2)
-    para(doc, "若论文打算讨论「题库与试卷的可迁移性」或「试题格式的标准化」,1EdTech 的 QTI(Question and Test Interoperability)是必须提及的标准,当前版本为 QTI 3。该标准定义了试题(Item)、测试(Test)、结果(Result)三类文档的 XML 结构,Moodle 与 TAO 均有部分实现。对本研究而言,引入 QTI 的实际意义有限(系统内部用关系表存储即可),但在第 2 章相关技术部分用一小段说明「为什么选择自定义模型而非直接采用 QTI」,可以体现对领域的了解。")
-    link_para(doc, [("1EdTech QTI 标准首页:", "https://www.1edtech.org/standards/qti")])
-
-    heading(doc, "4.3 学术检索方向", 2)
-    para(doc, "方案 B 第十一章已给出文献策略,本节在其基础上补充与开源调研对应的英文检索方向。建议每个研究点至少精读三篇核心期刊论文,把方法、指标与数据集记入笔记,研究点的表述即从中提炼。")
-    caption(doc, "表 4-2 按研究点补充的检索方向")
-    make_table(doc,
-        ["研究点", "建议检索词", "关注要点"],
-        [["问题一 组卷", "automatic test paper generation;constrained combinatorial optimization;genetic algorithm examination timetabling", "约束表达方式、适应度函数设计、与排课问题的同构性、评价指标(覆盖率、偏差)"],
-         ["问题二 一致性", "local-first software;optimistic replication;idempotent write;conflict resolution CRDT", "版本向量与修订号语义、幂等键设计、最终一致性的可测指标"],
-         ["问题三 作弊识别", "online proctoring detection;anomaly detection in e-assessment;behavioral biometrics keystroke", "信号清单、标注样本构造方式、召回率与误报率的权衡表述"],
-         ["支撑点", "transactional outbox pattern;reconciliation job;eventual consistency measurement", "对账任务设计、不一致检出率度量"]],
-        [3.4, 6.4, 5.6])
-
-    heading(doc, "4.4 许可证风险与合规建议", 2)
-    para(doc, "开源调研的一个实务问题是许可证。本研究所涉项目覆盖 AGPL-3.0、GPL-2.0/3.0、MPL-2.0、Apache-2.0、MIT 与未声明六种情形,处置方式差别很大。")
-    caption(doc, "表 4-3 许可证类型与处置建议")
-    make_table(doc,
-        ["许可证", "涉及项目", "约束强度", "建议"],
-        [["AGPL-3.0", "学之思 xzs-mysql / xzs,Open edX", "最强,网络服务亦触发开源义务", "仅借鉴设计与表结构,不复制代码;论文中作为对比对象引用"],
-         ["GPL-2.0 / 3.0", "Moodle、TAO、Aankh、ITMOproctor", "强,衍生作品须同许可", "同上;若确需复用片段,单独隔离并注明来源"],
-         ["MPL-2.0", "Safe Exam Browser 系列、ClassQuiz、hacklympics", "中等,文件级传染", "可复用单个文件并保留声明,适合参考具体算法实现"],
-         ["Apache-2.0", "or-tools、Timefold、rxdb、pouchdb、Dexie.js、ShedLock", "宽松,需保留声明与变更说明", "可作为依赖直接引入,注意 NOTICE 文件"],
-         ["MIT", "砚考、lsgwr 版、Proctoring-AI、Testcontainers、Django 系", "宽松,保留版权声明即可", "最适合作为代码级参考与依赖"],
-         ["未声明", "YXJ2018 版、shuzhao11/quiz、cppcpp/exam-system、wulivictor/ExamOnline", "法律状态不明", "只读不抄;引用时注明「未声明许可证」"]],
-        [2.6, 6.2, 4.0, 5.2])
-    note(doc, "建议:论文与源码仓库中统一采用 MIT 或 Apache-2.0 声明自身代码;对 AGPL/GPL 项目仅作设计参考,并在参考文献或脚注中标注「设计参考,未使用其代码」,以避免答辩时的合规质疑。")
-
-def chapter5(doc):
-    heading(doc, "第五章 论文写作与实验落地建议", 1)
-
-    heading(doc, "5.1 与论文章节的对应关系", 2)
-    para(doc, "本文档的素材可直接落入方案 B 已规划的七章结构中,对应关系如表 5-1。")
-    caption(doc, "表 5-1 调研素材与论文章节的对应")
-    make_table(doc,
-        ["论文章节", "可用的调研素材", "使用方式"],
-        [["第 1 章 绪论", "本文档第 1.4 节五条结论、表 2-2 能力对照、3.2.1 检索记录", "改写为国内外研究现状;空白结论作为研究意义的论据"],
-         ["第 2 章 相关技术", "表 2-1 技术栈落地证据、第 4.2 节 QTI、表 3-1/3-3 求解器与同步引擎", "技术选型依据,说明每一项均有成熟实践"],
-         ["第 3 章 需求分析", "表 2-2 中「无」的维度、3.3.2 信号对照表", "把差距转化为功能需求与非功能需求"],
-         ["第 4 章 系统设计", "砚考的微服务形态(反例)、其模块划分与表结构", "架构取舍论证:为什么模块化单体优于微服务"],
-         ["第 5 章 关键方法", "3.1.4 实验基线、3.2.4 协议设计、3.3.3 方法边界", "三个方法的立论与对照设计"],
-         ["第 6 章 实现与测试", "表 3-3 中 Testcontainers、ShedLock;3.4 节对账实验", "故障注入与一致性实验的工程依据"],
-         ["第 7 章 总结与展望", "2.4.2 的差距、第 4.1 节平台的能力边界", "局限说明与演进路径"]],
-        [3.4, 7.0, 5.0])
-
-    heading(doc, "5.2 可直接改写的研究现状段落", 2)
-    para(doc, "以下三段为按学术语体撰写的研究现状素材,分别对应三个研究点,可按需裁剪、补充文献编号后直接使用。")
-    para(doc, "(一)关于组卷。", bold=True, first_indent=True)
-    quote(doc, ["现有开源在线考试系统普遍将组卷实现为「按题型、难度、分值等条件从题库过滤后随机抽取」的筛选过程。学之思开源考试系统与「砚考」在线考试系统均采用此类规则驱动的组卷方式,规则可保存复用,候选不足时向用户提示,但系统本身不保证约束的可满足性,也未对求解质量给出度量。学术研究中虽有基于遗传算法的自动组卷实现,但多停留在算法演示层面,缺少对约束强度与求解成功率之间关系的系统考察。相比之下,通用约束求解器(如 Google OR-Tools 的 CP-SAT、Timefold Solver)已具备成熟的建模与求解能力,但尚未被适配到考试组卷这一具体场景。本文因此将组卷形式化为「题库—约束集—目标」三元组,并在统一接口下比较三类求解策略在不同约束强度下的表现。"])
-    para(doc, "(二)关于断网续考。", bold=True, first_indent=True)
-    quote(doc, ["断网续考是考试系统可靠性的核心诉求,但现有开源实现的处理方式高度一致:答题过程逐题提交并持久化,考生重新进入后可恢复此前作答,即所谓「断点续答」。Moodle 的 question_attempts 状态机以分步记录的方式保存作答过程,是该类设计中最为完备的公开实现;国内学之思与「砚考」亦提供中途退出续答与倒计时归零自动交卷能力。然而上述实现的共同前提是「网络恢复后以服务端数据为准」,客户端本地内容仅被视为尚未送达的暂存,系统并未定义同一作答在客户端与服务端被并发修改时的裁决规则,也未对断网期间的作答数据完整性给出度量。本文因此引入客户端本地草稿、服务端唯一索引幂等写入与基于游标的增量同步三者配合的一致性协议,并通过故障注入验证其在网络中断、服务重启与多端登录三种情形下的收敛性。"])
-    para(doc, "(三)关于作弊识别。", bold=True, first_indent=True)
-    quote(doc, ["在线考试的作弊识别在开源生态中呈现明显的路线集中:主流方案以摄像头画面为输入,通过人脸检测、视线估计、目标检测等技术识别异常行为,代表性实现包括 Proctoring-AI、MyProctor.ai 等;另一条路线是客户端锁机,以 Safe Exam Browser 生态为代表,通过限制终端行为而非识别行为来达成目的。国内系统则多采用浏览器事件计数的方式,对切屏、失焦、复制粘贴等行为累计次数并在达到阈值时强制交卷。上述方案各有其成本结构:视觉方案依赖终端设备、算力与隐私授权,锁机方案依赖专用客户端,计数方案则以二值判定牺牲了对中间可疑状态的刻画能力。本文采用多信号加权风险分的方式,在不引入深度模型的前提下融合五类低成本行为信号,并以标注样本上的召回率、误报率与单信号 AUC 度量其有效性。"])
-
-    heading(doc, "5.3 实验与基线设置建议", 2)
-    para(doc, "综合前文分析,三个研究点的实验可按表 5-2 组织。其中「对照物」一列是本轮调研最直接的产出:没有对照的实验只能自证,有对照的实验才能说明水平。")
-    caption(doc, "表 5-2 三个研究点的实验组织建议")
-    make_table(doc,
-        ["研究点", "基线(必须做)", "对照物(体现水平)", "核心指标"],
-        [["问题一 组卷", "随机抽题加重试(既有系统做法)", "cppcpp/exam-system 的遗传算法;or-tools CP-SAT 的精确解作为质量上界", "成功率、单次耗时、难度分布 KL 散度、知识点覆盖偏差、总分误差"],
-         ["问题二 续考", "仅服务端保存(无本地草稿)", "rxdb 式「最后写入胜出」的朴素合并", "答案完整率、重复提交次数、恢复时长、草稿丢失条数、语义正确率"],
-         ["问题三 作弊", "单信号阈值判定", "等权求和(无权重优化的朴素融合)", "召回率、误报率(200 条标注)、单信号 AUC、风险分分布"],
-         ["支撑点 一致性", "同步调用判分(无 outbox)", "仅有重试、无对账", "不一致检出率、对账修复率、端到端时延"]],
-        [2.8, 4.4, 5.6, 4.8])
-
-    heading(doc, "5.4 答辩可能追问的问题", 2)
-    para(doc, "调研过程中反复出现的几个对比点,很可能成为答辩提问的起点。提前准备应答素材,比临场解释更可靠。")
-    caption(doc, "表 5-3 预设追问与应答要点")
-    make_table(doc,
-        ["可能的追问", "应答要点"],
-        [["学之思、砚考已经做得很完整,你的系统有什么价值?", "它们解决的是功能覆盖,本文解决的是方法质量:组卷从筛选到求解、续考从保存到一致性协议、防作弊从计数到风险度量。表 2-2 可逐项说明。"],
-         ["为什么不用微服务?别人都用微服务。", "三个研究点都在单业务边界内闭环;断网续考的一致性本质是唯一索引,单库成本最低;拆分带来的跨库幂等与运维成本会挤占实验时间。砚考的微服务形态可作为对照,说明其代价。"],
-         ["为什么不直接用现成的组卷算法?", "现成实现多为规则筛选,无约束求解保证;学术型遗传算法实现缺少约束强度与退化曲线的度量。本文的增量在于定量关系,而非算法本身。"],
-         ["断网续考是不是已经有成熟方案?", "开源生态中未见以冲突裁决为核心的系统,既有实现停留在逐题保存与断点续答,隐含假设是「恢复后以服务端为准」。本文的协议显式定义了并发修改的裁决规则。"],
-         ["为什么不做摄像头监考?", "成本与合规:摄像头方案需要终端设备、算力与隐私授权。本文定位是低成本信号的有效性验证,已在 3.3.3 明确方法边界。"],
-         ["200 条标注样本会不会太少?", "样本量为可行性所限,但指标设计上采用单信号 AUC 与风险分分布,避免只报一个总体准确率;结论表述为信号有效性的初步验证,而非最终精度。"],
-         ["引用这些开源项目,是否存在抄袭风险?", "全部为设计层面参考,未复制代码;AGPL/GPL 项目仅作对比对象,许可证处置见表 4-3。"]],
-        [6.4, 9.0])
-
-    heading(doc, "5.5 仍需补充的工作", 2)
-    bullet_rich(doc, [("精读两个主参考仓库的源码。", True), ("本文档的能力对照基于 README 与文档描述,建议在动工前用半天时间通读学之思的答卷表结构与砚考的防作弊事件表,把字段级细节记入笔记,这对第 4 章的数据模型设计帮助极大。", False)])
-    bullet_rich(doc, [("补充核心期刊文献。", True), ("表 4-2 的检索方向需要落到具体论文,每个研究点至少三篇,并在第 2 章形成综述段落。这一步无法由开源调研替代。", False)])
-    bullet_rich(doc, [("验证 or-tools 的 Java 绑定可用性。", True), ("若计划把 CP-SAT 作为对照组,需提前验证 JDK 17 下的依赖引入与求解器调用,避免实验阶段才发现环境问题。", False)])
-    bullet_rich(doc, [("确认许可证声明的时效性。", True), ("星标数与提交时间随仓库变化,论文定稿前应重新查询一次并更新表 2-1 的数据与查询日期。", False)])

+ 0 - 19
verify_doc.py

@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-import sys, os
-sys.stdout.reconfigure(encoding="utf-8")
-from docx import Document
-base = os.path.join(os.path.expanduser("~"), "Desktop", "在线考试系统")
-d = Document(os.path.join(base, "在线考试系统开源方案调研与论文写作参考.docx"))
-for i, x in enumerate(d.paragraphs):
-    if "11 月 5 日" in x.text or "2026-11-05" in x.text:
-        print("P", i, x.text[:120])
-for ti, t in enumerate(d.tables):
-    for ri, row in enumerate(t.rows):
-        for c in row.cells:
-            if "2026-11-05" in c.text:
-                print("T", ti, ri, c.text[:40])
-            if "nan" in c.text:
-                print("NAN-T", ti, ri, c.text[:60])
-for i, x in enumerate(d.paragraphs):
-    if "nan" in x.text:
-        print("NAN-P", i, x.text[max(0,x.text.find("nan")-20):x.text.find("nan")+20])

+ 0 - 51
verify_optimized_plan.py

@@ -1,51 +0,0 @@
-# -*- coding: utf-8 -*-
-from docx import Document
-from docx.oxml.ns import qn
-from zipfile import ZipFile
-from pathlib import Path
-
-p = Path("在线考试系统实现方案(优化定稿版).docx")
-doc = Document(p)
-parts = [x.text for x in doc.paragraphs]
-for table in doc.tables:
-    for row in table.rows:
-        for cell in row.cells:
-            parts.append(cell.text)
-text = chr(10).join(parts)
-headings = [(x.style.name, x.text) for x in doc.paragraphs if x.style.name.startswith("Heading")]
-required = [
-    "方案 B + 评审整改 + 协议补强", "P0", "P1", "P2", "R1", "R10",
-    "answer_item(session_id, question_id)", "answer_operation", "opId", "payloadHash",
-    "base_version", "server_seq", "STALE_VERSION", "SUBMIT_UNKNOWN",
-    "UNIQUE submission(session_id)", "requestHash", "C10", "13 周",
-    "2026-11-10", "不能直接复用", "不能证明其已经实现", "R8 未关闭",
-    "先证明“不会错”,再测量“跑多快”"
-]
-for s in required:
-    assert s in text, s
-for bad in ["TODO", "待补充", "待填写"]:
-    assert bad not in text, bad
-assert len(doc.tables) == 21, len(doc.tables)
-assert "DUPLICATE_CONTENT" not in text
-assert "CONTENT_ALREADY_EQUAL" in text
-assert "会话语义正确率 = 故障结束后" in text
-assert "status、next_seq、version" not in text
-assert "状态迁移用会话行锁,不另设version" in text
-for status in ["APPLIED", "DUPLICATE", "STALE_VERSION", "SESSION_CLOSED", "REJECTED"]:
-    assert status in text
-from optimized_plan_content import EXAM_ANSWER, PROCTOR_EVENT, PROCTOR_SESSION
-for link in [EXAM_ANSWER, PROCTOR_EVENT, PROCTOR_SESSION]:
-    assert "/blob/master/" in link and "/domain/entity/" not in link and "/blob/main/" not in link
-with ZipFile(p) as z:
-    xml = z.read("word/document.xml").decode("utf-8")
-    assert "TOC " in xml and "1-2" in xml
-    assert xml.count("<w:hyperlink") >= 9
-    settings = z.read("word/settings.xml").decode("utf-8")
-    assert "updateFields" in settings
-print("path:", p.resolve())
-print("bytes:", p.stat().st_size, "paragraphs:", len(doc.paragraphs), "tables:", len(doc.tables), "headings:", len(headings))
-print("hyperlinks:", xml.count("<w:hyperlink"))
-print("top-level headings:")
-for style, value in headings:
-    if style == "Heading 1": print(" -", value)
-print("All structural and content checks passed.")