做课设或者接私活的时候,最怕的不是业务逻辑复杂,而是被一堆重复的增删改查拖死。这套在线课程管理系统源码,技术栈直接锁定在 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,正好是这两年 Java Web 前后端分离项目里最主流的一套组合,网上类似的架子也好找,但很多都是阉割版,功能少得可怜,代码风格也乱。这套源码我完整拆过一遍,后台管理、教师端、学生端三套角色权限齐全,课程、章节、作业、考试、选课、公告这些核心模块都能跑通,而且还带了配套文档,拿来当毕设、课设,或者直接作为自己学习前后端分离项目的参考,都很靠谱。
先说结论:这个项目单看技术难度不算很高,但它胜在“完整”和“规范”。从数据库设计到后端接口分层,从前端页面交互到权限拦截,整套链路是通的,不是东拼西凑的 Demo。对于正在学 SpringBoot 和 Vue3 的同学来说,这个项目最大的价值不是“跑起来”,而是“看懂别人怎么写”,看一个成熟的小型系统是怎么把各个零部件组织起来的。这篇文章我就从技术选型、系统架构、核心模块实现、踩坑记录这几个维度,把整个源码的底细翻出来讲透。
1. 项目整体设计与技术选型分析
1.1 为什么偏偏是这套技术栈
做课程管理系统这类业务系统,最忌讳的就是过度设计。项目本身就是典型的 CRUD 密集型应用,核心业务就是“用户-课程-学习记录”的关联管理,再加上一些文件上传和统计报表功能。SpringBoot2 负责把后端服务的骨架搭好,自动配置帮我们省掉大量 XML 配置,内嵌 Tomcat 让打包部署变得极其简单。选 SpringBoot2 而不是 SpringBoot3,是因为大量现成的教程、开源组件还停留在 2.x 生态,尤其是 MyBatis-Plus 对 SpringBoot3 的兼容在早期版本有坑,2.7.x 这条线成熟稳定,踩坑成本最低。
Vue3 这块,项目用的是组合式 API + Vite 构建。Vue3 的 Composition API 相比 Vue2 的 Options API,逻辑复用更灵活,整个项目的前端代码结构也更清晰,把不同功能的响应式状态和函数按业务维度组织,而不是按 data、methods 这种技术维度硬拆。配合 Element Plus 组件库,后台管理界面的开发效率非常高。说白了,Vue3 + Element Plus 就是目前国内中小型管理系统前端的标配组合,没有之一。
MySQL8.0 的选择没什么悬念。8.0 在性能和功能上对比 5.7 有明显的提升,窗口函数、公用表表达式(CTE)这些高级特性,在写成绩排名、统计报表这类需求时非常有用。加上 utf8mb4 字符集已经是默认配置,存 emoji 表情和生僻字都不会乱码。MyBatis-Plus 则是把 MyBatis 的灵活性保留下来,同时把单表 CRUD 的重复劳动降到最低,内置分页插件、代码生成器、条件构造器,开发效率直接拉满。
1.2 课程管理系统到底要解决什么问题
在线课程管理系统的核心,是解决“教学资源线上化流转”的问题。传统的线下教学模式里,课程安排、作业收发、考试组织、成绩登记都是线下人工操作,效率低且容易出错。这个系统要做的就是把整个教学过程搬到线上,让管理员统一管理课程资源和用户账号,让教师能够发布课程、布置作业、组织考试、批改成绩,让学生能够浏览课程、选课退课、在线学习、提交作业、参加考试、查询成绩。
从系统设计的角度拆解,这套系统的核心模型就三个角色:管理员、教师、学生。三个角色对应的权限和操作边界完全不同,所以权限管理是整个系统的地基。源码里通过 SpringBoot 拦截器 + 自定义注解的方式实现接口级别的权限控制,没有引入 Spring Security 那种重量级框架,这是一个非常务实的决策。Spring Security 功能虽然强大,但对于这种规模的项目来说,学习成本和配置复杂度反而是负担。
1.3 系统模块划分的合理性
从源码的模块划分来看,这套系统把功能分成了七大块:系统管理、课程管理、选课管理、在线学习、作业管理、考试管理、统计报表。每个模块在高内聚低耦合的原则下独立开发,模块之间通过数据库表的外键关系和接口调用进行交互。这种划分方式在实际开发中非常常见,每个开发人员可以独立负责一个模块,相互之间不会干扰。而且模块划分也直接对应了前端路由和后端 Controller 的命名,代码定位非常方便。比如课程管理模块,后端对应 CourseController,前端对应 course/ 目录下的页面,数据库对应 course_info 表,整个链路是一一对应的,不管是二次开发还是查 bug 都特别舒服。
2. 核心功能模块与数据库设计拆解
2.1 角色权限体系与功能矩阵
很多初学者拿到这种系统源码,第一件事就是去翻登录逻辑。实际上登录只是入口,权限才是灵魂。这套系统采用的是基于 RBAC(基于角色的访问控制)模型。用户表里有一个 role 字段,用数字表示角色类型,比如 1 管理员、2 教师、3 学生。登录成功后,后端根据用户角色生成对应的权限标识,然后通过拦截器统一校验接口访问权限。
实际操作中,源码的做法是自定义了一个 @RequireRole 注解,标注在 Controller 方法上,然后在拦截器里读取当前登录用户的角色信息,与注解里声明的角色进行比对。比如删除课程的接口只允许管理员和教师调用,就在方法上标注 @RequireRole({"admin", "teacher"})。这种方式比在业务代码里写 if-else 判断角色要优雅得多,权限逻辑集中管理,新增接口时只需要加注解即可,不会因为漏掉判断而出现越权操作。
角色权限矩阵如下表所示:
| 功能模块 | 管理员 | 教师 | 学生 |
|---|---|---|---|
| 用户管理 | 全部权限 | 查看个人信息 | 查看个人信息 |
| 课程发布 | 全部操作 | 新增/修改/删除 | 浏览/选课 |
| 作业管理 | 查看统计 | 布置/批改 | 提交/查看成绩 |
| 考试管理 | 配置考试 | 出题/发布/阅卷 | 参加考试 |
| 成绩查询 | 全站统计 | 所授课程成绩 | 个人成绩 |
| 公告管理 | 发布/删除 | 查看 | 查看 |
这个设计看起来简单,但非常实用。实际项目中权限控制最容易出现的问题就是接口越权,比如学生调用管理员的删除接口,如果漏掉校验就会造成数据安全事故。用拦截器统一处理,所有请求进来先过权限校验这一关,业务层就不用反复确认当前用户的身份了。
2.2 数据库表结构设计详解
数据库设计是这个项目里信息量最大的一部分,也是最值得仔细研究的部分。我整理了一下核心表的结构和设计思路,这些表之间的关联关系基本就是整个业务模型的骨架。
用户表 sys_user:字段包括用户 ID、用户名、密码(BCrypt 加密存储)、真实姓名、角色编号、头像地址、手机号、邮箱、账号状态、创建时间、更新时间。账号状态字段很重要,管理员可以把违规或毕业的学生账号禁用,禁用后登录接口直接拒绝。
课程分类表 course_category:自关联的树形结构,通过 parent_id 字段实现父子级分类,比如“编程开发”下面可以挂“Java”、“Python”子分类。为什么不用更复杂的前缀树或嵌套集模型?因为课程分类的数量通常不会很大,层级也控制在两到三级,用最简单的 parent_id 方案就足够了,查询时直接在代码里递归组装树形结构即可。
课程信息表 course_info:这是整个系统的核心表,包含课程标题、封面图片、课程简介、所属分类 ID、授课教师 ID、课程状态(草稿、已发布、已下架)、总学时、选课人数、发布时间。注意课程状态字段,这是审核流程的关键,教师在后台创建的课程默认是草稿状态,管理员审核通过后变为已发布,学生端才能看到课程。
课程章节表 course_chapter 和课程课时表 course_lesson:课程内容采用章节-课时两级结构。一门课下面有多个章节,每个章节下面有多个课时,课时里可以挂视频地址、文档附件、练习题目。视频地址存的是相对路径,文件本身存在服务器的指定目录。这种设计能灵活支持课程内容的组织,也方便后续统计每个课时的完成情况。
选课记录表 stu_course:核心业务表,记录了学生和课程之间的关联关系,字段包括主键 ID、学生 ID、课程 ID、选课时间、学习进度(百分比)。学习进度是学生在学习过程中点击课时时不断更新的,用于在首页展示“继续学习”的功能。学习进度存的是整数百分比,每学完一个课时就用课时总数做一次除法计算,逻辑简单直接。
作业表和考试表:作业表(homework_info)记录作业标题、所属课程、截止时间、满分分值、题目内容。作业提交表(homework_record)记录学生提交的内容、提交时间、批改状态、批改分数、教师评语。考试表(exam_info)类似,但多了考试时长、试卷总分、及格分数,考生提交后系统自动判分,选择题和判断题用代码比对答案,主观题留给教师人工批改。
2.3 数据库设计的几个关键决策
第一,所有业务表的主键都采用雪花 ID 策略,而不是数据库自增主键。原因是课程管理系统未来如果要拆分微服务或者做数据迁移,自增主键在分布式环境下会产生冲突,雪花 ID 是全局唯一的,可以避免这个问题。MyBatis-Plus 里只需要在实体类 ID 字段上添加 @TableId(type = IdType.ASSIGN_ID) 注解,框架会自动生成全局唯一 ID。
第二,金额和分数全部用整数存储。这里可能有人会有疑问,考试分数不是可以有小数吗?用整数的话精度够吗?实际操作中,项目的做法是分数全部按照“分”为单位存储,也就是数据库里存 85 代表 85 分,如果涉及 85.5 分,就存 855。这样避免浮点数精度丢失问题,展示的时候再除以 10 转成小数即可。虽然没有金额那么严格,但这个意识值得学习。
第三,逻辑删除代替物理删除。为了保留历史数据,所有核心表的删除操作都是逻辑删除,也就是在表中增加一个 deleted 字段,默认值 0,删除时执行 UPDATE 设置 deleted = 1,查询时 MyBatis-Plus 的 @TableLogic 注解会自动在 SQL 后面追加 deleted = 0 条件。这样做的好处是误删数据可以随时恢复,而且保留了完整的数据轨迹。实际开发中这个方案应用非常广泛。
3. 后端核心实现:SpringBoot2 与 MyBatis-Plus 落地细节
3.1 项目分层结构与包组织方式
后端代码的包结构直接体现了项目的分层思想。源码包结构如下:
com.example.course ├── common // 通用类:Result返回体、分页对象、异常处理 ├── config // 配置类:CORS、拦截器注册、MyBatis-Plus分页插件 ├── controller // 接口层:接收请求、参数校验、返回结果 ├── dto // 数据传递对象:接收前端参数,不直接暴露实体 ├── entity // 实体类:与数据库表一一对应 ├── mapper // 数据访问层:MyBatis-Plus的Mapper接口 ├── service // 业务逻辑接口 │ └── impl // 业务逻辑实现 ├── utils // 工具类:JWT工具、文件上传工具 └── vo // 视图对象:输出给前端的数据封装Controller 层非常薄,只做三件事:接收参数、调用 Service、返回 Result。所有的业务逻辑包括事务控制都放在 Service 层。我见过很多初学者喜欢在 Controller 里堆业务代码,这种习惯在项目规模小的时候看不出问题,一旦需求复杂起来,Controller 会变得不可维护,而且没办法复用。这套源码的分层很规范,属于可以直接抄作业的标准结构。
DTO 和 VO 的区分也很值得学习。DTO 是接收前端传来的参数,比如分页查询参数、新增课程的表单数据;VO 是返回给前端的数据结构,比如课程详情页面需要同时展示教师姓名和分类名称,这时候要把关联查询的结果封装成一个 CourseDetailVO,而不是直接把 CourseInfo 实体类序列化给前端。这样做的好处是前端拿到的数据形态刚好就是页面需要的,不会有多余字段,也不会把一些敏感字段(比如密码)泄露出去。
3.2 统一返回体与全局异常处理
前后端分离项目里,接口返回的数据格式必须统一。这套源码定义了一个 Result 类,包含 code(状态码)、message(提示信息)、data(业务数据)三个字段。所有接口返回的都是 Result 类型,前端拿到后首先判断 code 是否为 200,如果是就正常渲染数据,否则弹出错误提示。
全局异常处理是必须做的一个环节,源码里用 @RestControllerAdvice 注解实现了全局异常拦截器。不管是业务异常、参数校验异常还是空指针异常,都会被拦截并转换成统一的 Result 返回。这样做最大的好处是前端永远只处理一种异常形态。比如用户提交作业时没有登录,拦截器会抛出一个未认证异常,全局异常处理器捕获后返回 401 状态码,前端路由器检测到 401 就自动跳转到登录页,整个流程非常顺畅。
这里分享一个我实际开发中的经验:全局异常处理里一定要把异常详情记录到日志中。很多时候线上出现问题,我们看到的只是一个统一返回的“系统繁忙”,完全没法定位问题。合理的做法是:对用户展示友好提示,对后端日志输出完整堆栈,才能快速排查问题。这套源码在日志记录方面做得还可以,但如果你要对线上系统二次开发,建议在全局异常处理器里增加更详细的日志打印。
3.3 JWT 认证与登录状态管理
登录认证用的是 JWT(JSON Web Token)方案。用户提交用户名和密码,后端校验通过后生成一个 Token 返回给前端。这个 Token 由三部分组成:Header(头部)、Payload(载荷)、Signature(签名)。Payload 里面保存了用户 ID、用户名、角色等关键信息,注意不能放密码等敏感数据。Token 的有效期源码设置的是 24 小时,超过时间前端需要重新登录。
前端拿到 Token 后存储在 localStorage 里,每次发请求时在请求头中携带 Authorization 字段。后端通过拦截器从请求头中取出 Token,解析签名和有效期,获取当前登录用户信息并存入 ThreadLocal。这样后续业务代码中只要调用一个工具方法就能拿到当前用户信息,避免在接口参数里频繁传递用户 ID。
关于 Token 过期处理,源码的做法是让前端在请求拦截器里预判 Token 是否快过期。如果剩余有效期不足一定时间,就自动跳转到登录页。虽然这种做法不能完全避免 Token 过期导致的请求失败,但对于这个规模的项目来说已经够用。更高级的方案是引入 Refresh Token 双令牌机制,但会增加系统的复杂度,对于课程管理系统来说属于过度设计。
3.4 MyBatis-Plus 的高效使用姿势
MyBatis-Plus 在这套项目里发挥的作用非常关键。首先是内置的 BaseMapper 接口,提供了一批单表 CRUD 方法,比如插入、按 ID 查询、按 ID 修改、按条件分页查询等等。在业务代码里,大部分单表操作直接调用这些方法就搞定了,完全不用手写 SQL。
条件构造器 LambdaQueryWrapper 是另一个提效利器。比如查询某个教师下的已发布课程,代码可以这样写:
LambdaQueryWrapper<CourseInfo> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(CourseInfo::getTeacherId, teacherId) .eq(CourseInfo::getStatus, 1) .orderByDesc(CourseInfo::getCreateTime); List<CourseInfo> courseList = courseInfoMapper.selectList(wrapper);用 Lambda 表达式指向实体类的 getter 方法,避免了硬编码数据库字段名,即使后端修改了表字段名,代码编译阶段就能发现错误。这是 MyBatis-Plus 相比传统 MyBatis 的显著优势。
多表关联查询用的还是 MyBatis 的 SQL 注解或者 XML 文件。比如课程列表页需要显示教师姓名和分类名称,单靠 LambdaQueryWrapper 是搞不定的,需要写一个多表联查 SQL。MyBatis-Plus 不限制这种原生 SQL,你可以随时随地手写复杂查询,与普通的 MyBatis 使用方式完全一致。
分页插件使用上,源码在 Config 配置类中注册了 MybatisPlusInterceptor,并添加了 PaginationInnerInterceptor(数据库类型为 MySQL)。之后分页查询只需要传入 Page 对象即可。
Page<CourseInfo> page = new Page<>(current, size); LambdaQueryWrapper<CourseInfo> wrapper = new LambdaQueryWrapper<>(); // 拼接条件代码 IPage<CourseInfo> result = courseInfoMapper.selectPage(page, wrapper);返回的 result 里面不仅有当前页的数据列表,还有总记录数、总页数等信息,前端要做分页展示非常方便。整体评价:MyBatis-Plus 在这套系统里把“快”发挥到了极致,同时保留了手写 SQL 的灵活性,这就是它在本项目中被选为核心 ORM 框架的根本原因。
4. 前端实现:Vue3 工程化开发实践
4.1 前端工程结构与技术栈细化
前端部分的工程结构按照功能模块划分,而不是按技术类型划分。这一点非常有利于项目维护。目录设计如下:
src/ ├── api/ // 各模块的接口调用函数 ├── assets/ // 静态资源:图片、全局样式 ├── components/ // 通用组件 ├── layout/ // 布局组件:侧边栏、导航栏、主体区域 ├── router/ // 路由配置 ├── store/ // 全局状态管理(Pinia) ├── utils/ // 工具函数:请求封装、Token管理 └── views/ // 页面组件 ├── admin/ // 管理员专属页面 ├── course/ // 课程相关页面 ├── exam/ // 考试相关页面 ├── home/ // 系统首页 ├── login/ // 登录页面 ├── student/ // 学生专属页面 └── teacher/ // 教师专属页面项目基于 Vite 构建,相比 Webpack 的优点是开发环境下启动速度极快,依赖预构建和原生 ES Module 让热更新体验非常流畅。这里要特别强调一下 Vite 需要 Node.js 版本至少 14.18 以上,建议使用 16+ 版本,否则装依赖的时候会报一堆兼容性错误。
状态管理用的 Pinia(Vuex 的官方替代品),代码明显更简洁。比如用户状态管理,定义一个 store 保存用户信息和登录状态:
import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: {} }), actions: { setToken(token) { this.token = token localStorage.setItem('token', token) }, setUserInfo(info) { this.userInfo = info }, logout() { this.token = '' this.userInfo = {} localStorage.removeItem('token') } } })组件里通过 useUserStore() 获取 store 实例,修改状态时直接调用 action 方法,数据是响应式的,所有引用这个 store 的组件都会自动更新。相比 Vuex 的 mutation、action、getter 那一套, Pinia 上手成本低得多,和 Vue3 的组合式 API 配合起来尤其顺。
4.2 Axios 请求封装与前端路由权限
前端请求统一封装在 utils/request.js 中。基于 Axios 实例做了一层封装,集中处理请求头、响应拦截、错误提示。请求拦截器里自动把 Token 加到请求头中,响应拦截器里统一处理 Http 状态码和业务状态码。如果后端返回的 code 是 401,直接清除本地登录信息并跳转到登录页。
请求封装实际代码如下:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' import { useUserStore } from '@/store/user' const request = axios.create({ baseURL: '/api', timeout: 15000 }) request.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { const userStore = useUserStore() userStore.logout() router.push('/login') } ElMessage.error('服务器异常,请稍后重试') return Promise.reject(error) } ) export default request封装好了之后,每个 API 文件只需要定义具体的接口函数,导出给组件使用。比如课程接口:
import request from '@/utils/request' export function getCourseList(params) { return request({ url: '/course/page', method: 'get', params }) } export function getCourseDetail(id) { return request({ url: `/course/${id}`, method: 'get' }) }路由守卫是前端权限控制的第二道防线。后端拦截器保障了接口级安全,前端路由守卫则负责页面级导航控制。在 router.beforeEach 中判断用户 Token 是否存在,如果没有就强制跳转到登录页;如果用户角色是教师但访问了管理员页面,也会被重定向。前端权限控制的目的不是安全(真正的安全必须靠后端),而是提升用户体验,避免用户点了按钮才发现自己没权限。
4.3 核心业务页面实现要点
登录页面是最简单也最关键的页面,因为它是所有用户进入系统的入口。源码的实现思路是:表单校验通过后调用登录接口,成功后将 Token 和用户信息保存到 Pinia,然后根据用户角色跳转到不同的首页。前端代码注意点:登录按钮要防止重复提交,可以使用 loading 状态配合 disabled 属性;密码输入框要支持回车触发登录事件。
课程管理页面是教师角色的核心页面。列表页用 Element Plus 的 el-table 组件展示课程数据,支持关键词搜索、状态筛选、分页。新增/编辑课程用 el-dialog 弹窗内嵌表单实现,Form 表单里做一些基础校验,比如课程标题不能为空、学时必须大于 0。值得注意的是,课程封面图片上传组件单独封装了一个 UploadImage 组件,利用 Element Plus 的 el-upload 配合后端文件上传接口实现,上传成功后把返回的图片地址写到表单字段中。
学生端的课程学习页面使用的是视频播放 + 章节列表布局。左侧章节列表展示课时树形结构,右侧视频区域嵌入了 video 标签。实现学习进度的计算逻辑是:用户点击每一个课时时,前端向后端发送一条学习记录请求,后端更新学生选课记录中的学习进度字段。当前端再次进入学习页面时,根据进度百分比自动定位到上次学习的课时,这个功能对于用户粘性很有帮助。
统计报表部分使用 ECharts 图表库。教师可以查看自己课程的学生选课趋势图、成绩分布图。前端通过接口获取统计原始数据,用 ECharts 的折线图和柱状图渲染。ECharts 在 Vue3 中可以直接用深度响应式数据绑定图表配置,如果配置变化就调用 setOption 重新渲染。统计页面的开发难点不在图表本身,而在后端 SQL 的聚合查询,需要把学生选课时间按月份分组,然后统计每个月的选课人数。
5. 前后端联调、打包部署与高频报错排查
5.1 开发环境联调与跨域问题
前后端分离开发,联调阶段最常遇到的第一个问题就是跨域。前端开发服务器跑在 5173 端口,后端 SpringBoot 跑在 8080 端口,两者不是同一个源,浏览器出于安全策略会拦截跨域请求。源码中后端已经配置了全局 CORS 跨域过滤器,允许所有来源访问(开发环境下)。实际操作中发现,虽然后端允许了跨域,但请求测试时仍然报跨域错误,这种问题多半是请求方法的预检 OPTIONS 请求没有被正确拦截。
解决办法是把 CorsFilter 放在拦截器链的最前面,确保任何请求先经过跨域处理器。对于 Session 方式校验认证的项目,还需要设置 allowCredentials 为 true,同时 allowedOriginPatterns 不能使用通配符,要明确指定允许的来源地址(通常是 http://localhost:5173)。用 JWT Token 方式就没有这个烦恼,因为 Token 放在请求头里,不依赖 Cookie,跨域策略相对宽松。
还有一个更省事的方式:前端通过 Vite 的 proxy 代理配置解决开发环境跨域。也就是请求发给前端自己的服务器,由 Vite 服务器转发给后端。这种情况下不需要后端配置 CORS,代码写到:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })开发环境用 proxy,生产环境用 Nginx 反向代理,CORS 只在完全前后端分离部署且域名不一致的时候才需要。这个思路值得大家记住。
5.2 项目打包部署的完整流程
这套系统是前后端分离项目,打包部署分成两部分。后端用 Maven 执行mvn clean package -DskipTests命令,打包生成 jar 文件,然后通过java -jar course-system.jar命令启动。注意 jar 包名称和位置可以通过 pom.xml 的 build 配置定制。生产环境建议使用nohup后台运行,并指定 JVM 内存参数,比如-Xms512m -Xmx1024m,避免服务器内存波动时触发 Full GC。
前端用npm run build命令进行构建,产物是一个 dist 目录,包含静态 HTML、JavaScript 和 CSS 文件。部署时把 dist 目录下的所有文件拷贝到 Nginx 的 html 目录中,然后在 Nginx 里配置反向代理把 /api 开头的请求转发给后端的 8080 端口。这里有一个关键配置点:前端路由用 history 模式时,Nginx 需要配置 fallback,让所有路径都重定向到 index.html,否则刷新子页面时会报 404 错误。
server { listen 80; server_name your.domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }文件上传目录也需要注意,课程封面和视频上传后保存在后端服务器的本地磁盘中,生产环境下要配置 Nginx 的静态资源映射,把 /upload/ 路径映射到文件实际存储目录,否则前端页面无法正常加载图片和视频。
5.3 高频报错与排错记录
我在实测这套系统的过程中,整理了几个高频出现的问题,完全可以当避坑手册用:
MyBatis-Plus 分页失效。表现是分页查询返回全部数据,没有按照 Page 参数截取。大概率是没有配置分页拦截器。在 SpringBoot2 + MyBatis-Plus 3.5.x 版本里,光引入依赖不够,必须在配置类中注册 MybatisPlusInterceptor 并添加 PaginationInnerInterceptor。配置之后分页查询才生效。
后端返回 JSON 数据的时间字段格式错误。Java 后端 LocalDateTime 默认序列化格式是 "2024-01-01T12:00:00",前端页面展示起来不友好。解决办法是在配置文件中设置全局的日期格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8这个 time-zone 配置尤其重要,不配的话服务器时间跟北京时间差 8 小时。
MySQL8 连接不上或者时区报错。启动 SpringBoot 时报错信息一般会提示“The server time zone value”之类的内容。原因在于 MySQL8 的连接驱动默认要求明确指定时区。数据库连接 URL 上加上serverTimezone=Asia/Shanghai参数即可解决。另外 MySQL8 驱动类名是 com.mysql.cj.jdbc.Driver,不要再用旧的 com.mysql.jdbc.Driver。
文件上传大小限制。SpringBoot 内嵌 Tomcat 对默认上传大小限制是 1MB,课程视频动辄几十上百 MB,会上传失败。需要在 application.yml 中配置:
spring: servlet: multipart: max-file-size: 1024MB max-request-size: 1024MB如果把视频文件直接存数据库是绝对禁止的,数据库只存路径,文件放磁盘,配合 Nginx 静态映射访问。这套源码的存储方式完全合理。
前端打包后接口地址 404。检查请求是不是发了出去、路径前缀是否正确。生产环境下前端请求的后端地址是同一个域名下的 /api 前缀,通过 Nginx 代理转发到后端服务。如果后端接口本身没有 /api 前缀,Nginx rewrite 规则需要把 /api 去掉再转发,否则会 404。
Vue3 中使用响应式数据丢失问题。最常见的是用 reactive 定义对象后直接解构,导致解构出来的属性失去响应性。解决办法是使用 toRefs 包裹解构。在课程编辑表单里,从后端获取详情数据后赋给 reactive 对象,这个时候直接修改属性是能触发页面更新的,但如果一次性替换整个对象就会断掉响应式连接。推荐写法是:
const form = reactive({ courseTitle: '', courseHours: 0 }) const data = await getCourseDetail(id) Object.assign(form, data)使用 Object.assign 逐个属性赋值,保持响应式连接不断。
6. 二次开发建议与使用心得
这套源码另一个亮点是带了文档。文档内容包括系统概述、运行环境要求、部署步骤、功能模块说明、数据库设计说明。说实话,“含文档”这个点对于课设答辩和团队交接来说帮助很大。很多源码项目代码质量再好,没有文档你也很难快速上手,尤其是数据库初始化脚本和配置注意事项,文档里交代清楚了至少能省半天时间。
二次开发如果想要在这套系统基础上增加新功能,建议重点关注几个扩展点:
课程评价模块。可以在选课学习结束后让学生对课程进行评分和评论,表结构可以直接复用评论表扩展一个课程评分字段。教师端增加平均分展示,给课程质量优化提供数据支持。
消息通知模块。系统目前没有站内消息功能,可以在用户表已有手机号、邮箱的基础上增加一个 message 表,用于存储站内通知,教师发布课程或作业后自动向选课学生发送通知。后端用 Spring Boot 的异步事件机制实现解耦,不需要引进消息队列组件。
课程推荐功能。学生选课后可以统计课程分类的偏好,在首页增加“猜你喜欢”模块。实现逻辑是通过课程分类聚合学生历史选课记录,统计出高频率分类,然后从该分类中随机选取几门课程推荐给用户。数据库层面只需要几条 SQL 加代码就能完成,前端加一个推荐区域展示即可。
在线课堂互动功能。如果想要更完整的教学链路,可以在章节课时节点增加评论区,学生可以对某个课时的内容发表评论和提问。评论表设计可以关联课时 ID,然后前端在学习页面的视频下方展示评论列表,教师可以在后台回复。加上这个功能后,这台系统作为在线教育平台的完成度就更高了。
根据我个人的使用体验,这套源码比较适合有一点 Java 基础、想完整走一遍前后端分离项目开发流程的同学。你可以先按照文档跑起来,然后对照数据库设计看实体类和 Mapper,再到 Controller 看接口怎么暴露,最后到前端看页面怎么调用接口,一条链路走下来,整个项目的开发思路就清晰了。之后再看哪些地方设计得好、哪些地方还能改,围绕自己课设或者毕设的功能需求做裁剪和扩展。做课程设计最忌讳拿着代码就跑不管原理,把这套代码吃透了,写简历上或者面试时聊项目,你也能够从容应对技术提问。
最后分享一个我在测试这套系统时经常用的技巧:准备一份“测试清单”,把三个角色可以执行的操作逐项列出来,然后挨个走一遍,观察接口返回和页面表现,发现不符合预期的就记下来。比如学生能否成功调用教师接口、教师能否查看其他教师的课程、管理员角色能否正常生成统计报表。用这种方式跑过一遍,系统的功能边界和代码逻辑你基本上就心里有数了,远比只看代码要高效得多。