build_doc.py 8.0 KB

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