
从零搭一套面向真实教学场景的智能学习平台后端SpringBootMyBatisMySQL前端Vue全家桶这中间藏着的坑远比代码行数多。这篇文章把我从数据库建模到前端路由、从登录鉴权到视频播放、从开发环境崩溃到服务器部署的全部过程捋一遍源码思路和关键配置都会给到适合正在做课程设计、毕业设计或者公司内部培训系统选型的人参考。1. 先搞清楚要解决什么问题学习平台的核心链路很多人一上来就建表写接口搞了半个月发现做出来的东西是个课程展示网站而不是学习平台。这两种东西的差别非常大。课程展示网站只要把课程挂上去用户能看就行学习平台则要管住学习这件事本身用户学了没有、学到哪了、学得怎么样、平台怎么根据学习行为做反馈。我自己在做这套系统之前先画了一条核心链路课程管理、章节内容、视频学习、作业考试、学习记录、数据统计六块内容一条线串下来。每一环都要有数据支撑不能断。比如用户看完一节视频前端必须上报进度考试交卷之后后台要能自动判分并把分数回写到学习记录里管理员后台要能看到每个学生的学习时长、完成率、考试成绩。这些功能串起来才配叫学习平台。这个项目的角色权限也提前定了学生、教师、管理员三种角色。学生端管我的课程、在线学习、考试、成绩查询教师端管课程创建、章节编排、题库维护、试卷发布管理员端管用户审核、课程审核、全局数据看板。角色定清楚后面数据库设计、接口设计、前端路由都会轻松很多不然做到一半就乱套。2. 技术栈选型复盘这一套组合到底合不合适技术选型这块我不想写成教科书只说当时纠结过的几个点。2.1 后端为什么定在SpringBoot 2.7.xSpringBoot 3.x出来以后很多人直接上3.x结果发现JDK版本、javax包名全部变了MyBatis等第三方starter的兼容性也容易出问题。我的建议是如果不是从零开始并且有明确的升级需求老老实实用2.7.x。这个版本稳定、资料多、第三方starter成熟遇到问题基本都能搜到解决方案。JDK用1.8配上Maven管理依赖整个工程跑起来很顺畅。SpringBoot在这套系统里承担的核心职责是把Controller-Service-Mapper三层串起来通过starter机制快速整合SpringMVC、MyBatis、Druid连接池和JWT鉴权再加上统一的异常处理和参数校验。这些能力用原生SSH或者Servlet写当然也能实现但开发效率完全不是一个量级。2.2 ORM框架选MyBatis而不是JPA的原因MyBatis和Spring Data JPA之争老生常谈了。我的看法很简单这个项目里有大量自定义的联表查询比如学生课程列表需要联查教师姓名、课程章节课时、学习进度用JPA写JPQL或者Specification反而别扭而MyBatis的XML里直接写SQL逻辑一目了然性能上也更容易把控。这个系统我用的还是MyBatis-Plus不是裸MyBatis。MyBatis-Plus的BaseMapper自带单表CRUD像用户管理、课程分类管理这种简单场景直接调用省掉大量重复的XML。联表查询和复杂统计仍然手写SQL放在XML里两边优势都拿住。建议你不要在这一步过分纠结MyBatis-Plus只是增强不是替代底层还是MyBatis那一套机制。2.3 前端框架定位在Vue整套生态Vue的生态对中小型后台管理系统太友好了。Element UI提供现成的表格、表单、弹窗、分页组件后台管理系统无非就是这些界面元素来回组合。React当然也能做但学习成本和对团队的要求会更高一些。前端我这里用的是Vue 2.6 Element UI Vue Router Vuex Axios的组合。很多人在Vue 2和Vue 3之间摇摆其实对这个项目影响不大核心是组件化开发、状态管理、路由拦截这三板斧。Vue 3 Composition API写起来更现代但配套组件库和一些第三方插件的兼容性没有Vue 2那么无脑。如果你是做课程设计、毕业设计或者公司内部系统Vue 2.6这套技术栈最不容易卡壳如果你面向长期维护的新项目用Vue 3也没问题原理是相通的。2.4 数据库定位在MySQL 8.0MySQL 5.7和8.0我都用过。8.0有窗口函数做学习数据统计比如排名、同比、环比会顺手很多默认字符集是utf8mb4对中文和表情符号的支持也更稳。这台机器如果内存有限用5.7也不是不行但新建项目建议直接8.0。3. 数据库建模学习平台的核心表设计与索引规划数据库设计是这个项目最值得花时间的地方。编码写错了改起来容易表结构设计错了后面就是动一发牵全身的返工。我把核心表分成四组来讲。3.1 用户与权限相关表用户表sys_user不能只存账号密码还要考虑角色、状态、头像、真实姓名这些基础信息。系统里三个角色共用一个用户表用role字段区分前端再根据role字段渲染不同的菜单和操作按钮。没有必要把学生表、教师表、管理员表分开建否则登录逻辑要先查几个表才能判断用户身份增删改查也麻烦。密码存储用的MD5加盐或者用BCrypt。我建议用BCrypt同样的密码每次加密结果都不一样防止彩虹表碰撞。虽然代码里只是调用一个方法但这属于安全底线问题不建议省。用户相关的表结构大致如下字段类型说明idbigint主键自增usernamevarchar(50)登录账号唯一索引passwordvarchar(100)BCrypt加密后的密码real_namevarchar(50)真实姓名rolevarchar(20)student/teacher/adminavatarvarchar(255)头像地址statustinyint0禁用 1启用create_timedatetime创建时间3.2 课程与章节表课程表course是核心业务表。字段设计上需要注意几个点课程封面图片路径、课程分类ID、授课教师ID、课程简介、课程状态草稿/已发布/已下架。教师ID一定要加索引因为查看我创建的课程是一个高频查询。课程小节表course_section设计成自关联的子表一个课程下面挂多个章节章节下面再挂课时。为了方便处理我设计得比较扁平直接用course_id关联sort字段控制排序。课时信息包含视频地址、课件地址、课时时长、是否免费试看。视频地址这块我的做法是保存相对路径而不是完整URL这样开发环境、测试环境、生产环境切换时只需要改配置文件里的域名前缀不用动数据库。视频文件本身用m3u8格式切片后传到OSS或者服务器指定目录数据库只存路径。3.3 学习记录表学习记录表study_record是学习平台和课程展示网站的核心区别所在。用户每学习一个课时前端会定时上报学习进度后端写入或更新这张表。字段包括用户ID、课时ID、学习时长、视频播放进度、学习状态未完成/已完成、最近学习时间。这张表的设计上要特别注意组合唯一索引user_id section_id保证同一用户对同一课时只有一条记录。否则每次上报都插入一条新数据表很快就会爆炸。业务逻辑上做到存在即更新不存在才插入。3.4 考试相关表考试模块由试卷表exam_paper、试题表exam_question、试卷试题关联表exam_paper_question、用户答题记录表exam_answer_record组成。选择题、判断题这种客观题答案存在试题表的answer字段里交卷时后台直接比对判分。主观题则需要在答题记录表里单独存考生答案和教师评语、得分。这套设计不算复杂但已经是目前中小型学习平台里最通用的方案了。另外一点建议考试模块的建表时很多初学者会把试卷和试题合并到一张表里这是不行的。一份试卷多个试题一个试题可以被多份试卷复用必须拆成多对多的结构。4. 后端工程实战从统一返回到核心业务链路后端工程结构我是按功能模块分的common公共类、config配置类、controller接口层、service业务层、mapper数据访问层、entity实体类、vo视图对象。这样组织的好处是每个人都能快速定位某一行代码在哪个包下面后面扩展新模块也只需要复制一个目录结构。4.1 统一返回结果与全局异常处理接口层必须统一返回格式。我定义了一个Result类包含code、message、data三个字段。成功返回Result.success(data)失败返回Result.error(code, message)。这看起来很简单但能避免团队里每人各写一套返回格式的混乱前端Axios拦截器也只需要处理一种结构。异常处理我用了RestControllerAdvice ExceptionHandler的全局处理。业务异常比如密码错误、无权访问直接抛自定义的BusinessException全局捕获后转成对应的JSON返回。这样Controller里就不需要到处写try-catch。4.2 登录鉴权为什么选JWT而不是Session学习平台的用户端和管理端是前后端分离的接口要走无状态鉴权。如果使用Session后端需要维护会话状态分布式部署时还得搞Session共享麻烦。JWT的核心思路是用户登录成功后后端签发一个包含用户ID、角色、过期时间的Token前端存下来每次请求带上后端验证签名后从Token里取出用户信息。SpringBoot里集成JWT只需要引入jjwt依赖写一个拦截器在preHandle方法里校验请求头中的Authorization字段。还需要写一个配置类把不需要登录就能访问的接口路径放行比如登录接口、注册接口、课程列表接口。具体代码不算复杂public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(未登录); } // 解析验证token把用户ID和角色放入request Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } }4.3 拦截器注册的坑拦截器注册这块有个特别容易踩的细节实现了WebMvcConfigurer的配置类重写addInterceptors方法时如果项目里同时配置了跨域CorsFilter和拦截器拦截器里的preHandle执行顺序可能会在CORS处理之前导致跨域请求的OPTIONS预检请求直接被拦截器拦住报未登录错误。解决办法很简单在preHandle方法里把OPTIONS请求直接放行如上所示。这个坑不遇到真没想到遇到了之后排查了一晚上。4.4 核心业务课程模块课程列表接口要按角色做数据权限。学生看到的是已发布的课程教师看到的是自己创建的课程管理员看到的是全部课程。我用MyBatis的动态SQL来处理这个逻辑xml的sql标签里先判断角色再拼对应的where条件。课程详情接口要一次性返回课程基本信息、教师信息、章节列表、每个小节的学习状态。如果前端分别调四五个接口来拼这个页面性能差而且逻辑分散。后端这边用一次联表查询搞定Service层组装成一个CourseDetailVO返回给前端。这个VO组装过程是业务复杂度最高的地方之一。我的建议是不要为了少写代码把所有信息都塞在一个Map里定义清晰的VO类字段语义明确对前端友好也方便后续维护。4.5 考试判分逻辑客观题自动判分逻辑不复杂交卷时遍历每道题将用户答案和正确答案比对正确则累加该题分数最后写回总分。事务上要注意插入答题记录和更新考试状态必须放在同一事务里用Transactional注解保证否则可能出现答题记录写入了但状态没更新这种不一致。主观题我给的是先提交答卷教师端在待批改列表里打分写回评语和得分前端学生端才能看到最终成绩。这块扩展的思路是按状态字段流转待批改、已批改、已发布。5. 前端工程实战Vue怎么组织才能和后端无缝配合前端工程化这块我按工程结构、路由权限、请求封装、播放器集成四个部分讲。5.1 工程结构与路由权限前端src目录下我分了api接口请求、assets静态资源、components公共组件、router路由配置、storeVuex状态管理、views页面组件几个目录。页面组件再按模块二级划分student、teacher、admin、login。路由权限这里必须做两层控制。第一层是路由守卫全局前置守卫里判断是否已登录未登录的重定向到登录页登录后判断当前用户角色动态添加该角色对应的路由。第二层是菜单控制根据角色渲染不同的侧边栏菜单项。比如学生端菜单是我的课程、我的考试、学习记录教师端是课程管理、题库管理、试卷管理。具体做法是路由表在Vue Router中用addRoutes方法动态添加菜单数据由后端接口返回前端根据返回值渲染。不建议把所有菜单写死在前端因为角色一多维护起来就是灾难。5.2 Axios封装统一拦截和错误提示Axios请求封装看起来简单实际上要注意几个细节。第一是请求头统一带上token和Content-Type第二是响应拦截器里统一处理业务错误码code不是200时弹提示第三是401状态码时自动清空本地存储并跳转登录页。前端接口定义建议按模块拆分文件比如course.js里放课程相关接口exam.js里放考试相关接口。页面组件里只引入自己需要用到的接口函数不直接在组件内写请求路径后期域名一改只需要在api目录里的配置文件中改一个baseURL。5.3 视频播放模块的实现视频学习是学习平台的重头戏。我先后对比了几种方案原生video标签、vue-video-player、DPlayer、ckplayer。最终用的是vue-video-player因为它对hls协议的m3u8视频播放支持比较完整而且封装好的组件可以直接通过props传配置项。视频播放页面的逻辑重点不在播放器本身而在进度上报。我在播放器的timeupdate事件里做节流处理每15秒上报一次当前进度播放到视频总时长减5秒的位置时上报完成状态。这样既不会频繁请求后端造成压力又能记录用户真实学习数据。进度回显逻辑进入播放页先根据sectionId调接口查学习记录如果有记录把视频的currentTime设置为返回的进度值用户接着上次的地方继续学。这里有一个播放器ready事件和视频元数据loadedmetadata事件的时序问题必须在video元素load后执行seek操作否则会跳不到指定位置。5.4 Vuex状态管理的边界Vuex在这个项目里不滥用。我只维护了三个全局状态用户信息、角色权限列表、侧边栏展开收起状态。课程列表、考试列表这种业务数据不放到store里就在各自页面的data或ref里管理即可。把请求到的业务数据全部塞store不仅内存开销大还会导致页面刷新后数据丢失的协调问题得不偿失。6. 踩坑记录版本、映射、跨域、时区一个都没落下开发过程中踩过不少坑挑几个最有代表性的写出来帮后面的人省点时间。6.1 SpringBoot版本过高导致的依赖连锁问题我一开始图新鲜用了SpringBoot 3.0结果MyBatis-Spring-Starter的旧版本不兼容启动直接报错。换成SpringBoot 2.7.x版本后一切正常。如果你的项目也遇到springboot版本太高的问题建议先查一下关键依赖mybatis-starter、druid-starter、jjwt对SpringBoot版本的兼容要求而不是盲目升级框架。排查依赖冲突的通用手段是Maven引入依赖后跑一下mvn dependency:tree看看实际生效的版本到底是多少和SpringBoot BOM指定的版本是否匹配。6.2 MyBatis的XML映射文件没扫到导致启动报错MyBatis的Mapper接口和XML文件有两个容易踩的细节一是application.yml中要配置mapper-locations指明XML文件位置二是pom.xml里要确保资源目录包含src/main/resources下的xml文件。如果启动报Invalid bound statement (not found)需要依次检查Mapper接口的Mapper注解有没有写或者有没有在启动类上配MapperScan包路径、XML文件的namespace是不是接口全限定名、方法名和XML里的id是否一致、resultType或resultMap是否写对。这几个问题里有一个不满足就会报这个错。6.3 跨域配置的完整过程前后端分离项目必须处理跨域。我在后端的Config类里写了一个CorsFilter的Bean允许所有来源、所有方法、所有请求头。前端vue.config.js里devServer配置target指向后端的localhost:8080并用proxy代理。这样开发环境下前后端是不同端口也能正常请求。生产环境我是把前端打包后的dist目录直接放到Nginx里然后用Nginx反向代理/api前缀的请求到后端服务。生产环境不需要再配置跨域因为浏览器访问的是同源地址Nginx已经帮我们转发了。6.4 MySQL时区问题导致的时间差8小时一开始数据库连接串没配置serverTimezone导致查出来的时间比实际时间少了8小时。解决方式是在JDBC连接串上加上serverTimezoneAsia/Shanghai和useSSLfalse。Jackson序列化LocalDateTime时还需要在配置里指定时间格式否则前端拿到的是带T的ISO字符串格式不好处理。这里提醒一下Java 8的时间类型LocalDateTime是线程安全的也推荐用。老项目中Date类型容易出现格式化问题新项目建议直接LocalDateTime全栈走通。7. 部署上线从本地到服务器的完整链路7.1 后端打jar包部署SpringBoot服务端打包很简单Maven执行mvn clean package生成jar包后放到服务器上用java -jar运行。服务器配置了systemd服务或使用Docker跑容器两种方式我都在用。Docker部署的关键点是Dockerfile里基础镜像使用openjdk:8-jre-alpine这个镜像体积小启动快。启动命令里指定JVM参数FROM openjdk:8-jre-alpine COPY target/study-platform.jar /app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app.jar, --spring.profiles.activeprod]数据库连接、Redis、OSS密钥这些环境相关的配置不能放代码里要放在application-prod.yml中并且只在服务器上的环境变量里注入避免密码泄露。7.2 前端Nginx部署前端打包后生成dist目录上传到服务器上的/usr/share/nginx/html。Nginx配置要注意一个关键点Vue Router使用history模式时刷新一个非根路径会404需要配置try_files来指向index.html。location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }/api的接口请求用反向代理转发到后端服务location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }7.3 Docker Compose编排如果数据库和Redis也跑在Docker里建议用Docker Compose把MySQL、后端jar、前端Nginx一次性编排起来。配置好端口映射和数据卷持久化服务器重启后一条命令就能拉起全部服务。这里数据库的文件目录必须挂载到宿主机否则容器销毁数据就没了这个坑一定要避开。8. 后续可以继续扩展的方向这套系统已经能跑通课程学习考试测评进度跟踪的完整闭环但如果还要进一步提升平台的智能属性我给这几个扩展方向一是基于用户学习行为的学习路径推荐。现在系统里已经有study_record表积累一段时间后可以用用户的课程分类偏好、学习时长、完成率基于协同过滤或简单推荐规则给用户推荐下一门课。这块用Java做初版推荐逻辑或者把数据导出后用Python离线算好再回填都行。二是考试数据的分析报表。从exam_answer_record表能统计出每道题的错误率、每个知识点的薄弱学生名单教师端可以据此调整教学重点。用ECharts画雷达图和柱状图数据直接从后端聚合查询效果不错。三是消息通知模块。作业被批改、课程有新课、考试即将开始这些事件通过站内信通知用户。后端做一个定时任务扫描事件表前端用WebSocket或者轮询拉取未读数量实现起来也不复杂。从SpringBoot到Vue从MySQL到MyBatis整套链路跑完最大的感受是技术栈本身不复杂复杂的是怎么把业务逻辑理清楚怎么在开发过程中少被环境、版本、兼容性问题绊住脚。希望这篇文章能帮你把这个项目的路走顺。