资讯动态

SpringBoot2+Vue3前后端分离多媒体信息共享平台全流程实战

发布时间:2026/10/9 6:05:40 来源:尧图企业网站定制
兄弟们如果你正在找一套能直接跑起来、又能写进简历的 Java Web 前后端分离项目那“武理多媒体信息共享平台”这个 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 的组合值得你认真看一遍。这套东西我最近刚陪着几位同学从零复现了一遍踩了不少坑今天把整个拆解过程整理出来从库表设计到 Nginx 部署一次理清。这个平台说白了就是做一个高校内部的多媒体资源中心把课件、实验录像、课程视频、图片素材统一收拢起来支持上传、分类、检索、在线预览和管理员审核。相比网上烂大街的图书管理系统、商城系统它的业务场景更贴近真实需求而且多媒体文件处理这个点面试时很有得聊。它适合正在做毕业设计、想系统学一遍前后端分离开发、或者打算给简历加一个完整项目的同学。接下来我按自己的实操顺序来讲不搞虚的全是能落地的内容。1. 项目整体拆解这套系统到底解决什么问题1.1 多媒体共享平台的核心业务场景很多高校的资源其实是散的。课程录像在老师的 U 盘里优秀作业在 QQ 群里讲座视频在百度网盘里学生想找某个资料得挨个问。这个平台要解决的就是三件事资源统一入库、分类检索、权限控制。从业务角色来看系统只有两类人普通用户和管理员。普通用户登录后可以浏览资源列表、按分类筛选、在线预览、上传自己的资源上传的内容默认是待审核状态管理员登录后除了浏览还能审核资源、管理分类、管理用户。这里不需要复杂的 RBAC 多角色模型一个 user 表加 role 字段就够了别把简单问题复杂化。资源类型这块建议一开始就定好三种视频、图片、文档。类型不同前端预览方式就不一样后端存储和校验逻辑也不一样。你要是把类型设计成字符串枚举后续扩展音频或者其他格式也方便。1.2 为什么选 SpringBoot2 Vue3 这套组合这个技术选型放在 2026 年依然很能打。SpringBoot2 虽然官方维护期过了但国内企业存量项目和教程资料最丰富的就是 2.x 版本遇到问题基本都能搜到答案而且它对 JDK8 兼容最好很多学校的服务器环境还在 JDK8。SpringBoot3 强推 JDK17 和 Jakarta 命名空间新手折腾环境就要花不少时间没必要。Vue3 的组合式 API 用起来确实比 Options API 顺手尤其在管理后台这种组件复用场景多的项目里逻辑组织更清晰。搭配 Element Plus 做后台界面开发效率极高。MyBatis-Plus 则是国内中小项目的标配内置 CRUD 方法免去写大量 XML 的烦恼分页插件、条件构造器都很好用比裸 MyBatis 省事太多。MySQL8.0 就不用多说了稳定、性能好和 5.7 相比JSON 函数、窗口函数、utf8mb4 默认字符集都是实打实的提升。这套组合的通用性很强做完这个项目你换任何一套管理后台系统都能快速上手。2. 数据库设计多媒体资源平台的表结构规划2.1 用户与权限普通用户和管理员的差异化设计先画用户表。我当时的字段是这样CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码BCrypt加密, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 角色0普通用户 1管理员, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;这里有个容易纠结的点密码到底用什么加密。千万别用 MD5太容易被撞库。我用的 Spring Security 里的 BCryptPasswordEncoder单独引入工具类用就行不必引入整套 Security 拦截链后者对新手不友好。注册时加密登录时校验就这么简单。role 用 tinyint 而不是字符串查询效率高代码里定义常量就行。status 字段很重要管理员可以把违规用户禁用但我在第一版时漏了这个字段后面才补上你们设计表的时候直接带上。2.2 资源管理与分类文件表、分类表的关联思路资源表是整个系统的核心字段设计直接决定前端展示是否顺畅CREATE TABLE resource_info ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 资源标题, description text COMMENT 资源描述, category_id bigint(20) NOT NULL COMMENT 分类ID, resource_type varchar(20) NOT NULL COMMENT 资源类型video/image/document, file_url varchar(255) NOT NULL COMMENT 文件访问地址, cover_url varchar(255) DEFAULT NULL COMMENT 封面图地址, file_size bigint(20) DEFAULT 0 COMMENT 文件大小字节, uploader_id bigint(20) NOT NULL COMMENT 上传者ID, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待审核 1已发布 2已拒绝, download_count int(11) NOT NULL DEFAULT 0 COMMENT 下载/浏览量, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;分类表建议做成单表自关联支持两级分类就够了CREATE TABLE category ( id bigint(20) NOT NULL AUTO_INCREMENT, parent_id bigint(20) NOT NULL DEFAULT 0 COMMENT 父分类ID0表示顶级, name varchar(50) NOT NULL, sort_order int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;评论、收藏、浏览记录这三张表看需求来时间紧可以只做评论和浏览记录。评论表关联资源ID和用户ID浏览记录表在每次详情页请求时插入一条后续做热门推荐有数据基础。3. 后端项目实操SpringBoot2 MyBatis-Plus 落地记录3.1 项目初始化与依赖配置我用的是 Spring Initializr 生成的工程SpringBoot 版本选了 2.7.18这个版本在 2.x 系列里比较稳定。核心依赖只有这么几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency如果你用的 JDK9jwt 0.9.1 会报错需要额外加 jaxb 依赖或者直接换 jjwt 0.11.5 那套 API。建议跟我一样用 JDK8省心。核心配置文件长这样server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/media_share?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 servlet: multipart: max-file-size: 500MB max-request-size: 500MB jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted这里有几个细节要强调。第一数据库连接地址必须带 serverTimezone否则 MySQL8 会报时区错误。第二spring.servlet.multipart 的 max-file-size 我调到 500MB不然上传大视频直接给我抛异常具体多大多小你得根据自己的场景设置别照搬。第三map-underscore-to-camel-case 是 MyBatis-Plus 默认开启的你只要确认下没被自己改坏就行。3.2 JWT 登录鉴权与拦截器设计登录接口的思路用户提交用户名密码BCrypt 校验通过后用 JWT 生成一个 token 返回前端。前端拿到 token 存在 localStorage每次请求在 header 里带上。后端用一个拦截器统一校验白名单放行登录、注册、首页资源列表、文件预览这些接口。JWT 工具类网上有大量模板。我自己的做法public class JwtUtil { private static final String SECRET your-secret-key-here; private static final long EXPIRE 7 * 24 * 60 * 60 * 1000L; // 7天 public static String createToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }拦截器里注意一个关键点放行资源预览接口时不能只放行/upload/**你在 ResourceHandler 里映射的静态路径也要一起放行。具体映射位置在下面文件上传里详细说。我踩过的坑是前端 axios 请求拦截器里 token 读错了 keyheader 名写成了 Authorization但拦截器里取的是token前后不一致导致所有请求都 401。建议前后端统一叫Authorization值用Bearer前缀这是业界惯例。3.3 文件上传与静态资源映射的坑文件上传是多媒体项目的核心也是最容易出错的地方。我的实现是上传接口接收 MultipartFile用 UUID 生成文件名保留原扩展名存储到服务器本地某个目录然后在数据库里存访问 URL。PostMapping(/api/upload) public Result handleFileUpload(RequestParam(file) MultipartFile file, RequestParam(type) String type) { String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newName UUID.randomUUID().toString().replace(-, ) ext; String datePath new SimpleDateFormat(yyyy/MM/dd).format(new Date()); String dirPath uploadPath / datePath; File dir new File(dirPath); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(dirPath / newName)); String url /upload/ datePath / newName; return Result.success(url); }注意两个地方一是文件名用 UUID 重命名避免中文文件名和特殊字符造成 URL 解析问题二是按日期分目录存避免单目录文件太多。然后必须写一个 WebMvcConfigurer 把磁盘目录映射成 URLOverride public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath /); }这个映射不搞定你在前端video标签的 src 里写/upload/xxx.mp4就会 404。另外文件上传的速度也会影响体验网络环境差的时候建议前端先做分片或压缩这个后面讲前端再展开。3.4 分页查询与条件构造器的正确用法MyBatis-Plus 的查询是真的香但很多人分页配不对。你要先用配置类声明一个分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后 Service 里这样写public PageResourceInfo getResourcePage(int page, int size, String keyword, Long categoryId) { PageResourceInfo p new Page(page, size); LambdaQueryWrapperResourceInfo wrapper new LambdaQueryWrapper(); wrapper.eq(ResourceInfo::getStatus, 1) .like(StringUtils.hasText(keyword), ResourceInfo::getTitle, keyword) .eq(categoryId ! null, ResourceInfo::getCategoryId, categoryId) .orderByDesc(ResourceInfo::getCreateTime); return resourceInfoMapper.selectPage(p, wrapper); }LambdaQueryWrapper 里 eq 前面那个条件判断是关键keyword 为空就不拼这个条件。这种写法比手动拼接 SQL 干净一百倍。还有排序一定加不然列表刷新顺序乱跳。4. 前端项目实操Vue3 Element Plus 从零搭建4.1 工程初始化与路由配置前端我用的 Vite 创建不是 Vue CLI启动快、热更新快。npm create vitelatest media-web -- --template vue cd media-web npm install element-plus axios vue-router piniaVite 的版本现在都到 5.x 了但用法没大变。Element Plus 我直接全量引入就不搞按需导入那套了后台项目讲究开发速度打包体积大点无所谓。路由这块有个重点后台系统通常有布局套子路由的写法const routes [ { path: /login, component: Login }, { path: /, component: Layout, children: [ { path: , redirect: /dashboard }, { path: resource, component: ResourceList }, { path: category, component: CategoryManage }, { path: audit, component: AuditList } ] } ]导航守卫必须写不然任何人都能直接访问后台router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else if (!token) { next(/login) } else { next() } })很多人的项目卡在这一步token 存在了 localStorage但刷新页面后 Pinia 里的 userInfo 丢了。我的建议是核心用户信息也存 localStorage刷新后重新读取或者直接用 pinia-plugin-persistedstate 做持久化。4.2 视频、图片预览组件的封装思路多媒体平台比普通管理系统有意思的地方就是预览。图片好办Element Plus 的 el-image 自带 preview-src-list 实现点击放大视频就得自己封装了。我写了一个 MediaPreview 弹窗组件思路很简单父组件告诉它资源类型和 URL它根据类型渲染不同的标签。template el-dialog v-modelvisible :titletitle width70% video v-iftype video :srcurl controls autoplay stylewidth: 100% /video img v-else-iftype image :srcurl stylewidth: 100% / iframe v-else-iftype document :srcdocPreviewUrl stylewidth: 100%; height: 70vh / /el-dialog /template文档预览这里我多说一句PDF 直接用浏览器内置的 iframe 就能渲染Word 的话要么后端转 PDF要么用 Office 在线预览服务但 Office 服务需要外网访问本地测试不稳定。所以我最后做了一个方案上传文档时同时要求传 PDF 版本或者用 LibreOffice 定时任务批量转换。这个是很多毕设没做好的细节你们提前想好。Vue3 这里就体现出组合式 API 的好处了所有状态用reactive包起来逻辑清晰const state reactive({ visible: false, type: , url: , title: }) const open (row) { state.visible true state.type row.resourceType state.url row.fileUrl state.title row.title }4.3 接口对接与 axios 拦截器的取舍axios 实例一定要统一封装不要每次请求都写完整 URL。我的做法const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )baseURL 写/api开发环境靠 Vite 代理转发到后端 8080生产环境靠 Nginx 代理这样代码里不用感知环境变化很干净。State 管理我用了 Pinia但说实话这个项目级别全局状态就一个 userInfo 和一个侧边栏折叠状态不用 Pinia 也行。复杂的是上传表单的状态管理那个用 reactive 就够了。5. 环境搭建与部署MySQL 8.0、Docker、打包上线5.1 MySQL 8.0 安装与初始化要点本机装 MySQL8 我踩过一次大坑初始化后 root 密码默认是空的但连接工具连不上原因是 root 只允许 localhost 登录。大家用命令行进去改一下 root 的 host 就行。不过我更推荐用 Docker 装干净利落docker run -d \ --name media-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEmedia_share \ -v /my/mysql-data:/var/lib/mysql \ mysql:8.0这里 MYSQL_DATABASE 是容器首次启动时自动创建的库省事。但要注意字符集MySQL8 默认是 utf8mb4但为了保险你可以在 my.cnf 里显式配上[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci之前有人问我只改数据库字符集不改表字符集行不行。实际上建表语句里如果没指定 CHARSET会继承数据库的所以库级别设置对了基本就够用。但如果你已经有表了记得单独 ALTER TABLE 改一下否则中文乱码问题会折磨你到崩溃。5.2 前后端联调与跨域处理的两条路开发环境根本不需要后端配跨域Vite 代理就搞定了// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/login会被代理到http://localhost:8080/api/login浏览器无感不会有跨域报错。生产环境 Nginx 也要配反向代理location /api/ { proxy_pass http://127.0.0.1:8080/; }注意proxy_pass后面那个斜杠它会把/api前缀去掉再转发也就是说前端请求/api/login会变成后端的/login。如果你的后端 Controller 里写的是/api/login那 proxy_pass 就不能加斜杠。这个细节我在这套项目里栽过两次方案不统一前端、后端、Nginx 三处各写各的前缀导致生产环境 404。建议把所有路径统一规范后端接口一律不加/api前缀由网关层统一加。5.3 打包部署后端 jar 包 前端 dist 目录后端打包前确认 pom.xml 里没有把测试代码带进去我一般执行mvn clean package -DskipTests打出来的 jar 包直接扔服务器java -jar media-server.jar --spring.profiles.activeprod生产环境的配置建议独立出来application-prod.yml 里用环境变量注入数据库密码别把真实密码写进代码仓库。这个习惯从项目一开始就要养。前端打包更是要小心很多人npm run build出来是白屏大概率是资源路径问题。在 vite.config.js 里设base: ./改完这个静态资源都走相对路径不管部署到子目录还是 CDN 都不会断。Nginx 部署 Vue3 history 路由还有一个经典配置location / { try_files $uri $uri/ /index.html; }不配这个你刷新/resource页面就是 404因为 Nginx 不知道前端路由的存在。6. 常见问题与排错记录6.1 登录一直 401先分清是跨域还是鉴权失败我遇到过最典型的情况前端控制台报 401后端日志显示请求根本没进来。这种八成是 CORS 或代理没生效先用 Postman 直连后端接口验证接口本身是好的。Postman 通了说明后端没问题再从前端 F12 看 Network 里请求的 URL 是不是正确。我列个排查顺序照着做基本能定位检查点具体动作常见结果后端接口Postman 访问接口挂了还是没挂一目了然请求地址看 Network 面板实际 URL代理没生效会是完整地址或错误端口请求头确认 Authorization 字段存在拦截器加错了地方就看不到 header后端日志看拦截器是否打印排除白名单外请求白名单配置过度会放行掉不该放的请求JWT 解析失败还有一种隐蔽情况服务器时间和本地时间不一致导致过期时间判断不准确。你自己本机测试是不会发现的放在云服务器上特别容易踩先 date 命令看下服务器时间。6.2 MyBatis-Plus 常见的字段映射和自动填充问题我帮同学查过一个问题更新用户信息时数据库的 create_time 被置成了 null。原因是后端直接用传入的实体对象去 updateById而出参里 create_time 没值MyBatis-Plus 默认更新所有非空字段空值就给你设成 null 了。解决办法有两个一是更新前先查一遍原数据把 create_time 塞回去再更新二是用 UpdateWrapper 只更新指定字段LambdaUpdateWrapperUser wrapper new LambdaUpdateWrapper(); wrapper.eq(User::getId, userId) .set(User::getNickname, newNickname) .set(User::getAvatar, newAvatar); userMapper.update(null, wrapper);第二种方案更优雅我只更新需要变的列不动其他字段。自动填充这边如果不想每次手动 set createTime可以用 MyBatis-Plus 的 MetaObjectHandler 接口实现一个类搞定Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, Date.class, new Date()); this.strictInsertFill(metaObject, updateTime, Date.class, new Date()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, Date.class, new Date()); } }实体类里对应字段加TableField(fill FieldFill.INSERT)注解。这块配置一次开发时能省大量重复代码。6.3 文件上传失败、预览 404三板斧排查上传失败先看两个地方的错误信息后端控制台有没有 SizeLimitExceededException前端 Network 里返回的失败信息是什么。我遇到最多的是 multipart 大小没配500MB 视频直接挂其次是文件名带中文空格服务器返回 500因为文件系统编码问题。预览 404 就更简单了你在浏览器单独访问一下那个文件 URL。能访问就是前端组件问题打不开就是后端路径映射或 Nginx 静态资源配置问题。排查时直接看 Nginx error.log会告诉你是目录不存在还是权限不足。目录权限这块绝对不要让 Nginx 以 root 身份运行但存储上传文件的目录要给 nginx 用户读权限。大文件预览还有一个网络层面的问题视频播放器拖拽进度条时需要 Nginx 支持 Range 请求。Nginx 默认支持但如果你后端挂了静态资源处理就会出现播放器能加载但拖动没反应的现象。所以生产环境我强烈建议上传文件直接由 Nginx 或对象存储负责别走后端 Controller性能差太多。7. 个人体会与扩展建议7.1 这套项目适合谁来练手说句实在话如果你是零基础刚学完 JavaSE直接上来啃这套前后端分离项目会比较吃力。建议你先花两周把 SpringBoot 基础接口、MyBatis-Plus 增删改查、Vue3 基础语法过一遍再来看这套代码会顺畅很多。如果你已经写过单体项目那这套能帮你补上前后端分离、文件处理、JWT 鉴权这些实战经验含金量会体现在简历的技能栏里。7.2 可以扩展的三个方向做完这套基础版本后想加亮点的话我从易到难给你排三个方向。第一把本地文件存储换成 MinIO改动量不大但简历上能写集成对象存储面试官有共鸣。第二给列表页加 Redis 缓存和热门排行榜用定时任务统计资源浏览量这个扩展能展示你的缓存设计和任务调度能力。第三用 Elasticsearch 替代 MySQL 模糊查询做全文检索资源一多标题和描述的 like 查询是真的慢。我自己的体会是这项目真正的难点不在某个技术点而在完整链路的打通。从配置 MySQL8.0 环境、写后端接口、调前端页面到最终用 Nginx 部署上线每一步都会遇到意想不到的小问题但每解决一个你就对这个体系多一分理解。先把这套代码一次跑通再动手改自己的功能是最好的学习路径。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑