做了几年计算机毕业设计的指导,也帮不少同学救过急,说实话基于springboot + vue的校园系视频平台系统这个题目在当下确实算一个性价比很高的选择。技术栈主流、需求清晰、可扩展性强,不管是课程设计还是毕业论文,都能拿出完整的东西来。这篇博文就围绕这个系统,把我带学生做这类项目时沉淀的一些内容整理出来——从技术选型、功能拆分、数据库设计,到前后端落地、部署答辩,尽量把思路和踩过的坑都讲透。
1. 这个选题为什么值得做:技术栈权衡与项目定位
1.1 SpringBoot + Vue成为毕业设计主流的三个原因
先说结论:如果你的毕业设计题目没有强制指定技术栈,SpringBoot + Vue 前后端分离是目前最稳妥的组合之一。
原因很简单,第一是学习资料足够多。遇到问题随便搜一下,几乎都能找到对应的解决方案,不像某些小众框架,报错信息搜半天搜不到一个有效答案。第二是就业方向对得上,大部分中小型公司内部系统采用的就是这类前后端分离架构,做完这个项目,简历上有东西可写,面试聊项目也能聊得下去。第三是可演示性强,视频平台天然有视觉冲击力,答辩时展示效果比普通的增删改查系统好太多。
1.2 校园视频平台和公网视频平台的核心差异
做这个题目之前,先要搞清楚一个定位问题:校园视频平台不是B站,也不是爱奇艺。
公网视频平台要考虑海量用户、分布式存储、转码集群、推荐算法,这些对一个毕业设计来说既不现实也没必要。校园场景下的视频平台,核心是面向特定范围人群(学生、老师)的视频分享、学习资源管理、课程回放和活动记录。数据量级在几千到几万条视频之间,并发不会太高,但功能链要完整。
明确了这一点,技术选型就有了依据:单机部署、本地存储、MySQL数据库、Redis做缓存(可选),这套组合完全够用。把核心链路做扎实,比堆砌一堆用不上的微服务组件更有价值。
2. 功能模块拆解:先想清楚系统要解决什么问题
2.1 前台用户端的功能清单
我通常建议把系统划分成前台和后台两个端口,前台面向普通用户,后台面向管理员。前台功能围绕"看视频"这个核心动作展开:
- 用户注册登录:提供账号密码注册、登录,登录后使用JWT令牌维持会话状态。
- 视频浏览与播放:首页展示视频列表,支持按分类筛选、关键词搜索,点击进入详情页播放,记录播放次数。
- 视频上传:登录用户可上传视频,填写标题、简介、选择分类、设置封面(可自动截取或手动上传)。
- 互动功能:视频点赞、收藏、评论,评论支持按时间排序。
- 个人中心:管理本人上传的视频(编辑、删除)、查看收藏列表、查看点赞记录。
- 关注与动态(可选加分项):用户可关注其他创作者,在首页查看关注者的最新投稿。
2.2 后台管理端的功能清单
后台不需要做得太重,但要能体现"管理"的闭环:
- 用户管理:查看用户列表、禁用/启用账号、重置密码。
- 视频管理:审核视频(很多校园场景需要管理员审核后视频才公开可见)、下架违规视频、删除视频。
- 分类管理:对视频分类进行增删改查,修改后前端联动更新。
- 数据统计(加分项):展示总用户数、总视频数、今日上传量、播放量Top10等简洁的统计卡片。
2.3 权限与状态管理:区分"你"和"管理员"
权限设计不要一开始就搞复杂的RBAC模型,毕业设计阶段用角色字段就够了。用户表里加一个role字段(0为普通用户、1为管理员),后端在需要管理权限的接口上做拦截校验,前端根据角色控制路由和按钮显示,这样既清晰又够用。
视频的状态字段建议设计成:0待审核、1已上架、2已下架。普通用户只能看到"已上架"的视频,管理员能看到全部。这个设计虽然多了一点工作量,但写进论文里能体现出"内容安全"的意识,答辩老师通常会对这点感兴趣。
3. 数据库设计:视频系统的地基不能只靠感觉
3.1 核心表结构与字段说明
数据库设计决定了后面写代码是顺畅还是痛苦。以下是我在这类项目中常用的核心表结构设计。
用户表(user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键自增 |
| username | varchar(50) | 用户名,唯一索引 |
| password | varchar(255) | BCrypt加密后的密码 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(255) | 头像地址 |
| role | tinyint | 0普通用户,1管理员 |
| status | tinyint | 0正常,1禁用 |
| created_at | datetime | 创建时间 |
视频表(video)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键自增 |
| user_id | bigint | 上传者ID,外键 |
| category_id | int | 分类ID |
| title | varchar(100) | 标题 |
| description | text | 简介 |
| video_url | varchar(255) | 视频文件访问路径 |
| cover_url | varchar(255) | 封面路径 |
| status | tinyint | 0待审核,1已上架,2已下架 |
| play_count | bigint | 播放次数 |
| like_count | bigint | 点赞数(冗余字段,用于列表展示) |
| created_at | datetime | 创建时间 |
分类表(category):id、name、sort。
评论表(comment):id、video_id、user_id、content、created_at。
点赞表(like_record):id、user_id、video_id、created_at,联合唯一索引保证一个用户只能点赞一次。
收藏表(favorite):结构与点赞表类似。
3.2 关联关系与常见设计误区
先说关联关系:视频表通过user_id关联用户表,通过category_id关联分类表;评论、点赞、收藏都通过各自的user_id和video_id建立多对一关系。核心查询集中在"某视频的所有评论"和"某用户的所有收藏",所以在video_id和user_id上建索引很有必要。
这里有一个常见误区:很多人会在视频表里用一个字段存储点赞数,然后在每次点赞时like_count++,这是没问题的,但如果列表页要按点赞数排序,这个字段必须同步更新准确。我建议在点赞接口里同时更新冗余字段,同时保留点赞记录表用于判断"当前用户是否已点赞",两个配合使用,而不是只查记录表count一下,那样数据量稍微上来就会慢。
另一个容易忽略的细节是删除策略。用户删除视频时不要真的物理删除记录,用逻辑删除(status=-1)更安全,不然用户的收藏列表、评论关联都会出问题。数据库外键我建议在表设计里标注,但在实际建表时可以不加物理外键,通过代码逻辑维护一致性,否则批量操作时容易卡死。
4. 后端SpringBoot实现:从工程结构到接口落地
4.1 分层结构:Controller、Service、Mapper各司其职
工程结构我一般这样组织:
src/main/java/com/example/campusvideo ├── controller # 接口层,只接收参数和返回结果 ├── service # 业务层,核心逻辑 ├── mapper # 数据访问层(MyBatis-Plus继承BaseMapper) ├── entity # 数据库实体类 ├── dto # 前端参数对象 ├── vo # 返回给前端的数据对象 ├── config # 配置类(跨域、拦截器、WebMvc) ├── common # 统一返回结果、异常处理、常量 └── utils # 工具类(JWT、文件上传等)Controller层要瘦,Service层要厚。比如上传视频接口,Controller只负责校验参数和调用Service,而"保存文件、创建记录、处理封面"这类逻辑都放到Service里。这样写的好处是后期做单元测试很省力。
4.2 视频上传与存储:本地存储方案的完整链路
视频上传是校园视频平台里最有技术含量的模块,也是答辩时容易被追问的地方。这里给一个最实用的本地存储方案。
先配置上传路径:
# application.yml file: upload-dir: /data/campus-video/upload/ video-max-size: 200MB image-max-size: 5MBController接收MultipartFile,Service处理保存逻辑:
public String uploadFile(MultipartFile file, String type) { if (file.isEmpty()) { throw new BusinessException("文件不能为空"); } // 校验大小 long maxSize = "video".equals(type) ? 200 * 1024 * 1024 : 5 * 1024 * 1024; if (file.getSize() > maxSize) { throw new BusinessException("文件大小超出限制"); } // 生成唯一文件名,避免中文和空格 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()); File dir = new File(uploadDir + dateDir); if (!dir.exists()) { dir.mkdirs(); } // 保存文件 File target = new File(dir, filename); file.transferTo(target); // 返回可访问路径 return "/upload/" + dateDir + "/" + filename; }这种方案避免了把文件直接存在项目工程目录里的问题,也方便日后打包部署。但要注意,本地存储方案有一个必须处理的坑:SpringBoot的静态资源映射。默认情况下,程序不会把/upload路径映射到磁盘目录,所以需要加一个WebMvc配置:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /upload/** 映射到本地磁盘目录 registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadDir); } }4.3 鉴权与当前用户上下文
前后端分离项目,Session方案不太适用,用JWT更贴合实际。流程是:登录成功后端生成一个包含用户ID和角色信息的token,前端存到localStorage,每次请求在header里带上Authorization: Bearer token;后端写一个拦截器解析token,把用户ID放入ThreadLocal,Controller直接获取当前用户。
判断是否登录的核心代码大致是这个思路:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { throw new BusinessException(401, "未登录"); } // 解析token,拿到userId Long userId = JwtUtils.parseToken(token); UserContext.set(userId); return true; } }之所以推荐UserContext.set()这种方式,是因为避免了在每个Controller里手动从token里解析用户ID的重复代码,而且后期如果要加线程池或异步任务,也能更灵活地处理上下文传递。
5. 前端Vue实现:另一种视角的项目落地
5.1 工程搭建与请求封装
前端我推荐使用Vue 3 + Vite + Element Plus的组合,比Vue 2 + Webpack启动更快、生态也更新。工程初始化用:
npm create vite@latest campus-video-frontend -- --template vue npm install element-plus axios vue-router pinia请求封装是前端工程化里很关键的一步。直接在组件里写axios.get不是不行,但几十个接口都要处理token、报错提示、loading状态,代码就乱了。抽象一个request模块:
// src/utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: 'http://localhost:8080/api', timeout: 10000 }) // 请求拦截器:自动带上token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理错误 request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res.data } ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } ) export default request这样封装之后,组件里调用接口就非常清爽,比如获取视频列表:
const getVideoList = async (params) => { const data = await request.get('/video/list', { params }) return data }5.2 视频播放与列表分页的交互细节
视频播放直接用HTML5的<video>标签就够用了,不需要额外引入video.js这种重组件。因为校园场景大多播放mp4格式,浏览器原生支持播放。需要注意的细节是播放器尺寸和响应式:
<video :src="videoDetail.videoUrl" controls style="width: 100%; max-height: 600px; background: #000; outline: none;" ></video>列表分页推荐用Element Plus的el-pagination组件搭配后端分页接口。前端传pageNum和pageSize,后端返回total和records,前端在切换页码时重新请求数据。
视频列表卡片我建议只展示封面、标题、播放量、点赞数、作者昵称。点击卡片进入详情页时再加载完整信息。这样首页请求量小,加载速度快。
5.3 路由守卫与登录态联动
前端路由需要用beforeEach守卫来控制访问权限。比如"上传视频"、"个人中心"这两个页面,未登录用户点击应跳转到登录页。
// src/router/index.js router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })还有一个小细节:如果用户信息存在Vuex/Pinia里,页面刷新后状态会消失,需要在路由守卫里判断"有token但用户信息为空"时重新请求用户信息。这个坑很多人遇到但容易忽略。
6. 联调、部署与答辩的实战经验
6.1 前后端联调最常见的三类问题
项目做完了,前后端一对接,问题总是集中在三类:
第一类跨域问题。前端8080端口访问后端8080端口,浏览器的同源策略会拦截。解决办法在后端加一个全局跨域配置类,或者用@CrossOrigin注解。推荐全局配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }加了跨域配置还是会遇到问题的话,大概率是拦截器把OPTIONS请求拦了。要让preHandle方法放行OPTIONS请求,否则前端预检请求直接403。
第二类是路径和格式不一致。前端传的参数是JSON,后端接口用@RequestBody接收,对象字段没对上,报400。这个要靠统一规范来规避:前端封装request时默认Content-Type: application/json,后端所有接收对象参数的接口都用@RequestBody。
第三类是文件路径返回不对。本地调试时上传成功,但视频访问404。多半是静态资源映射没配,或者数据库存的是"相对路径"而前端没有拼接前缀。记住一个原则:数据库存相对路径(如/upload/20250401/xxx.mp4),前端通过环境变量配置VITE_API_BASE_URL拼完整URL,部署后只需要改一个配置。
6.2 部署方案与Maven打包
毕业设计阶段,部署不是硬性要求,但如果能演示"部署到服务器",绝对是加分项。
后端打包很简单:
mvn clean package -DskipTests打出来的jar包放到服务器上,用java -jar campus-video.jar启动即可。如果不想裸奔,可以加一个nohup后台运行:
nohup java -jar campus-video.jar > logs/run.log 2>&1 &前端打包:
npm run build生成dist目录,把里面的静态文件放到Nginx的html目录下。同时需要在Nginx配置一个反向代理,把/api请求转发到后端8080端口:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /upload/ { alias /data/campus-video/upload/; } }这里把前端history路由的try_files配置和后端代理都写好了,照着抄基本不会出大问题。
6.3 答辩演示的小技巧
从带学生的经验来看,答辩时最容易翻车的几个点:
一是演示环境不稳定。多媒体教室的网速和电脑性能都不好说,视频加载慢、页面白屏都是常事。建议提前准备好两套方案:笔记本现场演示 + 截图/录屏存档。万一现场网络不行,直接播放录好的演示视频。
二是被问到敏感点不要慌。比如"视频存在哪?""密码怎么加密?"这类基础问题要能说清楚。密码用BCrypt加密,视频文件存在服务器磁盘,数据库存的是访问路径,这几个关键点记住就能应对大部分问题。
三是主动展示亮点。如果做了审核功能、数据统计、收藏关注这些"自选动作",演示时要主动提。这比你被动地回答"你这个系统有什么特色"要好得多。
我个人的体会是,这类系统做的过程比结果更重要。哪怕最后功能没有100%做完,只要核心链路是通的,代码结构清晰,你在文档和论文里能把自己的设计思路、遇到的问题、解决办法讲明白,答辩就没有问题。如果时间紧张,优先保视频上传、视频播放、用户登录这三个核心闭环,先把骨架子撑起来。
最后再给一个小技巧:项目里的README文档值得花两个小时认真写。把技术栈、功能清单、部署步骤、演示账号都写清楚,不仅自己后面调试方便,指导老师检查项目时第一印象也会好很多。很多人不重视这个细节,但它在实际评审中的分量比你想象的要重。