公司动态

考试发布后临时延期、暂停或取消,已经进入考试的人怎么办?状态机、任务补偿与数据一致性设计

📅 2026/8/10 10:49:26
考试发布后临时延期、暂停或取消,已经进入考试的人怎么办?状态机、任务补偿与数据一致性设计
前言很多在线考试系统在设计第一版时考试生命周期可能只有三个状态未开始 ↓ 考试中 ↓ 已结束看起来已经足够。但系统真正投入企业以后很快就会遇到一些在产品演示阶段很少出现的问题。例如原计划上午9:00开始考试。8:55管理员突然收到通知会议临时调整考试推迟到下午2:00。问题来了已经有考生提前登录怎么办已经获取试卷的人怎么办如果试卷已经随机生成是保留还是作废再比如考试已经开始20分钟。此时企业网络发生故障。管理员希望先把全场考试暂停网络恢复后再继续。那么考试倒计时 答题保存 自动交卷任务 Exam Session 服务器时间应该如何处理更加棘手的情况是考试开始30分钟以后发现一道关键题目的标准答案配置错误。管理人员决定先暂停考试确认以后再决定继续还是取消。这时候一部分考生已经答完一部分还在答一部分甚至已经交卷。所以大型在线考试真正需要解决的并不是有没有“暂停”按钮。而是当考试状态发生变化时与它相关的所有业务状态能不能一起正确变化。这本质上是一个典型的状态机 分布式业务一致性问题。一、为什么不能直接在考试表增加一个status字段最简单的设计是UPDATE exam SET status PAUSED WHERE id 10001;后台页面上考试状态已暂停似乎完成了。但考生端可能仍然继续答题 ↓ 继续自动保存 ↓ 倒计时继续减少 ↓ 时间到了自动交卷原因很简单。真正正在运行的考试并不只有exam这一张表。至少还存在Exam ExamSession PaperInstance Answer Timer SubmitTask Notification Score AuditLog因此Exam.status PAUSED只是修改了一个“管理状态”。并没有自动改变所有正在执行中的业务。二、先把考试生命周期设计成真正的状态机相比0 未开始 1 开始 2 结束更加完整的考试状态可以设计为DRAFT ↓ PUBLISHED ↓ WAITING ↓ RUNNING ↓ FINISHED同时增加异常流转RUNNING ↓ PAUSED ↓ RUNNING以及PUBLISHED / WAITING ↓ RESCHEDULED和PUBLISHED WAITING RUNNING PAUSED ↓ CANCELLED因此完整状态大致可以表示为DRAFT ↓ PUBLISHED ↓ WAITING ↓ RUNNING ─────→ PAUSED ↑ │ └──────────────┘ ↓ FINISHED PUBLISHED / WAITING ↓ RESCHEDULED PUBLISHED / WAITING / RUNNING / PAUSED ↓ CANCELLED这里最重要的一点是不是任何状态都允许跳到任何状态。三、为什么状态转换必须有规则例如FINISHED以后管理员原则上不能简单执行FINISHED → RUNNING否则已经公布的成绩 排名 证书 培训档案都会受到影响。同样CANCELLED以后通常不能直接CANCELLED → RUNNING更加合理的方式可能是取消原考试 ↓ 复制考试配置 ↓ 创建新考试所以可以明确状态转换表当前状态允许操作DRAFT发布、删除PUBLISHED修改时间、取消WAITING延期、取消、开始RUNNING暂停、结束、取消PAUSED恢复、取消、结束FINISHED查看、统计、归档CANCELLED查看、重新创建状态机最大的价值就是防止出现业务上根本不应该发生的状态。四、管理员点击“暂停”系统到底应该暂停什么真正的暂停考试至少应该影响四层状态。第一层Exam全局考试状态RUNNING ↓ PAUSED第二层Exam Session每名考生正在进行的会话需要进入ACTIVE ↓ SUSPENDED不能继续按照正常Session处理。第三层Timer考试剩余时间必须冻结。例如考试总时长60分钟 已考试23分钟 剩余37分钟暂停发生以后即使系统10分钟以后恢复仍然应该剩余37分钟而不是剩余27分钟。第四层自动任务例如到时自动交卷 考试结束任务 成绩计算任务 通知任务都必须根据新的状态重新判断。所以一个真正的暂停动作实际上可能是管理员暂停 ↓ Exam状态更新 ↓ Session冻结 ↓ 记录暂停时间 ↓ 冻结倒计时 ↓ 停止/失效自动交卷任务 ↓ 通知在线客户端 ↓ 写入审计日志而不是一条SQL。五、倒计时为什么不能完全相信浏览器很多在线考试前端会维护remainingTime--;如果系统暂停浏览器本地计时器很容易继续运行。另外还存在刷新页面 浏览器休眠 电脑时间改变 手机切后台 网络断开等问题。因此正式考试时间应该以Server Time为准。一种比较合理的模型是记录start_time total_duration pause_duration剩余时间大致根据剩余时间 总时长 - 有效考试时间计算。其中有效考试时间 当前服务器时间 - 开始时间 - 累计暂停时间如果考试处于PAUSED状态则使用pause_time作为计算终点。六、建议不要反复修改“剩余秒数”一种看似简单的方式是remaining_seconds 2220然后每秒修改。这会制造大量UPDATE实际上没必要。更合理的方式是保存关键时间点started_at paused_at resumed_at total_paused_seconds例如09:00 开始 09:23 暂停 09:33 恢复那么total_paused_seconds 600最终倒计时由服务器动态计算。这样刷新浏览器 换电脑 断线重连以后都能够重新得到相同的时间结果。七、考试暂停以后考生还能保存答案吗这需要明确业务规则。通常有两种方案。方案一暂停后禁止继续作答服务端判断Exam.status PAUSED则ANSWER_SAVE请求返回考试当前已暂停请等待管理员恢复。这种方案最公平。方案二允许保存暂停前已经产生的答案存在一种边界情况管理员10:20:00点击暂停。考生在10:19:59.800已经提交保存请求网络到服务端时已经10:20:00.100如果完全按照收到请求时间拒绝可能丢掉暂停前的正常操作。因此可以通过AnswerVersion RequestTime SessionVersion综合判断。但实现会复杂很多。对于多数企业正式考试而言更重要的是暂停指令一旦生效后续写操作必须建立明确边界。八、这里为什么需要ExamVersion假设考试当前version 8 status RUNNING管理员A点击暂停。同时管理员B点击结束考试。两个请求几乎同时到达服务器。如果简单执行UPDATE exam SET status ...最终结果取决于谁最后写数据库。这是危险的。可以使用乐观锁UPDATE exam SET status PAUSED, version version 1 WHERE id ? AND version 8 AND status RUNNING;只有影响行数 1说明状态转换成功。另外一个请求发现version已经变化应该重新读取当前状态而不是继续覆盖。九、Exam Session同样需要版本控制全局考试有ExamVersion每名考生的考试会话也可以维护SessionVersion例如SessionVersion 15暂停后15 → 16客户端保存答案时携带{ sessionId: ES10001, sessionVersion: 15 }服务端发现当前已经16就知道这是暂停之前的旧状态请求。可以拒绝或者进入专门的兼容处理。这样可以减少旧页面 延迟请求 重复请求对新状态造成污染。十、考试延期和考试暂停不是一回事这两个业务经常被混为一谈。延期 Reschedule一般发生在考试还没有正式开始例如原计划 09:0010:00 修改为 14:0015:00通常应该处理开始时间 结束时间 通知 定时开始任务 考生待办但原则上还不应该存在ACTIVE Exam Session。暂停 Pause一般发生在考试已经RUNNING此时已经存在考生 试卷 答案 Session 剩余时间所以暂停比延期复杂得多。十一、已经有人提前进入考试延期怎么办假设08:55管理员决定09:00考试延期到14:00但系统允许考生提前进入考试说明页甚至已经执行PaperInstance Create这时候应该区分进入考试页面和正式开始考试两个动作。理想状态应该是Login ↓ Exam Waiting Room ↓ Start Allowed? ↓ YES Create / Activate Session ↓ Load Paper而不是只要进入页面 ↓ 立刻创建正式Session这样考试延期时只需要修改Exam Schedule而不需要批量修复大量Session。十二、试卷已经随机生成了延期以后还用不用这取决于Paper Instance是在什么时候生成。随机组卷系统通常有两种方式。方式一进入考试时生成User Enter ↓ Generate Paper延期时如果考生还没有真正开始可以Invalidate旧试卷实例。正式考试重新生成。方式二考试前预生成例如为提高高并发性能提前生成10000份试卷那么延期以后通常不需要重新生成只要这些试卷没有泄露 规则没有变化 题库没有修改仍然可以继续使用。因此建议Exam和PaperInstance分别维护版本。例如ExamVersion 10 PaperRuleVersion 3 PaperInstanceVersion 3十三、暂停期间发现题目错误怎么办这是一个非常典型的实际场景。例如考试已经进行30分钟管理员发现第25题标准答案错误。第一反应可能是修改题库答案。但正式考试中不能简单直接修改历史试卷引用的数据。原因是已经有考生交卷也有考生尚未交卷。如果直接修改原题早交卷人员按旧答案计算后交卷人员按新答案计算会产生严重的不公平。因此正式考试最好采用Paper Snapshot Question Snapshot机制。考试开始以后历史试卷内容冻结题库修改不能自动改变已经生成的试卷快照。十四、题目错误更适合“考后统一重算”例如确认Q25标准答案从B修改成C可以生成一个ScoreRecalculateTask统一处理定位受影响考生 ↓ 读取试卷快照 ↓ 应用纠正规则 ↓ 重新计算成绩 ↓ 记录OldScore ↓ 记录NewScore ↓ 更新通过状态 ↓ 重新判断证书 ↓ 写审计日志而不是考试过程中直接修改历史答卷。十五、取消考试比暂停更危险考试取消可能发生在三个阶段。第一种尚未开始最简单WAITING ↓ CANCELLED同时撤销通知 撤销定时任务即可。第二种已经开始但无人交卷需要处理所有Active Session ↓ CANCELLED已经保存的答案可以保留但标记INVALID / CANCELLED用于日志追溯。不建议直接删除。第三种部分人员已经交卷最复杂。因为已经产生成绩 排名 通过状态 证书 培训档案这时候考试取消应该触发一系列补偿动作。十六、为什么这里需要“补偿事务”假设系统已经执行考试提交 ↓ 计算成绩 ↓ 生成证书 ↓ 写入培训档案 ↓ 发送通过通知管理员突然取消考试。数据库很难用一个简单事务ROLLBACK回到30分钟前。因为很多操作已经跨服务 跨表 甚至已经发送消息这时候需要Compensation也就是业务补偿。十七、取消考试可以怎样设计补偿链例如Cancel Exam ↓ Mark Exam CANCELLED ↓ Cancel Active Sessions ↓ Invalidate Results ↓ Revoke Certificates ↓ Rollback Training Status ↓ Cancel Scheduled Jobs ↓ Send Cancellation Notice ↓ Write Audit Log这里关键不是DELETE成绩而是成绩仍存在 但状态变成INVALID。例如score 86 status INVALID_BY_EXAM_CANCEL以后管理员仍然能够知道原来考试过只是考试后来被取消。十八、为什么不要直接删除成绩如果考试发生争议以后追查张三明明参加过为什么系统什么都没有如果数据库已经DELETE就无法解释。更加合理的是保留历史事实但标记业务是否有效。例如ResultStatus设计为VALID INVALID RECALCULATED CANCELLED这样能够同时兼顾业务结果和审计追溯。十九、自动交卷任务是最容易遗忘的一个坑考试开始以后系统可能已经建立AutoSubmitTask例如10:00自动交卷考试在09:30暂停。管理员09:50恢复。理论新的结束时间应该变成10:20如果原任务10:00 Auto Submit仍然存在系统会发生非常严重的问题考试虽然显示还有20分钟却突然全部被自动交卷。所以暂停动作必须同步处理旧任务失效恢复以后重新Schedule New Task二十、定时任务最好不要依赖“删除任务一定成功”在分布式系统中可能出现任务已经进入执行队列你再删除调度任务已经来不及。因此任务执行时本身必须再次判断Exam.status Exam.version例如if (exam.getStatus() ! RUNNING) { return; }进一步判断task.examVersion currentExamVersion如果不一致直接忽略旧任务。这是一种很重要的双重保险不是只依赖取消Scheduler而是Scheduler取消 执行前状态验证二十一、考试恢复时应该创建新Session吗一般不建议无条件创建。如果只是正常暂停ACTIVE ↓ SUSPENDED ↓ ACTIVE原Session最好继续使用。因为原Session关联PaperInstance AnswerVersion LoginLog Device IP如果每次暂停就换Session追溯反而更复杂。可以继续sessionId但增加sessionVersion resume_count last_resume_time例如ES10001 Version 18 ResumeCount 2二十二、什么时候才应该创建新的Exam Session例如原Session已经失效Session Corrupted管理员执行“重新考试”而不是继续考试。原考试被取消重新发布的是New Exam改变了试卷规则需要New Paper Instance这时候才更适合创建新的Session。二十三、暂停以后浏览器没有收到通知怎么办管理员暂停时可以通过WebSocket SSE Long Polling主动通知在线考生。例如{ event: EXAM_PAUSED, examId: 10001, version: 18 }前端收到以后锁定答题区 停止本地倒计时 显示暂停提示但不能只依赖WebSocket。因为考生可能断网 连接已经失效 浏览器睡眠所以任何后端接口例如Save Answer Submit Paper都必须重新检查Exam Status否则没收到WebSocket通知的客户端仍然可以继续答题。二十四、状态检查应该放在哪一层不要只放在前端if (paused) { button.disabled true; }因为用户可以修改JavaScript 直接调用API 网络请求延迟真正的状态限制应该在Backend Domain / Service Layer例如if (!examStateMachine.canSaveAnswer(examStatus)) { throw new ExamPausedException(); }前端控制只是用户体验服务端控制才是业务规则。二十五、管理员连续点击两次“暂停”怎么办这是典型的幂等问题。第一次RUNNING → PAUSED成功。第二次请求又到达。如果系统再次新增PauseRecord 重新停止Session 再次累加暂停时间就可能产生错误。因此Pause Exam应该具备Idempotency第二次发现status PAUSED可以直接返回当前考试已经暂停。不能重复执行副作用。二十六、建议给管理员操作增加OperationId例如operationId UUID管理员操作{ operationId: OP-20260810-001, examId: 10001, action: PAUSE }系统维护OperationLog如果同一个operationId重复到达直接返回原来的操作结果。这对页面重复点击 浏览器重试 网关重试都非常有帮助。二十七、真正危险的是“部分成功”假设暂停考试需要执行5步1. 修改Exam状态 2. 暂停Session 3. 取消AutoSubmit 4. 通知客户端 5. 写日志执行到步骤3服务器宕机。可能出现Exam PAUSED但部分Session仍然ACTIVE这就是Partial Failure因此系统必须能够检测 恢复 补偿。二十八、是否必须做分布式事务不一定。考试系统很多场景并不适合为了所有操作都引入重量级2PC XA更加实用的方式通常是本地事务 事件 幂等 重试 补偿例如BEGIN UPDATE exam INSERT exam_operation_event COMMIT然后后台消费者ExamPausedEvent ↓ 暂停Session ↓ 重建Scheduler ↓ 推送通知每一步都要求Idempotent最终实现Eventual Consistency二十九、状态改变以后最好有一个Reconcile任务除了实时事件处理还可以增加Reconciliation任务。例如每隔一定周期检查Exam PAUSED但是否存在ACTIVE Session如果发现Exam PAUSED Session ACTIVE说明出现异常。系统自动执行Fix Session类似地Exam CANCELLED却仍然有VALID Result也应该被检测出来。这种最终一致性巡检对于复杂业务系统非常实用。三十、管理员到底应该看到什么考试暂停不能只是按钮变灰。管理员应该看到考试状态已暂停 暂停时间10:23:15 操作人员管理员A 暂停原因网络异常 当前考试人数826 已交卷174 暂停Session826恢复以后记录恢复时间 累计暂停时长 恢复人员如果取消取消原因 影响人数 已交卷人数 已生成成绩数 已生成证书数 补偿任务状态这样异常处理才真正可管理。三十一、审计日志是考试状态机不可缺少的一部分建议记录ExamId OperationId Operator BeforeStatus AfterStatus ExamVersion OperateTime Reason ClientIP Result例如考试2026年度安全知识考试 操作 PAUSE 操作前 RUNNING 操作后 PAUSED 操作人员 管理员A 时间 10:23:15 原因 园区网络异常 ExamVersion 17 → 18恢复18 → 19以后如果出现争议可以完整恢复考试状态变化时间线。三十二、以宏远培训考试系统为例暂停功能真正应该解决什么在企业正式考试场景中管理员最希望看到的通常不是“系统支持暂停。”而是“暂停以后不会把正在考试的数据搞乱。”以宏远培训考试系统的考试管理思路为例考试本身并不是孤立存在的。它通常同时关联考试场次 人员范围 试卷 答题记录 考试时间 考试Session 交卷状态 成绩 证书 一人一档 操作日志因此对于暂停、恢复等管理动作更合理的设计是把考试状态 考生状态 时间状态 答题状态 交卷状态一起纳入控制。例如大型集中考试过程中出现临时网络故障管理员执行暂停考试系统不应该简单理解为修改一个状态字段。而应该进一步确保当前Session冻结 剩余考试时间得到保护 答案继续保留 暂停期间禁止新的有效答题 自动交卷不会错误执行 恢复以后继续原来的答卷 异常过程能够查询日志对于宏远培训考试系统这类面向企业培训与正式考试一体化管理的平台而言这类状态控制的价值还在于考试结果最终可能继续影响培训计划完成状态 考试通过状态 补考安排 证书 个人培训档案所以暂停、取消等操作不能只处理考试页面。还必须考虑其后面的业务链。这也是企业级考试系统和简单在线答题工具之间一个比较明显的区别。三十三、考试取消以后对培训计划有什么影响假设培训流程课程学习 ↓ 正式考试 ↓ 考试通过 ↓ 培训完成 ↓ 生成证书如果考试后来CANCELLED那么之前已经生成培训完成状态的人怎么办这就需要设计Derived State也就是派生状态。例如培训完成不能简单保存为永久completed true而应该能够追溯completion_source EXAM10001当EXAM10001失效系统能够重新评估培训计划完成条件。三十四、证书也是同样的问题假设考试通过 ↓ 自动生成证书后来考试取消。不能简单DELETE certificate更加合理的状态ACTIVE ↓ REVOKED同时记录revoke_reason EXAM_CANCELLED这样既保持当前证书无效又保留历史上曾经生成过这张证书这一事实。三十五、一个比较完整的考试状态变更架构最终可以形成管理员操作 ↓ Command API ↓ Permission Check ↓ State Machine ↓ Optimistic Lock ↓ Exam Version ↓ Operation Event ↓ ┌─────────────────────────┐ │ Session Handler │ │ Timer Handler │ │ Scheduler Handler │ │ Result Handler │ │ Certificate Handler │ │ Notification Handler │ │ Audit Handler │ └─────────────────────────┘ ↓ Reconcile ↓ 最终一致这里State Machine负责允许不允许这样变。Version负责防止并发覆盖。Idempotency负责防止重复执行。Event负责通知其他业务。Compensation负责已经产生结果以后怎么撤销。Reconcile负责部分失败以后怎么最终修复。三十六、测试时必须覆盖哪些异常场景上线前至少应该测试1. 考试开始前延期考生还未登录。2. 考试开始前延期已有考生进入候考页。3. 考试进行中暂停无人交卷。4. 考试进行中暂停已有部分人员交卷。5. 暂停后恢复剩余时间是否正确。6. 暂停期间刷新浏览器能否正确恢复暂停状态。7. 暂停期间断网后重新登录是否仍然禁止继续答题。8. 管理员连续点击两次暂停是否重复累计时间。9. 两名管理员同时暂停和结束状态是否发生覆盖。10. 原自动交卷任务仍然触发系统能否通过版本检查阻止。11. 已生成证书以后取消考试证书是否正确撤销。12. 补偿任务中途失败是否能够重试和恢复。这些异常测试比点击暂停 页面显示已暂停更加重要。三十七、状态机设计最重要的5条原则总结下来第一不允许自由修改status所有状态变化必须经过State Machine第二考试和Session都应该有Version避免并发覆盖 旧请求污染新状态。第三时间以服务器状态为准不要完全依赖浏览器倒计时。第四副作用必须幂等例如暂停Session 撤销证书 取消任务都要允许安全重试。第五历史数据尽量失效不直接删除保留为什么失效 什么时候失效 谁操作的。三十八、结语在线考试系统中的开始 暂停 恢复 延期 取消 结束看起来只是几个按钮。但真正落到企业正式考试中每个按钮背后实际对应的都是一整条业务链。例如“暂停考试”真正影响的是Exam ↓ Exam Session ↓ Timer ↓ Answer ↓ Auto Submit ↓ Score ↓ Certificate ↓ Training Record ↓ Audit Log因此一个成熟的考试系统不应该采用管理员改status ↓ 其他模块自己看着办这种松散模式。更加可靠的设计应该是管理员Command ↓ 状态机校验 ↓ Version并发控制 ↓ 状态变更 ↓ 事件分发 ↓ 幂等处理 ↓ 失败补偿 ↓ 一致性巡检 ↓ 审计留痕从用户角度看管理员只点击了一次暂停考试。但从系统架构角度看真正需要保证的是考试暂停以后整个系统对“这场考试现在到底是什么状态”的理解始终一致。而这恰恰是企业级在线考试系统中最容易被一个简单按钮掩盖的技术问题。