做点播系统这个方向,很多人一上来就想着堆功能、加模块,结果往往忽略了一个基础事实:点播系统的核心链路无非就是“视频上传 → 转码存储 → 播放鉴权 → 浏览检索”,所有花哨功能都建立在这条主线上。这套基于 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 的源码项目,走的正是这条主线,同时把用户体系、视频管理、分类检索、收藏、评论、播放记录这些周边功能全部补齐,属于典型的完整全栈项目,特别适合做课程设计、毕业设计参考,也适合入行一两年的开发者拿来当全栈模板,研究一下前后端配合和文件上传播放到底是怎么串起来的。
我前后花了一周多时间把整套源码摸了一遍,又自己动手从零搭了一套验证过,今天把这套系统里值得说的设计思路、核心实现、以及我实际踩过的坑全部整理出来。里面有建表语句、有后端核心代码片段、有前端联调细节,也有部署时遇到的问题清单,尽量做到你能照着复现。
1. 项目概述与技术选型的几个关键取舍
1.1 这套点播系统到底在做什么
先明确一个概念:点播系统(VOD,Video on Demand)和直播是两回事。直播是边推流边观看,强调实时性;点播则是把视频文件提前上传到服务器,用户按需点击播放,核心诉求是存储、转码、分发和播放体验。
这套系统整体上分为两套角色。普通用户端的核心流程是:注册登录 → 按分类浏览视频列表 → 查看视频详情 → 在线播放 → 点赞收藏评论 → 查看自己的播放历史。管理端的核心流程是:管理员登录 → 上传视频 → 填写标题、封面、分类 → 后台自动处理视频转码 → 审核上架 → 对用户和评论进行管理。两套角色共用一套后端接口,通过角色权限做区分。
从业务量级上看,它属于中小型项目,不追求过高的并发和分布式能力,但麻雀虽小五脏俱全。权限、文件处理、数据库设计、前后端交互这些最常见的工程问题都在里面,所以拿来学习和二次开发的价值很高,而不是那种只有一个增删改查的“假项目”。
1.2 技术栈为什么这么组合
先看后端,SpringBoot 2.x 到现在依然是国内中小型项目使用率最高的版本。SpringBoot 3 虽然发布已有一段时间,但要求 JDK 17 起步,很多老团队和现成的类库还没来得及完全适配。这套源码选择 SpringBoot 2.x,实际项目中的兼容性问题少,网上资料也多,出了问题很好查。对应 JDK 版本建议用 JDK 8 或 JDK 11,完全满足使用。
持久层选 MyBatis-Plus 的理由非常直接:单表 CRUD 几乎不用写 SQL,内置的 LambdaQueryWrapper 让查询条件拼接非常优雅,分页插件一条配置就能用。相比原生 MyBatis 少了一堆 XML 映射文件,相比 JPA 又保留了 SQL 的可控性。对于点播系统这种大量单表查询、少量复杂统计的业务来说,属于“刚刚好”的选择。
前端 Vue3 是当前社区主流,配合 Vite 构建,开发体验比 Webpack 时代快了一个量级。UI 组件库用 Element Plus,后台管理界面基本可以无脑套组件。这里要提醒的是,Vue3 的生态和 Vue2 差异不小,特别是组合式 API 的写法,如果你以前只写过 Vue2,源码里的 setup 语法、ref、reactive、computed 这些需要先补一补基础。
MySQL8.0 在这个项目里的意义可能被很多人低估。8.0 默认字符集就是 utf8mb4,存储 emoji 和生僻字不会像 5.7 那样出现乱码问题;窗口函数、CTE 让排行榜、分类统计这类需求可以用更简洁的 SQL 实现;加上默认的 caching_sha2_password 认证插件安全性更好。不过这也带来一个经典坑:老版本驱动和 8.0 的认证方式不兼容,后面部署章节我会专门说。
1.3 版本组合最容易踩的坑
这套技术栈虽然成熟,但版本组合如果不匹配,项目根本跑不起来。我实际验证下来,最常见的问题有这么几个:
第一,SpringBoot 2.7.x 对应的 MyBatis-Plus 版本别乱用最新的 3.5.x 系列,建议固定在使用过验证的版本组合,否则可能出现分页插件失效、启动时 bean 循环依赖之类的诡异问题。
第二,MySQL 数据库驱动必须用 8.0 对应的com.mysql.cj.jdbc.Driver,如果还在用旧驱动会出现“Public Key Retrieval is not allowed”的报错,解决办法是连接串上加allowPublicKeyRetrieval=true。这个问题我在第 5 部分会放一个完整的排查记录。
第三,前端 Node 版本不要太老,Vite 3+ 要求 Node 14.18 以上,推荐直接用 16 或 18 LTS 版本,免得 build 的时候报一堆语法兼容错误。
版本这东西,看着是小问题,但在真实开发里最容易卡住新人。项目源码附带的文档里通常会把环境要求写清楚,但很多读者根本不看文档直接开跑,所以我在这里先打个预防针。
2. 核心业务模块与MySQL8.0数据库设计
2.1 功能模块全景
在写代码之前,数据库设计决定了整个系统的上限。我先把这套系统的功能模块拆开来看,再讲每张表是怎么设计的。
- 用户模块:注册、登录、信息修改、角色区分(普通用户 / 管理员)
- 视频模块:视频上传、信息维护、上下架、封面管理、播放地址管理
- 分类模块:视频分类树,比如电影、综艺、纪录片、学习课程
- 互动模块:评论、点赞、收藏、播放历史
- 后台模块:数据统计、用户管理、评论管理、视频审核
模块划分的背后有一个关键设计原则:视频的二进制文件和视频的元数据必须分离。视频文件本体存在磁盘或云存储上,数据库里只存路径、大小、时长、状态等元数据。这样数据库查询时不需要加载大对象,性能稳定,文件备份和数据库备份也可以分开做。
2.2 表结构设计与DDL
这套系统的核心表大概是这么几张:用户表、视频信息表、分类表、评论表、收藏表、播放历史表。我把建表核心部分写出来,给你做个参考。
用户表sys_user:
CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(加密存储)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `role` tinyint NOT NULL DEFAULT '0' COMMENT '角色 0-普通用户 1-管理员', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 1-正常 0-禁用', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';视频信息表video_info:
CREATE TABLE `video_info` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `title` varchar(100) NOT NULL COMMENT '视频标题', `description` text COMMENT '视频简介', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面图地址', `video_url` varchar(255) DEFAULT NULL COMMENT '播放地址', `duration` int DEFAULT NULL COMMENT '时长(秒)', `size` bigint DEFAULT NULL COMMENT '文件大小(字节)', `uploader_id` bigint DEFAULT NULL COMMENT '上传用户ID', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态 0-待审核 1-已上架 2-已下架', `play_count` bigint NOT NULL DEFAULT '0' COMMENT '播放次数', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category_status` (`category_id`, `status`), KEY `idx_uploader` (`uploader_id`), KEY `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='视频信息表';注意duration和size这两个字段,看起来不起眼,实际上是视频系统里的重要数据。播放列表要显示时长,后台要根据文件大小做存储统计,没有这两个字段,后面做任何扩展都非常被动。它们可以不用人工录入,上传后用 FFmpeg 读取媒体信息自动补全,这是我们服务器端处理视频时顺手完成的事。
2.3 索引与查询性能设计
数据库设计里最容易被忽视但最影响体验的就是索引。点播系统最常见的查询场景是:按分类查视频列表、按标题模糊搜索、查某个用户上传的视频、按时间排序取最新。针对这些场景,上面的 DDL 里已经建了几个复合索引,这里单独解释一下设计逻辑。
idx_category_status (category_id, status)这个复合索引,是为了支撑“某个分类下所有上架视频”的高频查询。最左前缀原则决定了查询条件里必须有category_id,然后status会被用于过滤。如果把两个字段的顺序反过来,就只能在按状态查分类时会用上,实际业务里“查某个状态的全部视频”远没有“查某分类的上架视频”频繁,所以顺序不能搞反。
播放历史表video_history的核心查询是“查询某个用户最近观看的 20 条”,所以索引必须设计成(user_id, watched_at),这样才能走索引排序,避免 filesort 带来的性能损耗。这个细节我见过很多项目栽跟头,业务代码里各写各的查询,完全不看索引能不能命中,数据量一小没问题,一旦播放记录累积到几十万条,接口响应能慢 10 倍以上。
MySQL 8.0 的窗口函数在这套系统里也有应用场景。比如后台要统计“各分类下播放量最高的前三名视频”,老写法要么用临时表 + 多次子查询,要么干脆在 Java 内存里做聚合排序。8.0 里可以直接用 ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY play_count DESC) 一条 SQL 搞定,既简洁又高效。这也是我认为这个项目值得深入阅读的理由之一:它让你看到新版本数据库特性是怎么落地到真实业务里的,而不是只停留在面试题层面。
3. 后端核心实现:鉴权、上传、播放链路
3.1 SpringBoot项目结构与配置文件
后端代码我按常见的分层结构来组织:Controller 层只做参数接收和结果返回,Service 层写业务逻辑,Mapper 层通过 MyBatis-Plus 与数据库交互,实体类与表字段一一对应。这个分层看着简单,但它是后面所有功能可维护性的基础。
application.yml里几个关键配置值得单独拿出来说。
spring: datasource: url: jdbc:mysql://localhost:3306/vod_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 200MB max-request-size: 500MB mvc: static-path-pattern: /static/** resources: static-locations: file:/data/vod/upload/ mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImplserverTimezone=Asia/Shanghai必须加上,否则数据库连接时区对不上,时间字段会差 8 小时。这属于只要踩过一次就永远记得的经典问题。
multipart 的max-file-size要结合你的业务调整。视频文件动辄几百 MB,如果保持 Spring Boot 默认的 1MB,上传大视频直接报错。但也不是无限调大,真正生产环境里大型视频建议走分片上传,这个我在后面讲上传链路时会展开。
static-locations配置了file:/data/vod/upload/,意思是把磁盘上的/data/vod/upload/目录映射为/static/**这个 URL 前缀的静态资源。上传的视频和封面都放在这个目录下,前端拿到类似http://服务器地址/static/video/xxx.mp4的地址就能直接访问播放。这是一种本地存储方案,好处是简单直接,适合这个量级的项目和单机部署。
3.2 JWT登录鉴权与拦截器实现
这套系统的用户鉴权用的是 JWT(JSON Web Token),而不是传统的 Session。原因很简单:前后端分离架构下,接口是无状态的,用户登录后,后端签发一个 token 给前端,前端每次请求把它放在请求头里,后端校验通过就放行,不需要在服务端保存登录状态。这样后端可以随意水平扩容,不需要考虑 Session 同步问题。
具体实现思路是这样的:登录接口验证用户名密码成功后,用工具类生成 token,token 里包含用户 ID、用户名、角色这些基础信息,设置过期时间(这套项目一般设 24 小时)。然后写一个拦截器,在preHandle里从请求头取出 token,校验签名和过期时间,解析出用户信息放到请求上下文里,方便后续 Controller 直接取用。
拦截器里要特别注意排除白名单。登录接口、注册接口、视频列表和播放接口通常不要求强制登录,如果不加白名单,游客连视频都看不了,产品逻辑就错了。所以拦截器注册时要明确:哪些路径放行,哪些路径需要鉴权,哪些路径需要管理员角色。推荐把 URL 前缀设计得清晰一点,比如/api/admin/**全部需要管理员权限,/api/user/**需要登录,/api/public/**放行,这样权限规则一眼就能看明白。
我实际补充一个做法:不要把 token 过期时间设得太长,也不要太短。24 小时在用户体验上可以接受,配合前端在 token 快过期时主动刷新,或者干脆做续签策略,比让用户反复重新登录体验好得多。源码里不一定实现刷新机制,但你自己扩展时这值得加。
3.3 MyBatis-Plus在业务中的实际玩法
这套系统的大多数数据操作用的都是 MyBatis-Plus 封装好的方法。比如分页查询视频列表,只需要:
Page<VideoInfo> page = new Page<>(current, size); LambdaQueryWrapper<VideoInfo> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(VideoInfo::getCategoryId, categoryId) .eq(VideoInfo::getStatus, 1) .orderByDesc(VideoInfo::getCreatedAt); videoInfoMapper.selectPage(page, wrapper);代码里没有一行写死的 SQL,查询条件全部用 Lambda 表达式表示,编译期间就能检查字段名是否正确。如果还用字符串拼条件,字段名写错了只有运行期才报错,那查错成本就高多了。这里我建议你重点看selectPage配合分页插件的过程,它是整个列表页能工作的基石。
Mapper 层除了继承BaseMapper<T>,复杂查询还可以配合 XML 或注解写原生 SQL。像分类统计、用户行为统计这种多表联查场景,MyBatis-Plus 的封装就有点勉强了,直接写 SQL 反而更清晰。一个建议是:简单单表操作交给 MyBatis-Plus,多表统计写原生 SQL,两者配合,开发效率最高。
价格里还有一个常见的坑是逻辑删除。很多项目会给表加deleted字段做逻辑删除,MyBatis-Plus 也有@TableLogic注解支持。这套系统里视频下架用的是status字段而不是删除,效果类似但语义更清楚。你要注意:如果一个表既配置了逻辑删除字段,又配置了唯一索引,删除后再插入同 username/page 的数据会撞唯一索引,这类问题排查起来极其隐蔽。
3.4 视频上传、转码与播放的完整链路
视频处理是点播系统的灵魂,也是最容易出现方案性错误的环节。这套源码采用的是这种链路:前端选择文件 → 通过 MultipartFile 上传到后端 → 后端把文件保存到本地磁盘 → 调用 FFmpeg 对视频做转码处理 → 生成可供网页播放的 HLS 格式文件 → 把播放地址存库 → 前端播放器播放。
为什么播放地址不直接用原始 MP4,而要先转成 HLS?这里要讲明白原理。网页播放 MP4 时,浏览器需要顺序加载文件,用户拖动进度条到中部,浏览器要么从头下要么下载到目标位置才能播放,表现就是卡顿。而 HLS 协议把整个视频切成一小段一小段(.ts 文件),配一个 .m3u8 索引文件,播放器按需拉取对应片段,切换清晰度也只是切不同的 m3u8。所以 HLS 在点播系统里几乎是最成熟的播放方案,兼容性也好,大多数播放器都内置支持。
具体转码命令大概是这样的:
ffmpeg -i input.mp4 \ -codec:v h264 -codec:a aac \ -hls_time 10 -hls_list_size 0 \ -hls_segment_filename output_%03d.ts \ output.m3u8-hls_time 10表示每个切片 10 秒,-hls_list_size 0表示在 m3u8 索引里保留所有切片。如果你还想做多清晰度,可以分别输出 1080p、720p、480p 三套 m3u8,再在上层做一个码率自适应逻辑,但这套系统不一定完整实现,属于可选扩展。
上传环节我要重点提醒两个问题。第一个是超大文件的处理:如果视频达到 1GB 以上,一次性上传对后端内存压力极大,过程中网络一断就得重传。成熟的方案是前端分片上传,把文件切成若干片依次发送,后端按片拼接,断点续传和多线程上传都是这条路。这套源码如果是简单模式,通常只有单文件上传,你拿来练手可以,真要做生产环境建议自己实现分片逻辑。
第二个是上传文件的安全校验。一定要校验文件 MIME type 和后缀名,不能用户传什么你都收。我见过有人上传一个 .jsp 文件到没有做静态资源隔离的服务器,后果很严重。视频系统里至少要做到:白名单后缀校验、文件大小上限、非视频文件拒绝处理。转码时用 FFmpeg 还有额外好处,即使文件内容伪装为视频,转码失败也会把这个文件挡在门外。
播放器端处理比较简单,前端拿到.m3u8地址后,用支持 HLS 的播放器加载即可。如果是原生 video 标签,多数浏览器不直接支持 m3u8,就需要引入 hls.js 这类库。这也是一个实际开发中很常见的兼容性差异,后面前端章节再展开说。
4. Vue3前端实现与联调细节
4.1 前端工程结构与路由权限
前端部分结构很清晰:路由配置、状态管理、API 请求、页面组件、公共组件分开放。我用 Vite 初始化工程,入口是main.js,根组件是App.vue,路由用 Vue Router,状态用 Pinia,HTTP 请求用 Axios。
路由设计上要注意一点:前后端两套界面的路由要分开组织。用户端走/home、/video/:id、/user这类路径;管理端走/admin路径,配合子路由做二级菜单。进入管理端路由前加一个路由守卫,检查本地存的用户信息里的角色字段,不是管理员就拦截下来,跳回首页并提示无权限。
前端角色控制只是体验上的限制,真正安全的后端一定要再校验一次,这已经是老生常谈,但仍然值得强调。只在前端藏掉管理入口而不拦截后端请求,等于把门锁在画上,一点防护意义都没有。
4.2 核心页面与组件实现
用户端首页是典型的视频信息流布局:顶部导航栏 + 分类 Tab + 视频卡片网格。视频卡片展示封面、标题、播放量、时长,点击跳转详情页。这里的列表数据来自后端的分页接口,滚动到底部自动加载下一页,也就是常见的触底加载。实现思路是在页面底部放一个哨兵元素,进入视口触发loadMore函数,把 current 页码加一后重新请求数据,追加到现有列表后面。
视频详情页是这个系统体验最核心的页面。页面头部左侧是播放器区域,右侧是视频信息和操作按钮区。播放器我建议直接封装成一个公共组件,接收视频地址和封面图作为 props,内部根据协议类型自动选择播放方式:
<video ref="videoRef" :poster="coverUrl" controls></video>如果后端返回的是.m3u8地址,就用 hls.js 进行处理:
import Hls from 'hls.js' if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(videoUrl) hls.attachMedia(videoRef.value) hls.on(Hls.Events.MANIFEST_PARSED, () => videoRef.value.play()) }如果不是 HLS,则直接赋值给 video 元素的 src。这个判断逻辑很值得记下来,因为很多新手直接把 m3u8 丢给 video 标签,结果在 Chrome 里黑屏,然后就开始怀疑人生。其实不是你的代码错了,而是原生支持的问题。
管理端页面主要是视频管理表格和上传表单。上传表单里要处理进度条,Axios 的onUploadProgress回调可以拿到上传进度百分比,实时更新进度条组件,这块交互体验直接决定用户对大文件上传的信心。
4.3 Axios封装与跨域问题处理
前端所有请求都走一个统一的 Axios 实例。两个核心功能必须在封装里体现:请求拦截器和响应拦截器。
请求拦截器负责把 token 从本地存储里取出来,加到请求头的 Authorization 字段里。响应拦截器负责统一处理错误:401 表示 token 失效,跳转登录页;500 表示服务端异常,弹出消息提示;业务错误码非 0 时提示具体原因。没有这一层封装,每个页面都要重复处理错误,代码会变得非常啰嗦。
```javascript // request.js service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg) return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error('请求失败,请稍后重试') return Promise.reject(error) } )跨域问题是前后端分离项目必踩的坑。前端开发环境跑在localhost:5173,后端跑在localhost:8080,直接请求会被浏览器拦截。开发环境有两种解决方式:一是在 Vite 配置里加 proxy,把/api代理到后端地址;二是后端写 CORS 配置类,允许指定来源跨域。这两种方式建议都用:开发环境用 Vite proxy,灵活且不需要后端配合;上线后如果前端是通过 Nginx 部署并反向代理到后端,同源请求根本没有跨域问题。如果上线后还有跨域,那多半是 Nginx 代理配置没写好,去检查 location 块的路由转发。
这里说一个我实际遇到的坑:响应拦截器里如果对 HTTP 200 之外的响应直接提示错误,但后端接口上传大文件时请求时间很长,容易触发代理服务器的超时配置,表现为请求还没完成就返回 504 或者直接断连。开发环境 Vite proxy 默认超时较长一般没事,线上 Nginx 一定要显式配置proxy_read_timeout大一点,视频上传场景建议不少于 300 秒。
5. 部署上线与实战问题排查
5.1 从源码到上线的完整部署步骤
拿到这套源码后,部署步骤大概是这样的,我按顺序写清楚,你照着操作能少走很多弯路。
后端部分:确认 JDK 8 或 JDK 11 已安装,修改application.yml里的数据库连接信息,创建好名为vod_db的数据库并导入项目附带的 SQL 脚本。然后用 Maven 打包:
mvn clean package -DskipTests打包完成后在target目录下得到可执行 jar 包,直接运行:
nohup java -jar vod-system.jar > app.log 2>&1 &前端部分:进入前端目录安装依赖并构建:
npm install npm run build构建产物在dist目录,把整个 dist 上传到服务器。这里有两个选择:如果项目配置了前后端合并部署,可以把 dist 文件放进后端项目的static目录,重新打包后一起运行;如果按我推荐的方案,用 Nginx 做静态文件服务并反向代理 API,前端请求/api时由 Nginx 转发到后端的 8080 端口。Nginx 的关键配置块大概是这样的:
server { listen 80; server_name 你的域名或IP; root /data/vod/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 300s; proxy_send_timeout 300s; client_max_body_size 500m; } location /static/ { alias /data/vod/upload/; } location / { try_files $uri $uri/ /index.html; } }这段配置里client_max_body_size 500m特别关键。Nginx 默认只能接收 1MB 的请求体,不调整的话大视频上传会在 Nginx 层直接被拒绝,后端日志里什么都看不到。
还要单独提醒一点:视频文件目录、数据库的数据目录和系统盘最好分开。视频文件动辄几十 GB,如果和系统装在同一个盘上,很容易把磁盘占满导致服务器出问题。生产环境至少要做到挂载独立数据盘,并把上面的路径都指过去。
5.2 常见问题速查与解决实录
我把这次实际搭建过程中遇到的问题整理成一张速查表,每一条都是我亲手踩过并验证解决的,比看十篇报错帖子都管用。
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 后端启动报 Public Key Retrieval is not allowed | MySQL8.0 使用 caching_sha2_password 认证,驱动默认不允许自动获取公钥 | 连接串加allowPublicKeyRetrieval=true |
| 数据库连接成功但时间差 8 小时 | JDBC 连接未指定时区 | serverTimezone=Asia/Shanghai加上即可 |
| 上传大视频秒失败,报文件过大 | SpringBoot multipart 默认限制 1MB | 调整max-file-size和max-request-size |
| Nginx 报 413 Request Entity Too Large | Nginx 默认限制请求体 1MB | 加client_max_body_size 500m |
| 前端点击播放黑屏无反应 | video 标签原生不支持 m3u8 | 引入 hls.js 或采用带 HLS 支持的播放器 |
| 列表接口响应慢,数据量不大但等了很久 | 慢 SQL,查询条件没有走索引 | 用 EXPLAIN 分析,补充复合索引 |
| 刷新页面后 404 | 前端路由是 history 模式,Nginx 没配置 fallback | try_files $uri $uri/ /index.html; |
| 上传后评论和收藏接口返回 401 | token 过期或请求头没带 Authorization | 检查 Axios 拦截器注入 token 的逻辑 |
| 前端请求跨域失败 | 端口不同,开发环境未配置代理 | 用 Vite proxy 或后端配置 CORS |
排查时我建议你先学会看日志。后端日志文件里打印了完整的异常堆栈,MyBatis-Plus 配置了StdOutImpl日志后,每条 SQL 和参数都会原样打出来,这是定位慢查询和参数错误的第一手资料。前端的问题按 F12 打开控制台和 Network 面板,看请求的 URL、状态码和响应体,大部分问题都能在这里直接看出方向。
5.3 项目扩展方向
这套系统如果只是照跑一遍,价值就局限在熟悉流程上。想让它发挥更大作用,我基于实际项目经验给你三个扩展方向。
第一个是视频转码队列化。当前如果是在上传接口里同步调 FFmpeg,大文件转码会导致接口长时间挂起,用户体验很差。扩展思路是把转码交给消息队列异步处理,上传接口只负责存文件和入队,后台消费者处理转码并回写状态。技术上可以用自带的线程池加延迟队列模拟,也可以引入专业的任务调度。
第二个是存储层抽象。当前是本地磁盘存储,扩展时建议在 service 层做一个 StorageService 接口,本地实现和云存储实现分别写。接口定义好之后,从单机部署切到云存储或者分布式存储,业务代码几乎零改动,只是换一个实现类。这是在真实架构演进里非常常见的一步,提前做好抽象省很多事。
第三个是统计维度扩展。当前播放量只是简单加一,可以加上按天统计播放趋势、按分类统计热度、用户观看时长分析。MySQL 8.0 的窗口函数和 JSON 函数足够支撑这类需求,算是把这个系统从业务工具推向数据分析平台的尝试。
我在实际使用中发现,很多人拿到源码后的第一反应是急着跑起来,而不是先读项目结构。但源码项目真正值钱的地方在于它的结构和取舍。比如 Controller 层没有业务逻辑、Service 层定义事务边界、实体类字段与数据库一一对应、前端 API 层集中管理请求,这些东西在你以后写任何项目都用得上。读代码时多想想“这个模块为什么这么放”“这个查询为什么这么写”,比实现功能本身更有收获。最后再分享一个小技巧:跑通项目后,先尝试修改一个完整的功能链路,比如把播放记录“插入前检查是否已存在”改成“存在则更新时间”,从前端页面到后端 Service 完整走一遍,你对这套系统的理解会立刻上升到能二次开发的程度。