1. 一张Excel台账引发的混乱:这套系统到底要解决什么
我去年给某高校管理学院做了一套实验室设备管理系统,说实话,一开始学院这边找到我时,需求就一句话:"全院几百台设备,台账乱得没法看,领导要数据也拿不快,你帮我们弄个系统管一下。"
这个场景我相信很多做校内系统的朋友都不陌生。管理学院的实验室设备和理工科实验室不太一样:理工科可能是一堆精密仪器、贵重设备,而管理学院更多是教学机房、多媒体设备、台式机、笔记本、服务器、打印机,还有一部分专业实验软件配套的硬件。设备单价不算特别高,但数量大、流转频繁、使用人杂。最常见的状态就是:学期初调课要借用一批笔记本、课题组临时借投影仪、机房某台电脑坏了不知道报修流程、毕业答辩要占用实验室时段。
我翻了翻他们的历史台账,是一个多人协同编辑的Excel文件,存在某位老师的电脑里。表格里有设备编号、名称、购置日期、存放地点、使用人几个字段,但实际维护情况很糟糕:有人把设备借走了,就在备注栏随手填"借给学生A了",归还时再删掉;维修记录则是打印出来的纸质单子,修完往抽屉一塞,时间长了谁也说不清这台机器修过几次、花了多少钱;至于报废,更是靠"东西实在不能用就扔到库房角落"来管理。
这套系统的核心目标,我梳理下来其实就四件:
- 把每台设备从入库到报废的全生命周期信息记录下来,形成规范台账;
- 把设备借用、预约、归还这些日常流转流程线上化,谁借的、什么时候还的、现在在谁手里都清清楚楚;
- 把维修、保养、报废这些处置过程纳入管理,不再依赖纸质单据和口头交代;
- 给学院管理层提供设备使用率、故障率、资产情况等统计口径,要数据的时候不用再让管理员对着Excel数半天。
"基于Spring Boot的实验室设备管理系统"这个题目,核心不在一堆花哨功能,而在于先把管理场景摸透,再用合理的技术把流程落地。很多项目失败,不是技术不行,而是代码写完之后发现流程不对,反复返工。所以这篇文章,我不打算只讲技术,而是把一个完整的管理系统从需求分析、设计取舍到编码落地的全过程拆开讲。你要是正准备做类似的管理类系统,这套思路可以直接套用。
2. 先梳理业务流程,再谈技术方案:你不是在写CRUD,是在重建管理秩序
接手这个项目之后,我没有直接建工程写代码,而是花了大概两周时间做业务调研和分析。这段前期工作看起来"没有产出",但它决定了系统后面是越用越顺还是上线就废。
2.1 摸清角色和使用场景:谁能干什么,必须一开始就定清楚
我走访了管理学院实验中心的老师、负责多媒体设备维护的助管、几位经常借用设备的专业课教师,还有学院分管资产的领导。最终把系统用户抽象成四类角色:
- 设备管理员:实验中心的核心用户。负责设备档案维护、借用审核、维修登记、盘点操作、报废提报。他们最痛恨的就是Excel表格版本混乱,以及别人借了设备不通过正规渠道登记。
- 教师/科研人员:发起借用、预约实验室、报修设备。他们要的是"方便"——手机上能发起申请,能查到设备在哪里、是否可用,审批流程不要太长。
- 学生(助管/课程使用者):学生主要分两类,一类是实验室助管,协助管理员做日常登记和维护;另一类是课程或毕设需要使用设备的学生。考虑到管理的简单性,学生端不单独开放完整的设备管理权限,只提供预约和使用登记入口。
- 学院领导:不看操作界面,只看报表。设备总量、在用率、维修成本、即将到达报废年限的设备列表,这些要有直观展示。
基于角色,我画出了核心用例图,其实就是问自己几个问题:这些人每天打开系统要做什么?操作完之后数据应该变成什么状态?谁对这个操作的准确性负责?把这些问题写清楚,比直接打开IDE建表重要得多。
2.2 设备生命周期状态机:整个系统的骨架
设备不是静态数据,它有生命周期。我把设备状态定义为六个,整个系统的流程设计都围绕这组状态流转展开:
- 在库(AVAILABLE):设备正常,可被借用或预约。这是绝大多数设备的默认状态。
- 借出(BORROWED):已被某人借走,未归还。
- 维修中(REPAIRING):故障已上报,正在走维修流程。
- 保养中(MAINTAINING):计划性保养或巡检,暂时不可借用。
- 待报废(TO_BE_SCRAPPED):无法修复或达到使用年限,等待报废审批。
- 已报废(SCRAPPED):审批通过,彻底退出使用。
每台设备的每一次状态切换,都对应一条业务记录。比如"在库→借出"对应借用单,"在库→维修中"对应维修单,"维修中→在库"对应维修完成验收单。这样设计之后,系统不再是简单的字段修改,而是一串可追溯的事件流。哪怕一个学期之后有人问"这台机器上学期为什么有半个月不在库",我们也能快速翻出对应记录。
我把这六种状态画成了一张流转图,写进设计文档里。后续所有功能模块都围绕这张图展开,谁负责触发状态变更、变更需要什么条件、变更后要写哪些日志,全部有据可依。
2.3 功能模块划分:一个模块一张表,边界要清
业务流程梳理清楚之后,功能模块就自然浮现了。我把系统划分为六个模块:
- 设备档案管理:设备信息增删改查、分类管理、批量导入导出、二维码标签打印。
- 实验室与存放地管理:管理学院的实验室房间、机房座位区域等存放位置。
- 借用与归还管理:借用申请、审批、出库登记、归还验收、超期提醒。
- 预约管理:实验室/设备按时间段的预约,主要支撑课程和答辩场景。
- 维修保养与报废管理:故障上报、维修处理、验收回库、保养计划、报废审批。
- 统计报表与系统管理:使用率统计、设备台账导出、用户管理、角色权限、操作日志。
这里要特别强调一点:在做模块划分时,我坚持"一个动态业务模块至少对应一张独立的业务表",而不是在设备表上加一堆状态字段和备注。比如维修记录、借用记录、保养记录必须独立建表。因为后续统计、追溯、审计都需要这些历史数据,如果只改变设备的状态字段,历史信息就全丢了。
3. 技术选型的务实考虑:为什么是Spring Boot + MyBatis-Plus + MySQL这套组合
开发这类校内管理系统的技术选型,我见过两种极端:一种是用PHP或者Layui时代的旧框架快速拼页面,代码维护起来很痛苦;另一种是把Spring Cloud那套全家桶搬上来,几个微服务部署起来运维成本意识很重。我的建议是:单体应用 + Spring Boot,是这类项目最省钱省心的选择。
3.1 技术栈清单与选择依据
最终确定的技术栈如下:
| 层次 | 技术选型 | 选择理由 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 快速构建、生态成熟、内嵌Tomcat,打包后一个jar能直接跑 |
| 持久层 | MyBatis-Plus 3.5.x | 单表CRUD零SQL,复杂查询手写XML,开发效率高 |
| 数据库 | MySQL 8.0 | 免费、稳定,支持事务和行级锁,数据量在这个场景完全没有瓶颈 |
| 缓存与分布式锁 | Redis 6.x | 预约实时冲突校验、查询缓存、登录token存储 |
| 权限认证 | Spring Security + JWT | 前后端分离,接口无状态认证,权限模型灵活 |
| 接口文档 | knife4j (Swagger增强版) | 方便前后端协作以及后续维护人员接手 |
| 工具类 | Hutool、EasyExcel、ZXing | 导入导出、二维码生成直接复用成熟组件 |
| 前端 | Vue 3 + Element Plus | 后台管理系统快速开发首选,组件齐全 |
先说为什么不用SSH或者老SSM框架。那些框架不是不能用,而是配置过于繁琐——大量XML配置、依赖手动管理、与现成工具链整合成本高。对管理学院这套系统来说,开发周期只有两个多月,后续维护大概率由学院的技术老师或学生接手,选一个上手门槛低、资料多得查不完的框架,比炫技重要得多。
再解释一下为什么不做微服务。有朋友可能觉得,既然是系统的"分析与设计",是不是要搞得很分布式?我认为这是过度设计。这类系统的用户规模撑死几百人,并发量极低,单体应用完全吃得消。微服务带来的服务拆分、网络通信、数据一致性、部署复杂度,对这个项目全是负资产。分模块是代码层面的事,不是把服务拆开跑多个进程。
3.2 一些值得说的细节依赖
除了上面主干,有几个依赖在真实开发中特别省事:
- EasyExcel:管理学院每年要上报资产台账,系统里必须有设备信息导出功能。EasyExcel比POI省事太多,几行代码就能导出Excel,也不会出现大数据量内存溢出的问题。
- ZXing:设备入库后要打印二维码标签,贴在设备上,扫码即可查看设备信息发起借用。用ZXing生成二维码非常成熟,拿一台普通的热敏标签打印机就能打。
- knife4j:接口文档自动生成。项目交接的时候,接手的人打开在线文档就能看到每个接口的参数和返回结构,省了一轮嘴对嘴交接。
3.3 单体架构也要留好接口边界
虽然是单体,我仍然按模块拆了Maven多模块结构,分为common、system、device、reservation、report几个子模块。好处是后续系统真的需要拆出去或者重构,边界是现成的。而且不同开发者在不同模块里写代码,合并冲突也会少很多。团队协作时这个设计非常实用。
4. 数据库设计的取舍:哪些细节决定了系统上线后要不要返工
数据库设计是这类系统最见功力的环节。很多半路出家的项目,表结构建得很随意,上线跑一个学期就开始各种打补丁。我讲讲几个关键设计决策。
4.1 设备档案表:核心是"资产编号"和"部门冗余"
设备表是整个系统的中心表。字段设计大致如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| device_no | varchar(50) | 设备唯一编号,业务编号,加唯一索引 |
| asset_no | varchar(50) | 学校固定资产编号,用于和学校资产系统对账 |
| name | varchar(100) | 设备名称,如"联想台式机" |
| category_id | bigint | 设备分类ID |
| lab_room_id | bigint | 存放实验室ID |
| responsible_user | varchar(50) | 设备责任人 |
| status | tinyint | 状态:在库、借出、维修中、保养中、待报废、已报废 |
| purchase_date | date | 购置日期 |
| price | decimal(10,2) | 购置价格,金额不用float |
| supplier | varchar(100) | 供应商 |
| warranty_end | date | 保修截止日期 |
| version | int | 乐观锁版本号 |
| deleted | tinyint | 逻辑删除标记 |
| create_time/update_time | datetime | 审计时间 |
这里有个特别容易踩坑的点:设备为什么要有device_no,而不是直接用数据库自增id当唯一标识?因为设备要贴二维码标签,要与学校资产系统对账,设备编号是人工可读的,比如"GX-MIS-2023-001"。如果直接用自增id,打印在标签上既不可读,也没有业务含义。而数据库主键id依然存在,只是作为内部引用和外键关联使用,两者分工明确。
另一个重点是lab_room_id和responsible_user我选择了保存冗余字段而非全部做成关联表。分类和实验室确实可以做外键关联,但在设备这个高频查询的表上,必要的冗余能省掉大量的联表查询。当然冗余的前提是数据要同步维护好,不能只修改分类表后设备表里的旧名称不动。
4.2 借用记录和预约记录:两张必须独立设计的表
借用业务,我没有用"借用主表+归还子表"的复杂模型,而是把每次借用做成一条记录:
- 申请借用时插入一条borrow_record,状态为"已借出",记录借用人、借用时间、预计归还时间;
- 归还时直接更新这条记录的归还时间、归还状态,并校验设备完好情况。
这样每次借用就是一行数据,统计"哪些设备借出超过7天未归还"、"学期内设备借出次数"就非常直接。
预约表设计成时间段粒度:device_id、start_time、end_time、user_id、status。为了在数据库层做冲突兜底,我加了一个组合唯一索引,用预约的设备和时间范围构造一个冗余字段,比如先映射到15分钟粒度的时间段,再对(device_id, slot_key)建唯一索引。这个设计看似有些土,但能真正兜住并发场景下的冲突。
4.3 通用字段和规范:越早统一后面越省心
我在所有业务表上都统一加了create_time、update_time、deleted三个字段。逻辑删除几乎成了这类管理系统的标配——设备被误删了能找回,报表统计也不会因为物理删除导致历史数据断裂。但逻辑删除只针对设备、用户等基础档案,真正的业务流水记录如借用记录、维修记录是永不删除的,只允许追加修正。
金额字段必须用decimal(10,2),不要用float/double——涉及资产统计时浮点误差会让你对不上账。日期字段区分date和datetime:购置日期、保修截止日期用date就够了,审批时间、归还时间这种精确到秒的用datetime。
5. 几个核心功能节点:并发、状态机与可追溯
5.1 借用归还:如何防止同一台设备被同时借出
这是系统里最容易出问题的地方。两台设备分别被两个老师发起借用申请,管理员在界面上同时点了审批通过,如果代码不做并发控制,数据库里可能出现同一台设备被借出两次的脏数据。
解决思路分两层。第一层,借用申请提交时只记录申请单,设备状态不变;管理员审批时,后端在事务里做一次"比较并更新":
@Transactional public boolean approveBorrow(Long borrowId) { BorrowRecord record = borrowMapper.selectById(borrowId); Device device = deviceMapper.selectById(record.getDeviceId()); // 乐观锁校验:设备状态必须是在库,version一致才允许更新 int rows = deviceMapper.updateStateIfAvailable( device.getId(), DeviceStatus.BORROWED.getCode(), device.getVersion()); if (rows == 0) { throw new BizException("设备当前不可借用,可能已被借出或正在维修"); } record.setStatus(BorrowStatus.APPROVED.getCode()); borrowMapper.updateById(record); return true; }这里的关键是updateStateIfAvailable对应的SQL:
UPDATE device_info SET status = #{newStatus}, version = version + 1 WHERE id = #{deviceId} AND status = #{expectedStatus} AND version = #{version}利用数据库行锁和版本号机制,保证同一时刻只有一个事务能成功修改设备状态。这就是为什么设备表里要设计version字段——它不是为了炫技,是应对并发审批的保命设计。
归还流程也类似,只是把状态从"借出"改回"在库"。归还时还必须记录设备完好情况,如果有损坏,直接启动维修流程而不是简单回库。
5.2 预约冲突:Redis锁与数据库唯一约束双保险
预约实验室和预约设备,典型场景是:期末有多个课程要用机房,老师在线选时间段,两个申请同时提交,都选了周五下午2点到4点。如果只在应用层校验时间段是否重合,高并发请求下依然可能同时通过校验。
我的做法是双保险。流程上先用Redis分布式锁,锁的key设计成reservation:{deviceId},抢到锁之后再做时间段重叠校验;同时,DB层的slot_key唯一索引作为兜底保证。就算某个请求绕过Redis锁直接打到数据库,唯一索引也会拒绝重复(牺牲一部分灵活度,换来绝对不冲突)。实际项目里,预约的并发量非常低,Redis锁基本不会实际冲突,但这个double-check机制让管理员后期处理数据时非常放心。
时间段重叠SQL也不难:
SELECT COUNT(*) FROM device_reservation WHERE device_id = #{deviceId} AND status != 'CANCELLED' AND start_time < #{endTime} AND end_time > #{startTime}只要这个查询在事务内执行且配合锁,就能保证同一设备在同一时间段内只有一条有效的预约。
5.3 维修和报废:状态流转全程留痕
维修模块设计了一个repair_record表,记录设备ID、故障描述、上报人、维修方式(自修/送修/厂商质保)、维修费用、开始时间、完成时间、维修结果。维修流程里最有价值的做法是每一步操作都要走状态流转接口,不允许直接改库。
故障上报后,设备状态从"在库"变为"维修中";维修完成,管理员做验收,设备回到"在库",同时维修记录里补充实际的维修费用和结论。有一次维修了三次还没修好的设备,系统的维修记录就自然形成了台账,管理员可以直接基于这些数据判断这台设备是不是该申请报废了,而不是拍脑袋决定。
报废流程纳入审批:管理员提报待报废,写明原因(达到使用年限、维修成本过高、损坏无法修复),资产负责人审批后设备状态才变成"已报废"。已报废设备在盘点报表里会有单独分类,不会再混在在用设备里统计。
5.4 二维码、导入导出和报表:管理员每天都在用的功能
设备入库时批量打印二维码标签的流程:设备信息创建后,系统用ZXing根据设备ID生成二维码图片,通过标签打印机打出来贴在设备上。管理员手机打开系统H5页面扫码,直接跳到设备详情页,可以发起借用、查看维修历史、更新存放位置。这个功能看起来不起眼,但它真正把线上台账和线下物理设备对应了起来。盘点的时候逐一扫码,设备是否账实相符一目了然。
导入导出我用了EasyExcel,设备管理员从旧的Excel表批量导入设备信息时,系统会做字段校验,错误行会生成错误报告,而不是整个导入失败。导出则支持按实验室、按分类、按状态过滤,直接生成资产上报要求的报表格式。统计报表模块里,我实现了几个核心指标:设备总体使用率、单台设备借用次数排行、维修费用按月统计、即将到达报废年限的设备清单。根据我的经验,这些报表才是学院领导真正会打开看的东西,也是这个系统能不能持续用下去的关键——管理员靠它交差,领导靠它决策。
6. 权限与安全:角色再多也不能乱
这类系统权限设计不好,上线第一天就会出乱子。管理学院除了实验中心的设备管理员,还有多位兼职设备管理员,分别负责不同实验室。如果让每个管理员都能改全院设备数据,责任边界就模糊了。
6.1 基于RBAC建模:用户-角色-权限三层结构
我采用标准的RBAC模型。数据库设计三张核心表:sys_user(用户)、sys_role(角色)、sys_menu/permission(权限点),加上用户角色关联表和角色权限关联表。管理员、教师、学生、学院领导对应不同的角色集合,权限点则具体到接口级别,比如"设备档案:新增"、"借用单:审批"、"报表:查看"。
Spring Security的配置上,我实现了JWT无状态认证:登录接口校验用户名密码后签发token,前端后续请求在Authorization头带上token,后端通过过滤器解析用户身份和权限。这样设计是因为系统还接了学院统一的H5移动端入口,接口需要无状态、可跨域调用。密码使用BCrypt加密存储,禁止明文。
6.2 数据权限:每个管理员只能管自己负责的实验室
光有角色权限不够。如果实验中心的张老师负责"管理科学实验室",李老师负责"信息管理实验室",那张老师就不应该能修改李老师负责实验室的设备数据。
我在设备表里设计了一个manage_dept_id或lab_room_group字段,并给管理员用户绑定负责的实验室范围。查询和操作数据时,MyBatis-Plus的拦截器会自动追加数据权限条件:AND lab_room_id IN (管理员负责的实验室列表)。这种数据权限控制在代码里维护一个DataScope注解,拦截器统一处理,业务代码里不需要到处写死条件。效果是:不同管理员登录系统,看到的设备列表天然只有自己负责的范围,借用审批也只能处理自己负责实验室的申请。
6.3 接口安全与操作审计
操作日志模块值得多写两行。所有设备状态的变更操作,包括谁在什么时间把哪台设备从"在库"改成"维修中",都要写入operation_log表。字段包括操作人ID、操作类型、目标设备ID、变更前状态、变更后状态、操作时间、IP地址。这个表也方便后续排障——一旦有设备状态和实际不一致,查操作日志就知道是谁动了手脚。
参数校验放在Controller层,用Validation注解做参数非空、长度、数值范围的校验。统一异常处理器捕获所有业务异常,返回统一的JSON错误结构,避免把堆栈信息直接暴露给前端。SQL全部使用MyBatis-Plus参数化查询,从源头杜绝拼接注入问题。
7. 从开发到部署:几个实际踩过且值得警惕的坑
这个项目开发过程总体顺利,但部署上线阶段和持续使用的过程中,还是踩了一些坑。挑几个典型的复盘一下,给准备做类似项目的朋友提个醒。
第一个坑是Long类型主键传到前端精度丢失。设备表主键是bigint,通过JSON传给前端时,如果前端用JavaScript Number类型接收,超过2^53就会丢失精度。表现是:列表点开某台设备详情,跳转后ID变成另外一个数,设备信息张冠李戴。解决办法是在Jackson全局配置中把Long序列化为String。这个坑在管理系统里非常常见,因为很多表的ID都是雪花算法生成的超长整数。
第二个坑是数据库时区配置。MySQL 8.0如果连接串没加serverTimezone参数,插入的时间会比实际时间早8个小时。表现为预约记录、借用记录的create_time对不上。统一在JDBC连接串中配置serverTimezone=Asia/Shanghai即可。
第三个坑是事务方法内部调用导致@Transactional失效。我在生成借用单的Service方法里调用了本类的另一个带@Transactional的方法,结果异常时数据没有回滚。原因是Spring事务基于代理对象,同类内部方法调用不经过代理。解决方式要么把事务方法写在独立的Service类里,要么用AopContext.currentProxy()获取代理对象再调用。
第四个坑是逻辑删除字段与唯一索引冲突。设备表加了逻辑删除后,如果device_no上有唯一索引,删除一台设备后重新创建同编号的新设备时,唯一索引依然被逻辑删除的那条旧数据占着,插入直接报错。我的处理是把唯一索引改成联合索引(device_no, deleted),因为逻辑删除只在有效数据上建立唯一性。
第五个坑是Nginx上传大小限制导致批量导入失败。设备导入的Excel文件稍微大一点,请求直接被Nginx以413错误驳回。排查后确认是Nginx默认client_max_body_size只有1m,在配置里调到10m才解决。如果你在本地跑得好好的,上了服务器一传文件就失败,先查这层。
第六个坑是Redis没开持久化的后果。预约功能依赖Redis锁,有一次服务器因为机房断电重启,Redis里没有做AOF持久化,重启后数据和锁全部丢失。好在因为DB层有唯一索引兜底,没有产生脏数据,但那次之后我就强制给Redis开了AOF持久化。这个教训让我更加确信:任何分布式锁的设计,都必须考虑锁服务挂掉之后的数据库兜底方案。
最后再分享一个运维层面的心得。我用一个systemd服务来托管Spring Boot的jar包,日志统一输出到指定文件,配置了开机自启和崩溃自动重启。学院那边的技术老师接手后,只需要会systemctl restart device-system这一条命令就够日常维护了。这类管理系统的生命周期往往有五六年甚至更长,把部署做得越简单,系统越不容易荒废。
还有个小经验是给所有设备的状态变更加Webhook通知,借用审批通过、维修完成、归还超期都会触发站内消息提醒。虽然短信或企业微信通知的接入成本不高,但考虑到学院内部的使用习惯,我先用站内消息,模版消息列表放在首页提醒区,领导和管理员一登录就能看到待办事项。这个简单设计极大提高了系统的使用率——因为管理员不需要再翻各个页面才知道今天有没有待审批的借用单。
我在实际做这个系统的过程中最大的体会是:技术永远是工具,真正的核心是前期对业务流程的尊重。哪怕Spring Boot写得再漂亮,如果设备状态机没定义好,借用流程没有考虑到并发审批,系统上线后一样会被实际使用场景逼着返工。反过来,只要你愿意花时间把角色、流程、状态、权限这些基础问题想清楚,Spring Boot这样成熟的技术栈能把你的设计快速变成稳定可用的系统,这也是它至今仍是这类管理系统的首选技术路线的原因。