
搞Java Web的人都懂一旦项目里带上“多媒体素材管理”这几个字十有八九是在做毕设、课设或者公司的内部资源库系统。这类项目看着简单真做起来却逃不过三个坎一是素材文件的上传、预览、转存二是权限与分类的梳理三是前后端联调时的接口设计。如果把技术栈再限定在SpringBoot2Vue3MyBatis-PlusMySQL8.0这一套那就更考验你对“分离架构”的理解了。这套源码我实际跑过也带着学生用它完整过了一遍开发流程。先说结论它非常适合用来做毕业设计、快速搭建后台管理类系统或者给公司内部做一套带权限的素材库原型。整条链路由SpringBoot2提供后端APIVue3负责页面交互MyBatis-Plus操作MySQL8.0前后端通过RESTful接口通信。后面我会把项目拆开来讲从需求分析一直讲到部署排坑尽量把每一个设计决策背后的原因说清楚而不是只给你堆一堆代码。1. 项目整体设计与思路拆解1.1 这套系统到底解决什么问题先想想原始的痛点。一家公司或者一个团队图片、视频、音频、文档散落在U盘、网盘、微信聊天记录里找素材靠“我记得好像在哪个文件夹”发素材靠“你加我QQ我传给你”权限管理基本为零。这不叫管理系统这叫素材黑洞。这套系统的价值就在于此把散落的资源统一收口到一个Web平台上做到“分类清晰、检索方便、权限可控、操作有记录”。具体到功能层面核心模块一般包括素材上传与存储支持图片、视频、音频、压缩包、PDF等常见格式分类与标签管理树形目录加标签双重维度方便多角度检索检索与预览按名称、格式、上传时间过滤图片视频能在线预览用户与权限管理员、编辑、访客等角色控制谁能传、谁能删下载与操作日志记录谁在什么时间下载了哪个文件。你细看就会发现这其实就是大部分后台管理系统的一个缩影文件管理解决了“资源”问题用户权限解决了“安全”问题CRUD接口解决了“数据流转”问题。所以做完这样一个项目你学到的不只是“素材管理”而是整个Web后台系统的通用套路。1.2 前后端分离架构的选型逻辑为什么是SpringBoot2Vue3而不是JSPServlet这里有一个很实际的原因现在的企业级开发前后端分离已经是默认姿势你很难再找到一个新项目还在用服务端模板渲染整个页面。特别是当项目涉及多角色、多页面、多交互的时候把前端页面交给Vue这类框架去管理状态和路由把后端专心做成API服务两边各司其职开发效率和后期维护都会好很多。SpringBoot2在这套体系里的角色是“后端底座”。它自动配置、内嵌Tomcat、起步依赖简化Maven配置让你不用再被Spring那堆XML配置文件折磨。Vue3则负责所有的页面渲染和用户交互组合式APIComposition API写业务逻辑更灵活配合Element Plus这类组件库后台管理页面的开发速度能提升好几个档次。MyBatis-Plus的存在则是一个“效率增强器”。它把单表CRUD的SQL直接封装掉了你只需要写一个Mapper接口继承BaseMapper基础的增删改查就有了。复杂的多表查询和统计报表再自己写XML既灵活又不啰嗦。选MySQL8.0就更好理解了它是现阶段最主流的关系型数据库窗口函数、JSON类型支持都很完善配合MySQL Workbench或Navicat都很顺手。1.3 这套源码的技术栈全景图从技术栈角度我帮你把关键点梳理成一张表方便你对照自己项目里的版本选型层次技术选型核心作用版本建议前端框架Vue3页面渲染、状态管理、路由控制3.x前端UIElement Plus表格、表单、弹窗、上传组件2.x构建工具Vite本地开发热更新、生产构建4.x以上后端框架SpringBoot2RESTful API、自动配置、事务管理2.7.xORM框架MyBatis-Plus单表CRUD、分页插件、条件构造器3.5.x数据库MySQL 8.0数据持久化存储8.0权限方案JWT或Session登录状态管理与接口鉴权JWT推荐文件存储本地磁盘或OSS素材文件的物理存储本地路径即可我看过不少类似的毕业设计源码这套结构是其中比较“正规军”的一套。它没有乱七八糟的魔改每个组件干自己该干的活模块分层也比较清晰适合在它的基础上继续扩展功能。2. 核心技术点逐一解析2.1 SpringBoot2与MyBatis-Plus的协同方式先讲后端持久层。SpringBoot2里集成MyBatis-Plus非常简单引入依赖之后在启动类或者配置类上加一个MapperScan注解把扫描路径指向你的Mapper接口包就行。之后所有Mapper接口继承BaseMapperTT是对应的实体类。我在实际项目里最喜欢的几个能力按“使用频率”排序条件构造器Wrapper。以前你要查某个分类下的素材列表得先写SQL再写Mapper方法现在直接用LambdaQueryWrapperLambdaQueryWrapperMaterial wrapper new LambdaQueryWrapper(); wrapper.eq(Material::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Material::getName, keyword) .orderByDesc(Material::getCreateTime); ListMaterial list materialMapper.selectList(wrapper);这段代码的含金量在于like方法第一个参数是布尔值只有传入关键字时才拼接这个条件省去了“拼SQL字符串”这种又丑又危险的操作。你没看错MyBatis-Plus的Wrapper就是干这个的推荐你学一下条件构造器这是它最实用的卖点。分页插件。配置一个MybatisPlusInterceptor把PaginationInnerInterceptor加进去然后用PageT对象配合selectPage方法PageMaterial page new Page(current, size); PageMaterial result materialMapper.selectPage(page, wrapper); long total result.getTotal(); ListMaterial records result.getRecords();如果你的项目里素材数量过万分页就不仅是体验问题还是性能问题。我在实际项目里发现很多人分页查询慢是因为在循环里查数据库一定要养成用批量查询替代循环查询的习惯。2.2 Vue3组合式API与页面组织前端这块Vue3最大的变化就是组合式API。以前Vue2用Options APIdata、methods、computed各写一坨组件一复杂就“东一块西一块”。Vue3允许你按业务逻辑组织代码比如上传素材相关的变量和函数都放在setup里的同一段区域。我看这套项目的代码结构页面大体上是这样的套路script setup import { ref, onMounted } from vue import { getMaterialList, deleteMaterial } from /api/material const loading ref(false) const tableData ref([]) const queryParams ref({ pageNum: 1, pageSize: 10, keyword: }) // 加载列表 const loadData async () { loading.value true try { const res await getMaterialList(queryParams.value) tableData.value res.data.records } finally { loading.value false } } // 删除单个素材 const handleDelete (row) { ElMessageBox.confirm(确定删除素材“${row.name}”吗, 提示, { type: warning }) .then(async () { await deleteMaterial(row.id) ElMessage.success(删除成功) loadData() }) .catch(() {}) } onMounted(loadData) /script看明白没有所有的逻辑都直接平铺在setup里加载数据、删除操作、提示反馈从上到下读一遍就懂。这里有一个核心的技巧凡是跟列表加载相关的逻辑尽量收敛到loadData一个函数里。避免你在新增、删除、编辑后反复粘贴“重新加载”的代码。然后配合Element Plus的el-table、el-form、el-dialog、el-upload这些组件一个后台页面基本就是“表格弹窗表单”三条腿走路。素材管理系统比一般系统多一个“预览”功能这个用el-image加preview-src-list就能实现点击放大或者是视频用video标签直接播放URL。2.3 MyBatis-Plus与SpringData JPA怎么选很多人在做类似项目时会纠结为什么不用SpringData JPA要用MyBatis-Plus我个人的看法是这样的。JPA的优势是实体映射和关联关系管理适合领域模型复杂、对SQL可控性要求不高的场景。但它的劣势也很明显一旦你遇到多表联查或者复杂统计要么写JPQL要么用原生SQL调试起来并不比写XML轻松。很多同学对JPA的“懒加载”“N1问题”感到头疼本质是因为ORM框架把SQL隐藏得太深出了问题反而难排查。MyBatis-Plus走的是“半自动化”路线单表操作它帮你省掉SQL多表查询你写XML完全可控。你打开一个Mapper XML文件每一条SQL都是白纸黑字哪里慢、哪里错一眼就能定位。我的建议是这个项目用MyBatis-Plus正好。它既让你享受到单表CRUD的快捷又不至于在复杂查询上失去控制。至于“SpringData JPA和MyBatis-Plus有什么区别”这类面试题你从“自动化程度”和“SQL可控性”两个维度去答十拿九稳。提示如果你拿到的是别人写的源码第一件事别急着跑起来先去看application.yml里的数据源配置和Mapper XML的路径把环境对上了再启动否则报错会非常打击信心。3. 核心模块设计与实操要点3.1 素材实体表设计我看过很多失败的素材管理系统八成是表设计没做好。素材表的字段设计一定要想清楚“上传时记录什么、查询时需要通过什么筛”。一个合理的基础表结构大致长这样字段名类型说明idbigint主键IDnamevarchar(128)原始文件名urlvarchar(255)文件存储路径typevarchar(32)素材类型image/video/audio/filesizebigint文件大小字节category_idbigint所属分类IDuploader_idbigint上传人IDtagsvarchar(255)标签逗号分隔create_timedatetime上传时间如果你想把系统做得更专业建议加上md5字段用来做文件去重。为什么因为用户经常重复上传同一个文件如果没有MD5校验磁盘空间会迅速被垃圾占满而且会让人觉得系统“不聪明”。我在给项目加了这个字段后重复上传直接返回已存在的记录体验好了不止一点。还有一点必须提素材物理路径不要直接存“全路径”因为部署环境一变路径全废。存相对路径比如/upload/202403/xxx.jpg再配合一个配置项存储根目录这样无论是本地还是换Linux服务器只需要改一行配置。3.2 文件上传的核心实现选型文件上传是一个舆情系统里最容易“翻车”的环节。如果只做本地存储用SpringBoot处理MultipartFile代码如下PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(上传文件为空); } // 生成唯一文件名避免重名覆盖 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFilename UUID.randomUUID().toString().replace(-, ) ext; // 按年月建目录 String dateDir new SimpleDateFormat(yyyyMM).format(new Date()); String filePath uploadConfig.getPath() / dateDir / newFilename; File dest new File(filePath); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); return Result.ok(素材对象); }这段代码里有几个关键细节值得你留意transferTo方法是Spring封装好的比FileOutputStream手动写流省事但你得保证目标目录存在文件名必须用UUID重命名否则不同用户上传同名文件时相互覆盖等你发现文件“神秘失踪”就已经晚了按年月分目录是为了避免单目录文件过多Linux下文件系统在单个目录存储上万文件时性能明显下降。如果后续素材量大或者想要更方便的访问加速可以换成MinIO或者OSS。但本地存储模式对毕设和内部系统完全够用先求能用再谈架构升级。3.3 JWT登录认证与权限拦截素材管理系统不能任何人都能删除素材所以登录和鉴权必须做。这套项目采用的是JWT方案登录成功后后端签发一段Token前端每次请求都把它放在请求头通常是Authorization: Bearer xxx后端通过拦截器校验Token并解析用户信息。我建议你在做这一块时把“认证”和“授权”拆开理解认证你是谁登录时校验用户名密码通过后生成JWT。JWT的载荷部分可以放用户ID和用户名过期时间一般设置2到24小时。授权你能干什么在SpringBoot里实现HandlerInterceptor在preHandle方法里校验Token再把用户ID存到ThreadLocal或者request属性里供后续业务代码取用。管理员接口可以再附加一个角色判断public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; }到这里登录、分类管理、素材上传、列表查询、素材删除、用户管理这几个主流程就能闭合成环了。4. 实操过程与关键环节实现4.1 环境准备MySQL 8.0安装与初始化很多新人在装MySQL8.0这一关就卡住半天我把自己常用的安装方式走一遍。Linux上最简单的办法是Dockerdocker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEmaterial_db \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0装完用docker ps确认容器在跑然后连接数据库执行脚本docker exec -it mysql8 mysql -uroot -p123456进入MySQL后创建专用账号不建议项目里直接用rootCREATE USER material% IDENTIFIED BY Material123!; GRANT ALL PRIVILEGES ON material_db.* TO material%; FLUSH PRIVILEGES;如果你是在Windows上安装请一定注意MySQL 8.0默认使用caching_sha2_password认证插件而有些老版本的连接驱动不支持这个插件。你的SpringBoot项目里要确保依赖是mysql-connector-j的8.0.x版本否则连数据库时会报Public Key Retrieval is not allowed。解决方法是连接字符串加上allowPublicKeyRetrievaltrueuseSSLfalse。4.2 SpringBoot2项目初始化与配置用IDEA新建SpringBoot项目时Spring Initializr默认可能已经给你选了SpringBoot3.x但这个源码用的是SpringBoot2.7.x所以务必改成2.7.x否则很多写法会不兼容。核心的application.yml配置大致如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/material_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: material password: Material123! servlet: multipart: max-file-size: 200MB max-request-size: 500MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有两个细节我用红笔画圈map-underscore-to-camel-case一定要开。数据库字段create_time自动映射为实体属性createTime不开的话你每个字段都得写TableField烦得很。multipart上传大小限制要根据你的素材场景调整。默认是1MB传视频根本不够。我实测过直接配置200MB比较稳。但如果你的服务器带宽有限建议前端同时加上上传前的文件大小校验别让用户白白等了半天发现传不上去。启动类上别忘了加MapperScan(com.example.material.mapper)然后后端接口就能跑了。4.3 Vue3项目初始化与前后端联调前端这边用Vite创建项目npm create vitelatest material-web -- --template vue cd material-web npm install npm install element-plus axios vue-router piniasrc/main.js里要挂载Element Plus和路由import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(ElementPlus) app.use(router) app.mount(#app)然后配置Vite开发代理把前端的/api请求转发到后端8080端口这一步可以避免你在开发时反复处理跨域问题// vite.config.js export default { server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }配置好代理之后前端写请求时统一用/api/material/list这种路径后端Controller的RequestMapping(/api/material)就正好接上。前端请求跨域是开发中新手最头疼的问题用代理是Vite官方支持的方式建议别在SpringBoot里配CORS那是临时方案。4.4 后端核心Controller代码实现以一个完整的素材列表接口为例Controller层的代码风格应该是这样的RestController RequestMapping(/api/material) public class MaterialController { Resource private MaterialService materialService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword, RequestParam(required false) Long categoryId) { return Result.ok(materialService.pageQuery(pageNum, pageSize, keyword, categoryId)); } PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file, RequestParam(required false) Long categoryId) { return Result.ok(materialService.upload(file, categoryId)); } DeleteMapping(/{id}) public Result delete(PathVariable Long id) { materialService.deleteById(id); return Result.ok(); } }注意这里的Result是你自定义的统一返回体里面至少包含code、message、data三个字段。我强烈建议你在项目一开始就统一返回结构否则写到最后接口返回五花八门前端封装axios拦截器时会非常痛苦。Service层再做一个防SQL注入检查、参数校验、业务逻辑编排。Controller层保持“薄”不写业务只接收参数、调用服务、返回结果。这个分层习惯一定要养成它比你在代码里写多少个炫酷功能都重要。5. 常见问题与排查技巧实录5.1 数据库连接不上或者启动报错这类问题在开发中最常见我把排查顺序固定成三步。第一步确认MySQL8.0真的启动了docker ps或者Windows服务里看MySQL状态第二步确认账号密码和权限没问题用Navicat或命令行先手动连一下第三步看application.yml里的url、username、password有没有写错特别留意serverTimezoneAsia/Shanghai和allowPublicKeyRetrievaltrue这两个参数。还有一种情况是SpringBoot2.7自带HikariCP连接池对MySQL8默认连接超时时间敏感。如果你启动没问题但隔一段时间第一次请求特别慢或者干脆报Communications link failure那就在连接URL后面加上connectTimeout3000socketTimeout60000可以有效缓解。5.2 前端页面白屏或接口404页面白屏先按F12打开开发者工具看Console有没有报错。如果报错是Failed to fetch基本是接口没通如果报404大概率是请求路径写错了。很多同学容易犯一个错就是后端RequestMapping写了/api/material前端请求又写成/api/material/list/多了一个斜杠后端匹配不上。还有一种容易忽略的情况前端代理配置了/api但后端接口实际没有/api前缀。我建议后端所有接口统一加/api前缀要么用server.servlet.context-path要么在每个Controller上写死。保持前后端路径风格一致排查时会省很多时间。5.3 上传大文件时请求超时或失败首次配置项目时最容易卡在这个点上。文件上传报错一般有两个原因一个是spring.servlet.multipart.max-file-size没调大默认1MB根本不够用另一个是Nginx或网关层的client_max_body_size限制如果你用Nginx做了反向代理这个不调大后端配多大都没用。还有一个我实战中踩过的坑前端Element Plus的el-upload默认走的是Ajax请求如果后端接口要求登录别忘记在on-success回调里做拦截判断。我见过太多项目上传接口返回401但前端没有任何提示用户还以为传成功了其实文件早被拦截了。5.4 MyBatis-Plus分页失效或查不出数据如果你在SpringBoot2项目里集成了MyBatis-Plus 3.5.x记得分页插件一定要显式注册。我发现很多源码虽然加了PaginationInnerInterceptor但是Bean没有生效结果selectPage返回的total永远是0。解决的办法是配置一个MybatisPlusInterceptor的Bean而且要注意MapperScan扫描的包路径对不对。如果单表查询明明有数据却查不到先看实体类上有没有加TableName让MyBatis-Plus知道这个实体对应哪张表。默认情况下它用类名驼峰转下划线如果你的类名和下划线表名对不上照样查不到。6. 部署与项目扩展建议6.1 前后端分离打包与部署开发完之后部署其实也不复杂。前端执行npm run build生成dist目录里面是纯静态文件可以用Nginx托管。后端执行mvn clean package -DskipTests生成一个可执行Jar包然后nohup java -jar material-server.jar --spring.profiles.activeprod server.log 21 Nginx配置里把/api前缀的请求反向代理到http://127.0.0.1:8080静态文件直接指向前端dist目录。这样一个单机部署方案就完成了。提示如果你的服务器内存有限推荐给JVM设置初始堆与最大堆比如-Xms256m -Xmx512m防止Jar包启动后把内存吃满导致进程被系统杀掉。6.2 从毕设到生产级还差哪几步这套系统作为毕设和原型绰绰有余但如果你要拿到真实业务里用有几个方向值得扩展。第一文件存储要接对象存储。本地磁盘方案在单机够用一旦需要扩容或者多实例部署就会出现“A服务器上传的文件B服务器查不到”的尴尬。换成MinIO之后上传和访问都走统一地址这个问题就烟消云散了。第二权限模型可以升级成RBAC。如果项目里角色不止“管理员/普通用户”两种建议把权限拆成“用户-角色-菜单权限”三层。做法也清晰角色表、菜单表、角色菜单关联表在后端用注解或拦截器做接口级权限校验。第三素材检索可以引入全文搜索。MySQL的LIKE %keyword%在数据量过万后性能不好你可以先试试用MySQL的全文索引顶一阵等规模再大再考虑Elasticsearch。如果非要上ESSpringBoot2集成起来也不复杂加个spring-boot-starter-data-elasticsearch写个Repository就行。第四加上日志与监控。把用户下载、删除等重要操作写入操作日志表再用SpringBoot Actuator暴露健康检查端点这样系统挂了能第一时间发现。6.3 个人实操体会最后分享一点我做这类项目最深的体会技术栈只是表面真正值钱的是你对业务和异常边界的思考。比如上传文件时要不要做类型校验删除素材时要不要同时清理物理文件分类被删了下面的素材怎么办这些问题在需求文档里往往不会写清楚但写代码时一个都躲不掉。我处理“删除分类”这个问题时的做法是删除分类时先检查该分类下有没有素材有的话提示用户“该分类下存在N个素材请先迁移或删除”同时提供“将该分类下素材移动到默认分类”的按钮。这样既不会误删用户资源也不会让系统处于脏数据状态。你们如果拿到这套源码建议不要急着改功能先把项目完整跑起来用一条核心链路“登录→上传图片→查看列表→预览→删除”走通一遍。走通之后你再来决定改哪个页面、加哪个接口心里就有谱得多。这套代码没有高深到需要你啃源码的程度但麻雀虽小五脏俱全。认认真真把它跑起来、拆一遍、改几处你对SpringBoot2Vue3前后端分离开发的整个脉络会比单纯刷十套面试题都清晰。祝你们一次跑通。