
做视频点播系统这个毕业设计最容易被卡住的不是代码本身而是不知道从哪下手。我在带过几届毕设、自己也完整写完这个题目之后最大的感受是Spring Boot视频点播系统本质上是一个“被视频播放包裹着的内容管理系统”。你把用户、分类、内容、播放、评论、权限这几条线理清了整个项目就有了骨架再往骨架里填上传、转码、播放器这些细节系统才真正“活”起来。这篇攻略不是一份跑通即丢的源码清单而是一条从需求拆解到数据库设计、从视频上传到转码播放、从后台权限到答辩准备的完整实战路线。适合正在做毕业设计的同学、想用Spring Boot练全栈的开发者也适合要快速搭建内部培训视频平台、但没看过整套实现思路的运维同事。我会把我在实操中踩过的版本坑、上传坑、播放器坑都写出来那些地方才是真正浪费时间的环节。1. 动手前的需求拆解视频点播系统到底要做什么这一节我们不看代码先把“系统要干什么”想清楚。毕设答辩的时候老师第一个问题往往不是“你怎么实现的”而是“你这个系统是干嘛的给谁用”。如果你连业务主线都讲不顺后面代码写得再漂亮也白搭。1.1 双角色业务主线用户怎么用管理员怎么管一个视频点播系统无论你是做慕课平台、短视频站还是企业内部培训系统核心的参与角色就两个普通用户和后台管理员。普通用户侧的完整链路是注册登录 → 在首页或分类页看到视频列表 → 点进视频详情看标题、封面、简介、播放量 → 点击播放 → 播放过程中自动记录进度 → 看完可以收藏、评论 → 去个人中心查看最近观看记录和收藏列表。管理员侧是另一条线登录后台 → 上传视频填写标题、分类、封面、简介 → 系统对视频做转码处理 → 审核通过后上架 → 用户端能看到新内容 → 后台可以看播放量统计、管理评论、禁用违规用户。把这两条链路写在纸上你就能得出一个很简单的结论**这个系统前端是一个“逛、选、看、评”的流程后端是一个“传、审、管、统”的流程。**前端页面不用多花哨但每个操作都要对应到后端接口后端接口不用多复杂但必须把用户和管理员这两条线切清楚。1.2 功能优先级清单哪些是保底项哪些是亮点项不少同学一上来就打开GitHub搜“视频点播系统源码”搜出来的多半是开源商城、博客后台、后台管理模板硬拿商城源码改成点播系统的结果就是表结构对不上、权限逻辑绕、改了半个月还在跟购物车较劲。这个坑我见得太多真不如自己从零搭一套干净的结构。按“先保毕业再做亮点”的原则功能优先级可以分成三档优先级功能模块说明保底必做用户注册登录、视频分类、视频列表与搜索、视频详情、视频播放、视频上传、评论、收藏、后台视频增删改查、用户管理这些功能构成一个闭环缺一个答辩都会被动强烈加分分片上传与断点续传、FFmpeg转码HLS、播放进度记录与续播、播放地址鉴权、播放量趋势统计、评论审核、Spring Boot Admin监控这些是“别人不做你做了”的亮点也是老师主要追问的部分尽量不要碰弹幕实时推送、在线支付会员、分布式微服务拆分在毕设周期里性价比极低容易做出半成品有一点要提前说**视频文件上传后的处理链路转码、切片、播放是整个系统的技术制高点也是答辩时最能展示你水平的部分。**如果你只做了静态MP4直出播放功能上不算错但就很难跟老师展开聊技术深度了。2. 技术选型定生死版本、框架、前端方案怎么定技术选型的核心不是“哪个技术新就选哪个”而是“在毕业设计这个周期内哪个组合能稳定跑通并且你能把原理讲清楚”。2.1 为什么是Spring Boot而不是SSH/SSM现在回看SSHStruts2 Spring Hibernate和SSMSpring SpringMVC MyBatis那一套本质上不是不能做而是配置成本太高。写一个接口要配XML、配web.xml、配数据源光环境就得搭两天。Spring Boot的价值在于自动配置和约定优于配置起步依赖一拉、内嵌Tomcat一跑、application.yml一写就能把精力集中在业务代码上。也有人会说那为什么不用FastAPI或者Express更轻还更快。我说句实话FastAPI确实轻量但毕业设计看的不只是“接口写得快不快”而是工程结构是否完整、事务边界是否清晰、权限是否落到位、文件流如何处理。Spring Boot把整套企业级开发的规矩都摆在你面前你在做毕设的过程中学到的那套分层思想Controller、Service、Mapper比快速写十个接口有用得多。所以答辩时被问“为什么选型”你就答“Spring Boot降低了集成成本同时保留了完整的分层架构适合快速实现业务闭环也方便后续扩展。”2.2 Boot 2.x还是3.x先把版本兼容性这本账算清这是我在实操里遇到的第一个大坑。Spring Boot 3.x 虽然新但它有几个硬性变化必须JDK 17Servlet包名从javax变成了jakarta很多旧版第三方starter不兼容。如果你是跟着老教程、老博客做会频繁遇到ClassNotFoundException。我给你的建议比较保守Spring Boot版本JDK要求Servlet包名推荐场景需要注意的配套2.3.xJDK 8/11javax.*老教程多配低版本依赖MyBatis starter 2.1/2.2左右2.6.xJDK 8/11javax.*毕设最稳选择资料丰富MyBatis starter 2.2.x/2.3.x3.xJDK 17jakarta.*想展示新技术接受踩坑MyBatis starter 3.xBoot Admin 3.x如果你时间紧闭眼选Spring Boot 2.6.x JDK 8/11这是最成熟、教程最多、坑最少的组合。如果你想在简历上写“用过Spring Boot 3”那也行但一定要提前把版本兼容性查清楚别等到启动失败再回头查哪条依赖不对。2.3 ORM层MyBatis、MyBatis-Plus还是Spring Data JPAORM层我直接说结论默认选MyBatis-Plus。它的好处是通用CRUD不用写SQL内置分页插件一句selectPage搞定Condition构造器做多条件查询非常直观。对毕设来说这能省下一大坨无脑的Mapper XML代码让你把时间花在转码、播放、权限这些真正有技术含量的模块上。但这里有一个答辩技巧如果你用了MyBatis-Plus一定要能回答出它底层是什么。其实MyBatis-Plus只是对MyBatis的增强底层还是四大对象、SQL解析、动态代理那一套。老师要是问“你的SQL是自己写的吗”你就说“简单的CRUD用Plus的封装分页和关联查询用自定义SQL写到XML里”。所以项目里留几张表的自定义SQL查询会更有底气。Spring Data JPA我一般不推荐毕设用不是它不好而是答辩时老师让你说SQL你对着JPA方法名解释起来不够直观不如MyBatis系的XML来的清楚。2.4 前端方案Thymeleaf还是前后端分离前端方案要看你想在简历上写什么。如果只求稳、求省事用Thymeleaf模板引擎后端Controller直接返回视图Session天然同源不需要处理跨域逻辑最顺。这是最不容易翻车的路线也是我平时给时间紧张的同学推荐的首选。如果你想把自己的简历写成“前后端分离项目”那就用Vue3 Element Plus Vite做前端后端只出REST接口。这条路视觉效果更好但你要提前解决几个额外问题跨域配置、Token存储、打包后的静态资源部署。还有一个容易被忽略的点不要以为IntelliJ IDEA社区版就不能开发Spring Boot社区版装个Spring Assistant插件或者直接用Maven命令mvn spring-boot:run启动完全够用不需要去折腾付费版。3. 数据库建模六张表理清整个业务主干建表是一个毕设项目的骨架建得好不好直接决定后面写代码是顺手还是拧巴。视频点播系统不需要太复杂的表结构六张核心表足够撑起全部业务。3.1 六张核心表的字段设计先看一张总览sys_user用户表id、username、password、nickname、avatar、role、status、create_time。密码这一列要存BCrypt加密后的密文不能存明文。video_category视频分类表id、name、sort、status。分类就是教育、科技、生活这种sort用来控制前台展示排序。video_info视频信息表id、category_id、title、cover_url、video_url、duration、play_count、state、uploader_id、create_time。state字段是核心0代表转码中/待审核1代表已上架2代表已下架。video_comment评论表id、video_id、user_id、content、status、create_time。status用来做评论审核0待审核1展示2违规隐藏。video_favorite收藏表id、video_id、user_id、create_time。业务上需要联合唯一索引防止同一个用户重复收藏同一条视频。play_record播放记录表id、video_id、user_id、progress、update_time。progress存观看到的秒数配合续播功能。这套表已经覆盖了“用户、内容、互动、轨迹”四个维度后面所有接口都是围绕这六张表增删改查。3.2 为什么视频表要做信息与资源分离如果你只有一个video_info表里面直接存“视频地址”字段看起来也能跑但一旦你开始做多清晰度、多版本视频就会非常痛苦。这里我建议拆出一个video_resource表。什么叫信息与资源分离video_info里存的是标题、封面、播放量、状态这种业务属性而video_resource负责存具体的视频文件地址、清晰度如流畅、高清、超清、转码状态、文件大小。一个视频信息可以对应多条资源记录。这就好比你在图书馆借书书的“标题和作者”是书的信息“具体在某层的某本”是它的馆藏资源一本书可以有很多复本。答辩时能讲清楚这个设计就已经比大多数“一张大表走天下”的同学高一个段位了。如果毕设时间紧不想拆两张表也可以把多清晰度合并成一个JSON字段或者逗号分隔的地址列表但这就不如拆表来得清晰了。3.3 字段设计里容易被忽略的细节建模阶段最容易被忽视的往往不是表数量而是这些不起眼的小细节状态字段一律用数字state、status都用tinyint0/1/2这种不要用字符串。字符串“审核中”和“已上架”看起来直观但代码里维护起来全是魔法值排序、统计、搜索都会不自洽。物理删除换成逻辑删除用户删除收藏、管理员删除视频都不要真正DELETE而是加一个deleted字段。这个点老师很喜欢问你的回答是“保留数据可追溯防止误删”。外键约束不要建在表里虽然理论上外键能保证一致性但因为分页、批量插入、扩展性等等原因项目里一般不用数据库外键而是在Service层保证关联数据的合法性。这个也能成为答辩时的设计亮点。索引要有目的地加分页列表常用category_id create_time组合索引评论表按video_id建索引播放记录表按user_id建索引收藏表用联合唯一索引。4. 视频上传与转码链路整个系统最有技术含量的部分如果说数据库是骨架那“视频文件怎么进来、怎么变成能播的格式、怎么被前端拉到”就是这套系统的血管。这一章是整个视频点播毕设的硬核区域也是我能给你最多实操经验的部分。4.1 上传接口从MultipartFile落地到磁盘Spring MVC接收文件非常直接用一个MultipartFile参数即可PostMapping(/api/admin/video/upload) public Result upload(RequestParam(file) MultipartFile file, RequestParam(title) String title, RequestParam(categoryId) Long categoryId) { if (file.isEmpty()) { return Result.error(文件不能为空); } // 文件后缀白名单防止上传可执行文件 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); ListString allowExts Arrays.asList(.mp4, .mov, .avi, .mkv, .webm); if (!allowExts.contains(ext.toLowerCase())) { return Result.error(不支持该视频格式); } // 按日期分目录20250501/xxx.mp4 String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); String fileName UUID.randomUUID().toString().replace(-, ) ext; File dir new File(VIDEO_UPLOAD_PATH datePath); if (!dir.exists()) { dir.mkdirs(); } File target new File(dir, fileName); file.transferTo(target); // 保存video_info记录state0转码中 // 提交异步转码任务 return Result.success(上传成功转码中); }几个关键点说一下。第一文件后缀判断必须做我见过传上去的“视频文件”其实是脚本文件这在实验环境里没关系但作为工程习惯白名单校验是底线。第二文件名不能用原文件名否则重名会覆盖中文名在Linux下还容易乱码用UUID最省心。第三按日期分目录是为了避免单个目录里堆积成千上万个文件也方便后续按时间清理旧文件。4.2 分片上传与断点续传毕业设计版怎么做得简单又能讲清很多毕设不需要做分片上传但如果你做了答辩就有了一个很好的话题。分片上传的原理其实很朴素把一个大视频文件在前端切成若干块比如每块5MB后端按块接收全部传完后合并成完整文件。打个比方整本书复印太重就一页一页传传完再装订。前端主要做三件事用File.slice()切块循环调用上传接口最后通知后端合并。后端要做的事是接收块文件→按顺序暂存→识别所有块都传完→合并。后端合并的代码思路// 每个分片上传时保存到临时目录chunk_temp/{identifier}/{index}.part // 全部传完后按index顺序逐个读出来写入一个完整文件 PostMapping(/api/video/chunk) public Result uploadChunk(RequestParam(file) MultipartFile chunk, RequestParam(chunkIndex) Integer index, RequestParam(chunkTotal) Integer total, RequestParam(identifier) String identifier) { File tempDir new File(CHUNK_TEMP_DIR identifier); if (!tempDir.exists()) { tempDir.mkdirs(); } // 保存当前块 chunk.transferTo(new File(tempDir, index .part)); // 判断是否全部块都上传完成 if (isAllChunksUploaded(tempDir, total)) { // 合并所有块为完整视频 File merged new File(VIDEO_UPLOAD_PATH, identifier .mp4); try (FileOutputStream out new FileOutputStream(merged)) { for (int i 0; i total; i) { File part new File(tempDir, i .part); out.write(Files.readAllBytes(part.toPath())); part.delete(); } } return Result.success(上传完成); } return Result.success(分片上传中); }这个逻辑“够用且能讲清楚”。更进一步的断点续传本质是让前端记录“哪些块已经传过了”没传的才传而不是从头再来。毕设里能做到这一点已经能写进项目亮点了。4.3 FFmpeg转码为什么必须做命令怎么敲视频上传后不能直接播吗能但不稳定。浏览器对视频格式的兼容性卡在一个很尴尬的位置不是所有MP4都能播关键是编码格式。很多手机拍出来的视频是HEVC或者高规格H.264在部分浏览器里要么黑屏要么没声音。而且MP4视频需要“元数据”才能拖动进度条FFmpeg转码能统一解决这些问题。我更推荐的做法是转成HLS流。HLS是苹果主导的流媒体协议把视频切成一个个小ts切片外加一个m3u8索引文件。好处是浏览器兼容性好、支持拖动进度、天然支持多清晰度切换。播放器只需要先读取m3u8然后按顺序加载ts切片体验比拖动一个巨大的MP4好得多。FFmpeg转HLS的命令长这样ffmpeg -i input.mp4 -profile:v baseline -level 3.0 -start_number 0 \ -hls_time 10 -hls_list_size 0 -f hls output/index.m3u8这个命令的意思是把input.mp4转成HLS每个ts切片10秒生成的m3u8索引文件包含所有切片。转出来的是一整个文件夹里面有index.m3u8加上一堆ts文件输出目录要单独建好。在Java里调用FFmpeg不推荐用Runtime.exec这种老方式用ProcessBuilder更可控String ffmpegPath /usr/bin/ffmpeg; ListString command Arrays.asList( ffmpegPath, -i, inputPath, -profile:v, baseline, -level, 3.0, -hls_time, 10, -hls_list_size, 0, -f, hls, outputDir /index.m3u8 ); ProcessBuilder builder new ProcessBuilder(command); builder.redirectErrorStream(true); Process process builder.start(); try (BufferedReader reader new BufferedReader( new InputStreamReader(process.getInputStream()))) { String line; while ((line reader.readLine()) ! null) { log.info(line); } } int exitCode process.waitFor();转码这个动作很耗CPU和时间一个几十MB的视频可能转几十秒一个几百MB的转几分钟。一定不能放在上传接口里同步执行否则前端上传后等十几秒才能响应体验极差。正确做法是上传接口落库后把转码任务丢进一个线程池异步执行转完再回写video_info中的video_url。毕设用ThreadPoolExecutor或者Spring的Async就够了如果老师追问生产环境怎么做就说换消息队列解耦。4.4 文件存储与访问的两种落地方式视频文件存哪里两种选择本地磁盘、云对象存储。毕设首选本地磁盘。你需要把你的视频文件目录放在项目外部不要放在src/main/resources或者项目目录内部否则打包部署后文件会丢、路径会乱。比如Windows下放在D:/video-files/Linux下放在/opt/video-files/。然后在Spring Boot里配置静态资源映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file:D:/video-files/); } }这样浏览器访问http://localhost:8080/files/20250501/xxx/index.m3u8就能直接拿到转码产物。注意addResourceLocations结尾的斜杠不能少这是很容易踩的细节。如果项目要部署到公网给别人访问本地磁盘的带宽和容量都不够那就要用阿里云OSS或者腾讯云COS这类对象存储上传完后拿到CDN加速地址。毕设阶段讲清楚区别就行不一定要真的接云服务。5. 播放模块鉴权、播放器、进度记录视频能传上来了下一步就是让它顺畅地播出来。这个模块的实操细节非常多播放器的坑也主要集中在网络请求层面。5.1 播放地址鉴权怎么防止别人直接复制链接白嫖很多毕设课设直接把视频地址存储在数据库里前端拿到什么URL就播什么URL。这个做法有一个致命问题我在浏览器F12里看到m3u8的地址直接复制到下载工具里就能绕过你的系统把你服务器上的视频全部拖走。毕设阶段比较合理的做法是播放地址不直接给而是先请求业务接口后端校验当前登录用户、校验视频状态再把播放地址返回给前端。比如GetMapping(/api/video/play/{videoId}) public Result play(PathVariable Long videoId, HttpSession session) { // 1. 校验用户是否登录 Object user session.getAttribute(loginUser); if (user null) { return Result.error(请先登录); } // 2. 查询视频信息校验state是否为1 VideoInfo video videoService.getById(videoId); if (video null || video.getState() ! 1) { return Result.error(视频不存在或未上架); } // 3. 查询播放记录返回上次进度 PlayRecord record playRecordService.getLastRecord(videoId, userId); // 4. 返回播放地址和进度 return Result.success(PlayVO.of(video.getVideoUrl(), record null ? 0 : record.getProgress())); }做到这一步已经能挡住“随便复制一条URL就能看”的初级问题。如果要更严格可以给播放地址加一个签名参数后端生成时带上用户ID和过期时间做一个MD5签名播放接口在返回给前端之前校验签名是否有效。生产级的方案是CDN防盗链或者Nginx secure_link毕设里你只需要在答辩时说清楚“我为什么不在数据库里直接存明文URL”老师就会觉得你有安全意识。5.2 播放器集成hls.js video.js实操HLS流在Safari里可以原生播放但在Chrome、Firefox里要靠hls.js。前端最简单的方案是直接用video.js它内置了hls.js的能力封装也更漂亮link hrefhttps://cdn.jsdelivr.net/npm/video.js/dist/video-js.min.css relstylesheet script srchttps://cdn.jsdelivr.net/npm/video.js/dist/video.min.js/script video idplayer classvideo-js vjs-default-skin controls/video script var player videojs(player, { controls: true, autoplay: false, preload: auto }); player.src({ type: application/x-mpegURL, src: videoUrl }); /script如果你不想引入这么大的播放器也可以只用原生video标签加hls.jsvar video document.getElementById(video); if (Hls.isSupported()) { var hls new Hls(); hls.loadSource(videoUrl); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function () { video.play(); }); } else if (video.canPlayType(application/vnd.apple.mpegurl)) { video.src videoUrl; }播放器遇到“黑屏/转圈/无法播放”的时候不要瞎猜打开浏览器的Network面板看m3u8的请求状态和Content-Type。如果返回的Content-Type是text/plain而不是application/vnd.apple.mpegurl多半就是静态资源映射或者转码文件后缀的问题。我当时排查了半天最后发现是静态资源Handler没生效直接输出了JSON错误信息播放器自然不认。5.3 播放进度记录与续播进度记录这个功能做起来不难但能让你的系统“高级感”提升一个等级。任何在线视频平台都会记住你上次看到哪下次进来接着播。用户端上报进度在timeupdate事件里做节流上报。注意不能每次timeupdate都发请求那个事件一秒能触发几次直接打爆后端接口。简单做法是每10秒上报一次video.addEventListener(timeupdate, function () { var current Math.floor(video.currentTime); if (current - lastReported 10) { // 上报当前进度 axios.post(/api/play/record, { videoId: videoId, progress: current }); lastReported current; } });用户再次点击播放时请求播放接口拿到的progress前端播放器初始化后直接video.currentTime progress就能实现续播。播放记录表里的update_time还有一个用处个人中心的“最近观看”列表按update_time倒序查play_record表就能展示出用户最近看了哪些视频、看到多少分钟了。这个功能在答辩演示时非常加分因为它把“记录”这个动作变成了可见的“功能”。6. 后台管理与权限控制最容易拉分的两个模块后台管理模块看着平淡但它承载了系统里所有“管理员才能做”的操作。权限控制做得好不好直接决定了老师对你系统安全性的评价。6.1 认证方案选择Session拦截器还是Spring Security JWT毕设里有两条路线可选各有优劣方案实现难度适用场景答辩亮点Session 拦截器低Thymeleaf模板渲染、前后端同源简单可靠逻辑清晰JWT 拦截器中前后端分离项目无状态认证贴近真实场景Spring Security JWT高追求完整安全框架配置链复杂版本坑多如果你是Thymeleaf模板渲染直接用Session HandlerInterceptor就够了实现一个登录拦截器public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { // 前端页面跳转登录页 response.sendRedirect(/login); return false; } return true; } }再在WebMvcConfigurer里注册指定哪些路径需要拦截registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/admin/**) .excludePathPatterns(/admin/login);如果你是前后端分离那建议用JWT登录成功后后端签发一个token前端每次请求放在Header里后端拦截器解析校验。这样做的好处是无状态不依赖Session也更贴近真实项目的认证方式。Spring Security的配置链在Boot 3下坑不少如果你只是想用JWT不一定非要引入整个Security框架自己写一个拦截器加一个JWT工具类也能达到同样效果。这里顺带说一个容易被问到的设计题如果有第三方需要调用你的接口比如开放一个“轮播图查询”接口给小程序你的接口应该放在哪里是否单独建服务毕设不需要拆分微服务你只要把对外开放的接口统一放在/api/open/**路径下让拦截器对这个前缀放行再加一个简单的API Key校验就行。这个设计能长期演进也不至于把内部后台接口暴露给外部。6.2 后台的增删改查和上下架是怎么串起来的后台管理视频的核心操作是五件事分页查询、按条件搜索、上架、下架、逻辑删除。这几件事用MyBatis-Plus做非常顺手。分页查询先配置分页插件Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }Service层代码public PageVideoInfo pageVideo(int pageNum, int pageSize, String title, Long categoryId, Integer state) { PageVideoInfo page new Page(pageNum, pageSize); LambdaQueryWrapperVideoInfo wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(title), VideoInfo::getTitle, title) .eq(categoryId ! null, VideoInfo::getCategoryId, categoryId) .eq(state ! null, VideoInfo::getState, state) .orderByDesc(VideoInfo::getCreateTime); return videoInfoMapper.selectPage(page, wrapper); }上架和下架就一句话public void changeState(Long videoId, Integer state) { videoInfoMapper.update(null, new LambdaUpdateWrapperVideoInfo() .eq(VideoInfo::getId, videoId) .set(VideoInfo::getState, state)); }这个模块你要记住一点管理员的后台操作要跟普通用户的操作隔离。所有/admin/**下管理接口拦截器除了校验登录还要校验当前用户的role是不是admin。普通用户绝不能调用/admin/**接口去改视频状态否则后台就形同虚设了。6.3 统计面板与Spring Boot Admin监控到底帮你盯什么后台另一个加分项是数据统计。两种统计要分清楚。第一种是业务统计用户总量、视频总量、今日上传量、播放量TOP10视频、分类播放占比。这些都是后端写SQL用GROUP BY统计前端用ECharts画图表。比如查询每个分类的视频数量SELECT c.name, COUNT(v.id) AS cnt FROM video_category c LEFT JOIN video_info v ON v.category_id c.id GROUP BY c.id, c.name第二种是系统运行状态监控这里就要用到Spring Boot Admin。它做的事情是可视化管理Spring Boot应用的运行状态内存使用、线程数、健康检查、最近请求日志。引入方式很简单服务端和监控端各对应一个依赖服务端加spring-boot-admin-starter-server和EnableAdminServer被监控端的Client引入spring-boot-admin-starter-client并在配置文件里指定服务端地址。毕设里如果你能把Spring Boot Admin跑起来答辩时打开监控页面给老师看效果远比自己嘴上说“我的系统很稳定”要好。这里再提醒一个版本坑Boot Admin 2.x对应Spring Boot 2.xBoot Admin 3.x对应Spring Boot 3.x版本错配会启动失败。6.4 评论审核与内容安全如果视频平台允许用户评论评论审核这个功能建议做。设计思路是评论表里加一个status字段默认0代表待审核管理员在后台审核通过后变成1用户端才展示。最简单粗暴的做法是新评论默认直接展示后台提供一个删除/隐藏入口管理员看到违规评论一键下架。如果你想做得稍微讲究一点可以在新增评论时过一遍敏感词。基础做法是维护一个敏感词列表for循环遍历判断评论内容是否包含包含就拒绝或打回审核。如果老师追问有没有更高性能的算法你可以说大规模敏感词过滤一般用前缀树但毕设阶段用列表已经够了。7. 我踩过的坑启动失败、白屏、视频播不了的完整排查过程这一章你认真看因为我在这个项目上花掉的时间有一半是在调这些“看起来莫名其妙”的问题。每一条都是我实际遇到过的。7.1 Boot 3包名迁移javax变成jakarta后连启动都过不去我第一次用Spring Boot 3做项目时按老教程引入了一个依赖启动直接报ClassNotFoundException: javax.servlet.Filter。起初我以为是依赖没下载完整反复clean、刷Maven都没用。后来查资料才明白从Spring Boot 3开始Servlet规范和Java EE的相关包名已经从javax.*迁移到jakarta.*。这意味着所有老教程里出现的javax.servlet.http.HttpServletRequest、javax.servlet.Filter都要改成jakarta.servlet.*。这个坑的修复不难难的是排查方向。如果你用Boot 3全项目搜索javax开头的东西改掉如果是老版本千万别手滑引入新版本依赖。我在文章前面推荐你选Boot 2.6就是为了避开这个坑。7.2 MyBatis starter版本与Boot版本不匹配导致的启动失败跟包名迁移类似的是MyBatis starter版本不匹配。症状非常典型项目启动到一半Log里出现nested exception is java.lang.NoClassDefFoundError或者Invalid value type for attribute factoryBeanObjectType。这个时候十有八九是MyBatis spring boot starter版本跟Spring Boot不兼容。排查方法我给你列出来mvn dependency:tree先看项目的spring-boot-starter-parent用的什么版本然后去确认mybatis-spring-boot-starter的对应版本。Spring Boot 2.x搭配MyBatis starter 2.2.x或2.3.xSpring Boot 3.x则要用3.x的starter。这个对应关系一定要提前查不要等跑不起来了再试。7.3 上传文件要么报错要么默默失败Spring Boot默认的上传大小限制是单文件最大1MB单次请求最大10MB。做视频点播系统你随便拖一个几十MB的视频上去就会看到白底红字的FileSizeLimitExceededException。修复方式是在application.yml里放开限制spring: servlet: multipart: max-file-size: 2048MB max-request-size: 4096MB还有一个更隐蔽的问题上传文件保存完毕后如果在Linux服务器上部署上传目录的属主不对程序可能静默写入失败。所以文件保存后一定要立刻判断target.exists()不要只靠transferTo不报错就以为成功了。7.4 静态资源映射路径错误导致视频404视频上传到本地磁盘后前端访问/files/**却返回404这是另一个高频问题。排查路径我给你梳理一下第一步先确认addResourceHandler的URL前缀是不是跟播放地址一致。我见过项目里Handler配的是/files/**但实际生成的视频地址是/upload/**自然打不开。第二步检查addResourceLocations的路径末尾斜杠。Windows下写file:D:/video-files/末尾必须有斜杠少了斜杠目录映射不生效。第三步如果直接访问http://localhost:8080/files/xxx/m3u8能打开但页面里播放器黑屏那就要看m3u8里的ts切片相对路径是否也指向/files目录。FFmpeg生成的m3u8有些情况下首行路径是本地绝对路径需要在转码参数里处理成相对路径或者手动改m3u8内容。7.5 播放器跨域与本地预览的防盗链冲突如果你选择前后端分离前端跑在5173端口后端跑在8080端口播放器去请求/files/**的资源就是跨域。最简单的方式是在后端的WebMvcConfigurer里配置跨域registry.addCorsMappings(new CorsRegistry() { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(*) .allowedHeaders(*); } });注意我上面这段写法不对真实代码应该是registry.addCorsMappings(new CorsRegistry() { Override public void addMapping(...) { } });我直接写正确格式Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(*) .allowedHeaders(*); } }这段配置在毕设演示环境里够用但你要心里有数生产环境这么放行等于把接口裸奔出来应该配置具体的前端域名白名单。还有一个坑是防盗链。如果你给/files/**加了Referer校验在本地做测试时会发现视频播不出来——因为本地文件的Referer往往是空的或者直接访问时的Referer跟配置不一致结果把自己拦截了。如果你做Referer或签名防盗链一定要放行空Referer或者干脆先不做把精力放在更核心的功能上。8. 答辩准备老师最爱问的几个问题与进阶空间代码写完不代表结束答辩才是毕业设计的最后一关。很多同学项目做得没问题但被老师追问几句就露怯原因不是不会做而是没提前把“为什么这样做”想清楚。8.1 高频问题与答题思路我把毕设答辩中老师问得最多的问题整理成一张表你可以对着检查自己的准备情况问题建议回答思路为什么选Spring Boot做这个项目简化配置、自动装配、内嵌容器降低部署成本同时保持Controller/Service/Mapper分层架构适合系统化开发视频文件存在哪里怎么保证能访问存在服务器外部磁盘目录按日期和UUID组织文件通过ResourceHandler映射到/files/**生产可替换为对象存储为什么要做FFmpeg转码统一编码格式和切片解决浏览器兼容和拖动进度问题也为未来多清晰度切换留扩展空间HLS和MP4直播有什么区别MP4需要单文件完整传输拖动时加载压力大HLS切成多个ts切片加载快、兼容好你的权限控制是怎么做的登录后Session存用户信息拦截器拦截/admin/**路径接口层校验角色区分普通用户和管理员密码存在数据库里安全吗用的是BCrypt加盐加密数据库泄露后也不能还原明文如果几百人同时播放卡不卡列表和热点数据可以加Redis缓存转码后的HLS切片适合做CDN加速播放进度上报可以异步化处理降低峰值压力分页是怎么实现的MyBatis-Plus分页插件本质是拦截器在SQL上自动拼接LIMIT并返回总数这几个问题每一个你都要能闭着眼睛讲两分钟。讲的时候不要背稿用“我当时是怎么设计/踩坑后怎么改”的口吻讲可信度最高。8.2 还能往哪个方向进阶如果基础版本做完了还想继续加分我给你几个性价比比较高的扩展方向转码队列化把线程池换成RabbitMQ或Kafka上传后发一条转码消息消费者异步处理。老师一听就知道你懂生产环境的解耦思路。播放排行缓存化播放量数据先写Redis定时刷入数据库排行榜接口直接查Redis减少数据库压力。基于规则的推荐不必上协同过滤按“同分类 收藏量高 最近播放时间近”三个条件组合推荐就能写出一个效果不错的“猜你喜欢”模块。Nginx前置把静态资源和转码产品都用Nginx托管后端只负责业务接口既能缓解带宽压力也为将来部署到服务器做准备。答辩时这些扩展方向不一定要全部实现但说得出思路会让老师觉得你有架构视野。最后再分享一点个人体会做这个毕设时间分配上最容易出问题的就是“死磕前端样式”和“死磕某些非核心功能”。我的建议是先按“Boot 2.6 JDK8 MyBatis-Plus Thymeleaf 本地存储 FFmpeg转HLS”这条最保守的路线把用户看视频、管理员传视频这条完整链路跑通再回头补JWT、统计图表、Spring Boot Admin这些亮点。链路通了心就定了亮点是锦上添花但核心链路是必须稳稳拿下的地基。