
做在线教育系统的毕设或者项目练手SpringBoot Vue 这对组合确实是绝大多数人的首选。一个是后端事实标准一个是前端生态里绕不开的框架再搭配 MySQL 和 MyBatis这套技术栈覆盖面广、岗位需求大、网上资料又多拿来做课程设计、毕业设计或者简历项目都很稳妥。但这个项目真正能做扎实的人其实不多多数人卡在前后端联调、权限控制、分页查询、播放器接入这些环节上。我从头到尾梳理一遍把设计思路、核心实现、关键配置和踩坑记录全部整理出来照着做能少走很多弯路。这个系统本质上就是一个典型的在线教育业务管理系统核心用户分为管理员、教师和学生三类角色业务上涉及课程管理、章节管理、视频资源、订单支付、学习记录等模块。前后端分离架构下后端提供 RESTful API前端通过 Vue 进行页面交互数据统一走 JSON 格式。下面我用实际的搭建过程来讲清楚整个系统是怎么从零到一实现的。1. 项目整体设计与技术选型思考1.1 为什么选 SpringBoot Vue 这套组合先聊技术选型。在线教育管理系统属于典型的管理类应用业务逻辑不算极端复杂但涉及多角色权限、文件上传、流媒体播放、订单支付等多个子模块。选择 SpringBoot 而不是传统的 SSM核心原因在于 SpringBoot 的自动配置机制极大简化了项目搭建成本内嵌 Tomcat 支持独立运行配合 MyBatis 做持久层控制也足够灵活。Vue 这边选择的是 Vue 2 Element UI 技术栈不是最新的 Vue 3原因是目前企业项目里 Vue 2 存量仍然很大且 Element UI 组件库对管理后台类页面的支持极其成熟表格、表单、分页、弹窗这些高频组件开箱即用。这套前后端分离架构的好处有几点。第一是职责清晰后端只需要输出 JSON 数据不用关心页面渲染逻辑前端专注做交互和展示开发效率大幅提升。第二是接口复用性高同一个后端接口既能支撑 Web 端也能为未来扩展移动端留好余地。第三是部署灵活前端打包成纯静态文件可以扔到 Nginx 托管后端独立运行各管各的。1.2 功能模块与角色权限分析一个完整的在线教育管理系统不要一上来就陷入细节功能表先抓核心业务闭环。我按照用户角色和核心业务流程拆成了两大端、三套权限。管理员端负责整体平台管控包含用户管理、课程审核、分类管理、订单管理、数据统计。教师端负责内容生产核心是课程管理、章节管理和视频上传。学生端则是购买课程、观看视频、查看学习进度。三套权限在数据层面通过role字段区分在接口层面通过拦截器统一校验前端再配合路由守卫做菜单级控制。数据库表结构上核心表是用户表、课程表、课程分类表、章节表、订单表。用户和课程的关系是教师拥有课程学生在订单支付后通过中间表关联课程权限。要注意的一点是课程表和章节表必须包含sort_order排序字段否则课时顺序会乱掉。视频表我建议和章节表合并或者用独立的资源表关键是要记录视频的存储路径或 URL同时标记视频时长和是否免费试看。1.3 技术栈版本与初始化配置记录这里给出我实际搭建环境时的版本参数避免大家在版本兼容上浪费时间核心后端环境是 JDK 1.8 SpringBoot 2.5.4 MyBatis 1.3.2 MySQL 5.7 Maven 3.6。数据库连接池使用 Druid既能做连接管理也方便监控 SQL 性能。前端环境是 Vue 2.6.x Element UI 2.15.x Axios Vue Router Vuex。需要特别注意版本匹配问题SpringBoot 2.x 与 MyBatis-Spring-Boot-Starter 2.x 兼容性很好但 SpringBoot 2.4 之后的版本对配置文件多文档块的支持有调整如果你要配置多环境需要注意spring.profiles.active的写法。另外Java 8 和 SpringBoot 2.5 是黄金搭配不建议在这个项目上直接上 Java 17容易遇到 Lombok 和 MyBatis 的相关兼容问题。2. 数据库设计与核心接口实现2.1 数据库表结构设计思路在动手写代码之前数据库设计是重中之重。我见过太多人把课程表设计成一张大宽表所有字段堆在里面结果后面扩展分类、加标签、做推荐时全都推倒重来。合理的做法是先画业务关系图明确实体之间的关联关系。核心表之一用户表sys_user字段包括id、username、password、nickname、avatar、role、status、create_time。密码这里必须用 BCrypt 加密存储不要用 MD5。MD5 撞库太容易了在项目里会被直接扣分。BCrypt 自带盐值处理每次加密的结果都不同安全性高一个量级。课程表edu_course核心字段有id、teacher_id、category_id、title、cover、intro、price、original_price、status、create_time、update_time。status字段用于控制课程上下架状态是草稿、待审核还是已发布这个在管理端非常重要。章节表edu_chapter和课时表edu_video是课程内容的骨架。章节是课程下的一级目录课时是实际可播放的视频单元。课时表要包含章节 ID、课时标题、视频地址、视频时长、排序号。如果要支持试看功能在课时表上加is_free字段值为 1 表示免费试看。订单表edu_order里有订单号、用户 ID、课程 ID、实付金额、支付类型、订单状态。订单号建议使用雪花算法生成不要用自增 ID这样一是不暴露业务量二是并发下不会冲突。2.2 分类表设计从单表到树形结构的演进课程分类是容易被忽视但坑很多的表。最简单的做法是设计一张单表包含id、name、parent_id三个字段一个顶级分类下挂多个子分类。但是做管理后台的时候你会发现如果分类层级只有一层单表完全够用如果涉及多级分类比如“编程开发”下挂“Java”、“Python”“Java”下再挂“SpringBoot”单纯的parent_id外键查询在列表展示时会很痛苦。我建议用前序遍历法或者简单方案在分类表中增加level字段和path字段。path字段保存从根节点到当前节点的 ID 链路比如“1,3,7”这样前端做菜单回显和后端做分类筛选都极其方便。查询某个分类下的所有子分类时直接用WHERE path LIKE 1,%即可不用递归。2.3 登录认证与 JWT 拦截器实现细节在线教育系统的权限控制是整个项目的高频考点面试官十个里有八个会问登录认证怎么做。这里我强烈建议用 JWT 拦截器的方式而不是传统的 Session。原因很简单前后端分离架构下后端接口是无状态的Session 跨域处理麻烦分布式扩展时还要引入 Redis 共享 SessionJWT 把用户信息直接加密放在 Token 里后端只要验签就能拿到当前用户身份天然适合这种场景。具体实现时用户提交用户名密码后端校验通过后生成 JWT Token 返回给前端。Token 里放入用户 ID、用户名和角色过期时间设置为 24 小时。前端把 Token 存在 localStorage 中每次请求在 Axios 拦截器里带上Authorization: Bearer token。后端拦截器这一步有细节要处理Spring Boot 拦截器需要注册到 WebMvcConfigurer 中并配置白名单如登录接口、课程列表接口、前端静态资源路径。拦截器里只做 Token 的解析与校验不写业务逻辑。解析 Token 时用 try-catch 包裹如果签名异常或过期直接返回 401由前端统一跳转登录页。2.4 基于角色的接口权限控制方案有了 JWT 之后再接一个基于角色的权限控制就顺理成章。我用两个注解来实现RequireAdmin和RequireTeacher放在 Controller 方法上即可。原理是在拦截器中先完成 Token 校验再通过反射读取方法上的注解判断当前用户的角色是否满足要求。这里需要重点注意角色控制必须放在后端不能只在前端隐藏按钮。很多管理系统在前端通过v-ifrole admin来控制按钮显示这只能防君子不能防小人。只要有人直接调用接口地址依然可以越权。真实的权限控制一定是后端的前端决定“看得到什么”后端决定“能做什么”。3. 从零搭建项目的核心环节实操3.1 后端骨架搭建与 MyBatis 核心配置项目初始化这一步我直接用一个干净的 Maven 工程结构来说明根目录online-edu下包含online-edu-admin模块和online-edu-api模块。这种多模块结构的优点是把后台管理的 Web 层和对外 API 层区分开后续扩展微信小程序端时只需增加一个模块即可不需要动主工程。实际开发中我会把事情做得更细一个模块管 Controller 和配置另一个模块管 Service 和 Mapper再抽出一个common模块存放统一的返回结果、异常处理、工具类。这个分层逻辑虽然初期看起来繁琐但代码维护起来极其舒服尤其当你一个接口要复用多个 Service 的时候。MyBatis 配置要关注mapper-locations、type-aliases-package和驼峰映射开关。具体在application.yml中的配置模板mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.onlineedu.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case必须设为 true这样数据库中的create_time能自动映射到 Java 属性createTime省去大量手写映射配置。log-impl建议在开发阶段打开可以实时看到 SQL 执行情况和参数值排查问题时非常有用。生产环境再关闭即可。3.2 MyBatis 分页插件的正确用法分页查询是后台管理系统的高频需求手写 LIMIT 不仅繁琐而且容易出现 SQL 注入风险。MyBatis 分页插件 PageHelper 是主流方案但用法上很多人会踩坑。第一步在pom.xml引入依赖dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.5/version /dependency注意版本对应关系用 PageHelper-SpringBoot-Starter 而不是单独的 PageHelper这样可以自动适配 SpringBoot 自动配置。第二步在application.yml中补充配置pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: truereasonabletrue我解释一下是干嘛的当页码超过总页数时自动归一到最后一页页码小于 1 时自动归一到第一页这能避免很多前端传参异常导致的报错。第三步在 Service 层使用分页PageHelper.startPage(pageNum, pageSize); ListEduCourse courseList courseMapper.selectCoursePage(condition); PageInfoEduCourse pageInfo new PageInfo(courseList);使用分页插件有一个很重要的注意事项PageHelper.startPage()只对接下来执行的第一条 SQL 查询生效所以它和查询语句之间不能有任何间隔操作。如果在startPage()和查询之间还有其他 SQL 操作分页会失效而且可能把别的查询结果错误分页。这个坑我踩过一次之后每次都把分页紧贴在 Mapper 调用之前。3.3 跨域配置与前端联调细节前端开发服务器默认跑在http://localhost:8080后端接口跑在http://localhost:8081端口不同必然产生跨域问题。解决跨域有几种方式这里推荐后端全局配置 CORS而不是用代理的方式。代理的方式在开发环境确实好用但一旦部署到生产环境前端和后端往往不在同一个域名下代理就会失效。CORS 配置是标准的 HTTP 层面解决方案前后端分离项目生产、开发都适用。后端配置方式Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里特别提醒一个点allowCredentials(true)时allowedOriginPatterns不能使用通配符*这是浏览器规范导致的限制。SpringBoot 2.4 之前的版本用allowedOrigins(*)之后的版本要用allowedOriginPatterns(*)否则配置会报错。3.4 文件上传与视频资源处理视频上传是在线教育系统的核心功能。我的实现思路是后端提供通用的文件上传接口接收 MultipartFile 类型的文件参数将其存储到配置好的上传目录并返回文件的访问 URL。这里涉及两个关键配置一个是文件的物理存放路径另一个是静态资源映射。开发环境下文件可以存到项目根目录下的/upload文件夹但在生产环境建议存到独立的文件服务器目录配合 Nginx 做静态资源映射这样 Tomcat 重启或重新部署时视频文件不会丢失。上传接口收到请求后先判断文件大小和类型是否符合要求。视频格式限定 mp4、m3u8 等常见格式大小限制根据服务器磁盘和带宽灵活设置。然后重新生成不带中文的文件名并保存返回给前端的 URL 同时入库。上传和 URL 映射的代码很简单但一定要做文件类型白名单校验否则会有恶意脚本上传的风险。3.5 视频播放与 m3u8 切片方案前端播放视频是另一个高频需求点。测试阶段大家习惯直接塞一个 mp4 文件用video标签播放实际生产环境这并不合适。大视频文件用video标签加载时浏览器需要先下载全部内容才能播放用户等待时间长且非常消耗带宽。推荐方案是使用 m3u8 切片播放配合 HLS 协议。将完整的视频文件切片成一个个.ts小片段再生成一个.m3u8的索引文件前端用 hls.js 或 Video.js 进行播放。切片过程可以用 FFmpeg 命令行完成命令记录如下ffmpeg -i input.mp4 -profile:v baseline -level 3.0 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls output.m3u8其中-hls_time 10表示每个切片 10 秒-hls_list_size 0表示不限制列表大小。切片后的文件放在服务器的视频目录下前端拿到.m3u8地址即可播放。这个方案的优势在于视频边下载边播放拖动进度条时只加载需要的片段带宽成本低很多。前端播放组件用 vue-video-player 或原生 video 加 html5 hls 插件都行。实测下来用 Video.js 搭配 videojs-contrib-hls 插件最稳定兼容性和交互体验相对均衡。3.6 前端路由与权限控制实现前端部分的核心架构是路由 菜单 状态管理。路由用 Vue Router 的懒加载方式配置页面组件用import()动态引入这样可以做按需加载首屏加载速度会明显提升。懒加载的使用方式如下const CourseList () import(../views/course/CourseList.vue)用户登录成功后将用户信息和角色存储在 Vuex 中同时写入 localStorage 以便刷新后恢复。路由守卫在每次跳转前检查是否存在 Tokenrouter.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })菜单根据角色动态渲染。管理员看到用户管理和系统配置菜单教师看到课程管理菜单学生看到课程中心和我的学习菜单。实现方式是在登录后根据角色拉取对应的菜单数据并动态生成侧边栏或者在前端用条件判断直接控制v-if显示。前者扩展性更好缺点是逻辑复杂后者简单直接适合课程设计和毕设场景。3.7 订单流程与课程权限发放在线教育系统的核心商业闭环是用户购买课程 - 支付成功 - 获得课程观看权限。虽然很多毕设项目不做真实支付对接但订单流程要真实完整。我的实现方式是用户确认购买后后端创建一条待支付订单订单状态为PENDING。支付功能我使用模拟支付接口——直接弹出支付成功确认框后端在接收到确认后把订单状态更新为PAID同时在user_course表中插入一条用户课程关联记录。后续用户查看课程详情时后端通过查询user_course表判断当前用户是否有权观看。这里要强调一点判断是否有权播放课程视频必须走后端接口不能依赖前端判断。前端只是体验层真正的内容保护在后端接口上通过用户 ID 和课程 ID 的关联校验来完成。最后一个非常容易被忽略的细节订单表要加上数据库唯一索引user_id和course_id的组合否则用户重复点击购买时会生成多条订单。4. 常见问题与排查技巧实录4.1 前端 404 和刷新空白问题页面刷新后加载不到路由或出现 404这是前端路由模式导致的经典问题。Vue Router 默认是hash模式地址栏会有#符号部署到服务器时刷新没有问题。如果把路由改成history模式地址栏看着清爽但服务器上刷新会直接 404因为真实的文件路径不存在Nginx 必须配try_files重写规则location / { try_files $uri $uri/ /index.html; }这个规则的作用是找不到真实文件时回退到index.html入口文件让前端路由接管页面渲染。另外需要提醒的是如果你的前端部署在子目录下后端接口地址和静态资源路径都要用相对路径或者配置publicPath来解决。4.2 前后端联调时接口 404 和参数接收不到接口 404 的问题最常见的原因是两个一是后端 Controller 类没有加RestController注解二是服务的 context-path 或者前端请求路径拼错了。建议在后端接口加一个自定义的BaseController异常处理发现异常直接返回统一格式的 JSON不要返回 Whitelabel Error Page 或者堆栈信息既影响前端处理也容易暴露内部结构。参数接收不到最典型的原因是前端用form-data格式提交而后端用RequestBody接收两边没对上。后端RequestBody对应的是 JSON 格式的 POST 请求直接对应application/json。前端axios.post(url, data)时 data 会被序列化成 JSON但如果自己设置了headers[Content-Type] application/x-www-form-urlencoded后端就必须改成RequestParam来接收。这是新手最容易踩的坑。4.3 MyBatis 查询结果为空但不报错MyBatis 查询返回空列表但 SQL 没问题这种问题首先是检查表名和字段名是否映射正确。如果你启用了驼峰映射create_time映射到createTime没问题但表名不一致是编译期无法发现的。建议在开发阶段把 MyBatis SQL 日志打开控制台直接打印查询语句复制到数据库工具里执行一次能过滤掉大部分问题。另一个隐藏较深的问题是 MySQL 的ONLY_FULL_GROUP_BY模式。如果你的分组查询里select字段没有全部包含在 group by 中MySQL 5.7 以上版本会直接报错。解决思路是修改 SQL 写法用ANY_VALUE()函数包裹字段或者在 JDBC 连接参数中关闭该模式。4.4 前端跨域配置生效的确认方法很多同学配置完 CORS 后依然报跨域原因通常有两个一是后端接口本身报错浏览器拦截了报错信息而不是 CORS 问题二是拦截器先于 CORS 配置执行导致被拦截的错误响应中没有 CORS 头。第一个问题好排查直接打开浏览器开发者工具的 Network 面板看请求状态码如果接口返回 500 就说明后端逻辑有问题先修复后端再说。第二个问题稍微复杂一点如果你的项目中配置了 Spring Security 或者自定义 FilterCORS 必须注册到这些过滤器之前否则被过滤器拦截的请求不会进入 CORS 处理链。建议在自定义拦截器中直接对所有 OPTIONS 请求放行因为浏览器跨域预检请求必须通过。4.5 常见错误速查与解决对照表现象可能原因解决方案前端请求接口 404路由错误或 context-path 不一致检查后端配置的server.servlet.context-path与前端请求 URL启动报端口被占用端口被其他服务占用改端口或关闭占用进程上传文件失败SpringBoot 上传文件大小限制配置spring.servlet.multipart.max-file-size和max-request-size前端刷新 404history 路由模式缺少配置Nginx 增加try_files规则SQL 分组报错MySQL 严格模式修改 SQL 或调整sql_modeToken 过期提示频繁Token 有效期设置过短设置合理的过期时间前端增加刷新处理逻辑Vue 页面空白无报错路由懒加载组件引入路径错误检查组件路径和后缀Vue 单文件组件需要.vue后缀4.6 前后端联调前的自测清单每完成一个模块在提交给前端联调之前先用 Postman 或 ApiPost 做一轮自测效率会高很多。自测清单包括验证登录接口返回 Token 结构是否正常用不同角色账号调用接口看权限是否生效分页接口检查返回结构的total、pageNum、pageSize、list字段是否完整上传接口返回的 URL 能否直接访问订单流程的完整链路是否闭环。很多人习惯边写后端边让前端联调结果两边互等、互相猜问题层出不穷。实际项目中正确的节奏是后端先定义好接口文档路径、请求参数、响应格式前端拿到文档先做 Mock 数据联调后端完成后再切换真实接口。花半天时间把接口规范定下来能省掉后续一周的扯皮时间。5. 个人实操体会与项目扩展建议这个项目做完一遍之后我的整体感受是它非常适合作为学习前后端分离开发的练手项目。它不只是一个 CURD 堆积的管理系统而是包含了鉴权、权限、文件存储、订单状态机、流媒体播放等多个真实业务场景每个点都能衔接上企业级开发的实际需求。有个建议想单独强调这个项目做完之后不要急着写进简历就草草收场。把它部署到服务器上用 Nginx 托管前端后端用 Maven 打包成 jar 包跑起来配一下 HTTPS 域名整个过程走一遍你对“上线部署”这个环节的理解会提升一个档次。面试时这块能讲的东西非常多服务器环境配置、前后端分离部署、静态资源缓存、日志收集都是加分项。数据库方面我还可以提一个扩展方向当前用户购买的是单门课程后续如果要上会员订阅制就要引入新的会员表或套餐表。此时可以重新设计订单模型加一个order_type字段区分单课购买和会员订阅。类似的扩展思路在做系统演进时会反复出现架构设计一开始就要想清楚哪些字段未来一定会扩展预留好扩展点。最后再分享一个体验如果你在做这个项目的选修模块时有选择恐惧症优先做数据统计模块用 ECharts 可视化图表展示课程销量、用户增长趋势。数据处理不复杂但视觉效果非常突出答辩或面试展示时很容易抓住注意力。整个系统从环境搭建到功能上线一个人完整做完大约需要一到两周。核心是不要只照着别人的源码抄每一层代码都要理解它为什么这么设计。把设计逻辑内化成自己的思路后面遇到任何管理类项目都能做到游刃有余。