ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Spring Boot与MyBatis的在线错题管理系统设计与源码解析

基于Spring Boot与MyBatis的在线错题管理系统设计与源码解析 简介一份面向Java开发者与毕业设计学生的在线错题管理系统完整源码包围绕错题录入、分类管理与统计分析等核心功能给出从数据库设计到后台接口、前端交互的实现方案帮助理解Java Web项目的整体开发流程。资源共8个文件压缩包整体67.62MB以RAR工程备份、ZIP源码包、SQL数据库脚本、DOCX设计说明文档和PNG界面截图为主便于按需查看代码、初始化数据库、阅读部署说明或预览运行效果。目前已有95人学习下载。通过研读和运行源码读者可掌握常见Java框架的整合方式、业务表结构设计思路及错题模块的编码技巧配套文档还能辅助梳理系统模块划分与答辩要点。这份资源已通过运行测试可直接作为课程设计或毕业设计的项目基础也适合作为同类管理系统的开发蓝本。1. 在线错题管理系统不是“题库 记录表”那么简单很多第一次拿到这个课题的人第一反应是做成“错题本”用户把做错的题复制进来数据库加一张表前端再写个增删改查列表。真这样做课程设计也能交差但答辩时大概率被一个问题卡住这道错题到底什么时候复习学生是否真的掌握了这个基于 Java 的在线错题管理系统源码技术组合是 Spring Boot MyBatis MySQL表面是常规项目内部却把“提交答案—自动判分—错题状态更新—复习计划”串成了一个闭环。对正在准备 java 基础面试或选毕设题目的学生来说读这套源码比背单独的 java 面试题有用得多。2. 数据模型Spring Boot MyBatis 下的核心表设计拿到源码包后先不要急着启动先看数据库脚本和实体类。这套系统的业务规则包括权限、判分、状态流转全部由表结构约束。先把表拆明白后面看代码就是顺水推舟。2.1 实体划分与选型理由系统按照“用户—课程—题目—错题—复习流水”五个维度拆表。用户不只有学生还有教师和管理员不同角色对应不同的接口权限。课程表主要用来把题目按科目或班级隔离避免一名学生看到全部课程下的题目。题目表需要同时支持选择题和简答题所以答案字段必须设计成可计算的形式选择题直接存A/B/C/D简答题存标准答案文本判分逻辑单独处理。错题记录表是整个系统的核心它保证同一名学生和同一道题只有一条主记录而不是每次答错都追加一行。这样累计错误次数、判断状态、计算复习时间都方便。持久层这套系统适合用 MyBatis 手写 SQL而不是完全依赖 MyBatis-Plus 自动 CRUD。原因在于错题列表需要同时关联题目表和课程表手写 JOIN 可以更直观地控制筛选条件和索引使用排查慢查询时也能直接从 SQL 入手。表关系如下表名关键字段用途建议索引sys_userid, username, password, role登录与角色uk_usernamecourseid, course_name, teacher_id课程隔离idx_teacher_idquestionid, course_id, type, content, answer, analysis题目与标准答案idx_course_typewrong_recordid, student_id, question_id, status, wrong_count, last_review_time错题状态主记录uk_student_question, idx_status_timereview_logid, record_id, question_id, review_time, is_correct每次重练的流水idx_record_id, idx_review_time需要注意wrong_record中的联合唯一索引它从数据层面阻止同一条错题重复插入。很多学生版本的错题系统没有这个约束导致统计错误次数时还要GROUP BY question_id数据一多自然卡顿。2.2 建表 SQL 与字段约束以下是核心表wrong_record的建表语句也是理解后续状态流转的关键CREATE TABLE wrong_record ( id bigint NOT NULL AUTO_INCREMENT, student_id bigint NOT NULL COMMENT 学生 id, question_id bigint NOT NULL COMMENT 题目 id, wrong_count int NOT NULL DEFAULT 1 COMMENT 累计错误次数, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0待订正1已订正2已掌握, first_wrong_time datetime DEFAULT NULL COMMENT 首次做错时间, last_review_time datetime DEFAULT NULL COMMENT 最近复习时间, PRIMARY KEY (id), UNIQUE KEY uk_student_question (student_id, question_id), KEY idx_status_time (status, last_review_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT错题记录表;这里有两个容易忽略的点。uk_student_question是联合唯一索引如果学生重复答错同一道题应用层先查再更新数据库的唯一索引是最后一道防线一旦并发过来两条 insert数据库会直接拒绝而不是产生两条脏记录。status使用tinyint而不是字符串好处是存储小、比较快缺点是不够直观所以代码里必须定义枚举类进行转换。idx_status_time是为“查询今天需要复习的错题”设计的。例如要查所有待订正且超过复习时间的记录WHERE status IN (0,1) AND last_review_time NOW()就可以走这个索引。如果只建单列索引MySQL 通常只能用到其中一个字段另一个条件退化为回表过滤。2.3 MyBatis 映射与多表联查错题列表页不会只显示错题 ID还需要展示题干、题型、课程名和最近错误时间。这里直接用 Mapper XML 写 JOIN比在 Service 层逐个 set 更清晰select idselectWrongRecordList resultTypemap SELECT wr.id, wr.status, wr.wrong_count, wr.last_review_time, q.content, q.type, c.course_name FROM wrong_record wr JOIN question q ON wr.question_id q.id JOIN course c ON q.course_id c.id WHERE wr.student_id #{studentId} if teststatus ! null AND wr.status #{status} /if ORDER BY wr.last_review_time DESC /selectstudentId这个参数必须由后端从登录 token 中取出不能直接信任前端传值。错题列表属于个人数据如果直接用接口入参里的studentId学生把参数改成 2 就能看到别人的错题这是该类项目里最典型的数据越权。status是可空参数用于前端点击“待订正 / 已订正 / 已掌握”Tab 时筛选。还要注意这条 SQL 没有把q.answer查出来这是有意的。列表页只需要题目内容答案应该等学生点击“查看答案”或者提交重练后再返回。如果后端一次性把答案带到前端学生打开浏览器控制台就能看到判分功能就成了摆设。3. 错题闭环录入、自动判分、重练接口实现数据表建好之后核心业务围绕“提交答案”这一个动作展开。先想清楚学生做错一道题系统要做什么记录错误次数、设置状态为待订正学生重练这道题时系统又要做什么判分、更新状态、留一条复习流水。这套源码把这两件事都收敛到几个接口里前端调用链很简洁。3.1 接口与 DTO 设计主要接口如下方法与路径作用请求参数POST /api/auth/login登录获取 tokenusername, passwordPOST /api/questions教师录入题目courseId, type, content, answer, optionsPOST /api/wrong/submit学生交答案并生成/更新错题questionId, studentAnswerPOST /api/wrong/review重练提交答案更新状态recordId, studentAnswerGET /api/wrong/list当前学生错题分页列表status, page, sizePOST /api/wrong/submit和POST /api/wrong/review看起来很像区别在于前者不依赖错题记录 ID后者必须指定 recordId。submit 用于首次做错系统需要先查是否存在旧记录review 用于后续复习状态机从这里开始运转。3.2 Service 层判分与错题更新以下代码截取的是核心处理逻辑Transactional(rollbackFor Exception.class) public WrongRecordVO submitAnswer(WrongSubmitRequest req) { Long userId SecurityContext.getCurrentUserId(); Question question questionMapper.selectById(req.getQuestionId()); if (question null) { throw new BizException(题目不存在); } boolean correct judgeAnswer(question, req.getStudentAnswer()); WrongRecord record wrongRecordMapper.selectByStudentAndQuestion( userId, question.getId()); if (correct) { if (record ! null) { wrongRecordMapper.updateStatus(record.getId(), 2); } return WrongRecordVO.of(true, record); } if (record null) { record new WrongRecord(); record.setStudentId(userId); record.setQuestionId(question.getId()); record.setWrongCount(1); record.setStatus(0); record.setFirstWrongTime(new Date()); wrongRecordMapper.insert(record); } else { wrongRecordMapper.increaseWrongCount(record.getId()); wrongRecordMapper.updateStatus(record.getId(), 0); } return WrongRecordVO.of(false, record); }这段代码有四个关键点。SecurityContext.getCurrentUserId()不是从请求参数拿而是从 JWT 过滤器写入的上下文拿judgeAnswer负责选择题和简答题的分流判分Transactional(rollbackFor Exception.class)保证插入错题记录、更新错误次数、写复习流水在同一事务里。很多初学者只写Transactional默认只回滚运行时异常一旦自定义业务异常是受检异常事务不会回滚。参数说明WrongSubmitRequest里的questionId来自前端选中题目studentAnswer是学生输入或选择的答案。record.getId()只是返回结果给前端展示不参与查询条件这就是和 review 接口最大的差异。3.3 自动判分与复习流水的边界选择题判分很简单studentAnswer去前后空格后与question.answer比较。简答题不能直接equals因为学生不会把标点、空格都写对。常见做法是分词后计算 Jaccard 相似度或者检查标准答案里的核心关键词是否都出现在学生答案中。判分函数可以单独抽出来private boolean judgeAnswer(Question question, String studentAnswer) { if (choice.equals(question.getType())) { return question.getAnswer().trim() .equalsIgnoreCase(studentAnswer.trim()); } double similarity JaccardUtil.similarity( question.getAnswer(), studentAnswer); return similarity answerThreshold; }参数说明answerThreshold从application.yml读取选择题忽略大小写是为了避免前端把 A 改成小写 a 后误判。简答题阈值设 0.75 左右比较稳太低会把错误答案放过去太高会导致几乎没学生能答对。重练接口里还有一处容易被忽略每答一次无论对错都要往review_log插入一条流水。这个表平时看不到价值但后期要统计“这道题复习了三次才掌握”时就缺不了。插流水和更新状态必须放在同一个事务里否则状态显示已订正流水却丢了后续做复习计划时会发现间隔数据不准确。提示判分阈值不要写死在方法里建议放到配置项review.answer-similarity-threshold0.75让教师能按课程调整宽松程度。4. JWT 鉴权与错题状态机的实现要点这套源码里最容易卡住的地方是权限和状态流转。表面上两者没有关系实际上都围绕同一件事让数据只能被合法用户按合法路径修改。这里把细节展开。4.1 JWT 登录与角色拦截登录接口返回 token 之后后续请求都通过拦截器解析 token。常见实现如下Component public class JwtAuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String auth request.getHeader(Authorization); if (auth null || !auth.startsWith(Bearer )) { response.setStatus(401); return false; } Claims claims JwtUtil.parseToken(auth.substring(7)); request.setAttribute(currentUserId, claims.get(userId, Long.class)); request.setAttribute(currentRole, claims.get(role, String.class)); return true; } }这段代码把解析出来的userId和role塞进 request 属性后续 Controller 直接request.getAttribute(currentUserId)不需要再查一次数据库。注意签名算法和密钥不要写在代码里密钥放到application.yml通过Value注入。角色拦截建议单独处理。比如录入题目只有教师和管理员能调用可以用一个RequireRole(TEACHER)注解加第二个拦截器。如果所有接口都只校验登录不校验角色学生登录后可以调教师接口数据安全就失控了。源码里如果没有这个粒度二次开发时优先补上。4.2 状态机与乐观更新错题状态不是简单靠if...else改的它有一个明确的状态机当前状态触发操作下一状态0 待订正学生查看答案并确认“已订正”1 已订正0 待订正重练答对1 已订正1 已订正再次重练答对2 已掌握1 已订正重练答错0 待订正2 已掌握复习时再次答错0 待订正如果直接在前端把状态值从 0 改成 2整个复习体系就失效了。正确做法是在 Service 层判定当前状态允许哪些跳转。例如只有状态 1 才能进入状态 2一次答对不能直接从 0 跳到 2避免学生碰运气蒙对导致误判。更新语句可以加乐观条件UPDATE wrong_record SET status #{targetStatus}, last_review_time NOW() WHERE id #{recordId} AND status #{expectStatus}这样的更新语句会返回影响行数。影响行数为 1说明修改成功为 0说明同一时间已经有另一个请求改掉了状态前端需要提示“记录已更新请刷新”。这个策略在重复提交场景下比先查后写更可靠。4.3 事务和索引常见问题这套源码在运行阶段最容易遇到的三个问题其实也都是 Java 后端面试常问的点。事务失效。Transactional加在submitAnswer方法上如果同一个类的其他方法内部直接调用this.submitAnswer()Spring 代理不会生效事务也就没了。要避免在 Service 内部用this调用带事务方法可以拆成两个 Bean 或把事务注解加到内层方法。异常被吞。事务方法里有try { ... } catch (Exception e) { return null; }数据库异常被捕获后事务照样提交脏数据就落库了。正确做法是捕获后做日志记录然后重新抛出。索引失效。错题列表的慢查询大多来自WHERE student_id ? AND question_id ?没有走联合索引。我会在压测前用EXPLAIN SELECT ...看key字段确认是uk_student_question而不是全表扫描。这个点在数据量只有几千条时看不出来但答辩时老师大概率会问“这个系统上线后数据量大了怎么办”。5. 部署、验证与源码二次开发技巧最后落到能把这套源码跑起来并且改成自己的东西。部署本身不复杂但第一次运行的人容易在环境匹配上消耗太多时间。5.1 从源码包到本地运行解压得到源码后建议按如下顺序操作。先创建数据库比如online_wrong_question再导入源码包里的zaixiancuoti.sql。不要直接在可视化工具里双击打开 SQL 文件复制到查询窗口文件较大时会漏掉中间段用命令行导入最稳妥mysql -uroot -p zaixiancuoti.sql然后用 IDEA 以 Maven 工程方式打开源码等待依赖解析完成。如果本地是 JDK 8 而 pom 里配置的是 JDK 11编译会报错先看pom.xml里的java.version保持一致。接着修改application.yml中spring.datasource的 URL、用户名、密码。最后运行启动类看到 Tomcat 端口日志后说明启动成功。5.2 用 curl 快速验证完整闭环不一定要先写前端命令行可以直接验证整个链路TOKEN$(curl -s -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:student,password:123456} | jq -r .data.token) curl -s http://localhost:8080/api/wrong/list?status0page1size10 \ -H Authorization: Bearer $TOKEN第一条命令拿到登录 tokenjq -r .data.token表示取 JSON 中的 token 字段并去掉引号第二条命令查询待订正错题。如果返回 401先检查拦截器的放行路径里是否包含了/api/auth/login。如果不放行登录接口自己也要求 token系统就锁死了。5.3 把“错题列表”升级成“到期提醒”如果只是作为课程设计现有功能已经够了。想在答辩里多一个可讲的点推荐给wrong_record增加next_review_time字段把普通错题本升级成带复习计划的系统。每次重练正确后按间隔更新下一次复习时间查询时只取到期记录SELECT wr.id, q.content FROM wrong_record wr JOIN question q ON q.id wr.question_id WHERE wr.student_id #{studentId} AND wr.status IN (0, 1) AND wr.next_review_time NOW() ORDER BY wr.next_review_time ASC LIMIT 10;间隔取 1 天、3 天、7 天即可把间隔列表放到配置项里不在代码中硬编码。前端可以每天早上加载一批“今日待复习错题”学生进入后直接开始重练。接口返回值里额外带上next_review_time前端还能显示“还有 3 天到期”这个细节在答辩演示时比单纯列表更直观。本文还有配套的精品资源点击获取
返回列表