
简介这份资源是面向高校计算机相关专业学生与Java后端初学者的一份毕业论文文档主题为基于Spring Boot的在线考试系统设计与实现可帮助读者理解如何将Spring Boot、Java与MySQL整合落地到实际项目中。压缩包内仅含1个docx文件约3.88MB即完整论文正文涵盖摘要、目录、需求分析、系统架构、数据库设计、功能实现与测试等章节采用MVC模式划分管理员、教师、学生三大模块并涉及用户认证、试题管理、成绩统计、密码加密等实现细节。目前已有122人学习下载适合作为课程设计、毕业设计选题参考或用于梳理在线考试系统的整体开发流程与关键技术选型读者可从中获取完整的项目设计思路、模块划分方式与论文写作框架。1. 在线考试系统为什么总在“交卷那一刻”翻车做过在线考试系统的人大多有一个共同记忆平时压测都挺稳一到集中交卷的那几分钟接口响应时间从 80ms 飙到 3s数据库连接池直接打满运气差一点还能看到几份卷子状态卡在“考试中”。这不是玄学是典型的写放大叠加锁竞争——每份答卷提交时既要写答案明细又要更新考试记录状态还要触发判分三件事挤在一个事务里并发一上来就互相拖。基于 Spring Boot 的在线考试系统核心要解决的就是把“出题、组卷、作答、交卷、判分、成绩分析”这条链路做稳。它适合两类人一类是要在课程、培训、认证场景里落地一套可用系统的开发者另一类是想拿它练手 Spring Boot 全家桶Security、JPA/MyBatis、Redis、消息队列的进阶学习者。这篇笔记按我实际搭过的一套模拟项目来讲从表结构、组卷算法、交卷削峰一路讲到判分和防作弊的边界能照着复现也能看到哪些地方别硬抄。2. 先把领域模型和表结构定死别让答案表拖垮整个库在线考试系统最容易埋雷的地方不是代码是表设计。很多人上来就exam、question、answer三张表打天下结果做到“一场考试多套卷”“同一题不同分值”“主观题人工阅卷”时全得重构。我一般会先把领域模型拆成五块试卷模板、题目、选项、考试场次、作答记录。下面这套结构是我在模拟项目X里跑通的版本MySQL 8 InnoDB字段做了精简但保留了关键约束。2.1 核心表结构与索引设计-- 题目表题干与题型分离选项单独存 CREATE TABLE question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, type TINYINT NOT NULL COMMENT 1单选 2多选 3判断 4主观, stem TEXT NOT NULL COMMENT 题干, difficulty TINYINT DEFAULT 3 COMMENT 1-5用于组卷抽题, knowledge_tag VARCHAR(64) COMMENT 知识点标签用于分析, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 选项表一道题多个选项正确与否用 is_correct 标记 CREATE TABLE question_option ( id BIGINT PRIMARY KEY AUTO_INCREMENT, question_id BIGINT NOT NULL, content VARCHAR(512) NOT NULL, is_correct TINYINT DEFAULT 0, sort_no INT DEFAULT 0, KEY idx_qid (question_id) ) ENGINEInnoDB; -- 考试场次一场考试对应一套卷绑定时间窗 CREATE TABLE exam ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, duration_min INT NOT NULL COMMENT 作答时长(分钟), total_score INT NOT NULL DEFAULT 100, status TINYINT DEFAULT 0 COMMENT 0未开始 1进行中 2已结束 ) ENGINEInnoDB; -- 试卷题目关联同一题在不同场次可有不同分值 CREATE TABLE exam_question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL, question_id BIGINT NOT NULL, score INT NOT NULL, sort_no INT NOT NULL, UNIQUE KEY uk_exam_q (exam_id, question_id) ) ENGINEInnoDB; -- 作答记录一个用户一场考试一条主记录 CREATE TABLE answer_sheet ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL, user_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0作答中 1已交卷 2已判分, objective_score INT DEFAULT 0, subjective_score INT DEFAULT 0, submit_time DATETIME, UNIQUE KEY uk_exam_user (exam_id, user_id) ) ENGINEInnoDB; -- 答案明细主观题答案可能很长用 TEXT CREATE TABLE answer_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sheet_id BIGINT NOT NULL, question_id BIGINT NOT NULL, answer TEXT COMMENT 选择题存选项id逗号拼接主观题存文本, score INT DEFAULT NULL COMMENT 判分后回填, KEY idx_sheet (sheet_id) ) ENGINEInnoDB;这套结构的关键取舍有三个。第一exam_question单独存分值而不是把分值写死在question上因为同一道题在练习卷和正式卷里权重不同这是血泪经验。第二answer_sheet和answer_item拆开主记录只存状态和总分明细单独写交卷时更新主记录、批量插明细避免一行大记录反复锁。第三uk_exam_user唯一索引是防重复交卷的第一道闸比在代码里查一遍再插更可靠。2.2 组卷抽题的参数怎么设组卷不是随机ORDER BY RAND()就完事那样在几万道题时会全表扫描。常见做法是按知识点和难度分层抽题用knowledge_tag difficulty建联合索引再按配额抽。下面是我常用的抽题逻辑参数含义写在注释里。// 按知识点配额抽题每个标签抽 count 道难度区间 [minD, maxD] public ListQuestion pickByTag(String tag, int count, int minD, int maxD) { // 先按索引过滤再随机排序数据量大时用子查询限制范围 String sql SELECT * FROM ( SELECT id, stem, type, difficulty FROM question WHERE knowledge_tag ? AND difficulty BETWEEN ? AND ? ORDER BY RAND() LIMIT ? ) t; // count 建议不超过该标签题量的 30%否则随机性下降 return jdbcTemplate.query(sql, rowMapper, tag, minD, maxD, count); }参数上我一般这样定难度区间默认[2,4]太简单的题区分度低太难的题容易让整卷均分崩掉单标签抽取比例不超过该标签总题量的 30%否则同一套卷重复率高整卷题量控制在 40 到 60 道超过 60 道时前端渲染和交卷写入都会变重。如果题量真的很大ORDER BY RAND()要换成“先取 id 区间再随机”的写法否则慢查询日志会被它刷屏。3. 交卷削峰把三件事拆开别挤在一个事务里前面说的“交卷翻车”根因就是提交动作太重。我的做法是把交卷拆成三步先落答案快再异步判分慢最后回写成绩可重试。这样交卷接口只做一次主记录状态更新加一次批量插入响应能压到几十毫秒。3.1 交卷接口的最小实现Transactional public SubmitResult submit(Long examId, Long userId, ListAnswerItemDTO items) { // 1. 幂等校验唯一索引兜底这里先查一次减少异常 AnswerSheet sheet sheetMapper.findByExamAndUser(examId, userId); if (sheet null || sheet.getStatus() ! 0) { throw new BizException(重复交卷或考试不存在); } // 2. 批量写答案明细用 foreach 拼一条 insert减少网络往返 answerItemMapper.batchInsert(sheet.getId(), items); // 3. 更新主记录状态为已交卷提交时间以服务端为准 sheetMapper.markSubmitted(sheet.getId(), new Date()); // 4. 发消息触发异步判分不阻塞交卷响应 mqProducer.send(exam.judge, sheet.getId()); return SubmitResult.ok(sheet.getId()); }逻辑说明第 1 步的幂等校验不能只靠查最终要靠uk_exam_user和状态字段双重保证否则并发下两个请求可能都查到“作答中”。第 2 步批量插入时items建议按 500 条一批切分太大容易触发max_allowed_packet。第 4 步发消息用本地消息表或事务消息保证“状态已改但消息没发出去”这种情况能被补偿扫描捞回来。参数上交卷接口的连接池超时我设 3sMQ 发送超时 1s判分消费者并发度按 CPU 核数的 2 倍起步。如果考试规模在千人以内其实可以不用 MQ用 Spring 的Async加线程池也能扛但要注意线程池队列满了之后的拒绝策略别把交卷请求给拒了。3.2 判分服务的异步消费RabbitListener(queues exam.judge, concurrency 4) public void onJudge(Long sheetId) { AnswerSheet sheet sheetMapper.findById(sheetId); if (sheet.getStatus() ! 1) return; // 已判分则跳过保证幂等 ListAnswerItem items answerItemMapper.listBySheet(sheetId); int objective 0; for (AnswerItem item : items) { Question q questionCache.get(item.getQuestionId()); if (q.getType() 4) continue; // 主观题留给人工 // 选择题比对多选要求完全一致单选直接相等 if (isCorrect(q, item.getAnswer())) { item.setScore(examQuestionMapper.getScore(sheet.getExamId(), q.getId())); objective item.getScore(); } else { item.setScore(0); } answerItemMapper.updateScore(item.getId(), item.getScore()); } sheetMapper.markJudged(sheetId, objective); }这里有个容易忽略的点判分消费者必须幂等因为 MQ 可能重复投递。我用status ! 1直接跳过简单有效。客观题判分逻辑里多选题的“完全一致”判定要先把选项 id 排序再比对否则用户按不同顺序勾选会被误判这个坑我在模拟项目X里踩过一次后来统一在写入前就排序。4. 避坑与排查这五个问题几乎每个在线考试系统都会遇到4.1 交卷时数据库连接池被打满现象是交卷高峰期接口大面积超时日志里全是Connection is not available。原因通常是交卷事务里做了太多事或者判分逻辑同步执行把连接占住不放。解决分两步先把判分改成异步交卷事务只保留写答案和改状态再把连接池maximumPoolSize从默认 10 调到 30 到 50同时把connectionTimeout设成 3s让拿不到连接的请求快速失败而不是无限等。改完再压一轮观察activeConnections曲线是否还贴着上限。4.2 考试时间到了但用户还能提交现象是end_time已过接口仍返回成功。原因是只在前端做了倒计时后端没校验。解决是在交卷接口里加服务端时间判断now end_time 宽限就拒绝宽限一般给 30 到 60 秒用于补偿网络延迟。注意别用客户端传的时间一律以服务端为准否则改本地时间就能绕过。4.3 主观题分数回填后总分对不上现象是人工阅卷完成绩单总分和明细之和不一致。原因多是回填主观题分数时只更新了answer_item忘了重算answer_sheet的总分。解决是把“重算总分”做成一个独立方法每次分数变动后调用并且用UPDATE ... SET subjective_score (SELECT SUM...)在数据库层算避免应用层累加误差。重算后加一条校验日志总分不等于明细和就告警。4.4 同一用户重复交卷产生两条记录现象是成绩列表里一个用户出现两条。根因是并发提交时幂等校验失效。解决是依赖uk_exam_user唯一索引插入冲突时捕获DuplicateKeyException并返回“已交卷”而不是先查后插。这个改动很小但能彻底堵住并发漏洞。4.5 组卷抽题慢到超时现象是创建考试时接口要好几秒。原因是ORDER BY RAND()在大表上全扫描。解决是给knowledge_tag和difficulty建联合索引抽题时先用索引缩小范围再在小结果集里随机或者维护一张“可抽题 id 池”的缓存表定时刷新抽题直接从池里取。题量过万后第二种方案更稳。5. 判分准确性与防作弊的边界一个可验证的收尾技巧判分这块客观题好办真正难的是主观题和防作弊的度。我的经验是主观题不要指望自动判分做到百分百做成“关键词命中 人工复核”更现实。具体做法是给每道主观题配一组关键词和权重判分时统计命中情况给一个参考分最终分由阅卷人确认。这样既减轻阅卷量又不会因为算法误判引发争议。防作弊方面切屏检测、复制粘贴拦截这类前端手段只能提高成本挡不住有心人。真正有效的是后端行为分析记录每道题的作答时长、修改次数、选项切换频率交卷后跑一遍异常检测。比如某道题作答时间小于 3 秒且正确或者整卷选项分布高度集中就标记为可疑推给人工复核。下面这个校验方法我一般放在判分之后跑。// 异常作答检测返回可疑原因列表空列表表示正常 public ListString detectAnomaly(Long sheetId) { ListString flags new ArrayList(); ListAnswerItem items answerItemMapper.listBySheet(sheetId); long totalMs 0; for (AnswerItem item : items) { // duration_ms 在作答时由前端定时上报服务端只做参考 totalMs item.getDurationMs(); // 规则1作答时间过短且答对 if (item.getDurationMs() 3000 item.getScore() ! null item.getScore() 0) { flags.add(题 item.getQuestionId() 作答过快且正确); } } // 规则2整卷平均每题耗时低于 5 秒 if (!items.isEmpty() totalMs / items.size() 5000) { flags.add(整卷平均作答时间异常偏低); } return flags; }参数上3 秒和 5 秒这两个阈值不是拍脑袋是我在模拟项目X里按正常考生的作答分布取的 5% 分位。不同题型要分开设选择题阈值可以低一些主观题低于 10 秒基本不正常。检测结果只做标记不直接判作弊最终由人工决定这样既保留了威慑又避免了误伤。最后说个我自己的习惯每次上线前我都会用脚本模拟 200 个用户同时交卷观察交卷接口 P99 和判分队列积压量。P99 超过 500ms 或者积压超过 1000 条就先别上回去看连接池和消费者并发。这个习惯帮我省了好几次半夜被叫起来处理的麻烦。希望帮到你。本文还有配套的精品资源点击获取