做在线视频网站开发这类项目我最深的体会是它不像普通管理系统那样把增删改查写完就收工而是从视频上传、转码、存储到播放鉴权一整条链路里挤满了容易被低估的细节。这篇文章主要围绕我实际完成的基于 Java SSM Django 的在线视频网站项目展开把编码选型、数据库设计、视频处理、播放器接入、后端接口实现和最终交付的经验完整拆开来讲。适合正在做视频平台开发、流媒体服务、内容管理系统或者准备用 SSM / Django 做全栈项目作为毕业设计或求职项目的朋友收藏参考。1. 这个项目的核心闭环选题与技术栈拆分1.1 视频网站的业务闭环七大核心模块动手之前我先把视频网站的业务闭环完整捋了一遍因为“视频网站”这四个字听起来熟悉但真正落到功能清单上比想象中要多很多。一个能拿得出手的在线视频系统至少包含七个闭环环节用户注册登录、视频上传、服务端转码与存储、前台分类浏览与搜索、在线播放器播放、评论互动、后台内容管理。其中用户注册登录是基础我用 SSM 的 Shiro 权限框架来处理Session 管理登录态管理员和普通用户分角色控制菜单与接口权限。视频上传环节要解决文件怎么到服务器、大文件怎么断点续传服务端转码环节则要处理不同格式的兼容性比如用户传一个 MKV 或者 MOV浏览器未必能直接播放这就得有转码服务兜底。存储环节更要规划清晰原始文件和转码产物要分开目录避免后续清理困难。前台浏览与搜索相对直接分类列表加关键字检索就能撑起基础体验。在线播放器环节要处理的是 M3U8 和 MP4 的选择、播放进度记录、清晰度切换。评论互动是内容生态的重要组成包含评论列表、楼中楼回复和点赞。后台内容管理则是对视频进行上下架、分类调整、数据统计。这些环节单独拆开看都不算难但串在一起的时候任何一个环节出了问题都会直接反映到用户看到的页面上。所以我在规划阶段就明确了一点先做出一条完整可跑的链路再去优化单个环节的体验。1.2 JavaSSM 和 Django 是如何分工的项目标题里既有 JavaSSM又有 Django很多人第一反应是“是不是写错了”。其实我最终采用的是双服务协作结构SSM 作为主后端负责核心业务接口和视频处理任务的调度Django 作为辅助服务负责视频内容管理和部分数据展示场景。为什么这样拆分SSM 的优势在于接口分层清晰、事务控制顺手尤其是视频上传和播放鉴权这类对敏感度有要求的接口我能精确控制每一步逻辑。Django 则胜在 ORM 和自带 Admin 后台视频内容管理偏重 CRUD 和表格展示用 Django 的模型管理后台可以很快搭出一个带搜索、筛选、分页的内容管理界面比从零写一套 SSM 后台页面省不少事。有人会问那直接用 Django 一套做完不就行了也不是不行但项目里如果既有 Java 生态的需求又有 Python 生态的需求双栈配合反而能把各自优势用起来。比如我的视频转码状态通知先由 Java 后端写入 MySQL 任务表Django 通过 ORM 监听任务表中的状态变化把转码成功的记录自动同步到在线视频列表。两边通过数据库作为中间层耦合度很低。1.3 技术选型中的取舍判断我听过不少人在技术选型时陷入“想展示所有技术”的误区最后项目里同时塞了 Redis、Elasticsearch、RabbitMQ、Docker结果每个都用得很浅甚至有些根本没在核心链路里发挥作用。我的建议是选型要为业务闭环服务不要为了技术亮点硬塞组件。在线视频网站的核心痛点是文件上传、视频转码、播放兼容性这三个问题解决好了项目就成功了大半。缓存、消息队列、搜索集群这类组件在数据量没到一定规模之前用数据库加定时任务也能扛住。我做这个项目时只引入了 MySQL、FFmpeg 进程调用和 Redis 缓存其余全靠 SSM 和 Django 自身能力解决。这样项目结构简单清晰部署的时候也不容易被各种中间件问题卡住。2. 数据库与表结构设计视频平台开发的“地基工程”2.1 核心业务表与字段说明在线视频网站的数据库设计可以直接决定后面开发的幸福感。我建表时坚持一个原则业务状态用数字标识大字段独立存储常见查询字段尽量冗余。最终的核心表包括用户表、视频信息表、视频分类表、评论表、点赞表、收藏表和管理员表。用户表是基础除了常规的 id、username、password、email我用一个 role 字段区分普通用户和管理员省去单独建权限关联表。视频信息表是重中之重字段包括 id、title、description、cover_url、video_url、duration、size、category_id、uploader_id、play_count、status、create_time。这里 cover_url 和 video_url 我存的是相对路径而不是完整地址这样换域名或者服务器环境时不用改数据库内容。status 字段用 0 表示待转码、1 表示已就绪、2 表示审核不通过后续视频上下架也通过这个字段控制。评论表采用自关联设计parent_id 为 0 时表示一级评论不为 0 时表示对某条评论的回复这样能轻松实现评论楼中楼效果。点赞表和收藏表都是关联表user_id 和 target_id 建立联合唯一索引防止用户重复操作这是我在实际开发中非常看重的一点因为并发请求下没加唯一索引很容易出现重复数据。2.2 MyBatis 与 Django ORM 的同源化处理因为项目同时用了 SSM 和 Django数据库表就要同时被两套代码访问。我的做法是不分别建表而是以 SQL 建表脚本为准先手动设计好完整的数据库表结构再用 MyBatis 写 Mapper 接口用 Django 的 inspectdb 命令从现有数据库反向生成 models。这里有个实操细节值得分享如果你在 Django 里使用 inspectdb 生成模型生成的 models 文件字段注释可能不完整需要手工补充 verbose_name而 MyBatis 的实体类则建议通过 MyBatis Generator 插件生成生成后把日期类型手动确认一下避免 LocalDateTime 和 Date 混用导致序列化格式不一致。整个过程最忌讳的是在 SSM 里改了一个字段名忘了同步 Django 的 models于是我会把数据库表结构变更记录写进项目的调试文档每次修改都先改 SQL 脚本再同步两边代码。2.3 为查询性能埋的伏笔索引与冗余字段在线视频网站最频繁的查询是分类页的列表查询和搜索页的关键字查询。分类页常见的查询模式是“按视频分类 按发布时间排序 分页”所以我给 category_id 和 create_time 建了联合索引搜索页如果走 MySQL 的 LIKE 查询至少要给 title 建普通索引虽然 LIKE %keyword% 不一定能命中索引但右侧匹配 like keyword% 是可以走索引的。播放量的统计我直接在视频表里放一个 play_count 字段播放接口每次对它加 1而不是每次都 count 播放日志表。这么做虽然牺牲了一点精确性但换来的是列表页展示播放量时零额外开销。对于视频时长、封面、视频地址这些字段查询时几乎每次都用到所以不做额外拆分而视频的原始文件大小和存储路径这类管理端才需要的字段我会把它们放到 video_file_info 表里独立存储让主表保持轻量。3. 从零处理视频内容编码、转码与流媒体服务搭建3.1 视频编码技术的基础认知视频编码技术这块是很多初次接触视频网站开发的人最容易茫然的地方。这里用一个生活化类比解释视频文件就好比快递包裹容器格式是外包装MP4、MOV、AVI 就是不同的包装盒编码格式是盒子里的货物打包方式H.264、H.265、VP9 就是不同的打包法。浏览器能不能直接播放某个视频看的是容器和编码是否被支持。目前兼容性最好的组合是 MP4 容器 H.264 视频编码 AAC 音频编码几乎所有浏览器都能直接播放。而 H.265 虽然在同等画质下体积更小但浏览器支持参差不齐服务器端转码成本也更高在线视频网站的主力转码规格我推荐优先用 H.264。另一个关键概念是 HLS全称 HTTP Live Streaming。HLS 把视频切片成一个个 ts 小文件再生成一个 m3u8 索引文件播放器按需加载分片这样既能实现流畅播放也能方便做多清晰度切换。现代浏览器不能直接播放 m3u8需要借助 hls.js 这样的库把分片拉下来再交给 vídeo 元素播放。3.2 用 FFmpeg 完成转码和 HLS 切片转码和切片我用的工具是 FFmpeg。这个命令行工具独立于 Java 和 Django部署在服务器上之后Java 后端通过 ProcessBuilder 启动子进程调用它。常用的转码命令大概长这样ffmpeg -i input.mkv -c:v libx264 -preset fast -crf 23 -c:a aac -b:a 128k output.mp4如果要把转码产物切成 HLS 分片可以这样ffmpeg -i output.mp4 -profile:v main -level 3.1 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls output.m3u8这里的 -hls_time 10 表示每个切片时长 10 秒-hls_list_size 0 表示不限制列表中的分片数量。实际使用时我还会加 -vf scale1280:720 之类的参数做清晰度降级因为用户上传的源视频可能分辨率很高不同码率产物可以后续用于切换清晰度。Java 端调用 FFmpeg 的代码核心逻辑如下用 ProcessBuilder 而不是直接执行字符串拼接命令避免参数转义问题ProcessBuilder builder new ProcessBuilder( ffmpeg, -i, sourcePath, -c:v, libx264, -preset, fast, -c:a, aac, -b:a, 128k, targetPath ); builder.redirectErrorStream(true); Process process builder.start(); int exitCode process.waitFor(); if (exitCode 0) { // 更新视频状态为已就绪 } else { // 记录错误日志并回滚状态 }命令执行过程中的细节FFmpeg 的日志默认输出到 stderr如果不处理会越积越多我建议重定向到项目日志文件方便排查转码失败原因。3.3 Java 后端和 Django 的流转配合转码完成后原始文件、转码后的 MP4、m3u8 和 ts 分片会落在不同目录我把文件路径规则固定下来原始文件存放在 original/yyyy/MM/dd/ 下转码产物存放在 converted/videoId/ 下。这样既方便按时间归档也能通过 videoId 快速定位某个视频的全部产物。Java 后端负责上传时把原始文件存到指定位置然后插入一条任务记录到 video_task 表任务状态从 0待处理到 1处理中再到 2成功或 3失败。Django 这边通过一个定时脚本或者信号机制扫描任务表状态当状态变为 2 时自动将转码产物信息同步到视频主表把 status 字段从待就绪更新为可播放。这样双服务之间没有直接调用接口只通过数据库状态机协作即便某个服务重启任务也不会丢失。3.4 转码任务的异步化不要让用户干等在线视频网站中用户上传一个视频后不可能在原地等五分钟直到转码完成这是最基础的用户体验问题。因此转码必须走异步流程。在不引入消息队列的情况下我用 Java 的线程池加数据库任务表实现简单的异步队列。ExecutorService executor Executors.newFixedThreadPool(4); executor.submit(() - { // 调用 FFmpeg 转码 // 更新任务状态 });线程池大小我设置为 4因为转码是 CPU 密集型任务线程数超过 CPU 核心数反而会因为上下文切换拖慢速度。启动时把任务表中状态为 0 或 1 的记录重新纳入队列避免服务重启导致任务悬空。这个设计对于中小型项目完全够用如果以后要扩展可以平滑替换成 RabbitMQ 或 Redis 队列。4. 在线播放器播放页与播放鉴权全流程4.1 播放器选型与接入方式播放器这块我对比过原生 HTML5 video、video.js、ckplayer 和阿里云播放器几个方案。原生 video 最简单但样式和控制逻辑要自己写很多ckplayer 对国内视频格式兼容性较好不过包体较老video.js 插件生态成熟、支持自定义皮肤还能配合 videojs-contrib-hls 播放 HLS 流用起来比较顺。最终我选的是 video.js hls.js 的组合。video.js 负责整体播放器 UI 和交互hls.js 负责把 m3u8 分片拉取后转换为浏览器可播放的 fMP4 流。这样用户在播放列表页点击视频时前端拿到的是一个 m3u8 地址而不是直接把 MP4 整段塞进 video 标签里加载速度会好很多也方便后续扩展多清晰度切换。4.2 播放页面的设计与接口交互播放页主要分成三个区域顶部是播放器容器左侧或下方是选集列表右边是相关推荐和评论区。播放页初始化时前端会调用一个视频详情接口返回的信息包括视频标题、封面、播放地址、点赞数、评论列表等。播放地址的获取我单独拆了一个接口 playUrl而不是和详情接口合并因为播放地址需要额外的鉴权处理职责分离后逻辑更清晰。播放进度的保存也是一个容易被漏掉的功能。常规做法是播放器监听 timeupdate 事件每 5 秒向后端发送一次进度更新刷新页面时播放器读取上次进度并 seek 到对应位置。我用 local storage 来存每个视频的进度点只在用户主动退出播放页时同步到后端这样请求数量少很多也避免了频繁写数据库。4.3 防盗链与播放权限的轻量方案在线视频网站如果完全不设防播放地址很容易被爬走或盗用别人会把你的视频地址直接嵌入到自己的网站里白白消耗你的带宽。我做了一个轻量级的播放鉴权方案核心思想是签名 URL 加过期时间。后端在返回播放地址时不直接返回真实地址而是返回一个带 token 的签名地址格式类似 /play/{videoId}?tokenxxxexpire1234567890。token 的生成规则是对 videoId、过期时间戳和预设密钥做 MD5 拼接播放器请求播放地址时后端先校验 token 是否正确以及是否过期。同时我在 Nginx 层做了一层 Referer 校验只有本站域名发来的请求才允许访问视频文件目录。要注意的是签名防盗链不能彻底防住视频被下载因为播放器拿到分片后用户还是能用抓包工具获取 m3u8 地址。更严格的做法是视频切片内容加密和动态 m3u8 地址但那些属于商业级方案毕业设计或中小型项目做到签名地址加 Referer 校验已经能挡住绝大多数盗链和爬取行为。5. SSM 后端核心接口的实现与优化5.1 视频上传链路从 MultipartFile 到分片上传视频上传接口是用户进入内容创作的第一步也是坑最多的地方。小视频直接通过 Spring MVC 的 MultipartFile 接收就能搞定但短视频平台里 H5 录制或者文件较大时单次上传很容易超时还会占用大量内存。我实现的上传方案分两种文件小于 200MB 直接走单次上传接口大于 200MB 走分片上传。分片上传流程是前端先把文件按固定大小切成片例如每片 5MB然后逐片上传后端收到后把分片按顺序写入临时目录全部上传完成后调用合并接口把分片文件按序号拼接成一个完整文件。合并完成后计算文件的 MD5 值和上传前前端计算的值做比对确认文件没有丢失或损坏。PostMapping(/upload/chunk) public Result uploadChunk(RequestParam(file) MultipartFile file, RequestParam(identifier) String identifier, RequestParam(chunkNumber) Integer chunkNumber) { String dir uploadPath / identifier; File targetDir new File(dir); if (!targetDir.exists()) { targetDir.mkdirs(); } File targetFile new File(dir / chunkNumber .part); file.transferTo(targetFile); return Result.success(); }合并接口用 RandomAccessFile 按分片顺序写入相比先读取全部分片到内存再写内存占用会小很多。实际测试下来一个 1GB 的视频文件分片上传加合并整个过程内存峰值控制在 300MB 以内效果还是不错的。5.2 评论、点赞、收藏的并发与一致性处理评论和互动是视频网站的活跃度来源这里的并发控制值得单独说。点赞接口最容易出问题用户在快速点击时会连续发送多条请求如果在数据库层面没有唯一约束就会出现一个用户给同一条视频点多个赞的数据。我在点赞表上建了 (user_id, video_id) 联合唯一索引数据库层面先挡住重复同时在 Service 层加幂等校验先查是否存在该记录存在则直接返回已点赞不存在才插入。评论的计数如果不加控制查询评论总数时会消耗不少性能。我选择在视频主表维护一个 comment_count 字段每次插入一条顶级评论或回复时对应的评论计数加一删除评论时减一。这样查询列表页时不需要 count 评论表。评论列表本身的分页我按时间倒序使用 limit 查询并在 parent_id 0 的一级评论上做了分页二级回复默认最多显示 3 条这既保证了展示效果也避免了 MySQL 深度分页的性能问题。5.3 搜索、排行榜与相关推荐在线视频网站最常见的三个附加功能是搜索、排行榜和相关推荐。搜索的基础实现我用的是 MySQL LIKE 查询虽然性能上限不高但中小型项目足够用SELECT * FROM video WHERE title LIKE CONCAT(%, #{keyword}, %) AND status 1 ORDER BY create_time DESC LIMIT #{pageSize} OFFSET #{offset};排行榜功能我按不同维度做播放量榜直接按 play_count 排序热门榜按最近 7 天的播放增长量排序。为了不每次查询都扫描全表我会在 Redis 里维护一个播放量 Top 20 的缓存定时从数据库重建。相关推荐则简单通过同分类 标签匹配实现随机取同分类下播放量较高的若干条视频用 ORDER BY RAND() 在数据量大时效率不高所以我用了 ORDER BY id DESC LIMIT n 取最近发布视频来替代。5.4 Django 管理后台对内容管理的加持Django 在这个项目里负责内容管理最大的价值在于把后台页面开发成本降到最低。我在 Django 侧定义了 Video、VideoCategory、Comment 三个模型注册到 Admin 后台然后通过自定义 Admin 类开启列表页搜索、筛选和富文本编辑。后台管理员可以对视频进行上下架操作可以修改视频分类也可以查看最近评论并删除违规内容。截至这一步整个项目管理侧的功能就完整了SSM 负责对外 APIDjango 负责管理页面两边通过同一张 MySQL 表协作。前端代码直接放在 SSM 的 webapp 目录下用 Thymeleaf 渲染页面Django 只提供后台地址不参与用户侧页面的渲染这样权限边界很清晰。6. 调试、部署与交付的完整复盘6.1 调试文档不是记流水账应该包含四类重点这类项目交付时通常会带一份调试文档但很多人的调试文档其实就是把数据库初始化步骤复制粘贴一遍对使用者帮助有限。我从实际维护经验出发把调试文档设计成四个部分环境依赖清单、数据库初始化步骤、常见报错对照表、启动验证流程。环境依赖清单要写清楚 JDK 版本、Maven 版本、Python 版本、Django 版本、MySQL 版本、FFmpeg 安装路径因为这些版本之间可能互相不兼容。常见报错对照表是最有价值的部分我记录了几个实际踩过的坑MySQL 8 的驱动类名是 com.mysql.cj.jdbc.Driver而 5.x 用的是 com.mysql.jdbc.DriverDjango 访问 MySQL 需要提前安装 mysqlclient 并确保系统有 MySQL 开发头文件Java 调用 FFmpeg 时如果环境变量没有配好ProcessBuilder 会直接抛找不到程序的异常。启动验证流程要给出“从零到能看”的最短路径先启动 MySQL 并导入初始数据再启动 Django 后台最后启动 SSM 服务访问前台首页确认视频列表加载点进详情页确认播放器能放 m3u8 流。这一串操作如果照着文档走一遍能通说明交付物是可靠的。6.2 部署阶段最容易被卡住的三个位置第一个卡点是 Nginx 的静态视频文件路径配置。视频文件往往很大Tomcat 默认的静态资源处理性能和并发能力都不够我选择用 Nginx 直接 serve 视频目录但路径 alias 配置很容易写错导致播放 404。核心写法是location /video/ { alias /data/www/converted/; expires 1d; add_header Cache-Control public; }注意 alias 结尾的斜杠不能丢。第二个卡点是 MySQL 的 max_allowed_packet 默认值只有 4MB 或 64MB上传视频时如果直接走数据库存储元数据偶尔会出现 MySQL server has gone away我虽然没有把视频文件存进数据库但分片上传的临时文件校验和任务表字段如果设得过大也会触发这个报错需要把 max_allowed_packet 调到 128MB 并重启服务。第三个卡点是服务器时间和本地时间不一致签名 URL 的过期校验一旦使用服务器当前时间客户端请求可能因为时区差异被判过期我这里统一约定所有时间戳都按秒级 Unix 时间戳传输避免 Date 字符串的格式差异。6.3 从“源码文档讲解”到真正能验收的收尾站在项目交付的角度源码、文档、调试文档和讲解本质上是在说同一个故事的不同侧面。源码展示的是实现能力文档展示的是梳理能力调试文档展示的是工程素养讲解则是在证明这一切是真正自己做过而不是拷贝来的。我自己在准备讲解部分时会从三个角度去讲这个在线视频网站项目第一句讲技术亮点“基于 JavaSSM 和 Django 双服务架构实现视频上传、转码、播放和内容管理的完整闭环”第二句讲遇到的最大难点“大文件上传和服务端转码过程中的状态管理”第三句讲怎么解决的“用分片上传加线程池异步转码加数据库任务状态机把用户等待时间降到最低”。这三句话几乎把整个项目的骨架都覆盖了后面不管是现场演示还是回答细节追问都有了落点。我在实际交付这个项目时还有一个体会项目做得好不好不在于用了多少前沿框架而在于每个环节是不是真的想通并处理干净了。在线视频网站开发恰恰是这样一个能把各种基础技能聚在一起的题目做完之后Java 的并发编程、数据库设计、Python 的快速开发、Web 前端的播放器集成全都有了真实场景下的练习。最后再分享一个小技巧把 FFmpeg 的转码日志按日期分割保存排查线上问题时你会感谢当初这个决定。