
最近我把一套 Java Web 智能学习平台从零搭了出来后端用的是 SpringBoot2前端是 Vue3持久层交给 MyBatis-Plus数据库选了 MySQL8.0项目里还顺手写了完整的接口文档和部署说明。做这个项目一方面是为了把全栈技术栈串起来另一方面也是想验证一套学习类产品的通用功能怎么落地——用户、课程、题库、练习、错题本、统计报表这条路走通之后换成培训、考试、知识库场景基本就是改改表结构的事。这篇文章会把整个系统的设计思路、技术选型理由、数据库建模过程、后端接口设计、Vue3 前端工程化、Docker 部署 MySQL8.0 的实操步骤以及我实际踩过的坑全部捋一遍。如果你是准备做毕业设计、个人作品集、或者公司内部想快速搭一套在线学习后台的开发者这套源码的思路可以直接复用。本文不贴整段源码重点讲清楚“为什么这么做”和“哪些地方容易翻车”代码只给关键片段和配置示例。1. 项目概述与需求拆解1.1 智能学习平台到底解决什么问题很多新手一听“学习平台”就以为是一个视频网站加一个题库其实真做起来逻辑链比想象中长。用户从注册登录开始要能浏览课程、查看章节、记录学习进度还要能做专项练习、参加模拟考试系统自动判分之后把错题收进错题本再根据错题分布反推薄弱知识点最后给出推荐练习。这一条链路走完才配叫“智能学习平台”而不是单纯的 CRUD 管理系统。我做的这套系统把核心流程分成三层。底层是基础数据层包含用户、课程、章节、试题、知识点这些静态资源中间是业务逻辑层包含练习记录、考试记录、错题归集、学习进度统计最上层是展示与交互层包括用户端的学习页面和管理端的后台页面。分层明确之后前后端联调、权限控制、数据统计都变得有迹可循也方便后续把某个模块单独抽出来升级。1.2 用户角色与功能权限划分权限设计我一开始就做了三个角色普通用户、老师、管理员。普通用户能看课程、做题、考试、查错题和自己的统计老师可以维护题库、给试题关联知识点、查看所带学生的练习情况管理员负责用户管理、课程上架下架、全局数据报表。这里有个容易被忽视的点如果直接把所有菜单都丢给前端用 Vue Router 的 meta 控制显隐刷新页面后权限状态很容易丢失。我最后的做法是后端接口统一校验前端只做菜单展示层面的隐藏核心数据依然靠拦截器保护。也就是说前端权限是体验问题后端权限才是安全问题。1.3 “智能”二字体现在哪儿项目标题里最显眼的词是“智能”如果只是增删改查那就名不副实了。我实现的三处智能逻辑分别是自动判分、错题归集、知识点薄弱度计算。自动判分比较好做单选、多选、判断这三种题型固定答案提交后遍历比对即可错题归集是在保存答题记录的事务里同步写入错题本知识点薄弱度是用做题记录里某个知识点下的错误率加权算出推荐指数错误率超过阈值就把这个知识点下的题目推荐给用户作为专项练习。这三块逻辑并不复杂但把“学习”和“平台”真正串了起来。对使用者来说它不是等用户主动找题而是系统根据行为数据把合适的题目推给用户这也是这个项目能在简历上拿出来说的亮点。2. 技术选型解析为什么是这套组合2.1 SpringBoot2 而不是 SpringBoot3现在确实 SpringBoot3 已经发布有一段时间了但我最终还是选了 SpringBoot2.7.x。原因很现实SpringBoot3 强制要求 JDK17而很多公司的线上环境还停留在 JDK8 或 JDK11如果项目要给别人二次开发版本兼容性会是一个大坑。SpringBoot2 的生态更成熟网上搜问题几乎每一条都有解决方案第三方 starter 的兼容性也更好。还有一点是 MyBatis-Plus 对 SpringBoot2 的支持最稳。虽然新版 MyBatis-Plus 也适配 SpringBoot3但如果你用的是 3.5.x 以下的老版本插件升级时会出现一堆意想不到的报错。做个人项目或中小型系统稳定性比技术新更重要。2.2 Vue3 组合式 API 与生态成熟度前端选择 Vue3 不是跟风而是确实有价值。用script setup组合式 API 写业务逻辑比 Vue2 的 options API 清晰太多。比如做做题页面时计时器逻辑、题目切换逻辑、答题记录逻辑可以分别写成独立的let timer ref(null)、let currentIndex ref(0)、const answerMap reactive({})代码组织起来很自然。配套组件库我用了 Element Plus状态管理用了 Pinia路由用了 Vue Router 4。这三个组合在 Vue3 生态里基本是事实标准。如果你要问 Vue2 迁移 Vue3 成本高不高我的经验是组件库换掉后改造成本集中在表单校验、弹窗调用这类 API 变化上业务逻辑本身改动量不大。2.3 MyBatis-Plus 减少样板代码MyBatis-Plus 在我心里的定位是“单表 CRUD 的终结者”。用户表、课程表这种基础实体用BaseMapper提供的方法就够了完全不用写 XML。像insert、selectById、updateById这类操作一行代码搞定开发效率提升非常明显。真正需要手写 SQL 的场景是关联查询和复杂统计比如查询用户最近十次考试记录并按时间排降序、统计每个知识点的错误率这些我会在 Mapper 接口上直接使用Select注解写 SQL。用 MyBatis-Plus 不存在“不能用 XML”的限制反而是哪里合适用哪里灵活度更高。2.4 MySQL8.0 带来的便利与要求MySQL8.0 最吸引我的是窗口函数、公用表表达式CTE和更好的 JSON 支持。在做用户学习时长统计时窗口函数能直接算出累计值避免在 Java 里做一堆循环计算。字符集方面8.0 默认是 utf8mb4直接能存表情符号不用再像 5.7 时代那样手动改配置。但 8.0 也带来了一些麻烦最典型的是驱动包和时区问题。com.mysql.jdbc.Driver在 8.0 下已经废弃必须用com.mysql.cj.jdbc.Driver连接字符串里还要加serverTimezoneAsia/Shanghai否则默认时区和数据库时区不一致查询出来的时间会差 8 小时。这些问题在后面的部署章节我会再详细讲。3. 数据库设计与 MyBatis-Plus 实践3.1 核心表结构的设计思路数据库设计决定了这个项目能撑多大、能扩展多方便。我总共设计了 9 张核心表用户表、课程表、章节表、试题表、知识点表、练习记录表、考试记录表、错题本表、学习进度表。只用 9 张表是因为学习平台的核心链路就是围绕“人-课-题-记录”展开不需要一上来就铺几十张表。用户表比较常规id、username、password、nickname、role、avatar、create_time。密码记得要加密存储我用的是 BCrypt。课程表和章节表是一对多关系所以章节表里有course_id外键。试题表设计时加了type字段区分单选、多选、判断用knowledge_point_id关联知识点再用answer字段存正确答案analysis字段存解析。练习记录表和考试记录表有点相似所以我让它们共用了一套字段逻辑比如user_id、question_id、selected_answer、is_correct但考试记录多一个exam_score字段。错题本表相对简单我只存user_id和question_id每次自动判分发现错误时先查一下这条记录有没有存在重复错题只更新时间戳避免数据膨胀。3.2 自动填充与逻辑删除MyBatis-Plus 有两个特别常用的功能自动填充和逻辑删除。自动填充我用来处理create_time和update_time在insert和update时自动写入当前时间没写这两行代码的话每次新增数据都要手动setCreateTime(new Date())表多了以后非常啰嗦。实现方式是在实体类上给字段加TableField(fill FieldFill.INSERT)注解然后实现MetaObjectHandler接口的insertFill和updateFill方法。逻辑删除我用的字段名叫deleted加TableLogic注解配置后所有查询自动加WHERE deleted 0条件删除操作变成UPDATE这对保留用户答题历史非常有意义因为错题本和答题记录里的数据关联性强物理删除会引发一堆数据丢失问题。3.3 分页插件配置与关联查询MyBatis-Plus 分页需要手动配置一个拦截器。网上很多教程只说加配置不提版本问题。在 SpringBoot2 环境下用MybatisPlusInterceptor加PaginationInnerInterceptor就行如果用的是PaginationInterceptor老配置控制台会直接提示过时。关联查询方面比如查询课程列表时要把章节数量带出来这种用Select注解写一段子查询 SQL 最直观。写 SQL 时我会刻意避免SELECT *而是把需要的字段列清楚性能好而且后面加字段也不会把无关数据带到前端。3.4 防止 SQL 注入的细节如果完全依赖 MyBatis-Plus 的 WrapperSQL 注入风险基本为 0因为框架底层用的是预编译。但手写 SQL 时可就要小心了${}和#{}的区别一定要搞清楚。${}直接拼接字符串如果传入用户可控的值等于把数据库操作权限拱手让人#{a}是预编译参数才是安全用法。我当时犯过一个错误在动态排序时把“排序字段”用order by ${sortField}拼接结果接口被调用方传了个奇怪的表达式进来好在本地测试时发现了。后来的做法是排序字段做白名单校验只允许后端定义好的几个值彻底堵死这个口子。4. 后端核心模块开发实录4.1 JWT 登录鉴权的完整流程登录模块我选择用 JWT 拦截器实现没有引入 Spring Security因为这套平台的权限模型没那么复杂用 Security 反而引入大量默认配置和学习成本。用户携带用户名密码请求登录接口后端校验通过后生成 token格式是Header.Payload.Signature把用户 id 和角色放进 payload返回给前端。前端拿到 token 后放在localStorage每次请求在 axios 请求拦截器里拼到Authorization头。后端写一个LoginInterceptor实现HandlerInterceptor接口在preHandle方法里校验 token 有效性再把解析出的用户信息放入ThreadLocal。这里有个经验拦截器放行路径要小心/api/login、/api/register必须放行/api/**下面的业务接口全部拦截。如果你把静态资源路径也拦截了页面会白屏排查起来很花时间。4.2 做题与自动判分的接口逻辑做题接口是POST /api/practice/submit请求体里是一道题的 id 和用户选择的答案。后端收到请求后先从题库里查出标准答案比对文本是否一致得到is_correct后保存练习记录然后根据对错更新错题本。这里面有个关键点保存练习记录和更新错题本必须放在同一个事务里否则出现“记录存了但错题本没更新”这种数据不一致问题。用Transactional注解我是踩过坑的。一开始我把它放在 Controller 方法上事务倒是生效了但异常抛出后前端拿到的错误信息不友好。后来我把事务放在 Service 实现类上Controller 只做参数校验和结果返回职责清晰排错也方便。另外特别注意Transactional只在公有方法上生效同类内部调用是失效的这个坑几乎每周都能在报错群里看到有人在问。4.3 知识点薄弱度与推荐逻辑薄弱度计算我用了最简单的公式错误次数除以该知识点下总做题次数。比如某个知识点做了 10 题错了 7 题薄弱度就是 70%。我设定一个阈值超过 60% 就把这个知识点下的未练习题目推荐给用户。为了跑这个逻辑我建了一个视图查询把练习记录按知识点分组聚合。实际实现用SELECT里的GROUP BY knowledge_point_id再在 Java 里做阈值过滤。推荐题目列表直接用IN查询取出。这套逻辑简单但实用而且可以方便地改成协同过滤或者更复杂的推荐算法后续优化空间不缺。5. Vue3 前端工程化要点5.1 Vite 项目初始化与环境变量Vue3 项目我用 Vite 3 初始化命令是npm create vitelatest learning-frontend -- --template vue。Vite 比 Webpack 在开发环境快太多热更新几乎秒开。要注意的是 Node 版本要求Vite 3 需要 Node 14.18Node 12 启动会直接报错所以我先检查了本机的node -v。项目里我配置了环境变量文件.env.development里写VITE_API_BASE_URL/api.env.production里写线上地址。环境变量必须以VITE_开头才能在客户端代码里使用我最早按习惯写了个API_BASE_URL结果页面里一直拿不到值查了半天才发现是前缀问题。5.2 Axios 请求封装与 Token 注入所有请求我都封装在src/utils/request.js里导出统一的request函数。创建 axios 实例时设置baseURL和超时时间请求拦截器加 Token响应拦截器做统一错误处理。这里有一个细节经常被忽略响应拦截器里要根据 HTTP 状态码做不同处理401 跳到登录页并清空本地缓存403 提示无权限500 提示服务器异常。还有一类问题是跨域。开发环境我用 Vite 的 proxy 把/api代理到后端http://localhost:8080生产环境直接用 Nginx 反代或者把前端打包文件放进 SpringBoot 的static目录。如果后端不做 CORS 配置前端直连后端接口会报跨域错误这个几乎每个人都会遇到。5.3 路由权限与状态持久化路由设计我采用了静态路由加动态路由结合的方式。登录、注册、首页这些公共路由写在constantRoutes里需要权限的页面在登录成功后由后端返回菜单列表前端再通过router.addRoute动态注册。这样做的最大好处是用户刷新页面后权限路由可以重新从后端拉取不会出现白屏。Pinia 管理用户状态时我会把用户信息同时持久化到localStorage。项目里有几个页面是必须登录才能访问的我用全局路由守卫beforeEach检查 store 里有没有 token没有就跳登录页。要注意的是localStorage里存的 token 需要和后端拦截器配合校验如果 token 过期后端返回 401前端响应拦截器会自动清空并跳转登录页。5.4 动态表单、Tab 样式与代码定位技巧做题目管理时我遇到了一个很典型的 Vue3 需求编辑选择题时选项数量不是固定的需要动态添加和删除表单行。这个用 Element Plus 的v-for循环加form.optionList数组即可添加选项就是push一条空数据删除选项就是splice(index, 1)。但有个小坑点击删除后组件不会自动刷新校验状态所以删除后要手动调用clearValidate重置一下表单校验。Tab 标签页的样式调整也是这类后台系统的常客。Element Plus 的 Tabs 默认样式比较朴素想改标签高度、字体颜色、选中下划线颜色可以用 CSS 变量覆盖比如--el-tabs-header-height和.el-tabs__item.is-active { color: #409EFF; }实测这样改比重新写组件来得稳。还有一个效率技巧我装了一个 Vue3 的 Vue Language Features 插件配合模板内联了>docker pull mysql:8.0 mkdir -p /opt/mysql/data /opt/mysql/conf /opt/mysql/logs docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDRoot123456 \ -e TZAsia/Shanghai \ -v /opt/mysql/data:/var/lib/mysql \ -v /opt/mysql/conf:/etc/mysql/conf.d \ -v /opt/mysql/logs:/logs \ mysql:8.0启动容器后用docker ps确认状态如果状态是Up说明容器起来了。接着进入容器执行初始化脚本docker exec -it mysql8 mysql -uroot -p然后手动创建数据库和用户特别交代一句如果你的项目代码部署在另一个容器里不要用 root 账号直连单独创建一个业务账号并授权这样更安全。CREATE DATABASE learning_platform DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER learning% IDENTIFIED BY Learning123; GRANT ALL PRIVILEGES ON learning_platform.* TO learning%; FLUSH PRIVILEGES;连接串里要带上useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai否则中文乱码和时区问题会找上门。这里特别提醒符号在 YAML 配置文件里要转义写amp;我一开始没转义SpringBoot 启动时直接报了配置解析错误。6.3 后端打包与前端构建的联动后端我用 Maven 打包执行mvn clean package -DskipTests生成可执行的 JAR 文件。JAR 包可以直接java -jar learning-platform.jar启动也可以配合 Dockerfile 打成镜像。我的 Dockerfile 比较简单基础镜像用openjdk:8-jre-alpine把 JAR 复制进去暴露 8080 端口启动命令是java -jar /app.jar。前端构建执行npm run build生成的dist目录就是纯静态文件。我最后选择把这套前端文件复制到 SpringBoot 项目的src/main/resources/static目录下重新打 JAR 包后前端页面和后端接口共用同一个端口访问这样省掉了 Nginx 配置也避免了一大堆跨域问题。如果你要用 Nginx 单独部署前端记得把/api路径 proxy 到后端服务比如proxy_pass http://localhost:8080;。6.4 首次启动时遇到的环境问题我第一次打包部署时踩了好几个环境相关的坑。第一个是 JDK 版本SpringBoot2.7 默认用 JDK8 编译但我本机装了 JDK17Maven 编译报错“无效的目标发行版”后来在pom.xml里显式指定了java.version1.8/java.version才解决。第二个是 MySQL8.0 驱动版本默认引入的驱动是com.mysql:mysql-connector-j但版本需要匹配 MySQL 8.0 才能使用com.mysql.cj.jdbc.Driver。还有一个容易忽略的点SpringBoot 启动时如果连不上数据库应用不会马上失败但第一个数据库请求会超时报错。所以调试时建议先单独测试数据库连通性再启动应用不然排查起来就分不清是网络问题、防火墙问题还是账号密码问题。7. 常见问题与排查速查表下面这张表是我把整个开发过程中遇到的高频问题整理出来的每个问题都标了现象和建议的排查思路遇到类似的直接按表操作。常见问题现象排查思路与解决方法前端请求跨域浏览器控制台报 CORS 错误开发环境用 Vite proxy 或后端配置 CorsFilter生产环境用 Nginx 代理/api路径时间差 8 小时数据库时间正常接口返回时间少了 8 小时数据库连接串加serverTimezoneAsia/Shanghai容器内设置TZAsia/Shanghai中文乱码数据库中文显示问号或乱码数据库字符集用 utf8mb4JDBC 连接串加characterEncodingutf-8JWT 登录后刷新失效用户刷新页面后跳回登录页用户信息持久化到 localStorage刷新后再从后端拉取动态路由事务不生效执行报错后数据部分写入事务注解放在 Service 公有方法上并确认方法是被外部调用而不是同类内部调用MyBatis-Plus 分页不生效分页查询返回全部数据检查是否配置了MybatisPlusInterceptorPaginationInnerInterceptor老版本改用新 API动态表单删除选项后校验残留删除一行后仍显示校验提示删除后调用表单实例的clearValidate方法重置校验状态SpringBoot 启动报 MySQL 驱动错误提示 Loading classcom.mysql.jdbc.Driver失败驱动类改为com.mysql.cj.jdbc.Driver确认依赖是 MySQL8.0 的 connectorJAR 包部署后找不到静态资源前端页面 404确认前端dist内容已复制到src/main/resources/static目录重新打包这里再额外补充一个经验排查接口问题不要闷头看代码先看后端控制台日志。SpringBoot 默认的日志已经能打印 SQL 语句和异常堆栈能省大量时间。如果生产环境日志不够用了再考虑引入 Logback 的滚动策略按天和大小切分日志文件。最后总结的部分我想换成实际体感分享这套项目做下来我个人最大的体会是学习平台这种系统难的不是某个功能而是数据流转的通路。用户做题、系统判分、错题回写、统计归集每一步之间环环相扣单点功能做出来了很容易但链条完整跑通就需要对数据库设计和事务控制有比较扎实的理解。如果你准备基于这个项目做二次开发我的建议是先从题库模块着手把题目的题型扩充成填空题和简答题判分逻辑会从简单的文本比较升级到关键词匹配和语义相似度计算这才是学习平台真正能做出差异化的地方。做在线学习相关的产品数据和反馈永远是核心资产功能再花哨最后拼的还是对用户学习行为的理解深度。