尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

员工说答案没保存,管理员到底怎么查?从答题版本、操作日志到考试争议追溯链路

员工说答案没保存,管理员到底怎么查?从答题版本、操作日志到考试争议追溯链路 企业组织在线考试时管理员最难处理的问题往往不是“系统打不开”而是考试结束以后出现这样一句话“我这道题明明答了为什么系统里面没有”接下来往往还有更多争议“我最后已经改成C了为什么系统显示B”“我明明点了交卷为什么后台显示没有提交”“考试中间断过一次网是不是系统把我的答案弄丢了”“我做完80道题为什么最后只有76道有答案”如果系统只能告诉管理员考试状态已完成 成绩82分实际上几乎无法回答这些问题。因为真正需要追溯的不是“最终成绩”而是这名考生在什么时间对哪一道题做了什么操作请求有没有到达服务器服务器保存的是哪个版本网络有没有异常最终交卷又采用了哪一个版本。因此一个适合正式企业考试的系统答案保存不能只设计成一条简单的UPDATE exam_answer SET answer ? WHERE user_id ? AND question_id ?而应该把答题过程放进完整的Exam Session、答案版本、操作事件、网络事件和提交日志中。本文从在线考试系统技术设计角度讨论管理员遇到“答案没有保存”争议时应该怎样排查以及考试系统应该怎样建立一条真正可以复核的考试数据链路。一、“答案没保存”其实至少有六种完全不同的问题用户说“答案没保存。”这句话从技术角度看并不准确。因为最终表现虽然都是“后台答案不对”实际原因可能完全不同。至少可以分成以下几种情况。1. 用户实际上没有完成操作例如考生点击了选项但是浏览器页面已经卡死JavaScript事件没有触发页面还没有完成初始化浏览器被强制关闭。此时用户视觉上可能认为自己选择了答案但应用层并没有产生有效保存请求。2. 客户端产生了答案但请求没有发送成功例如用户选择答案C ↓ 浏览器状态已经变成C ↓ 准备发送服务器 ↓ 网络突然断开这时候浏览器页面看到的是C。但是数据库可能还是B。如果系统没有本地缓存和重试机制答案就可能停留在客户端。3. 请求发出去了但服务器没有成功落库例如客户端发送答案 ↓ 网关收到 ↓ 应用服务处理 ↓ 数据库连接异常 ↓ 事务回滚此时需要继续判断客户端有没有收到失败响应系统有没有重试有没有错误日志4. 新答案被旧请求覆盖这是在线考试中比较隐蔽的问题。假设考生快速修改同一道题A → B → C客户端依次发送version 11 A version 12 B version 13 C但由于网络延迟服务器实际收到顺序可能是13 → 11 → 12如果数据库只是简单覆盖UPDATE exam_answer SET answer :answer最终数据库可能变成B于是考生就会说“我最后明明选的是C。”而且他说的可能确实是真的。这就是为什么正式考试的答案保存最好增加Version版本控制。5. 答案保存成功但交卷读取了错误的数据也存在一种情况单题答案表已经保存正确。但是交卷计算成绩时读取了缓存旧数据使用了过期快照查询条件错误Session状态不一致。最终产生的成绩仍然可能错误。所以“答案保存成功”和“最终阅卷使用了正确答案”是两件不同的事情。6. 答案和成绩其实都正确只是用户记错了正式考试同样存在这种情况。例如考生认为第38题我选择的是C但日志显示10:32:14 选择C 10:34:27 修改为B 10:34:28 保存成功最终交卷答案B如果系统有完整记录争议很容易解决。如果系统没有记录则管理员只能在“员工坚持说自己选了C”和“数据库里确实是B”之间进行判断。这就是考试系统为什么必须具备过程追溯能力。二、管理员排查时不要先看成绩应该先找到Exam Session正式在线考试最好为每一次考试生成唯一会话。例如exam_session_id EX202608080001568这个Session应该代表某一名考生参加某一场考试的完整生命周期。例如Exam └── User └── Exam Session一次Exam Session至少可以记录session_id exam_id user_id paper_id start_time end_time submit_time status client_ip device_id last_heartbeat管理员处理争议时第一步不应该直接查成绩表而应该先查exam_session_id然后围绕同一个Session向下追踪。例如ExamSession │ ├── AnswerVersion ├── OperationLog ├── NetworkEvent ├── HeartbeatLog ├── SubmitLog ├── MonitorEvent └── ScoreResult这样才能把一次考试真正串起来。三、第一步先确认考生到底什么时候进入考试例如员工说“我从9点一直答到11点。”管理员首先应该查看Session。例如考生张三 考试2026年度安全生产知识考试 Session ID EX202608080001568 进入考试 09:02:16 正式开始答题 09:02:24 最后心跳 10:57:42 交卷 10:58:06 状态 SUBMITTED首先确认是否真正进入考试有没有生成有效Session有没有异常重新进入有没有多个Session。尤其要注意一种情况同一名员工 ↓ 电脑A进入考试 ↓ 关闭浏览器 ↓ 电脑B重新登录如果系统设计不严谨可能产生Session A Session B那么争议就不只是答案保存问题而是最终成绩到底对应哪一次会话。因此一个正式考试通常应该明确同一考生 同一考试允许多少个有效Session。四、第二步不要只看“最终答案”要看答案Version传统答案表可能只有user_id question_id answer例如10086 Q0038 B这只能告诉管理员数据库现在是B。但是管理员真正想知道的是它为什么变成B因此可以增加答案版本机制。例如answer_id session_id question_id answer version client_time server_time sync_status某一道题的操作可能变成Q0038 Version 1 09:25:16 答案A Version 2 09:26:08 答案C Version 3 09:27:41 答案B最终Current Version 3 Final Answer B管理员立刻能够知道员工不是从来没有选择C。而是曾经选择C 后来又修改成B这对于处理考试争议非常重要。五、为什么答案Version最好由客户端和服务器共同参与最简单的方式可以让客户端维护版本。例如{ sessionId: EX202608080001568, questionId: Q0038, answer: C, version: 18 }用户再次修改{ sessionId: EX202608080001568, questionId: Q0038, answer: B, version: 19 }服务器当前数据库version 18收到version 19允许更新。如果后来网络中又到达一条旧请求version 17服务器判断17 19直接拒绝覆盖。伪代码可以类似if (request.getVersion() currentAnswer.getVersion()) { updateAnswer( request.getAnswer(), request.getVersion() ); }更严谨一点还可以加入乐观锁。例如UPDATE exam_answer SET answer :answer, version :newVersion, update_time NOW() WHERE session_id :sessionId AND question_id :questionId AND version :newVersion;然后根据affected_rows判断更新是否成功。这样可以避免旧数据覆盖新答案。六、为什么不能只记录浏览器时间日志中最好同时保存client_time server_time因为客户端时间是不完全可信的。例如{ questionId: Q0038, answer: B, clientTime: 10:31:58, serverTime: 10:32:02 }考后复核时应该主要以server_time进行排序。原因包括用户可能修改电脑时间客户端时间可能存在偏差不同电脑时钟不一致浏览器睡眠可能影响时间逻辑。因此正式证据链一般应该遵循服务端时间负责统一排序客户端时间作为辅助信息。七、第三步查这道题有没有“保存成功事件”一套系统如果只有数据库结果没有保存过程日志排查非常困难。更加合理的方式是每一次保存都产生结构化事件。例如{ eventType: ANSWER_SAVE, sessionId: EX202608080001568, questionId: Q0038, version: 19, serverTime: 10:32:02, result: SUCCESS }失败则可以记录{ eventType: ANSWER_SAVE, sessionId: EX202608080001568, questionId: Q0038, version: 20, serverTime: 10:32:14, result: FAILED, errorCode: DB_TIMEOUT }这样管理员可以明确区分第一种情况根本没有发送第二种情况发送了但是失败第三种情况发送并保存成功这是完全不同的三个结论。八、第四步检查当时有没有网络异常假设日志显示10:31:52 Q0038 Version 18 保存成功 10:32:06 网络断开 10:32:20 用户修改Q0038为C 10:33:16 网络恢复这时就要继续查断网期间修改的答案有没有进入本地缓存成熟一点的考试系统可以把答题保存分成两层用户答题 ↓ 本地临时存储 ↓ 服务器同步正常情况答案产生 ↓ Local ↓ Server ↓ SYNCED网络异常答案产生 ↓ Local ↓ PENDING网络恢复读取PENDING ↓ 重新发送 ↓ 服务器确认 ↓ SYNCED因此管理员还应该能够看到LOCAL_SAVE SYNC_PENDING SYNC_SUCCESS SYNC_FAILED等状态。九、第五步查断网以后到底恢复了哪些答案例如一次网络异常可能产生10:32:06 网络断开 Q038 Version 20 PENDING Q039 Version 8 PENDING Q040 Version 4 PENDING10:33:16恢复网络以后Q038 Version 20 SYNC_SUCCESS Q039 Version 8 SYNC_SUCCESS Q040 Version 4 SYNC_SUCCESS那么可以基本判断断网并没有导致这三道题最终丢失。相反如果Q038 Version 20 PENDING但是后面没有SYNC_SUCCESS管理员就应该继续分析为什么没有上传成功。可能包括页面直接关闭浏览器缓存被清理Session已经超时上传接口失败考试已经结束服务器拒绝新版本。因此“支持断点续考”不能只理解成页面还能重新打开。真正意义上的断点恢复还应该包括考试状态恢复 剩余时间恢复 历史答案恢复 待同步答案恢复十、一个容易被忽略的问题断点续考时要判断服务器和本地谁更新假设服务器上Q038Version 20 Answer B浏览器本地Version 21 Answer C重新进入考试以后不能简单服务器覆盖客户端否则本地最新答案可能丢失。但也不能客户端全部覆盖服务器因为客户端不是最终可信数据源。更加合理的流程应该是本地Version ↓ 服务器Version ↓ 比较版本 ↓ 校验Session状态 ↓ 校验考试时间 ↓ 决定是否允许同步例如localVersion 21 serverVersion 20 考试仍在进行 ↓ 允许上传Version 21如果已经考试结束 Session SUBMITTED那么即使客户端还有Version 21服务器通常也不应该继续接受。否则可能出现交卷以后还能修改答案。十一、第六步检查切题操作和答案保存是不是同一个时间点用户可能说“我明明点了C然后切到了下一题。”但是从系统设计角度需要确认切题之前是否真正完成保存。例如不推荐selectAnswer(); goNextQuestion(); saveAnswerAsync();因为页面已经切换但保存可能失败。更加稳妥的逻辑可以是用户选择答案 ↓ 更新本地状态 ↓ 加入保存队列 ↓ 异步同步服务器此时切题并不等于保存完成。因此系统界面最好能够体现已保存 正在保存 待同步这样的状态。尤其是在正式考试中可以减少用户认知差异。十二、第七步检查最后一次心跳考试过程中通常会存在心跳。例如10:30:00 HEARTBEAT 10:30:20 HEARTBEAT 10:30:40 HEARTBEAT 10:31:00 HEARTBEAT突然10:31:20 无心跳一直到10:34:15 HEARTBEAT RECOVER那么可以判断这段时间客户端与服务器之间可能存在网络异常。如果员工争议的答案修改时间正好位于10:31:20 ~ 10:34:15就应该优先检查本地缓存 同步队列 网络恢复日志而不是直接查看最终数据库答案。十三、第八步查交卷到底发生了什么员工经常会说“我明明点了交卷。”这时需要区分点击交卷和服务器确认交卷完全不是一回事。一个完整交卷链路可能是用户点击交卷 ↓ SUBMIT_CLICK ↓ 检查待保存答案 ↓ ANSWER_FLUSH ↓ 提交请求 ↓ SERVER_ACCEPT ↓ 成绩计算 ↓ SUBMITTED日志可以类似10:58:01 SUBMIT_CLICK 10:58:01 FLUSH_PENDING_ANSWER pending_count 2 10:58:02 ANSWER_SYNC_SUCCESS 10:58:03 SUBMIT_REQUEST 10:58:04 SUBMIT_ACCEPTED 10:58:05 SCORE_GENERATED 10:58:05 SESSION_SUBMITTED只有完成到SESSION_SUBMITTED才能真正认为本次考试已经成功交卷。十四、交卷以前最好先执行一次答案Flush假设用户最后一分钟快速回答Q078 Q079 Q080这些请求可能还停留在客户端保存队列如果立即执行交卷submitExam()后台就可能比最后几个答案更早收到交卷请求。因此比较合理的流程是点击交卷 ↓ 停止继续答题 ↓ Flush待保存答案 ↓ 等待服务器确认 ↓ 提交考试伪代码async function submitExam() { disableAnswering(); await flushPendingAnswers(); await submitExamSession(); }当然在实际高并发考试中还需要考虑超时与失败重试。十五、服务器交卷时最好再次核对答案版本服务器收到交卷请求以后可以读取ExamSession当前所有最终答案。例如Q001 Version 3 Q002 Version 1 Q003 Version 7 ... Q080 Version 4并把这批答案形成交卷快照。例如ExamSubmissionSnapshot快照可以包含session_id question_id answer answer_version submit_time这样即使以后管理员修改标准答案重新计算成绩调整试题处理错题仍然能够知道考生当时交卷使用的究竟是哪一个答案版本。这对正式考试尤其重要。十六、为什么不能直接使用“当前答案表”作为永久证据假设考试结束后发现一道题标准答案错误。管理员进行了成绩重新计算如果系统没有保存试卷快照和交卷快照后续就可能很难回答重新计算前和重新计算后的数据有什么变化更加合理的结构应该区分答题过程数据 ↓ 交卷快照 ↓ 评分结果 ↓ 成绩版本例如Submission Version 1 Score 82 标准答案修正 Score Version 2 Score 84并记录谁修改 什么时候修改 为什么重新计算这样才能形成真正可审计的数据链路。十七、考试争议追溯最好形成一条Timeline管理员最终并不希望查看十几张数据库表。真正好用的后台应该把所有日志整理成时间线。例如某名考生09:02:16 登录系统 09:02:24 进入考试 09:15:08 Q001 Version 1 保存成功 09:25:16 Q038 Version 1 A 09:26:08 Q038 Version 2 C 09:27:41 Q038 Version 3 B 10:31:20 网络异常 10:31:46 Q052 Version 4 本地待同步 10:32:18 网络恢复 10:32:19 Q052 Version 4 同步成功 10:46:28 发生一次切屏 10:57:52 Q080 Version 2 保存成功 10:58:01 点击交卷 10:58:02 待保存答案全部同步完成 10:58:04 服务端接受交卷 10:58:05 成绩生成 10:58:05 考试完成管理员看到这条时间线以后很多争议可以直接定位。十八、建议把日志统一设计成Event模型从系统架构角度看与其为每一种行为单独设计一套完全不同的日志结构不如增加统一事件模型。例如ExamEvent字段可以包含event_id session_id user_id event_type question_id event_data client_time server_time ip device_id risk_level事件类型LOGIN FACE_VERIFY EXAM_ENTER ANSWER_CHANGE ANSWER_SAVE ANSWER_SAVE_FAILED NETWORK_DISCONNECT NETWORK_RECOVER PAGE_HIDDEN PAGE_VISIBLE HEARTBEAT SUBMIT_CLICK SUBMIT_ACCEPT SCORE_GENERATED这样管理员查询session_id就可以获得全部考试事件。十九、event_data可以存什么不同事件的数据不同。可以采用event_data存放附加信息。例如答题事件{ answer: C, version: 18, saveStatus: SUCCESS }网络事件{ offlineDuration: 62 }切屏事件{ duration: 15, questionId: Q052 }交卷事件{ pendingAnswers: 0, answerCount: 80 }这样事件结构保持统一业务数据又具有扩展性。二十、日志很多会不会影响考试系统性能这是一个实际问题。如果1000名考生 × 80道题 × 多次修改 × 大量心跳日志数量会非常大。因此不能简单每一个前端动作 同步写数据库否则日志本身反而可能拖慢考试。可以考虑客户端 ↓ 业务接口 ↓ 核心数据立即保存 ↓ 事件进入消息队列 ↓ 异步日志服务 ↓ 日志存储例如答题请求 │ ├── 核心答案表同步处理 │ └── ExamEvent异步处理这里的原则是核心答案数据优先保证一致性审计日志在不影响证据完整性的前提下进行异步化。二十一、心跳日志也不一定每20秒永久保存一条假设1000人 每20秒一次心跳 考试2小时单场考试就可能产生360 × 1000 36万条心跳如果考试规模继续扩大数据会迅速增加。因此可以采用正常心跳只更新last_heartbeat_time异常状态变化才写事件ONLINE → OFFLINE OFFLINE → ONLINE例如最终日志只保留10:31:20 NETWORK_DISCONNECT 10:32:18 NETWORK_RECOVER duration 58s这样比保存全部正常心跳更有分析价值。二十二、管理员实际排查可以按照“八步法”如果遇到员工反馈“答案没有保存。”管理员可以按照下面顺序检查。第一步查Session确认是不是这个人 是不是这场考试 是不是这个Session第二步查最终提交状态确认EXAMING SUBMITTING SUBMITTED TIMEOUT当前到底属于什么状态。第三步查争议题答案Version例如Q038 V16 A V17 C V18 B确认最终版本。第四步查保存结果确认ANSWER_SAVE_SUCCESS 还是 ANSWER_SAVE_FAILED第五步查网络事件重点看争议时间前后有没有NETWORK_DISCONNECT NETWORK_RECOVER第六步查本地待同步数据确认有没有PENDING最终有没有变成SYNCED第七步查交卷日志确认SUBMIT_CLICK之后是否完成SUBMIT_ACCEPT第八步查交卷快照确认最终参与阅卷的answer answer_version到底是什么。经过这八步绝大部分“答案没保存”问题都可以被明确归类。二十三、一个完整的争议排查示例假设员工反馈“第52题我最后选择的是D但系统显示没有作答。”管理员查询Exam Session EX202608080001568得到10:31:18 Q052 Version 3 B 服务器保存成功 10:31:20 网络断开 10:31:46 Q052 Version 4 D 本地保存 状态PENDING 10:32:18 网络恢复 10:32:19 Version 4同步请求发送 10:32:19 服务器返回 SESSION_TIMEOUT 10:32:20 Q052 Version 4 同步失败 10:32:21 考试自动交卷这时原因就非常明确员工确实在本地选择了D。但这个答案产生时网络处于离线状态恢复以后考试已经超时服务器按照考试规则拒绝了新的答案版本。最终阅卷使用Version 3 B这种情况下不能简单告诉员工“数据库里面就是B。”而应该能够解释完整过程。这才是争议追溯系统真正的价值。二十四、另一种情况系统Bug也应该能够被日志发现例如管理员查询Version 18 C ANSWER_SAVE_SUCCESS并且数据库当时确实成功保存。但是Submission Snapshot里面却是Version 17 B那么问题就可能已经从考生网络转变为交卷服务读取旧版本这时技术人员可以继续排查Redis缓存数据库主从延迟ORM缓存事务隔离交卷查询逻辑。也就是说好的日志不是为了证明系统永远没有问题。恰恰相反它的价值是如果系统真的有问题也能够找到问题发生在哪个环节。二十五、宏远培训考试系统在这类场景中的设计思路在企业正式考试项目中宏远培训考试系统对于答题记录和考试争议处理也更强调考试过程留痕而不是只保留最终成绩。例如管理员可以从考试记录、答题情况、登录情况、考试日志以及相关监考信息中对考生考试过程进行排查。在系统持续优化时还可以进一步围绕一次Exam Session把人员登录 考试进入 答题保存 网络状态 切屏异常 重新进入 交卷 成绩生成形成更加统一的考试事件链路。对于集团统考、安全生产考试、技能竞赛等严肃考试场景这种设计的意义并不仅仅是方便技术人员查Bug。更加重要的是在出现员工申诉、成绩争议或者考试异常时管理员能够给出基于数据的处理依据。二十六、正式考试真正需要的不是“自动保存”而是“可证明保存”很多系统产品介绍都会写支持答案自动保存。但是从技术角度看这句话其实还不够。企业真正应该关心的是保存了什么 什么时候保存 保存是否成功 当前是哪个版本 网络断开怎么办 恢复以后有没有同步 最终交卷采用哪个版本 管理员能不能查出来所以一套成熟的答案保存系统应该从自动保存升级为自动保存 Version 本地缓存 同步状态 服务端确认 交卷快照 操作日志二十七、可以把整套追溯体系理解成三层第一层业务数据真正决定考试结果。包括ExamSession ExamAnswer SubmissionSnapshot ScoreResult第二层状态数据用于处理异常和恢复。包括AnswerVersion SyncStatus Heartbeat NetworkStatus SessionStatus第三层审计数据用于考后复核。包括OperationLog ExamEvent SubmitLog MonitorEvent三层组合以后系统才能既保证考试能继续。也保证考试过程能解释。二十八、完整架构可以这样理解考生答题 │ ↓ 浏览器本地状态 │ ┌─────────┴─────────┐ │ │ ↓ ↓ 本地缓存 保存队列 │ │ └─────────┬─────────┘ ↓ 答案保存API │ ↓ Version版本校验 │ ┌───────┴───────┐ │ │ ↓ ↓ ExamAnswer ExamEvent │ │ ↓ ↓ 当前答案 操作日志 │ ↓ 交卷 │ ↓ Submission Snapshot │ ↓ 成绩计算 │ ↓ Score Result │ ↓ Exam Timeline最终所有信息都通过session_id关联。二十九、管理员后台最好最终呈现成什么样技术架构做得再复杂最终管理员不应该直接查数据库。后台可以设计一个考试争议追溯页面顶部显示姓名张三 工号100086 考试年度安全知识考试 SessionEX202608080001568 考试时间09:02:24—10:58:05 成绩82 状态已完成中间答题版本Q038 09:25:16 A 09:26:08 C 09:27:41 B ← 最终版本下面异常事件10:31:20 网络中断 10:32:18 网络恢复 10:46:28 切屏15秒最后提交流程10:58:01 点击交卷 10:58:02 答案同步完成 10:58:04 服务端接收 10:58:05 成绩生成管理员最终不需要知道Redis MQ Version Lock Transaction也能清楚回答用户的问题。这才是系统架构最终应该服务的业务目标。三十、总结员工说“我的答案没有保存。”这句话听起来像一个简单的数据问题。实际上背后可能涉及客户端状态 网络请求 本地缓存 答案版本 服务器写入 Session状态 考试时间 交卷流程 成绩计算因此正式在线考试系统不能只保留最终答案 最终成绩更加可靠的设计应该形成Exam Session ↓ 答案Version ↓ 自动保存 ↓ 网络状态 ↓ 断点同步 ↓ 交卷快照 ↓ 成绩结果 ↓ 操作日志 ↓ 时间线证据链这样当员工再次提出“我明明答了为什么没有”管理员就不需要凭经验判断也不需要简单回复“系统里就是这样。”而是可以真正回答几点几分选择了什么答案产生了哪个版本这个版本有没有到达服务器网络当时是否正常最终交卷使用的是哪个版本以及成绩是基于哪一份答案计算出来的。对于企业级在线考试系统而言真正成熟的“答案自动保存”不是尽量不要丢数据。而应该进一步做到每一次答案变化都有迹可循每一次异常都有依据可查每一次成绩争议都能够沿时间线还原。这才是一套正式考试系统在可靠性和可审计性上的真正价值。
返回列表