☰
告别CRUD管理系统!用SpringBoot+Vue打造校园多媒体共享平台
2026/10/14 7:30:48 网站建设 项目流程

每年毕业设计选题的时候,总有一批人扎堆去做“XX管理系统”,图书管理系统、宿舍管理系统、仓库管理系统——代码差不多,答辩PPT差不多,老师听了也差不多。真正的问题是,这类系统业务太薄,一个CURD页面就能讲完,很难体现你对全栈的理解。我这边建议换一个思路:做一个面向校园场景的多媒体信息共享平台。基于SpringBoot+Vue前后端分离,Java后端配合MySQL数据库,业务上覆盖用户上传、分类管理、管理员审核、在线预览、下载统计、权限控制这些真实站点才会有的环节,既有复杂度又有业务故事,用来应付毕设、课设,或者单纯想练手全栈,都够扎实。这篇文章不聊虚的,直接把这个平台从需求边界、数据库设计、核心接口到权限方案、排错记录完整拆开讲一遍。

1. 这个选题到底值不值得做:先把需求边界划清楚

1.1 业务角色与核心流程

做项目之前先别急着建表,得想清楚“谁在用这个平台”。我按常见的校园场景把用户切成四类:普通用户(学生/教师)、资源审核员、超级管理员,以及未登录的游客。

普通用户能干什么:浏览公开课件、视频、音频、图片、文档,按分类筛选,按关键词搜索,登录后可以上传资源、收藏资源、发表评论、下载资源、管理自己的个人中心。资源审核员负责处理待审核资源,判断内容是否合规、分类是否正确、信息是否完整,审核通过的资源才会出现在前台列表里。超级管理员多出来的职责是用户管理、分类管理、公告管理、轮播图管理、统计数据总览。游客只能看前台列表和预览部分公开资源,不能上传下载,也不能评论。

核心流程一句话就能说清:用户上传资源,系统写一条待审核记录;审核员操作状态流转,要么通过要么驳回;通过后资源进入公开展示池;用户浏览、预览、下载、评论、收藏,系统把关键行为记下来形成统计数据。这条链路从内容生产到内容消费再到数据沉淀,是多媒体平台和普通CRUD系统最大的区别,后边所有设计都围绕它展开。

1.2 功能清单:该有的一个不少,失控的坚决不做

列出功能清单的时候要克制,很多同学容易把题目越做越大,最后变成“万能平台”。我的建议是先划一条线:内容管理、用户体系、平台运营、数据统计,这四块足够撑起一个完整的毕设,其余全部靠后。

内容侧核心功能包括:资源多类型上传(视频、音频、文档、图片、压缩包)、分类与标签管理、前台资源列表和详情页、在线预览/播放、热门资源排序。用户侧包括:注册登录、个人资料、我的上传、我的收藏、我的评论。平台运营侧包括:资源审核、公告发布、轮播图管理、用户启停用、分类维护。数据统计侧包括:今日上传量、审核通过率、分类分布、下载排行Top10、近七天新增趋势。

一些看起来很酷但不要碰的:在线剪辑、视频转码、实时聊天、在线直播、定时任务爬虫。转码要FFmpeg,聊天要WebSocket,直播要推流拉流,每多一个都意味着至少多出一周的调试时间,而且答辩时老师很可能揪着性能问到底。毕设项目的评价标准是“完成度”和“技术表达”,不是功能数量。

1.3 数据表一页纸设计

先把表规划铺开,后面建表就不慌。这个平台我建议至少设计十张业务表,分别是:

表名说明
sys_user用户表,存账号、密码、昵称、头像、角色、状态
category分类表,如课件、视频、音频、图片、文档、压缩包
resource资源表,多媒体核心表,存路径、大小、类型、状态、统计字段
tag标签表,独立维护
resource_tag资源标签关联表,多对多
comment评论表
favorite收藏表
download_record下载记录表,同时用于统计
notice公告表
banner轮播图表
operation_log操作日志表

这十一张表对毕设来说规模刚刚好,既能体现你对关系型数据的理解,也不会让建表本身变成负担。资源表是整张业务网的枢纽,设计时多花点时间是值得的,接下来单独讲。

2. 技术栈为什么这么配:选型理由和版本搭配

2.1 后端组合:SpringBoot + MyBatis-Plus + MySQL

后端我推荐SpringBoot 2.7系列,别追最新。原因很现实:2.7是2.x的最后稳定版本,网上资料最多,碰到问题一搜一大把;如果选3.x,JDK版本要求高,老电脑和实验室环境容易翻车,而且很多教程还是2.x的写法,不匹配。数据库用MySQL 8.0,ORM框架用MyBatis-Plus,理由更简单——分页和条件构造器能省掉大量重复SQL,写起来比纯MyBatis舒服,答辩时也好解释。

我自己习惯的版本搭配是:SpringBoot 2.7.18 + MyBatis-Plus 3.5.5 + MySQL 8.0.36 + JDK 1.8。JDK不要上11、17,保持8最稳,Maven打包部署都顺。

2.2 前端组合:Vue + Element UI + Axios

前端这块,如果你是新手,我建议Vue2.7 + Element UI,这是最成熟最“配齐”的组合。虽然Vue3现在是大趋势,但Vue3 + Element Plus对新手来说坑更多,比如组合式API理解成本、UI组件库部分用法变化、网上老资料很多对不上。如果你已经有Vue3基础,也可以选Vue3,但要固定版本,别混着用。

项目里用到的前端技术栈是:Vue2.7 + Vue Router 3 + Vuex 3 + Element UI 2.15 + Axios。页面分为两部分:前台门户(首页、分类列表、资源详情、搜索、个人中心)和后台管理(用户管理、资源审核、分类、公告、轮播、统计),两个独立区域用路由嵌套隔开,各自有布局。

2.3 认证方案:JWT还是Session

认证不管怎么做都能跑通,但答辩分差异很大。Session方案简单,可是前后端分离场景下要处理跨域携带Cookie,部署时还要考虑会话共享,光解释就费劲。JWT方案把用户信息签进Token里,后端无状态,前端每次请求在Header里带Token,更适合现在的主流前后端分离架构。

JWT适合这个项目的原因还有一条:你需要角色信息来控制后台权限。JWT的payload里直接放userId和role,拦截器解析出来就能用,不用每次查数据库。Session要查Redis或Session,复杂度更高。

2.4 文件存储:本地磁盘比对象存储更适合毕设

对象存储(比如阿里云OSS)确实专业,但毕设场景要考虑成本和网络环境,而且老师更关心的是“你知不知道文件放哪、怎么访问”。本地磁盘方案足够,在配置文件中指定一个basePath,文件按年/月/日子目录落盘,文件名用UUID重命名防冲突,然后通过Spring Boot的静态资源配置把本地目录映射成/files/**访问路径。

这个方案的好处是:整个项目就是一个独立的Java进程,本地跑起来就能演示,不需要额外依赖。缺点是文件会占服务器磁盘,但这在毕设答辩的可接受范围内。

3. 数据库设计:这个平台的地基怎么打

3.1 资源表:整张业务网的枢纽

我直接给出资源核心表的建表SQL,这是整个平台最关键的一张表:

CREATE TABLE `resource` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(120) NOT NULL COMMENT '资源标题', `summary` varchar(500) DEFAULT NULL COMMENT '简介', `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `file_name` varchar(255) NOT NULL COMMENT '原始文件名', `file_path` varchar(255) NOT NULL COMMENT '存储路径', `file_size` bigint(20) DEFAULT NULL COMMENT '文件大小,字节', `file_type` varchar(20) DEFAULT NULL COMMENT '资源类型:video/audio/doc/image/other', `suffix` varchar(20) DEFAULT NULL COMMENT '扩展名:mp4/mp3/pdf/jpg', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面图地址', `download_count` int(11) NOT NULL DEFAULT 0 COMMENT '下载次数', `view_count` int(11) NOT NULL DEFAULT 0 COMMENT '浏览量', `like_count` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数', `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '0待审核 1已通过 2已驳回', `audit_remark` varchar(255) DEFAULT NULL COMMENT '审核备注', `uploader_id` bigint(20) NOT NULL COMMENT '上传者ID', `create_time` datetime NOT NULL COMMENT '上传时间', `update_time` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_status` (`status`), KEY `idx_uploader` (`uploader_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='多媒体资源表';

这里有几个设计点要讲明白。一是file_size用bigint而不是int,存的是字节数,一个2GB视频就是2147483648字节,超出int范围,新手常在这里踩坑。二是file_type和suffix分开存,file_type用于前端做图标和预览分发,suffix用于后端类型校验。三是status定义成舌头值枚举0/1/2,不要用字符串,查询效率高,代码里用常量或枚举类维护可读性。

3.2 冗余字段和多对多表的设计思路

列表页如果每次都去查文件大小、封面路径、下载量,不需要。但这些数据就在resource表内,这就是冗余设计带来的便利。另一类冗余是统计数据:download_count、view_count、like_count直接冗余在资源表里,每次点赞、下载直接做update resource set like_count = like_count + 1,展示时不用聚合查询,性能好且SQL写起来简单。

维护型数据用关联表。标签是多对多关系,所以有resource_tag表。但注意,分类不要搞多对多,一个资源只属于一个分类,用category_id外键就够了。评论表里除了评论内容,还要存user_id和resource_id,并设计一个parent_id支持回复楼层,如果不想做楼中楼,这个字段传0就行,保留扩展性。

3.3 MyBatis-Plus分页与VO封装

数据访问层用MyBatis-Plus写起来很快。核心查询用LambdaQueryWrapper构造条件,配合Page分页:

Page<Resource> page = resourceMapper.selectPage( new Page<>(current, size), new LambdaQueryWrapper<Resource>() .eq(Resource::getStatus, 1) .eq(categoryId != null, Resource::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Resource::getTitle, keyword) .orderByDesc(Resource::getCreateTime) );

但直接把实体类返回给前端有风险,因为uploaderId、filePath这些字段不该暴露。所以业务层要转成VO,把上传者昵称、分类名称这种前端展示需要的信息并进去。我通常写一个ResourceVO,在Service层循环组装,虽然多几次内存操作,但对毕设数据量来说完全够用,代码结构也更清楚。

4. 核心业务链路:上传、审核、预览、下载统计的落地细节

4.1 上传接口的工程化写法

上传是整个平台最核心的接口,也是老师最爱追问的环节。我按工程化的思路给出完整逻辑,不只是写一个MultipartFile接参就完事:

@PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file, @RequestParam("categoryId") Long categoryId, @RequestParam("title") String title, HttpServletRequest request) { if (file.isEmpty()) { return Result.error("上传文件不能为空"); } long maxSize = 200 * 1024 * 1024L; // 200MB if (file.getSize() > maxSize) { return Result.error("单文件大小不能超过200MB"); } String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase(); Set<String> allowSuffix = new HashSet<>(Arrays.asList( "jpg", "jpeg", "png", "gif", "mp4", "avi", "mov", "mp3", "wav", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "zip", "rar", "txt" )); if (!allowSuffix.contains(suffix.replace(".", ""))) { return Result.error("不支持的文件类型"); } String dateDir = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); File destDir = new File(fileProperties.getBasePath() + dateDir); if (!destDir.exists()) { destDir.mkdirs(); } String newFileName = UUID.randomUUID().toString().replace("-", "") + suffix; File destFile = new File(destDir, newFileName); file.transferTo(destFile); Resource resource = new Resource(); resource.setTitle(title); resource.setCategoryId(categoryId); resource.setFileName(originalFilename); resource.setFilePath("/files/" + dateDir + "/" + newFileName); resource.setFileSize(file.getSize()); resource.setFileType(FileTypeUtil.detect(suffix)); resource.setSuffix(suffix.replace(".", "")); resource.setStatus(0); resource.setUploaderId((Long) request.getAttribute("userId")); resourceMapper.insert(resource); return Result.ok("上传成功,请等待审核"); }

几个细节要注意。file.transferTo(destFile)在SpringBoot 2.x里是直接可用的,但目标目录必须已存在,所以先mkdirs()。文件名必须用UUID重命名,否则两个用户上传同名文件会互相覆盖。类型校验按扩展名做是黑名单思想的低级版本,白名单比黑名单安全,因为黑名单永远可能漏掉一个新的危险扩展名。

4.2 审核机制:状态机与流水日志

审核的核心是状态流转。资源上传后status=0,审核员在后台看到待审核列表,点击“通过”则置为1,点击“驳回”则置为2,同时填一个audit_remark,用户个人中心能看到驳回原因。

这里有个常见问题:审核操作要不要记录日志?要。答辩时老师一定会问“怎么知道是谁审核的、什么时候审核的”。我在operation_log表记录每次操作,包括操作人、操作类型、资源ID、操作时间和备注。代码里把审核逻辑包在一个@Transactional方法中,修改资源状态和插入日志同生共死。

4.3 在线预览与播放:别把静态资源路径搞乱

前端预览分几种情况。视频资源用<video>标签,源地址直接指向/files/...;音频用<audio>;图片直接用<img>;PDF用<iframe>;Office文档比较麻烦,最简单的方案是提示下载后用本地软件打开。如果你想把Office在线预览做出来,可以接第三方预览服务,但依赖外网,不建议作为毕设主功能。

需要注意的问题是静态资源映射。Spring Boot默认静态目录是classpath:/static/,你上传的文件在磁盘上,不在这个目录里。必须自定义WebMvcConfigurer把本地磁盘路径映射为/files/**:

@Configuration public class FileConfig implements WebMvcConfigurer { @Resource private FileProperties fileProperties; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + fileProperties.getBasePath()); } }

file:协议前缀不能省略,而且getBasePath()返回的路径结尾必须带斜杠,比如D:/upload/,少一个斜杠路径拼接就是错的。

4.4 下载统计:简单计数也能做得有说服力

下载功能本身简单,难点在“统计”。最低级做法是前端放个<a>标签直接跳文件地址,这绕过了后端接口,下载量统计完全是0。正确做法是走后端接口:点击下载时请求/api/resource/download/{id},后端先更新download_count,然后写入一条download_record,再用ResponseEntity把文件流转给浏览器。

@GetMapping("/download/{id}") public ResponseEntity<byte[]> download(@PathVariable Long id) { Resource resource = resourceMapper.selectById(id); if (resource == null || !resource.getStatus().equals(1)) { return ResponseEntity.notFound().build(); } File file = new File(fileProperties.getBasePath() + resource.getFilePath().replace("/files/", "")); resourceMapper.update(null, new LambdaUpdateWrapper<Resource>() .eq(Resource::getId, id) .setSql("download_count = download_count + 1")); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + resource.getFileName()) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(FileUtil.readBytes(file)); }

用setSql做自增而不是先查后设,是为了避免并发下丢更新。这个细节在答辩时讲出来,老师会觉得你是真写过代码的。

5. 前后端权限与联调:最容易丢分的环节

5.1 后端JWT拦截器:接口层的最后一道门

权限不只是登录校验,要区分“是否登录”和“是否有权限进来”。我写一个JwtInterceptor统一处理所有/api/**请求,放行登录注册接口,其他接口必须带Token。拦截器里解析Token后把userId和role塞进request属性,Controller方法直接取:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); } try { Claims claims = JwtUtil.parse(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或Token过期\"}"); return false; } } }

角色校验可以用一个自定义注解做,比如@RequireRole("admin"),加在后台管理接口上。拦截器里读handlerMethod.getMethodAnnotation(RequireRole.class),比对当前用户角色,不一致就返回403。这种基于注解的权限控制,比在每个Controller里手动if判断要优雅得多,代码结构也更清晰。

5.2 前端路由守卫:菜单可以藏着,路由必须守住

前端不能只做菜单隐藏,路由守卫是真正拦人的地方。基于角色码在前端做路由前置守卫:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const role = localStorage.getItem('role') if (to.meta.requiresAuth && !token) { next('/login') return } if (to.meta.roles && to.meta.roles.indexOf(role) === -1) { next('/403') return } next() })

后台管理路由统一加meta.roles: ['admin', 'super_admin'],普通用户手动输入URL也进不去,只能看到403页面。这个细节做完,答辩时直接说“后端有拦截器,前端有路由守卫,双重控制”,基本不需要多解释。

5.3 跨域与CORS配置:联调时最先炸的地方

前后端分离以后,Vue跑在8080端口,SpringBoot跑在8081端口,你从8080页面发请求到8081,必然触发跨域。配置CORS最标准的方式是重写addCorsMappings:

@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); }

注意allowedOriginPatterns("*")和allowCredentials(true)不能同时用allowedOrigins,是因为携带凭证时浏览器不允许通配来源,用allowedOriginPatterns就能绕开。如果你还把拦截器里的OPTIONS请求放行了,后面就不会遇到预检请求被拦截的问题。

6. 毕设/课设的推进节奏与答辩准备

6.1 预置功能为什么是加分项

很多同学拿到项目代码后第一件事是删功能,我建议反过来,先把预置好的基础功能跑通,形成完整闭环,再规划哪些地方做个性化改造。这个平台预置的公告、轮播图、数据统计三种功能,看着不起眼,但训练的是“从运营者视角看平台”的能力。你把公告CRUD做出来,前端首页动态渲染,老师在演示阶段看到你能在后台发一条公告、前台立刻展示,这个交互效果比任何一行代码都有说服力。

6.2 八周推进里程碑

毕设整体时间不用拉太长,按八周规划刚刚好:

周次任务预期产出
第1周原型图和数据库设计功能清单、ER图、建表SQL
第2周搭建SpringBoot+Vue骨架前后端跑通、登录注册、JWT认证
第3周资源上传与列表展示上传接口、文件存储、前台列表页
第4周审核流程与后台框架审核列表、状态流转、后台布局
第5周评论收藏下载与个人中心核心交互全部闭环
第6周公告轮播统计与权限完善运营功能、按钮级权限、路由守卫
第7周自测、修Bug、部署联调本地生产模式打包,全流程回归
第8周答辩PPT、演示数据、提问预案熟练演示,准备扩展性问题

不要想着前两周就写完代码,数据库设计和骨架搭建占一半时间很正常。磨刀不误砍柴工,表结构定错了,后面改字段比写代码痛苦得多。

6.3 答辩前准备好的关键问题

根据经验,老师对这类题目大概率会问:上传文件为什么不用OSS?本地存储挂了怎么办?审核状态流转逻辑是什么?JWT为什么比Session适合?前端权限做在路由里会不会被绕过?数据统计是怎么做的?

每个问题都不要背答案,理解底层逻辑后再用自己的话讲。比如“上传为什么不用OSS”,回答思路是“对象存储确实可靠,但会引入第三方依赖,为了演示部署的独立性,我采用本地磁盘加目录映射,同时把存储路径抽象到配置文件,以后切换存储方式只需要改实现类”。这个说法说明你考虑过扩展性,比“因为OSS要花钱”强很多。

7. 排错实录:这些坑我替大家踩过了

7.1 MySQL8连接报错:时区与驱动类

用MySQL 8时,连接串里的驱动类是com.mysql.cj.jdbc.Driver,不再是5.x的com.mysql.jdbc.Driver。如果启动报Access denied或CLIENT_PLUGIN_AUTH is required,基本是驱动没换或者密码加密方式问题。另外useSSL=false&serverTimezone=Asia/Shanghai这串参数要带上,不然会出现The server time zone value 'Öйú±ê׼ʱ¼ä'这种乱码时区报错。

7.2 上传大文件被拒绝:multipart配置

默认情况下SpringBoot能上传的文件就1MB左右,超过就报MaxUploadSizeExceededException。很多人会把这个错误理解成程序崩溃,其实是配置没加。在application.yml里增加:

spring: servlet: multipart: max-file-size: 200MB max-request-size: 200MB

顺带提一句,如果处理后端转发到另一台服务的场景,还要调nginx的client_max_body_size,但毕设本地跑只要上面这个配置就够了。

7.3 静态资源404:文件能传就是打不开

文件明明存到了本地路径,访问http://localhost:8080/files/xxx.mp4却404,十有八九是addResourceHandlers没生效。检查三点:自定义配置类有没有加@Configuration注解;addResourceLocations的路径是否以file:开头且以/结尾;注册路径/files/**和前端拼的路径前缀是否一致。这三项都对了,刷新页面就通。

7.4 部署后前端路由刷新404

用Vue Router的history模式时,本地开发没问题,打包部署到后端后一刷新404。原因是SpringBoot默认找不到对应的Controller,直接返回404。解决方式是在后端写一个转发规则,把非/api和静态资源的路径转发到index.html,交给Vue Router处理。如果嫌麻烦,把Router改成hash模式,URL会多一个#,但对功能没影响,毕设场景完全够用。

7.5 文件路径被硬编码:换机器就废了

最常见的低级错误是把文件路径写成D:/upload/或者/root/upload/写死在代码里。换一台电脑就404。正确做法是放配置文件,用@ConfigurationProperties绑定一个fileProperties,部署时改一个配置项就行。同样的道理适用于数据库连接、端口号。最怕答辩现场换演示环境,整套系统全部崩掉,前面讲得再好也白搭。


我自己的体会是,这类平台型毕设最忌贪大,能把“上传-审核-展示-下载-统计”这条闭环讲透,已经是优秀水平。如果后续想在技术深度上做加分,可以试试把文件下载改成分片传输,或者给评论做敏感词过滤,又或者把统计页面接上ECharts图表。这些方向都保留在当前架构的扩展点上,不会破坏现有代码。项目做完以后建议打包一次生产环境试试,mvn package之后Java -jar直接跑起来,前端dist复制到static目录,同一个端口对外服务,既方便演示,又能顺手理解部署流程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询