
做了好几个类似Spring Boot的视频网站项目之后我觉得可以把我复盘出来的东西认真写一写。这个基于Spring Boot的视频播放网站算不上什么特别新奇的项目但如果你真打算动手做——不管是为了毕设、个人作品集还是接一个小型商业项目——这里面从技术选型到部署上线的坑比想象中要多得多。尤其是当你把前端、视频处理、权限控制这些模块串在一起Spring Boot本身那些看似简单的配置往往会变成最花时间的部分。这篇文章我按我实际打过的项目来拆从整体设计思路、核心表结构、上传与转码流程到Vue打包后怎么塞进Spring Boot部署、哪些配置容易踩雷全部会聊到。1. 内容整体设计与思路拆解1.1 先想清楚你要做的是网站还是平台拿到基于Spring Boot的视频播放网站这个需求时第一步不是急着建工程而是要先界定边界。视频播放网站往小说只是一个能上传视频、播放视频的Web应用往大了说它会牵扯到视频转码、弹幕、评论、会员权限、后台管理、流量统计等一系列模块。我一般会根据项目目标把它拆成三个版本基础版用户注册登录、视频上传、视频列表、视频播放HTML5播放器直接拉静态视频文件、后台管理上架下架、分类管理。进阶版视频分片上传与断点续传、FFmpeg转码、视频封面生成、用户收藏点赞评论、浏览记录。完整版会员体系与付费视频、弹幕系统、推荐系统、视频水印、后台数据看板、权限体系细粒度控制。标题里只说了基于Spring Boot的视频播放网站但当你真正上手时至少要到进阶版才算是网站而不是静态视频列表页。因为如果不做转码和分片直接拿MP4在浏览器里播移动端兼容性和加载速度会让你崩溃。1.2 技术选型为什么是这套组合Spring Boot背后的Spring生态成熟稳定社区活跃度高整合第三方组件极其方便。我实际使用中选型如下模块选型原因后端框架Spring Boot 2.7.x稳定兼容性比3.x好对JDK8/11友好持久层MyBatis-Plus省去大量单表CRUD代码分页插件好用权限认证Spring Security JWT无状态认证适合前后端分离数据库MySQL 8.x主流事务支持好缓存Redis用于验证码、热门榜单、播放次数缓存视频存储MinIO本地私有化或阿里云OSS本地环境用MinIO公网项目用OSS视频处理FFmpeg转码、截图、水印全靠它前端Vue 2 / Vue 3 Element UI打包后由Spring Boot托管静态资源部署简单这里有个值得注意的取舍为什么不选Spring Boot 3.x虽然3.x出来很久了但它基于Jakarta EE部分老版本依赖比如某些MyBatis-Plus插件、非官方starter会出现兼容性问题。如果你的项目有现成的旧业务代码要迁移或者使用了一些不太活跃的第三方库2.7.x反而是最稳的。如果是全新项目且团队成员习惯新生态那3.x也没毛病只是要做好依赖适配的心理准备。1.3 项目结构怎么划分才不混乱Spring Boot项目结构虽然网上说法很多但我实际打完几个项目后觉得按业务模块分包比按技术类型分包更顺畅。下面这个结构是我比较常用的com.example.videoplayer ├── common # 通用工具、返回结果封装、异常处理 │ ├── Result.java │ ├── ResultCode.java │ └── GlobalExceptionHandler.java ├── config # 配置类WebMvc、Redis、MinIO、拦截器 ├── controller # 接口层按业务拆Controller ├── service # 业务层 │ └── impl ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体 ├── dto # 请求/响应对象避免直接用实体暴露给前端 ├── utils # JWT工具、MD5工具、FFmpeg命令工具 └── task # 定时任务清理临时文件、刷新缓存等按业务模块化背后的逻辑是视频播放网站的功能会持续叠加今天加弹幕、明天加推荐如果所有Controller堆在一个包下后期维护成本剧增。从第一天起就按业务拆包比之后重构省心十倍。2. 核心表结构设计与权限模型2.1 画清楚六张核心表视频播放网站的表设计不复杂但关系到核心体验。我用了六张表撑起主业务表名核心字段用途userid, username, password, avatar, role, vip_expire用户信息与角色videoid, title, description, cover_url, video_url, status, category_id, uploader_id, play_count, created_at视频主表video_categoryid, name, sort视频分类commentid, video_id, user_id, content, parent_id, created_at评论支持层级danmuid, video_id, user_id, content, time_point弹幕记录视频时间点play_recordid, user_id, video_id, progress, updated_at播放进度记录视频表里有个字段经常被忽略——status。它不能只做简单的上架/下架我习惯定义成一组状态0转码中上传后正在后台处理1待审核转码完成等待管理员审核2已发布审核通过可正常播放3已下架违规或管理员手动下线这样设计的好处是前端可以明确区分提示文案视频正在转码中请稍后视频正在审核中视频已下线而不是笼统地显示播放失败。状态机的价值不在于多复杂而在于让用户在任何情况下都知道发生了什么。2.2 用户角色跟视频权限怎么拆分很多初学者会把权限简单的做成普通用户和管理员两个角色。但如果要做付费视频、VIP会员视频这个模型就不够用了。我实际使用的是角色Role 资源状态双重校验用户表里的role字段普通用户、管理员、超管。视频表里的vip_only字段标记该视频是否需要VIP。用户表里的vip_expire字段记录VIP到期时间。在后端校验的时候不能只判断是不是VIP而是要判断VIP是否在有效期内。这里我写过一个经典bug用户VIP过期后因为之前生成的JWT里写了viptrue导致过期后仍能播放VIP视频。后来改成每次请求都查库校验vip_expire时间问题才解决。JWT里的信息永远只能做展示不能做权限判断的唯一依据。权限拦截器的核心思路是这样的Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 从请求头取token // 2. 解析token能解析出来说明登录过 // 3. 判断请求的接口是否需要VIP权限需要就校验vip_expire // 4. 校验通过放行否则返回402/403 return true; } }2.3 数据库层面的防刷设计做视频网站一定会遇到刷播放量的问题。刚开始我的做法是每次播放请求直接video.play_count 1结果一个用户反复刷新页面播放量就涨得离谱。后面改成同一个用户对同一个视频Redis里存一个24小时有效的标识有过就不计数没有才加1。这样避免频繁操作MySQLRedis自身的原子性也防住了并发下的计数错误。3. 视频上传、转码与播放链路3.1 为什么上传必须做分片直接往服务器上传一个几百MB的MP4表面看没什么问题实际上一旦网络波动TCP连接断开整个文件就要重传。这对用户体验是毁灭性的。所以我的上传流程是前端计算文件MD5通过Web Worker不卡UI。后端检查MD5对应的文件是否已存在秒传判断。后端生成本次上传的uploadId前端按固定大小如5MB把文件切成多个分片。每个分片独立上传后端收到后写入临时目录并记录分片索引。所有分片传完前端主动请求合并分片接口。后端按索引顺序拼接分片生成完整文件再扔进视频处理队列。分片大小需要根据实际网络环境调整。个人项目部署在普通服务器上5MB一个分片比较合适如果在内网环境可以调到10MB甚至20MB如果用户可能在弱网环境下使用2MB更稳。没有绝对的标准但建议设成可配置的项而不是写死在代码里。3.2 FFmpeg转码不转码之前一切都是白干如果你仅仅把MP4上传后直接给前端播放很快会遇到几个问题浏览器对视频编码格式支持不一致尤其是Safari对某些MP4的H.264编码没问题但对WebM就可能无法播放。视频文件体积大加载缓慢拖拽进度条卡顿。iPhone上部分视频无法正常播放。解决这些问题的标准方案就是转码为HLS协议输出。用FFmpeg把MP4转成m3u8索引文件和一个个.ts分片前端使用hls.js播放。这样视频首屏加载快、拖动进度条流畅兼容性也最强。实际用到的核心命令大概是这样的ffmpeg -i input.mp4 -profile:v baseline -level 3.0 -start_number 0 \ -hls_time 10 -hls_list_size 0 -f hls output.m3u8解释下几个参数-profile:v baseline使用H.264的baseline级别兼容性最好老设备也能解。-hls_time 10每个ts分片时长10秒个人网站建议10-15秒太短会导致请求过多。-hls_list_size 0表示保留所有分片在m3u8列表中否则默认只保留最近几个分片。转码是非常耗CPU的操作。在2核4G的小服务器上一个30分钟的视频可能转码要跑5-10分钟。所以绝不能在前台接口里同步执行转码必须丢到后台异步处理。我用的是最简单的方案上传完成后把转码任务信息扔进数据库任务表然后用Spring Boot的Async注解异步执行另一台机器或同一台机器的后台线程池去消费。3.3 视频播放鉴权怎么做才不卡顿视频直链如果不做鉴权别人拿到URL就能随意播放、下载甚至盗链。但如果你每次播放都先请求后端接口拿一个临时播放地址再交给播放器又会增加一次跳转延迟。我的方案是URL签名防盗链视频上传后保存原始路径不直接暴露。前端请求/api/video/getPlayUrl?idxx后端校验用户权限是否登录、是否VIP校验通过后用HMAC算法生成一个带过期时间的签名URL。播放器拿这个签名URL去请求视频后端解析签名过期或无效则拒绝。签名URL的格式类似这样/video/2025/04/01/xxxx.mp4?signmd5(uuidexpireTime密钥)expire1714560000这样即使URL被别人抓包拿到过期后也会失效。签名密钥一定不能放在前端代码里只放在后端配置中。4. Spring Boot项目实战难点记录4.1 Spring Boot自动装配原理在项目中的实际引用很多初学者在网上刷到过Spring Boot自动装配原理的面试题但在实际项目中什么时候真正用到这个知识我举一个我项目里遇到的例子。我想给项目加一个统一的日志切面用来记录每个接口的耗时和异常。刚开始我在pom.xml里引入spring-boot-starter-aop然后写一个Aspect类。但系统里有个第三方SDK它自己带了一个旧版本的AOP相关依赖启动时直接报了NoSuchMethodError。排查的路径就是围绕自动装配Spring Boot在spring.factories或AutoConfiguration.imports中注册了大量自动配置类它们通过ConditionalOnClass、ConditionalOnMissingBean等条件决定是否生效。当项目里同时存在两个版本的AOP库时自动配置会依据类路径上的实际类来决定装配什么这时版本冲突就会导致装配出错误的结果。后来我用mvn dependency:tree查清楚冲突来源在pom.xml里用exclusion排除掉第三方SDK传递的旧依赖问题才解决。所谓熟悉自动装配原理在实战中就是遇到启动报错或Bean找不到时能快速从自动配置的角度定位到类路径冲突、条件不满足之类的根因。问我为什么推荐spring-boot-starter-*统一管理版本因为很多奇怪的无厘头报错最后查下来都是依赖版本不一致。4.2 Vue打包放进Spring Boot的具体操作这是热搜里一个非常高频率的关键词vue打包放进springboot中。很多人在开发环境前后端分离跑得很爽但部署线上时不想单独整一台Nginx想直接让Spring Boot把前端静态文件也托管了。可以而且很简单但要注意几个坑。首先在Vue项目里需要调整打包配置。以Vue 2为例修改vue.config.jsmodule.exports { publicPath: ./, // 关键用相对路径否则部署到Spring Boot子路径下会白屏 outputDir: dist, assetsDir: static, productionSourceMap: false }publicPath如果默认是/打包出来的JS/CSS路径会以根路径开始而Spring Boot托管时资源路径通常在/static/或直接用/一旦路径对不上最典型的表现就是页面白屏控制台报404找不到js。然后在Spring Boot中把打包好的dist目录放到src/main/resources/static里或者打到Jar外的指定目录同时为了支持Vue Router的history模式非hash模式需要配置一个路径转发规则Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { // 将非接口路径的请求都转发到index.html交给前端路由处理 registry.addViewController(/).setViewName(forward:/index.html); registry.addViewController(/{path:[^\\.]*}).setViewName(forward:/index.html); } }这个配置的意思是所有不包含点的路径即不是静态资源文件的请求都转发到index.html。否则你在前端路由/video/detail刷新一下Spring Boot会返回404。4.3 Spring Boot版本太高导致的依赖问题搜索词里springboot版本太高能成为热搜说明这事踩坑的人真不少。Spring Boot 2.7升3.x最典型的问题是javax.*变jakarta.*。比如旧代码import javax.servlet.http.HttpServletRequest在Spring Boot 3里直接编译不过得改成jakarta.servlet.http.HttpServletRequest。很多第三方库如果不适配Jakarta在Spring Boot 3下根本启动不了。另外有些同学喜欢用最新版Spring Boot比如3.3.x然后去搜索资料网上的教程还停留在2.x时代照抄之后各种报错。我的建议是如果你是为了快速做项目而不是尝鲜选稳定版本而不是最新版本。我用2.7.x完成了好几个项目一次都没遇到大坑。当然这有个前提你不依赖JDK17的新语法特性。Spring Boot 3.x的新特性当然很好但团队里的协作成本、第三方库兼容风险都要提前测。4.4 定时任务与配置管理视频播放网站里定时任务用到的场景很多我举几个实际案例每天凌晨清理临时上传目录中超过24小时未合并的分片文件。每10分钟同步一次Redis中的播放量到MySQL。每天刷新用户VIP过期状态。Spring Boot里开启定时任务非常简单启动类上加上EnableScheduling然后在具体方法上加Scheduled(cron ...)。但真正线上跑的时候有几个坑值得注意单机环境下Scheduled默认是单线程执行的。如果任务A跑了很久任务B会排队等。如果你有多个任务且耗时不一建议自己配置一个线程池Configuration public class SchedulingConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10)); } }定时任务要考虑幂等。比如同步播放量到MySQL这个任务如果你部署了两台实例做负载均衡每台都会跑一次任务播放量就double了。解决方案是加分布式锁Redis setnx或者干脆让定时任务只在某一台机器上开启配置开关。4.5 Spring Boot配置里的那些不用可惜的细节最后聊几个Spring Boot配置中的实用细节属于书上不讲、实际很香的类型第一个是application.yml里的多环境配置。做视频网站时我习惯拆成application-dev.yml本地开发、application-test.yml测试服务器、application-prod.yml生产环境。启动时用spring.profiles.activeprod指定即可。这背后的逻辑是不同环境的数据库地址、Redis地址、OSS密钥都不一样硬写在一个文件里每次部署都要改太苦了。第二个是自定义配置项。MinIO的endpoint、AccessKey、SecretKey签名URL的密钥等这些不属于Spring Boot标准配置范畴但你会需要它们直接在application.yml里自定义myconf: minio: endpoint: http://127.0.0.1:9000 access-key: admin secret-key: admin123 bucket: video然后用ConfigurationProperties绑定到一个配置类里Data Component ConfigurationProperties(prefix myconf.minio) public class MinIOProperties { private String endpoint; private String accessKey; private String secretKey; private String bucket; }这样配置也规范化了代码里不会出现魔数。我一直坚持一个原则任何和环境相关的值绝不硬编码在业务代码里。第三个是全局异常处理。Spring Boot项目里接口报错默认返回的是一堆堆栈信息对前端极不友好。配置一个RestControllerAdvice类统一捕获异常并转成JSON格式返回前端就能根据code字段统一处理错误弹窗。这事越早做越好因为项目一大接口一多再去拦截就非常费力。5. 部署上线与性能优化的几点心得项目写完后真正上线又有一轮新的磨炼。我当时部署选的是宝塔面板 Docker。原本纠结要不要用K8s后来想清楚了个人项目或中小型项目用Docker Compose管理几个容器就够了K8s的运维成本对一个小团队来说是负担而不是加分项。部署清单大致是这样的用Docker跑MySQL和Redis数据目录挂载到宿主机避免容器删除后数据丢失。写Dockerfile打包Spring Boot应用注意Docker镜像使用JDK基础镜像Jar包直接COPY进去。MinIO单独用Docker跑映射端口9000API和9001控制台。Vue打完包后要么塞进Spring Boot的Jar里要么单独用Nginx托管两者都可以。如果视频量大建议还是单独跑Nginx放静态资源把Spring Boot纯粹当后端API服务器这样静态资源和后端接口都互不干扰。性能优化这部分我总结了几条实在的视频播放的核心瓶颈在带宽和视频编码不在框架。你需要关注的是视频这个文件被访问时是否走本地磁盘IO尽量给MinIO走内网地址而非公网。热门视频列表不要每次查数据库缓存到Redis里设置5分钟过期即可能扛住大部分流量。视频转码任务堆积时不要无限增加线程要看CPU核数。2核机器建议FFmpeg并发数控制在1-2个否则系统Load会爆炸。6. 常见问题与排查技巧实录我整理了一些做这个项目时高频出现的问题直接列成速查表方便你排查问题表现排查思路与解决方案前端打包后放进Spring Boot白屏控制台报错找不到JS文件检查Vue的publicPath是否设为./清浏览器缓存视频播放器转圈不播放m3u8请求404确认视频转码是否完成检查MinIO/OSS的访问权限确认签名URL是否过期上传大视频超时Nginx返回504调整Nginx的client_max_body_size和proxy_read_timeout上传接口走独立路径不过长超时VIP视频未登录也播放成功JWT里vip字段过期每次播放请求必须查库校验vip_expire不能只依赖JWT断言数据库连接池爆掉报错Connection is not available排查有没有连接没关闭调整HikariCP最大连接数检查慢SQL是否锁表导致连接被占满定时任务重复执行播放量翻倍检查是否多实例部署加上Redis分布式锁有一个隐蔽的问题要特别提一下如果前端用了hls.js播放m3u8而后端Nginx对m3u8和ts文件的Content-Type没配好浏览器不认也会导致播放失败。需要在Nginx配置里加上location ~* \.(m3u8|ts)$ { add_header Cache-Control no-cache; types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } }我当初为了这个能下载但播不了的问题折腾了整整一个晚上最后才发现是Content-Type的问题。7. 最后再分享两个实操小技巧第一个是关于FFmpeg转码时的进度监控。由于转码是异步的用户需要知道视频处理到百分之多少了。FFmpeg本身会输出time字段来表示当前处理时间我写了一个监听器去解析FFmpeg命令行输出的time参数再用已处理时间 / 视频总时长算出百分比存到Redis里。前端轮询接口就能实时展示转码进度体验会好非常多。第二个是关于Vue打包后静态资源的版本问题。Spring Boot里静态资源默认有缓存前端改了JS重新打包扔进去用户浏览器可能还是旧资源。解决方法是让前端打包时给JS和CSS文件名加上hash值Vue CLI默认会做同时后端设置带no-cache的响应头这样每次部署后用户拿到的都是最新的文件又不会每次都下载重复资源。做这个项目最大的感受是Spring Boot本身的上手难度并不高真正考验人的是整个视频处理链路和部署细节。你越是对那些看起来不起眼的配置较真线上出的幺蛾子就越少。希望这篇总结能让你在动手做基于Spring Boot的视频播放网站时少走点我走过的弯路。