Java SSM Vue MySQL在线视频点播系统开发实战与避坑指南
简介:这是一套基于 Java、SSM、Vue 与 MySQL 构建的在线视频点播系统,面向计算机相关专业学生,适用于毕业设计、课程设计及期末大作业。项目已通过导师指导并获高分评价,下载后无需修改即可运行使用。资源包共 1414 个文件,包含 Java 源码、JSP 页面、JS/CSS 前端资源、图片、SQL 脚本以及论文文档;其中 JS 与 CSS 负责页面交互与样式,JSP 与 Java 实现后端业务逻辑,SQL 为数据库初始化脚本,压缩包整体约 48.04MB,可在 IDEA、MySQL 8.0、Tomcat 7/8 与 Maven 环境中部署。目前已有 44 人学习/下载。除了完整可运行的系统源码和数据库脚本外,还附带论文与常用工具,能够帮助读者快速理解 SSM 框架的整合方式、Vue 前端与后端的数据交互,以及 MySQL 表结构设计;对于希望快速搭建高完成度点播系统、用于答辩展示或二次开发的学习者,这套材料具有很高的实用价值。
1. 基于java+ssm+mysql的在线视频点播系统:先搞清楚这套东西值不值得做
看到“基于java+ssm+vue+mysql的在线视频点播系统”这个包名,第一反应是典型的毕设全家桶:后端用Java跑SSM框架,前端用Vue做单页应用,数据落在MySQL里,再附带一份能交差的项目源码和论文。这类资源的价值不在“下载下来能跑”,而在你能不能花两三天把它真正落地,并借这个过程把 java、ssm、vue、mysql 这几个关键词连成一条完整的业务链路。它适合两种人:一是JavaWeb课程设计需要交完整系统的学生,二是刚学完SSM想补一个前后端分离项目的新手。
这套系统要做的事说穿了就三件:用户能登录、能看到视频列表、点开能播放,后台能维护视频。围绕这三件事,数据库至少要有用户表、视频表和播放记录表;后端要拆出认证、列表、播放、记录四个模块;前端要有一个像样的播放页。技术栈看起来偏老,但SSM对理解Java基础帮助很大,Vue现在仍是主流,MySQL几乎每个后端面试都会问到,拿它当毕设或简历项目的性价比不错。下面按我落地这类项目的顺序来写,从建表到调接口,再到播放器踩坑,全程可以照着做。
2. 在线视频点播系统的表设计:用户、视频、播放记录三张核心表
2.1 Vue前端的功能页面和路由参数怎么定
先别急着写代码。一个点播系统前端页面不需要很多:登录/注册页、首页视频列表、视频播放页、后台管理页。播放页必须知道“当前播的是哪个视频”,这个信息用vue-router的动态路由来传最合理。路径写成 /video/:id ,进入播放页后 this.$route.params.id 就能拿到视频主键,再拿它去调后端接口。视频id不要放在 query 里,因为分享链接时 ?id=123 可能被各种工具截断,动态路由的语义也更清楚。
前端目录我一般这样分:
-
views/Home.vue:视频列表,卡片布局,点击跳播放页。 -
views/VideoPlayer.vue:播放器容器,负责取路由参数、请求详情、初始化播放器。 -
views/Admin.vue:后台管理,做视频上传和表单编辑。 -
router/index.js:路由注册。
路由文件尽量用懒加载,别把全部页面一次性打包进去。下面是简化版路由配置:
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{ path: '/', redirect: '/home' },
{
path: '/login',
component: () => import('@/views/Login.vue')
},
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/video/:id',
name: 'VideoPlayer',
component: () => import('@/views/VideoPlayer.vue')
},
{
path: '/admin',
component: () => import('@/views/Admin.vue')
}
]
export default new VueRouter({
mode: 'hash', // 用 hash 模式,避免刷新时 404
routes
})
这里用 mode: 'hash' 是关键。用 history 模式看起来更干净,但生产环境如果用 Tomcat 或 Nginx 没做 fallback 配置,刷新页面就会 404。很多第一次拿 Vue 和 Java 后端联调的人都会在这个点上翻车。毕设项目最省事的就是 hash 模式,URL 里多个 # 不影响演示。
路由配好之后,播放页里这样读视频 id:
// views/VideoPlayer.vue
export default {
name: 'VideoPlayer',
computed: {
videoId() {
return this.$route.params.id
}
},
created() {
this.loadVideoDetail()
},
methods: {
async loadVideoDetail() {
const res = await this.$axios.get(`/api/video/detail/${this.videoId}`)
this.video = res.data.data
}
}
}
这里的 $axios 是封装好的 axios 实例。我习惯在 main.js 给 axios 加一个请求拦截器,统一处理后端返回的 code/message/data 结构,页面代码会清爽很多。如果不想封装得太复杂,至少要做到:所有请求走同一个 baseURL,并且把 token 放进请求头。后面接口调不通时,只需要在浏览器 Network 面板看请求头,不用逐个页面去猜。
2.2 SSM后端API接口划分:Controller、Service、Mapper三层怎么拆
后端是评分老师最看重的部分。SSM 三个字母放在一起,实际上是一个分层约定:Spring 管对象,SpringMVC 管接口路由,MyBatis 管数据库访问。在视频点播系统里,最少需要下面这一组接口:
| 功能 | 请求方式 | 路径 | 参数 | 说明 |
|---|---|---|---|---|
| 用户登录 | POST | /api/auth/login | username/password | 校验后返回用户信息和token |
| 视频列表 | GET | /api/video/list | pageNum/pageSize | 分页返回视频列表 |
| 视频详情 | GET | /api/video/detail/{id} | 路径参数 | 返回标题、封面、播放地址 |
| 保存播放记录 | POST | /api/play/record | userId/videoId/progress | 更新播放进度 |
| 后台保存视频 | POST | /api/admin/video/save | 表单字段 | 管理员新增/编辑视频 |
接口路径统一用 /api 开头,是为了方便前端 devServer 做接口转发,也为以后把服务拆出去留余地。Controller 里不要直接返回 Map ,习惯性定义 Result 包装类,包含 code 、 message 、 data 三个字段,前端拦截器按 code === 0 判断成功。这个约定前后端都省心。
下面是一段典型的视频列表接口代码:
// VideoController.java
@RestController
@RequestMapping("/api/video")
public class VideoController {
@Autowired
private VideoService videoService;
@GetMapping("/list")
public Result<PageInfo<VideoVO>> list(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
PageInfo<VideoVO> page = videoService.pageVideos(pageNum, pageSize);
return Result.ok(page);
}
@GetMapping("/detail/{id}")
public Result<VideoVO> detail(@PathVariable("id") Integer id) {
return Result.ok(videoService.getDetail(id));
}
}
几个习惯能帮你少踩坑: @RequestParam 一定要写 defaultValue ,否则前端漏传一个参数,整个接口直接 400;返回前端前把数据库实体转成 VO,不要把 User 、 DO 直接暴露;分页对象用 PageInfo ,它已经帮你算好 total、pages、size。
Service 层尽量面向接口写。定义一个 VideoService ,用 VideoServiceImpl 实现,以后想加 Redis 缓存、改播放统计,只需要动实现类,不碰 Controller。这个习惯也常出现在 java 面试题里,能说清楚“面向接口编程”到底解决了什么问题,比单纯背概念有用得多。
Mapper 层只负责 SQL。视频列表需要和分类表联查时,写一个 XML 的 <select> 即可。注意 MyBatis 的驼峰映射:数据库字段是 cover_url ,实体里是 coverUrl ,记得在 application.properties 里加上:
mybatis.configuration.map-underscore-to-camel-case=true
不加这一行,联查结果里封面、创建时间这些字段全是 null ,这类问题在控制台看到 SQL 都不容易发现。
2.3 MySQL建表脚本:视频表为什么只存URL,不存二进制文件
数据库部分别只建一张用户表交差,至少要有用户表、视频表、播放记录表三张。视频文件不要塞进数据库,只存 URL 地址。原因很简单:BLOB 字段会把数据库文件撑得很大,Tomcat 再把二进制流吐给浏览器,内存和带宽都扛不住。生产环境视频文件一般放对象存储或 Nginx 静态目录,数据库只存相对路径,访问时拼成完整 URL。
下面这套建表脚本可以作为起步:
-- 用户表
CREATE TABLE `tb_user` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(64) NOT NULL,
`password` VARCHAR(128) NOT NULL,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 视频表
CREATE TABLE `tb_video` (
`id` INT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(200) NOT NULL,
`cover_url` VARCHAR(500) DEFAULT NULL,
`video_url` VARCHAR(500) NOT NULL,
`category_id` INT DEFAULT 0,
`status` TINYINT NOT NULL DEFAULT '1',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_title` (`title`),
KEY `idx_category_status` (`category_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 播放记录表
CREATE TABLE `tb_play_record` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`video_id` INT NOT NULL,
`progress_seconds` INT NOT NULL DEFAULT 0,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_video` (`user_id`, `video_id`),
KEY `idx_video_update` (`video_id`, `update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
字段细节有几个重点。 VARCHAR 长度:标题 200,URL 500,基本够用;URL 不要用 TEXT ,TEXT 建索引有前缀长度限制,也不方便做唯一约束。 DATETIME 用 DEFAULT CURRENT_TIMESTAMP 自动填充创建时间,比在 Java 里 new Date() 再塞进去省事。 status 用 TINYINT 控制上架状态,列表查询只 show status = 1 ,删除是改状态而不是物理删除,这样播放记录和关联引用不会断。
播放记录表是这套系统里最容易设计错的一张表。它记录的是“用户看到第几秒”,不是交易流水,所以同一个用户 + 同一部视频最多一行,来了新进度就 update。建表时直接加 UNIQUE KEY uk_user_video (user_id, video_id) ,这已经决定了后面去重的逻辑。 idx_video_update 是给“某个视频最近被谁看过”这类后台功能准备的。
我一般不建物理外键。表之间的关联在 Service 层控制,视频删除时先处理播放记录,再改视频状态。外键在 MyBatis 批量操作时很容易变成性能瓶颈,也会让导出数据、清空测试数据变得很麻烦。索引也不是越多越好,像 tb_play_record 这种写多读少的表,一个唯一索引加一个联合索引就够,再多反而拖慢写入。
注意:数据库字符集必须用 utf8mb4,不要用 utf8。utf8 在 MySQL 里存不了 emoji,而且 cover_url 里一旦出现特殊字符,容易报
Incorrect string value。
3. 让视频播放器真的能播:m3u8、vue-video-player与跨域配置
3.1 为什么选择 m3u8 而不是 mp4
先把一个概念说清楚: m3u8 不是一个视频文件,它是一份文本播放列表,里面写了很多 .ts 小分片的地址。播放器拿到 m3u8 后,跟服务端要一段缓冲一段,边下边播。在线视频点播里,m3u8 这种 HLS 格式比单文件 mp4 实用得多:支持分片缓存,支持码率切换,也方便控制访问权限。
如果直接对 <video> 标签塞一个 mp4,本地演示也许能放,但一旦放到服务器上,带宽和启动耗时都会暴露问题。m3u8 的另一个理由是浏览器兼容性。Safari 原生 video 能直接播 HLS,Chrome 和 Edge 不支持原生 m3u8,必须在播放器层面引入 HLS 处理逻辑。所以“vue播放m3u8播放器”这个诉求,本质上绕不开给播放器加一层转码解析能力。
毕设系统比较合适的做法是做一个“生成播放地址”的接口,后端把 m3u8 地址返回给前端,前端只负责播放。这样系统结构更像真实点播平台,而不是一个简单的 <video> 静态页。
3.2 搭一个挣得起 HLS 的 Vue 播放器组件
依赖我用 video.js 7 及以上,它默认内置 videojs-http-streaming ,可以直接播 HLS;如果只用 hls.js 自己驱动,控制条、音轨切换都要自己写,工作量不小。视频组件的核心代码如下:
<template>
<div class="player-box">
<video
ref="videoElement"
class="video-js vjs-big-play-centered"
controls
playsinline
preload="metadata"
></video>
</div>
</template>
<script>
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
export default {
name: 'HlsPlayer',
props: {
src: {
type: String,
required: true
}
},
data() {
return {
player: null
}
},
mounted() {
this.player = videojs(this.$refs.videoElement, {
autoplay: false,
controls: true,
sources: [
{
src: this.src,
type: 'application/x-mpegURL'
}
]
})
this.player.on('error', (err) => {
console.error('player error:', err)
})
},
beforeDestroy() {
// 路由切换时一定要销毁,否则会留下计时器和DOM节点
if (this.player) {
this.player.dispose()
this.player = null
}
}
}
</script>
关键参数是 type: 'application/x-mpegURL' ,它告诉 video.js 这是 HLS 流,不是普通 mp4,内部才会走 HLS 解析器。 autoplay: false 是有意设置的,现代浏览器对带声音自动播放有限制,强制 muted 也影响体验,不如让用户点一下。 preload="metadata" 只加载元数据,不会把整个分片列表全部拉下来。 playsinline 保证 iPhone 上不会自动全屏。
父页面接上视频地址后:
<template>
<div>
<HlsPlayer v-if="video.videoUrl" :src="video.videoUrl" />
</div>
</template>
v-if 很关键。如果后端接口还没返回地址就渲染播放器,video.js 会初始化一个空 player,之后更新的 src 未必生效。正确做法是先拿到 videoUrl 再挂载组件,或者说用 v-if 控制子组件生命周期。
3.3 跨域和静态资源转发:播放器黑屏时的排查顺序
开发环境里,Vue 跑 8080,后端 Tomcat 跑 8081,前端请求 /api/video/detail/1 要能落到后端,常见的做法是在 vue.config.js 配置接口转发:
// vue.config.js
module.exports = {
devServer: {
port: 8080,
proxy: {
// 所有 /api 开头的请求都转发到后端
'/api': {
target: 'http://localhost:8081',
changeOrigin: true
}
}
}
}
这段配置解决前端到后端的跨域。但注意,它只转发了 /api 开头的请求。如果视频地址是 http://localhost:8081/files/xxx.m3u8 ,播放器直接拿这个地址去播,请求不会经过 /api ,跨域问题照样存在。
解决视频资源跨域,常见做法是后端映射一个 /files/** 静态资源路径,让前端 devServer 也把它转回 8081。后端写法:
// WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String location = "file:D:/video-repo/";
registry.addResourceHandler("/files/**")
.addResourceLocations(location);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("http://localhost:*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(3600);
}
}
allowedOriginPatterns 是 Spring 5.3 之后的写法;如果你的 SSM 还是 Spring 4,就换成 allowedOrigins ,把允许的源地址写全。
遇到播放器黑屏,不要马上怀疑组件。先打开浏览器 Network 面板看两类请求:第一, xxx.m3u8 返回 200 还是 403;第二,列表里的 .ts 请求是否被 CORS 拦截。如果 .ts 请求在 Console 里报跨域,就在后端给 /files/** 加 Access-Control-Allow-Origin 。如果 m3u8 能打开,ts 一直 403,多半是目录权限或鉴权接口问题。这个排查顺序能省下不少玄学调试时间。
4. SSM接口层避坑:分页、事务与MySQL连接池的5个血泪经验
骨架搭好之后,真正决定项目能不能演示的是接口层。这个章节写的是我在类似项目里碰到次数最多的几类问题,每一条都是“现象 → 原因 → 解决”完整链路。
4.1 分页接口翻车:pageNum 从 0 还是 1 开始,total 为什么对不上
现象:视频列表第一页数据不对,前端传 current=1 ,后端用 PageHelper 分页,结果返回的列表像是第二页的内容,但 total 又看着正常。
原因:PageHelper 的页码从 1 开始,Element UI 的 Pagination 组件 current 也从 1 开始,两边本来是对得上的。问题出在有人“好心”在 Service 里写了一句 pageNum - 1 ,理由是 MySQL LIMIT 从 0 开始。结果前端第一页实际上被减成了第 0 页,数据错位。
还有一个更隐蔽的问题: PageHelper.startPage() 和真正的 Mapper 查询之间不能插别的查询。看下面这段错误示例:
// 错误:startPage 和 mapper 中间插了另一个查询
PageHelper.startPage(pageNum, pageSize);
videoMapper.selectVideoById(1); // 这个查询会把分页吃掉
videoMapper.selectVideoWithCategory(); // 这里已经不分页了
startPage 设置的 ThreadLocal 状态,会在下一次 MyBatis 查询时被消费。中间一旦插入别的 Mapper 查询,分页就作用到错误的对象上,列表不翻车才怪。
正确写法是:
public PageInfo<VideoVO> pageVideos(Integer pageNum, Integer pageSize) {
// startPage 后紧跟真正要分页的查询
PageHelper.startPage(pageNum, pageSize);
List<VideoVO> list = videoMapper.selectVideoWithCategory();
return new PageInfo<>(list);
}
前端传什么,后端就透传什么,pageNum 默认值给 1。不要自己加偏移量,也不要为了兼容 Element Pagination 做额外转换。分页逻辑的规则统一后,前后端都轻松。
4.2 @Transactional 事务失效:private方法、异常被catch、rollbackFor
现象:后台批量更新视频状态时,主表改了,关联的记录没改,而且控制台没报错。
原因:Spring 事务基于 AOP 代理,只有外部调用经过代理时才会生效。 @Transactional 加在 private 方法上,代理根本不会拦截;加到 public 方法上,但方法内部通过 this.xxx() 自调用,也不会经过代理;还有一种常见情况是方法里把所有异常都在 catch 里吞掉,事务没有回滚机会。
另外,Spring 默认只对 RuntimeException 回滚,对 checked exception 不回滚。如果你抛了一个 Exception ,事务照样提交。用 @Transactional 不加 rollbackFor = Exception.class ,等于给事务留了后门。
正确的播放记录保存写法:
@Service
public class PlayRecordServiceImpl implements PlayRecordService {
@Autowired
private PlayRecordMapper playRecordMapper;
@Autowired
private UserService userService;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveProgress(Integer userId, Integer videoId, Integer progressSeconds) {
// 写入播放记录
playRecordMapper.upsertPlayRecord(userId, videoId, progressSeconds);
// 假设还要更新用户连续观看天数,这一步失败时整个事务一起回滚
userService.updateUserStat(userId);
}
}
这里两个写操作必须在一个事务里。 saveProgress 方法是 public,由 Controller 调用,能正常触发代理。 rollbackFor = Exception.class 保证任何异常都回滚。如果你看过一些 java 面试八股文,会发现“Spring 事务失效场景”几乎都对应这几条,项目中真正改一遍比背十遍强得多。
如果事务加了但建表用的是 MyISAM 存储引擎,事务也不会生效。InnoDB 才支持事务,MyISAM 在并发插入时还会整表锁死,毕设项目务必统一用 InnoDB。
4.3 播放记录并发写入:重复进度与锁等待超时
现象:用户拖进度条,前端快速上报多次进度,后端报 Lock wait timeout exceeded; try restarting transaction ,或者记录表里同一个用户同一视频出现两行。
原因:应用层用“先 select,再 insert,再 update”的方式来写播放记录,两个会话同时 select 都没找到记录,都走 insert,重复行就进来了。锁等待则是更新同一行时,一个事务持锁太久,另一个事务超过 innodb_lock_wait_timeout 默认的 50 秒。
解决:表加唯一索引,写入用 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE 一条语句完成,避免先查再插的竞态。MyBatis 的 Mapper XML 写成这样:
<!-- PlayRecordMapper.xml -->
<insert id="upsertPlayRecord">
INSERT INTO tb_play_record(user_id, video_id, progress_seconds)
VALUES (#{userId}, #{videoId}, #{progressSeconds})
ON DUPLICATE KEY UPDATE
progress_seconds = VALUES(progress_seconds),
update_time = NOW()
</insert>
这段 SQL 依赖 uk_user_video 唯一索引。命中重复键时,MySQL 执行 update,否则执行 insert。高并发下两条插入请求同时到达时,InnoDB 会锁住唯一索引节点,其中一条等待,另一条提交后自动触发 update,不会插入重复数据。
提示:MySQL 8.0.20 开始
VALUES()函数标记为弃用,新项目可以用AS new_row语法替代;如果 MySQL 版本是 5.7,VALUES()完全没问题。还有一点,ON DUPLICATE KEY UPDATE返回 1 表示插入成功,返回 2 表示更新成功,返回 0 表示值没变化,别拿它当写入成功与否的唯一判断。
4.4 数据库连接池参数没调:连接数打满后系统直接瘫痪
现象:单用户操作完全正常,用 JMeter 并发二三十个请求后,后端报 Connection is not available, request timed out ,数据库端也出现 Can't connect to local MySQL server through socket '/tmp/mysql.sock' 。
原因:连接池最大连接数配置过高或不合理,Tomcat 线程抢不到连接;更常见的是应用每请求通过 DriverManager.getConnection() 新开连接,用完不关,把 MySQL 连接数打满。还有 Linux 下把 jdbc url 写成 localhost:3306 ,JDBC 会尝试走 unix socket 而不是 TCP,导致 socket 相关报错。
解决:用 Druid 连接池并在 application.properties 里明确限制:
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
spring.datasource.druid.max-wait=10000
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.validation-query=SELECT 1
参数含义: max-active 是连接池最多拥有的活动连接数,毕设场景 20 足够; max-wait=10000 是拿不到连接时最多等 10 秒,超过就报错; validation-query 用于探活,连接归还时执行 SELECT 1 ,避免把已断掉的连接发给业务。
如果项目不是 Spring Boot,是传统 SSM 的 XML 配置,Druid 的配置思路一样,只是换到 spring-datasource.xml 里。
连接池报错后先看数据库端:
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';
如果连接数已经打满,多半是代码里没用 Spring 管理连接,或者 finally 里没关连接。手动编写 JDBC 是最常见的原因: Connection 、 Statement 、 ResultSet 三个资源一个都不能漏。用 MyBatis 配合 Spring 管理后,事务结束连接会归还池子,这个问题基本就消失了。
5. 验收与升级:给毕设加一条可复现的验证路径
5.1 三个验证点
写完不是结束,要能证明系统是“跑得通”而不是“能启动”。我习惯准备三组验证。
第一组是接口回归。用 curl 把登录、列表、详情、保存进度四个接口按顺序调一遍:
curl -X POST http://localhost:8081/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
curl "http://localhost:8081/api/video/list?pageNum=1&pageSize=5"
确认返回结构里 code 、 data 、 total 都有值。第二组是播放链路,打开 Chrome Network,确认 .m3u8 和 .ts 都返回 200;拖动一次进度条,看 /api/play/record 收到请求。第三组是数据库一致性验证,连续播放同一视频两次,查 tb_play_record 只有一行:
SELECT COUNT(*) FROM tb_play_record WHERE user_id = 1 AND video_id = 2;
再用 EXPLAIN 看索引是否被命中:
EXPLAIN SELECT * FROM tb_play_record
WHERE user_id = 1 AND video_id = 2;
type 不是 ALL 就说明走了索引。
5.2 低成本加分方向
如果基础功能已经跑通,想再给自己加一点工作量,我推荐三个方向。第一,把播放进度存 Redis,定期落库,下次打开视频弹窗问“是否从上次进度继续播放”,这个功能很小但演示效果很好;第二,给视频表加一个 play_count 字段,在后端播放接口里做自增,首页按播放量排序;第三,把视频地址从本地路径改成对象存储 URL,数据库只存 key,这部分改动只涉及配置层,不影响表结构。
我做这类项目有个习惯:不管源码包是谁写的,拿到手第一件事是清掉示例数据,然后按上面三组验证自己跑一遍。跑不通就拆开查,查不清楚就重构;跑得通,最后至少挑一个功能亲手改一轮。能讲清楚“为什么这样写”的地方,才是你真正得到的东西。希望你手里的在线视频点播系统,不只是过查重的论文,而是一个能让你站稳脚说话的完整作品。希望帮到你。
更多推荐
所有评论(0)