资讯动态

SpringBoot+Vue3在线考试系统:交卷并发与状态一致性实战

发布时间:2026/10/14 5:38:42 来源:尧图企业网站定制
简介这是一套面向高校教学场景的在线考试系统完整源码基于SpringBoot与Vue3前后端分离架构实现适合计算机专业学生、Java全栈开发者及需要搭建考试平台的院校参考学习。系统划分管理员、老师、学生三类角色管理员掌握最高权限老师负责考试、题库、成绩与学生管理学生可参与考试、查看成绩、试题练习及留言互动覆盖了在线考试业务的主要流程。资源包共约2000个文件以1689个js脚本、192个md说明文档、57个json配置、46个java后端类及若干xml、html、sql文件为主压缩包大小约39.74MB前后端代码结构清晰。已有182人学习关注。读者可从中获取完整的项目分层设计、接口实现与数据库脚本对照Controller层代码理解考试、成绩、留言等模块的请求处理逻辑并借助说明文档快速梳理工程目录与依赖关系为二次开发或课程设计提供可直接运行的参考方案。1. 在线考试系统为什么总在“交卷”那一刻翻车做过在线考试系统的人都有一个共识功能列表看起来简单真正难的是交卷瞬间的并发写入和状态一致性。基于 SpringBootVue3 实现的在线考试系统代码核心要解决的不是“能不能答题”而是“一百个人同时点交卷时答案不丢、分数不错、状态不乱”。这套技术栈的组合逻辑很清晰SpringBoot 负责考试会话、答题卡状态机、自动判分和防重复提交Vue3 负责倒计时、断线重连、本地暂存和交卷前的二次确认。适合谁适合需要在内网或私有环境部署考试模块的团队适合要二次开发题库和判分规则的后端也适合想拿一个完整前后端分离项目练手的开发者。但如果你只把它当成 CRUD 模板交卷那一刻的玄学问题会让你重新理解“状态机”三个字。2. 把考试会话拆成状态机SpringBoot 侧的表结构与接口边界2.1 为什么不能把答题记录直接塞进一张表很多初版设计会把考生答案直接更新到exam_record表字段用 JSON 存所有题目答案。这种方案在单机低并发下能跑但一旦遇到“暂存 交卷 自动判分 补考”四个动作交叉就会出现覆盖写和脏读。常见做法是拆成三张表考试会话表、答题明细表、判分结果表。会话表管状态明细表管每道题的作答快照结果表管最终分数和判分日志。这样交卷时只需要锁定会话行批量写入明细再异步触发判分不会因为某一道题写入失败导致整张卷子状态回滚。-- 考试会话表一个考生一场考试一条记录 CREATE TABLE exam_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL, user_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未开始 1答题中 2已交卷 3已判分, start_time DATETIME, submit_time DATETIME, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_exam_user (exam_id, user_id) ); -- 答题明细表一道题一条记录交卷时批量写入 CREATE TABLE answer_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id BIGINT NOT NULL, question_id BIGINT NOT NULL, answer_content TEXT, score DECIMAL(5,2) DEFAULT NULL, is_correct TINYINT DEFAULT NULL, KEY idx_session (session_id) );逻辑说明exam_session的version字段是交卷防重复提交的关键。前端每次暂存或交卷都带上当前版本号后端用UPDATE ... WHERE version ?做乐观锁。如果更新影响行数为 0说明已有其他请求修改过状态直接返回“请勿重复交卷”。answer_item不设唯一键因为允许考生修改答案交卷时先删后插或按 session 批量覆盖。参数上status用 TINYINT 而不是枚举字符串是为了在索引和状态判断上更省空间score用 DECIMAL 而不是 FLOAT避免判分累加时出现 0.10.2 的浮点误差。2.2 交卷接口的幂等设计三行代码挡住重复提交交卷接口是整个系统最容易翻车的地方。前端倒计时结束自动交卷、用户手动点交卷、网络重试导致的重复请求三路并发打过来如果没有幂等控制轻则答案重复写入重则分数算两遍。我一般会在服务层用“状态机 乐观锁”双保险。Transactional(rollbackFor Exception.class) public SubmitResult submitExam(Long sessionId, Integer version, ListAnswerItemDTO answers) { // 1. 乐观锁更新状态只有答题中(1)才能变成已交卷(2) int updated examSessionMapper.updateStatusToSubmitted(sessionId, version); if (updated 0) { // 影响行数为0说明版本号不对或状态已不是答题中 throw new BizException(请勿重复交卷或考试状态已变更); } // 2. 批量写入答题明细先清理旧暂存 answerItemMapper.deleteBySessionId(sessionId); answerItemMapper.batchInsert(sessionId, answers); // 3. 投递判分消息异步判分避免交卷接口超时 judgeProducer.send(new JudgeMessage(sessionId)); return SubmitResult.ok(); }逻辑说明第一步的updateStatusToSubmitted对应 SQL 是UPDATE exam_session SET status2, submit_timeNOW(), versionversion1 WHERE id? AND status1 AND version?。这条语句同时完成了状态校验和版本校验数据库行锁保证原子性。第二步先删后插是因为暂存数据可能不完整交卷数据才是最终答案。第三步用消息队列异步判分是因为判分可能涉及主观题或复杂规则放在交卷事务里会拉长响应时间。参数上version由前端从会话详情接口获取每次暂存后版本号会变前端需要同步更新本地版本号否则交卷时会因为版本过期被拒绝。2.3 自动判分的三种题型处理与分数回写判分逻辑要区分客观题和主观题。单选题、多选题、判断题可以自动判分填空题需要做字符串归一化简答题只能标记待人工阅卷。常见做法是定义一个判分策略接口每种题型一个实现类通过 Spring 的Component注入到工厂里。public interface JudgeStrategy { boolean support(QuestionType type); JudgeResult judge(Question question, String userAnswer); } Component public class SingleChoiceJudge implements JudgeStrategy { Override public boolean support(QuestionType type) { return type QuestionType.SINGLE_CHOICE; } Override public JudgeResult judge(Question question, String userAnswer) { // 用户答案和标准答案都做 trim 和大小写归一化 String standard question.getCorrectAnswer().trim(); String actual userAnswer null ? : userAnswer.trim(); boolean correct standard.equalsIgnoreCase(actual); return new JudgeResult(correct, correct ? question.getScore() : BigDecimal.ZERO); } }逻辑说明support方法让工厂在运行时根据题型选择策略新增题型只需要加一个实现类不用改判分主流程。judge方法里对答案做归一化处理是因为前端传过来的选项可能带空格或大小写不一致。判分结果回写到answer_item的score和is_correct字段同时累加到exam_session的总分字段。注意多选题的判分规则要单独处理全对得满分漏选得部分分错选得零分。这个规则建议做成配置项不要硬编码在代码里因为不同考试可能要求不同。3. Vue3 侧的关键实现倒计时、断线暂存与交卷确认3.1 倒计时不能只靠 setInterval前端倒计时如果用setInterval每秒减一页面切到后台或浏览器休眠时定时器会被节流甚至暂停导致倒计时不准。血泪经验是倒计时必须基于服务端时间戳做差值计算而不是本地累加。Vue3 里可以用computed结合Date.now()和服务端返回的serverTime与endTime来计算剩余秒数。// 基于服务端时间戳的倒计时逻辑 const remainSeconds ref(0); let timer null; function startCountdown(serverTime, endTime) { // 计算服务端时间和本地时间的偏移量 const offset Date.now() - new Date(serverTime).getTime(); const endTimestamp new Date(endTime).getTime(); const tick () { const now Date.now() - offset; remainSeconds.value Math.max(0, Math.floor((endTimestamp - now) / 1000)); if (remainSeconds.value 0) { clearInterval(timer); autoSubmit(); // 倒计时归零自动交卷 } }; tick(); timer setInterval(tick, 1000); }逻辑说明offset是本地时间和服务器时间的差值每次计算剩余时间都用Date.now() - offset模拟服务端当前时间这样即使本地时钟不准或页面休眠倒计时也不会漂移。autoSubmit触发前要检查是否已经交卷避免重复请求。参数上serverTime和endTime由考试详情接口返回格式建议用 ISO8601 字符串避免时区解析歧义。3.2 断线暂存用 localStorage 做本地后悔药考试过程中网络抖动是常态如果每次暂存都依赖接口成功考生可能因为一次断网丢失大量答案。我一般会在 Vue3 里做一个本地暂存层每次答案变更先写localStorage再异步调暂存接口接口失败时标记待同步网络恢复后重试。// 答案变更时的本地暂存与异步同步 function onAnswerChange(questionId, answer) { const key exam_${examId}_${questionId}; // 先写本地保证断网不丢 localStorage.setItem(key, JSON.stringify({ answer, timestamp: Date.now() })); // 再异步同步到服务端 syncAnswer(questionId, answer).catch(() { // 同步失败不阻塞考生标记待同步 pendingSync.add(questionId); }); } // 网络恢复后批量重试 window.addEventListener(online, () { pendingSync.forEach(qid { const cached localStorage.getItem(exam_${examId}_${qid}); if (cached) { const { answer } JSON.parse(cached); syncAnswer(qid, answer).then(() pendingSync.delete(qid)); } }); });逻辑说明localStorage的 key 用考试 ID 和题目 ID 组合避免不同考试之间串数据。pendingSync用一个 Set 存待同步的题目 ID网络恢复后遍历重试。注意交卷成功后要清理本地缓存否则下次进入同一场考试会读到旧答案。参数上timestamp用于判断本地缓存是否过期比如超过考试结束时间两小时的缓存可以直接丢弃。3.3 交卷前的二次确认与答案完整性校验交卷按钮不能点一下就提交必须做二次确认并且要提示未作答的题目数量。Vue3 里可以用一个弹窗组件在打开时计算已答题数和总题数如果存在未答题用醒目颜色标出。// 交卷前校验答案完整性 function beforeSubmit() { const total questions.value.length; const answered questions.value.filter(q { const cached localStorage.getItem(exam_${examId}_${q.id}); return cached JSON.parse(cached).answer; }).length; if (answered total) { const confirmed window.confirm(还有 ${total - answered} 题未作答确定交卷); if (!confirmed) return; } // 提交时带上当前会话版本号 submitExam(sessionId, currentVersion.value, collectAnswers()); }逻辑说明collectAnswers从localStorage或响应式状态里收集所有答案按后端接口要求的格式组装。currentVersion是会话版本号每次暂存成功后从接口响应里更新。注意window.confirm只是最简实现实际项目里建议用自定义弹窗组件因为confirm在移动端样式不可控而且会阻塞主线程。参数上未答题数量只做提示不强制阻止交卷因为有些考试允许部分作答。4. 避坑与排查交卷失败、分数不对、倒计时跳变的真实原因4.1 交卷接口返回“重复提交”但考生只点了一次现象考生手动点交卷接口返回“请勿重复交卷”但考生确实只操作了一次。原因通常是前端在倒计时归零时自动触发了一次交卷同时考生也点了按钮两个请求几乎同时到达。解决前端加一个submitting标志位交卷请求发出后立即置为 true按钮置灰倒计时自动交卷前也检查这个标志。后端乐观锁是最后一道防线但不能依赖它来兜底用户体验。4.2 自动判分后总分和明细分数对不上现象会话表里的总分是 85但答题明细里每道题得分加起来是 83。原因通常是判分过程中某道题抛异常被吞掉或者异步判分消息重复消费导致分数累加两次。解决判分逻辑要保证幂等每条判分消息带唯一消息 ID消费前先查判分日志表是否已处理。总分回写用UPDATE exam_session SET total_score (SELECT SUM(score) FROM answer_item WHERE session_id ?)不要用代码累加后再写避免中间状态不一致。4.3 倒计时在切换标签页后突然跳变现象考生切到其他标签页查资料切回来发现倒计时少了好几分钟。原因就是前面说的setInterval被浏览器节流。解决按 3.1 的方案用服务端时间戳做差值计算每次tick都重新算剩余时间而不是依赖定时器累加。另外可以在visibilitychange事件里主动触发一次tick让页面恢复时立即刷新倒计时。4.4 暂存接口频繁报 409 冲突现象考生快速修改答案时暂存接口返回 409 或版本冲突。原因每次暂存都带版本号但前端在请求未返回时就发了下一个暂存请求导致版本号落后。解决暂存接口可以不做乐观锁只做覆盖写因为暂存数据允许丢失。把乐观锁只用在交卷接口上。或者前端做请求队列保证同一时间只有一个暂存请求在飞。4.5 交卷后页面卡死或白屏现象点击交卷后页面长时间无响应最后白屏。原因通常是交卷接口同步做了判分和统计耗时过长前端等待超时后组件卸载但异步回调还在操作已卸载的组件状态。解决交卷接口只做状态变更和答案落库判分走异步消息。前端在交卷请求发出后立即跳转到“交卷成功”页面不要在原地等待判分结果。判分完成后通过轮询或 WebSocket 通知前端更新成绩。5. 从能跑到好用判分规则配置化与考试数据复盘判分规则配置化是这套系统从“能跑”到“好用”的分水岭。我一般会把判分策略做成数据库配置而不是写死在代码里。比如多选题的“漏选得分比例”、填空题的“是否忽略大小写”、简答题的“关键词命中给分”都放在exam_config表里判分时读取配置动态执行。这样同一套代码可以支撑不同科目的考试不用每次改需求都重新发版。-- 判分规则配置表 CREATE TABLE judge_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL, question_type VARCHAR(32) NOT NULL, rule_json JSON NOT NULL COMMENT 规则参数如{partialScore:0.5,ignoreCase:true}, UNIQUE KEY uk_exam_type (exam_id, question_type) );逻辑说明rule_json用 JSON 类型存储方便扩展不同题型的规则参数。判分策略实现类在judge方法里注入JudgeRuleService根据考试 ID 和题型查出规则再决定具体判分逻辑。参数上partialScore表示漏选得分比例ignoreCase表示填空题是否忽略大小写。注意JSON 字段的查询性能不如普通字段但判分规则读取频率低可以接受。考试数据复盘是另一个容易被忽略的环节。交卷判分完成后除了总分还应该记录每道题的作答时长、修改次数、暂存次数。这些数据可以用来分析题目难度和考生行为。比如某道题修改次数特别多说明题目表述可能有歧义某道题暂存次数少但正确率低说明考生可能没掌握知识点。我一般会在answer_item表里加answer_duration和modify_count两个字段前端在暂存时上报时间戳后端计算差值累加。最后一个具体技巧交卷接口的响应里不要只返回成功或失败要返回一个submitToken前端拿着这个 token 去轮询判分结果。判分完成后submitToken对应的结果接口返回分数和明细。这样即使判分服务挂了交卷动作本身不会失败考生体验是“已交卷成绩计算中”而不是“交卷失败请重试”。这个设计我踩过坑才改过来早期版本把判分和交卷绑在一个事务里结果一次判分超时导致考生交卷失败答案全丢只能人工恢复。从那以后凡是考试系统交卷和判分必须解耦。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑