资讯详情

SSM框架在线视频播放网站毕设项目:从零搭建到播放链路全解析

📅 2026/9/30 15:31:43 | 华诺云谱 👁 阅读
SSM框架在线视频播放网站毕设项目:从零搭建到播放链路全解析
又是一年毕设季后台常有人问“想做点带功能、好演示、又不会太难的系统选什么好”。如果你也在java和ssm这套经典技术栈里纠结题目我强烈建议你认真看看“在线视频播放网站”这个方向。它不像图书管理系统那么单薄又比电商秒杀这种高并发课题容易驾驭核心链路包含用户体系、文件上传、视频流式播放、收藏评论、进度记忆演示效果非常直观。这篇文章我会从一个完整落地项目的角度把idea环境下用SSM从零搭建在线视频播放网站的关键设计、表结构、播放接口原理和那些网上搜不到的坑一次讲清楚适合拿去参考复现也适合答辩前自查一遍。1. 选题评估与技术选型SSM视频站在毕设里为什么“含金量适中”1.1 视频网站相比图书商城、后台管理系统赢在哪先聊选题。很多同学第一反应是做商城或后台管理但这类系统的最大问题是“逻辑太透明”——无非是一堆增删改查页面答辩时老师一眼就能看穿深度。视频播放网站不一样它有一个天然的演示高潮当你把鼠标点到视频卡片进入播放页进度条开始走、视频流畅播放出来场内所有人的注意力都会被吸引。观赏性带来的是印象分而印象分在答辩环节往往比代码量更值钱。再从技术覆盖面看视频网站几乎把SSM项目的常见要点全占齐了注册登录涉及SpringMVC的请求校验和会话管理视频上传涉及文件流处理、存储路径设计、格式校验播放页涉及静态资源映射和流式响应播放记录涉及异步请求和数据库更新收藏、评论、分类检索涉及MyBatis动态SQL和一对多关联查询。也就是说做完这一个项目你其实把SSM三个框架的核心用法都过了一遍后续面试被问到MyBatis动态SQL、SpringMVC拦截器、事务传播行为时都能举出真实场景。1.2 SSM vs Spring Boot毕业设计到底选哪个近两年越来越多同学问“现在企业都上Spring Boot了我毕设还用SSM是不是过时了”。我的观点是如果你是零基础或者对Spring底层的配置机制还比较模糊SSM反而是更好的学习载体。为什么因为SSM要求你亲手配置web.xml、spring-mvc.xml、spring-mybatis.xml你会在这个过程中明白DispatcherServlet在哪注册、SqlSessionFactory的Bean怎么创建、Mapper代理是怎么被扫描出来的。而Spring Boot默认配置把这些全藏起来了项目确实更容易跑通但答辩时老师只要追问一句“你的数据源是怎么注入的”你就很难接得住。当然我也见过不少学校明确规定要用Spring Boot的那你可以参照本文的业务拆分与表结构设计把外层框架替换成Spring Boot核心思路完全通用。本文后续内容仍以SSM为主线因为它在IDEA里从零搭建的过程更繁琐也更容易暴露问题把这些处理好了任何框架都难不倒你。还有一点选型细节JDK建议用1.8Tomcat用8.5或9.0MySQL用5.7。这套组合在SSM环境下兼容性最稳网上能查到的报错案例也最多遇到问题不至于孤立无援。IDEA建议用社区版加Tomcat插件的方式或者直接用Ultimate版内置的Application Server集成两个方案我都试过后面会细说。2. 功能模块与数据库设计先想清楚演示路径再动手建表写代码前最重要的一件事不是画架构图而是把“答辩演示时你要点哪些按钮、看到什么效果”列成清单再倒推功能模块和表结构。我当时的演示路径是这样的注册新用户 → 登录 → 首页看到视频分类和最新视频列表 → 点击视频卡片进入播放页 → 视频播放进度条正常走动 → 刷新页面进度记忆还在 → 点收藏、给评论 → 个人中心看到收藏列表和播放历史。这个路径定下来功能边界立刻清晰了。2.1 模块边界划定整个系统我拆成了七个模块每个模块的职责和边界如下表所示模块核心功能涉及的表用户模块注册、登录、个人信息管理user、user_role视频分类模块分类维护与首页导航video_category视频信息模块视频上传、列表展示、详情查询video播放模块视频流式输出、鉴权、播放进度记录play_record收藏模块用户收藏与取消收藏favorite评论模块用户评论展示与新增comment搜索模块按标题模糊搜索video不要贪多。很多人喜欢在毕设里硬加弹幕、会员、充值、后台权限管理结果前端工作量爆炸核心链路反而做得粗糙。我的经验是宁可每个模块都做得完整扎实也别铺十一个半成品模块。弹幕和会员这类功能一旦要做得好看所花的时间堪比重做一遍项目完全得不偿失。2.2 核心表结构与字段设计我建表时的一个原则主键统一用BIGINT自增创建时间统一用create_time状态字段统一用TINYINT。这套规范看起来简单但能让三层代码里的字段命名高度一致减少MyBatis映射时到处写resultMap的麻烦。下面给出几张核心表的精简建表语句字段注释都写在SQL里可以直接对着改。用户表CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(128) NOT NULL COMMENT 密码建议MD5加盐, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像路径, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;视频表CREATE TABLE video ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(100) NOT NULL COMMENT 视频标题, description VARCHAR(500) DEFAULT NULL COMMENT 视频简介, cover VARCHAR(255) DEFAULT NULL COMMENT 封面图路径, video_url VARCHAR(255) NOT NULL COMMENT 视频相对路径, category_id BIGINT DEFAULT NULL, user_id BIGINT DEFAULT NULL COMMENT 上传用户, duration INT DEFAULT 0 COMMENT 视频时长单位秒, play_count INT DEFAULT 0 COMMENT 播放次数, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT视频表;播放记录表CREATE TABLE play_record ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, video_id BIGINT NOT NULL, progress_second INT DEFAULT 0 COMMENT 播放进度秒, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_video (user_id, video_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT播放记录表;这里有个容易忽略的设计点play_record表我加了一个UNIQUE KEY uk_user_video目的是让“一个用户对一个视频只有一条进度记录”更新时直接INSERT ... ON DUPLICATE KEY UPDATE progress_second VALUES(progress_second)。如果不加这个唯一键每次上报进度就不断插新记录一个月后你的播放记录表会膨胀到几万行垃圾数据个人中心的历史播放列表也会变得特别难看。2.3 分类与评论表的设计思路分类表很简单只有id、name、sort三个字段就够。评论表需要冗余一个username字段而不是在查询时JOIN用户表取昵称这样评论列表的渲染速度会明显更快代码也更简单。虽然是反范式设计但在毕设这种数据量级别下冗余字段带来的开发效率提升远大于理论上的存储浪费。CREATE TABLE comment ( id BIGINT NOT NULL AUTO_INCREMENT, video_id BIGINT NOT NULL, user_id BIGINT NOT NULL, username VARCHAR(50) DEFAULT NULL COMMENT 冗余用户名避免联表查询, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_video (video_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评论表;表与表之间的外键我没有物理创建只在逻辑上关联。有些学校的评阅老师喜欢看外键但实际开发里外键会带来很多删除限制的麻烦比如用户删号时如果评论表里还有关联数据物理外键会直接让你删不掉。我的建议是逻辑外键加索引删除时手动在Service里做级联清理这样既满足老师对关联的需求又不会把自己卡死。3. IDEA环境搭建与SpringMVC配置让三层架构按这个顺序跑通3.1 IDEA中创建Maven Web项目与Tomcat关联SSM项目建议直接建Maven工程不要手动往WEB-INF里塞jar包。很多人卡在第一步就是因为在网上下了一个半手工的SSM项目jar包冲突得一塌糊涂。新建项目时选Maven - Create from archetype - maven-archetype-webappGroupId用com.exampleArtifactId用项目名。记得先在IDEA的Build Tools - Maven里配置好本地仓库和自己的settings.xml否则默认下载源在国外等依赖能急死人。pom.xml里最核心的依赖版本我列一下直接参考properties spring.version5.2.6.RELEASE/spring.version mybatis.version3.5.5/mybatis.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.48/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.1.22/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.11.2/version /dependency dependency groupIdcommons-fileupload/groupId artifactIdcommons-fileupload/artifactId version1.4/version /dependency /dependencies这里特别提醒一点不要为了“省事”导入javax.servlet-api后打包时又带进去会导致Tomcat启动时类加载冲突。servlet-api的scope一定要设成provided让Tomcat自己去提供。如果你不确定直接在Deployment里把Web项目的lib目录翻一遍确认没有servlet-api.jar存在。关联Tomcat时我推荐用IDEA Ultimate自带的Tomcat Server集成。点开Run - Edit Configurations - - Tomcat Server - LocalApplication server指向本机Tomcat目录Deployment选项卡里选war explodedApplication context设为/video。这里有个多年来的经典坑如果你选了war包模式热部署会失效每次改Java代码都要重新build整个包开发体验非常痛苦。war exploded是解压目录IDEA会直接把classes和resources同步到Tomcat的webapps下改代码后按Ctrl Shift F10热编译一下就能生效。3.2 Spring与MyBatis配置分工详解SSM的配置文件看起来多其实思路只有一个Spring容器负责管理业务层和MyBatis的SqlSessionFactorySpringMVC容器只负责Controller层的Bean。按照这个原则拆分就不会把配置写得一锅粥。web.xml里要做两件事加载Spring根容器、注册SpringMVC的前端控制器。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mybatis.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping /web-app注意点SpringMVC的url-pattern用的是/而不是*.do。用/意味着所有请求都进DispatcherServlet这样才能实现REST风格的路径配合静态资源映射能覆盖视频封面等目录的访问。用*.do虽然老教程多但对video/play/1这种路径匹配很别扭还要额外处理后缀强烈不建议在视频站里用。spring-mybatis.xml里配置数据源和事务管理bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/video_db?useUnicodetrueamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value123456/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.video.entity/ property nameconfiguration bean classorg.mybatis.spring.boot.autoconfigure.MybatisProperties/ /property /bean等等MybatisProperties这个类是Spring Boot的这里不要照抄。直接在SqlSessionFactoryBean里嵌套一个org.apache.ibatis.session.Configuration的bean用来开驼峰映射和日志bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.video.entity/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.video.dao/ /beanmapUnderscoreToCamelCase这个配置极其重要。数据库字段是play_countJava属性是playCount不开驼峰映射你查询出来的对象里playCount永远是null排查起来还特别隐蔽因为它不报错。3.3 静态资源和上传解析器的配置细节spring-mvc.xml里除了组件扫描Controller还有两个必配项静态资源映射和CommonsMultipartResolver。视频站的封面、上传的临时文件都涉及静态资源访问不配mvc:resources的话浏览器请求图片路径会被DispatcherServlet拦截后报404。mvc:resources mapping/cover/** locationfile:/E:/video_res/cover// mvc:resources mapping/video_res/** locationfile:/E:/video_res// bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value104857600/ property namedefaultEncoding valueUTF-8/ /bean关于这套配置我多说几句。file:开头的绝对路径的意思是访问/cover/xxx.jpg时Spring会去磁盘上的E:/video_res/cover/目录找文件。这是SSM项目里处理大文件存储的标准做法视频文件千万不要放src/main/webapp/upload底下因为IDEA每次重新打包war exploded时可能把目录清掉。直接把上传的文件放项目外用虚拟映射暴露访问路径重启不丢打包不重这个设计能帮你少掉一大半头发。CommonsMultipartResolver的maxUploadSize我给了100MB500MB以内的视频基本够用。如果你要支持超过这个体积的视频光改这里没用还得到Tomcat的conf/web.xml里同步调整后面第4节会展开讲。4. 视频上传与本地存储方案路径设计决定你少踩多少坑4.1 为什么选本地磁盘而不是OSS、七牛云视频网站的毕设存储方案通常是本地磁盘、OSS对象存储或FastDFS分布式文件存储。很多同学一听“分布式”就兴奋非得上FastDFS最后光搭环境就耗了三天。我要说的是毕设的评分逻辑是链路完整 技术点可讲不是技术栈越重越好。你完全可以在答辩时说“本系统以本地磁盘为主同时抽象了存储接口后续可扩展OSS”这句话比硬上三台虚拟机跑FastDFS然后在传输文件时报错强一百倍。本地磁盘方案最大的优势是链路短。上传接口拿到MultipartFile直接写磁盘返回一个URL路径前端拿到路径就能预览没有任何中间环节的调优问题。代价是不支持横向扩展但毕设本来就没几个人并发访问这点代价可以完全忽略。4.2 存储目录结构设计我建议把资源目录统一放在一个独立磁盘根路径下比如E:/video_res/里面按业务拆子目录E:/video_res/ ├── cover/ 封面图 ├── video/ 视频正文按日期分子目录 │ └── 20240601/ │ └── xxx.mp4目录结构影响的是文件名的唯一性和可维护性。我生成的视频文件名用yyyyMMddHHmmss 随机数避免中文文件名和空格带来的URL编码问题。很多浏览器不支持URL里的中文你上传一个“搞笑视频合集.mp4”前端video标签直接加载失败排查半天最后发现是文件名的锅这种经历真的不想再有第二次。上传Controller的大致逻辑如下RequestMapping(value /upload, method RequestMethod.POST) ResponseBody public Result upload(RequestParam(file) MultipartFile file, RequestParam(title) String title, HttpServletRequest request) { if (file.isEmpty()) { return Result.error(文件为空); } String originalFilename file.getOriginalFilename(); // xxx.mp4 String extName originalFilename.substring(originalFilename.lastIndexOf(.)); // 校验扩展名只允许 mp4、webm、ogg if (!Arrays.asList(.mp4, .webm, .ogg).contains(extName.toLowerCase())) { return Result.error(不支持的视频格式); } String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); String finalName System.currentTimeMillis() _ new Random().nextInt(1000) extName; String savePath E:/video_res/video/ datePath / finalName; File dest new File(savePath); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); // 保存 video 记录video_url 存相对路径如 /video_res/video/20240601/xxx.mp4 Video video new Video(); video.setTitle(title); video.setVideoUrl(/video_res/video/ datePath / finalName); videoService.insert(video); return Result.success(video); }这里一个关键点是数据库的video_url存的是可访问的URL路径而不是磁盘绝对路径。磁盘路径是内部实现URL路径是外部访问入口两者在保存时要明确分层。后续播放接口会根据这个URL路径再加一层鉴权这点下一节详细说。4.3 Tomcat的隐含限制maxPostSize与maxSwallowSize上传文件时报org.apache.catalina.connector.Request.parseParameters或客户端直接收到41390%的情况不是你的代码问题而是Tomcat自带的请求体积限制在作怪。Tomcat 8.5里POST请求的默认maxPostSize是2MB这意味着只要表单里带了大文件Tomcat在解析参数阶段就会拒绝请求。修改方案有两种。简单粗暴的是在Tomcat的conf/web.xml里加一段multipart-config max-file-size209715200/max-file-size max-request-size209715200/max-request-size max-request-size209715200/max-request-size /multipart-config等等multipart-config需要放在Servlet的annotation里Tomcat全局web.xml里加这个不生效。正确做法是在conf/server.xml的Connector节点加属性Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxPostSize209715200 maxSwallowSize209715200 /maxPostSize管表单POST的解析体上限maxSwallowSize管Tomcat吞掉请求体的上限两个都调大上传200MB的文件才不会被中途掐断。如果是用IDEA集成Tomcat的方式直接改IDEA配置里的Tomcat server.xml不方便最稳妥的办法是到Tomcat安装目录的conf/server.xml里改完重启服务。还有一个更通用的方案前端不用表单提交文件而是用XMLHttpRequest的FormData异步上传这种情况Tomcat对form解析的限制会绕过一部分但maxSwallowSize依然会卡你所以还是建议两个参数一起调。5. 播放核心链路Range请求、进度记忆与防盗链5.1 播放接口为什么不能直接返回文件地址很多SSM视频站教程的做法是把视频存到webapp/static/video/xxx.mp4前端video src${pageContext.request.contextPath}/static/video/xxx.mp4直接播放。这个方案演示确实没问题但它有两个致命缺陷。第一用户打开浏览器开发者工具直接在Network面板看到完整的视频URL复制粘贴就能把视频下载走你的“在线播放”就只剩一个壳子。第二你无法统计播放次数、无法记录播放进度因为这些请求完全绕过了后端应用。正确的做法是视频的真实磁盘位置只存在于服务端客户端永不感知。播放时前端访问/video/play/{id}后端根据视频ID找到磁盘路径用RandomAccessFile结合HTTP Range头做分段输出。这样后端在整个链路里掌握了播放行为也就能顺带完成播放次数1和进度记忆的写入。5.2 基于Range的伪流媒体输出实现现代浏览器的video标签默认就会发送Range头比如Range: bytes0-如果你不做206响应直接200返回整个文件浏览器虽然也能播但拖动进度条时会变得非常卡顿因为浏览器得重新从头缓冲。所以正确的播放响应必须识别Range头。核心代码大概长这样RequestMapping(value /play/{id}, method RequestMethod.GET) public void play(PathVariable Long id, HttpServletRequest request, HttpServletResponse response) throws IOException { Video video videoService.getById(id); if (video null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } String filePath video.getDiskPath(); // E:/video_res/video/xxx.mp4 File file new File(filePath); if (!file.exists()) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } String range request.getHeader(Range); long start 0; long end file.length() - 1; if (range ! null range.startsWith(bytes)) { String[] arr range.replace(bytes, ).split(-); start Long.parseLong(arr[0]); if (arr.length 1 !arr[1].isEmpty()) { end Long.parseLong(arr[1]); } } long contentLength end - start 1; response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); response.setContentType(video/mp4); response.setHeader(Accept-Ranges, bytes); response.setHeader(Content-Range, bytes start - end / file.length()); response.setHeader(Content-Length, String.valueOf(contentLength)); try (RandomAccessFile raf new RandomAccessFile(file, r)) { raf.seek(start); byte[] buffer new byte[4096]; long totalRead 0; int len; while (totalRead contentLength (len raf.read(buffer, 0, (int) Math.min(buffer.length, contentLength - totalRead))) ! -1) { response.getOutputStream().write(buffer, 0, len); totalRead len; } } }这段代码有几个细节值得你注意。RandomAccessFile的seek(start)可以把读取起点精确跳转到请求的字节位置这是实现拖拽播放的基础。Math.min那行保证最后一次读取不会越过end边界避免多输出字节导致播放器计算时长出错。Content-Type一定要设video/mp4如果你存的是webm格式就要改成video/webm否则部分浏览器会直接用下载方式处理响应。如果你不想手写这么底层的逻辑Spring里也有ResourceRegion可以简化实现但毕设答辩时能讲明白Range手动实现的过程反而是一个很大的加分项。老师会问“为什么你的视频能拖拽播放”你把这行代码指给他看价值比背一百个面试题都大。5.3 播放进度上报与播放次数播放进度的核心是那个uk_user_video唯一键。前端播放器监听timeupdate事件每5秒向后端发一次/record/progress请求带上videoId、progressSecond和视频总时长。后端用一个INSERT ... ON DUPLICATE KEY UPDATE完成“有则更新、无则插入”代码非常简洁PostMapping(/record/progress) ResponseBody public Result reportProgress(RequestParam Long videoId, RequestParam Integer progressSecond, HttpSession session) { Long userId (Long) session.getAttribute(loginUserId); if (userId null) { return Result.error(请先登录); } playRecordService.saveOrUpdate(userId, videoId, progressSecond); return Result.success(); }播放次数不要在这里更新。很多人在play接口里写play_count 1结果每次拖拽进度都触发play请求播放次数被刷到几十万。我的做法是只有进入播放页时由播放页的初始化接口调用一次play_count 1这个接口和真正的视频流接口分离统计口径就清晰了。5.4 防盗链的实用级方案坦白说SSM里做真正的防盗链很复杂要搞 Token 签名、IP 黑白名单、Referer 校验。但毕设有毕设的做法我提供一个性价比最高的方案在play路径里加一个时间戳签名参数前端播放时先向后端请求一个带签名的播放地址签名有10分钟有效期。简单实现如下public String generatePlayUrl(Long videoId, Long userId) { long expire System.currentTimeMillis() 10 * 60 * 1000; String sign md5(videoId _ userId _ expire _salt); return /video/play/ videoId ?expire expire sign sign; }play接口里先校验签名有效性过期或签名不匹配直接返回403。用户就算拿到地址10分钟后也失效常规的复制链接下载方式就被堵住了。粘贴到迅雷之类工具里下载时由于URL带动态签名迅雷校验不过也无法完成下载足够应付演示场景。6. 常见问题与完整排查链路这些坑我劝你提前知道这一节我挑几个在整个项目开发中真实遇到、且网上答案往往不完整的问题给大家完整还原排查思路而不是只给一句“改一下配置”的结论。6.1 上传文件后访问404排查链路还原现象上传接口返回成功浏览器访问返回的/video_res/video/20240601/xxx.mp4地址页面404。排查步骤第一先在浏览器里直接访问磁盘路径的映射URL确认是SpringMVC静态资源映射的问题还是文件根本没存到对应位置。第二到IDEA的File - Project Structure里看Web项目的Web Resource Directory设置确认没有把项目输出目录弄乱。第三检查spring-mvc.xml里的mvc:resources配置location属性必须写file:前缀如果漏了file:Spring会以为你指向的是classpath路径。这个问题的隐藏根源还有可能是Tomcat的docBase没指对。在IDEA的Deployment配置里Application context填的是访问根路径而外部目录的资源映射是Tomcat内部处理的如果mvc:resources写的是相对路径就会被根路径干扰。用绝对路径加file:前缀后所有版本都稳定。6.2 启动时Bean冲突或SqlSessionFactory创建失败现象Tomcat启动时报SqlSessionFactoryBean相关的BeanCreationException或者提示Invalid bound statement (not found)。完整排查链路先看spring-mybatis.xml里的mapperLocations路径是否和实际mapper目录匹配classpath:mapper/*.xml必须在resources/mapper下注意如果放在src/main/java下Maven默认不会把它打进classes目录。这个问题非常经典初学者喜欢把UserDao.xml和UserDao.java放在同一个包下mapperLocations却写的classpath:mapper/*.xml结果永远找不到。解决方法是Mapper接口的XML统一放src/main/resources/mapper/并且打开IDEA右侧Maven面板执行clean加compile到target/classes/mapper目录下确认XML是否真实存在。如果不在说明Maven资源过滤设置有问题需要在pom.xml里声明build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory includes include**/*.xml/include include**/*.properties/include /includes /resource /resources /build6.3 播放页跨域请求失败如果你把前端页面和后端接口分开了比如前端在IDEA里单独跑、后端在Tomcat上跑播放页的fetch请求会出现跨域报错。SSM里启用跨域有三种方式在Controller上加CrossOrigin、写一个HandlerInterceptor统一加Header、在后端Filter里设置响应头。我建议直接用Filter方案因为视频站的封面加载和视频流本身都是异步请求统一加头最省事public class CORSFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { HttpServletResponse response (HttpServletResponse) res; response.setHeader(Access-Control-Allow-Origin, *); response.setHeader(Access-Control-Allow-Methods, POST, GET, OPTIONS, DELETE); response.setHeader(Access-Control-Max-Age, 3600); response.setHeader(Access-Control-Allow-Headers, x-requested-with, Content-Type); chain.doFilter(req, res); } }这里有一个安全提醒Access-Control-Allow-Origin设成*在演示环境没问题但如果你的系统里做了登录Cookie认证*会导致携带Cookie的跨域请求失败浏览器会直接拒绝。因为想要带Cookie就必须指定具体源不能用通配符。毕设里如果前后端同源部署根本不需要这个Filter跨域问题自然不存在只有前后端分端口联调时才需要临时加联调完删掉即可。6.4 视频文件体积大导致IDEA编译卡顿这个问题不算报错但特别影响开发心情。视频文件如果放在项目内或target目录里IDEA的索引和Maven的资源拷贝每次都会扫描这些大文件CPU直接飙满。我的建议是视频素材统一放E:/video_res不参与IDEA项目文件索引如果被迫放在项目内就在.gitignore或IDEA的File Types里忽略.mp4后缀让索引跳过。另外war exploded模式下IDEA往Tomcat的webapps目录同步文件时遇到大视频资源会特别慢。所以上传资源的存储路径必须绝对独立于Web应用目录这也是我反复强调项目外存储的原因——它不是一个风格偏好而是能让开发体验发生质变的工程决策。最后分享两个实战体会这类在线视频播放网站做完之后我个人最大的体会是它的核心复杂度不在业务功能的数量而在视频文件的“流式输出”和“路径管理”这两块。很多时候界面已经全部写完了上传也正常了但播放器就是转圈不播最终查下来都是Range响应头没设置、或者存储路径与URL映射不一致导致的。把这两条链路彻底打通系统就成功了一大半。再给一个很实用的小技巧答辩前一定要预置一份演示数据。手动在页面上一一上传视频又慢又容易出现格式问题我建议先写一段SQL脚本把已上传到E:/video_res里的视频文件名直接插入到video表配合后台写好的封面图URL首页一打开就是整整齐齐的视频卡片播放点进去即出画面。现场的稳定性永远是准备的副产品。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑