# -*- 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()