☰
Spring Boot视频创作平台毕设实战:从上传转码到部署
2026/10/1 18:20:31 网站建设 项目流程

视频创作平台这类毕设题目,每年都能在答辩现场见到好几个,但做得像样的不多。很多人把"视频创作平台"理解成一个简单的视频列表加播放器,结果答辩的时候被老师一个问题问到哑火:"你的视频是怎么存储的?上传一个1GB的文件会不会把服务搞挂?转码任务是怎么处理的?"你要是没想过这些问题,这个项目基本就只是玩具级别。

我前阵子刚好帮一个学弟捋过一个完整的 Java 视频创作平台项目,从立项、设计、编码到部署全程跟了一遍。今天把这套东西整理出来,说说怎么用 Java(Spring Boot)把一个视频创作平台从零搭到能答辩、能跑演示、能回答老师追问的程度。这里面的东西不挑你是有 Java 基础的老手还是刚学完 SSM 的新手,照着思路走,至少不会在核心问题上翻车。

1. 视频创作平台的业务拆解与功能规划

很多毕设翻车,翻在业务边界没划清楚。你要做一个视频创作平台,那它到底是一个"爱优腾"的视频播放网站,还是一个"B站/YouTube"的创作者中心?两者的复杂度差了不止一个量级。

从标题看,核心落在"创作管理系统"上,也就是说,重点不是海量视频分发,而是围绕"创作者"的一整套内容管理闭环:注册登录、创作空间、视频上传与转码、封面生成、分类标签、发布管理、数据统计(浏览量、点赞、评论),以及普通用户的浏览、播放、互动。这跟纯视频门户的最大区别,是系统必须有"创作者视角"和"用户视角"两套界面和两套权限。

1.1 用户角色与权限模型

用最朴素的 RBAC(基于角色的访问控制)模型就够了,别上来就整用户-角色-权限三张表加一堆关联,毕设没必要,也容易把前端页面搞复杂。我建议直接两种角色:

  • 普通用户(USER):浏览视频、搜索、点赞、收藏、评论、关注创作者。
  • 创作者(CREATOR):在普通用户能力基础上,额外拥有"创作中心"入口,可以上传视频、管理作品列表、查看每个视频的播放数据和评论列表。
  • 管理员(ADMIN,如果愿意做可以加):用户管理、视频审核、违规内容下架。这个角色我不是必须推荐,很多毕设不加管理员,答辩时老师会问"谁来保证内容安全",加一个很简单的视频状态字段(待审核、已发布、已下架)就能圆回来。

权限控制不用上 Spring Security 的重型过滤器链,用拦截器(HandlerInterceptor)加 JWT Token 解析就够,或者简单点用 Session + 拦截器判断角色。我见过不少毕设用 Shiro,也行,但如果你对配置不熟,答辩的时候被问"Shiro 的 Realm 是干什么的"容易卡壳。用 Spring Boot 拦截器反而更好讲:请求进来 → 拦截器解析 Token → 把用户信息塞到请求上下文 → Controller 参数里直接拿当前用户。

1.2 核心功能清单

模块功能点关键实现
用户模块注册、登录、Token发放JWT / Session,密码加密存 MD5(盐) 或 BCrypt
创作者中心作品管理、上传入口、数据概览角色判断 + 独立路由
视频上传单文件上传、分片上传、断点续传MultipartFile / 分片合并
视频处理转码为 H.264、截取封面、获取时长FFmpeg 命令行调用
视频发布填写标题、简介、分类、封面、可见性表单 + 文件字段合并提交
浏览播放列表页、搜索、分类筛选分页查询 + 播放地址鉴权
互动模块点赞、收藏、评论、浏览量异步计数 + Redis 缓存热点
管理后台用户管理、视频审核/下架简单 CRUD + 状态流转

这个表格可以直接当成你开题报告的功能模块图来用,每一行都是一个能写成一个小节的功能点,工作量可视。

1.3 需求边界划定:哪些必须做,哪些不要碰

我见过很多同学一开始雄心勃勃,想加"视频剪辑""在线直播""AI 审核",结果做到中期发现时间根本不够。答辩老师看的不是你功能列表长得吓人,而是你做完的东西是不是完整闭环。

必须做的核心闭环:注册登录 → 进入创作者中心 → 上传视频 → 系统转码并生成封面 → 填写信息并发布 → 前台展示 → 用户观看并互动 → 创作者看到数据。这个链路从头到尾都通,就已经是一个合格的毕设。

建议放弃的功能:在线剪辑(工程量大)、多人协同(还要做同步协议)、视频推荐算法(做不好就是被老师追问的靶子)、付费订阅(涉及支付 API,麻烦)。这些有没有都不影响你拿高分,反而砍掉它们能让核心逻辑更扎实。

2. 技术选型与系统架构设计

技术选型这件事,一定要做"有理由的选择",而不是"听别人说这个火所以用这个"。答辩老师特别喜欢问"为什么用 Redis""为什么不用 JPA 用 MyBatis"。你回答"项目需要所以用了"会被扣分,回答"因为某某场景有某某特性"才能站住。

2.1 后端框架:Spring Boot 3.x 还是 2.x,MyBatis Plus 还是 JPA

先说版本。如果你 JDK 用的是 8,那老老实实用 Spring Boot 2.7.x;如果你电脑装的是 JDK 17 甚至 JDK 21,可以用 Spring Boot 3.x,但要注意它的一些坑:javax 包名改成了 jakarta,MyBatis Plus 要用 3.5.3+ 专版,很多老教程的写法直接搬过去会编译报错。毕设求稳的话,我推荐 Spring Boot 2.7.x + JDK 8/11 的组合,资料多、报错搜得到答案,怎么都能跑起来。

ORM 我强烈推荐 MyBatis Plus。理由很简单:它内置了 BaseMapper,单表的 CRUD 完全不用手写 XML,你的注意力可以放在业务逻辑上,而不是 UserMapper.xml 里那一条 selectById。而且在答辩的时候,你可以很自然地说"常规接口用 MyBatis Plus 的封装方法,复杂统计查询手动写 SQL",这个组合是业界最常见也是最实际的写法。

JPA 也不是不行,但 JPA 的懒加载、缓存机制、对象关系映射在答辩时容易挖坑,一句"为什么查出来是 LazyInitializationException"就能让很多人当场卡住。MyBatis Plus 的 SQL 逻辑直白,出了问题你知道它在跑什么。

2.2 存储方案:MySQL + Redis + 本地文件系统

视频文件本身不能丢数据库里,这应该是常识。数据库只存视频的元数据(标题、简介、URL、大小、时长、状态、创建人 ID),视频实体文件存服务器磁盘或对象存储(OSS)。

我建议毕设用本地磁盘存储,根目录下建 upload/video 和 upload/cover 两个目录。理由朴素:不花钱、不依赖外网、答辩现场的断网环境也能正常演示。如果担心项目显得"不够工业化",可以在文档里写一句"生产环境可替换为 OSS/MinIO,本系统基于本地存储实现,通过文件服务抽象层屏蔽存储差异",然后留一个 FileStorageService 接口,本地实现类里写文件读写逻辑。这一步在设计文档上是加分项,代码上的工作量又很小。

Redis 在这个项目里不是必选项,但你加了它,答辩的含金量立刻不一样。我建议至少用在两个位置:一是热点视频的播放量计数,避免每次都去 update Java_learning 表的 view_count(明星字段),而是先 INCR Redis 再异步同步到 MySQL;二是视频分类列表的缓存,分类这种变更频率极低的数据,缓存到 Redis 里能省掉大量重复查询。

2.3 模块工程结构与包组织

工程结构不要搞那种大而全的多模块 Maven 项目(parent 里挂一堆 api、common、admin、core 子模块),对毕设来说过了,启动和调试都要多花不少时间。就一个 Spring Boot 单体应用,内部按分包:

com.creator.video ├── controller # 接收请求,参数校验 │ ├── admin │ ├── creator │ └── user ├── service # 业务逻辑 │ ├── impl ├── mapper # MyBatis Plus 的 Mapper 接口 ├── entity # 数据库实体 ├── dto # 前端请求/响应参数对象 ├── config # 配置类(跨域拦截、拦截器注册) ├── interceptor # 登录/权限拦截器 ├── common # 常量、统一返回结果、异常处理 └── util # JWT、加密等工具

这个结构的核心思想是"分层 + 按模块分包",controller 只用 service,service 不用 controller 的东西,entity 不直接返回给前端(用 dto 隔离)。有些同学图省事把 entity 直接挂在接口上返回,结果查出密码字段返回给前端,这在答辩现场被指出来会非常尴尬。

Controller 层还应该做一个统一异常处理和统一返回结构。我自己固定习惯是 Result :{ code: 200, msg: "success", data: ... }。一旦结构统一,前端处理响应就简单很多,全局异常捕获也只需要 @RestControllerAdvice 一个类。

2.4 数据库表设计:别只建了 users 和 videos

数据库表设计是答辩必考项。你用 MySQL 建表,至少要覆盖这些:

  • user:用户表(id、username、password、nickname、avatar、role、status、create_time)
  • video:视频表(id、user_id、title、description、category_id、cover_url、video_url、duration、size、status、view_count、like_count、favorite_count、publish_time)
  • category:分类表(id、name、sort)
  • comment:评论表(id、video_id、user_id、content、create_time)
  • favorite:收藏表(id、user_id、video_id、create_time)加唯一索引防止重复收藏
  • likes:点赞表(id、user_id、video_id、status)或者直接用 redis set 记录点赞用户

几个关键细节:

  • 视频状态 status 用 tinyint:0 待审核、1 已发布、2 已下架。发布流程虽然简单,但有一个"待审核/已发布"状态流转,答辩老师就很难说你的系统没有内容管理意识。
  • 关注关系可以不做单独表,如果做,就是 follower_id 和 followee_id 两张表或者一张表搞定,别搞复杂了。
  • 视频字段不要存绝对的完整路径,比如 C:\Users\xxx\upload/xxx.mp4。存相对路径 /video/xxx.mp4,访问时由前端拼上服务器地址。不然换环境跑 Demo 的时候所有路径全废,那是本人亲身踩过的坑。

详细一点,video 表的建表语句可以是这样(简化):

CREATE TABLE `video` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '上传者ID', `title` varchar(100) NOT NULL COMMENT '视频标题', `description` varchar(1000) DEFAULT NULL COMMENT '视频简介', `category_id` bigint NOT NULL COMMENT '分类ID', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面相对路径', `video_url` varchar(255) NOT NULL COMMENT '视频文件相对路径', `duration` int DEFAULT NULL COMMENT '视频时长(秒)', `size` bigint DEFAULT NULL COMMENT '文件大小(字节)', `status` tinyint DEFAULT '0' COMMENT '0待审核 1已发布 2已下架', `view_count` int DEFAULT '0', `like_count` int DEFAULT '0', `favorite_count` int DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `publish_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_category` (`category_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='视频信息表';

索引不需要多,但 user_id、category_id、status 这种查询条件字段一定要有,答辩老师如果看到 SELECT 语句全表扫描,基本会追一句"数据量大怎么办",索引就是最好的答案。

3. 核心模块实现与关键代码

我挑几个最有含金量、也是答辩最容易紧盯的模块来讲:登录鉴权、视频分片上传、FFmpeg 转码、播放鉴权、互动与计数。这些是视频创作平台跟普通 CRUD 项目的分水岭。

3.1 用户登录与 JWT 鉴权

毕设用 JWT 已经是约定俗成的玩法。核心逻辑就三步:登录成功发签 Token;前端把 Token 存在 localStorage,请求时放进请求头 Authorization;后端拦截器校验 Token 合法性。

我建议不要自己拿秘钥手动塞 payload 写一个"手写 JWT",直接用 jjwt 库。

<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>

工具类:

public class JwtUtil { private static final String SECRET = "your-secret-key"; private static final long EXPIRE = 1000 * 60 * 60 * 24; // 24小时 public static String createToken(Long userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET) .parseClaimsJws(token).getBody(); } }

拦截器里面做的事:拿到请求头 Authorization → 去掉 "Bearer " 前缀 → parseToken → 把 userId 放进 request 的 attribute → 放行。写一个 LoginInterceptor 和 AdminInterceptor,注册的时候配置好哪些路径需要登录、哪些需要管理员权限。

这里有一个实操注意点:拦截器配置路径千万别写错,不然会出现"前端明明带了 Token 还是 401"。我建议这样配:

registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login", "/register", "/video/list", "/video/detail/**", "/video/cover/**", "/video/stream/**");

也就是说,所有接口默认都要登录,只有登录注册、游客能看的视频列表和视频流地址放行。这个设计逻辑在答辩时非常好讲:未登录只能浏览和播放,一旦要做互动或进入创作者中心就必须登录。

3.2 视频上传:分片上传才是真知识点

普通表单上传用 MultipartFile 接收一个文件就完事了。但视频文件和图片不一样,动辄几百 MB 到几个 G,单次 POST 容易超时、内存溢出、断网重来。毕设里你不一定要把分片上传做到完美,但你必须"理解并实现"它。

业界通用思路是前端把文件切成片(比如每片 5MB),顺序传给后端,后端保存每一个分片到临时目录,等所有分片传完后触发合并接口,把分片按序号拼接成完整文件。

前端如果用 Vue,可以用 vue-simple-uploader,把分片大小、并发数一配就行。后端需要提供两个接口:

// 接收分片 @PostMapping("/upload/chunk") public Result<?> uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("identifier") String identifier, @RequestParam("chunkNumber") Integer chunkNumber) { String dir = uploadDir + "/chunks/" + identifier; File f = new File(dir, chunkNumber + ".part"); file.transferTo(f); return Result.success(); } // 合并分片 @PostMapping("/upload/merge") public Result<?> merge(@RequestParam("identifier") String identifier, @RequestParam("fileName") String fileName) { File dir = new File(uploadDir + "/chunks/" + identifier); File[] parts = dir.listFiles(); Arrays.sort(parts, Comparator.comparingInt(f -> Integer.parseInt(f.getName().replace(".part", "")))); File merged = new File(uploadDir + "/temp/" + fileName); try (FileOutputStream fos = new FileOutputStream(merged)) { for (File part : parts) { Files.copy(part.toPath(), fos); } } // 删除临时分片目录 FileUtil.delete(dir); return Result.success(merged.getAbsolutePath()); }

这个实现是最简版,够演示了。但你在写进博文的时候有一个小设计别漏掉:记录视频上传状态。你可以建一张 video_upload_record 表(id、user_id、identifier、file_name、total_chunks、uploaded_chunks、status),每次上传分片更新 uploaded_chunks,这样断点续传只需要在合并前检查前端传的 uploadedChunks 是否等于 totalChunks。回答"断点续传怎么实现"的追问时,你的回答就会比那些直接抄分片代码的同学深一层:不光是分片合并,还包含分片上传进度的持久化与对账。

3.3 视频转码与封面截取:FFmpeg 是加分大项

视频上传完不能原样就用于播放。浏览器能直接播放的格式有限,手机录的 MOV 很多浏览器不支持,所以系统要在后端调用 FFmpeg 做转码,输出统一的 H.264 + AAC 的 MP4,同时截取第一帧或指定时间点作为封面。

转码命令大概长这样:

ffmpeg -i input.mp4 -c:v libx264 -c:a aac -movflags +faststart -pix_fmt yuv420p output.mp4

在 Java 里用 ProcessBuilder 调用命令行:

public void transcode(String inputPath, String outputPath) { List<String> command = new ArrayList<>(); command.add("ffmpeg"); command.add("-i"); command.add(inputPath); command.add("-c:v"); command.add("libx264"); command.add("-c:a"); command.add("aac"); command.add("-movflags"); command.add("+faststart"); command.add("-pix_fmt"); command.add("yuv420p"); command.add("-y"); command.add(outputPath); ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(true); Process process = pb.start(); process.waitFor(); }

注意两个地方:-movflags +faststart 会让 MP4 文件把 moov 原子(视频索引信息)移到文件头部,这样浏览器可以边下边播,不用等整个文件下载完;-pix_fmt yuv420p 是保证在浏览器里兼容性最好的像素格式,漏掉它可能产生花屏或无法播放的问题。

截取封面命令:

ffmpeg -i input.mp4 -ss 00:00:02 -frames:v 1 cover.jpg

综合下来,我会建议把转码和封面生成放进一个异步服务里,用一个简单的线程池(ThreadPoolTaskExecutor)执行。上传接口先把源文件落盘,返回"视频正在处理中",后台线程做转码和封面,完成后再更新数据库中视频的状态为可发布。异步化会让你上传接口响应很快,同时给答辩留下一个"耗时任务异步处理"的解题思路。

实操里面最容易忽略的一点:FFmpeg 的路径问题。在 Windows 上如果你直接写 "ffmpeg",必须保证 ffmpeg.exe 在 PATH 环境变量里;部署到 Linux 服务器也一样。更稳妥的方式是配置 app.ffmpeg.path=/usr/bin/ffmpeg,读配置文件。这个细节看起来小,但现场演示的时候转码失败立刻黑脸。

3.4 播放器加载与播放地址鉴权

视频播放的常规做法是前端写一个 video 标签,src 指向后端提供的一个接口,比如 /video/stream/{videoId}。这里有个安全感和答辩质量的矛盾点:如果不鉴权,视频直接暴露,任何人都能下载源文件;如果太复杂的鉴权(URL 签名、防盗链),毕设演示又会给自己添乱。

我建议做一层轻量鉴权:视频文件不放在静态资源目录下,而是通过一个接口输出流,登录用户或请求里带合法 Token 才能访问。实现用 ResponseEntity :

@GetMapping("/video/stream/{videoId}") public ResponseEntity<Resource> stream(@PathVariable Long videoId, @RequestHeader(value = "Authorization", required = false) String token) { // 校验token,或者视频如果是公开状态,允许游客临时访问 Video video = videoService.getById(videoId); if (video == null || video.getStatus() != 1) return ResponseEntity.notFound().build(); Resource resource = fileStorageService.loadAsResource(video.getVideoUrl()); return ResponseEntity.ok() .contentType(MediaType.parseMediaType("video/mp4")) .body(resource); }

这种接口方式比直接把 video_url 暴露给前端更安全,而且你在答辩时可以讲:这是一种"服务端流式输出",不是简单静态文件托管,为后续实现播放权限、转码后切片(HLS)留下了扩展点。

3.5 点赞、收藏、评论与播放量计数

互动模块的难点是并发。一次点赞如果直接 UPDATE video SET like_count = like_count + 1,在高并发下会产生行锁竞争。毕设里你可以不搞太复杂,但至少要体现一个思路:热点数据先走 Redis,再异步落库。

我推荐两种做法选其一:

做法 A(纯 MySQL 计数):每个点赞操作先查 likes 表判断是否已点赞,再执行 UPDATE 计数。简单直接,也够用。唯一要留心的是 likes 表加唯一索引(user_id, video_id),防止重复点击产生脏数据。

做法 B(Redis 计数 + 定时落库):点赞只写 Redis,SETEX 一个 key,比如 like:video:123 是一个 set,成员是用户 id;播放量用一个 INCR 指令,value 就是视频 id;系统每隔 5 分钟跑一个定时任务,把 Redis 中攒的计数同步到 MySQL。这个方案在答辩中更亮眼。

我自己实际项目里用的是做法 B 的简化版,核心就两个逻辑:

// 用户点赞 public void like(Long videoId, Long userId) { Boolean first = stringRedisTemplate.opsForSet().add("like:video:" + videoId, userId.toString()); if (Boolean.TRUE.equals(first)) { stringRedisTemplate.opsForValue().increment("like:count:" + videoId); } } // 定时同步到MySQL @Scheduled(fixedDelay = 60000) public void syncLikeCount() { Set<String> keys = stringRedisTemplate.keys("like:count:*"); for (String key : keys) { String videoId = key.substring("like:count:".length()); Integer count = Integer.parseInt(stringRedisTemplate.opsForValue().get(key)); videoMapper.updateLikeCount(Long.valueOf(videoId), count); stringRedisTemplate.delete(key); } }

当然这只能保证最终一致,会出现最多 1 分钟的计数延迟,但这在真实业务场景里很正常,答辩老师听到你说"用最终一致性换性能",反而会认可你有工程意识。

4. 开发过程中典型的坑与排查实录

这部分内容全是实操现场踩出来的,体感比任何教程都真实,遇到同款问题可以直接照方抓药。

4.1 MultipartFile 传到一半就报 MaxUploadSizeExceededException

症状:上传一个几百 MB 的视频,Nginx 或 Spring 直接抛异常,前端收到 500。原因:Spring Boot 默认单文件上传上限是 1MB,你没配。

解决:

spring: servlet: multipart: max-file-size: 2048MB max-request-size: 4096MB

如果你前面做了分片上传,单片 5MB,这里的限制其实不用开这么大。但如果你图省事直接做整文件上传,那 2GB 的上限必须配到,否则必炸。

4.2 FFmpeg 转码进度看不到,以为卡死了

ProcessBuilder 执行 ffmpeg 时,如果把输出流不读取,缓冲区满了 ffmpeg 会被阻塞,看起来像"程序卡住"。我排过好多次这种问题,几乎都是因为没消费子进程的输出流。

解决:在 process.waitFor() 之前起一个线程读 errorStream 和 inputStream。或者用 commons-exec 的 DefaultExecuteResultHandler。我用得很顺手的一个方式是先开两个线程:

new Thread(() -> { read(process.getInputStream()); }).start(); new Thread(() -> { read(process.getErrorStream()); }).start(); int exitCode = process.waitFor();

同时你还可以设置一个超时机制,比如 5 分钟没处理完就 process.destroyForcibly(),防止某个坏视频文件把整个转码线程池耗干。

4.3 MySQL 视频表里存了路径,一换环境全挂

开发时存储相对路径,这也是我在第 2 节特意强调的事。我见过学长的毕设代码里 video_url 存的是 "http://localhost:8080/upload/xxx.mp4",拿到别的电脑上一跑,全部显示不出来,本地路径失效。别让这种低级事故砸了答辩。

更好的做法是:数据库里只存逻辑路径 /file/video/2024/05/xxx.mp4,页面展示时用一个全局方法拼接域名前缀。项目里写一个 UrlUtils 或配置属性,前端拿到的完整地址由后端统一生成。

4.4 前端跨域和带 Token 的图片加载

后端如果单独部署在 8080,前端跑到 5173(Vite 默认端口),跨域是必现的。全局配置一个 CORS:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

但注意一点:如果你用了拦截器校验 Token,跨域的预检请求(OPTIONS)会在进入拦截器之前就被拦截器处理掉,导致真正的前端请求因为预检没放行而失败。解决办法是在拦截器里加一行判断:

if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; }

这个坑特别隐蔽,不加这一行,前端会一直报"跨域请求失败",但不是 CORS 配置问题,是拦截器把预检请求拦了。

4.5 点赞数被刷,Redis 里每秒 INCR 几十次

毕设演示时,如果被围观的同学反复点赞,会显得这个系统"太容易刷量"。真实环境下的反刷机制(IP 限制、设备 ID、行为特征)对毕设来说太重,但我建议至少做一层用户维度限流:同一个用户对同一视频点赞,后端判断 likes 表里已存在记录就拒绝,而不是前端按钮置灰就完事。前端置灰可以被绕过,后端校验才是真的校验。

我习惯把"发放点赞令牌"做成一个接口,点赞前先 GET /like/status/{videoId},如果已经点了,后端再次调用点赞接口时直接返回"已点赞"。

5. 性能优化与部署上线要点

毕设到这一步就接近收尾了。这一部分做得好不好,直接决定答辩打分是在"优秀"档还是"良好"档。我把部署和优化串成一条线来说。

5.1 热点视频列表的缓存策略

视频首页轮播图、热门视频列表,数据不常变,但会被反复查询。每刷一次首页都去 MySQL 里 ORDER BY view_count LIMIT 10,就是白白消耗数据库。可以把热门榜单缓存到 Redis,设置 5 到 10 分钟过期。

实现很简单:

public List<VideoVO> getHotVideos() { String key = "hot:videos"; String cached = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(cached)) { return JSONUtil.toList(cached, VideoVO.class); } List<VideoVO> list = videoMapper.selectHotVideos(10); stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(list), 5, TimeUnit.MINUTES); return list; }

这里要注意的是缓存穿透:如果数据库里没有热门视频,缓存也没数据,每次请求都会打到数据库。可以在代码里判断 list 为空时也缓存一个空数组,过期时间设短一点,比如 60 秒。

5.2 定时任务清理临时文件

分片上传产生的 chunks 目录、合并后的 temp 文件、转码前的源文件备份,都会吃掉大量磁盘空间。写一个定时任务,每天凌晨清理超过 24 小时的临时目录。

@Scheduled(cron = "0 0 3 * * ?") public void cleanTempFiles() { File chunksRoot = new File(uploadDir + "/chunks"); File[] dirs = chunksRoot.listFiles(); if (dirs != null) { for (File dir : dirs) { if (System.currentTimeMillis() - dir.lastModified() > 24 * 60 * 60 * 1000) { FileUtil.delete(dir); } } } }

这个模块加进去,5 行代码,但能让答辩老师看出你的系统是有"运维意识"的,而不是个玩具。

5.3 部署时环境准备清单

毕设部署通常就一台云服务器(2核4G 就够)。按照这个顺序操作基本不会乱:

  1. 安装 JDK 8 或 11,配置 JAVA_HOME。
  2. 安装 MySQL 8.0,把本地导出的 sql 导入。
  3. 安装 Redis。
  4. 服务器安装 FFmpeg(apt install ffmpeg 或 yum install ffmpeg)。
  5. 把 Spring Boot 项目打成 jar 包,mvn clean package。
  6. 上传 jar,nohup java -jar video-platform.jar & 启动。
  7. 前端项目 npm run build,把 dist 目录丢给 Nginx,配置 location /api 反向代理到后端 8080。

Nginx 配置反向代理的时候,如果不配的话,前端会一直请求 /api 得到 404:

location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

注意 proxy_pass 后面那个斜杠,有斜杠表示把 /api 前缀去掉再转发,没有斜杠则是完整的 /api 路径转发。我遇到过很多次这种"前端请求正常后端返回 404"都是这个斜杠的问题。

还有视频文件的 Nginx 直出路径,如果你让 Java 接口输出视频流,就不需要配置;如果你想让 Nginx 直接访问本地静态文件,就要加 location /file/ 的 root 映射。

5.4 答辩演示时的几个小建议

这部分不算代码,但很关键。平时开发你可能开着可写日志、没关 debug,演示的时候控制台刷屏、页面网络请求全是 500 警告,印象分会打折扣。我建议正式演示前做三件事:

  • 把日志级别调到 WARN,只保留关键异常日志。
  • 准备一条 1 分钟内的短视频用于演示转码,不要现场传一个 1GB 的大视频,等转码就等半天,尴尬到脚趾抠地。
  • 提前测一遍断网环境(如果没有云服务器,至少保证本机演示时不要依赖外网 CDN),很多视频播放器组件或前端库如果走 CDN 加载,断网时页面空白,这是最常见的翻车原因。

6. 从毕设代码到可扩展项目的进阶方向

项目做完、答辩完,代码别急着压箱底。这个系统其实是很好的起点,后面你想继续深入几个方向,难度和收益我列个对比。

方向技术重点难度价值
视频分片播放 HLSFFmpeg 切 m3u8 + M3U8 播放器中商用级刚需
评论系统改造引入 WebSocket 实时弹幕中交互体验提升
推荐算法简单的内容标签匹配 + 用户行为记录中高算法岗简历亮点
短视频(竖屏信息流)双列流、翻页预加载、点赞动画中紧跟行业形态

我个人觉得最容易入手的是 HLS。源 MP4 整文件播放,占带宽、不支持进度条快进精度高,也防不了盗链。如果你用 FFmpeg 把一个视频切成 4 到 6 秒的切片,生成 m3u8 索引文件,前端用 hls.js 播放,系统整体就"高级感"上来了。而且你已经有了 FFmpeg 这套 CLI 调用经验,切 HLS 只是把输出命令换一下:

ffmpeg -i input.mp4 -codec: copy -start_number 0 -hls_time 6 -hls_list_size 0 output.m3u8

另外,把本地存储抽象成 MinIO 或者 OSS,是"批量生产级"的一个套路。你只要把 FileStorageService 接口做好,加一个 OssFileStorageServiceImpl,在配置文件里切换实现类型。这个扩展点写进简历,面试官普遍会觉得这个人有抽象设计意识。

我在帮学弟做这个项目的时候,最大感悟是:毕设项目不要太贪大,但一定不要在每个关键环节上只做表面功夫。上传只做单文件没有问题,但你要能说明为什么不支持分片,下一步怎么演进;用本地存储没有问题,但你要能说明云存储的接入边界在哪里;不搞推荐算法没有问题,但你要能说出业务上可以用什么手段优化曝光。这些"能讲清楚取舍"的能力,答辩老师最买账。

最后再分享一个小技巧:把数据库表的设计文档、接口文档用 Markdown 整理成一份 PDF,答辩之前打印出来。老师问任何一张表、任何一个接口,你都能指着文档回答,展示出来的工程素养比代码本身还加分。这个习惯我到现在做项目还在用,实测有效。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询