尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot+Vue3校园资料分享平台:前后端分离完整实战源码解析

SpringBoot+Vue3校园资料分享平台:前后端分离完整实战源码解析 做校园资料分享这块最头疼的往往不是某个技术点有多难而是一堆细碎的东西凑在一起后互相打架。文件上传、分类检索、积分扣减、权限校验、前后台联调每一个单拎出来都不算新东西但要把它们捏合成一个能真正跑起来、学生愿意用、管理员敢上线的系统里面值得抠的细节远比想象得多。这篇博文围绕我用 Java SpringBoot Vue3 MyBatis MySQL 从零搭起来的前后端分离校园资料分享平台把整个实现过程、关键代码、踩过的坑和排查思路完整梳理一遍给正在做类似课设、毕设或者想练手前后端分离项目的朋友一个可参考的完整版本。这个系统解决的核心问题很明确——校园里资料散落在群文件、个人网盘和聊天记录里没人统一管理也没法快速检索。平台要做的就是提供一个集中上传、分类归档、多条件检索、积分激励下载的闭环流程。适合谁看如果你的技术栈正好是 SpringBoot 和 Vue3想找一份覆盖鉴权、文件处理、事务控制、部署上线的完整工程参照或者你接手了类似源码但看不懂表结构和调用链这篇内容能帮你省下不少时间。标题里提到的源码不是噱头后面我把核心模块的表关系、接口设计、关键实现都拆开讲。1. 项目需求拆解与整体架构设计1.1 校园资料分享场景下的核心需求校园资料分享平台表面上是个网盘实际和通用网盘有本质区别。通用网盘看重存储容量和传输速度校园场景更看重资料的归属关系和激励流转。先说归属关系。一份课件按课程归属一门课程按专业归属专业又挂在学院下面。如果按通用网盘的文件夹思路来建目录层级会越挖越深后台管理时想批量调整归属就要改一整条路径非常痛苦。所以我一开始就把分类体系拍板成扁平的三级树学院 - 课程 - 资料而不是文件系统式的无限层级。每一份资料通过外键关联到课程ID课程再关联到学院ID查询时用 JOIN 或者冗余字段解决避免递归遍历目录结构。然后是激励流转。校园资料平台最怕只下载不贡献时间一长就成了僵尸库。我设计了两条规则上传资料获得积分下载他人资料消耗积分上传者被下载时获得额外积分。这样就把资料的贡献者和使用者绑定成一个循环。积分扣减必须和下载记录写入放在同一个事务里否则就会出现积分扣了但下载记录没写或者记录写了积分没扣这类对账问题这是整个系统里最容易出 bug 的地方后面我会专门展开讲。除了这两条主线还有几个附加需求管理员后台的资料审核防止有人传无关文件、公告通知、个人中心查看上传和下载历史。审核功能一开始没做后来上小范围测试时才意识到没有审核环节的开放上传平台一天就能被传进一堆东西。这个教训提醒我做平台类系统永远要把内容安全机制放在功能上线之前。1.2 前后端分离架构为什么是首选技术选型时考虑过用 Thymeleaf 服务端渲染一把梭毕竟传统 SSM 项目很多就是这么做的。但对这个项目来说前后端分离几乎是必然选择原因有三点。第一权限控制的复杂度不在一个量级。校园资料平台有学生、教师、管理员三种角色前端需要根据角色动态渲染按钮、菜单和路由表。服务端渲染方案里角色判断代码和页面模板混在一起每加一个角色判断就要动模板维护成本很高。分离后前端通过路由守卫和指令级权限控制后端只通过 JWT 中携带的角色信息做接口级权限验证职责边界非常清晰。第二文件上传和预览的交互体验。资料上传需要实时显示进度、上传完成后立即回显文件信息资料列表需要分页、筛选、排序无缝切换这些都是典型的重交互场景用 Vue 做比 jQuery 模板引擎顺手太多。Vue3 的 Composition API 配合自定义 hooks把上传进度、列表筛选这类逻辑抽出来复用代码比 Vue2 时代的 options API 整洁不止一档。第三前后端可以并行开发。这是实际项目里最现实的收益。后端把接口文档定好前端用 Mock 数据先行渲染页面后端所有接口用 Postman 先过一遍再和前端联调。整个开发周期能压缩三到五成。我见过太多前后端不分离的小组两个人改同一个模板文件来回冲突那种痛苦做过的都懂。1.3 技术选型背后的权衡先交代完整技术栈后端是 SpringBoot 2.7 MyBatis MySQL 8.0前端是 Vue3 Vite Pinia Element Plus鉴权用 JWT文件存储在本地磁盘反向代理用 Nginx。没有引入 Redis 和消息队列原因很简单——校园场景的并发量用不上引入中间件反而增加部署成本和学习成本。SpringBoot 选 2.7 而不是 3.x主要考虑 MyBatis 和相关第三方组件的兼容生态。3.x 基于 Jakarta EE很多老版本的 MyBatis 插件、代码生成器需要额外适配没必要在业务开发期纠结这种兼容问题。MyBatis 和 MyBatis-Plus 之间我选了原生 MyBatis因为平台有大量动态 SQL 场景多条件组合查询、批量更新状态原生的 XML 映射更容易控制和阅读。Plus 提供的 LambdaQueryWrapper 写起来快但 SQL 一旦复杂到需要子查询和多表关联还是得回落到 XML与其两套混用不如从一开始就用原生 XML。Vue3 这边用了 Vite 而不是 Vue CLI最直观的原因是冷启动速度和热更新体验。Vite 基于 esbuild 预构建依赖开发时不用打包整个项目改一行代码秒级刷新。配合script setup语法组件内部逻辑比 Vue2 清晰很多。状态管理选了 Pinia它对 TypeScript 的支持、模块化的 store 设计都比 Vuex 4 更现代而且没有 mutations 这层冗余概念写起来就是普通函数调用新手也很好理解。数据库周边MySQL 8.0 默认字符集是 utf8mb4可以直接存 emoji 表情这比 MySQL 5.7 省了一步初始化配置的坑。文件存储选了本地磁盘而不是 OSS一方面是项目不需要高可用和弹性扩展另一方面 Object Storage 服务需要额外的 AccessKey 配置和费用对毕设或者小型实战项目来说属于过度设计。我只需要保证存储路径可配置、上传请求带上权限校验、静态资源映射指向正确目录这个方案完全够用。2. 数据库设计与表结构细节2.1 核心表设计与关系梳理数据库设计是整个项目里最重要但很多人最不重视的一步。我见过太多人上来就建表写到哪里缺字段就加到哪里最后表结构变得面目全非。这个平台的表结构我按用户 - 内容 - 行为三个维度来划分一共七张核心表。用户维度是sys_user和sys_role。用户表存储账号、密码BCrypt 加密后的密文、昵称、学院、专业、积分、角色ID。注意积分字段我直接冗余在用户表上而不是每次都去 SUM 积分流水表因为首页和个人中心都要频繁展示积分冗余字段用一次 UPDATE 的代价换掉一次随时可能变慢的聚合查询非常划算。角色表就三条数据学生、教师、管理员配合 Spring Security 或自写的拦截器做接口鉴权。内容维度是category和resource。分类表维护树形结构用parent_id指向父级资料表是核心表字段包括标题、描述、文件原始名、存储文件名、文件大小、文件MD5、所属分类、上传者ID、审核状态、下载次数、上传时间。这里有个关键设计存储文件名用 UUID 重命名原文件名只作为展示字段避免用户上传张三的作业最终版v2(1).pdf这种文件名直接落到磁盘上引起非法字符和路径冲突问题。行为维度是download_record、comment和score_log。下载记录表每次下载写一条带上下载者ID、资料ID、扣减积分评论表关联资料ID和用户ID积分流水表记录积分的增加和扣减方便后台审计。三张表都建了对应的联合索引后面查询优化部分细说。E-R 关系上最核心的一条链是sys_user1 - Nresourcecategory1 - Nresourceresource1 - Ndownload_record。表之间我刻意没有建物理外键原因很实际校园项目的代码里到处都是先删主表再清关联数据的逻辑物理外键会把删除操作锁死反而限制业务灵活性。逻辑关联靠代码层保证配合定时清理孤儿数据比外键约束灵活得多。2.2 权限控制与数据隔离的实现权限这块最容易误做的设计是每个接口里写死角色判断比如管理员接口里 if (isAdmin)学生接口里 if (isStudent)。这种写法在需求变更时能把人逼疯。我的做法是基于 SpringBoot 拦截器 自定义注解 JWT 角色声明三层配合把权限搞干净。JWT 登录成功后payload 里写入roleId和userId签发时设置过期时间一般 2 小时。前端把 token 存进 Pinia 和 localStorage每次请求由 axios 拦截器自动加上Authorization: Bearer token头。后端自定义一个RequireRole(roleId 1)注解标注在 Controller 方法上拦截器里解析 token 后比较角色不匹配直接返回 403。这样一来业务代码里没有任何权限判断语句Controller 只关心自己的业务逻辑权限规则集中在一个拦截器里管理后期要加教师可以审核资料这种规则只需要改注解。数据隔离指的是不同用户能看到的数据范围不同。学生只能看到审核通过的资料管理员能看到所有状态但只能操作未审核的用户只能删除自己上传的资料管理员可以删除任意资料。这些规则落到 SQL 上就是在 Service 层拼条件查询列表时根据当前用户角色决定 SQL 里是否追加status 1删除时根据角色决定是否追加user_id #{当前用户}。这种查询即过滤的方式保证即使前端跳过按钮直接调接口后端也不会泄漏不该看的数据。2.3 索引设计与常见查询优化索引设计我遵循两个原则区分度高的列优先建索引查询频率决定索引优先级。实际建了这几组索引resource.status resource.category_id联合索引覆盖后台审核列表和管理员按分类筛选的场景download_record.user_id download_record.create_time联合索引支撑个人中心我的下载记录按时间倒序分页resource.user_id单列索引关联个人上传列表resource.md5建唯一索引用于上传时秒传判断如果 MD5 相同直接提示该资料已存在可直接跳转下载省一份磁盘空间。有个值得说的查询优化案例是多条件组合检索。资料列表页支持按标题模糊搜索、按分类筛选、按学院筛选、按上传时间排序条件可能全空也可能全填。这个需求如果用 Java 代码里 if else 拼接 SQL 字符串又丑又容易出 SQL 注入所以必须用 MyBatis 的动态 SQL。where标签自动处理 AND 前缀if标签逐条件判断需要排序时用choose选择一个排序字段杜绝外部传入任意列名排序。整个 XML 部分我把公共的sql片段抽出来比如查询字段列表、连表查询的 JOIN 条件多个查询复用改动字段时只改一处不会再出现列表新增了一个字段但导出功能没同步这种问题。分页优化上也踩过坑。MyBatis 分页插件 PageHelper 用法很简单PageHelper.startPage(pageNum, pageSize)后面紧跟第一条查询语句就生效。但这里有个隐蔽的坑startPage 之后的查询如果不是期望分页的那条 SQL会直接把分页参数误作用到其他查询上比如先查用户信息再查资料列表这种顺序敏感的代码。我的习惯是所有开启分页的查询方法单独成行方法内部第一行就 startPage绝不在两条查询之间调用宁可多写一个 Service 方法。另外 PageHelper 会执行一条 COUNT 查询再查数据数据量大时 COUNT 也可能成为瓶颈如果确认总条数对用户不是必须的也可以用自定义的 LIMIT 分页简化掉这条额外查询。3. 后端核心功能实现与踩坑记录3.1 基于SpringBoot的文件上传与预览方案文件上传是这个系统的核心命脉做得不好用户第一眼就会流失。上传接口采用 SpringMVC 的MultipartFileController 接收参数后Service 层做四步处理校验文件类型、计算 MD5、存储文件到磁盘、写入数据库记录。文件类型校验很多人只按扩展名判断.jpg改名成.pdf就能绕过所以我用Apache Tika 读文件真实的 MIME 类型比扩展名可靠得多。扩展名只是展示用MIME 类型才是安全判断的依据。允许的类型通常是 pdf、doc、docx、xls、xlsx、ppt、pptx、zip、rar、图片这些基本覆盖校园资料场景。上传大小限制在 SpringBoot 配置里设成spring.servlet.multipart.max-file-size100MB超过直接抛异常前端同步限制文件选择框的大小。需要提一句图片预览相对简单但 word 和 pdf 预览方案完全不同。pdf 可以直接用浏览器内置插件渲染docx 要先转 pdf 才行通常借助 LibreOffice 无头模式转换这块成本较高我在第一版里只做了 pdf 预览和图片预览Office 文件统一走下载不硬撑预览等项目跑起来再扩展。文件存储路径设计成/data/campus-resource/files/2025/06/按年月分目录存储文件名是UUID.pdf这种格式。路径拼接不要用字符串我用Paths.get()方法跨平台拼接避免 Windows 和 Linux 的分隔符问题。上传成功后 MySQL 里存的是相对路径绝对路径只存在配置项里这样将来迁移服务器或者换存储位置时数据库不用动只改配置项就行。静态资源映射也是一个坑点。SpringBoot 默认静态资源目录是 classpath 下的 static想要把磁盘上的外部文件映射成可访问的 URL需要在配置类里实现WebMvcConfigurer.addResourceHandlers()把/files/**映射到本地磁盘路径。千万别把文件放到项目目录内不然重新部署打 jar 包时新上传的文件会被一起打进 jar 或者直接丢失我就吃过这个亏后来老老实实放到独立目录。3.2 MyBatis多条件检索与分页插件用法资料检索场景如果用固定 SQL 写死后续每加一个筛选条件就得改一次接口所以动态 SQL 是这套系统的命门。我以资料列表接口为例把 XML 的写法完整贴一下select idselectResourcePage resultTypecom.example.dto.ResourceDTO SELECT r.*, c.name AS categoryName, u.nickname AS uploaderName FROM resource r LEFT JOIN category c ON r.category_id c.id LEFT JOIN sys_user u ON r.user_id u.id where if testkeyword ! null and keyword ! AND (r.title LIKE CONCAT(%, #{keyword}, %) OR r.description LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND r.category_id #{categoryId} /if if testcollege ! null and college ! AND u.college #{college} /if if teststatus ! null AND r.status #{status} /if /where ORDER BY choose when testorderBy downloadr.download_count DESC/when when testorderBy newestr.create_time DESC/when otherwiser.create_time DESC/otherwise /choose /select这段 SQL 的关键在where标签它会在条件非空时自动在开头补上 WHERE并且自动去掉首个条件的 AND 前缀避免用户只传了 keyword 没传 categoryId 时出现WHERE AND title LIKE ...的语法错误。实时验证的时候建议在 MyBatis 配置里打开控制台 SQL 打印mybatis.configuration.log-impl设为StdOutImpl开发期方便排查 SQL 拼接问题上线前再关掉。分页这里我直接说说 PageHelper 的细节。它是基于 MyBatis 拦截器实现的原理是拦截目标 SQL改写为带 LIMIT 的查询同时执行一条 COUNT 查询。用法只有一句话PageHelper.startPage(pageNum, pageSize); ListResourceDTO list resourceMapper.selectResourcePage(query); PageInfoResourceDTO pageInfo new PageInfo(list);这里有个坑必须提醒PageHelper 只对紧随其后的一条 SQL 生效。如果你在 startPage 和 mapper 调用之间写了别的 SQL比如先查询一次分类列表分页参数就会作用在分类查询上页面数据就全乱了。所以 Service 层我要求分页方法里第一行必须是PageHelper.startPage()这条规则写进了项目的代码规范后面接手的人也没再翻车。3.3 用户积分/下载权限的设计与并发控制积分逻辑是这个平台区别于普通网盘的核心模块我按下载扣积、上传增积、被下增积三个动作来设计最核心的是扣减和记录的一致性问题。下载流程拆成五步校验用户积分是否足够 - 校验资料状态是否可下载 - 扣减用户积分 - 写入下载记录 - 给资料上传者增加积分。这五步必须在同一个事务里否则任意一步失败都会导致账目不平。Spring 的Transactional注解默认只在 RuntimeException 时回滚所以 Service 方法内部如果 catch 了异常一定要手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()或者干脆不 catch 让异常往上传我最开始就是 catch 了异常导致事务没回滚积分扣了但记录没写用户投诉数据不对查了半天才定位到。具体代码大致是Transactional(rollbackFor Exception.class) public void downloadResource(Long userId, Long resourceId) { User user userMapper.selectById(userId); Resource resource resourceMapper.selectById(resourceId); // 业务校验积分不足直接抛出业务异常 if (user.getScore() resource.getScoreCost()) { throw new BusinessException(积分不足无法下载); } // 扣减上传者所在用户积分 userMapper.decreaseScore(userId, resource.getScoreCost()); // 写入下载记录 downloadRecordMapper.insert(new DownloadRecord(userId, resourceId, resource.getScoreCost())); // 上传者加分 userMapper.increaseScore(resource.getUserId(), resource.getScoreCost()); // 下载次数 1 resourceMapper.increaseDownloadCount(resourceId); }并发问题是个很容易忽略的点两个用户同时下载同一份资料理论上积分扣减是各自独立的问题不大但同一个用户双击两次下载按钮如果没做幂等控制就会扣两次积分。我的方案是前端点击下载后立即禁用按钮后端在 download_record 表建user_id resource_id的联合唯一索引插入重复记录时数据库会报错并自动回滚事务双保险。这个思路本质上是把防重从 UI 层下沉到数据库层比单纯前端禁用可靠得多。4. 前端Vue3实现与前后端联调4.1 Vue3项目搭建与组件划分前端用 Vite 脚手架创建npm create vitelatest选 Vue 模板然后装上 Vue Router、Pinia、Axios 和 Element Plus。Element Plus 的按需导入用unplugin-auto-import和unplugin-vue-components两个插件配置开发时自动按需引入组件和 API产物体积比全量引入小一半以上首次加载速度明显提升。组件划分上我把页面拆成几个核心块左侧分类树组件、中间资料卡片列表组件、顶部搜索栏、上传对话框、下载确认对话框。每个页面组件只负责组装业务逻辑抽到 composables 里。比如useResourceList这个 hook 封装了列表数据加载、筛选条件重置、分页切换、排序切换四个功能页面里只需要调用对应函数逻辑复用性很好。一个值得说的细节是分类树的加载策略。学院-课程-资料三级分类如果用递归组件一次性加载全部节点数据量大时首屏渲染会卡。我的做法是懒加载只先加载学院一层点击展开时按parent_id动态请求子分类。Element Plus 的el-tree支持lazy属性搭配load方法每次展开节点时请求对应子集性能完全可控。这个优化在数据量小的时候看不出差别但等分类涨到几百个节点时就是天壤之别。本地 Mock 联调也很重要。后端接口没写好之前前端页面不能干等我用 Vite 的server.proxy把/api前缀的请求代理到后端开发服务器同时后端接口还没就绪时临时用 Mock.js 拦一下。这样前后端并行开发后端接口 Ready 后前端代码几乎不用动因为请求路径和字段约定提前定好了联调只是验证数据准确性。4.2 登录鉴权与Token机制登录流程前端这侧核心是路由守卫 状态同步 响应拦截三件套。路由守卫用 Vue Router 的beforeEach判断访问目标是否需要登录权限Pinia 里存用户信息和 token页面刷新时从 localStorage 恢复防止刷新后登录态丢失。// axios 请求拦截 request.interceptors.request.use(config { const token useUserStore().token; if (token) { config.headers.Authorization Bearer ${token}; } return config; }); // axios 响应拦截处理 401 过期 request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { useUserStore().clearToken(); router.push(/login); } return Promise.reject(error); } );Token 过期是前后端分离最常见的坑之一。JWT 过期时间设 2 小时如果用户正好在下载资料的瞬间过期请求返回 401前端直接跳登录页会丢失用户正在做的操作体验很差。我的方案是当后端返回token 即将过期这个自定义错误码时比如剩余时间少于 10 分钟前端用 refresh_token 静默换新 token再重放一次原始请求只有 refresh_token 也失效了才跳登录页。这个无感刷新机制极大提高了下载这种长时间操作的完成率代码量也不大核心就是一个isRefreshing标志位防止多个请求同时刷新 token 造成并发覆盖。密码这块前端只负责传输加密HTTPS 下其实可以明文传输但开发环境没有证书后端用 BCrypt 对密码做不可逆哈希。千万不要在前端做复杂的加密后传到后端再脱敏因为拿到的还是同一个密文拦截者照样可以用来重放。BCrypt 自带盐值同一密码两次加密结果都不同安全性模型远比对称加密可靠。4.3 文件列表渲染与下载功能文件列表展示我用了卡片式计算属性驱动。资源卡片需要展示标题、描述、分类名、上传者、下载次数、文件大小、积分价格和封面缩略图。文件大小后端返回的是字节数前端格式化时用逗号分隔等方法处理避免出现10240000B这种反人类展示。格式化逻辑可以放在工具函数里用循环除以 1024 的方式算出 KB/MB/GB保留两位小数。function formatFileSize(bytes: number): string { if (bytes 0) return 0 B; const k 1024; const sizes [B, KB, MB, GB, TB]; const i Math.floor(Math.log(bytes) / Math.log(k)); return ${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}; }下载功能的链路是用户点击下载按钮 - 前端弹出确认框显示将消耗积分 - 用户确认 - 调用后端下载接口 - 后端校验积分数量并扣减 - 返回文件流地址。这里有两种实现方案一种是后端直接response.write文件字节流另一种是后端返回文件 URL前端用window.open打开。我选了后者因为直接写流虽然看着简单但会把大文件加载到内存而且没法做断点续传。后端返回已鉴权的临时下载 URLnginx 直接代理到磁盘文件做静态传输不仅性能更好还天然支持 Range 请求。校验逻辑前置到获取 URL这个接口里积分也只在这个前置接口扣避免了下载一半失败还扣了积分的尴尬。5. 部署上线与常见问题排查5.1 环境准备与MySQL初始化部署到 Linux 服务器时环境依赖其实只有三个JDK 1.8、MySQL 8.0、Nginx。JDK 用yum install java-1.8.0-openjdk或者直接把本地打包好的jre目录拷过去也行MySQL 8.0 的安装我推荐用官方 rpm 包而不是系统自带的 mariadb之前用yum install mysql-server装出来的可能是旧版本字符集和语法兼容性都踩过坑。MySQL 初始化时有两件事不能省。第一字符集必须指定utf8mb4建库语句写成CREATE DATABASE campus_resource DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;如果建库时不指定默认可能欠着 latin1后期写入中文乱码再改库又是加班。第二连接数和服务参数建议调整一下max_connections默认 151 对并发稍高的场景可能不够innodb_buffer_pool_size在 2G 内存的服务器上至少要给 512M不然查询一多就慢。这些参数改在/etc/my.cnf里重启 mysql 生效。后端打包用 Mavenmvn clean package -DskipTests打出来的 jar 包直接nohup java -jar campus-resource-1.0.0.jar --spring.profiles.activeprod 启动。生产环境的数据库密码不要写死在application.properties用启动参数或环境变量注入比如--spring.datasource.password${DB_PASSWORD}防止源码泄漏时连数据一起暴露。前端构建是npm run build产物在dist目录。Nginx 里做两件事location /指向 dist 静态文件location /api反向代理到 SpringBoot 的 8080 端口location /files指向磁盘文件存储目录做静态转发。server { listen 80; server_name campus.example.com; location / { root /opt/campus-resource/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /files/ { alias /data/campus-resource/files/; } }try_files那行是 Vue Router 的 history 模式必备的否则刷新/resources/list这种非根路径时 Nginx 会直接 404。这个坑几乎每个 Vite Vue Router 项目都会遇到先记下来。5.2 部署过程中的实际问题实际部署时踩得最狠的一个坑是上传文件后预览链接 404。前端上传成功后回显的 URL 是http://服务器IP/files/2025/06/uuid.pdf但在浏览器里打开却是 404。排查后发现是 nginx 的alias和root混淆了root是指定完整路径再拼接 URLalias是直接把 URL 映射到指定路径。location /files/里必须用alias /data/campus-resource/files/;而不是root用root会把/files/当目录前缀去找/data/campus-resource/files/files/2025/06/...自然 404。这个问题查了半小时写下来希望有人能直接避过。第二个问题是 jar 包启动时报端口被占用。这是因为旧进程没杀干净ps -ef | grep java找到旧进程kill掉就行。更规范的做法是启动脚本里先按端口号找到 PID 再 kill避免手动操作遗漏。我后来写了一个stop.sh一键停止旧服务再启动新的部署流程稳定很多。第三个问题是跨域。前后端分开跑在 8080 和 80 端口时前端 axios 请求/api会产生跨域。开发期通过 Vite proxy 解决生产期通过 nginx 的同域名反代解决所以完全不需要在 SpringBoot 里开启CrossOrigin。事实上如果生产环境配了 nginx 同域名代理还在 SpringBoot 里开了跨域等于主动把接口暴露给任何第三方网站调用安全上是绝对不可取的。5.3 常见问题速查表整理一份我在这个项目里遇到和排查过的高频问题配合排查方向和解决思路方便大家直接照着查。问题现象可能原因解决/排查思路上传大文件报 413nginxclient_max_body_size默认 1M在 http/server 块中设client_max_body_size 100M同时确认 SpringBoot 的 multipart 限制后端返回 400 而非 500时间格式、枚举值传错导致 Jackson 反序列化失败前端按后端约定的格式传值不能确定时先看 nginx/后端日志SpringBoot 会打印详细 binding 错误文件预览 404nginxalias/root用混或磁盘路径不存在先在服务器上ls文件路径确认文件在再看 nginx 配置里 location 的路径映射积分扣了但下载记录没写事务未捕获异常导致部分提交Service 方法不要吞异常用Transactional(rollbackFor Exception.class)把异常抛出到 Controller分页数据重复或者丢失PageHelper.startPage 不止在一处调用分页参数被复用每个分页方法里 startPage 和 mapper 调用之间不要插别的查询用后立即返回前端登录状态刷新丢失Pinia 状态未持久化在 Pinia store 里监听$subscribe或手动把 token 写入 localStorage初始化时读取前端打包后刷新某个路由 404Vue Router history 模式缺try_filesnginx location / 里加try_files $uri $uri/ /index.html;下载接口很慢后端直接转发文件流换成获取临时 URL nginx 静态转发的模式走代理静态文件最后一个问题值得单独说一句很多人前端写完 axios 封装后觉得万事大吉真到部署时才在控制台看到跨域、404、413 一窝蜂涌上来。前端联调阶段最好就把 nginx 代理配置和本地 proxy 配成一致的形态相当于提前暴露生产问题不要等部署再一次性排查。总结之外的一个小建议这一套系统做完最后的体会不是哪个框架有多强而是前后端分离项目里接口约定、字段命名、异常码体系这些软约束比技术本身更容易决定项目成败。JWT 里放什么东西、返回码什么时候用 200 什么时候用 401、时间戳是传字符串还是毫秒数这些一开始就要和后端同事或者自己定死写进接口文档比写代码重要十倍。还有一个很实际的建议如果你准备拿这套源码做毕设或者二开请先跑通上传 - 审核 - 下载 - 积分变化这条完整链路再去加花哨功能这条主流程稳了平台才算真正立得住。我们当时就是先保主流程再慢慢加私信、收藏这些次级功能整个迭代节奏没乱过。
返回列表