又到了一年一度计算机毕业设计选题的季节。最近不少学弟学妹私信问我要题,我第一个推荐的就是这个:基于SpringBoot的校园文化交流短视频平台。从名字看,它解决了两个问题:一是帮学生在SpringBoot这条主流技术栈里找到一个能落地、有完整业务闭环的毕业设计方向;二是把短视频这种高频互动场景和高校校园文化的展示结合起来。平台核心做的事情非常清楚:学生注册登录后,可以上传校园风光、社团活动、课堂记录、才艺展示等短视频,支持按院系分区浏览、点赞、评论、收藏、关注,管理员在后台完成内容审核和数据统计。这篇文章就是我完整开发这一套系统的复盘,包含选题逻辑、技术栈选型、数据库设计、上传转码细节、互动功能实现,以及我在这个过程中踩过的SpringBoot相关的坑,适合正在准备计算机毕业设计、想快速积累全栈项目经验的Java学习者。
1. 选题与需求确认:为什么是校园短视频而不是又一个“管理系统”
1.1 每年毕业设计的海量题目,为什么我选了短视频方向
计算机毕业设计有个很普遍的现象:大多数人都在做“XX管理系统”——图书管理系统、宿舍管理系统、教务管理系统、实验室管理系统。这类题目不是不能做,而是每年都有一批学生做,评委老师看答辩看到麻木,想拿高分很难。真正的问题在于,管理系统往往是纯CRUD,没有复杂的业务逻辑、没有文件处理、没有音视频链路、没有社区互动,你很难在论文里写出有深度的“难点与创新点”。
我当时换了个思路:能不能找到一个足够贴近真实互联网产品的场景?短视频社区就是其中一个。校园文化本身是一个内容丰富的领域:开学季、社团招新、体育比赛、校园美食、期末复习、毕业季,这些都是天然的视频素材。把“校园文化分享”和“视频互动”绑定,业务上说得通,技术上也有足够的复杂度。课程设计或者毕设里,老师问“为什么做这个题目”,你可以回答:校园文化内容长期分散在公众号、朋友圈、QQ空间里,缺少一个集中的、可检索、可互动的视频社区,这个平台就是解决这个信息聚合和传播问题。这个理由比“为了完成毕设”扎实得多。
1.2 三类用户与十项核心功能
整个平台我按用户角色拆成三类:游客、注册学生、管理员。
- 游客:可以浏览首页推荐流、按分类查看视频、搜索视频,但不能发布内容,不能点赞评论。这样设计是为了保留一个“内容展示窗口”,同时体现权限控制的必要性。
- 注册学生:可以上传视频、管理自己发布的视频、点赞、评论、收藏、关注其他用户、查看个人主页和消息通知。
- 管理员:负责用户管理(封禁/解封)、视频审核(通过/下架)、分类维护、数据统计。
从功能模块上,我拆出了十个核心功能点:
- 注册登录:邮箱或手机号注册,使用JWT做无状态鉴权。
- 视频发布:前端选择视频文件,填写标题、简介、分区分类,后端执行上传、校验、转码、封面生成。
- 视频推荐流:首页默认按综合热度推送,支持按分类、按时间、按播放量筛选。
- 视频详情与播放:前端播放器支持HLS或MP4格式,展示视频信息、作者信息、相关推荐。
- 点赞/取消点赞:要求同一个用户对同一个视频只能点赞一次。
- 评论与回复:支持一级评论和二级回复,评论按时间倒序。
- 收藏功能:用户可以把视频加入收藏夹,方便后续查看。
- 关注/取关:建立用户之间的关注关系,形成“我关注的人动态”。
- 个人中心:展示我发布的作品、我的收藏、我的关注、我的粉丝。
- 后台管理:内容审核、用户管理、分类管理、数据看板。
1.3 把项目拆成三个里程碑,防止设计失控
我见过太多人做毕业设计,一开始什么都想加,最后代码写了一堆,核心功能反而没跑通。我的做法是分三个里程碑:
- 里程碑一:MVP版本。用户登录注册、视频上传、首页列表、视频播放。这个阶段的目标是先把完整链路打通,跑通“登录→发布→播放”这条主线。
- 里程碑二:社区化版本。加上点赞、评论、收藏、关注、个人主页。这个阶段开始涉及Redis、事务、索引优化,是整个项目技术深度的主要来源。
- 里程碑三:运营化版本。后台管理、内容审核、数据统计、消息通知。这些功能主要用于论文里的“系统功能设计”章节,也是答辩时展示完整性的加分项。
这样拆的好处是,每个阶段都有明确的交付物,不会因为后期反复改需求导致代码越来越乱。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型逻辑:SpringBoot 到底“赢”在哪里
2.1 不比不知道:SpringBoot 和 SSM、Flask 的真实对比
做选型的时候,我把主流方案放在一起列了张表:
| 方案 | 开发效率 | 生态成熟度 | 答辩/面试价值 | 适合场景 |
|---|---|---|---|---|
| SSM(Spring+SpringMVC+MyBatis) | 低,需要手写大量XML配置 | 成熟但过时 | 能体现底层理解,但表述繁琐 | 课程设计 |
| SpringBoot | 高,自动配置省去大半配置 | 极成熟 | 主流,毕业设计和求职都认 | 中小型项目、微服务基础 |
| Flask(Python) | 高 | 一般 | 较为局限,面试时不被重视 | 小工具、算法演示 |
| Node.js Express | 中 | 中 | 偏前端方向,Java岗不占优 | 实时应用 |
最终选SpringBoot,不是因为它是“万能答案”,而是它真正卡在了一个最合理的位置:底层还是Spring的IOC和AOP,答辩时可以往上溯源讲原理;自动配置机制又大幅减少了繁琐的Bean配置,开发效率高;同时生态里有MyBatis-Plus、Redis、JWT、FFmpeg、MinIO这些现成方案,踩坑少,资料多。对计算机毕业设计来说,这几乎是“全都要”的选择。
2.2 版本与环境的“老生常谈”:JDK1.8 还是 JDK17
这个点我必须在开头强调:不要一上来就选SpringBoot 3.x。SpringBoot 3.x底层迁移到了Jakarta EE,默认要求JDK 17+,而很多学校的实验室电脑和部署服务器还是JDK 1.8。一旦版本选错,后面会遇到一堆依赖兼容问题,光解决环境就能耗掉好几天。
我的建议是SpringBoot 2.7.x + JDK 1.8。理由很简单:
- 2.7.x仍然有持续的安全维护,足够稳定。
- 它对JDK 1.8的支持非常完善,业界大量生产项目还在用这个组合。
- MyBatis-Plus、JWT、Redis等主流依赖对2.x的适配最成熟,网上搜“SpringBoot整合XX”时,90%的教程都是2.x的,照着做不会踩到由版本引起的神秘错误。
创建项目我推荐直接用IDEA的Spring Initializr或者访问start.spring.io。依赖选Spring Web、MyBatis Plus、MySQL Driver、Redis、Validation,后面再按需加JWT、MinIO等工具库。
2.3 完整技术栈与每层选型的理由
整个项目我用的技术栈如下:
- 后端:SpringBoot 2.7、Spring MVC、MyBatis-Plus、JWT、Lombok。
- 数据库:MySQL 8,用于所有结构化数据持久化。
- 缓存:Redis,用于点赞去重、计数、会话管理、热点视频缓存、排行榜。
- 存储与视频处理:本地磁盘或MinIO,配合FFmpeg做格式转换和封面截取。
- 前端:Vue 3 + Vite + Element Plus + Axios + Pinia,打包后由Nginx托管或由SpringBoot静态资源映射访问。
- 部署:Docker Desktop + docker-compose,在本地验证容器化部署。
有人可能会问,为什么不用Spring Cloud那一套?因为毕业设计不需要微服务。把一个单体能做清楚的业务硬拆成多个服务,只会增加分布式事务、服务治理的复杂度,反而讲不清楚。单体架构 + 合理的模块分层,对中小型项目来说无论从开发、部署还是答辩角度都是最优解。
2.4 MySQL + Redis:配合比想象中更重要
MySQL负责最终持久化,Redis负责扛住高频读取和计数,这个分工要非常清晰。比如视频的播放量、点赞量这种数据,如果每次都去MySQL里UPDATE,热点视频一来就容易出现锁竞争甚至拖垮数据库。我的做法是:
- 点赞状态用Redis的Set结构保存,key为like:video:{videoId},value为用户ID集合。
- 点赞数、评论数先用Redis的incr/decr或者SCARD聚合获取,由定时任务定期同步回MySQL。
- 首页推荐流按热点视频加Redis缓存,缓存key包含分类和分页参数,失效时间设置为5分钟。
答辩老师经常会问“如何保证缓存和数据库的一致性”,你不需要把情况说得太复杂,只要讲明白“数据先写入Redis,再通过定时任务批量刷回MySQL,MySQL是最终数据源”这个思路就够了。这种方案在毕业设计量级下是一个合理的工程取舍。
3. 把校园文化“建模”进数据库:核心表与接口约定
3.1 九张表的职责划分
数据库是整个系统的地基,表设计得好不好直接决定后面写代码的心情。我总共设计了九张核心表:
| 表名 | 职责 |
|---|---|
| t_user | 用户信息,注册登录、昵称头像、个人签名 |
| t_video | 视频信息,标题、简介、URL、封面、状态、计数 |
| t_category | 分类表,院系、社团、校园活动等 |
| t_comment | 评论表,支持层级回复 |
| t_like | 点赞关系表,记录用户与视频的点赞关系 |
| t_favorite | 收藏关系表 |
| t_follow | 用户关注关系表 |
| t_message | 消息通知表,评论通知、点赞通知、粉丝通知 |
| t_admin | 管理员表 |
为什么点赞要单独建一张表而不是只在视频表里加一个like_count字段?因为点赞是一个“关系”,我们需要知道谁赞过哪个视频,这样才能做“我点过赞的列表”和防重复点赞。计数只是这个关系统计出来的结果,两者不能混在一张表里。很多新手在这里图省事,最后想做“我的点赞列表”时发现数据根本捞不出来。
3.2 视频表字段详解与索引设计
t_video是整张系统的核心表,字段设计我列一下:
- id:主键,自增。
- title:视频标题,长度限制60个字符。
- description:视频简介,限制200个字符。
- cover_url:封面图地址,转码成功后生成。
- video_url:播放地址,转码后的MP4地址。
- category_id:关联t_category。
- user_id:发布者ID,关联t_user。
- duration:视频时长,单位秒。
- status:状态,0待审核,1已发布,2已下架,3审核失败。
- play_count、like_count、comment_count:展示用计数字段。
- create_time、update_time、audit_time:创建/更新/审核时间。
索引设计我踩过一个坑:早期只在单字段上建索引,结果按分类+状态+时间查询时,where条件一多,查询慢得难受。后来设了联合索引:
sql复制idx_category_status_time (category_id, status, create_time)
idx_user_time (user_id, create_time)
这样无论是“首页按分类拉视频”还是“个人中心看我发布的视频”,都能走索引,避免全表扫描。
3.3 实体关系:哪些表是多对多的中间表
从ER关系上看:
- 用户与视频是一对多:一个用户发布多个视频。
- 视频与分类是多对一:一个分类下可以有多个视频。
- 用户与视频的点赞关系是多对多:一个用户可以点赞很多视频,一个视频可以被很多用户点赞,t_like就是中间表。
- 收藏同理,t_favorite是另一个中间表。
- 关注关系是用户表的自关联:t_follow中,follow_user_id表示关注者,be_followed_user_id表示被关注者。需要注意加唯一索引,防止同一对关系重复插入。
画好ER图后,把实体类、Mapper接口、Service层的结构一次性生成出来,后面写业务接口就会非常顺畅。
3.4 统一返回体与API风格
前后端分离项目,接口规范很重要。我定义了一个统一的返回体Result
java复制public class Result<T> {
private Integer code;
private String message;
private T data;
}
code为200表示成功,401表示未登录,403表示无权限,500表示服务器异常。所有Controller方法统一返回Result类型,前端axios响应拦截器里统一处理。
接口风格遵循RESTful约定:
- POST /api/user/register
- POST /api/user/login
- POST /api/video/publish
- GET /api/video/feed?categoryId=1&page=1&pageSize=10
- POST /api/video/{videoId}/like
- DELETE /api/video/{videoId}/like
- POST /api/video/{videoId}/comment
- GET /api/user/{userId}/profile
3.5 “发布视频”接口的完整链路
发布视频这个接口是整个系统后端逻辑最集中、最值得在论文里写清楚的一个接口。整体流程分五步:
- 鉴权:从请求头里解析JWT,提取当前用户ID。
- 参数校验:标题不能为空、分类必须存在、文件大小不能超过限制。
- 文件存储:把视频文件写入本地磁盘或MinIO,拿到临时路径。
- 转码与封面:调用FFmpeg进行格式转换,生成MP4格式和视频封面。
- 入库:把视频信息写入t_video,状态设为待审核。
这五步只有最后一步涉及数据库写入,前面的耗时操作都可能在2到5秒甚至更长。一开始我把转码做成了同步调用,前端发布视频时按钮一直转圈,体验很差。后来我改成异步,发布接口只做“文件保存+信息入表”,转码和封面生成全部丢给后台线程池处理,前端立刻收到“上传成功,审核中”的反馈。这个改动对用户体验的提升非常明显。
4. 视频上传与转码链路:隐藏的工程难点
4.1 为什么老师的浏览器会“黑屏”:转码的必要性
如果只是把视频文件存到服务器上,然后把原始路径返回给前端播放,你会发现一个很尴尬的情况:老师用浏览器打开你上传的MOV或者AVI文件,浏览器播放器直接黑屏或者提示不支持格式。原因很简单,浏览器原生播放器对视频编码格式有严格要求,主流支持的是H.264编码的MP4,而手机或者相机拍出来的视频可能是其他编码格式。
所以这个系统必须有一个转码环节。FFmpeg是目前最主流的音视频处理工具,可以完成格式转换、编码转换、分辨率缩放、封面截取等工作。我在做这个模块之前也觉得视频处理很高深,实际操作下来其实就是几条FFmpeg命令加一点Java调用逻辑,难度没有想象中大,但带来的“专业感”非常高。
4.2 SpringBoot 大文件上传的配置细节
SpringBoot默认的上传文件大小限制只有1MB,短视频动辄几百MB,不配置直接报错。需要在application.yml里设置:
yaml复制spring:
servlet:
multipart:
max-file-size: 500MB
max-request-size: 600MB
这里还要注意一个坑:SpringBoot使用Tomcat作为内嵌容器时,文件上传会写入Tomcat的临时目录。Linux服务器下,这个目录可能位于/tmp,系统重启后会清空,导致上传过程中出问题。我当时的处理是显式指定一个独立的临时目录:
yaml复制server:
tomcat:
basedir: /data/campus-video/tmp
另外在后端接收MultipartFile时,不要直接调用transferTo然后就走人。要判断文件是否为空、文件扩展名是否在白名单里、文件大小是否符合预期。否则恶意用户可以直接上传一个JSP或者PHP文件,虽然我们不用Tomcat跑这类脚本,但这种安全习惯最好从毕设就开始培养。
4.3 用 FFmpeg 转码并生成封面:命令行与Java调用
转码的核心命令,我实测下来最稳的是这一组:
bash复制ffmpeg -i input.mov -c:v libx264 -profile:v high -b:v 2000k -c:a aac -b:a 128k output.mp4
参数说明:
- -c:v libx264:视频编码使用H.264。
- -profile:v high:H.264的High规格,兼容绝大多数设备和浏览器。
- -b:v 2000k:视频码率设为2000kbps,短视频场景下清晰度和体积平衡得比较好。
- -c:a aac:音频编码使用AAC。
- -b:a 128k:音频码率128kbps。
封面截取我一般取视频第5秒的画面:
bash复制ffmpeg -i output.mp4 -ss 00:00:05 -vframes 1 -q:v 2 cover.jpg
在Java里调用FFmpeg,我建议使用ProcessBuilder,而不是Runtime.exec拼接命令字符串:
java复制ProcessBuilder pb = new ProcessBuilder(
"ffmpeg", "-i", sourcePath,
"-c:v", "libx264", "-profile:v", "high", "-b:v", "2000k",
"-c:a", "aac", "-b:a", "128k",
"-y", targetPath
);
pb.redirectErrorStream(true);
Process process = pb.start();
原因很简单:Runtime.exec拼接整个命令行时,如果文件路径包含空格或者特殊字符,极容易解析出错;ProcessBuilder的列表参数天然规避了这个问题。
4.4 分片上传的取舍:校园网不稳定怎么办
手机端拍摄的原始视频体积往往比较大,校园网环境又经常抖动,用一次性上传很容易失败。我后来加了分片上传能力:前端把文件切成每片5MB,逐片上传;后端收到所有分片后,按顺序合并成完整文件。
前端核心逻辑用Axios配合Blob切片:
javascript复制const CHUNK_SIZE = 5 * 1024 * 1024;
let start = 0;
let chunkIndex = 0;
while (start < file.size) {
const chunk = file.slice(start, start + CHUNK_SIZE);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', chunkIndex++);
formData.append('uploadId', uploadId);
await axios.post('/api/upload/chunk', formData);
start += CHUNK_SIZE;
}
await axios.post('/api/upload/merge', { uploadId, fileName: file.name });
后端合并可以使用FileChannel:
java复制try (FileChannel out = FileChannel.open(targetPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
for (int i = 0; i < totalChunks; i++) {
try (FileChannel in = FileChannel.open(Paths.get(chunkDir, uploadId + "_" + i))) {
in.transferTo(0, in.size(), out);
}
}
}
这个机制不一定所有论文都要求,但如果你的项目里写了“支持大文件上传、断点续传”,答辩的含金量明显不一样。
5. 互动与社区:点赞、评论、关注背后的编码细节
5.1 点赞“只加一次”:Redis 集合去重和计数同步
点赞功能看起来简单,真正做的时候很容易出bug。最典型的错误是:用户疯狂点击点赞按钮,数据库里插入多条点赞记录,like_count被加了好几次。解决方式不是“前端禁掉按钮”,而是在后端做幂等。
我的方案是用Redis的Set结构:
java复制public Result<Void> likeVideo(Long videoId) {
Long userId = currentUserId();
String key = "like:video:" + videoId;
Boolean added = redisTemplate.opsForSet().add(key, userId.toString());
if (Boolean.TRUE.equals(added)) {
// 第一次点赞,写入数据库关系表
likeMapper.insert(userId, videoId);
// 点赞数+1也可以直接incr
redisTemplate.opsForValue().increment("like:count:" + videoId);
}
return Result.success();
}
SADD的返回值是新增成功的数量,如果返回0,说明这个用户之前已经点过赞,直接忽略。取消点赞用SREM,如果返回1,说明移除成功,再删关系表记录。
点赞数同步回MySQL,我用了一个定时任务:
java复制@Scheduled(fixedDelay = 300000)
public void syncLikeCount() {
// 遍历视频列表,使用SCARD获取点赞数,更新t_video.like_count
}
这个方案能保证点赞数最终一致,即使Redis宕机,MySQL里也有上一次同步的兜底数据。
5.2 嵌套评论:用自关联设计父级与回复
评论功能我一开始只做了单层评论,后来发现用户之间没法互相回复,体验和需求都说不通。做二级评论时,采用最常见的parent_id自关联方案:
- 字段:id、video_id、user_id、content、parent_id、reply_user_id、create_time。
- parent_id为null或0,表示一级评论;有父评论ID的,表示回复。
- reply_user_id记录“回复对象”,前端展示时用来拼接“@某某”。
查询列表时,最容易犯的错误是在循环里一条一条查二级评论,导致N+1查询。MyBatis-Plus里应该这样处理:先一次性查出当前页所有一级评论ID,再使用IN查询批量查出所有二级评论,然后在Java内存中按parent_id分组组装成树形结构。
删除评论时,如果删除一条一级评论,我需要同时删除它的所有子回复。实现时先查所有子评论ID再批量删除,并让评论总数一次性减去子评论数量。
5.3 关注关系与订阅信息流
关注关系用t_follow表记录。用户关注别人时,我先检查是否已经存在记录,没有就插入。取关则删除记录,并且要带上唯一索引防止重复数据。
“关注动态流”是最直观的订阅流场景。当用户进入“关注”Tab时,后端逻辑:
- 查当前用户关注了哪些人,得到user_id列表。
- 用这些ID + status=1条件去t_video表按create_time倒序查询。
- 如果关注列表为空,返回空推荐即可。
这个逻辑在数据量几千条时性能没有问题。论文里可以写明“基于拉取模式实现简单关注流”,后续如果要支撑更大的量级,再考虑推拉结合模式。这样的表述既展示了技术理解,又不会给自己挖“为什么不支持百万并发”的坑。
5.4 热门榜:时间衰减排序公式与定时重算
首页推荐流不能总是按时间倒序,否则老视频永远不会出现在前排。我设计了一个热度评分公式:
score = (likeCount * 3 + commentCount * 5 + playCount * 1) / pow((hoursSincePublished + 2), 1.5)
其中likeCount、commentCount、playCount分别来自Redis或MySQL的计数,hoursSincePublished是发布距离现在的小时数。这个公式的思路是:互动次数越高,视频越热;但发布时间越久,热度衰减越明显,防止老视频霸榜。
后台用@Scheduled每分钟执行一次重算,把分数写入Redis的ZSet:
java复制redisTemplate.opsForZSet().add("hot:rank", videoId.toString(), score);
首页拉取热门榜时,直接用ZREVRANGE取出前N个视频ID,再回表查询视频详情。这个方案实现简单,但面试或答辩时能讲清楚“为什么用ZSet而不是MySQL排序”,为什么用“衰减因子”,属于亮点环节。
6. SpringBoot 实战排坑记录:事务失效、循环依赖、资源404
6.1 循环依赖:两个Service互相引用直接启动失败
在实现“视频审核”功能时,我需要VideoService里调用UserService更新作者的发布计数,而UserService里又要调用VideoService查询用户作品列表,结果一启动就报错:
code复制The dependencies of some of the beans in the application context form a cycle
SpringBoot 2.6版本开始默认禁止循环依赖。网上有人建议在配置里加spring.main.allow-circular-references=true,我强烈不建议用这个方案,它只是在掩盖设计问题。正确的做法是把公共逻辑抽出去,或者调整依赖方向。
我当时的做法是:把“用户发布计数更新”逻辑抽到UserStatsService,VideoService和UserService都依赖这个新的服务,循环自然解开了。这样代码也更清晰,答辩时能顺着讲“依赖倒置”原则。
6.2 事务失效的五个场景,我踩过其中三个
事务失效是SpringBoot面试的高频题,也是我在开发中实际踩过的坑。我总结出的失效场景主要有五种:
- @Transactional注解写在private方法上。Spring使用代理实现事务,private方法不会被代理,事务完全失效。
- 同类内部调用:同类的一个方法调用另一个带@Transactional的方法,调用的是this对象而非代理对象,事务失效。
- 异常被try-catch吃掉:事务只在异常抛到代理层时触发回滚,如果你在Service内部把异常捕获了,事务不会回滚。
- 数据库引擎是MyISAM:MyISAM不支持事务,需要换成InnoDB。
- 多线程调用:事务方法在子线程中执行,每个线程各拿一个连接,它们的回滚互不相同。
我在实现“发布视频同时更新用户作品数”时踩了内部调用坑。解决方式是把事务放在Controller调用的Service入口方法上,并把需要回滚的两个操作放到同一个类里,让它们经由Spring代理进入,保证同一个事务边界。同时明确rollbackFor = Exception.class,因为默认情况下只有RuntimeException才会触发回滚。
java复制@Transactional(rollbackFor = Exception.class)
public void publishVideo(VideoDTO dto) {
videoMapper.insert(video);
userMapper.increaseVideoCount(dto.getAuthorId());
}
6.3 自动装配原理:先能答上面试,再能做对开发
有一次在答辩模拟时,老师突然问“SpringBoot为什么能自动配置”,我一下子卡住了。后来把原理理清楚,发现并不复杂:
SpringBoot的自动装配核心是@SpringBootApplication里的@EnableAutoConfiguration注解。框架会读取META-INF/spring.factories(2.7及以前)或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(2.7+),遍历所有自动配置类的类名,再通过@ConditionalOnClass、@ConditionalOnMissingBean等条件注解判断当前环境中是否满足了启动条件,如果满足就创建对应的Bean。
举个例子,当你的pom里引入了Redis的starter,自动配置类RedisAutoConfiguration就会生效,读取yml里的spring.redis配置项,创建RedisTemplate、StringRedisTemplate等Bean。这就是“加一个依赖就能用”的秘密。
对开发者的实际意义是:当你自动配置不生效时,第一时间检查类路径是否引入了匹配的依赖、properties里是否写对了配置项、是否自己创建了同名Bean导致@ConditionalOnMissingBean跳过自动配置。
6.4 常用注解与配置速查清单
整个项目开发过程中,我反复用的注解有这些:
- @RestController:返回JSON的Controller。
- @Service:业务层Bean。
- @Mapper:MyBatis Mapper接口。
- @Autowired / @Resource:依赖注入。
- @ConfigurationProperties:绑定配置文件前缀到实体类。
- @Validated:参数校验。
- @Transactional(rollbackFor = Exception.class):声明式事务。
- @Async:异步方法。
- @Scheduled:定时任务。
- @Slf4j:日志输出。
配置方面,我建议用@ConfigurationProperties而不是散落的@Value去读配置,比如上传路径、FFmpeg路径、JWT密钥这些统一放到一个配置类里:
java复制@Data
@Component
@ConfigurationProperties(prefix = "campus.video")
public class VideoProperties {
private String uploadPath;
private String ffmpegPath;
private long maxFileSize;
}
这样在yml里写成campus.video.upload-path=/data/campus-video/upload,代码里直接注入VideoProperties就可以用了。后期部署环境切换,只改yml,不用动任何Java代码。
6.5 上传后的图片404:静态资源映射引发的“灵异事件”
本地测试时上传视频、生成封面一切正常,部署到Linux服务器后,上传的视频能播放,但封面图片访问全部404。排查了很久发现原因在SpringBoot对静态资源的默认映射上。
SpringBoot默认只映射classpath:/static/等目录,外部文件系统里的路径并不会自动暴露到HTTP访问。解决方式是通过实现WebMvcConfigurer的addResourceHandlers,把自定义的上传目录映射成URL路径:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**")
.addResourceLocations("file:" + videoProperties.getUploadPath() + "/");
}
}
这样配置后,浏览器访问/upload/xxx.jpg就能从磁盘目录里加载文件。如果用了Nginx反向代理,还需要在Nginx配置里把/upload/路径的请求也转发到SpringBoot上,否则永远只看到Nginx的404页面。这个坑非常典型,强烈建议在部署阶段提前验证。
7. 部署、演示与答辩:把项目从“能跑”变成“能讲”
7.1 打包与配置分离:不要把数据库密码硬编码
项目开发过程中,数据库、Redis、JWT密钥这些配置都写在本地application.yml里。为了部署到服务器后能灵活切换环境,我用了SpringBoot的Profile机制,把生产环境配置单独放到application-prod.yml,启动时显式指定使用哪个环境。
打包命令:
bash复制mvn clean package -DskipTests
生成的jar包放在target目录。启动时:
bash复制java -jar campus-video.jar --spring.profiles.active=prod
这样本地开发用dev环境,服务器上用prod环境,互不干扰。数据库密码等敏感信息放在环境变量里读取,比如:
yaml复制spring:
datasource:
url: jdbc:mysql://${DB_HOST}:${DB_PORT}/campus_video?useUnicode=true&characterEncoding=utf8
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
启动命令里用--env DB_HOST=xxx来注入。这个习惯虽然只是顺手的事,但在论文“系统部署”章节里是实打实的工程化亮点。
7.2 Docker Desktop 部署 SpringBoot 项目
我用Docker Desktop把后端、MySQL、Redis编排到一起,验证容器化部署流程。项目根目录写一个Dockerfile:
dockerfile复制FROM openjdk:8-jre-alpine
COPY target/campus-video.jar /app/app.jar
WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]
docker-compose.yml里编排服务:
yaml复制services:
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: campus_video
ports:
- "3306:3306"
redis:
image: redis:7
ports:
- "6379:6379"
app:
build: .
depends_on:
- mysql
- redis
ports:
- "8080:8080"
environment:
DB_HOST: mysql
REDIS_HOST: redis
构建和启动:
bash复制docker build -t campus-video-api .
docker compose up -d
这个过程中最容易踩的坑有两个:一是容器内访问数据库和Redis时,host要写服务名而不是localhost;二是MySQL容器首次启动时需要时间初始化,应用启动太快会连不上数据库,正确的做法是在应用代码里配置连接重试,或者在docker-compose里加healthcheck,等数据库健康后再启动应用。
7.3 答辩演示最容易翻车的三个细节
答辩演示环节,我只说三个最容易翻车的地方:
第一,准备一个演示账号。不要现场注册,注册流程包含邮箱验证码,万一验证码服务没配好,整个开场白就垮了。提前注册好一个昵称顺眼的用户,登录进去直接展示。
第二,演示设备上提前布好环境。如果答辩现场网络不好,前端页面和视频资源全都在线访问,大概率加载不出来。我的做法是把前端打包后的dist目录由Nginx托管,视频和图片资源放在本地磁盘,通过资源映射访问,提前导出演示视频备份,现场即使断网也能展示。同时准备一段2分钟的录屏,万一现场环境彻底崩了,直接放录屏兜底。
第三,演示顺序要按“故事线”走,不要跳着点。我推荐的演示路径是:登录→浏览首页推荐流→点击某个视频播放→点赞/评论→进入个人主页→点击“发布视频”上传一个提前准备好的短视频→提示审核中→切到管理员账号审核通过→回到普通用户看到视频已经发布。这条线把核心功能全部串起来,逻辑非常顺畅。
7.4 高频答辩题速答思路
答辩时老师的问题基本集中在以下几个方面,我提前准备了思路:
- SpringBoot自动装配原理:回答自动配置类、条件注解、spring.factories/AutoConfiguration.imports三层逻辑。
- 事务失效场景:列举同类调用、私有方法、try-catch、引擎不支持、多线程五个场景,并说明本项目如何处理。
- 为什么用JWT而不是Session:因为前后端分离,JWT无状态,适合负载均衡,签名验证避免会话共享。
- 缓存与数据库一致性:本项目使用Redis计数+定时任务回写MySQL,最终一致性方案。
- 项目最大难点:我会答视频上传转码和互动数据一致性两个点,分别展开说明设计思路和踩坑过程。
把这些问题的答案提前整理成小纸条背熟,答辩时基本能从容应对。
这套项目做完半年后,我回头看,最深的体会是:不要把毕业设计当成“交作业”,而是当成一次完整的产品开发去对待。从选题、需求拆解、技术选型到代码落地,每一步都值得认真记录,因为这些过程才是答辩和论文里真正有价值的内容。如果你想在这套项目上继续延伸,可以加视频标签和课程关联、更细粒度的用户画像、基于内容的推荐算法甚至AIGC生成校园文创摘要,这些都是很好的扩展方向。最后再分享一个小技巧:写代码之前,花一个晚上把表结构和接口清单列清楚,开发时间至少能缩短一半,这个习惯我到现在还在用。
