|
|
@@ -0,0 +1,382 @@
|
|
|
+# -*- 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)}")
|