恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于SpringBoot+Vue的学校防疫物资管理系统解析
首页
资讯中心
/
基于SpringBoot+Vue的学校防疫物资管理系统解析
基于SpringBoot+Vue的学校防疫物资管理系统解析
发布时间:2026/9/9 4:28:16
最近在帮几个准备做毕业设计的同学看项目发现好几个人不约而同选了“学校物资管理系统”这个方向。这类管理系统在课设、毕设里确实非常常见但大多数同学找到的参考项目要么只有后端没有前端要么前端直接用非前后端分离的模板渲染能真正把 SpringBoot Vue MyBatis MySQL 这一套完整打通、并且附带清晰部署教程的项目并不多。这次要拆解的《前后端分离学校防疫物资管理平台系统》正好是一套能跑通全流程的完整项目后端是 SpringBoot MyBatis MySQL前端是 Vue 全家桶源码结构清晰SQL 脚本、启动步骤、服务器部署说明都给你备齐了。我会按自己实际阅读这套项目并动手部署的思路把业务模块、技术选型逻辑、后端核心实现、前端写法、本地启动步骤、云服务器部署流程以及我真实跑项目时踩过的坑一条条讲清楚。如果你打算拿它当毕设底子或者想通过一个完整案例把前后端分离的技术栈从头到尾串一遍按这个顺序读下来会顺很多。1. 从需求到模块学校防疫物资平台到底要管哪些事我每次拿到一个项目第一件事都不是打开代码而是先看业务。业务理顺了代码看起来才会有章法改起来也知道改哪里。很多同学一上来就翻 entity、翻 controller翻完就懵了这么多类到底谁调用谁先把这个项目的业务场景说清楚。学校这个场景下物资管理通常绕不开三类角色系统管理员负责账号开设、基础数据维护、查看全量数据权限最高。物资管理员/库管员负责日常入库、出库、盘点维护物资台账。普通教职工/院系申请员可以浏览可用的物资列表提交领用申请查看自己名下的记录。传统做法里这些工作大多靠纸质台账和 Excel 完成。问题是显而易见的库存数字实时性差、领用记录容易丢、月底统计报表全靠手工算、某类物资快用完时也没人及时知道。这个平台要做的就是把这些线下流程搬上线让每一次入库、出库都有记录让库存数字可查、可预警、可统计。1.1 功能模块总览从标题和源码结构来看项目功能基本可以分为下面这些模块我按重要程度排了个序模块功能描述登录与权限基于 JWT 的登录认证按角色区分菜单权限和接口访问权限物资类别管理维护物资的分类信息支撑物资台账的分类筛选物资台账管理物资的新增、修改、删除、查询包含名称、规格、单位、库存、安全库存等字段入库管理入库单录入保存后自动累加对应物资的库存并写入入库流水出库管理出库单录入校验库存充足后自动扣减库存并写入出库流水库存预警物资库存低于安全库存阈值时在列表和首页给出预警提示统计报表基于入库/出库流水数据生成图表比如分类占比、出入库趋势申请审批教职工提交物资领用申请管理员审核通过后自动出库1.2 核心业务流程整个系统最核心的一条链路是申请人员选择物资、填写数量提交申请管理员审核审核通过后扣减库存生成出库记录同时判断库存是否低于安全库存触发预警。这条链路看起来简单但落到数据库和代码上有两个关键点必须处理到位一个是扣库存时的并发问题另一个是事务问题。如果只是简单地“先查询库存再判断够不够再扣减”在高并发或多人同时操作时很容易超领。后面讲后端实现时我会专门展开这部分这也是这个项目里比较值得学习的地方。2. 技术选型复盘SpringBootVueMyBatisMySQL为什么是主流组合先说明一点这套技术栈不一定每个环节都是“最好的”但它胜在资料多、上手快、市场上认知度高。老系统还在用 SSH 或 SSM 那一套新项目尤其是中小型管理系统SpringBoot 基本是默认选择前端从 jQuery 开发模式演进到组件化之后Vue 和 React 成了主流ORM 层 MyBatis 则是国内 Java 圈子讨论最多、面试也最爱问的一个。所以这套组合用于学习和毕设性价比非常高。2.1 后端SpringBoot 到底省了哪些事如果你用过传统 SSM 项目一定记得那些繁琐的 XML 配置web.xml、spring-mvc.xml、mybatis-config.xml、数据源配置一不小心漏掉一个 bean 就启动失败。SpringBoot 用“约定优于配置”的思路把这些大量简化了。整合了内嵌的 Tomcat打成一个 jar 包就能直接跑不需要单独装容器。通过 starter 依赖一键引入 Web、MyBatis、MySQL 驱动等能力不用自己手工处理版本冲突。application.yml 一个文件搞定数据源、端口、MyBatis 映射等配置。自带 Spring MVC 的自动配置写 Controller 时基本不用继承什么父类。对于这个项目来说SpringBoot 2.x 配合 MyBatis 是最稳妥的组合网上能查到的问题解决方案也最多比追新版踩坑要省心得多。2.2 前端Vue 的价值在哪Vue 在这个项目里承担的是典型的 SPA单页面应用开发模式。它把页面拆成组件靠数据驱动视图更新。用户看到的不是一个个独立的 HTML 页面被反复刷新而是应用启动后前端路由器根据 URL 动态加载对应组件通过 AJAX 请求后端接口拿数据再渲染到页面上。组件化带来的直接好处是复用。比如物资列表表格、入库出库弹窗、分页条这些在多个页面会用到的东西抽成组件后写一次可以到处用。对毕设级的项目来说Vue 的生态也足够友好Element UI 提供现成的表格、表单、弹窗组件ECharts 提供图表路由和状态管理都有成熟方案不需要从零造轮子。2.3 ORM 层为什么选 MyBatis 而不是 JPA / MyBatis-Plus这是评论区最容易吵起来的话题。我的看法是对于想搞懂 SQL、想应对面试、想灵活控制查询的人来说原生 MyBatis 其实更适合学习。它把 SQL 写在 mapper.xml 里你可以精确控制每一条 SQL 的写法遇到复杂多表查询、动态条件拼装时优势很明显。JPA 虽然开发快但自动生成的 SQL 对新手来说像个黑盒出了问题不好排查。MyBatis-Plus 确实更省事CRUD 方法直接能用但标题写的是 MyBatis那我们就在 MyBatis 的体系下讲。学习阶段把 MyBatis 的if、where、foreach这些动态 SQL 用熟以后切到 MyBatis-Plus 也非常快两者本质是同一个底层思想。2.4 整体部署形态前后端分离的项目最终部署出来一般是这个形态Vue 项目执行构建后生成一堆静态文件HTML、CSS、JS部署到 Nginx 静态目录。Nginx 同时承担反向代理的角色当浏览器请求/api/xxx时把请求转发给后端的 SpringBoot 服务。SpringBoot 通过 MyBatis 操作 MySQL 数据库返回 JSON 给前端。这样做的好处是前端静态资源可以走 Nginx 的高并发处理后端服务只负责业务逻辑两者可以独立部署、独立扩容。开发的时候前端自己起一个开发服务器通过代理转发解决跨域也完全不影响联调。3. 后端编码实录数据表设计、登录鉴权和库存业务核心逻辑后端代码是整个系统的地基。我推荐按“数据库 → 实体 → Mapper → Service → Controller”的顺序去看这样最不容易乱。3.1 数据表设计从典型的实现来看项目里会包含这些核心表我先列一个表说明用途表名用途关键字段sys_user系统用户id, username, password, nickname, role, statuscategory物资类别id, name, remarkmaterial物资台账id, category_id, name, spec, unit, stock, safe_stock, update_timestock_record出入库流水id, material_id, type(1入库/2出库), quantity, operator, remark, create_timeapply_order领用申请单id, user_id, material_id, apply_num, status, audit_user, audit_time其中material表的stock字段是当前实时库存safe_stock是安全库存阈值。stock_record表用于记录每一次出入库的明细是统计报表的数据来源。apply_order表用于审批流申请记录先落库状态为待审核管理员审核通过后才真正扣减库存。关键表的建表 SQL 大概是这个风格我贴一部分供参考CREATE TABLE material ( id int NOT NULL AUTO_INCREMENT, category_id int DEFAULT NULL COMMENT 分类ID, name varchar(100) NOT NULL COMMENT 物资名称, spec varchar(100) DEFAULT NULL COMMENT 规格型号, unit varchar(20) DEFAULT NULL COMMENT 计量单位, stock int NOT NULL DEFAULT 0 COMMENT 当前库存, safe_stock int NOT NULL DEFAULT 10 COMMENT 安全库存阈值, remark varchar(255) DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT物资台账表;这里有个细节所有表的字符集尽量用utf8mb4因为 MySQL 的utf8编码只能存 3 个字节遇到 emoji 或者特殊生僻字会报错。表字段按实际业务取不用追求多够用且完整最好。3.2 登录与 JWT 鉴权登录流程并不复杂前端把用户名密码 POST 给后端后端用username查出用户记录用 BCrypt 对密码做比对比对通过后生成一个 token 返回给前端。前端把这个 token 存起来之后每次请求都放在 Header 里后端通过拦截器统一校验。为什么用 JWT 而不是传统的 Session因为前后端分离以后前端可能跑在 8081后端跑在 8080Session 依赖 Cookie跨域场景下 Cookie 的维护成本比较高。JWT 把用户信息签名进 token 里后端不保存状态服务端只需要校验签名和过期时间即可天然适合这类分布式的部署形态。拦截器的核心思路是拦截/api/**下的所有请求从请求头取Authorization字段解析 token校验通过后把当前用户信息放到ThreadLocal里方便 Controller 直接获取当前操作人信息。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } // 解析token拿到当前用户id和角色放入ThreadLocal return true; } }角色权限这里常见的做法是给不同角色分配不同的菜单和接口权限。简单实现可以用自定义注解RequirePermission(admin)配合 AOP 做接口级校验也可以在拦截器里判断角色字符串。具体实现程度看源码怎么写的但思路一定是“每个受保护接口必须经过认证敏感接口必须校验角色”。3.3 库存扣减的并发事务处理这段是整个后端业务里我认为最值得学习的部分因为它是典型的“看起来简单、做起来容易出错”的逻辑。很多人一开始会这么写Material material materialMapper.selectById(materialId); if (material.getStock() num) { throw new RuntimeException(库存不足); } material.setStock(material.getStock() - num); materialMapper.updateById(material);这个写法有一个问题如果在判断完库存之后、执行更新之前另一个请求也读到了同一份数据两个请求都会认为库存充足都会去扣减最终库存就变成负数了。这在并发场景下叫“超卖”。更稳妥的做法是把判断下推到 SQL 里配合事务一起使用UPDATE material SET stock stock - #{num}, update_time NOW() WHERE id #{materialId} AND stock #{num}这条 SQL 的意思是只有当当前库存大于等于本次出库数量时才执行扣减。MySQL 的行锁会保证同一时间只有一个请求能成功更新这一行更新后返回的影响行数如果为 0说明库存不足直接抛异常、回滚事务。事务则通过 Service 层的Transactional来保证扣减库存、写入流水、更新申请单状态这三步要么全部成功要么全部失败。Transactional(rollbackFor Exception.class) public void auditApply(ApplyOrder order) { int rows materialMapper.deductStock(order.getMaterialId(), order.getApplyNum()); if (rows 0) { throw new RuntimeException(审核失败库存不足); } stockRecordMapper.insert(new StockRecord(order.getMaterialId(), 2, order.getApplyNum(), 管理员, 审核通过出库)); applyOrderMapper.updateStatus(order.getId(), 2); }这里有一个新手高频踩坑点Transactional只有在异常被抛出时才会回滚如果你在事务方法内部自己try...catch把异常吃掉事务是不会回滚的。所以要让异常抛出方法外或者手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。3.4 MyBatis 动态 SQL 与分页物资管理页面有一个高频需求就是多条件组合查询按名称模糊查询、按分类筛选、按库存状态筛选。MyBatis 处理这种动态条件非常舒服用where配合if标签就可以select idselectByCondition resultTypecom.example.entity.Material SELECT * FROM material where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if teststockStatus 1 AND stock lt; safe_stock /if /where ORDER BY update_time DESC /select分页一般直接引入 PageHelper 插件在 Service 层先PageHelper.startPage(pageNum, pageSize)然后执行查询返回的结果会被自动包装成带total的分页对象。注意分页插件必须紧跟一条查询语句中间如果穿插其他查询会拿到错误的分页结果。另外MyBatis 的一级缓存是 SqlSession 级别的同一个 SqlSession 中执行相同的查询会走缓存二级缓存默认关闭开启后要特别注意如果手动改了数据库数据但缓存没刷掉会查到旧值。库存这种变化频繁的数据不建议开二级缓存。4. 前端编码实录路由守卫、API 封装和物资管理页面前端部分的核心思路是把网络请求统一封装把页面按路由拆开把可复用的 UI 片段抽成组件然后通过接口和数据串起来。4.1 前端目录结构一套典型 Vue 项目的源码组织方式大致是src/ ├── api/ # 每个模块的接口请求函数 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # 状态管理比如用户信息、token ├── utils/ # axios封装、工具函数 └── views/ # 页面登录页、物资管理、入库出库、统计报表等4.2 axios 请求封装axios 封装是整个前端项目的“水管”每个请求都会经过它。封装时最关键的几个点设置baseURL开发环境指向代理前缀/api生产环境同样是/api让 Nginx 统一转发。请求拦截器里从 localStorage 拿 token统一加到请求头Authorization。响应拦截器里统一处理后端返回的 JSON 结构比如code 200才算成功遇到 401 状态码清掉本地 token跳转登录页。const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.msg || 请求失败)) }, error { return Promise.reject(error) } )4.3 路由与权限控制前后端分离项目里路由控制分两个层面第一层是未登录的人不能进系统第二层是登录用户只能看到他角色对应的页面。Vue Router 的全局前置守卫可以统一处理这件事router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else { next() } })如果需要按角色控制菜单可以在路由配置里给每个页面标记meta.roles然后在守卫里比对当前用户的角色。这里要留意一个部署相关的问题Vue Router 如果用的是history模式URL 里没有#号看起来更干净但部署到服务器时如果用户直接刷新一个子页面比如/materialNginx 会去服务器上找这个路径对应的文件找不到就 404。解决办法是在 Nginx 里加try_files $uri $uri/ /index.html;。这个问题后面部署章节会再提。4.4 核心页面拆解登录页相对简单用el-form做表单校验提交后调登录接口拿到 token 存起来跳转首页。物资管理页则是一个标准的“搜索区 表格 分页 操作按钮”结构。搜索区包含物资名称、分类、库存状态点查询后重新请求列表表格用el-table渲染每一行有入库、出库、编辑、删除按钮分页用el-pagination切换页码或页大小后重新拉列表。入库、出库一般用el-dialog弹窗实现。表单里选择物资、填写数量、填写备注提交后调对应接口。提交成功之后要做两件事刷新当前列表把刚才的库存变化反映出来同时如果有必要刷新首页的预警统计。整个前端最容易忽略的其实是数据更新后的 UI 一致性。比如出库成功后库存数字变了但如果你只刷新了当前页表格没有同步更新首页的统计卡片用户就会觉得数据对不上。这里建议把获取列表和获取统计抽成单独的方法在操作成功后按需调用。5. 本地跑通全流程环境配置、SQL 脚本导入和项目启动不管是学习还是做毕设第一步肯定是先把项目在本地跑起来。本地跑通了后面的改造和二次开发才有基础。5.1 前置环境版本建议这套技术栈我用下来版本匹配建议是这样的软件建议版本备注JDK1.8稳定兼容性最好Maven3.6管理后端依赖Node.js14.x / 16.x太新的 Node 有时和旧依赖不兼容MySQL5.7 / 8.0utf8mb4 字符集IDEA2020自带 Spring 插件前端包管理器npm源不稳定时用淘宝镜像MySQL 8.0 和 5.7 在连接配置上有个小区别8.0 的驱动类是com.mysql.cj.jdbc.Driver5.7 是com.mysql.jdbc.Driver。用 8.0 时建议在 JDBC URL 后面加上serverTimezoneAsia/Shanghai否则会报时区错误。5.2 创建数据库并导入 SQL 脚本以 MySQL 5.7/8.0 为例先在命令行或 Navicat 里创建数据库CREATE DATABASE IF NOT EXISTS school_material DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后导入项目里自带的 SQL 脚本。项目的 SQL 文件一般在db/或sql/目录下用 Navicat 直接运行即可。导入完成后应该能看到 sys_user、material 这些表并且 sys_user 表里有一条初始化的 admin 账号。这里我建议你拿到项目后先检查一下 SQL 脚本里sys_user的密码字段确认是明文还是 BCrypt 加密串。如果是加密串那初始密码一般在部署文档里有说明如果是明文那你本地跑通后第一件事就应该改成 BCrypt 加密存储避免安全问题。5.3 后端启动前必须检查的配置用 IDEA 打开后端目录等 Maven 下载完依赖后重点检查application.ymlserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/school_material?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true jwt: secret: 你的签名密钥 expire: 7有几个容易踩的细节map-underscore-to-camel-case: true这个配置非常重要。数据库字段是category_idJava 属性是categoryId不开启下划线转驼峰查询结果映射出来 categoryId 永远是 null。mapper-locations 路径要和实际放置 mapper.xml 的目录一致不一致启动会报Invalid bound statement。JWT 的 secret 自己随便改一个长字符串不要用默认值。5.4 启动后端与接口验证配置没问题后直接运行 SpringBoot 的启动类看到 “Started xxApplication” 日志就说明启动成功了。为了验证登录接口可以用 Postman 或 Apifox 发一个 POST 请求POST http://localhost:8080/api/login Content-Type: application/json { username: admin, password: 123456 }正常情况下会返回一个 token。拿这个 token 作为Authorization请求头再去请求用户列表或物资列表接口如果能拿到数据说明后端这套链路是通的了。5.5 启动前端前端目录下执行npm install npm run servenpm install时间比较长如果网速慢会卡住建议先用npm config set registry https://registry.npmmirror.com切换成国内镜像。启动后访问http://localhost:8081用初始账号登录然后把物资列表、新增物资、入库、出库、统计报表都点一遍确认主流程没有问题。开发环境下前端的请求会通过vue.config.js里的 devServer.proxy 转发到http://localhost:8080这样本地开发是不存在跨域问题的。6. 云服务器部署实测jar包打包、Nginx代理与进程守护本地没问题以后很多人会想把项目部署到云服务器上一方面是毕设答辩时需要演示线上效果另一方面也是简历上的一个亮点。这步比想象中简单但也比想象中容易踩坑。6.1 项目打包后端打包在项目根目录执行mvn clean package -DskipTests打包完成后target目录下会生成一个xxx.jar这就是后端服务的可运行产物。前端打包在前端目录执行npm run build完成后会生成一个dist目录里面是编译好的静态文件。后面需要把这个dist上传到服务器或者直接打包上传然后解压。有一个点必须提醒如果前端项目里的接口地址写的是http://localhost:8080/api这种开发环境地址打包前一定要改成/api或者用环境变量区分生产环境。否则部署到服务器以后浏览器访问的是服务器的页面但接口请求却指向了用户本地的 8080必然失败。6.2 服务器环境准备服务器系统一般用 CentOS 7.x 或 Ubuntu 20.04需要在上面装好 JDK、MySQL、Nginx。没有图像界面的环境下基本都用命令行操作# 以 Ubuntu 为例 sudo apt update sudo apt install openjdk-8-jdk mysql-server nginxMySQL 安装好之后创建数据库并导入和本地一样的 SQL 脚本。要注意的是服务器上的 MySQL 默认只监听本地如果后端服务和数据库在同一台机器上那 Java 里的 JDBC URL 配置localhost就好如果你打算让本地连服务器的数据库需要改 MySQL 用户权限和防火墙但出于安全考虑不建议在生产环境开放数据库远程端口。6.3 Nginx 配置要点把前端dist目录里的内容上传到服务器的某个目录比如/usr/share/nginx/html。然后编辑 Nginx 配置server { listen 80; server_name 你的服务器IP或域名; root /usr/share/nginx/html; index index.html; # 前端刷新404问题 location / { 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有一个经典陷阱proxy_pass末尾到底加不加斜杠。如果写的是proxy_pass http://127.0.0.1:8080;不带斜杠请求/api/login会原样转发给后端变成http://127.0.0.1:8080/api/login这就要求后端 controller 的 RequestMapping 包含/api前缀如果写的是http://127.0.0.1:8080/;带斜杠前缀会被替换掉请求就变成http://127.0.0.1:8080/login。项目里如果后端接口统一带/api那就不带斜杠直接用第一种写法最不容易出错。配置完执行nginx -t检查语法然后systemctl reload nginx让配置生效。6.4 后端 jar 包的进程守护后端不能直接java -jar xxx.jar跑完就不管了因为一旦关掉终端进程就没了。更规范的做法是用 systemd 把它注册成系统服务。sudo vim /etc/systemd/system/school-material.service写入[Unit] DescriptionSchool Material Management System Afternetwork.target mysql.service [Service] ExecStart/usr/bin/java -jar /opt/app/school-material.jar Restarton-failure Userroot [Install] WantedBymulti-user.target然后sudo systemctl daemon-reload sudo systemctl start school-material sudo systemctl enable school-material sudo systemctl status school-material用journalctl -u school-material查看日志。如果进程启动失败多半是数据库连接地址、账号密码写错了或者端口 8080 被占用。6.5 部署后的验证与常见问题部署完成后浏览器访问http://服务器IP应该能看到前端登录页输入账号密码能正常进入系统。如果登录时报错按顺序排查后端服务是否活着systemctl status school-material或者直接curl http://127.0.0.1:8080/api/login看有没有响应。Nginx 日志有没有报错tail -f /var/log/nginx/error.log。防火墙有没有放行 80 端口云服务商的安全组也需要在控制台放行。数据库连接是否正常看后端日志里有没有 Connection refused。7. 免踩雷经验跨域拦截、刷新404和库存并发问题最后这部分是我在这次实战中觉得最有价值的内容。很多问题本身不难解决但没踩过的人根本想不到会在哪里栽跟头。7.1 本地开发时的跨域处理前后端分离必然面对跨域。本地开发时最好的解决方案不是在后端加一个允许所有来源的 CORS 配置而是用 Vue 脚手架自带的代理能力。在vue.config.js里配置module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样前端发请求时所有/api开头的请求都由开发服务器转发到后端浏览器始终只和localhost:8081通信不存在跨域。如果还需要后端支持外部调用再额外加一个 CORS 配置但要限制允许的来源不要图省事allowedOrigins(*)。7.2 前端刷新页面 404这个问题在 history 路由模式下必现。开发时没问题因为 Vue 开发服务器会把所有路径回退到 index.html部署后 Nginx 不会所以必须在静态文件配置里加try_files $uri $uri/ /index.html;。如果你发现部署后刷新首页没问题刷新http://IP/material这种深层路径就 404基本都是这个配置没加。加完nginx -t systemctl reload nginx就能解决。7.3 登录过期后页面表现异常如果后端返回 401 而前端没有统一处理常见的表现是页面某个请求报错但用户还停在当前页面操作一次报错一次再刷新就直接跳回登录页甚至白屏。好的做法是在 axios 响应拦截器里统一处理 401清除本地 token、跳转登录页并且最好用window.location.href强制刷新避免 Vue Router 内存路由状态和实际页面状态不一致。7.4 库存并发问题的复现与自测库存扣减的逻辑写完建议你用 JMeter 或 Postman 的并发测试功能测一下同一个物资设置库存为 10然后开 20 个线程同时提交出库 1 个看最终库存是不是 0是不是只有 10 个请求成功。如果扣出了负数说明你的扣减逻辑还是“先查后扣”不是“条件更新”。我自己实测下来用UPDATE material SET stock stock - #{num} WHERE id #{id} AND stock #{num}这种写法配合事务并发场景下基本不会出问题。这也是这个项目里最值得拿出来讲的一个点面试时跟面试官聊清楚很加分。7.5 编码和时区问题中文乱码和时区错误也是部署阶段的常客。JDBC URL 里一定要带这三件套useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiMySQL 建库时指定utf8mb4前端页面meta里指定charsetutf-8。三个环节都对了中文一般不会乱。最后再分享一个实用的扩展方向。如果你打算拿这套项目做二次开发我首推给申请审批流程接上工作流引擎比如 Flowable把现在简单的“管理员审核”升级成多级审批流其次是给物资管理加上 Excel 导入导出这个在真实勤务场景里几乎是刚需然后就是统计报表的升级从固定图表改成可配置的数据看板。这些扩展方向都能让你的毕设或项目经历在答辩时更有说头。我个人最大的体会是这种管理系统看上去“基础”但它把权限认证、事务控制、前后端联调、服务器部署这些工程化知识完整地串到了一起。能把这套项目从头到尾彻底跑通、再改出一个自己的亮点功能比闷头刷几十道面试题要更有底气。如果你在启动或部署时遇到了具体报错别慌按数据源、端口、Nginx 转发、后端日志这个顺序排查基础环境对了后面基本就顺了。