最近一直在帮某高校实验室做内部培训平台的选型,翻遍了各种开源课程项目,最后锁定了这套基于SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0的在线课程管理系统。前后折腾了大概两周,从跑通源码到改造上线,踩了不少坑也积累了不少经验。这套系统的价值在于:它不是那种只有CRUD教学demo级别的半成品,而是一个功能相对完整的、带权限模型和前后端分离骨架的真实项目,尤其适合做毕业设计、实习作品,或者作为企业内部培训系统的基础版本工程。
这篇就把我拆解这套源码、替换中间件、最后部署上线的完整过程写出来,包括我为什么不直接用SpringBoot3、Vue3到底比Vue2好在哪、MySQL8.0在表设计里真正用到了什么特性,以及部署阶段必踩的几个坑。不管你是刚学完SSM、想拿真实项目练手的同学,还是准备把这套代码改造成自有业务的开发者,这篇都能帮你少走点弯路。
1. 技术栈选型背后的思考:为什么是SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0
很多人拿到一套源码第一反应是“能不能跑”,我拿到源码第一反应是“这技术栈是不是我熟悉的那套”。在真正开始动手之前,先花点时间把这套组合背后的选型逻辑理清楚,后续改代码心里才有底。
1.1 后端框架:SpringBoot2其实是“稳妥”的代名词
先说SpringBoot2。很多刚接触项目的同学会有疑问:SpringBoot3都出了那么久了,为什么这套系统还在用SpringBoot2.7?其实答案很现实:稳定性和生态兼容性。SpringBoot2.7是SpringBoot2时代的收官版本,大量第三方starter、老版本依赖、网上流传的各种教程代码,几乎都是围绕2.x写的。对于学习型项目或毕业设计来说,SpringBoot2能查到的资料最多、踩坑成本最低。
另外一个核心原因是MyBatis-Plus和SpringBoot2的兼容性最成熟。虽然MyBatis-Plus官方早就适配了SpringBoot3,但实际开发中你会发现很多自定义的拦截器、分页插件、代码生成器在SpringBoot3环境下偶尔会有未知问题。选SpringBoot2,等于把“能跑通”这件事先锁死了。
这套系统的后端分层也比较标准:Controller拿到前端请求,走Service处理业务逻辑,再通过Mapper操作数据库。它没有过度设计,没有把DDD那套复杂的分层搬进来,对于大多数高校课程管理、企业内部培训这种业务规模,这套简单的三层结构反而更好维护、更好读。
1.2 前端框架:Vue3组合式API的优势到底体现在哪
这套系统的前端用的是Vue3,而且是Composition API风格,不是Vue2那种Options API的老写法。如果你之前只会Vue2,拿到这套源码的第一反应可能是“怎么全是setup函数”,但适应之后你会发现确实好用。
最直观的好处是业务逻辑的内聚。拿课程管理页面举例,Vue2里一个数据请求要想完整走完,需要分别维护data里的响应式字段、methods里的请求函数、mounted里的初始化逻辑,三个地方割裂开,一旦页面大了,改一个功能要在文件里反复跳。Vue3组合式API允许你把“课程列表查询”相关的所有逻辑——loading态、分页参数、列表数据、查询方法——统统写在一个独立的块里,甚至抽成一个useCourseList函数,不同的页面还能复用同一套逻辑。这套源码里有两个模块明显受益于此:后台的课程管理页面和学生端选课页面,两者的查询逻辑几乎一样,但因为组合式API的支持,代码抽象得非常收敛。
还有一点,Vue3配合Vite,开发模式的冷启动速度和热更新速度比Vue2的Webpack方案快一个量级。你改一行字,差不多1秒内页面就刷新了。开发体验这东西,一旦用上了,真回不去。
1.3 持久层:MyBatis-Plus为什么比纯MyBatis更适合这类项目
不用原生MyBatis而用MyBatis-Plus,核心就一个字:懒。对于课程管理这种以单表CRUD为主的业务系统,用MyBatis-Plus内置的BaseMapper可以直接省下大把XML映射文件。比如课程分类的增删改查,大部分接口根本不需要SQL,一行mapper.insert(entity)就完事,条件查询用QueryWrapper链式拼条件即可,代码量能减少40%以上。
但要注意,MyBatis-Plus也有它“聪明过头”的地方,这个后面我会专门讲分页插件和逻辑删除的坑。可以先记一个结论:凡是涉及多表关联、复杂统计的SQL,千万不要指望MyBatis-Plus的自动方法,老老实实写XML里的自定义SQL,性能可控、逻辑也清楚。
1.4 MySQL8.0:我们真正用到的新特性
这套系统的建表脚本是按MySQL8.0的标准写的,其中有两个点我特别认可。
第一个是窗口函数的使用。课程管理首页一般要展示分类排行、热门课程列表这类数据,传统写法是先查出全表再在Java内存里排序,笨且慢。MySQL8.0的ROW_NUMBER()、RANK()这类窗口函数,几条SQL就搞定了。比如统计每个分类下报名人数最多的Top3课程,用窗口函数可以直接在一句SQL里完成分组内排序,不用自连接,不用临时表。
第二个是CTE公共表表达式(WITH语句)。当课程分类层级深的时候(比如按方向、阶段、技术栈三级分类),传统写法要么递归查多次,要么用connect by语法。MySQL8.0的WITH RECURSIVE可以一次性把树状结构拉出来。这套系统里分类树加载用了这个特性,SQL直接写在XML文件里,表述非常清晰。
还有一个不是特性的细节:MySQL8.0的JDBC驱动类从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver,并且serverTimezone参数必须显式配置,否则启动直接报时区异常。这个坑我后面在部署部分会再提一次,因为太常见了。
2. 系统功能拆解:课程管理不只是简单的增删改查
很多人一看“课程管理系统”这五个字,下意识觉得就是“课表CRUD”。真把代码打开之后才会发现,一套完整的课程管理系统包含的不仅是课程信息本身,还有用户权限、选课流程、进度追踪、支付状态(哪怕是模拟的)这一大串关联逻辑。我拆源码的时候,习惯先把功能模块画成心智地图,再逐个模块看实现。
2.1 用户侧和管理员侧的双端视图
这套系统在权限设计上不是“所有用户同一个界面”,而是分成了两类角色:管理员和普通学生(或学员),对应的是后台管理端和门户端双端界面。
管理员端的功能集中在课程生命周期管理上,包括课程分类维护、课程基本信息发布(封面、简介、价格、授课老师)、章节与课时内容编排、上下架状态控制、学员订单查询。学生端则是一个真实使用者视角的界面,能够浏览课程列表、按分类筛选、查看课程详情、报名选课、查看已选课程和学习进度。
这个双端设计看起来不复杂,但在实际开发里意义很大:它决定了整套系统的数据模型——任何一张业务表几乎都带status字段、用户都要有role字段、课程与用户之间都要有中间关联表。理解了这个模型,你再看代码就不会觉得“明明很简单为什么建了这么多个Mapper”。
2.2 课程状态与发布流程的管理细节
课程不是一创建就直接展示在学生端的,后台设计了一个状态流转机制,非常贴近真实业务。课程刚创建时处于草稿状态,管理员可以继续编辑章节信息,只有显式点击发布之后,课程才会在校验器通过的前提下进入“已上架”状态,学生端才可见。
这个机制在代码里具体表现为课程表的course_status字段,取值从0到3分别代表草稿、待审核、已上架、已下架。状态流转被封装在Service层里,Controller只负责接收状态变更请求,不会直接改字段状态。
我在改造的时候特地保留了这个设计,因为哪怕是给内部培训用,“草稿——发布——下架”这套流程也能避免误操作导致的不该显示的课程漏到门户端去。如果你把这套二次开发用于真实业务,建议把状态流转这块继续加强,配上操作日志,谁在什么时候下了架,以后随时能追溯。
2.3 选课与订单:表关系里的核心关联
选课逻辑是课程系统和“单纯的内容管理系统”最大的区别所在。它会涉及三张核心表:课程表、用户表、选课订单表(或学习记录表)。学生点击选课按钮之后,系统首先校验该课程是否已上架、是否已超过选课人数上限,然后查这个学生有没有重复选课记录,全部通过后再插入一条订单数据,同时把课程的已选人数selected_count加一。
这套系统在这儿做了一个很聪明的小设计:它没有真接支付,而是把订单状态设成了待支付、已支付、已取消三类,同时给了一个“模拟支付”的入口——管理员可以一键把订单置为已支付,用于测试完整链路。如果你后续要做真实的课程收费,只需要在支付回调里把订单状态改成功、再触发课程解锁逻辑即可,代码侵入面非常小。
我当时测的时候用脚本连续选了1024门课(每门课2个线程并发抢),发现库存扣减用的是简单的先查后改逻辑,并发高的时候会出现超选风险。但这个量级对毕设、培训平台完全够用,真要上生产还得加行锁或Redis预扣。这个点解释清楚了,面试官问并发问题时你也能答得出来。
3. 数据库设计:表结构背后的关键决策复盘
数据库是这套系统的地基。我花了一个晚上把全部建表SQL过了一遍,大概30多张表,每一张都完整带注释。整体设计思路比较务实:没有为了“显得高级”去做过度设计,但几个关键点上的处理非常到位。
3.1 三张核心表:课程表、章节表、选课记录表
先看课程主表(course),字段覆盖了课程名称、封面URL、简介、分类ID、授课讲师、课程价格、总课时、已选人数、状态、创建时间、更新时间等。特别注意它有两个“维护成本较高”的字段:总课时和已选人数。总课时是冗余存储的,每次对该课程的章节增删时,需要同步更新计数。已选人数的更新依赖选课记录表的INSERT和状态变更,也是冗余计数。
这种冗余在课程列表页的价值非常大:学生端打开首页的时候,不需要count联表统计每个课程的选课人数,直接取一个字段即可,查询性能高了一个数量级。但这种设计也要求代码里对计数一致性做兜底,这套系统的处理方式是每天凌晨跑一个定时任务,重新统计每个课程的真实选课人数回填字段。属于典型的缓存式计数表,思路可复制。
章节表(course_chapter)通过course_id与主表关联,每个章节下还有小节表(course_section)存视频地址、文档地址和预计时长。文件地址在代码里用的是相对路径字符串,配合本地文件上传或阿里云OSS的SDK都能适配。学生端播放页直接渲染sections数组生成课程目录树,前端写法非常规整。
3.2 MySQL8.0特有的建表细节
建表脚本里用上了MySQL8.0的几个默认特性。第一就是字符集,全部统一成utf8mb4,排序规则utf8mb4_general_ci,这个是我强烈建议照抄的。很多老的MySQL5.7项目还在用utf8,前端一旦录入emoji表情或生僻字,入库就报错,用utf8mb4就不会有这问题。
第二是时间字段统一用了datetime类型,默认值CURRENT_TIMESTAMP,更新时间字段还加了ON UPDATE CURRENT_TIMESTAMP。这比在Java代码里手动给updateTime逐个赋值省心得多,也保证数据库层面的数据不会因为遗漏赋值而出现空值。
第三是索引设计。拆索引的时候我发现课程表里有三个核心索引:idx_category_id(分类查询)、idx_status(状态过滤)、联合索引idx_category_status_id。联合索引的设计逻辑是让最常用的查询——“某个分类下已上架的课程列表”——可以走索引覆盖,避免回表。你自己设计表的时候,优先想想最频繁的WHERE条件组合,再决定要不要建联合索引。
这里有一个看着不起眼、实际上很关键的表:管理员操作日志表(sys_operation_log),记录管理员对课程的每次变更操作,包括操作人、操作类型、操作描述、IP、时间。这个表可能不是业务核心,但在真实场景里是“自救神器”。有一次测试环境里课程被误删除,我就是靠回看操作日志定位到是哪个时间点误触的接口,才把数据还原了。强烈建议任何项目保留类似表,哪怕只记录关键业务动作。
3.3 软删除和逻辑删除的取舍心得
这套系统的删除操作大部分用了逻辑删除,也就是给表加一个deleted字段,默认0表示未删除,删除时UPDATE成1,查询时MyBatis-Plus自动追加WHERE deleted = 0的条件。这么做的好处是课程误删后还能找回,而且选课记录和学生进度不会因为课程删除而产生外键级联的不可控制风险。
MyBatis-Plus实现逻辑删除很简单,在实体字段上标@TableLogic注解即可,全局逻辑删除也在配置文件里配置了。但这个机制有过一个坑:如果自己写XML自定义SQL时忘了加deleted条件,逻辑删除就失效了,删除掉的数据照样能被查出来。我在这套系统里排查一个分类树显示异常的问题时就遇到过——分类树查询SQL是手写的递归CTE,里面就忘了过滤deleted = 0,导致逻辑删除的分类一直挂在树上不掉。这是我拿到源码后修的第一个bug,也是所有用了MyBatis-Plus逻辑删除的项目最容易漏掉的一点。
4. 前后端分离下的核心交互细节
前端Vue3跑在5173端口,后端SpringBoot跑在8080端口,两个服务独立部署,所有数据交换都通过HTTP API完成。前后端分离项目最常遇到的就是跨域、鉴权、统一返回格式这三个问题,这套系统全都给出了比较成熟的解法。
4.1 统一返回体与请求流程设计
后端所有Controller的返回值几乎都不是裸返回实体类,而是走一个统一响应包装类——常见叫法是Result或R,内部包含code(状态码)、message(提示信息)、data(业务数据)三个字段。前端针对这个格式封装了一个全局的request.js模块,所有请求发出后统一拦截响应。
前端拿到code之后分三种情况处理:2xx开头的视为成功,直接取出data交给业务代码;401是未登录或Token过期,自动清空本地用户信息并跳转登录页;其他非2xx的code则弹出错误提示,在页面上直观展示。这样一来,每个页面的请求代码只关心成功场景,异常统一兜底,代码整洁度提高很多。
我见过不少前后端分离项目,Controller直接return实体类,前端判断也东一块西一块,日志一打没法看。这套系统的做法其实是最值得学的“基本功”——先把响应协议定死,再讨论业务。
4.2 登录鉴权:JWT无状态方案与私密路由
系统用的是目前主流的前后端分离鉴权方案:JWT(JSON Web Token)。用户登录成功后,后端生成一个Token字符串返回给前端,前端存在浏览器的localStorage里,每次请求Header带上Authorization: Bearer <token>。后端通过拦截器(或AOP切面)统一解析Token,解析失败就直接拒绝请求。
JWT方案的核心优势是无状态。后端服务重启之后,所有已登录用户的Token依然有效,不需要像Session方案那样担心服务端会话丢失;横向扩展时也简单,任意实例都能解析同一套加密算法生成的Token,天然支持多实例部署。
这套系统里还有个细节:登录接口返回完Token后,紧接着返回了用户基础信息和角色标识。前端拿到角色标识后再判断渲染哪些路由。前端路由配置里为管理员端加了一层守卫,如果没有Token、或者角色不是管理员,直接重定向到登录页。这个“路由守卫”动作看起来简单,实际是前端的最后一道拦截——绕过它只能看到空的组件外壳,任何数据都拉不到。
4.3 分页查询与条件搜索的前后端配合
课程列表页用的是典型的分页加条件筛选交互:前端传current(当前页码)、size(每页条数)、keyword(搜索关键词)、categoryId(分类ID)四个关键参数。后端用MyBatis-Plus的分页插件处理,返回一个IPage对象,里面封装的是记录列表、总条数、总页数、当前页数。
重点提一下分页插件配置。MyBatis-Plus的分页功能需要显式注入一个PaginationInnerInterceptor,不注入的话,调用selectPage方法返回的数据依然是全表数据。我最初拿到源码跑起来发现首页没分页效果,排查了半天才发现配置文件里这段被注释掉了,注释解除后立刻正常。这不是源码的问题,而是环境配置的坑——建议任何跑不起来、分页不生效的问题,第一反应都去查配置类有没有被Spring扫到、有没有在启动时正常注入。
前端的分页组件我看了下用的是el-pagination,切换页码时重新发起请求,并把新的current和size同步到URL的query参数上。这样刷新页面时页码不会丢失,只是一个很小的体验细节,但很多项目会忽略它,刷新后直接回到第一页,很不专业。这个小功能在后端没有任何对应实现,纯粹是前端把分页状态挂到了路由上,成本极低,值得照搬。
5. 从源码到本地跑通:部署实战与踩坑记录
拿到源码之后最头疼的就是环境搭建。我按照这篇源码附带的文档手把手操作过一遍,整体流程顺畅,但中间有四个坑让我印象特别深刻,单独拿出来说说,都是那种你在教程里看不到、但自己独立做十有八九会遇到的事情。
5.1 标准部署流程速览
环境准备阶段需要的软件如下(版本号很有讲究):
| 软件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | SpringBoot2.7的最后绝配版本,不要用JDK17 |
| Maven | 3.6+ | 配置阿里云镜像加速依赖下载 |
| Node.js | 16+ | Vite5要求Node16及以上,低了会报错 |
| MySQL | 8.0+ | 必须8.0,5.7在窗口函数语法上过不了 |
| Redis(可选) | 5.0+ | 用于缓存,不配也能跑,但建议配 |
后端启动流程三步走:第一步用Navicat或命令行执行项目里带的course_admin.sql建表脚本,初始化整个数据库;第二步在application.yml修改数据库地址、账号、密码,同时确认serverTimezone=Asia/Shanghai这个参数没被删;第三步在项目根目录执行mvn spring-boot:run,看到Tomcat started on port(s): 8080就是成功启动。
前端启动流程更简单:npm install(第一次会很慢,建议配淘宝镜像)然后npm run dev,Vite默认在5173端口起开发服务。浏览器输入http://localhost:5173就能看到门户首页,默认管理端账号约定俗成是admin/admin123,文档里有写。
5.2 坑一:时区参数缺失导致的数据库连接异常
这是新手最容易卡死的问题。MySQL8.0和低版本最大的感知区别之一,就是连接字符串必须携带时区参数。如果你在application.yml里只写jdbc:mysql://localhost:3306/course_db不放任何参数,启动日志里会出现一个特别长的错误,核心信息是The server time zone value ... is unrecognized or represents more than one time zone。
解决方案就是在连接URL末尾加上?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8,顺便把编码问题一起解决。这个解决方案几乎每个用MySQL8.0的SpringBoot2项目都适用,属于写进简历都不为过的典型排错经验。
5.3 坑二:前端接口地址指向错误导致白屏
前后端分离项目第一次跑通时,最典型的症状不是报错,而是“白屏但没任何提示”——页面加载出来了,但列表数据全是空的。这种情况八个都是前端的API地址没配对。这套源码里前端环境配置文件有个.env.development,里面需要指定后端接口的基础地址,默认写的是http://localhost:8080/api。
如果你后端端口改了,或者后端接口带了项目访问前缀(比如/course-admin/),前端这边没同步,就会出现“接口404但页面不白屏”的诡异情况。排查手法很土但有效:打开浏览器控制台,看Network面板里请求的实际URL,跟后端Controller的@RequestMapping路径对一下,任何一个地方对不上都会失败。这个排查思路适用于一切前后端分离项目,比瞎猜强一百倍。
5.4 坑三:第一次启动时初始化数据缺失
如果你拿到的这份源码是在简化环境里跑过的,可能会缺少一部分初始化数据,尤其是“系统管理员”这个账号和基础菜单。具体表现是:能启动,但访问后台管理页时调用的菜单接口返回空数组,页面上一片空白。
解决办法是去sys_user表手动INSERT一条管理员账号,密码字段需要是BCrypt加密后的密文。这里有一个特别反直觉的细节:如果你用明文密码去登录,Spring Security的BCryptPasswordEncoder永远比对失败,你根本不知道是自己密码错了还是系统坏了。正确的做法是找一个在线BCrypt工具,把123456明文生成密文,再替换进去,或者在源码里建一个临时的启动Runner把明文加密后打印到控制台再手动去库里改。
生成之后重启后端服务,用刚设置的管理员账号登录,一切都通了。这个小坑我在新环境部署时至少踩过两次,建议你第一次就把它手动完成,省得后续排查半天。
5.5 坑四:跨域报错——CORS配置的正确姿势
前后端分离最烦的一件事就是跨域。前端页面在5173端口,去请求8080端口的接口,浏览器默认会拦截这种“跨源”请求,现象是控制台报Access-Control-Allow-Origin相关的红字错误,但后端日志里几乎看不到任何异常。
这套系统的后端用一个全局配置类实现了CORS跨域支持,所有来源、所有路径的请求都放行,allowCredentials也叫true。这里有个安全上的注意事项:开发环境放行所有来源没问题,但部署到生产环境时推荐改成白名单域名,比如https://your-course-domain.com。否则任何一个网站都可以往你的后台接口发请求,配合JWT窃取Cookie(如果有)就能搞出很多安全问题。
最重要的是:不要在后端每个Controller都自己写@CrossOrigin注解,太乱了,统一放在一个配置类里,好维护不易错。
6. 文档、源码与二次开发:这套系统的真实使用价值
最后说说文档和二次开发这块。常看开源项目的人都有这种经验:源码不是最贵的,文档才是。这套系统附带了一份比较完整的开发文档,从环境搭建到API说明再到表结构解释都有,在这个类别里算相当良心的。
6.1 拿到源码后的第一个动作:跑一个端到端用例
我建议任何拿到这套源码的人,先别急着改业务。第一步按照文档把前后端跑通,然后手动完整体验一遍核心链路:管理员登录→新建课程分类→新建课程→添加章节→发布课程;换学生账号→登录→浏览课程→选课→查看已选课程;再回退管理员端→查看选课订单。这个过程能让你在半小时内快速搭建这套系统的完整认知,后面做二次开发才知道动哪里。
6.2 三个最值得关注的扩展方向
第一个扩展方向是课程资源存储。目前这套系统的章节资源是本地路径字符串,如果你有线上部署需求,建议把资源上传改为OSS/MinIO兼容接口,代码改动只涉及上传Service层和返回URL的逻辑,核心表结构不需要变。
第二个扩展方向是引入Redis做缓存。现在的课程列表每次请求都直接打MySQL,虽然表数据量小,跑热数据扛得住,但如果你想学习缓存穿透、缓存击穿这些典型问题怎么处理,完全可以把课程列表和分类树放一份到Redis里。预约、选课并发控制也可顺势用Redis做防重,这属于业务规模的“正经升级”。
第三个扩展方向是增加一个讲师或教师角色。目前的角色模型只有管理员和学生两级,如果希望更贴近真实学校场景,可以在用户表加一个teacher_id关联,让讲师只对自己创建的课程有管理权限。这个改动虽然涉及权限判断的部分较多,但是Spring Security配置里的hasRole已经预留了按角色鉴权的接口写法,照着官方文档往上加角色就行。
6.3 这套源码适合什么人、不适合什么人
我的判断是:这套系统最适合的人群是两类。一类是刚掌握SSH或SSM基础、需要一个“真实完整项目”来学习前后端分离技术和怎么组织代码的人;另一类是毕业设计、实训作品需要快速出一套带文档、带完整功能的项目的学生。它的价值在于“完整度”——从代码到文档到SQL再到前端界面都是齐的,你可以在上面学到很多书本上没有的真东西。
不太适合的,是完全没接触过JavaWeb的纯小白。这套系统涉及Vue3组合式API、Spring Security、JWT、MyBatis-Plus高级特性,如果连HTTP请求、Servlet生命周期这些基础都不清楚,遇到报错会非常崩溃。建议先跑过一个小小的前后端项目,再来碰它。
我自己的改造经验是给这套系统加了一个“讲师管理自己课程”的子模块,顺带把文件上传从本地改成了MinIO。整个过程三周左右,每天大概投入三小时,最大的收获不是功能本身,而是通过拆解这套系统,把前端Vue3和SpringBoot2两边的代码组织方式真正吃透了。如果你也想拿这套源码练手,我特别推荐先按我前面说的端到端用例走一遍,再动手改第一行业务代码。整个过程里踩的那些坑,最后都会变成你简历上真实的项目经验和面试里头头是道的谈资。