简介这份资源是一份基于SpringBoot的在线刷题系统毕业设计文档面向计算机相关专业学生及需要完成类似课程设计、毕业设计的开发者。系统采用Java语言与SpringBoot框架搭建后端前端使用Vue技术数据存储于MySQL数据库开发平台为IntelliJ IDEA管理员端支持编辑题目、编辑试题与发布题目学生端具备刷题和查看错题本功能可有效解决线下刷题错题整理困难、教师统计不易、纸张浪费等问题。资源包共1个docx文件大小约5.79MB内容涵盖摘要、绪论、国内外研究现状、课题研究目的与意义等完整章节结构规范可直接作为论文写作与项目实现的参考模板。目前已有72人学习下载适合需要快速理解系统架构、梳理功能模块与撰写设计文档的读者借鉴使用。1. 从一份 .docx 需求到能跑起来的在线刷题系统SpringBoot 到底扛了什么很多人拿到「基于 SpringBoot 的在线刷题系统」这个题目时第一反应是去搜现成源码结果下载下来一堆跑不起来的压缩包或者只有几张截图没有数据库脚本。我当年做第一个刷题系统时也翻过车题目表、选项表、答题记录表三张表没设计好做到一半发现多选题没法存答案只能推倒重来。这个标题真正要解决的问题是把「题库管理 在线答题 自动判分 错题回顾」这条链路用 SpringBoot 串起来让一个普通 Java 开发者能在本地把它跑通、改得动、讲得清。它适合正在做课程设计或毕设的学生也适合想练手 SpringBoot 全栈 CRUD 的初级工程师。核心不在框架多新而在业务表结构和判分逻辑能不能立住。2. 题库与答题的数据模型先把表设计对再谈代码刷题系统看着简单本质是一个「题目—选项—作答—判分」的状态机。表设计错了后面写多少 Controller 都是白搭。我一般会先把实体关系画清楚再动手建表这一步花两小时能省两天返工。2.1 五张核心表与字段取舍一个能支撑单选、多选、判断三种题型的模型最少需要这几张表。注意答案字段的存储方式这是最容易踩坑的地方。表名关键字段说明questionid, content, type, difficulty, category_id, correct_answer, analysistype 用 1/2/3 区分单选/多选/判断question_optionid, question_id, option_label, option_content, sort_no选项单独成表方便乱序和扩展categoryid, name, parent_id支持章节/知识点两级分类answer_recordid, user_id, question_id, user_answer, is_correct, answer_time每次作答一条记录wrong_bookid, user_id, question_id, wrong_count, last_wrong_time错题本可定时从 answer_record 汇总correct_answer字段对单选题存A多选题存A,C,D按字母升序拼接判断题存T或F。这样判分时不用查选项表直接字符串比对性能好且逻辑简单。很多人把答案存成选项 id 数组判分时要 join 查询题量一大就慢。2.2 建表 SQL 与索引CREATE TABLE question ( id BIGINT NOT NULL AUTO_INCREMENT, content VARCHAR(1000) NOT NULL COMMENT 题干, type TINYINT NOT NULL COMMENT 1单选 2多选 3判断, difficulty TINYINT DEFAULT 1 COMMENT 1易 2中 3难, category_id BIGINT NOT NULL, correct_answer VARCHAR(50) NOT NULL COMMENT 多选按字母升序逗号拼接, analysis VARCHAR(2000) DEFAULT NULL COMMENT 解析, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_type_difficulty (type, difficulty) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE answer_record ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, question_id BIGINT NOT NULL, user_answer VARCHAR(50) NOT NULL, is_correct TINYINT NOT NULL DEFAULT 0, answer_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_time (user_id, answer_time), KEY idx_question (question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;answer_record上的idx_user_time联合索引很关键错题本和答题历史都按用户时间查没有这个索引数据量上万后列表页会明显卡顿。question表的idx_type_difficulty服务于「随机抽 N 道中等难度多选题」这类组卷查询。2.3 用 SpringBoot 实体类映射Data TableName(question) public class Question { TableId(type IdType.AUTO) private Long id; private String content; private Integer type; private Integer difficulty; private Long categoryId; private String correctAnswer; private String analysis; TableField(exist false) private ListQuestionOption options; }TableField(exist false)标记的 options 字段不映射数据库列查询题目详情时再单独填充避免每次列表查询都带出一堆选项。这里用 MyBatis-Plus 的注解如果你用原生 MyBatis把TableName换成 XML 里的 resultMap 即可逻辑一样。3. 判分逻辑与组卷接口把核心业务写扎实数据模型立住之后真正决定系统好不好用的是判分和组卷。这两块写歪了前端做得再漂亮也是花架子。3.1 判分服务的三种题型处理判分不能只做字符串相等多选题的答案顺序、大小写、空格都要归一化。我一般写一个独立的 JudgeService把归一化逻辑收在一处。Service public class JudgeService { public boolean judge(Question question, String userAnswer) { if (userAnswer null || userAnswer.trim().isEmpty()) { return false; } String correct normalize(question.getCorrectAnswer()); String user normalize(userAnswer); // 多选题排序后比对避免 A,C 与 C,A 被判错 if (question.getType() 2) { return sortLetters(correct).equals(sortLetters(user)); } return correct.equalsIgnoreCase(user); } private String normalize(String ans) { return ans.replace( , ).replace(, ,).toUpperCase(); } private String sortLetters(String ans) { char[] arr ans.replace(,, ).toCharArray(); Arrays.sort(arr); return new String(arr); } }normalize处理了中文逗号、空格和大小写sortLetters把A,C和C,A都变成AC再比。参数上要注意如果前端传的是选项 id 而不是字母这里就得改成查选项表映射别硬套。判分结果写回answer_record同时更新wrong_book答错则 wrong_count1答对则从错题本移除或标记已掌握。3.2 随机组卷的 SQL 与接口组卷要按分类、题型、难度、数量抽题。MySQL 里ORDER BY RAND()在几万题时性能很差常见做法是用主键范围随机或先查 id 列表再内存随机。public ListQuestion randomPaper(Long categoryId, Integer type, Integer difficulty, int count) { // 先查出符合条件的 id 集合数据量大时这一步走覆盖索引 ListLong ids questionMapper.selectIdsByCondition(categoryId, type, difficulty); if (ids.size() count) { return questionMapper.selectBatchIds(ids); } Collections.shuffle(ids); ListLong picked ids.subList(0, count); return questionMapper.selectBatchIds(picked); }selectIdsByCondition只查 id 列走idx_category或idx_type_difficulty比直接SELECT *快很多。Collections.shuffle在内存里打乱几万 id 的列表也就几毫秒。参数 count 建议设上限比如单次不超过 100防止有人构造请求一次抽全库。3.3 答题接口的幂等与防重复提交在线答题最怕用户狂点提交同一条记录写好几遍。我一般用「用户题目时间窗口」做去重或者前端提交时带一个 requestId后端用 Redis 存 5 秒。public AnswerResult submit(Long userId, Long questionId, String userAnswer, String requestId) { String key answer:lock: userId : requestId; Boolean ok redisTemplate.opsForValue().setIfAbsent(key, 1, 5, TimeUnit.SECONDS); if (Boolean.FALSE.equals(ok)) { throw new BizException(请勿重复提交); } // ... 判分、落库逻辑 }setIfAbsent就是 SETNX5 秒过期。requestId 由前端每次进入答题页生成一个 UUID。这个方案比数据库唯一索引轻也不会因为网络重试误伤正常提交。4. 避坑与排查那些让我熬夜的报错刷题系统跑起来容易跑稳难。下面几条是我和身边人真实踩过的按「现象 → 原因 → 解决」写清楚。4.1 多选题答案顺序导致误判现象用户明明选对了系统判错错题本里多了一堆「冤案」。 原因判分时直接字符串相等A,C和C,A不相等。 解决判分前对多选答案做排序归一化就是 3.1 里的sortLetters。上线前一定要用乱序答案跑一遍单元测试。4.2 组卷接口偶发返回重复题目现象同一份试卷里出现两道一模一样的题。 原因selectBatchIds传入的 id 列表本身有重复或者并发下随机种子相同。 解决selectIdsByCondition里加DISTINCT内存随机后用LinkedHashSet去重再截取。并发场景给组卷加个短锁或直接依赖数据库随机。4.3 答题记录时间字段时区错乱现象答题历史按时间排序顺序是乱的或者显示时间差 8 小时。 原因数据库CURRENT_TIMESTAMP用服务器时区Java 端new Date()用 JVM 时区两边不一致。 解决统一用 UTC 存储连接串加serverTimezoneUTC展示时再转本地时区。或者干脆全部用LocalDateTime并在配置里固定时区。4.4 题目列表分页越翻越慢现象前几页很快翻到几百页后响应好几秒。 原因LIMIT offset, size在 offset 很大时要扫描并丢弃前面所有行。 解决改用游标分页记住上一页最后一条的 id查询WHERE id lastId LIMIT size。刷题列表这种顺序浏览场景非常适合。4.5 错题本数据与答题记录对不上现象用户答对了题错题本里还在或者答错了没进错题本。 原因判分和错题本更新不在同一个事务里中间异常导致状态不一致。 解决把判分落库和错题本更新放进同一个Transactional方法或者用答题记录定时任务重算错题本保证最终一致。5. 让刷题系统从能跑到好用三个进阶技巧系统能跑通只是及格线下面这几个点能让它从「毕设水平」往「能给别人用」靠一靠。5.1 用缓存扛住高频的题目详情查询刷题时用户会反复看题目详情和解析这类数据读多写少非常适合缓存。我一般用 Spring Cache Redis在 Service 方法上加注解。Cacheable(value question, key #id, unless #result null) public Question getDetail(Long id) { Question q questionMapper.selectById(id); if (q ! null) { q.setOptions(optionMapper.selectByQuestionId(id)); } return q; } CacheEvict(value question, key #question.id) public void updateQuestion(Question question) { questionMapper.updateById(question); }Cacheable的unless防止空值被缓存导致缓存穿透。CacheEvict在更新时清掉对应 key。注意题目选项变了也要清缓存如果选项单独更新得手动cacheManager清别只依赖注解。缓存过期时间建议设 30 分钟到 1 小时题库变动不频繁。5.2 答题统计用定时任务预计算如果每次打开个人主页都实时COUNT答题记录用户一多数据库压力就上来了。常见做法是每天凌晨跑一个定时任务把统计结果写进汇总表。Scheduled(cron 0 0 3 * * ?) public void buildDailyStat() { ListUserStat stats answerRecordMapper.aggregateByUser(); for (UserStat s : stats) { userStatMapper.upsert(s); } }cron表达式0 0 3 * * ?表示每天凌晨 3 点执行。aggregateByUser用GROUP BY user_id一次算完所有用户比循环单查快得多。汇总表加唯一索引user_idupsert用INSERT ... ON DUPLICATE KEY UPDATE。这样个人主页只查一行汇总毫秒级返回。5.3 用接口测试固定判分行为判分逻辑一旦改动很容易悄悄改坏。我习惯给它写一组参数化测试把边界情况钉死。题型正确答案用户答案期望结果单选Aatrue单选ABfalse多选A,CC,Atrue多选A,CAfalse判断Ttrue需在归一化里处理最后一行提醒判断题如果前端传true而后端存T归一化里要加映射否则全判错。这种细节不写测试根本发现不了。我自己的习惯是每加一种题型或改一次判分规则先把这张表补一行再改代码。做刷题系统这几年最大的教训就是别信「这么简单的逻辑不会错」——判分错一次用户对整个系统的信任就没了。把归一化和测试做扎实比堆多少功能都值。希望帮到你。本文还有配套的精品资源点击获取