“校园资料分享平台”这个项目我前前后后做了两周代码整理出来以后感觉特别值得写一篇完整的复盘。SpringBoot Vue3 MyBatis MySQL前后端分离涵盖用户注册登录、资料上传下载、分类检索、评论收藏、后台管理这几大块整套源码既能当学习项目也能直接作为课程设计或毕业设计的底子。如果你正在学Java后端或者准备用这套主流技术栈从零做一个能拿得出手的完整项目这篇内容可以帮你把整体脉络和关键细节一次性理顺。我会尽量把设计思路、数据库表结构、接口定义、前端页面组织、部署注意事项都讲透重点放在实操和踩坑上。1. 项目整体设计与技术选型1.1 这个平台要解决什么问题校园里的学习资料散落在各个群聊、网盘、私人邮箱里找一份靠谱的复习资料全靠运气。这个平台的核心就是把这些资料集中管理起来按课程、按学院、按资料类型分类支持上传、下载、评分、评论和收藏。打个比方它就是一个校园内网的“百度网盘 文库社区”用户既能找到别人分享的真题、课件、笔记也能把自己的整理稿上传赚积分或获得关注。站在学习和求职的角度做这个项目最有价值的不是“会写CRUD”而是通过一个完整闭环把日常零散的知识点串起来。注册登录涉及安全与认证文件上传下载涉及大数据量传输列表查询涉及分页与模糊搜索权限控制涉及角色与状态管理前后端联调涉及跨域和接口约定。每一环都是Java面试里常问的高频点做完这个项目再去面试面试官问“你有没有做过完整的Web项目”你至少能拿出一个成体系的东西来讲。这个项目适合哪几类人一是Java后端初学者想通过项目把SpringBoot和MyBatis真正用起来二是前端同学想练Vue3配合真实接口开发三是准备课程设计、毕业设计的学生需要一套结构清晰、可扩展、能讲清楚设计亮点的系统。源码我整理成了前后端两个独立目录部署时只需改配置文件里的数据库账号密码本地就能跑起来。1.2 为什么是SpringBoot Vue3 MyBatis这套组合选技术栈不能拍脑袋得看它解决什么、学完之后能不能迁移到实际工作。SpringBoot Vue3 MyBatis MySQL这个组合在当前国内中小型项目中几乎是出现频率最高的搭配没有之一。SpringBoot负责把Spring家族那套繁琐的XML配置收起来内嵌Tomcat一个jar包直接跑开发效率很高。它的自动配置机制让你不用手动管理Bean但底层仍然是Spring IoC和AOP那一套面试时“SpringBoot的自动配置原理”这类题就是从这来的。Vue3配合Composition API让前端组件逻辑更好组织组合式函数可以把资料列表、用户信息这种业务逻辑单独抽出来复用。MyBatis则是半自动ORMSQL由你掌控在复杂查询、动态多条件查询、报表统计上非常灵活不像JPA那样帮你自动生成SQL反而难调优。MySQL就不用多说了开源、稳定、生态好作为中小型系统的存储数据库完全够用。选择前后端分离架构还有一个现实原因。现在企业里前端团队和后端团队往往分开前后端分离不仅让两边可以并行开发还能独立部署、独立扩展后端接口将来接App或小程序都不用动。对个人项目来说分离架构更能模拟真实工作场景——你写完后端接口用Swagger或Postman自测再联调前端页面整套流程跟公司里一模一样。不夸张地说做完这个项目你基本就掌握了国内JavaWeb岗位日常开发的标配流程。2. 系统架构与数据库设计2.1 前后端分离的整体结构整个系统分成前端和后端两个独立工程。前端使用Vite作为构建工具创建Vue3项目配合Vue Router做前端路由、Pinia做全局状态管理、Axios做HTTP请求。页面方面包含用户端和管理端两套视图用户端有登录注册页、首页资料广场、资料详情页、上传页和个人中心管理端则放在一套带侧边栏的布局里包括用户管理、资料审核和分类管理功能。前端通过axios调用后端接口接口统一返回JSON格式数据。后端是标准的SpringBoot分层结构Controller负责接收请求参数和返回结果Service负责业务逻辑MapperDAO负责数据库操作。用户认证用JWTJSON Web Token登录成功后生成token返回给前端前端存储起来后续请求在Header里携带。文件上传使用本地磁盘存储数据库里保存文件的相对路径。整体结构清晰没有引入微服务和中间件一台服务器就能跑起来适合学习和演示。2.2 数据库表设计数据库设计的核心是“业务拆表”。六张表基本覆盖了平台的业务闭环用户表、分类表、资料表、评论表、收藏表、下载记录表。下面把每张表的关键字段和设计理由拆开说。用户表user主要字段有id、username、password、nickname、avatar、role、status、create_time。password存的是BCrypt加密后的密文绝对不允许明文入库这是个安全红线role字段用整数区分普通用户和管理员status字段控制账号是否被禁用。这里有一个容易被忽略的点用户名要加唯一索引否则注册接口并发时可能插入重复账号这个坑我后面排查问题时会重点提。分类表category字段比较简单id、name、parent_id、sort。parent_id支持二级分类比如“计算机课程”下可以挂“数据结构”“操作系统”这样资料归类更灵活。资料表resource是最核心的表id、user_id、category_id、title、summary、file_path、file_size、file_type、cover_url、download_count、like_count、status、create_time、update_time。file_path保存服务器上的相对路径file_size以字节为单位status有0待审核、1已发布、2已下架三种状态。设计时把下载次数和点赞次数冗余在这张表里目的就是列表页展示时避免每次都做聚合查询这是非常典型的用空间换时间的思路。评论表commentid、resource_id、user_id、content、parent_id、create_time。parent_id支持楼中楼为0表示一级评论。收藏表favoriteid、user_id、resource_id、create_time唯一约束加在(user_id, resource_id)上防止重复收藏。下载记录表download_logid、user_id、resource_id、create_time主要用来统计热门资料和做行为分析。2.3 核心接口设计接口设计遵循RESTful风格统一前缀/api用状态码区分业务结果。我定义了统一的返回结构Result包含code、message、data三个字段code200表示成功401表示未登录或token过期500表示服务器异常。前端axios拦截器统一处理这些状态码页面不需要每个接口都写一遍错误提示。核心接口大致分五类一是认证接口POST /api/auth/register和POST /api/auth/login二是资料接口GET /api/resource/page分页查询、GET /api/resource/detail/{id}详情、POST /api/resource/upload上传、GET /api/resource/download/{id}下载、POST /api/resource/like点赞三是评论接口GET /api/comment/list/{resourceId}、POST /api/comment/add四是收藏接口POST /api/favorite/add、DELETE /api/favorite/cancel、GET /api/favorite/list五是后台管理接口GET /api/admin/user/page、PUT /api/admin/resource/audit等。每个接口都区分了用户角色权限后台接口只有管理员token能访问。3. SpringBoot后端核心实现3.1 项目初始化与统一返回结构后端我建议直接使用Spring Initializr创建项目依赖选Spring Web、MyBatis、MySQL Driver、Lombok、Validation。创建完成后目录结构分三层controller、service、mapper再加上config、common、entity、vo几个包。common包里放统一返回类Result、全局异常处理器GlobalExceptionHandler、JWT工具类config包里放WebMvcConfig配置跨域和拦截器和MyBatis配置。统一返回结构不是可有可无的它决定了前后端联调是否顺畅。我见过的很多半成品项目接口返回一会儿是Map、一会儿是原始对象前端解析逻辑写得乱七八糟。我这里的Result类用泛型封装静态方法success(data)和error(code, message)快速构造返回对象配合全局异常处理器业务代码里只需要写return Result.success(vo)不管哪里抛异常返回给前端的格式都不会乱。全局异常处理器是SpringBoot里非常实用的一环。在类上标注RestControllerAdvice方法上标ExceptionHandler分别处理业务异常、参数校验异常和兜底异常。这样用户上传空文件、传参不合法这类情况都不会把堆栈信息赤裸裸地抛给前端而是统一返回“参数错误文件不能为空”这样的友好提示。实际开发里这一步尽早做不然后面每个接口都写try-catch会写到崩溃。3.2 JWT认证与权限拦截的实现思路JWT认证是这套系统里比较核心的一块。登录成功后后端用用户id和角色生成tokentoken的有效期设置了24小时。前端把token存在localStorage里每次请求在Authorization头里携带。后端拦截器拦截所有需要登录的接口从Header里取出token并解析解析成功就把userId放进请求上下文后续业务直接用。代码层面有几个细节值得说。拦截器实现HandlerInterceptor接口preHandle方法里做token校验如果Header里没有token或者解析失败直接返回401并写出JSON不进入Controller。这里要注意拦截器排除掉登录、注册、首页资料列表和文件访问路径否则用户在未登录状态下连公开内容都看不了体验会很差。WebMvcConfig里用registry.addInterceptor(authInterceptor).addPathPatterns(/api/).excludePathPatterns(/api/auth/, /api/resource/page)这种方式配置。JWT的密文密钥不要硬编码在代码里放到application.yml里通过Value注解注入这样换环境时只需改配置。密钥长度要够HS256算法要求至少32字节否则启动时就报错。生成token时用Jwts.builder()设置subject为userIdclaim里放角色过期时间用当前时间加上24小时签名时用Keys.hmacShaKeyFor()把密钥转成Key对象。解析token时Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)注意try-catch捕获JwtExceptiontoken过期或非法直接返回401。3.3 文件上传下载的一些处理要点资料分享平台的核心资产就是文件上传下载这部分做得好不好直接影响用户体验。SpringBoot处理文件上传时在Controller方法参数上用MultipartFile接收文件application.yml里配置spring.servlet.multipart.max-file-size和max-request-size。我设置的是100MB校园资料大多为PDF、PPT、压缩包、视频太小了不够用太大了服务器容易扛不住。需要说明的是这只是Spring层面的限制Nginx和服务器网关也要同步调大否则请求还是会被拦在外面。文件存储路径设计同样要注意。我采用“按日期分目录 UUID重命名”的方式比如/home/resource/files/2024/06/01/reandom-uuid.pdf。这样做有两个好处一是避免把所有文件堆在一个目录下单目录文件太多会影响磁盘检索性能二是UUID重命名避免文件名冲突和路径注入风险。原文件名用StringUtils.cleanPath提取存入数据库时单独存一个original_name字段用户下载时后端通过Content-Disposition响应头把真实名字返回给浏览器。下载的实现是重点也是坑点。很多新手直接用InputStream读取文件然后手动复制费劲还容易乱码。正确做法是先把文件包装成FileSystemResource设置Content-Disposition头为attachmentfilename用URLEncoder.encode处理中文名然后直接用ResponseEntity.ok().contentType(...).headers(headers).body(resource)Spring会帮你处理文件流性能好代码也简洁。下载前还要先判断文件在磁盘上是否真实存在不存在就返回业务错误否则前端会收到一个空的200响应用户根本不知道发生了什么。3.4 MyBatis分页与动态SQL实战MyBatis在这个项目里体现价值的地方一个是分页查询一个是动态多条件查询。分页我用的是PageHelper插件也是面试里经常被问到的“mybatis的分页插件的用法”。用法非常固定在Mapper方法执行前调用PageHelper.startPage(pageNum, pageSize)紧跟其后的第一条查询语句会被拦截并自动拼接LIMIT然后用PageInfo包装查询结果pageInfo.getTotal()拿到总条数pageInfo.getList()拿到当前页数据。具体代码PageHelper.startPage(pageNum, pageSize); ListResourceVO list resourceMapper.selectPageList(keyword, categoryId); PageInfoResourceVO pageInfo new PageInfo(list);这里有个重要细节startPage只对紧随其后的第一条查询生效。如果你的业务是先查询count再查询list或者查询前有其它无关SQL分页就可能失效甚至影响错误的数据。所以PageHelper.startPage必须紧挨着目标查询。动态SQL是MyBatis的另一大杀器。资料列表页支持按关键词模糊搜索、按分类过滤、只看已发布资料这三个条件组合起来就有8种情况。如果在Java代码里手动拼接SQL又丑又容易漏条件。用 标签配合 标签MyBatis会自动处理and和or的位置空条件直接跳过这是我最推荐的写法select idselectPageList resultTypecom.example.vo.ResourceVO SELECT r.*, u.nickname AS uploaderName, c.name AS categoryName FROM resource r LEFT JOIN user u ON r.user_id u.id LEFT JOIN category c ON r.category_id c.id where if testkeyword ! null and keyword ! AND (r.title LIKE CONCAT(%, #{keyword}, %) OR r.summary LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND r.category_id #{categoryId} /if AND r.status 1 /where ORDER BY r.create_time DESC /select关于MyBatis的缓存简单说一句这个项目里我没有开启二级缓存。资料平台的查询频率虽高但资料一发布就可能被更新状态加上多表关联查询缓存一旦失效反而要处理各种脏数据问题。一级缓存默认开着同一个SqlSession内重复查询会命中对性能足够。面试如果被问到MyBatis缓存机制能从一级缓存、二级缓存、缓存失效条件这几个维度讲清楚比盲目在项目里开二级缓存更能体现理解深度。4. Vue3前端核心实现4.1 用Vite快速搭建项目前端部分我用Vite创建Vue3项目比Webpack快很多。命令就一行npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 pinia axios element-plusElement Plus是必装的组件库后台管理页面里的表格、表单、弹窗、分页都能直接用现成组件比自己写样式高效太多。装完以后建议先把目录结构规划好src/api目录放按模块拆分的请求文件src/router放路由配置src/stores放Pinia状态src/utils放封装的axios工具类src/views放页面组件。这个目录规划越早做越好项目大了以后改起来很痛苦。路由设计上我采用了懒加载的方式页面组件用() import()动态导入首屏只加载当前页面需要的JSVite会自动代码分割。这和Vue Router的createRouter配合十分顺畅。同时我在路由配置里加了一个全局前置守卫判断用户访问需要登录的页面时如果没有token就跳转到登录页登录后根据角色决定能不能进入管理端。4.2 Axios封装与Token管理Axios封装得好不好直接决定前端代码的整洁度。我第一次写项目时每个页面里都直接调用axios.get每个接口都要写一遍token代码改个baseURL能改到哭。这次我做了统一封装代码里就清爽多了。封装的逻辑核心是拦截器。request拦截器负责从localStorage取出token并放到请求头response拦截器负责统一处理返回数据。后端返回结构是result.code、result.message、result.data拦截器里判断code为200就返回result.data非200就弹ElMessage提示并返回Promise.reject。特殊情况是code为401时说明token过期需要清除本地登录状态并跳转到登录页。这个逻辑统一放在一个地方页面代码只需要关心业务数据不用管错误处理。import axios from axios import { ElMessage } from element-plus import router from ../router const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) localStorage.removeItem(userInfo) router.push(/login) return Promise.reject(new Error(res.message)) } if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default service4.3 资料广场与上传页面的核心逻辑资料广场是用户端的首页核心是一个带搜索、分类筛选和分页的表格或卡片网格。页面结构上顶部是搜索框和分类下拉框中间是资料卡片列表卡片上展示标题、摘要、上传者、下载量、点赞数底部是分页组件。数据来源是GET /api/resource/page接口参数有pageNum、pageSize、keyword、categoryId。这里有一个Vue3组合式API的典型用法把列表查询逻辑抽到一个组合式函数里。比如在src/composables/useResourceList.js里暴露searchParams、list、total、loading、fetchList方法页面组件直接解构调用。这样列表逻辑可以复用代码也不臃肿。请求参数用reactive定义当keyword或categoryId变化时调用fetchList并重置pageNum为1分页组件的current-change事件只更新pageNum并重新请求。上传页面的核心是一个Element Plus的上传组件但需要注意它的v-model规则。el-upload默认的action参数是直接提交到后端但我们所有请求都走axios拦截器需要手动设置token。更稳妥的做法是设置auto-upload为false在用户点击提交时用FormData手动组装文件字段和表单字段调用封装好的uploadResource方法。这里我遇到过一个问题FormData里的file字段名要和后端RequestParam(file)对应否则后端收到的MultipartFile是null文件压根上传不上去。4.4 后台管理页面的实现思路后台管理页面我采用经典布局左边侧边栏右边内容区。侧边栏菜单包含用户管理、资料审核、分类管理三个模块通过路由切换对应页面。资料审核页面是后台最有代表性的一个。管理端通过tab切换“待审核”“已通过”“已下架”三种列表每组数据都有对应的操作按钮。审核通过时调用PUT /api/admin/resource/audit接口修改资料表和分类表的status字段。这个页面的布局和交互比较重复我采用了混入的方式提取表格的通用逻辑包括获取列表、修改状态、删除这样三个列表的代码量能减少一半。用户管理方面管理员需要能够搜索用户、查看注册时间、启用/禁用账号。禁用功能实现比较简单就是修改user表的status字段但这里要特别小心不能把管理员自己的账号禁用掉所以后端接口在操作前会判断目标用户角色。前端层面禁用按钮在点击后会二次确认防止手滑这也是后台系统的基本要求。5. 联调、部署与常见问题排查5.1 前后端联调的跨域问题前后端分离做联调时第一个拦路虎几乎都是跨域。前端的开发服务器跑在5173端口后端跑在8080端口两者不一致浏览器就会因为同源策略拦截请求。解决办法有两种我建议先用后端CORS配置解决开发环境的问题再在部署时用Nginx反向代理彻底规避。后端CORS配置很简单在WebMvcConfig里实现WebMvcConfigurer的addCorsMappings方法允许那几个来源和请求方式Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }需要注意allowedOriginPatterns不要写成allowedOrigins()因为allowCredentials(true)和allowedOrigins()不能同时使用浏览器会报“credentials mode is include”。这个问题我排查了很久是新手很容易踩的暗坑。生产环境推荐用Nginx做反向代理把前端静态资源和后端接口都挂在同一个域名下从根源上解决跨域。核心配置就一段location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这样的话前端请求的baseURL就不用写完整地址直接/api开头Nginx自动转发到后端。5.2 常见问题与排查速查表这个项目从开发到上线的过程中我记录了不少典型问题整理成速查表供大家参考问题现象排查方向解决方案前端请求接口报跨域Nginx配置 / CORS确认后端allowedOriginPatterns配置正确或统一用Nginx代理注册时提示用户名已存在数据库唯一索引给user表的username加唯一索引并在业务层先查重文件上传报MaxUploadSizeExceededExceptionSpring配置 / Nginx配置同步修改multipart.max-file-size和Nginx的client_max_body_size下载文件名乱码Content-Disposition编码对文件名做URLEncoder.encode并在Header里加上filename*PageHelper分页数据异常startPage位置确保startPage紧邻目标查询中间不要插入其它SQL登录后请求401token存储或拦截器逻辑检查前端是否在请求头携带token检查拦截器排除路径后台审核后前台看不到数据状态和缓存确认资料status已改为1确认查询条件包含status1JWT生成时报SecretKey太短密钥长度HS256要求密钥不少于32字节换成长字符串5.3 一些值得分享的避坑经验最后分享几个我在这个项目里特别想提醒的坑。第一个是Lombok在JDK新版本下的编译问题。如果你用的JDK版本高于17而Lombok版本太老编译时可能报错干脆用新版SpringBoot初始化的项目默认自带适配版本的Lombok不要手动去改版本号。第二个是业务逻辑不能写在Controller里。前期图省事把审核逻辑直接写在Controller方法里到后来想复用或者加事务发现根本没法改。尽量养成Service层写业务逻辑的习惯Controller只负责参数接收和结果返回这个习惯对后面接手真实项目帮助很大。第三个是关于资料下载的权限。我一开始没有限制下载权限任何人都可以下载结果下载记录表意义不大。后来改成了必须登录才能下载下载记录每次插入一条log热门排行就基于这个log表统计。这样下载次数更真实也能为后续做积分体系留好扩展点。第四个是数据库连接池的配置。默认的数据库连接配置在访问量上来后会频繁重连建议在连接池参数里给hikari连接池设置合适的最大连接数和连接超时时间尤其部署到云服务器时默认参数很可能不够用spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000最后再提一个实用的小技巧开发阶段在application.yml里开启MyBatis的SQL日志打印把后缀加上logging.level.com.example.mapperdebug就能在控制台看到每条SQL的实际执行语句和参数排查动态SQL问题特别直观。生产环境记得关掉避免SQL语句泄露数据库表结构。这套源码整理完之后我这次最大的收获是明白了“技术栈只是工具业务闭环和工程规范才是项目价值的核心”。如果有人拿这套源码去改造成其他领域的资料平台其实只需调整分类表和资料字段整体骨架完全可以直接复用。做项目就是这样把一套东西做扎实迁移到新场景时你会发现自己已经具备了独立设计的能力。