SSM+Vue在线视频点播系统毕设源码全解析:分层架构到播放联调
简介:一套基于Java + SSM + Vue + MySQL的在线视频点播系统毕业设计项目,面向计算机相关专业学生,适用于毕业设计、课程设计及期末大作业。项目已通过导师指导并完成调试,下载后可直接部署运行,无需修改。包内含完整前后端源码、数据库脚本、论文文档及配套软件工具,覆盖用户登录注册、视频分类检索、在线播放、后台管理等核心功能模块,前端基于Bootstrap、ElementUI、Layui构建,界面简洁且交互流畅。资源共1414个文件,以Java、JSP、JS、CSS等前后端代码文件为主,另含SQL初始化脚本、MP4演示视频及部署说明,压缩包整体约48MB。项目结构规范、注释清晰,便于二次开发与学习参考,开发环境建议使用IDEA、MySQL 8.0及Tomcat 7/8。目前已有44人学习下载,适合需要完整可运行项目作为参考的初学者和开发者。
1. 拿到"在线视频点播系统"源码包后,先搞清楚它到底考你什么
这类基于 java+ssm+vue+mysql 的在线视频点播系统,在毕业设计里出现频率极高。你从压缩包里解压出来的东西,通常是一个后端工程、一个前端工程、一份 sql 脚本和一篇论文文档。但大多数同学搞错了一件事:这个项目能不能通过答辩,关键不在视频能不能播出来,而在你能否讲清楚 SSM 各层之间怎么协作、Vue 怎么把 token 管起来、视频文件为什么不能直接存数据库、以及数据库表为什么要那样拆。四年的学习成果,浓缩到 20 分钟演示里,评委问的都是这些点。
把这套系统当成一个标准的全栈 CRUD 增强版来看:用户注册登录、视频列表分页、后台管理维护、播放页展示、播放记录留存。技术栈里每个单词都有自己的职责,SSM 负责处理请求和数据,Vue 负责渲染页面和状态管理,MySQL 负责持久化。本文从后端骨架、前端对接、数据库设计、联调自测四个方向,把这套毕设源码包里最常见的实现方式拆开讲清楚。
2. SSM 后端的三个关键骨架:分层、鉴权、视频资源接口
2.1 包结构分层约定:Controller 里不写业务,Service 里不碰 SQL
SSM(Spring + SpringMVC + MyBatis)整合的项目,源码包里不管包名怎么改,核心结构永远是那四层。拿到代码第一步,先看 src/main/java 下的包路径,一般会分成 controller、service、mapper(或 dao)、entity(或 pojo)、common(或 util)这几个包。这个分层不是形式主义,它直接和论文里的"系统设计"章节对应。
Controller 层只做三件事:接收前端传参、调用 Service、返回统一结果。Service 层写业务逻辑,比如视频上传时的文件名生成、播放记录的幂等处理、分页参数的兜底。Mapper 层是 MyBatis 的接口,真正执行的 SQL 在 resources/mapper 目录下的 XML 文件里。Entity 对应数据库表,字段名一般和表字段保持一致,用驼峰命名。
@RestController
@RequestMapping("/api/video")
public class VideoController {
@Autowired
private VideoService videoService;
@GetMapping("/list")
public Result getList(@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "12") Integer pageSize,
@RequestParam(required = false) String categoryId) {
PageInfo<Video> pageInfo = videoService.queryPage(pageNum, pageSize, categoryId);
return Result.success(pageInfo);
}
}
这段代码里, @RestController 表明所有方法返回值直接以 JSON 形式写回响应体,不用再手动拼接 ResponseBody。 @RequestMapping 定义类级别的访问前缀,让整个控制器的 URL 都以 /api/video 开头。 PageInfo 是 PageHelper 分页插件返回的对象,pageNum 和 pageSize 分别表示第几页和每页条数,前端传参时可以忽视这两个参数,由后端给默认值兜底。
如果源码包里没有 PageHelper,而是手写 LIMIT 语句,常见写法是在 Service 层先计算 (pageNum - 1) * pageSize ,然后传给 Mapper。注意这里有个边界:pageNum 从 1 开始,SQL 里 LIMIT 的偏移量要从 0 开始,所以必须减 1。很多同学演示时点下一页发现数据重复,多数是这里忘了减。
2.2 登录鉴权:拦截器挡未登录请求,注解控制管理员功能
视频点播系统里,普通用户可以浏览和播放,但视频上传、分类管理、用户管理这些功能必须限制为管理员。SSM 项目里做鉴权有两种常见方案:拦截器 + 注解,或者 Spring AOP + 自定义注解。前者更直观,源码包和论文里都好解释。
登录成功后,后端生成一个 token(通常是 UUID 或 JWT),存到 Redis 或直接返回给前端。考虑到毕设项目一般没引入 Redis,绝大多数源码包的做法是把 token 存到数据库的 user_token 表或直接放在内存 Map 里。比较稳妥、写进论文也说得通的是前者——新建一张 token 表,字段包括 id、user_id、token、expire_time。每次请求时,拦截器把前端传来的 token 和这张表比对。
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private UserTokenMapper userTokenMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行预检请求,否则跨域时前端会报错
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String token = request.getHeader("token");
if (token == null || token.isEmpty()) {
response.setStatus(401);
return false;
}
UserToken userToken = userTokenMapper.selectByToken(token);
if (userToken == null || userToken.getExpireTime().before(new Date())) {
response.setStatus(401);
return false;
}
// 把 userId 放到 request 里,Controller 里可以从 request 获取当前登录人
request.setAttribute("userId", userToken.getUserId());
return true;
}
}
这里有个很关键的点:SpringMVC 拦截器默认只拦截 Controller 层的请求,不拦截静态资源。如果你在 SpringMVC 配置文件或 Spring Boot 的配置类里注册拦截器时,设置了 excludePathPatterns("/login", "/register", "/api/video/list", "/img/**") ,那些不需要登录就能访问的接口要在这里明确放行。最常见的坑是:配了拦截器之后,登录接口也被挡住,前端一调用就报 401,调试半天才发现拦截器没排除登录路径。
2.3 视频点播接口设计:静态文件直出和流媒体转发的取舍
视频点播系统的核心接口无非两类:视频列表查询、视频详细信息与播放地址。前者是普通 CRUD 分页,后者涉及到一个存储问题——视频文件放哪。毕设项目里最常见的做法是:视频文件上传到服务器本地指定目录(如 /usr/local/upload/video/ ),数据库 video 表里存相对路径 /upload/video/xxx.mp4 ,前端播放器通过完整的 URL 访问。
在 SSM 工程里,如果用的是 Spring Boot,可以在 application.yml 里配置静态资源映射:
spring:
mvc:
static-path-pattern: /upload/**
resources:
static-locations: file:/usr/local/upload/
这段配置的意思是把 URL 前缀 /upload/ 映射到服务器的本地磁盘目录 file:/usr/local/upload/ 。配置完成后,视频文件的访问路径就是 http://localhost:8080/upload/video/20240511_164532.mp4 ,前端拿这个地址直接赋给播放器的 src 属性即可。视频存本地磁盘的好处是降低数据库压力、备份方便,论文里写"采用文件系统存储视频资源,数据库只保存元数据和路径信息"这句话就够用了。
有些源码包做了进阶处理,把视频从 mp4 转成 m3u8 切片,用 HLS 协议播放。如果你的源码包里发现了 m3u8 相关代码,说明它的播放链路走的是流媒体方案。后端的做法通常是用 FFmpeg 把 mp4 转成 m3u8 加 ts 切片,再通过静态映射把切片文件暴露出去。Vue 前端播放 m3u8 时,video.js 需要额外引入 videojs-contrib-hls 插件或使用原生支持的 hls.js 库,这个上下文里涉及的依赖问题和跨域问题比较多,我放在本文第 3 章播放器小节里展开。
3. Vue 前端接住后端:路由、鉴权、视频播放三件套
3.1 axios 封装与跨域代理:别把 baseURL 写死在每个组件里
Vue 工程里,src 目录下一般有 api 文件夹,里面按模块拆分接口定义。找一个后端同学做的前端代码,最常见的封装模式是:先创建一个 request.js 或 http.js,统一实例化 axios,然后设置 baseURL、请求超时、请求拦截器和响应拦截器。这样做的好处是,当后端接口地址变更时,只改一个文件。
import axios from 'axios'
const service = axios.create({
baseURL: '/api',
timeout: 15000
})
// 请求拦截器:每次请求自动带上 token
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['token'] = token
}
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截器:全局处理 401 和业务错误码
service.interceptors.response.use(response => {
const res = response.data
if (res.code === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
return Promise.reject(new Error('未登录'))
}
return res
}, error => {
return Promise.reject(error)
})
export default service
如果这里 baseURL 设置成 /api ,那开发环境下必须配合 Vue CLI 的 devServer 代理,把你的请求转发到后端启动的 8080 端口上。在 vue.config.js 里配置 devServer.proxy : '/api': { target: 'http://localhost:8080', changeOrigin: true } 。这语法需要注意,changeOrigin 必须为 true,否则后端拿到的 Host 头是 localhost:8080 而不是你自己的开发地址,部分后端框架会因此校验失败。
一个常见的疑问是,打包部署后代理就失效了,这个 /api 前缀怎么处理。生产环境下,一般用 Nginx 做反向代理,把 /api/ 开头的请求转发到 Java 后端服务。如果后端 Controller 的 RequestMapping 已经带了 /api 前缀,那 Nginx 转发时要配置 proxy_pass http://localhost:8080; (不带斜杠),这样才能保留完整的 URL 路径。
3.2 路由守卫和菜单权限:为什么刷新页面后菜单状态会丢
Vue Router 里挂一个全局前置守卫,是控制页面访问权限的惯用做法。在 router/index.js 中添加 beforeEach ,判断目标路由是否需要登录,需要的话检查本地有没有 token。这里有个源码包里经常出现的隐蔽 bug:把 token 存到了 sessionStorage,但刷新页面后 sessionStorage 仍然存在,没有影响。真正的坑在于,部分源码包只在登录时把用户信息保存到 Vuex 里,一旦刷新,Vuex 重新初始化,用户信息变为空,而路由守卫只判断了 token 没判断用户信息,结果出现"能进页面但页面上不显示用户名"的怪现象。
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.path === '/login') {
next()
} else {
if (!token) {
next('/login')
} else {
next()
}
}
})
路由守卫的代码逻辑很简单,但放到"在线视频点播系统"里,它管的是这样一个流程:用户未登录时访问 /play/3 ,要跳转到登录页,登录成功后再跳回 /play/3 。完整实现需要记录 from 参数,在登录成功后 redirect。而管理员访问 /admin/video/add 时的权限验证,则应该在后端接口层面配合完成,前端只做入口隐藏,不做真正鉴权——前端隐藏菜单只是体验优化,后端拦截才是安全底线。
3.3 vue 播放 m3u8 的实现方案:video.js 依赖和初始化要点
Vue 项目里实现视频播放,原生 <video> 标签直出 mp4 只是入门写法。支持 m3u8 播放才是让视频点播系统"看起来更像正规视频网站"的加分项。m3u8 是 HLS 协议的索引文件,里面记录的是一串 ts 视频分片地址。浏览器不能直接播放 m3u8,所以需要 hls.js 或 video.js 这类库把它转换成可播放的格式。
在 Vue 2 工程里,安装 video.js 和 hls 支持的依赖命令如下:
npm install video.js@7 videojs-contrib-hls@5
安装完成后,常见的用法是封装一个 VideoPlayer 组件,在组件挂载后初始化播放器,在组件销毁前销毁播放器。初始化的核心配置包括:autoplay 是否自动播放(移动端基本都会被浏览器策略挡住,需要加 muted)、controls 是否显示控制条、sources 指定视频源地址。
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
export default {
name: 'VideoPlayer',
props: {
src: { type: String, required: true }
},
data() {
return { player: null }
},
mounted() {
this.player = videojs(this.$refs.videoPlayer, {
controls: true,
autoplay: false,
preload: 'auto',
sources: [{
src: this.src,
type: this.src.endsWith('.m3u8') ? 'application/x-mpegURL' : 'video/mp4'
}]
}, () => {
console.log('播放器初始化成功')
})
},
beforeDestroy() {
if (this.player) {
this.player.dispose()
}
}
}
用 video.js 播 m3u8 时,后端返回的 m3u8 文件如果带跨域限制,播放器加载分片时会报跨域错。排查这类问题时先看 Network 面板确认 m3u8 请求的 response header 里有没有 Access-Control-Allow-Origin ,没有就在后端配置全局 CORS 过滤器。还有一点容易被忽略:m3u8 索引文件里引用的 ts 分片路径是相对路径时,浏览器会基于当前 m3u8 的 URL 去拼接,如果后端做了 Nginx 反向代理,出现拼接地址 404,要去检查 Nginx 的 location 配置里有没有 add_header Access-Control-Allow-Origin * 。
4. MySQL 表设计与数据库交付物:从 ER 图到初始化脚本
4.1 核心表结构拆解:视频点播系统要建哪几张表
数据库设计是论文里的大章节,也是答辩提问的重灾区。一个合格的在线视频点播系统,最少要有五张核心表:用户表、视频表、分类表、评论表、播放记录表。用户表和视频表是主实体,分类表和评论表是关联增强,播放记录表是点播系统区别于普通视频网站的功能点。
用户表(t_user)常规字段有 id、username、password、nickname、avatar、role、create_time。role 字段用 int 或 varchar,1 表示管理员,0 表示普通用户。密码必须加密存,毕设项目里用 MD5 加密的不少,但答辩时最好能说出"MD5 不安全,生产环境应该用 BCrypt"这句话,这既能体现你有安全意识,也不会因为项目里用了简单加密而尴尬。
视频表(t_video)建议字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| title | varchar(100) | 视频标题 |
| category_id | bigint | 所属分类 |
| cover_url | varchar(255) | 封面图路径 |
| video_url | varchar(255) | 视频文件完整路径 |
| description | text | 简介 |
| play_count | int | 播放次数 |
| status | tinyint | 0 下架 1 上架 |
| create_time | datetime | 创建时间 |
play_count 字段在这里值得多说一句。如果每次播放都执行 UPDATE t_video SET play_count = play_count + 1 WHERE id = ? ,并发高时会产生行锁竞争。但在毕设场景里,这个写法足以应付。喜欢加分的话,可以把它做成一个独立的播放计数接口,前端在播放器 loadstart 或 playing 事件里调用,不要在后端主动从播放接口里触发,这样列表页刷新时不至于因为统计播放次数而增加数据库压力。
播放记录表(t_play_record)的结构是用户与视频的多对多关系拆出来的,字段有 id、user_id、video_id、progress、update_time。progress 记录观看进度(单位秒),方便用户下次从上次的位置继续看。这个表是点播系统的特色表,论文里可以展开讲业务意义,实现上要注意唯一索引 (user_id, video_id),保证一个用户对同一个视频只有一条播放记录,看视频时走 update 而不是 insert。
4.2 索引设计:列表查询和播放记录高频操作慢在哪
毕设项目数据量小,运行起来感受不到索引的价值。但答辩时导师必定会问:这张表索引怎么建的?为什么这样建?这比代码本身更能看出你的数据库功底。视频点播系统里高频查询有两条:一条是首页列表的分页查询,一条是根据用户 ID 查历史播放记录。
列表页查询语句常见的是 SELECT * FROM t_video WHERE status = 1 ORDER BY create_time DESC LIMIT 0, 12 。这个查询的过滤条件是 status,排序条件是 create_time,最合适的索引是组合索引 (status, create_time) 。如果单建 status 索引,MySQL 也能用,但需要额外的排序操作,数据量大时会有 filesort。
播放记录表的高频查询是 SELECT * FROM t_play_record WHERE user_id = ? ORDER BY update_time DESC LIMIT 10 ,建一个 (user_id, update_time) 联合索引就够了。很多源码包给出的创建索引 SQL 脚本是分开建两个单列索引,这确实也能走索引,但论文里写"为了提高查询效率,建立组合索引"这句话,显得更专业。
ALTER TABLE t_video ADD INDEX idx_status_create_time (status, create_time);
ALTER TABLE t_play_record ADD INDEX idx_user_update_time (user_id, update_time);
注意索引的顺序:等值条件的字段放前面,排序字段放后面。如果反了, (create_time, status) 这种索引在 status 做等值过滤时无法生效,因为索引最左前缀原则要求先匹配第一个字段。
4.3 数据库脚本交付:init.sql 里除了建表语句还要有什么
源码包里那个 sql 文件,通常命名为 init.sql 或 vdb.sql,它的作用不仅是建表,还要能让人一条命令跑完就得到一个可用的数据库。一份完整的数据库交付脚本应该包含:建库语句、建表语句、初始数据、可选的外键约束(也可以不加)。
初始数据特别重要。很多同学拿到源码后,mysql 导入脚本,启动项目,页面是空的,就以为项目有问题。实际上源码包作者没往 sql 里塞测试数据,你需要自己往视频表里插数据才能看到效果。如果源码包里 sql 文件太大,可能里面已经包含了演示数据。导入后优先查一下表里有没有数据:
SELECT COUNT(*) FROM t_video;
基础数据至少要包括一个管理员账号(用户名 admin,密码一般是 123456 或 admin 的加密形式)和一两个普通用户账号。视频数据建议插入十条左右,且视频地址填你本地能访问到的 mp4 或 m3u8 链接——这一步是保证页面能立刻跑起来的关键。有些同学导入别人的 sql 后,视频表里存的是原作者的本地服务器路径(如 C:\upload/video/xxx.mp4 ),自己的电脑上根本没有这个文件,播放自然白屏。需要把 video_url 改成自己可访问的路径,比如放一个测试视频到你的 Nginx 静态目录,或者直接用网上的公开测试视频地址。
5. 答辩前必做的全链路自测与三个高频坑收尾
5.1 最小流程验证:注册、上传、播放、记录四步走
把项目跑起来后,别急着演示功能。按真实的用户路径走一遍闭环:注册新账号、用管理员账号登录后台、上传一个视频、用普通用户账号播放这个视频、查看播放记录是否生成。这四步串起来,整个系统的核心链路就全部覆盖了。任何一个环节断了,优先定位是前端的问题还是后端的问题——打开浏览器开发者工具,看 Network 面板里的请求状态码和响应内容,比盲改代码高效得多。
上传功能尤其值得单独验证。视频上传走了文件流,常见的问题有两类:一是 SpringMVC 配置文件里的 maxFileSize 限制太严,默认 1MB 或 2MB,上传一个 500MB 的视频会直接报 MaxUploadSizeExceededException,需要在配置里调整;二是上传成功后,前端页面上图片能显示但视频播不了,这就要回头检查是不是 Nginx 或 Spring Boot 静态资源映射没配对。
5.2 环境根因排查:Tomcat、MySQL 时区与跨域
三个典型的"跑不起来"问题,每个都有明确的症状和对应的解决手法。
第一个,部署到 Tomcat 后后端能起来,但登录接口一调用就报 404。先看后端接口路径前面是不是带了项目上下文路径。Spring Boot 的 Tomcat 部署,默认上下文路径是 / ,但你用 IDEA 配置外部 Tomcat 运行时,context 可能是 /online_video ,前端 baseURL 是 /api ,请求落到 Tomcat 里实际路径变成 /online_video/api/... 。前端和后端对不上,自然 404。解决办法是在 Vue 的 devServer 代理里加上 pathRewrite ,把 /api 替换成 /online_video/api 。
第二个,MySQL 连接报错 "The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized"。这是因为你用的 MySQL 驱动版本(8.x)要求指定时区,而 MySQL 服务器本身时区设置不规范。在 JDBC 连接串上增加 serverTimezone=Asia/Shanghai 即可解决。如果数据库名是 utf8mb4,还要加上 characterEncoding=utf8mb4 ,否则中文会乱码。
第三个,Vue 开发环境接口正常,部署到服务器后所有请求都报跨域。这是因为开发环境 devServer 代理帮你转发,而生产环境没有做任何转发。用 Nginx 部署前端时,必须配置反向代理,把 /api 请求转发到 Java 后端: location /api/ { proxy_pass http://127.0.0.1:8080/; } ,注意这里的 proxy_pass 末尾有斜杠,表示把 /api/ 前缀去掉后转发,后端 Controller 的 @RequestMapping("/api/...") 只要目标后端没改前缀,会自行匹配。
如果后端也配置了 CORS 过滤器,生产环境和前端请求同时带了预检 POST 和 OPTIONS,记得在后端拦截器里放行 OPTIONS 请求,否则跨域情况下浏览器发起的预检请求被拦截器拦截返回 401,前端就拿不到实际的响应。
5.3 论文和答辩演示的配合:源码包里的论文要怎么改
高分毕设项目源码包里带的论文模板,质量参差不齐,但整体结构和章节框架是标准的。你需要做的不是重写,而是按学校要求替换个人信息和系统截图,再重点补充两个部分:一是把第 3 章"系统设计"里的数据库表设计改成你自己项目里实际的表结构,包括字段注释和表间关系说明;二是确认论文里描述的技术版本和你本地跑起来的环境一致。
答辩演示时,可以预演两个问题:视频播放失败怎么办、权限绕过怎么办。老老实实说"我先看 Network 请求是不是 404,再看文件路径在服务器上能不能访问到"比硬编一个答案更能获得认可。权限问题同理,答出"前端路由守卫只做展示层拦截,真正控制权限的是后端拦截器和 Service 层的鉴权判断"这一层逻辑,就足以回应对系统安全性的考察。
更多推荐

所有评论(0)