简介基于SpringBoot的在线错题管理系统设计与实现的完整资源包适合Java学习者、课程设计与毕业设计学生使用。系统采用SpringBoot、JSP与MyBatis搭建核心功能包括错题录入支持文本和拍照图片记录科目、类型、知识点、难易程度、出错原因及正确解法、条件查询、删除管理并支持按图片或文本人工选题组卷与打印管理员端提供用户注册管理和后台管理。资源共6个文件约42.33MB包含项目源码压缩包、需求文档、数据库SQL脚本、使用说明、设计文稿及演示视频RAR可覆盖开发、部署与答辩演示需求。已有145人学习过该资源下载后可按文档与视频快速还原运行环境并结合源码理解SpringBoot与MyBatis整合、错题模块的数据库设计与接口实现对毕业设计或项目实战有直接参考价值尤其适合缺少完整项目经验的初学者从建表脚本入手逐层理解控制层、业务层与数据访问层的协作。1. 基于SpringBoot的在线错题管理系统先把「错题本」和「错题分析系统」分清基于SpringBoot的在线错题管理系统是 Java 毕设里最常见的后台管理型题目用 SpringBoot 写好接口连上 MySQL 存错题再用 Vue 或 Thymeleaf 把增删改查页面画出来。它很容易被轻视因为听起来就是一本带搜索功能的错题本。但真正能过答辩、甚至毕业后还在用的版本赢在数据建模而不是页面数量有没有错因字段决定你能不能回答“学生为什么错”有没有掌握度决定复习列表能不能按优先级排有没有分类统计决定你比 Excel 好在哪。这套系统适合刚把 SSM 学完、想用 SpringBoot 独立完成全栈项目的人下手。2. 需求建模与技术选型把错题表结构立住再谈 SpringBoot 代码我先给结论这套系统的工程量不大真正花时间的是把表结构和状态枚举设计对。有人在 IDEA 里新建 SpringBoot 项目后直接写 Controller写到第三天发现缺错因字段回去改表连带改 Mapper、Service、前端表单这就是典型的返工。所以哪怕你急着看到页面也先忍一忍把数据模型定下来。2.1 错题实体的核心字段错因、章节、掌握度三件套错题表最忌讳只存“题面 答案 解析”那只是拍照存档。要支撑后续统计分析至少要有下面这些字段字段类型说明idbigint主键user_idbigint所属学生subjectvarchar(50)科目chaptervarchar(100)章节/知识点question_typevarchar(20)选择题/填空题/解答题contenttext题干answertext正确答案wrong_answertext学生错答analysistext解析wrong_reasonvarchar(20)错因编码mastery_leveltinyint掌握度 1~5sourcevarchar(50)作业/考试/练习create_time / update_timedatetime自动填充deletedtinyint逻辑删除标记wrong_reason是这套系统的灵魂。常见错因四类NOT_FAMILIAR知识点不熟、CARELESS粗心大意、METHOD思路错误、TEMPO时间不够。没有错因所谓“分析”只能按科目数数答不出“哪种题型最稳定地丢分”。mastery_level则用来支撑复习排序每次重新做对就加 1再做错就减 1列表页按这个值升序排让最弱的题排在最上面。deleted是逻辑删除标志毕设答辩时老师经常问“为什么不用物理删除”理由就是错题数据有复查价值删错了还能恢复这就是你的后悔药。2.2 为什么选 SpringBoot 2.7.18 MyBatis-Plus而不是最新 3.x技术选型上我见过太多人栽在版本上。springboot版本太高 对毕设不是好事SpringBoot 3.x 虽然已经是很成熟的新版但它把javax命名空间改成jakarta导致你从网上抄来的大量 2.x 代码直接编译报错MyBatis-Plus、Druid、Knife4j 这些配套库对 3.x 的适配版本也有限制。所以我一般固定用SpringBoot 2.7.18 MyBatis-Plus 3.5.3.1 MySQL 8.0 Druid 1.2.20 Lombok 1.18.30。这个组合的另一个理由是资料密度绝大多数毕设教程、博客、GitHub 项目都是在这个版本上写的你遇到报错时搜索“springboot 关键词 报错”第一页结果基本都能直接套。SpringBoot 的自动装配原理决定了你只要引入spring-boot-starter-web和mybatis-plus-boot-starterDataSoutce、SqlSessionFactory、事务管理器都会自动配置好不需要写一行 XML 配置这也是毕设工期能压下来的原因。2.3 从 ER 图到建表 SQL三张表的关系与索引设计在线错题管理系统的表不建议只建一张错题表最少要有用户表、错题表、复习记录表三张。规划表结构时先画简单关系一个用户拥有多条错题一条错题对应多条复习记录错题表里冗余存subject、chapter、question_type而不是拆成字典表是为了减少 JOIN毕竟毕设数据量到不了需要第三范式的规模。CREATE DATABASE error_notebook DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE error_notebook; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(20) DEFAULT STUDENT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE error_question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, subject VARCHAR(50), chapter VARCHAR(100), question_type VARCHAR(20), content TEXT, answer TEXT, wrong_answer TEXT, analysis TEXT, wrong_reason VARCHAR(20), mastery_level TINYINT DEFAULT 3, source VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0, KEY idx_user_subject (user_id, subject), KEY idx_user_mastery (user_id, mastery_level) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT错题表; CREATE TABLE review_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, question_id BIGINT NOT NULL, user_id BIGINT NOT NULL, review_result TINYINT COMMENT 0仍错 1做对, review_time DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT复习记录表;这里有几个容易忽略的参数charsetutf8mb4比utf8多支持表情符号插入题目里的数学符号时不会报字符集错ON UPDATE CURRENT_TIMESTAMP让update_time自动维护idx_user_subject和idx_user_mastery两个联合索引分别照顾按科目筛选和按掌握度排序两个高频查询。如果不建索引等表里攒到几千条错题统计接口会明显变慢这个坑在答辩演示时很容易暴露。2.4 用 IDEA 创建 SpringBoot 项目时的版本设置与 pom.xml 依赖IDEA 新建 SpringBoot 项目时Spring Initializr 默认拉最新稳定版你需要把版本手动改成 2.7.18。创建完成后pom.xml 里核心依赖这样给parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-extension/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies注意mybatis-plus-boot-starter里已经带了 mybatis 核心不要再重复引入mybatis-spring-boot-starter否则会出现 SqlSessionFactory 被覆盖的诡异报错。第一次启动时如果你发现卡在下载依赖超过十分钟去 Maven 全局 setting.xml 里配置阿里云镜像这是所有用 IDEA 创建 springboot 项目 的人都会遇到的第一道坎。3. 核心功能实现错题录入、筛选、统计与复习闭环表结构定了后面的代码其实都是体力活。这一章我按一条完整业务线来讲录入错题 → 多条件筛选 → 错因统计 → 复习提醒。顺序不要乱因为每个功能都依赖前一个产出的数据。3.1 用 MyBatis-Plus 实体与 QueryWrapper 实现错题多条件筛选先写实体类注意TableName和逻辑删除注解Data TableName(error_question) public class ErrorQuestion { TableId(type IdType.AUTO) private Long id; private Long userId; private String subject; private String chapter; private String questionType; private String content; private String answer; private String wrongAnswer; private String analysis; private String wrongReason; private Integer masteryLevel; private String source; private LocalDateTime createTime; TableLogic TableField(select false) private Integer deleted; }说明三个关键点TableId(type IdType.AUTO)配合数据库自增主键TableLogic会自动在 MyBatis-Plus 生成的 SQL 上追加deleted 0条件并且把 delete 操作变成 updateTableField(select false)让普通查询不返回这个字段避免前端误用。多条件筛选是错题列表页的核心。我一般不用手写 XML直接用 QueryWrapper 动态拼条件QueryWrapperErrorQuestion wrapper new QueryWrapper(); wrapper.eq(user_id, currentUserId) .eq(StringUtils.hasText(subject), subject, subject) .eq(StringUtils.hasText(questionType), question_type, questionType) .eq(StringUtils.hasText(wrongReason), wrong_reason, wrongReason) .ge(masteryLevel ! null, mastery_level, masteryLevel) .orderByAsc(mastery_level) .orderByDesc(create_time); PageErrorQuestion page errorQuestionMapper.selectPage( new Page(pageNum, pageSize), wrapper);这里的参数问题值得说透第一个参数为 false 时MyBatis-Plus 会直接忽略后面那个条件所以前端没传科目时不会拼出subject null这种错误 SQLmastery_level用ge而不是eq是为了实现“只看掌握度 1 和 2 的题”这类范围查询排序先按掌握度升序再按创建时间降序保证最弱且最新的题排最前。Page自带分页拦截器不用手写 LIMIT。3.2 错因字典与掌握度状态用枚举还是字典表错因到底是枚举还是数据库字典表我建议分题量来看。如果用户量就一个学生用枚举就够了实现简单代码里能直接 switch如果做成多用户在线版老师可能想自定义错因那就拆一张dict_wrong_reason表。毕设答辩里“可扩展性”是高频提问所以我会用枚举 表冗余两种方式折中。public enum WrongReason { NOT_FAMILIAR(知识点不熟), CARELESS(粗心大意), METHOD(思路错误), TEMPO(时间不够); private final String label; WrongReason(String label) { this.label label; } public String getLabel() { return label; } public static WrongReason fromCode(String code) { for (WrongReason reason : values()) { if (reason.name().equals(code)) { return reason; } } return null; } }fromCode这个静态方法是给前端回显用的保证从前端传来的“KNOWLEDGE_NOT_FAMILIAR”之类编码能被安全转换不认识的一律返回 null而不是直接抛异常。掌握度状态我更建议做成 1 到 5 的整数而不是“会/不会”布尔值每次复习记录为“做对”时该题掌握度加 1上限 5记录为“仍错”时减 1下限 1。这样统计模块就能按掌握度分桶比如“掌握度 ≤ 2 的题是需要重点突破的”。3.3 统计接口按科目、题型、错因聚合出复习优先级统计是评委最可能追问的部分也是这个系统区别于普通增删改查的价值点。我提供一个 Mapper XML 里的聚合查询select idcountByGroup resultTypemap SELECT subject, question_type, wrong_reason, COUNT(*) AS total FROM error_question WHERE deleted 0 AND user_id #{userId} GROUP BY subject, question_type, wrong_reason ORDER BY total DESC /select这条 SQL 的返回结果是多行 mapService 层再把它组装成前端容易渲染的树形结构先按科目分再按题型分最后展开每种错因的数量。注意deleted 0在这里是必需的因为 XML 手写 SQL 不会自动带上 MyBatis-Plus 的逻辑删除条件。索引方面idx_user_subject已经覆盖user_id subjectGROUP BY 在数据量不大时也不会有性能问题。前端拿到统计后可以画成柱状图也可以做成“薄弱知识点 TOP10”列表这部分是答辩时的亮点。3.4 用 Redis 缓存统计结果springboot 整合 redis 的时机统计接口每次查全表聚合数据一多就慢。所以一般在统计模块里加一层 springboot 整合 redis 的缓存public StatsDTO getStats(Long userId) { String key stats:user: userId; Object cached redisTemplate.opsForValue().get(key); if (cached ! null) { return (StatsDTO) cached; } StatsDTO stats statsMapper.aggregateByUser(userId); redisTemplate.opsForValue().set(key, stats, 30, TimeUnit.MINUTES); return stats; }缓存的 key 用stats:user:{userId}TTL 设 30 分钟。但这里有个一致性问题用户录入新错题后统计缓存必须立刻失效否则列表显示的数量和详情页对不上。我一般在新增和删除错题的 Service 方法里加一行redisTemplate.delete(stats:user: userId)比设置短 TTL 更可靠。这个场景也适合用来讲清楚“什么时候用 Redis”——不是所有数据都该缓存只有聚合结果这种计算成本高、实时性要求不高的数据才值得。4. 在线错题管理系统开发中的 5 个经典翻车点这一章全是血泪经验。每条都是我在实际跑这套系统时真的撞过的墙按“现象 → 原因 → 解决”写方便你直接对照排查。4.1 数据明明在库里列表却查不到现象数据库里通过客户端能看到几十条错题接口返回却是空数组手动在 MySQL 执行 SQL 也查得到但 MyBatis-Plus 查询为空。原因八成是逻辑删除条件冲突。你在实体上加了TableLogic又在 XML 或 QueryWrapper 里手动写了deleted 0MyBatis-Plus 会额外追加一个deleted 0两个条件叠加一般不会出错真正的问题是你在 XML 里写了deleted 1或者查了别的表的关联字段。另一个可能TableField(select false)加在deleted上导致带着逻辑删除条件的查询字段在 INSERT 时被排除新插入的数据没有默认值写入后deleted是 NULL。解决不要把deleted暴露给业务 SQL。统一用TableLogic管理XML 里如果需要写条件写deleted 0并保证实体字段映射正确插入时让数据库默认值生效或者显式在实体里初始化deleted 0。4.2 时间差了 8 小时时区问题的玄学现象前端页面显示“2025-02-14 10:30”数据库里存的是“2025-02-14 02:30”或者反过来。原因MySQL 驱动和 JDBC URL 没有统一时区。MySQL 8.0 默认 useSSL 和服务器时区处理都变了JDBC URL 里缺serverTimezoneAsia/Shanghai时驱动会用 JVM 默认时区而服务器可能是 UTC。解决JDBC URL 固定写成jdbc:mysql://localhost:3306/error_notebook?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue同时在 application.yml 里给spring.jackson.time-zone: GMT8两边一起改别只改一端。4.3 前端选了“知识点不熟”后台查不到数据现象下拉框明明展示的是“知识点不熟”选中后提交成功但筛选“知识点不熟”时一条都没有。原因前端提交的是中文 label数据库存的是英文 code或者反过来。这是毕设里最常见的枚举串味问题。解决前端下拉框的 value 存NOT_FAMILIARlabel 显示“知识点不熟”后端接收枚举编码后统一做映射。给前端提供的字典接口返回[{code: NOT_FAMILIAR, label: 知识点不熟}]不要直接拿枚举的toString()塞给前端。4.4 分页查询统计数量翻倍现象列表每页 10 条数据正常但分页器的 total 是 20而错题表实际只有 10 条。原因列表查询联了 user 表或 review_record 表left join 之后一对多导致主表行数膨胀MyBatis-Plus 分页插件在计算 count 时把 join 后的行数也带进去了。解决先分页查错题主表 ID再用这些 ID 去联查附加信息也就是“先分页后 join”或者在 count 查询里加上COUNT(DISTINCT error_question.id)。我习惯用第一种因为它对索引更友好。4.5 Redis 缓存反序列化异常现象统计接口第一次请求正常第二次直接报SerializationException: Cannot deserializeRedis 的 key 在客户端里显示成\xAC\xED开头。原因默认的RedisTemplate使用 JDK 序列化要求对象实现Serializable而且序列化后的二进制可读性很差。你存的 StatsDTO 没实现序列化接口或者中途改过字段类型。解决显式配置StringRedisSerializer做 key 的序列化、Jackson2JsonRedisSerializer做 value 的序列化。更简单的方案是直接用StringRedisTemplate把 StatsDTO 转 JSON 字符串存储取的时候再反序列化。这样问题最少也方便你在 Redis 客户端里直接肉眼检查缓存内容。5. 前后端联调与部署从 SpringBoot 接口到 Vue 页面再到 Docker系统得有界面。毕设现在主流是前后端分离前端 Vue3 Element-Plus后端 SpringBoot 提供 JSON 接口。这一章讲联调和部署里最容易被卡住的几个点。5.1 跨域报错不能用前端代理硬扛现象前端axios请求http://localhost:8080/api/error-question控制台报CORS policy: No Access-Control-Allow-Origin header。有人为了省事在 vue.config.js 里开 proxy结果部署后又继续报错因为前端静态页面和服务不在同一台机器。解决后端统一加跨域配置这是正路Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173, http://your-domain.com) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }注意allowedOrigins不能写*因为allowCredentials(true)和通配符不能共存maxAge(3600)表示预检请求结果缓存一小时减少每次请求都先发 OPTIONS 的延迟。如果你在联调阶段确实要省事可以临时允许所有来源但上线前必须收紧否则任何人网站都能调你的接口这是安全红线。5.2 登录方案用 JWT Token 而不是 Session在线错题系统一般有学生和老师两种角色。前后端分离下我不推荐 Session因为跨域和移动端都要处理 Cookie麻烦。用 JWT 是最常见的做法。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); Long userId JwtUtil.parseUserId(token); if (userId ! null) { request.setAttribute(currentUserId, userId); return true; } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录过期\}); return false; } }这段逻辑把解析出的 userId 放在 request attribute 里后面的 Controller 直接取避免每个接口重复解析。JwtUtil里要控制过期时间我一般设 12 小时refresh token 这套机制对毕设来说可选。注册拦截器时用excludePathPatterns放行/api/login、/api/register和静态资源否则登录接口自己都进不去这就是最常见的“启动不报错、一调就 401”的原因。5.3 用 Docker 部署 SpringBoot 项目从 jar 到容器部署是把项目交付给用户看的关键一步。先把后端打成 jar再写 DockerfileFROM openjdk:11-jre-slim WORKDIR /app EXPOSE 8080 COPY target/error-notebook-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT [java, -Dfile.encodingUTF-8, -jar, app.jar]构建命令docker build -t error-notebook:1.0 .启动命令docker run -d \ --name error-notebook \ -p 8080:8080 \ -e TZAsia/Shanghai \ -e DB_HOSTmysql-container \ -e DB_USERroot \ -e DB_PASSWORD123456 \ --network my-net \ error-notebook:1.0这里的几个参数必须说清楚DB_HOSTmysql-container不能写 localhost因为容器里的 localhost 是容器自己不是宿主机上的 MySQL--network my-net让后端容器和 MySQL 容器处在同一个 Docker 网络里否则它们互相访问不到。如果 MySQL 跑在宿主机而不是容器里就把 DB_HOST 改成宿主机局域网 IP并在 MySQL 里给用户开%的远程权限。5.4 application.yml 里的部署参数与环境变量覆盖我把后端配置统一放到 application.yml但部署时不允许改代码所以关键参数全部用环境变量占位server: port: ${SERVER_PORT:8080} spring: datasource: url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/error_notebook?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: ${DB_USER:root} password: ${DB_PASSWORD:root} driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 5 min-idle: 5 max-active: 20 redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} password: mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0${DB_HOST:localhost}的写法是“默认值”本地不设环境变量时用 localhost容器部署时通过-e DB_HOSTxxx覆盖。allowPublicKeyRetrievaltrue这个参数是 MySQL 8.0 客户端连接时的必备项不加可能报Public Key Retrieval is not allowed。map-underscore-to-camel-case: true让wrong_reason自动映射成wrongReason实体里就不用写一堆TableField。本地调试时我会把log-impl打开能在控制台看到打印的 SQL部署时记得关掉否则日志会被刷爆。6. 用数据验证错题系统值不值得做一条 SQL 看复习效果系统做到这里功能都是“有”了但怎么证明它有效我给自己定了一个验收指标同一道题被重复记录两次以上说明掌握度更新机制失效。为此我常跑一条排查 SQLSELECT subject, chapter, content, COUNT(*) AS repeated_times FROM error_question WHERE deleted 0 AND user_id #{userId} GROUP BY user_id, subject, chapter, content HAVING COUNT(*) 2 ORDER BY repeated_times DESC LIMIT 10;如果这条 SQL 查出的重复错题很少说明学生真的在按“做对就加掌握度”的规则去更新状态如果重复记录很多那就是录入维度不对要么是学生不填错因要么是前端表单没把“这道题其实已经会了”的情况处理掉。我会在产品里加一个约定录入错题时只有选了错因和掌握度才能保存否则前端按钮置灰。这个小约束能逼着使用者思考也是这个系统比普通笔记软件实用得多的原因。另外一个进阶验证是复习计划表的使用率统计review_record里 30 天内复习过的题数占错题总数的比例。比例低于 20%系统做得再好也是摆设。我用第一人称说个习惯每周末导一次重复错题列表把重复三次以上的题单独建一个“顽固题单”下周一重点重做。这套系统真正的价值不在代码量而在你敢不敢用它倒逼自己的学习行为。希望帮到你。本文还有配套的精品资源点击获取