做毕设或者自用项目选到“基于SSM的在线视频网站”这个题目的人,我猜你多半是计算机相关专业的,或者正在自学Java Web想找个综合性项目练手。这个题目看起来传统,但说实话,它比那些花里胡哨的“微服务秒杀系统”更适合拿来练基本功,也更容易在答辩或者面试的时候讲清楚。
SSM就是Spring + SpringMVC + MyBatis这套经典的Java后端组合。这东西放今天依然不过时,很多老牌企业内部系统还在跑着这套架构。在线视频网站这个需求正好能把这套框架的所有核心能力都用上:Spring管对象和事务,SpringMVC接请求和返回视图,MyBatis和数据库打交道。再加点前端页面,一个完整的全栈项目就出来了。更重要的是,视频网站天然自带用户系统、内容管理、文件上传、播放展示这些模块,每一个都有明确的业务逻辑,拿来练手或者做毕设,都不愁没东西写。
我这次打算从零开始把项目的设计思路、核心实现、常见坑都过一遍,重点放在那些你照着教程敲代码时最容易忽略的细节上。
1. 需求拆解与整体架构设计
1.1 视频网站到底在做什么
在动手写代码之前,先把需求想明白,这是决定项目质量的关键一步。市面上能叫“视频网站”的产品很多,但从教学和毕设角度,核心需求一般可以收敛成三大块:
第一块是内容管理。视频的上传、编辑、上下架、删除,这个模块决定了你的网站有没有内容可看。第二块是用户体系。注册、登录、权限控制,管理员和普通用户要有不同的操作权限。第三块是交互和展示。浏览视频列表、视频详情页、播放页面、搜索,还有评论和弹幕这些轻社交功能。
很多初学的人容易一上来就想着做推荐算法、做高清转码、做分布式存储,这些对于SSM项目来说都属于过度设计,很容易把自己埋进坑里。最务实的做法,是先把上面三大块做得完整、能跑通,然后再考虑优化和扩展。
1.2 SSM架构里每一层到底该放什么
SSM架构的分层思想其实很好理解,我把它们对应到实际项目里去解释:
- Controller层(SpringMVC):负责接收前端请求,解析参数,调用Service,把结果返回到视图或者以JSON格式返回。这一层我把它理解成“前台接待员”,只负责接单和回话,不负责干活。
- Service层(Spring):存放业务逻辑。比如用户注册时要校验用户名是否已存在、加密密码、创建默认头像,这些判断和处理都放在这一层。Controller不直接操作数据库,必须通过Service转一手,这个习惯要养成。
- Dao层(MyBatis):就是简单的增删改查,对应数据库表格。这一层尽量不要写业务判断,保持纯粹的SQL操作。
- 数据层:MySQL数据库,存放用户表、视频表、评论表、分类表这些核心数据。
这种分层带来的最大好处是解耦。数据库从MySQL换成别的,最多改Dao层;页面从JSP换成Vue,Controller层接口不变就行。项目后期加功能时,不用把老代码翻个底朝天,这种维护成本上的优势在写大作业时可能感觉不明显,但在真实项目里就是决定开发效率的关键。
1.3 环境选型和版本搭配
关于环境版本,这里有一个很重要的经验:不要盲目追新版本。网上教程多、踩坑资料多、和你项目环境匹配的版本才是最好的版本。
我用过比较稳的一套组合:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 稳定,SSM生态最成熟的版本 |
| Maven | 3.6+ | 依赖管理必备 |
| MySQL | 5.7 或 8.x | 建议本地装好,字符集utf8mb4 |
| Tomcat | 8.5 | 支持Servlet 3.1,兼容性最好 |
| Spring | 5.2.x | 不要用太老的4.x |
| MyBatis | 3.5.x | 和Spring整合用mybatis-spring 2.x |
有一点容易踩坑:Spring 5 要求JDK8以上,如果你还在用JDK7,那得先把JDK升上来。另外,如果把Spring升到6.x,那就必须配合Spring Boot 3和JDK17,这对SSM传统项目没必要,反而会引入一堆兼容问题。
2. 数据库设计与核心表结构搭建
2.1 需要哪些表,怎么设计才合理
表结构设计是别人看不到但你一写代码就感觉得到的地方。设计得好,写Mapper的时候行云流水;设计得不好,后期加个功能可能要把表拆了重建。
我的建议是,最开始就规划好以下六张表:
用户表:用户id、用户名、密码、昵称、头像、邮箱/手机号、角色(0管理员/1普通用户)、注册时间、状态(是否封禁)。密码字段别用varchar存明文,后面我会细说加密方案。
视频分类表:分类id、分类名、父分类、排序。这个表是为了模拟“电影区”“动漫区”“科技区”这种板块结构,做导航菜单时直接用。
视频信息表:视频id、标题、描述、封面图路径、视频文件路径、所属分类id、上传者id、播放量、状态(公开/私密/审核中)、上传时间、更新时间。这是最核心的表,所有展示信息都从这里取。
评论表:评论id、视频id、用户id、评论内容、评论时间、父评论id。父评论id字段是为了支持楼中楼回复,如果没有这个需求可以去掉。
弹幕表:弹幕id、视频id、用户id、弹幕内容、弹幕出现时间点(以秒为单位)、发送时间。播放器和弹幕列表就靠这个表关联。
收藏表:收藏id、用户id、视频id、收藏时间。体现“我的收藏”这个功能时要用。
2.2 建表语句中容易忽略的坑
建表时几个细节值得特别留意。
第一个是字符集。建库时必须指定utf8mb4,因为utf8只支持3个字节的字符,遇到生僻字或者emoji表情就会报错或者存成乱码。视频网站这种用户内容型的项目,弹幕和评论里出现emoji的概率还是很高的,所以建库时我会写成:
CREATE DATABASE video_site DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第二个是播放量的字段设计。很多人喜欢用int型存储,但我会建议直接用bigint。原因很简单,万一哪天需要做刷量测试或者数据迁移,int上限21亿听起来挺多,但对于一个有野心的项目还是图个安心更好。更重要的是,做热门视频排序时SQL就是ORDER BY play_count,这种高频查询用bigint也没有额外负担。
第三个是逻辑删除的问题。视频表里我加了一个status字段,而不是直接DELETE。原因很实在:你做后台管理的时候,很可能需要“下架”一个违规视频,而不是“彻底删除”它。用stauts=0表示下架,stauts=1表示正常,这样既能维护内容安全,又保留了数据回溯的可能。
2.3 视频文件存储:数据库只存路径
这里有个新手很容易搞错的概念:视频文件本身怎么存?是把视频文件和信息都塞进数据库吗?
答案是:数据库只保存文件路径,视频文件放在磁盘目录里。视频文件动不动几百MB,MySQL虽然支持存二进制大对象(BLOB),但当你把数据量一放大,数据库会变得极其臃肿,读写性能也肉眼可见地下降。
我的方案是,配置一个虚拟目录映射到磁盘目录。比如Tomcat的conf/web.xml里加一个映射,或者用SpringMVC的ResourceHandler:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把虚拟路径 /video/** 映射到本地的 /data/videos 目录 registry.addResourceHandler("/video/**") .addResourceMapping("/data/videos/"); } }前端访问视频时就是http://localhost:8080/video/20240601/abc.mp4,数据库里只存/20240601/abc.mp4这段相对路径。这种做法的好处是,视频文件增加删除都不影响数据库性能,备份时也可以单独打包文件目录,灵活性高很多。
3. 核心功能模块的实现与关键逻辑
3.1 用户注册登录:安全细节不能省
用户模块是整个项目里安全要求最高的一块,容不得马虎。
注册这块,两个重点必须处理到。首先是用户名唯一性校验,前端要校验,后端Service层还要再校验一次。前端校验只是提升用户体验,后端校验才是真正的安全防线,因为接口可以被绕过直接调用。我曾经就是在后端漏了这层校验,结果测试时创建出两个一模一样的用户名,搞得登录时数据错乱。
其次是密码加密。明文存储密码是绝对的大忌,一旦数据库泄露,用户所有信息就等于裸奔。SSM项目里我不会用MD5这种可被彩虹表破解的过时方案,而是用Spring Security里自带的BCryptPasswordEncoder,或者最少也要用加盐的MD5。
BCrypt用起来其实很简洁:
// 加密 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encodedPassword = encoder.encode(user.getPassword()); user.setPassword(encodedPassword); // 校验 boolean matches = encoder.matches(inputPassword, storedHashPassword);登录之后,我用Session保存用户信息,然后在拦截器里做登录状态校验。不管是游客访问还是用户访问,需要登录的接口一律拦截。比如:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object userId = session.getAttribute("userId"); if (userId == null) { // 重定向到登录页面 response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }这个拦截器就是整个网站的“门卫”,没有登录凭证的请求一律不允许进入受保护页面。
3.2 视频上传:从文件到地址的完整链路
视频上传是视频网站里最消耗精力的一部分。很多人觉得这不就是form表单提交一个file字段吗?但在实际项目里,我没少踩这里的坑。
前端表单需要配置enctype="multipart/form-data",否则文件数据传不到后端。以最简单的JSP页面为例:
<form action="${pageContext.request.contextPath}/video/upload" method="post" enctype="multipart/form-data"> <input type="text" name="title" placeholder="视频标题" /> <input type="file" name="videoFile" accept="video/*" /> <input type="submit" value="上传" /> </form>后端接收需要SpringMVC的MultipartFile接口。下面我贴一段关键代码:
@PostMapping("/upload") @ResponseBody public Result uploadVideo(@RequestParam("videoFile") MultipartFile file, @RequestParam("title") String title, HttpSession session) { // 文件判空 if (file.isEmpty()) { return Result.error("上传文件不能为空"); } // 大小限制 (配置中默认为100MB) if (file.getSize() > MAX_UPLOAD_SIZE) { return Result.error("文件超过大小限制"); } // 生成唯一文件名,防止覆盖 String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString().replace("-", "") + ext; // 按日期分目录存储 String dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date()); String realPath = "D:/data/videos/" + dateDir; File dir = new File(realPath); if (!dir.exists()) { dir.mkdirs(); } // 文件落盘 file.transferTo(new File(realPath + "/" + fileName)); // 保存数据库记录 videoService.saveVideo(title, "/" + dateDir + "/" + fileName, session); return Result.success("上传成功"); }这个过程中有两个容易踩的细节。第一个是文件名必须用UUID重命名。比如你上传一个别人也上传了同名文件,如果你的存储逻辑是直接覆盖,那等十分钟后回来看视频,可能已经播放成另一个人的内容了。另一个细节是按日期分目录存储。我见过有的项目把所有视频直接丢在同一个目录下,文件少的时候还好,文件一旦上千,同一个目录的遍历速度就会肉眼可见地下降。
还有一个比较实际的问题是具体“视频格式兼容性”。不同浏览器对视频格式的支持是不一样的,MP4通常是最稳妥的选择。上传时最好在后端限制下扩展名,比如只允许.mp4、.webm、.ogv,并且在播放页用HTML5的video标签。要再加一层保险,可以用FFmpeg把非MP4格式的视频统一转码成H.264编码的MP4,不过这个属于扩展功能,可以作为答辩加分项。
3.3 视频播放页面:播放量、评论、弹幕一起上
播放页面是整个网站信息密度最高的页面。除了video标签以外,展示视频信息、统计播放量、拉取评论列表,这三个操作通常就在一个请求里完成。
播放量统计这里有一个经验:不要在用户每次刷新页面时都无脑UPDATE一次播放量。这样会导致两个问题,一是数据库写入压力大,二是用户可以疯狂刷新来刷高播放量。我采用的方案比较简单可靠:Session里记录一个flag,同一用户同一视频,在有效期内只增加一次播放量。比如:
// 伪代码 Object flag = session.getAttribute("played_" + videoId); if (flag == null) { session.setAttribute("played_" + videoId, "1"); videoMapper.increasePlayCount(videoId); }评论功能则是播放页的一个标配。评论列表用分页加载就行,每条评论要带上用户的昵称和头像,这就要用到三表联查。写Mapper的时候,多表关联查询的SQL要仔细点,注释写清楚字段来源,不是简单的select *就完事。
弹幕的实现思路也不复杂。前端在播放器上创建一个透明的div画布,然后把用户发送的弹幕内容通过Ajax提交到后端存储;播放时每秒钟去数据库取一次当前时间点附近的弹幕,然后渲染到画布上。这个功能做出来视觉效果很好,答辩时有很强的展示效果,而且代码量不大,非常适合作为加分项。
3.4 MyBatis核心操作与动态SQL的巧妙应用
在所有技术点里,MyBatis是我最想多说两句话的地方,因为很多教程都只教了最基本的selectById、insertUser,但实际项目里远远不够用。
比如视频列表页的搜索和筛选功能,就是一个标准的动态SQL场景。用户可能按标题模糊搜索,也可能按分类筛选,这两种情况还可以同时出现。如果用硬编码的方式把所有可能情况都写一遍SQL,代码会冗余到没法看。正确解法是用MyBatis的<if>标签:
<select id="searchVideos" resultType="Video"> SELECT * FROM t_video WHERE status = 1 <if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="sortType == 'hot'"> ORDER BY play_count DESC </if> <if test="sortType == 'new'"> ORDER BY create_time DESC </if> </select>这种写法的好处非常明显,将来加新筛选项,只需加一段<if>标签,不影响已有逻辑,也不影响SQL执行效率。这就是动态SQL的威力。
另外一个细节是#{}和${}的区别。在搜索和排序这两种场景要非常谨慎:#{}是预编译参数,会生成占位符,永远安全;而${}是字符串拼接,只适合表名/列名这种没法占位的地方,但也有SQL注入风险。排序字段如果用${},一定要后端先做白名单校验。我自己的习惯是,能用#{}就绝不用${}。
4. 前后端联调中的经典Bug与排查经验
4.1 前端访问不到后端接口的排查路径
SpringMVC写好后,我遇到的第一个高频Bug是:前端页面能正常打开,但是一提交表单就报404或405。
排查这个问题的路径是有顺序的。第一步看控制台日志,服务器有没有收到请求。如果没收到,那多半是URL路径写错了或者前端请求的上下文路径缺失。在JSP里,${pageContext.request.contextPath}这个前缀必须加上,否则部署到非根路径下时,所有请求都会404。
第二步看收到的请求和Controller的方法是否匹配。@RequestMapping里的路径不区分大小写的严格性不同,要注意Tomcat对大小写的敏感度。如果是POST请求,肯定不能配成@GetMapping,前端用@RequestBody传JSON时后端又用的是表单接收,也会报415错误,这属于媒体类型不匹配,是SpringMVC很常见的报错。
第三步看参数绑定。前端传的参数名和后端@RequestParam里的value不一致,就会报“Required request parameter xxx for method parameter type String is not present”。这种问题要养成前后端参数名对齐的好习惯。
下面是几个常见错误和对应的解决方向:
| 症状 | 原因 | 解法 |
|---|---|---|
| 提交表单404 | 路径差contextPath前缀 | JSP页面路径补全ContextPath |
| 报415 | 前端Content-Type和后端接收方式不一致 | 统一JSON或统一表单 |
| 参数为null | name属性不匹配 | 检查前后端参数名 |
| 405 | 请求方式与Mapping不符 | GET/POST改一致 |
4.2 文件上传中文乱码和临时目录问题
SSM项目涉及文件上传时,两个问题非常典型。第一个是中文文件名乱码。如果上传的文件叫“剧集01.mp4”,存下来发现变成了乱码,那要先检查web.xml里的编码过滤器有没有配置。要确保CharacterEncodingFilter配置的是一个setCharacterEncoding("UTF-8")并且把所有URL都包含进去。其次检查Tomcat的server.xml里连接器是否加上了URIEncoding="UTF-8"。
第二个问题更隐蔽,是文件大小超出默认限制。SpringMVC的默认文件上传限制是1MB,上传稍大一点的视频就会直接抛异常。要在Spring配置里把限制调大:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 最大上传大小,单位字节,这里设置100MB --> <property name="maxUploadSize" value="104857600"/> <!-- 编码 --> <property name="defaultEncoding" value="UTF-8"/> </bean>4.3 数据库连接断层:MyBatis耗时查询的排查
一个隐蔽但是真实存在的坑,是在一次视频列表查询中发现MySQL连接总是超时。排查半天发现根本不是代码问题,而是MySQL的wait_timeout默认8小时,连接池里的连接过了这个时间就失效了,再取出来用就会报出连接错误。
这个问题的解决方案也很简单:在连接池配置里加上testWhileIdle和timeBetweenEvictionRunsMillis,定期检测断开的连接。这里我明确一下,对于SSM项目,如果你用Druid连接池,配置这段就很好用:
spring: datasource: druid: testWhileIdle: true validation-query: SELECT 1 time-between-eviction-runs-millis: 60000这是一个“不常遇到,遇到就一脸懵”的问题。如果你在正常开发预览视频列表时偶尔出现第一遍访问很慢甚至报错,第二遍就好了,很大概率就是连接池里的死连接没被清掉。
4.4 静态资源加载不出来
启用SpringMVC后,我遇到过本地项目一直加载不了JS和CSS的情况。很多教程里也提到了这个问题,因为前端控制器DispatcherServlet默认拦截了所有请求,包括静态资源。
解决办法是在SpringMVC配置里增加:
<mvc:default-servlet-handler/>以及:
<mvc:resources mapping="/static/**" location="/static/"/>这两句配置做的事情是让Tomcat默认的DefaultServlet处理未被Controller映射的静态资源,避免JS和CSS文件全部返回404。
5. 项目安全加固与性能优化方向
5.1 SQL注入防护
SSM项目使用MyBatis时,SQL注入的防线在Mapper层。我在前面已经强调了#{}和${}的区别,这里再单独强调一次:所有用户输入的字段,一律使用#{}占位,利用JDBC预编译防注入。如果是排序字段这种必须动态拼接的地方,必须先做白名单校验,后端只允许”play_count”、”create_time”等固定值进入${}。
5.2 XSS攻击防护
视频网站因为有评论区,天然容易成为XSS攻击的靶子。攻击者可能在评论里提交<script>alert('xss')</script>,如果原样存储,其他用户打开页面就会执行这个脚本,轻则弹窗骚扰,重则盗取Session Cookie。
处理XSS的常规做法是后端在接收评论时进行HTML转义。在JSP页面中,可以用${fn:escapeXml(comment.content)},或者在后端统一通过过滤器对请求参数做转义。前端的富文本输入尤其要注意,能去掉HTML标签的尽量去掉标签,需要保留加粗等格式的话,再考虑白名单过滤方案。
5.3 缓存策略:把热点数据留住
视频网站的首页是会不断被访问的高频接口。每次打开都去查数据库,肯定扛不住。最简单有效的优化,就是给热门列表和分类导航加一层缓存。
在SSM生态中,最容易接入的缓存是Redis。缓存首页热门视频时,可以每10分钟或每次有视频上下架时更新一次缓存,把热点数据长期留在内存里,数据库的压力就会大幅下降。如果不想引入Redis,也可以用Spring Cache配合简单的内存缓存,效果聊胜于无。在答辩时,这个优化点一定要写上,这是很明显能体现工程意识的加分项。
需要说明的是,缓存不是越多越好。评论区数据频繁变动,就不适合长时间缓存。按我的习惯,只有列表类和配置类数据才会考虑缓存,实时性要求高的数据必须走实时查询。
5.4 表单重复提交与接口幂等
用户点上传按钮时稍微卡顿了一下,又习惯性多点了几次,结果后台多了三个相同文件。这是真实存在的尴尬场景。解决思路是在前端加“提交时按钮置灰”,并在后端做一个简单的防重机制。比如上传接口在Redis里记录一个用户维度的短暂锁,5秒内相同用户重复提交直接拒绝。这种设计体现出的工程思维,是答辩时评委愿意听到的内容。
6. SSM项目如何变出亮点,从及格到优秀
6.1 功能亮点:少而精的关键特性
如果时间有限,精力有限,我建议不要贪多。把一组功能做到完整且精致,远比铺开十个半成品要好。
我自己比较推荐的功能组合是“用户体系 + 视频管理 + 播放/评论/弹幕 + 后台管理”。每个功能都做到闭环:能注册、能登录、能断开;能上传、能播放、能下架;能评论、能回复、能删除。整个网站业务流程是通的,这才像一个能真正跑起来的作品。
至于推荐算法、弹幕蒙层、视频转码、点赞排行这些,属于可选加分项。如果你时间充裕,加一个弹幕就足够了,成本不高,视觉效果还突出。
6.2 答辩/汇报时的亮点表达
很多人觉得项目平平无奇,其实不是没亮点,而是不会表达。以下角度都是可以重点展开的:
比如,使用Redis缓存热门视频列表,降低数据库查询压力;使用BCrypt加密算法对用户密码进行哈希存储,保证数据安全;视频文件采用磁盘存储 + 数据库路径映射的结构,避免数据库膨胀;播放量防刷策略保证数据的准确性。
这些点都不算是多么惊天动地的技术,但每一句都能体现你在做项目时想过“为什么”,而不是只写了一段“能跑就行”的代码。在简历上写项目经历时,也应当按照“业务描述 + 技术方案 + 设计理由”的结构去呈现。
6.3 一人维护SSM项目的日常节奏
最后说点实际的。SSM项目跑起来之后,日常维护的活儿并不少。数据库要定期备份,视频文件目录要清理垃圾文件,日志要定期查看有没有异常报错,用户的违规评论要及时处理。如果你是在本地开发,那还需要注意磁盘空间,尤其是视频文件占用增长很快。
这些事儿都是做项目过程中积累下来的“隐形经验”。面试官问项目细节时,你说得出“我定期备份数据库、清理过期视频、监控磁盘容量”这些真实做过的事,比背十篇八股文都有用。
这个SSM在线视频网站,要说难,其实都不难;要说简单,从数据库设计到权限控制再到文件上传,打磨起来又处处是细节。把每一步都做扎实、做完整,你收获的绝不是一个能演示的网页,而是一整套“从需求到落地上线”的思考方式。这个能力,是不管以后用Spring Boot还是换成别的技术栈都一直受用的。