每年一到毕业设计选题季,"SpringBoot+Vue 网络课程管理系统"这类题目都会毫无悬念地冲上热搜。我前前后后带过不少这个方向的课题,也见过太多答辩现场翻车的案例:有的同学把系统做成了纯CRUD,评委一句"这和仓库管理系统有什么区别"就把人问愣了;有的同学辛辛苦苦录了视频,结果播放器压根跑不起来,页面一片黑;还有的部署环节崩在MinIO端口上,演示时图都加载不出来。这个题目看起来简单,实际上是把用户权限、课程流转、视频存储、学习行为记录、前后端联调、容器化部署全揉在了一起,任何一个环节出问题,整个项目都会被拖垮。
这篇文章我就把这个题目从头到尾拆一遍。内容包括:这类系统的真实业务边界在哪里、SpringBoot和Vue各自该承担什么、数据库表怎么设计才不返工、视频点播和文件存储怎么接入才稳、前端动态路由和播放页怎么实现、最后部署和答辩有哪些保命细节。不管你是准备拿它做毕设,还是想快速搭一套在线课程平台的雏形,这篇文章都能直接当作选型参考和开发手册来用。
1. 这类选题经久不衰的真实原因:先搞清楚系统要解决什么问题
很多人在选题时只是觉得"在线教育热门、SpringBoot熟悉、Vue学过一点"就选了它,但完全没想明白系统到底在解决什么问题。这直接导致后面开发时东一榔头西一棒子,做着做着把自己绕晕。所以第一步,先把需求盘清楚。
1.1 一个在线教学运营平台的完整业务闭环
网络课程管理系统不是"一个课程表 + 一堆视频链接"这么简单。从运营视角看,它至少要跑通这样一个闭环:
- 管理员维护课程分类、审核教师上传的课程、管理首页轮播和公告;
- 教师创建课程、维护章节和视频、发布作业、查看选课学生列表;
- 学生浏览课程、加入学习(报名或购买)、观看视频、记录学习进度、参与讨论和作业提交。
这个闭环再往下拆,就变成一张清晰的功能清单。我在实际做项目规划时,通常会把它分成七个功能域:
| 功能域 | 核心功能点 | 后端核心任务 |
|---|---|---|
| 用户体系 | 注册、登录、角色切换、个人中心 | JWT鉴权、RBAC权限控制 |
| 课程管理 | 分类、课程信息、章节、视频关联 | 课程状态机流转(草稿→待审→已上架→下架) |
| 教学运营 | 选课报名、课程评论、公告轮播 | 报名关系维护、热门课程统计 |
| 学习过程 | 视频播放、断点续播、学习时长记录 | 学习记录表增量更新、进度计算 |
| 文件存储 | 视频上传、封面图片、文档附件 | MinIO对象存储、URL签名 |
| 统计分析(可选加分项) | 选课人数趋势、课程热度榜、学习完成率 | 定时任务聚合统计数据 |
| 系统管理 | 菜单管理、角色管理、数据字典 | 动态菜单权限接口 |
这里要注意一个取舍问题:作业、考试、题库这些功能,如果你是准备冲刺高分或者导师明确要求,可以做;如果只是想稳稳落地,建议放在"二开扩展"里,先把主链路打磨好。很多同学一上来就想做"全覆盖大平台",结果光数据库就建了四十多张表,最后连登录都调不通,这是最常见的翻车原因。
1.2 从需求反推边界:哪些功能决定系统的"成色"
这个题目最容易被低估的是三个功能点,它们恰恰是区分"普通管理系统"和"在线教学运营平台"的分水岭。
第一个是课程上下架流转。课程不是创建完就直接能看的,必须有管理员审核环节,涉及草稿、待审核、审核通过、已上架、被驳回、已下架这些状态。这一条状态机是课程系统的灵魂,没有它系统就是一张静态信息表。
第二个是学习进度记录。学生把视频看到一半退出,再进来要能接着播。这要求前端按一定频率上报播放进度,后端维护一条"用户-课程-章节-视频-进度"维度的最新记录。这也是评委最容易追问的点:你系统里"学习完成率"是怎么算出来的?如果答不上来,基本就是重大扣分项。
第三个是视频存储与播放。视频文件不能打进项目里,也不能上传到服务器本地随便仍,一般要接MinIO这类对象存储,然后用HLS切片(m3u8)的方式让浏览器流畅播放,还要考虑防盗链和URL签名。这一块做得怎么样,直接决定演示时会不会当场翻车。
把这三点想清楚、做透,系统就已经超过80%的同类毕设了。剩下的就是工程化细节:参数校验、异常处理、日志埋点、HTTP状态码规范、Vue路由守卫和接口统一封装,这些属于"面子里子"各占一半的活儿。
2. 技术选型与项目骨架:SpringBoot+Vue这套组合的平衡点
选型这块最容易踩的坑不是"不知道该选什么",而是"听网上说新的好,结果把自己坑了"。
2.1 版本选型的三个坑:SpringBoot、JDK、Vue
先说后端框架版本。现在网上铺天盖地都是SpringBoot 3.x的教程,但毕设项目我强烈建议用SpringBoot 2.7.x。原因很朴素:很多学校机房、实验室电脑装的还是JDK 8,SpringBoot 3.x强制要求JDK 17,你写的时候在自己电脑上跑得飞起,到答辩演示换成教室机器或者老师电脑,一跑就报"UnsupportedClassVersionError",这是真实发生过的惨案。SpringBoot 2.7 + JDK 8这套组合,兼容性最稳,资料最多,网上任何问题都能搜到答案。
另一个是前端框架版本。建议直接用Vue 3 + Vite + Element Plus。市场趋势摆在那里,Vue 2已经停止维护了,现在新开项目还选Vue 2等于给自己埋雷。Vite比Webpack快得多,更重要的是开发体验好——改完代码秒级热更新,做页面调试的时候效率差好几倍。
版本选型总结成一句话:后端求稳,前端求新。SpringBoot 2.7.x + JDK 8 + Vue 3 + Element Plus + MyBatis-Plus + MySQL 5.7或8.0,这套组合已经经受过大量毕设项目检验,不会出幺蛾子。
2.2 后端项目结构:从目录就能看出你"懂行"
项目结构体现的是一个人的工程素养,也是答辩时老师快速判断你是不是网上随便抄一个单模块Demo的标准。在线课程系统这种规模,无论是单模块还是多模块Maven工程都是合理的,但内部目录一定要按业务分包,而不是按技术类型分包。
我习惯的分包方式是:
com.example.course ├── config // 配置类:跨域、MyBatis-Plus分页、MinIO客户端、WebMvc拦截器 ├── controller // 控制层:按业务域拆,如AuthController、CourseController、VideoController ├── service // 业务层:接口 + impl实现 ├── mapper // 数据访问层(MyBatis-Plus的Mapper接口) ├── entity // 实体类(对应数据库表) ├── dto // 前端传入的请求参数对象 ├── vo // 输出给前端的视图对象 ├── common // 全局返回结果、状态码枚举、异常处理、常量 ├── security // JWT拦截器、用户上下文、鉴权注解 └── utils // 工具类:日期、文件、MD5等注意controller里面只放参数接收和返回,业务逻辑全部下沉到service层。很多新手喜欢在Controller里写几百行SQL查询,这在大项目里是灾难。service层不光是"中转站",还要承担事务控制——比如学生报名课程时,要同时写入报名关系表和累加课程的选课人数,这两个操作必须共用一个@Transactional。这些细节在你写代码的时候做好,后面写论文时"系统设计"这一章几乎可以直接套用,浑然天成。
另外专门提一个很多人忽略的点:统一返回结构。后面Vue前端要根据状态码做统一的请求拦截,接口返回格式必须一致,不能有的接口返回{code:200, data:...},有的直接返回一个数组。我在项目里用的返回格式是:
{ "code": 200, "message": "操作成功", "data": {} }错误状态码用枚举管理,比如400参数错误、401未登录、403无权限、404资源不存在、500服务器异常。前端拿到非200的状态码时弹出统一提示。这套规范越早定好,前后端联调时越省心。
2.3 鉴权方案:JWT + Sa-Token还是Shiro?
在线课程系统最麻烦的一个点是:同一个用户可能是学生,也可能是老师。所以登录鉴权不能只判断"有没有登录",还要判断"以什么角色在操作"。
常见的三种方案是Spring Security、Sa-Token和手写JWT拦截器。毕设这个规模,我强烈推荐Sa-Token,原因有三个:
- 它对JWT、登录、权限注解、踢人下线、记住我等功能都做了开箱即用的封装,引入依赖后配置几十行就能用,比Spring Security那套复杂的过滤器链友好得多;
- 它内置了
@SaCheckRole("teacher")这种注解,直接在Controller方法上标一下就能做角色校验,代码可读性很高,答辩时也容易讲; - 它支持token自动续期和同端互斥登录,能体现你对安全的思考。
用Sa-Token做角色权限的基本姿势是这样的:
// 登录成功后会返回token,前端存起来 StpUtil.login(userId); String token = StpUtil.getTokenValue(); // 需要教师权限才能调用的接口 @SaCheckRole("teacher") @PostMapping("/course") public Result<Void> createCourse(@RequestBody CourseForm form) { ... } // 不需要登录也能访问的接口(比如首页课程列表) @SaIgnore @GetMapping("/course/published") public Result<PageResult<CourseVO>> publishedCourseList() { ... }这套方案的落地性很强,而且它比手写JWT拦截器更能体现"工程规范性"。手写JWT不是不行,但你要自己处理token过期、重复校验、权限匹配这些问题,调试起来非常耗时,容易陷进去出不来。
3. 数据库与后端核心模块:课程、用户、学习进度三者的关系设计
数据库设计是整个系统最能看出"下过功夫"的部分。这一节我直接给出经过反复实践验证的核心表设计逻辑,以及几个关键接口的实现思路。
3.1 核心表的拆分与字段设计
在线课程系统最少需要这几类表,我按业务域列出来:
用户域:tb_user、tb_role、tb_user_role
- tb_user:id、username、password(BCrypt加密存储)、nickname、avatar、phone、email、status(启用/禁用)、create_time
- tb_user_role:user_id、role_id,因为一个人可能同时是"学生+老师",所以不能只给user表加一个role字段了事
课程域:tb_course_category、tb_course、tb_course_chapter、tb_course_video
- tb_course:id、teacher_id、category_id、title、cover_url、intro、price、status(草稿0/待审1/上架2/驳回3/下架4)、student_count、create_time、publish_time
- tb_course_chapter:id、course_id、title、sort(章节排序)
- tb_course_video:id、chapter_id、title、video_url、duration(秒)、video_size、sort
学习域:tb_course_student(选课报名关系)、tb_learn_record(学习记录)
- tb_course_student:id、course_id、user_id、join_time,唯一索引(course_id, user_id)
- tb_learn_record:id、user_id、course_id、chapter_id、video_id、progress(0-100的整数或小数保持)、last_watch_time、update_time
运营域:tb_course_comment、tb_notice、tb_carousel(首页轮播)
这里有两个容易踩的坑:
一个是学习记录表要不要存明细历史。如果为了统计"每天学习时长",需要一张明细表,每次上报进度都插一行;如果只是做"续播"功能,只需要保存最新一条进度。我建议先做最新记录这张表保住主线,把每日学习时长的统计做成定时任务,从明细表聚合。千万别一上来两张明细表一起搞,数据量虽然不大但逻辑会绕,调试起来是真痛苦。
另一个是课程价格字段。如果系统有免费课程也有付费课程,price字段建议用整数分值存储,比如19.9元存为1990,前端展示时再除以100。一分钱误差引发的脏数据,排查起来非常恼火。
3.2 三种角色的权限设计:一张用户表如何优雅落地
权限这块用"用户-角色-菜单"三层结构来做最清晰。数据库里维护角色表和用户角色关联表,菜单维护在前端动态路由里。
关键点是后端接口必须做角色校验,不能只靠前端隐藏按钮。因为前端隐藏是没有安全性的,懂行的人打开开发者工具就能直接调接口。我每个需要权限的Controller方法上都加了Sa-Token的角色注解,比如:
- 课程新增、修改、视频上传:
@SaCheckRole("teacher") - 课程审核上架、用户禁用:
@SaCheckRole("admin") - 获取我的课程、上报学习进度:
@SaCheckRole("student")
前端路由守卫配合后端的401/403状态码,形成双重保障。答辩时如果老师问"如果学生绕过前端直接调用教师接口怎么办",你就说"后端有角色校验,会被Sa-Token拦截并返回403",这一句话就能明显加分。
3.3 课程审核上架流程:状态机必须由后端掌控
课程状态流转这个功能,新手最容易做成"在数据库里直接改status字段"。表面上能用,但埋下了严重的一致性隐患:谁改的?什么时候改的?为什么改的?都没有记录。
更好的做法是设计一个独立的审核表,把每次审核动作留痕:
tb_course_audit - id - course_id - audit_user_id - action // 提交审核、审核通过、驳回、下架 - reason // 驳回原因 - create_time学生端永远只能查询status = 2(已上架)的课程;教师端能看到自己课程的全状态列表;管理员看到待审核课程时,选择通过或驳回并填写理由。实现起来其实不复杂,就是"修改状态+插入一条审核记录"两个操作放在同一个事务里,但带来的效果是系统逻辑严密了一大截。
3.4 断点续播和学习时长统计:接口怎么做才合理
学习进度上报接口是前端播放器和后端配合的关键。前端播放器需要在timeupdate事件里节流上报进度,比如每隔5秒上报一次,带三个参数:videoId、progress(当前播放秒数)、duration(视频总长)。后端处理逻辑:
- 查询该用户对该视频的学习记录是否存在;
- 存在则更新进度字段,不存在则创建;
- 进度超过95%的,视为视频学习完成,打上完成标记。
这里注意一个细节:判断"完成"不要用精确到100%,因为视频最后一秒由于网络或播放器原因很可能永远播不到,95%是更合理的阈值。我见过一个项目,完成率老是统计不出来,最后发现就是卡在进度必须等于1.0这个死条件上。
学习时长统计可以做成一个定时任务,每天凌晨扫描昨天的学习记录明细,计算出每个用户每个课程的学习时长,写入一张汇总表。前端"学习中心"页面直接查汇总表,不用实时去聚合,性能好且统计口径稳定。
4. 视频点播是分水岭:MinIO存储、m3u8切片与前端播放器
这个模块是全网课程管理系统翻车的重灾区,我单独拿出一整节说,因为它足够复杂,也足够能体现你的工程能力。
4.1 为什么直接传MP4不行
很多第一次做这个系统的同学会想:课程视频不就是上传一个MP4文件,然后前端用<video src>标签播放吗?
在只有一两个视频、只有你一个人看的Demo环境里确实可以。但稍微往真实场景想一下,问题就全冒出来了:
- MP4文件的
moov原子通常在文件末尾,浏览器播放时必须先下载到文件尾才能开始渲染,一个200MB的视频用户可能要先等很久才能看到第一帧(虽然也有渐进式播放优化,但体验和可靠性很难控); - 用户拖动进度条时,播放器需要向服务器发起Range请求,大文件拖拽响应非常慢;
- 视频没有加密保护,拿到链接就能随便下载;
- 文件全塞在一台服务器上,流量一大就崩。
所以行业主流的方案是文件存MinIO,视频处理成HLS流(m3u8+ts切片),前端用HLS播放器播放。这在国内视频SaaS和大部分在线教育平台里是标配方案。
4.2 MinIO接入SpringBoot的核心要点
MinIO是一个开源的轻量级对象存储,S3协议兼容,本地就能跑,特别适合毕设和中小型项目。接入SpringBoot时,先把依赖加好:
<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.5.7</version> </dependency>然后在application.yml里配置连接信息:
minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket: course-bucket接着写一个MinioService,把常见的上传、下载、删除、生成签名URL都封装好。核心方法长这样:
public String uploadFile(MultipartFile file, String objectName) { try { minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); // 返回可访问的完整URL(一般在Nginx里配个转发,把 /minio/ 指到MinIO) return "http://localhost:9000/" + bucketName + "/" + objectName; } catch (Exception e) { throw new BusinessException("文件上传失败:" + e.getMessage()); } }这里有个我踩过的大坑:MinIO默认端口是9000,很多同学本机的后端端口也是8080,前端代理也是8080,三者很容易混。建议后端端口用8081,MinIO的API端口用9000,控制台端口用9001,前端Vite开发服务器用5173,一口一个端口,联调时映射关系清清楚楚。
另一个坑是存储桶内文件覆盖问题。课程视频的文件名必须唯一,我建议用course/{courseId}/{chapterId}/{videoId}/{timestamp}.mp4这种带业务含义的路径,配合时间戳。千万不要用用户上传的原文件名,因为中文字符和空格在URL里会被编码,播放器解析时各种意外情况。
4.3 m3u8切片:视频处理怎么做才不阻塞主流程
视频不处理直接放MinIO,播放器能用,但体验差。要做HLS转换,技术栈是FFmpeg。在服务器上装一份FFmpeg,然后在视频上传后异步执行切片命令:
ffmpeg -i input.mp4 -codec copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls output.m3u8-hls_time 10表示每10秒切成一个ts分片,-codec copy表示不重新编码视频流,速度很快。如果源视频编码格式比较古老(比如H.264之外的),就需要先转一次码,命令会更长。
关键点是这个转码过程不能放在请求接口的同步流程里,因为一个大视频转码可能要几十秒甚至几分钟,前端早就超时了。我会做一个异步的消息队列任务:上传原始视频到MinIO后,接口立即返回"上传成功,转码中",同时向SpringBoot的事件监听器或者简单的线程池提交转码任务,后端转码完成后更新视频表里的状态和m3u8地址。前端播放页根据视频状态轮询或手动刷新,状态变成"已转码"后再渲染播放器。
如果你不想引入复杂的消息中间件,Spring的@Async配合线程池就足够撑起毕设级别的转码任务了。等做完初版,再考虑上ActiveMQ之类的消息队列,属于锦上添花。
4.4 前端播放器:怎么把m3u8放出来
前端播放器的选型我推荐这套:video.js+videojs-contrib-hls,或者直接用hls.js配原生的<video>标签。因为Vue 3项目里vue-video-player这个老库的更新情况不太稳定,很多同学装完发现报错一大堆,折腾几天都跑不起来,性价比太低。
用hls.js最干净的做法是:
import Hls from 'hls.js' function playVideo(videoEl: HTMLVideoElement, src: string) { if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(src) hls.attachMedia(videoEl) } else if (videoEl.canPlayType('application/vnd.apple.mpegurl')) { // Safari等原生支持HLS的浏览器 videoEl.src = src } }这样就不用管播放器插件的怪问题了,直接在封装的VideoPlayer.vue组件里调用。写完这个播放器组件,整个系统最硬核的部分就落地了。
视频播放时还要注意URL签名时效的问题。MinIO默认的永久URL是公开的,别人拿到就能随便下载,所以正式项目里要改用getPresignedObjectUrl生成临时签名URL,比如有效期2小时。前端播放器和下载链接都走这个签名URL,过期后重新向后端请求。这个细节很能体现你懂业务安全。
5. Vue前端的关键实现:从登录到学习页的完整链路
前端工作量其实比后端大得多,因为要写登录、首页、课程详情、播放页、个人中心、教师后台、管理后台,这套下来至少20个页面起步。我挑三个最关键的实现展开说。
5.1 动态路由与菜单权限:不同角色看到的侧边栏不一样
学生进来看到的是"首页、课程中心、我的学习";教师看到的是"课程管理、我的学生、数据统计";管理员看到的是"用户管理、课程审核、平台数据"。这不能用三个独立路由表硬写,因为角色可能叠加,而且后续加角色会非常难受。
标准做法是:后端登录接口返回当前用户的角色列表和可访问菜单列表,前端登录后调用router.addRoute动态注册路由。
项目里我把路由拆成静态路由和动态路由两部分。静态路由只包含登录页、404页、首页,动态路由就是需要权限的路由。路由定义里加meta信息:
{ path: '/course/manage', component: () => import('@/views/teacher/CourseManage.vue'), meta: { roles: ['teacher'], title: '课程管理' } }然后在路由守卫里做判断:
router.beforeEach((to, from, next) => { const userStore = useUserStore() if (!userStore.token) { next('/login') } else if (!userStore.menusLoaded) { userStore.fetchMenus().then(menus => { // 遍历菜单并 addRoute menus.forEach(menu => router.addRoute(menu)) next({ ...to, replace: true }) }) } else { next() } })这里有个隐藏坑:动态添加路由后,如果下次刷新页面,动态路由会全部丢失(因为它们是浏览器内存里的),所以必须在刷新后、路由守卫第一次跳转前重新请求菜单并addRoute。逻辑写对后,刷新页面也不会白屏或者跳到404。
5.2 视频学习页:UI要克制,功能要闭环
学习页是整个前端交互最重的页面。布局方面我推荐左侧章节目录、中间视频播放器、下方评论区,右侧显示课程信息和学习进度。核心交互就一回事:点章节,换视频,记进度。
实现思路:
- 进入页面时请求课程详情接口,拿到课程的基本信息和章节列表;
- 同时请求学习记录接口,拿到该用户在每个视频上的进度,按videoId映射成一个字典;
- 播放器加载时,根据当前视频的进度设置
currentTime,实现断点续播; - 监听
timeupdate事件,节流调用后端上报进度接口; - 切换章节时,记录当前播放位置再切到下一个视频。
这个流程看起来不难,但有一个很实际的问题:切换章节时如果直接把播放器src换掉,播放器会黑屏一下然后重新加载,体验一般。我会在组件里用key绑定的方式强制重新挂载整个播放器,每次切换都重新初始化,杜绝各种状态残留问题。代码大概长这样:
<VideoPlayer :key="currentVideo.id" :src="signedVideoUrl" :start-time="currentProgress" @progress="handleProgressReport" />这个"用key强制重挂载"的思路,在很多场景里都能省掉一堆头疼的问题,不只是播放器,比如表单重置、图表刷新、图片验证码切换,都能用。
5.3 教师端课程管理:表单校验和文件上传的细节
教师端最核心的页面是"课程编辑页",基本要包含:课程基本信息表单(分类选择、封面上传、简介富文本)、章节管理(增删改排序)、视频管理(每个章节下面挂视频)。
视频上传这个交互一定要做好上传进度条和失败重试。前端用axios的onUploadProgress拿上传进度百分比,接口超时时间拉长到10分钟。上传成功后的返回结果要回填到表单里,比如把视频地址存进表单的videoUrl字段。因为这一步是学生看课的前提,交互不稳,演示时必炸。
还有分类选择,不要用<select>硬编码一个下拉列表,要让后端提供分类接口,前端动态加载。这样管理员后台新增分类后,前端不用改代码就能同步。这种"数据驱动配置"的思想在答辩时也是一个加分点。
5.4 前后端联调:跨域与接口规范
Vue开发服务器默认在5173端口,后端在8081端口,跨域是绕不开的。开发阶段用Vite代理最省事,在vite.config.ts里配置:
server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } }这样前端代码里请求路径全写/api/...,就不用在后端配繁琐的CORS了。但注意,线上部署时代理就失效了,所以生产环境要么用Nginx反向代理,要么后端统一配好CORS。一般情况下,我会在后端SecurityConfig里把跨域配置写好,前端线上环境直接请求后端域名,省去Nginx配代理的复杂度。
接口规范方面,和前面讲的统一返回结构配合,前端封装一个request工具函数,在拦截器里统一处理token附带和错误提示。token存localStorage,每次请求头带上Authorization: Bearer {token},后端Sa-Token自动识别。
6. 部署与答辩:本地跑通不算完,能演示才算数
很多人的项目在本地开发环境跑得飞起,一到答辩现场就四处冒烟。提前把部署链路打通,能救你于水火。
6.1 Docker Compose编排依赖服务
后端是SpringBoot,前端是Vue构建后的静态资源,再加上MySQL、MinIO、Redis(如果用了的话),手工一个个启动太容易乱。Docker Compose可以直接编排一套环境,我通常的编排方案是这样:
services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: course_db ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql minio: image: minio/minio command: server /data --console-address ":9001" ports: - "9000:9000" - "9001:9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin volumes: - ./minio-data:/data backend: build: ./backend # Dockerfile,里面打SpringBoot的jar包 depends_on: - mysql - minio ports: - "8081:8081" frontend: build: ./frontend # Dockerfile,先npm build再用nginx托管 depends_on: - backend ports: - "80:80"这套编排在本地和答辩教室都能一键启动,而且MySQL和MinIO的数据目录都挂载到了宿主机,换机器部署时把数据目录拷走即可,演示时即使断网,本地数据都完好。
6.2 环境配置和接口地址管理的细节
SpringBoot里我建议用多个配置文件管理不同环境:
application.yml // 公共配置 application-dev.yml // 本地开发:localhost application-prod.yml // 服务器部署:正式域名和IP启动时用--spring.profiles.active=prod指定环境。这样本地跑和部署是两个配置,不会出现"本地好好的,服务器接口全是localhost"这种低级错误。
前端环境变量我用Vite的模式机制,建立两个环境文件:
.env.development // VITE_API_BASE_URL=/api 走代理 .env.production // VITE_API_BASE_URL=http://your-server-ip/api 直连后端构建时Vite自动读取对应文件。部署到服务器后,前端Nginx里再把/api反向代理到后端8081端口。这一套走下来,前后端跨域问题彻底告别。
6.3 答辩演示时的几条保命细节
最后说几个我见过的惨痛教训,你们绕开走就行:
- Demo数据一定要准备好。别用空库演示,至少放5门课程、每门课2-3个视频(短视频1-2分钟即可)、若干用户、几条评论和公告,演示时点击每一步都能看到数据变化,观感完全不同。
- 视频要提前转码好。如果演示时现场等着FFmpeg转码,体验很灾难。做演示视频的流程是:先上传到MinIO,转码完成后再去答辩现场。现场直接播放m3u8,秒开才是正常状态。
- 网络断了也能演示。视频切片和封面图都在本地MinIO里,接口也在本地,答辩现场如果没网,只要电脑能访问localhost,整个系统完整可用。这是Docker Compose本地化部署的一个隐藏优势。
- 演示脚本要过一遍。从登录开始,演示学生看课、教师发布课程、管理员审核,每个步骤2分钟,总共10分钟以内。过程中鼠标别乱点,手别抖,因为视频学习进度上报是定时调用的,手指一动可能就调了接口。
说到最后,这个题目之所以每年都火,是因为它离真实产品很近:有用户体系、有内容生产、有学习消费、有内容运营,是一套完整的业务小闭环。把它做完做透,你收获的不只是一篇论文和一个能演示的系统,而是真正理解了一个"平台型产品"从数据库设计到前后端联调再到部署上线的全过程。如果你能在这个基础上再加上一两个亮点功能,比如学习行为分析、课程推荐、授课评价等,整个项目的完成度和答辩表现会再上一个档次。