
1. 项目概述1.1 这个系统到底解决什么问题实验室器材管理这个题目在计算机毕设里属于经久不衰的类型。原因很简单任何高校、科研机构都有实验室任何实验室都有一堆设备要管。小到试管烧杯大到几十万的精密仪器谁来借的、借了多久、什么时候还、有没有损坏这些信息如果靠 Excel 或者纸质台账一旦设备数量超过一两百台立刻就乱了。实验室器材管理系统本质上做的是三件事器材台账数字化、借用归还流程化、库存统计自动化。把原来需要人工登记、人工盘点、人工催还的工作全部搬到系统里完成。对于毕设来说这个题目最大的优势在于逻辑清晰、业务闭环完整评委一眼就能看懂你做了什么也容易做出功能亮点。这套系统我按 Java 技术栈来做前后端分离架构。后端用 Spring Boot MyBatis Plus前端用 Vue Element UI数据库用 MySQL。这套组合是目前毕设项目里最主流的配置网上资料多、碰到问题能快速搜到解决方案而且对你后续找工作写进简历也有实际帮助——企业里大量中小型管理系统用的就是这套技术栈。1.2 系统核心功能拆解整个系统围绕器材这个核心实体展开功能覆盖了实验室器材管理的完整生命周期功能模块核心作用涉及角色用户登录与权限管理区分管理员和普通用户的操作范围管理员/普通用户器材信息管理器材的增删改查、分类、状态管理管理员器材借用与归还在线申请借用、管理员审批、归还登记全部用户耗材库存管理低库存预警、入库登记、出库记录管理员损坏与报修登记登记器材异常状态、维修进度跟踪管理员数据统计看板设备使用率、借用排行、库存分布可视化管理员其中借用归还流程是系统的业务核心。我的做法是让普通用户提交借用申请管理员审批通过后器材状态自动变为已借出用户归还时管理员确认器材完好状态并点击归还状态再变回在库。这个流程里包含了状态机的转换逻辑做的时候需要仔细设计后面会展开讲。2. 数据库设计地基打好了上面才好盖楼2.1 数据表结构规划数据库设计是这类管理系统的命门。设计得好后面写代码一路顺畅设计得不好改表结构能改到怀疑人生。我按照业务模块拆分了七张核心表用户表sys_user存放用户基本信息包含用户名、密码BCrypt加密存储、真实姓名、角色标识。角色我用数字区分0 代表管理员1 代表普通用户不引入独立的权限表。毕设阶段没必要把权限体系做太重通过拦截器判断 role 字段就能满足需求。器材表equipment这是系统的主表字段包括器材编号、名称、分类、规格型号、存放位置、当前状态、购入日期、价格等。其中状态字段用 tinyint 类型0 表示在库、1 表示已借出、2 表示维修中、3 表示已报废。状态设计成数字而不是字符串是为了后续做条件查询时效率更高也更规范。注意器材编号建议设计成业务编码比如EQ 日期 流水号。这样在界面展示时一目了然也方便后续对接资产盘点工具。借用记录表borrow_record记录每一次借用归还操作。核心字段有器材 ID、借用人 ID、借出时间、预计归还时间、实际归还时间、审批状态、备注。审批状态同样用数字标记0 待审批、1 已通过、2 已拒绝、3 已归还。一张表搞定整个流程不需要拆分成申请单和归还单两张表可以减少关联查询的复杂度。耗材表consumable管理非固定资产类物资比如试剂、手套、滤纸等。字段包括名称、规格、总库存、已用数量、预警阈值、存放位置。每次出库做库存扣减库存低于预警阈值时系统在首页弹提示。器材分类表equipment_category做树形分类支持一级和二级分类。字段就三个ID、父级 ID、分类名称。虽然简单但在查询和前端下拉联动时都靠它。报修记录表repair_record关联器材表和操作用户记录故障描述、报修时间、维修状态、维修结果。维修状态用字典0 待处理、1 维修中、2 已完成、3 无法修复需报废。操作日志表operation_log用 AOP 切面自动记录所有增删改操作包括操作人、操作类型、操作内容、操作时间、IP 地址。表结构设计简单但面试时说到系统安全性这就是一个加分点。七张表之间的关系也很清晰借用记录表通过 foreign key 关联器材表和用户表报修记录表关联器材表操作日志表关联用户表。我建表时统一使用 InnoDB 引擎和 utf8mb4 字符集前者保证事务支持后者解决生僻字和 emoji 符号的存储问题——别小看这个细节很多人在项目答辩演示时输入特殊字符就直接乱码场面相当尴尬。2.2 为什么不用单表设计我刚带过的几个学生刚开始都问过一个问题为什么要拆这么多表一个表把所有字段放进去不行吗单表设计的坑等你写到统计报表的时候就明白了。比如你要查近三个月借用次数最多的器材单表设计意味着你要在同一张大表里做多条件聚合查询数据量一上来索引再优化也顶不住。而拆分成器材表 借用记录表之后一条简单的GROUP BY equipment_id ORDER BY COUNT(*) DESC就能搞定。而且拆表之后器材基本信息和借用记录可以独立维护修改器材属性不需要动历史记录逻辑上更清晰。设计数据库之前强烈建议先用草稿纸画出业务流程图把每个角色在每个流程节点需要看到什么数据、操作什么数据列出来再反推需要哪些表和字段。这个习惯能帮你少走很多弯路。3. 后端实现Spring Boot 框架下的业务逻辑落地3.1 项目初始化与通用配置后端我用 Spring Initializr 生成基础工程Java 版本选择 1.8不是越新越好——JDK 8 的兼容性最好各种第三方库版本都不会出问题线上部署也好找环境。核心依赖就五个Spring Web、MyBatis Plus、MySQL Driver、Lombok、Validation。application.yml里需要配置的数据源信息这里直接给出我项目的配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/lab_equipment?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0两个关键配置点单独说。map-underscore-to-camel-case用于把数据库字段user_name自动映射到实体类属性userName极大减少手写 ResultMap 的工作量——要知道这套系统几十个字段手写映射是纯苦力活。MyBatis Plus 的逻辑删除配置是让删除操作变成更新的操作打上删除标记这样数据在数据库里还留着需要排查问题时能找到历史数据对毕设来说安全性更好答辩时也能解释这个设计思路。3.2 统一返回结果与异常处理写后端接口的第一步不是写业务代码而是先把返回格式定好。我定义一个ApiResponseT类包含code、message、data三个字段。所有的 Controller 接口统一返回这个对象前端不需要每个接口单独写错误判断逻辑。异常处理用RestControllerAdvice全局拦截。业务异常类BusinessException可以带错误码抛出后由全局异常处理器统一返回前端。这样做的最大好处是代码里不需要到处都是 try-catch业务逻辑能保持干净。比如用户借器材时库存不足直接在 Service 层throw new BusinessException(该器材当前不可借用)即可前端能收到清晰的提示信息。3.3 器材借用流程的实现细节器材借用是整个系统的核心业务我在实现时把流程拆成两步操作第一步创建借用申请public void createBorrowRequest(BorrowDTO dto, Long userId) { // 校验器材状态 Equipment equipment equipmentMapper.selectById(dto.getEquipmentId()); if (equipment null || equipment.getStatus() ! 0) { throw new BusinessException(该器材当前不可借用); } // 校验该用户是否有未归还的同器材借用记录 Integer count borrowRecordMapper.selectCount( new LambdaQueryWrapperBorrowRecord() .eq(BorrowRecord::getEquipmentId, dto.getEquipmentId()) .eq(BorrowRecord::getUserId, userId) .eq(BorrowRecord::getStatus, 1)); // status1 表示已借用未归还 if (count 0) { throw new BusinessException(您有未归还的同器材记录请先归还); } // 创建申请记录 BorrowRecord record new BorrowRecord(); record.setEquipmentId(dto.getEquipmentId()); record.setUserId(userId); record.setBorrowTime(LocalDateTime.now()); record.setExpectedReturnTime(dto.getExpectedReturnTime()); record.setStatus(0); // 待审批 borrowRecordMapper.insert(record); }这里最关键的是那条校验该用户是否有未归还的同器材记录。没加这条之前容易出现一个 bug学生借了一台仪器没还又想再借另一台系统还允许他操作等到管理员审批时才手忙脚乱地驳回。加了这条约束之后从源头就堵住了。第二步管理员审批审批通过则Transactional(rollbackFor Exception.class) public void approveBorrow(Long recordId, Long adminId) { BorrowRecord record borrowRecordMapper.selectById(recordId); if (record null || record.getStatus() ! 0) { throw new BusinessException(该申请记录状态异常); } // 再次校验器材状态防止审批期间器材被他人借走 Equipment equipment equipmentMapper.selectById(record.getEquipmentId()); if (equipment.getStatus() ! 0) { throw new BusinessException(器材当前已被借出无法审批通过); } // 更新借用记录状态 record.setStatus(1); record.setApprovedBy(adminId); record.setApproveTime(LocalDateTime.now()); borrowRecordMapper.updateById(record); // 更新器材状态 equipment.setStatus(1); equipmentMapper.updateById(equipment); }注意这里加了Transactional注解保证更新借用记录状态和更新器材状态两步操作在同一个事务里。如果第二步失败第一步会自动回滚不会出现系统里记录显示已借出但器材状态没变的数据不一致问题。这是整个项目里最有技术含量、也最能在答辩时讲出深度的地方——事务一致性问题评委很爱问。3.4 归还流程与器材状态流转归还逻辑比借用要多想一步不仅仅是把状态改回去还要登记归还时的器材状况。我的示意实现如下Transactional(rollbackFor Exception.class) public void returnEquipment(Long recordId, ReturnDTO dto) { BorrowRecord record borrowRecordMapper.selectById(recordId); if (record null || record.getStatus() ! 1) { throw new BusinessException(借用记录不存在或状态异常); } Equipment equipment equipmentMapper.selectById(record.getEquipmentId()); // 根据归还状况决定状态流转 if (损坏.equals(dto.getCondition())) { equipment.setStatus(2); // 维修中 // 同时创建报修记录 RepairRecord repair new RepairRecord(); repair.setEquipmentId(equipment.getId()); repair.setDescription(dto.getIssue()); repair.setReportTime(LocalDateTime.now()); repair.setStatus(0); // 待处理 repairMapper.insert(repair); } else { equipment.setStatus(0); // 回到在库 } equipmentMapper.updateById(equipment); record.setStatus(3); // 已归还 record.setActualReturnTime(LocalDateTime.now()); record.setCondition(dto.getCondition()); record.setRemark(dto.getRemark()); borrowRecordMapper.updateById(record); }这里引入了一个重要的设计决策归还时不直接硬编码判断损坏就改状态而是通过归还状况参数驱动状态流转。这样以后有了新的状态比如耗材不足需补充只需要在判断逻辑里加分支就可以不用动方法签名和调用方代码。归还流程里还应该检查是否超期。超期归还是在借用记录里打一个超期标记这个标记会统计进用户的借用信用记录里。因为设备数量多了之后不自觉的学生是存在的系统替他记住了管理员催还时也有依据。4. 前端页面实现核心页面开发流程4.1 工程搭建与路由设计前端用 Vue CLI 搭建工程Element UI 做组件库Axios 做 HTTP 请求Vue Router 管理路由。项目结构按页面模块组织不按文件类型分这样每个功能模块的代码都在同一个文件夹里维护起来更直观。路由设计上我用动态路由配合权限控制登录时根据用户角色动态挂载路由。// 基础公共路由 export const constantRoutes [ { path: /login, component: Login }, { path: /, component: Layout, redirect: /dashboard, children: [ { path: dashboard, component: Dashboard }, ] } ]; // 管理员专属路由 export const adminRoutes [ { path: equipment, component: EquipmentManagement }, { path: category, component: CategoryManagement }, { path: borrow, component: BorrowApproval }, { path: repair, component: RepairManagement }, { path: statistics, component: Statistics }, ];普通用户登录后只挂载器材浏览、个人借用记录、借用申请三个路由管理员多挂载器材管理、审批管理、报修管理、统计报表。用路由守卫判断登录状态和角色未登录跳转登录页。4.2 器材管理列表页开发要点器材管理页是核心页面包含搜索表单、表格、分页、新增/编辑弹窗。搜索条件我设置了三个器材名称模糊搜索、分类下拉选择、状态下拉选择。表格列展示器材编号、名称、分类、存放位置、状态、价格、操作列。状态列用 el-tag 标签渲染不同颜色区分不同状态el-tag :typestatusTagType(row.status) sizesmall {{ statusText(row.status) }} /el-tagfunction statusText(status) { const map { 0: 在库, 1: 已借出, 2: 维修中, 3: 已报废 }; return map[status] || 未知; } function statusTagType(status) { const map { 0: success, 1: warning, 2: danger, 3: info }; return map[status] || info; }新增/编辑弹窗用 el-dialog 包裹 el-form表单校验用 Element UI 自带的 rules 配置比如器材名称必填、价格必须是数字、存放位置必填。提交时注意把数字字段用 Number() 做类型转换不然前端传过去的是字符串后端 Integer 接收时虽然能自动转但万一前端传了空字符串后端直接报 400 错误。4.3 借用申请与审批页面联调普通用户的借用申请页面是一个独立的弹窗通过选择器材表里的某行数据触发自动带出器材编号和名称。用户只需要填预计归还日期和用途说明提交后通过 Axios POST 到/api/borrow/apply接口。审批页面是管理员视角的待办列表。我用两个 Tab 区分待审批和已审批。待审批列表每行数据有通过和拒绝两个操作按钮点击后弹出确认框同时可以填写审批意见。这里的前端交互要注意一点因为数据有延迟按钮点击后要立即 loading 状态防止用户重复提交。我用了一种简单有效的方式每个操作函数里判断this.submitting标志位处理完再释放。审批页面背后还有一个隐藏逻辑审批通过的时功能上要弹窗让管理员确认借用时长超过系统设定的最大借用期限比如 30 天时给出黄色警告。这一步是有实际意义的因为在校园实验室场景里某些高价值设备是不允许外借超过一周的。5. 统计看板与核心报表实现5.1 首页统计卡片数据首页 Dashboard 的四个统计卡片器材总数、在库数量、借出数量、维修数量。这四个数据本质是同一个器材表按状态字段做分组统计后端写一个聚合接口一次返回GetMapping(/overview) public ApiResponseOverviewVO overview() { OverviewVO vo new OverviewVO(); // 器材总数 vo.setTotalCount(equipmentMapper.selectCount(null)); // 当前各状态数量 ListMapString, Object statusCounts equipmentMapper.selectMaps( new QueryWrapperEquipment() .select(status, COUNT(*) as count) .groupBy(status)); for (MapString, Object item : statusCounts) { Integer status (Integer) item.get(status); Long count (Long) item.get(count); switch (status) { case 0: vo.setAvailableCount(count); break; case 1: vo.setBorrowedCount(count); break; case 2: vo.setRepairCount(count); break; case 3: vo.setScrappedCount(count); break; default: break; } } return ApiResponse.success(vo); }这个例子很好地说明了为什么要用数字状态字段而非字符串——这里 Group By 聚合后出来的一列就是整型完全不需要额外的映射处理。5.2 借用排行与使用率统计借用排行统计能从借用记录表里汇总出最常被借的前十台器材ListMapString, Object topBorrowed borrowRecordMapper.selectMaps( new QueryWrapperBorrowRecord() .select(equipment_id, COUNT(*) as borrow_count) .eq(status, 1) .groupBy(equipment_id) .orderByDesc(borrow_count) .last(LIMIT 10));这里 status 为 1 表示当前还在借出状态的记录统计的是当前被借用次数。当然如果想把历史全部统计进去可以去掉这个条件但实际业务里当前占用情况对管理员更有参考价值——设备是不是有闲置风险从这儿一眼就能判断出来。拿到这些统计数据后前端用 ECharts 的柱状图展示。前端只需要把从后端拉到的数据转成 ECharts 需要的{ name: ..., value: ... }格式option 的配置固定在组件内部数据通过 props 传入。这样图表组件可以在多个页面复用。我踩过一个比较深的坑ECharts 图表容器如果初始化时页面处于隐藏状态比如用了 Tab 切换经常出现宽度自适应失败、图表变形的问题。解决办法是在图表初始化时调用chart.resize()或者给容器设定一个明确的高度值别完全依赖百分比。5.3 低库存预警提醒的实现低库存预警是我认为该项目里很能体现有产品思维的功能。管理员登录后在首页顶部看到一个黄色提示条内容是以下耗材库存不足蒸馏水剩余 2 瓶、灭菌手套剩余 1 盒点击可直接跳转到耗材管理页。后端实现也简单查询耗材表里current_stock warn_threshold的记录public ListConsumable getLowStockList() { return consumableMapper.selectList( new LambdaQueryWrapperConsumable() .le(Consumable::getCurrentStock, Consumable::getWarnThreshold) ); }这个预警功能代码量不大但正好切中实验室管理的真实痛点——耗材用完了没人及时知道等到做实验时才发现没东西可用特别影响效率。在文档和答辩 PPT 里把这个场景讲清楚评委很容易产生共鸣。6. 部署上线全记录6.1 环境准备从零开始搭一套可用环境部署是拿到源码之后的第一道坎。这里我把自己的部署过程完整梳理一遍照着操作基本能一步到位。第一步安装 JDK 1.8。如果用 Linux 服务器用 yum 或者 apt 装就行。装完验证一下java -version看到java version 1.8.0_xxx就说明没问题。如果系统自带的是 JDK 11 或者更高版本建议先卸掉或者改环境变量因为有些项目的依赖在 JDK 11 下会出现兼容问题比如 Java EE 相关的包在 JDK 11 中被移除了。第二步安装 MySQL 5.7。版本选择很关键MySQL 5.7 跟 MyBatis Plus 和 JDBC 驱动的兼容性最稳定MySQL 8.0 需要额外注意认证插件的问题新手容易栽进去。包括 2024 年之后的 MySQL 8.4 的驱动配置语法有调整如果非要用 8.x 的版本记得在连接串里加allowPublicKeyRetrievaltrueuseSSLfalse不然会报数据库连接失败。装好之后创建数据库CREATE DATABASE lab_equipment DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第三步导入数据库脚本。我提供的源码包里附带一个lab_equipment.sql文件。用命令行导入mysql -u root -p lab_equipment lab_equipment.sql导入成功后可以用SHOW TABLES;验证是否有七张表。如果提示报错95% 的情况是数据库没先创建先执行USE lab_equipment;再导入。第四步安装 Node.js。前端项目打包需要用到 npm 命令Node 版本建议用 14.x 或 16.x LTS 版本太新的版本比如 20.x在某些老项目里会遇到依赖不兼容的问题。安装方式不用多说官网下载安装包一路下一步即可。6.2 后端项目启动配置修改与本地跑通后端项目导入 IDEA 后第一步要修改application.yml里的数据库密码改成你自己 MySQL 的密码。启动前还有两个容易忽略的细节第一个细节检查 Maven 仓库路径。IDEA 默认使用它内置的 Maven本地仓库路径默认在C:/Users/用户名/.m2/repository我建议改成你自己指定的目录不然下载依赖的时候可能会因为 C 盘空间不足报错。第二个细节Lombok 插件必须在 IDEA 里装好。不装的话实体类里用Data注解生成的 getter/setter 方法全部找不到启动直接报编译错误。配置改好之后直接运行LabEquipmentApplication.java的 main 方法。看到 Spring Boot 启动成功的日志控制台输出 Tomcat started on port 8080说明后端已经跑起来了。这时候可以用 Postman 或者浏览器访问http://localhost:8080/api/test返回 JSON 数据就代表环境没问题。6.3 前端项目启动依赖安装与跨域配置前端项目我用 npm 管理依赖。在项目根目录打开终端执行# 安装依赖首次执行比较慢耐心等待 npm install # 启动开发服务器 npm run serve启动成功后终端会提示App running at: http://localhost:8081。这里有个很关键的配置前端的开发服务器端口和后端最好错开不然会撞端口。Vue CLI 默认端口是 8080而后端 Spring Boot 也默认 8080必撞。我习惯把 vue.config.js 里配置端口改成 8081同时配置代理module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };代理配置的作用是让前端开发环境里的所有/api开头的请求自动转发到后端8080端口完美解决跨域问题。如果不用代理前端请求后端会报 CORS 错误浏览器拦截。登录进去之后先用管理员账号admin / admin123测试几个核心流程新增器材、提交借用申请、审批通过、归还器材。走完这条链路系统就算真正跑通了。6.4 生产环境打包从开发到上线的一键部署开发环境跑通之后如果要部署到服务器需要分别打包前后端。后端打包用 Maven 的 package 命令产物是一个可执行的 jar 包。我在pom.xml里配置了 Spring Boot Maven 插件所以打出来的 jar 是 fat jar自带所有依赖直接就能跑mvn clean package -DskipTests打包完成之后jar 包在 target 目录下。上传到服务器执行java -jar lab-equipment-1.0.0.jar --spring.profiles.activeprod --spring.profiles.activeprod是启用生产环境的配置文件里面会自动读取 prod 环境下的数据库链接。如果没有 profiles 配置直接运行 jar 包也行但生产环境的密码建议不要用明文写在配置里用环境变量注入SPRING_DATASOURCE_PASSWORDyourpassword java -jar lab-equipment-1.0.0.jar 前端打包是npm run build打包产物在dist目录下是纯静态文件。用 Nginx 部署server { listen 80; server_name your-domain.com; root /opt/lab-equipment/dist; index 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 / { try_files $uri $uri/ /index.html; } }这里的关键是 Nginx 的location /api反向代理到本机 8080 端口静态页面请求走try_files指向 index.html解决 Vue Router 的 history 模式刷新后 404 的问题。7. 常见问题与排查技巧7.1 数据库连接失败与中文乱码数据库连接失败是最常见的问题报错信息千奇百怪但原因基本就那么几个错误现象根本原因解决办法Access denied for user用户名或密码错误检查 application.yml 的密码配置Communications link failureMySQL 服务没启动或端口不对检查 MySQL 服务状态确认端口 3306Unknown database数据库名拼写错误核对连接串里的数据库名The server time zone valueJDBC 连接串缺少时区配置连接串加serverTimezoneAsia/Shanghai中文乱码的场景一般是两种数据库表字段乱码和接口返回乱码。表字段乱码通常是建表时字符集不是 utf8mb4解决方法是删除重建表导入 SQL 前确保文件编码是 UTF-8或者在连接串里加characterEncodingutf8。接口返回乱码则要检查项目里所有 Java 文件的保存编码IDEA 右下角能看到当前文件编码统一改成 UTF-8。7.2 端口占用问题启动时报Port 8080 was already in use是本地开发经常遇到的。# Windows 下查看并杀掉端口占用进程 netstat -ano | findstr 8080 taskkill /pid 进程号 /f # Linux / Mac 下 lsof -i:8080 kill -9 进程号如果不想杀进程也可以直接把项目的端口改掉前端代理同步改一下就行。7.3 MyBatis Plus 的常见报错Mapper 接口无法注入或 XML 文件找不到这是新手最容易踩的坑。检查三件事第一启动类上有没有加MapperScan(com.example.mapper)注解没有的话 Mapper 根本不会被 Spring 扫描到。第二如果使用 XML 文件写 SQL检查application.yml里是否配置了mybatis-plus.mapper-locations。第三实体类和数据库表字段的映射关系如果开启了驼峰映射还不能识别就在字段上加TableField注解手动指定。我习惯用 MyBatis Plus 的 LambdaQueryWrapper 写查询不写 XML。比如new LambdaQueryWrapperEquipment().eq(Equipment::getStatus, 0)这种写法最大的好处是编译期就能发现字段名拼写错误不至于等到运行时才报 SQL 语法错误。7.4 请求超时与前端报错处理前端页面操作时如果出现Network Error或者接口一直 pending按顺序排查第一打开浏览器 F12 开发者工具的 Network 面板看请求状态第二确认后端启动没有报错看控制台日志第三确认前端代理配置正确。这三步走完80% 的问题都能定位。还有一种情况是后端接口逻辑抛了异常但前端拿到的是 500 状态码和一串冗长的错误堆栈。原因是全局异常处理没有生效或者是代码里直接 catch 了异常而没有重新抛出。解决方法是确保 Service 层抛出的异常是自定义的BusinessException而不是裸的Exception——全局异常处理器只拦截自定义异常类型的抛出不拦截所有Exception的话会导致前端拿到的错误信息根本不可读。7.5 部署后图片不显示或样式错乱前端 build 之后如果图片不显示或者 CSS 样式不对通常是静态资源路径问题。Vue CLI 默认的publicPath是绝对路径/如果你是部署在子目录http://ip:8081/project/下需要把 vue.config.js 里的publicPath改成./这样资源引用自动变成相对路径。另外我在项目里用到了 ECharts 的字体文件build 的时候会经过 webpack 的字体处理如果出现字体文件 404检查public目录是否有缺失。8. 性能优化与经验扩展8.1 索引设计与查询优化当器材表的数据量超过几千条后列表页的查询速度会明显变慢这是正常的。解决办法是给常用查询字段加索引。我在这套系统里给三个字段加了普通索引器材表的category_id、status字段借用记录表的equipment_id字段。加索引的 SQLALTER TABLE equipment ADD INDEX idx_category_id (category_id); ALTER TABLE equipment ADD INDEX idx_status (status); ALTER TABLE borrow_record ADD INDEX idx_equipment_id (equipment_id);加索引之后按分类查器材、按状态查器材、按器材查借用记录的速度会有数量级的提升。但索引不是越多越好每次写入数据时都要维护索引写操作反而变慢。对于毕设项目给核心查询字段加 3~5 个索引就足够。8.2 前端缓存策略器材分类这种不常变的数据每次打开页面都从后端拉一遍是浪费的。我用 localStorage 做了一层简单缓存设置 10 分钟过期时间。实现代码非常简单const KEY equipment_category; const EXPIRATION 10 * 60 * 1000; export function getCategoryCache() { const data localStorage.getItem(KEY); if (!data) return null; const { timestamp, value } JSON.parse(data); if (Date.now() - timestamp EXPIRATION) { localStorage.removeItem(KEY); return null; } return value; } export function setCategoryCache(value) { localStorage.setItem(KEY, JSON.stringify({ timestamp: Date.now(), value: value })); }这个优化在开发环境看不出明显效果但在生产环境、网络条件一般的情况下页面加载速度的体感提升很明显。8.3 项目扩展方向的思考答辩时评委经常会问你这个系统还能怎么优化提前想好几个扩展方向回答时就能从容应对。第一个扩展方向增加消息通知功能。比如借用申请通过后主动给用户发一封邮件或者微信模板消息。技术方案可以采用 Spring Boot 的邮件服务模块不需要修改业务逻辑在 Controller 层调用即可。第二个扩展方向扫码借还。用二维码生成工具在器材上贴好二维码用户扫码后自动弹出借用申请页面器材 ID 通过 URL 参数传入。前端只需要新增一个扫码页面配置二维码扫描插件后端接口完全不需要改。第三个扩展方向对接学校统一身份认证系统CAS/LDAP。因为实验室器材系统往往是学校信息化平台的一个模块把登录方式改成单点登录用户不需要单独注册账号。面试时提到这个方案能显示你对系统集成的理解。还有一个就是报表可视化。现在柱状图、饼图都有了可以扩展一个时间维度的趋势图比如按月统计器材借用次数分析不同学期的借用高峰时段。这在真实场景中是很有价值的数据沉淀。9. 写在最后的一些实操心得这套实验室器材管理系统从数据库设计到部署上线完整走下来大概需要三到四周。精力主要花在借用归还的状态流转和三个报表页面的数据聚合上其余模块都是常规的增删改查。如果你正为毕设选题发愁或者已经选了题目但不知从何下手先把数据库建好、把登录权限跑通再一个模块一个模块往里填这个节奏最不容易中途放弃。根据我个人经验多用业余时间把代码里的注释写清楚、把 README 里部署步骤写详细这些看起来不起眼的功夫在最终答辩时能省掉大量沟通成本。评委打开你的项目仓库第一眼看到的永远是 README 而不是代码好的文档本身就是加分项。最后分享一个我习惯用的小技巧在管理系统的列表页加一个导出 Excel按钮实现方式用 Java 的 EasyExcel 库三十行代码搞定。这个功能非常小但每次答辩演示时评委都会看到这个点然后点头——因为真实业务场景里教师就是需要把设备台账导出来存档的。这种贴近真实需求的小功能比堆砌十个华而不实的模块都管用。