基于SpringBoot+Vue的实验室管理系统全栈开发实战
1. 项目概述与需求分析1.1 这系统到底解决什么问题实验室管理系统这名字听起来挺宽泛但落到实际场景里痛点其实非常集中。我接手做这个项目的时候对接的是一个高校的院级实验室他们原来的管理方式基本靠 Excel 加纸质登记表。器材借出要翻本子耗材库存拍脑袋设备维修记录经常丢学期末对账更是噩梦。所以这套系统的核心目的就三个字管清楚。管清器材在哪、管清耗材剩多少、管清谁借了什么什么时候还。也正是因为需求足够具体技术选型才没有悬念。后端用 SpringBoot前端用 Vue数据库用 MySQL持久层用 MyBatis。这套组合在中小型管理系统里几乎是标准答案——SpringBoot 负责快速搭建业务接口Vue 负责页面交互MySQL 存业务数据MyBatis 管 SQL 映射。对实验室这种体量的系统来说这套技术栈的性价比是最高的不重不轻刚好够用。1.2 谁适合参考这个项目如果你想拿这个项目练手或者你正好有类似的实验室管理需求那么下文提到的内容会对你非常有用。这个系统的功能边界主要围绕实验室日常运营来划管理员、教师、学生三类角色对应不同的操作权限。管理员管全局教师审批借用学生发起借用和归还。我会把设计思路、核心代码逻辑、数据库表结构、以及我踩过的坑一起讲清楚。你可以照着搭一遍也可以直接把它改造成适合自己场景的版本。我尽量把每一步的“为什么这么做”也讲明白而不是单纯贴代码。2. 整体架构与核心设计思路2.1 为什么是 SpringBoot Vue 这套组合先聊后端。SpringBoot 最大的价值在于“约定优于配置”它把 Spring 生态里繁琐的 XML 配置几乎都变成了自动配置。实验室管理系统这种 CRUD 为主的业务系统用 SpringBoot 开发效率极高。你只需要关注 Controller、Service、Mapper 三层逻辑而不需要花大量时间在 Bean 配置和依赖管理上。前端选择 Vue 则是因为它的组件化开发模式非常适合这种多页面管理系统。实验室管理系统天然包含多个功能模块——器材管理、耗材管理、借用记录、审批流程、统计报表。如果用传统 JSP 或 Thymeleaf 写页面之间的状态共享、数据刷新都是麻烦事。Vue 的组件化、响应式数据绑定和路由机制能把这些模块拆得干干净净。MyBatis 的选择逻辑更实际实验室管理系统的查询需求往往比较复杂比如按器材名称模糊查询、按借用状态过滤、按时间范围统计。MyBatis 允许你手写 SQL灵活处理这类动态查询条件同时还能通过 XML 文件把 SQL 和业务逻辑分离后期维护起来非常方便。2.2 系统功能模块划分我把整个系统拆成了六个核心模块这个划分方式在后面做数据库设计和接口设计的时候会一直沿用用户管理模块登录、注册、角色分配管理员/教师/学生、个人信息维护。器材管理模块实验室器材的基础信息录入、分类管理、库存管理、状态管理。耗材管理模块耗材的入库、出库、库存预警、消耗记录。借用管理模块学生发起借用申请、教师审批、借出登记、归还登记、逾期提醒。设备维护模块设备故障报修、维修记录、维护状态跟踪。统计分析模块借用频次统计、耗材消耗统计、设备使用率统计。这个划分方式的关键点在于把“审批”和“台账”分开。审批是流程台账是记录两者混在一起容易导致数据混乱。学生发起借用时生成的是申请记录教师审批通过后才生成真正的借用台账这样后续的归还、续借操作都以台账为准逻辑清晰。2.3 角色权限控制设计权限控制我选了最经典的 RBAC 模型Role-Based Access Control。数据库里就是三张核心表用户表、角色表、用户角色关联表。具体到这个系统我直接用了最简化的方案用户表存一个 role 字段值为 ADMIN、TEACHER、STUDENT 三种。为什么不全上 Spring Security 做细粒度权限因为实验室管理系统的核心需求是“功能级权限控制”而不是“数据级权限控制”。也就是说学生能看到什么菜单、能调用哪些接口这个必须控制但学生之间互相能看到谁的借用记录这个需求不强。所以用了拦截器加注解的方式在 Controller 层校验角色。如果将来要做数据权限隔离再把 RBAC 完整落地也不迟。提示不要一上来就堆 Spring Security JWT Redis 全家桶。权限控制和业务一样够用就好。后面我会单独讲为什么轻量方案在这个场景里反而更稳。3. 数据库设计与核心表结构3.1 表结构设计思路数据库设计是整个系统的地基我的建议是你花的每一分钟在这里都值得。实验室管理系统涉及的核心数据包括用户、器材、耗材、借用记录、维护记录所以我设计了这么几张核心表sys_user用户表存账号、密码MD5加盐加密、姓名、角色、联系方式。lab_equipment实验室器材表存器材名称、编号、分类、规格、所属实验室、状态可用/借出/维修/报废。lab_supply耗材表存耗材名称、规格、单位、库存量、预警阈值。lab_borrow_record借用记录表存借用单号、器材ID、借用人ID、审批人ID、借出时间、计划归还时间、实际归还时间、状态。lab_maintenance_record设备维修记录表存设备ID、报修人、故障描述、维修状态、维修费用。sys_notice系统公告表用于管理员发布公告。我这里重点说一下借用记录表的设计。这张表是整个系统最核心的一张表我特意加了两个时间字段plan_return_time和actual_return_time。前者是学生申请时填写的计划归还日期后者是实际归还时系统记录的时间。后续做逾期统计、信用评价都靠这两个字段。3.2 器材与耗材的区分设计很多初学会把器材和耗材混在一张表里这是一个常见的错误。虽然它们都有库存的概念但业务逻辑完全不同。器材是“固定资产”有唯一编号借出去必须归还支持维修和报废流程。耗材是“消耗品”出库即消耗关注的是库存量和补货时机。所以我用两张表分开管理。器材表用status字段管理生命周期耗材表用stock字段管理库存量。这个区分在后端 Service 层的代码实现里也有体现——器材借用要验证当前状态且更新状态耗材出库则只做库存扣减和流水记录。3.3 关键字段与索引设计注意事项建表的时候有几个坑值得提前说。主键建议用自增ID简单稳定不要用 UUID 做主键因为索引性能和存储空间都会吃亏。借用单号这种业务字段单独建一个唯一索引字段比如borrow_no用于展示给用户看方便查询和沟通。时间字段统一用datetime类型不要把日期存成字符串。排序、比较、统计查询都会更加高效和准确。如果你用字符串存日期七月份排序排在八月份后面这种经典坑绝对会踩。状态字段建议用 tinyint 或 varchar。我倾向于用 varchar 存英文枚举值可读性好前端也不用做额外的数字映射。比如器材状态存AVAILABLE、BORROWED、MAINTAINING。外键不要加这是我最想强调的一点。实际开发里外键约束只会成为性能瓶颈和运维灾难。表之间的关联关系在应用层维护通过 Mapper 里的 SQL 做 JOIN 查询。逻辑上仍然有外键关系但物理上不约束。4. 后端核心代码实现解析4.1 项目工程结构划分后端工程我按标准的 Maven 结构组织包名以com.labmanage开头com.labmanage ├── controller // 控制层接收前端请求 ├── service // 业务层核心业务逻辑 ├── dao // 数据访问层对应 MyBatis Mapper ├── entity // 实体类对应数据库表 ├── dto // 数据传输对象封装请求和响应 ├── common // 通用工具类、常量、统一返回结果 ├── config // 配置类 └── interceptor // 拦截器这个结构不是随便分的每一步都是为了降低耦合。比如 dto 和 entity 分开前端传来的参数不直接绑定到数据库实体上而是通过 DTO 接收和校验避免字段暴露的风险。统一返回结果我封装成了ApiResponse包含code、message、data三个字段。code 为 0 表示成功非 0 表示各种错误类型。这样前端拦截器可以统一处理错误码不用每个请求单独判断。4.2 用户登录与 Token 鉴权实现登录逻辑用的是最经典的方案登录成功后后端生成一个 Token我用的是 UUID 加自定义盐值加密后生成的存到 Redis 里设置过期时间。前端后续每次请求带上这个 Token后端拦截器解析 Token 判断登录状态和用户身份。为什么不直接用 JWT单独说一下这个问题。JWT 是无状态的服务端不存会话这对分布式场景是优点。但实验室管理系统是单机部署Redis 方案更简单可控。而且 JWT 一旦签发无法主动失效用户修改密码、管理员踢人这种操作实现起来很别扭。Redis 方案虽然多一次网络开销但对于这个量级的系统完全可以忽略。拦截器核心逻辑是这样的public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/api/user/login)) { return true; } // 从 header 里取 token String token request.getHeader(Authorization); if (StringUtils.isEmpty(token)) { return JsonResult.error(未登录); } // 验证 token 并获取用户信息 UserContext currentUser userService.getUserByToken(token); if (currentUser null) { return JsonResult.error(登录已过期); } UserContext.set(currentUser); return true; } }这个拦截器是全局注册的省去了在每个 Controller 里重复做登录判断的麻烦。用户信息塞到 ThreadLocal 之后Service 层在任何位置都能直接拿到当前登录用户。4.3 器材借用的完整业务闭环器材借用是核心流程涉及的操作链路比较长我完整走一遍。前端提交借用申请时传过来的是{ equipmentId, borrowDays, reason }后端 Service 收到后做三步校验public void applyBorrow(BorrowApplyRequest request) { // 1. 器材是否存在且当前状态为 AVAILABLE LabEquipment equipment equipmentMapper.selectById(request.getEquipmentId()); if (equipment null || !AVAILABLE.equals(equipment.getStatus())) { throw new BusinessException(该器材当前不可借用); } // 2. 学生是否存在黑名单记录或逾期未还记录 checkStudentCredit(CurrentUser.getUserId()); // 3. 生成借用记录状态置为 PENDING LabBorrowRecord record new LabBorrowRecord(); record.setBorrowNo(generateBorrowNo()); record.setEquipmentId(equipment.getId()); record.setStudentId(CurrentUser.getUserId()); record.setPlanReturnTime(LocalDate.now().plusDays(request.getBorrowDays())); record.setStatus(PENDING); borrowRecordMapper.insert(record); }这里有个业务细节值得注意申请阶段只生成借用记录不修改器材状态。等到教师审批通过时才把器材状态改成BORROWED。这样做的好处是如果申请被驳回器材状态从头到尾不需要回滚操作数据一致性风险更低。审批通过的操作也做了事务控制注意这里有个容易遗漏的操作——需要一次性更新两条数据。一条是借用记录的状态从 PENDING 改成 BORROWED另一条是实验室器材的状态从 AVAILABLE 改成 BORROWED。这两条数据必须放在同一个事务里操作否则一旦有一个成功一个失败数据库状态就会错乱。归还逻辑我用了延迟到课设结束后手动确认的方案。学生提交归还申请管理员在线下检查器材完好后在系统里点击确认归还。系统记录实际归还时间并自动计算是否逾期。逾期会产生一条提醒记录直接推给管理员。4.4 MyBatis 动态 SQL 的使用要点MyBatis 在这个项目里最频繁使用的功能就是动态 SQL。借用记录的查询条件很多变学生只能看自己的管理员可以按状态、按器材名称、按时间范围筛选。如果不用 MyBatis 的if标签你就得写多个 Mapper 方法或者手动拼接 SQL 字符串效率和可维护性都很差。典型的分页查询如下select idselectBorrowRecords resultTypecom.labmanage.entity.LabBorrowRecord SELECT * FROM lab_borrow_record where if testkeyword ! null and keyword ! AND equipment_name LIKE CONCAT(%, #{keyword}, %) /if if teststatus ! null and status ! AND status #{status} /if if teststudentId ! null AND student_id #{studentId} /if if teststartTime ! null AND create_time gt; #{startTime} /if /where ORDER BY create_time DESC /selectwhere标签会自动处理第一个条件前面的AND这个技巧非常实用。如果参数为 null对应的条件就自动跳过大大简化了代码。注意gt;是 XML 中大于等号的转义写法直接写符号会导致 XML 解析报错。分页这里我用了PageHelper插件。用法简单粗暴在查询前调用一行PageHelper.startPage(pageNum, pageSize)紧跟着的下一条查询就会自动拼接 LIMIT 语句并返回带总记录数的分页信息。这个插件在这个体量的项目里够用也好用。5. 前端页面与交互实现要点5.1 Vue 项目结构与公共组件封装前端我用的是 Vue 2 加 Element UI 的组合。想必你已经发现了这套组合在管理后台类项目里出现频率极高原因是 Element UI 的表单、表格、弹窗组件几乎是量身定做开发效率比手动写 CSS 高一个数量级。前端的工程结构如下src ├── api // 接口请求统一封装 ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── store // Vuex 状态管理 ├── utils // 工具函数 ├── views // 页面组件 │ ├── login.vue │ ├── equipment │ ├── supply │ ├── borrow │ └── dashboard └── App.vue我特别建议把所有后端接口调用集中在api目录下每个模块一个 JS 文件。比如api/borrow.js里导出applyBorrow(params)、approveBorrow(id)、returnEquipment(id)这些方法。这样前端任何一个页面里要发起请求只需要引入对应模块代码重用率高后期接口地址或参数变了也只改一个文件。5.2 路由守卫实现登录控制与权限拦截前端路由需要根据用户角色动态生成。Vue Router 提供了全局前置守卫router.beforeEach在每次路由跳转前检查登录状态和用户角色。核心逻辑是router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) return } if (token to.path /login) { next(/) return } const role store.state.user.role if (to.meta.roles !to.meta.roles.includes(role)) { next(/403) return } next() })meta.roles是路由元信息在路由配置时给每个页面标注允许访问的角色。比如借用记录页面的meta.roles是[STUDENT, TEACHER, ADMIN]而用户管理页面的meta.roles只有[ADMIN]。路由这层控制保证的是页面级权限配合后端接口的角色校验双保险。5.3 器材借用表单的关键交互细节前端写借用申请表单时有一个非常细节但是对用户体验影响很大的点计划归还日期的选择范围控制。学生提交借用申请时归还日期不能乱选最短借用 1 天最长借用 30 天超过范围应该在日期选择器上直接禁用。Element UI 的DatePicker组件提供了picker-options属性来控制日期可选范围。配置 disabledDate 函数在当前日期和最大借用日期之外的时间置灰用户想选都选不了。这个细节虽然简单但能从源头上过滤掉大量无效申请减少后端校验压力也让用户明白“最长只能借 30 天”这个规则。另一个交互细节是表格里的状态标签。借用记录有PENDING、BORROWED、RETURNED、OVERDUE、REJECTED五种状态直接用英文展示对用户很不友好。我封装了一个状态转换组件传入状态值显示对应颜色的中文标签待审批显示蓝色借用中显示橙色已归还显示绿色已逾期显示红色已驳回显示灰色。视觉上非常直观信息扫一眼就能确认。5.4 统计报表页面的数据可视化仪表盘页面用于展示核心指标我用ECharts做图表展示。按器材分类统计数量用饼图按月份统计借用量用折线图。前端把后端返回的原始数据做二次处理格式化成 ECharts 需要的结构。这里有个坑值得提醒ECharts 的图表容器在页面隐藏或刚加载时宽度为 0会导致图表渲染异常。解决办法是在mounted钩子里的this.$nextTick中调用chart.resize()方法或者在窗口 resize 事件里也挂上 resize 方法。如果你发现切换菜单后图表尺寸不对基本就是这个问题。6. 完整实操过程与环境搭建指南6.1 从零搭建开发环境第一步安装 JDK 和 Maven。JDK 版本建议 1.8 或 11Maven 版本 3.6 以上。装好之后确认环境变量配置正确命令行执行java -version和mvn -v都能正常输出。第二步安装 MySQL。版本建议 5.7 或 8.0。安装完成后需要创建一个数据库命令如下CREATE DATABASE lab_manage DEFAULT CHARACTER SET utf8mb4;这里要特别提醒字符集一定要指定utf8mb4不要用默认的utf8。utf8在 MySQL 里其实是 utf8mb3无法存储 emoji 字符而且某些生僻汉字也会存不进去。后面做耗材名称录入时如果有特殊符号容易触发 Incorrect string value 错误。第三步下载前端依赖。进入前端项目目录执行npm install这一步需要联网且比较耗时。如果觉得慢可以配置淘宝镜像源。安装完成之后目录下会出现 node_modules 文件夹说明依赖已经就绪。6.2 数据库初始化与测试数据准备项目里我准备了一个lab_manage.sql脚本包含了建表语句和基础的测试数据。你可以直接导入 MySQL 使用也可以用命令方式执行mysql -u root -p lab_manage lab_manage.sql我给了三组初始化账号来快速验证不同的权限效果角色账号初始密码管理员admin123456教师teacher123456学生student123456首登建议让用户修改默认密码这里仅用于快速演示和测试。导入测试数据的重点工作是填充一些真实的实验室物品数据比如示波器、万用表、焊台、函数信号发生器、电阻电容耗材包等。测试数据的好坏直接影响开发调试效率数据太少无法覆盖边界场景数据太多又干扰排查二十条左右是比较舒服的密度。6.3 前后端联调配置与启动顺序联调阶段最核心的配置文件是前端的接口请求地址。我开发时用的方案是后端接口运行在 9090 端口前端通过devServer.proxy配置代理解决跨域问题。配置文件在项目的vue.config.js中module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } } }这样前端页面发起的/api/xxx请求会被 devServer 转发到后端 9090 端口后端返回后按普通请求处理完全规避了浏览器跨域限制。生产环境下则通过 Nginx 配置相同效果的 proxy_pass 实现反向代理。后端的启动的配置在application.yml中server: port: 9090 spring: datasource: url: jdbc:mysql://localhost:3306/lab_manage?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 username: root password: 你的数据库密码 redis: host: localhost port: 6379 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.labmanage.entity优先启动 Redis 服务再启动 MySQL 并保证数据库已导入。然后运行 SpringBoot 主类看到 “Started Application” 日志说明启动成功。接着执行npm run serve启动前端浏览器访问http://localhost:8080就能看到登录页了。6.4 数据库备份与上线部署建议上线部署时分表其实不需要但这个项目的运维重点在于数据库的定时备份。实验室的数据如果丢失后续的清查和追溯将非常痛苦。我建议服务器用 cron 做每日备份0 2 * * * mysqldump -u root -p数据库密码 lab_manage /backup/lab_manage_$(date \%Y\%m\%d).sql保留最近三十天的备份文件即可再配合云存储做异地备份基本就能放心了。部署方式上推荐 SpringBoot 打成 jar 包后用系统守护进程如 systemd托管比用 nohup 裸跑更可靠崩溃能自动拉起。前端通过执行npm run build打包成静态文件再由 Nginx 做静态文件服务和 API 反向代理实现前后端分离部署。7. 常见问题排查与避坑实录7.1 数据库中文乱码问题现象后端保存到数据库里的中文变成???。原因字符集的坑。最常见的场景是安装 MySQL 时没有指定默认字符集或者 JDBC 连接串里没加characterEncodingutf8mb4。排查方式执行SHOW VARIABLES LIKE character%;查看当前字符集配置。如果看到character_set_server是latin1那就是安装问题了。解决路径修改 MySQL 配置文件 my.ini在[mysqld]下加character-set-serverutf8mb4。重新启动 MySQL 服务。数据库表在创建时指定 DEFAULT CHARSETutf8mb4。JDBC 连接串里显式加上characterEncodingutf8mb4。只要做到这四步乱码问题基本都能根治。7.2 MyBatisgt;报错问题现象Mapper XML 里写starttime #{startTime}项目启动直接报 XML 解析异常。原因XML 文件中和是特殊字符直接写会破坏 XML 结构。其实真相是在 XML 文件中不能直接使用小于号。解决方式转义写法写为lt;写为gt;。如果不想阅读困难也可以改用 MyBatis 的![CDATA[ ]]包裹。if testendTime ! null AND create_time ![CDATA[ ]] #{endTime} /ifCDATA 区段里的内容会原样保留不会被 XML 解析器当特殊字符处理。这个技巧建议所有使用 MyBatis 的人都记住。7.3 器材状态不同步的隐藏 Bug现象借用已审批通过器材列表里状态还是“可用”导致同一台器材被多人同时借用。原因审批操作里只更新了借用记录表的状态没有同步更新器材表的状态。两条 SQL 不在同一事务中执行出现部分成功。解决方式代码层面用Transactional注解确保两条更新操作在同一事务里。如果第二条失败第一条自动回滚。这也暴露了一个设计层面的经验凡是会修改多张表数据的操作必须明确标注事务范围并在写完代码后对测试用例覆盖这种异常场景。实验室器材这种跨表状态一旦不同步线下管理会立刻乱套。7.4 前端请求跨域失败的排查思路如果你发现前端页面登录功能时好时坏打开浏览器控制台看到Access-Control-Allow-Origin相关报错说明跨域配置有问题。正常情况下严格按照 6.3 节的 devServer 代理配置不会出现这个错误。容易触发跨域问题的场景往往是后端接口请求路径写错了走了绝对路径而不是/api开头的代理路径。我在开发时遇到过前端代码写死了http://localhost:9090/api/xxx导致浏览器直连后端接口被跨域拦截即使后端开启了 CORS 也偶尔出现 option 预请求报错的情况。经验是一切请求都走相对路径让代理层统一转发。前端任何 axios 请求的 baseURL 都配置成空字符串或/api不做全路径拼接跨域问题就和你基本无缘。8. 性能优化与后续功能扩展建议8.1 当前系统的性能瓶颈分析实验室管理系统在院校场景里并发量一般不会太高几十个人同时操作已经算多了所以在系统架构上不用特意强调高并发设计。但几个关键点还是值得提前优化一下索引优化是最性价比高的优化手段。借用记录表的状态字段、借用时间字段、器材表的分类字段都值得建索引。查询条件里高频出现的字段建了索引后数据量到十万级别也能保持毫秒级响应。列表接口不要查全字段。前端表格往往只需要展示核心字段而详情页才需要全部数据。列表查询的 SQL 里不要使用SELECT *把返回字段缩减到十几个。这能减少网络传输和数据库 IO大数据量下体感明显。静态资源交给 Nginx 缓存。前端打包后每个文件都有独立的 hash 值可以放心设置 immutable 缓存策略。首次加载后后续访问基本走浏览器缓存服务器的压力会大幅下降。8.2 可以继续扩展的方向后续扩展有很多非常实用的方向。消息通知渠道丰富化目前系统的通知是站内信形式。把借用审批结果改成邮件加站内信双重通知直接降低沟通成本。再往后可以接入企业微信或钉钉的 webhook 机器人审批一通过手机立刻收到消息。预约功能设备借用不只是当天申请教师和学生需要提前预约特定时段。扩展一张预约表和排期逻辑让系统具备简单的排课能力尽管增加复杂度但管理效率会明显提升。二维码扫码出入库给每台器材生成唯一二维码贴到设备上。管理员扫码即可完成借出和归还操作不需要手动搜索设备再点按钮。这个改造前端写一个扫码页面就行后端接口完全可以复用现有的业务逻辑。信用积分体系基于借用记录自动给学生计算信用分逾期扣分、良好归还加分。信用分低于阈值时自动限制借用权限。这是实验室管理从实名制走向精细化运营的一个很好的抓手。9. 项目部署实录与最终经验总结9.1 一次完整的部署流程记录用一台 2 核 4G 的云服务器做演示部署。系统装的是 Ubuntu Server这个配置跑前后端完全够用。安装基本环境apt update apt install nginx mysql-server redis-server openjdk-11-jdk maven后端项目在本地执行mvn clean package -DskipTests打成 jar 包传到服务器后放到/opt/labmanage目录。用 systemd 管理进程写一个 service 配置文件[Unit] DescriptionLab Manage System Afternetwork.target [Service] Typesimple Userroot WorkingDirectory/opt/labmanage ExecStart/usr/bin/java -jar /opt/labmanage/labmanage.jar --spring.profiles.activeprod Restarton-failure RestartSec10 [Install] WantedBymulti-user.target前端执行npm run build后把dist目录上传到服务器/var/www/html/labmanage。Nginx 配置的关键代码是server { listen 80; server_name labmanage.example.com; location / { root /var/www/html/labmanage; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files这一行是核心它能把前端路由的访问全部引导到 index.html由 Vue Router 接管页面分发。如果漏掉这行刷新某个子页面会出现 404。9.2 开发过程中关键的经验总结每次从头写这类管理系统我都把以下几点视为最大的收获第一技术选型不用过度纠结前沿与否看项目尺寸和团队熟悉度。SpringBoot Vue MySQL MyBatis 的组合虽然看起来不那么“炸裂”但它稳固、成熟、社区资料极多遇到任何问题都能很快找到答案。第二数据库设计的时间投入绝对值得。建表前多思考业务字段之间的关联想清楚哪些字段要建索引不要把业务复杂度留到写代码之后。每发现一个问题要在 SQL 层面打补丁就意味着代码里要堆更多的 if-else。第三权限控制的粒度要随时准备做加减法。现阶段用简单的角色字段就够但随着系统覆盖的角色越来越多、权限需求越来越细早期留好了 RBAC 的表结构做升级就不是伤筋动骨的事而是顺水推舟的正常迭代。最后的一个重要提醒就是一定要做数据备份。很多开发者在教学演示期不在乎数据丢失但真实上线后数据就是系统唯一的资产备份方案必须提前落定并反复演练恢复流程。9.3 这套系统的可持续迭代建议实验室管理系统这类低并发、重流程的业务系统最有价值的资产不是代码本身而是使用它的人每天沉淀下来的数据。设备使用记录、耗材消耗趋势、学生借用习惯这些数据积累到一定数量后完全可以引入简单的统计策略来辅助实验室的日常决策。比如月初导一张上月耗材消耗排行表就能很清楚地看到哪些耗材采购频率高、哪些库存长期积压。比如对长期借用未归还的设备主动发提醒邮件就能把逾期率压下来。这些都不是高深技术但能切实减少管理员的工作量提高整个实验室的运转效率。我个人在维护这套系统的时候有一个切身体会学生最关心的功能不是审批流程多完善而是“能不能查到我当前借了什么设备、什么时候到期、该去哪个柜子还”。把这些细节做到位系统的接受度就算成了。中间有几次学生在归还时才发现自己借的器材型号不对后来顺手加了一个学生端“我的借用”页签的到期时间倒计时提醒催还率立马上来了。如果你正在做类似的管理系统把时间花在这些真实的业务痛点上远比纠结某个代码框架的细节更有价值。