基于SpringBoot的高校实验室安全巡检系统:设计、实现与部署
2026/9/18 20:47:22 网站建设 项目流程

实验室安全巡检这种系统,网上一搜一大把,但绝大部分是把“台账管理”做成增删改查,真正能让学校安全办愿意天天打开用的并不多。我做这个基于SpringBoot的高校实验室安全巡检系统,初衷很简单:院里实验室分散在不同楼层,安全员靠纸质检查表跑一遍要半天,隐患信息传上来的格式五花八门,口头上报了但几天没人盯着整改。所以我用SpringBoot从零搭了一套集巡检任务、扫码打卡、隐患闭环、预警推送于一体的平台,把“谁在什么时候检查了哪个房间、发现了什么问题、整改到什么程度”全部串起来。这篇文章就把我的系统设计思路、核心代码、踩过的坑写透,给做毕业设计或者想在校内落地类似系统的同学一个尽量能直接复用的参考。

1. 项目从立项到落地,我的系统拆解思路

1.1 实验室安全隐患场景到底要管什么

动手之前,我先跟着安全员跑了两趟实验室,把实际巡检场景列了个清单。高校实验室和普通办公楼不一样,危险源种类多、责任人分散、检查项差异大,但落到系统上无非这几类核心对象:

  • 房间与点位:每一间实验室是一个基础单元,室内又细分气瓶存放区、危化品柜、配电箱、消防器材、通风系统、应急喷淋等检查点。
  • 巡检任务:按日检、周检、月检的周期生成,分派给对应实验室负责人或学院安全员,扫码之后按表单逐项确认。
  • 隐患事件:检查发现异常后形成一条隐患记录,经历上报、审核、整改、复查、销号的全过程。
  • 预警通知:当隐患超过整改期限没处理、或者任务超时未执行时,系统主动推送消息给相关人员。

系统边界想清楚之后,我确定整体形态是B/S管理端加移动巡检端,管理后台处理任务派发、数据统计、人员权限,现场巡检端则通过扫码和勾选的方式快速录入。这个划分也直接决定了技术选型的方向。

1.2 为什么最终选了SpringBoot而不是SSM或其他框架

作为毕业设计,最稳妥的方案当然是SSM(Spring + SpringMVC + MyBatis),但它需要写大量的XML配置、web.xml、多数据源配置模板,光是让项目跑起来就要折腾好几天。我选SpringBoot,核心原因是三个:

第一,自动装配机制把基础设施配置收编了。SpringBoot的自动配置原理说白了就是通过@EnableAutoConfiguration结合spring.factories里一大堆AutoConfiguration类,在满足条件时自动注册DataSource、RedisTemplate等Bean。我只需在application.yml里填连接信息,不用手写spring-mybatis.xml

第二,内嵌Tomcat简化了部署。打包成jar直接跑,对于学校和服务器环境都比较友好,不用单独装Tomcat、配置数据源JNDI。

第三,生态整合效率高。MyBatis-Plus、Redis、WebSocket、Spring Security这些要用的组件都能通过starter方式快速引入,避免大量重复的样板代码。

我也考虑过用Go或者Python FastAPI重写,但考虑到学校现有老师的熟悉度、后续维护成本,以及毕业设计答辩时技术栈的完整度,SpringBoot+Vue前后端分离是最合适的组合。这里有个经验:做毕设不要追求技术上的“新奇特”,稳定、能完整演示、答辩讲得清楚,远比框架版本新更重要。

1.3 技术版本与项目结构定稿

版本问题我特别想提醒一句,最初我在IDEA里直接创建SpringBoot项目时默认拉到了SpringBoot 3.x,结果配套的JDK必须17起,项目跑起来之后一些第三方依赖还不兼容。做毕设如果不想折腾,目前最稳的组合是JDK 1.8 + SpringBoot 2.7.x。JDK 8的生态兼容性足够用,网上查到的大部分问题解决方案都基于这个版本,遇到坑很容易找到答案。

后端最终技术栈如下:

  • SpringBoot 2.7.x
  • MyBatis-Plus 3.5.x(数据访问增强)
  • MySQL 8.0(存储业务数据)
  • Redis 5.x(缓存点位信息、验证码、在线会话)
  • Spring Security + JWT(认证与授权)
  • WebSocket(预警消息推送)
  • ZXing(二维码生成)

项目目录我按功能分包,而不是严格按controller/service/mapper三层分包,这样找代码更快:

com.lab.safety ├── config # WebSecurityConfig、WebSocketConfig、MybatisPlusConfig ├── controller # 所有Request入口 ├── service # 业务接口与实现 ├── mapper # MyBatis-Plus接口 ├── entity # 数据库实体类 ├── dto # 接收前端参数、返回视图对象 ├── common # 统一返回体、异常处理、工具类 └── task # 定时扫描与预警任务

这个分包结构带来的最大好处是把“实体”和“接口入参”解耦,比如新增巡检任务时前端传的参数和PatrolTask实体字段并不完全一致,用DTO兜一层后不会污染实体结构。

2. 数据建模与权限体系的设计中心

2.1 巡检任务、隐患台账、预警记录三类核心表的联动

数据库是整套系统最不能省心的地方。我设计的核心表总共8张,其中最关键的联动关系是:patrol_task(巡检任务)产生patrol_record(巡检记录),巡检记录关联hazard_info(隐患登记),隐患经过整改最终在alarm_log(预警日志)中留痕。

巡检任务表我保留了这几个关键字段:

CREATE TABLE `patrol_task` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `task_no` VARCHAR(32) NOT NULL COMMENT '任务编号', `lab_id` BIGINT NOT NULL COMMENT '实验室ID', `task_type` TINYINT NOT NULL COMMENT '任务类型:1日检 2周检 3月检', `check_list_json` TEXT COMMENT '检查项清单JSON,如洗眼器/通风橱/气瓶状态', `assignee_id` BIGINT NOT NULL COMMENT '被指派人ID', `creator_id` BIGINT NOT NULL COMMENT '创建人ID', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待执行 1执行中 2待整改 3待复查 4已完成 5已逾期', `deadline_time` DATETIME NOT NULL COMMENT '最迟完成时间', `finish_time` DATETIME DEFAULT NULL COMMENT '实际完成时间', `create_time` DATETIME NOT NULL, `update_time` DATETIME NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

检查项我用JSON字符串存check_list_json,而不是单独建一张检查项明细表。原因很简单:不同实验室的检查项差异大,如果把检查项硬拆成行,表行数会爆炸,而且每次生成任务要动态拼接大量SQL。JSON方案虽然查询时不能直接关联,但配合后端反序列化成List<CheckItem>,反而更灵活。如果是纯演示项目,这一招能省很多工作量。

隐患表则使用状态字段控制整个闭环:

CREATE TABLE `hazard_info` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `record_id` BIGINT NOT NULL COMMENT '巡检记录ID', `hazard_desc` VARCHAR(500) NOT NULL COMMENT '隐患描述', `hazard_level` TINYINT NOT NULL DEFAULT 1 COMMENT '隐患级别:1一般 2较大 3严重', `images` TEXT COMMENT '现场图片,逗号分隔', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审核 1待整改 2整改中 3待复查 4已销号', `audit_user_id` BIGINT DEFAULT NULL COMMENT '审核人', `assign_user_id` BIGINT DEFAULT NULL COMMENT '整改负责人', `rectify_deadline` DATETIME DEFAULT NULL COMMENT '整改期限', `create_time` DATETIME NOT NULL, `update_time` DATETIME NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三类表的联动关系一句话总结就是:任务是源头,记录是凭证,隐患是事件,预警是监督。安全员扫一个点位二维码,生成一条巡检记录,记录一旦出现异常项,就自动写入隐患表,同时根据隐患级别启动对应的整改超时提醒。这个链路才是整个平台的核心,而不是简单做一套多表CRUD。

2.2 RBAC角色权限怎么拆才能撑住学校多级管理

高校的安全管理架构通常是:校安全办 → 学院安全员 → 实验室负责人/管理员 → 巡检执行人 → 普通教师/学生。角色之间的权限不一样,所以我用了RBAC(基于角色的访问控制)模型,通过sys_user_rolesys_role_menu建立用户、角色、菜单三者的关联。

权限控制分两层:

  • 接口层:使用Spring Security的@PreAuthorize注解做方法级拦截,比如只有ROLE_ADMINROLE_SAFETY_OFFICER能创建巡检任务,普通巡检员只能提交巡检记录。
  • 数据层:通过自定义DataScope拦截器,学院安全员只能看到本学院数据,实验室负责人只看到自己管辖的实验室内记录。

这层尤其容易出问题。很多毕设Demo把所有用户都当成“超级管理员”,菜单全部可见,数据全量可查,这在实际评审中会被一眼看穿。我的实现是在PatrolTaskMapper里加了一个自定义SQL,通过当前登录用户的college_idrole动态拼接过滤条件。

例如查询任务列表时:

public class PatrolTaskServiceImpl extends ServiceImpl<PatrolTaskMapper, PatrolTask> implements PatrolTaskService { @Override public Page<PatrolTaskVO> pageTasks(PageParam param, LoginUser user) { LambdaQueryWrapper<PatrolTask> wrapper = Wrappers.lambdaQuery(); if (!user.isAdmin()) { wrapper.and(w -> w .eq(PatrolTask::getCollegeId, user.getCollegeId()) .or() .eq(PatrolTask::getAssigneeId, user.getUserId())); } if (StrUtil.isNotBlank(param.getStatus())) { wrapper.eq(PatrolTask::getStatus, param.getStatus()); } return baseMapper.selectPage(param.getPage(), wrapper); } }

这里最关键的一点是:管理员和普通用户走的是同一个接口,但SQL条件不同。数据权限不在前端强控,而是在后端根据角色动态拼接,这样绕过前端直接调接口也拿不到越权数据。

2.3 二维码点位与任务生成的联动逻辑

现场巡检我采用的是“扫码打卡”模式。每个实验室门口张贴一个二维码,二维码内容是一个带签名信息的URL,例如/patrol/check?pointId=1001&token=xxx

这个URL不能直接用固定id,否则会被人伪造打卡。我在生成二维码时会把labId + 随机盐 + 时间戳组合起来做一次AES加密后作为参数token,服务端通过@PathVariable解密并校验。这样既保证标识不可仿造,又能在一定程度上防止单人拿着二维码照片远程打卡。

任务生成的联动逻辑是:管理员在配置页面设置好每个实验室的日检/周检/月检策略后,系统每天晚上由@Scheduled定时任务扫描配置表,生成第二天需要执行的任务,并按责任关系自动把任务分派到对应实验室负责人名下。生成任务时同时调用RedisTemplate把任务ID与二维码的点位码绑定,方便扫码时做快速校验。

这套联动逻辑实现了“配置一次、自动运转”的效果。演示的时候不用像普通Demo一样每次手动造任务,只要提前配好策略,打开页面就能看到数据自动流转,现场感和完整度都会好很多。

3. 核心代码实践:巡检闭环与预警推送

3.1 巡检任务状态机的设计与非法状态流转拦截

巡检任务是整个系统里状态变化最多的地方,设计不好就会出现“任务已完成后还能再提交、已逾期任务还能标记正常”这种逻辑漏洞。我在PatrolTask里维护了一个状态机:

待执行(0) → 执行中(1) → 待整改(2) → 待复查(3) → 已完成(4) ↘ ↘ 逾期(5)

为了不让状态流转散落到Service的各个方法里,我单独写了一个PatrolFlowService,所有状态变更必须经过这个入口,在入口里统一做前置校验:

public void changeStatus(Long taskId, Integer targetStatus, LoginUser operator) { PatrolTask task = getById(taskId); Integer current = task.getStatus(); // 允许的流转映射 Map<Integer, List<Integer>> allowed = new HashMap<>(); allowed.put(0, Arrays.asList(1, 5)); allowed.put(1, Arrays.asList(2, 4, 5)); allowed.put(2, Arrays.asList(3, 5)); allowed.put(3, Arrays.asList(4, 5)); List<Integer> nextList = allowed.getOrDefault(current, Collections.emptyList()); if (!nextList.contains(targetStatus)) { throw new BizException("非法状态流转:" + current + " -> " + targetStatus); } // 执行更新并写入状态变更流水 ... }

这个设计的价值在排错时特别能体现出来。有一次测试人员用脚本批量提交巡检记录,发现一条已完成的记录被重复提交导致数据错乱,排查后发现是Controller直接调用了updateId绕过了状态机校验。后来所有写操作强制走流程服务,问题不再出现。

前端按钮的显隐也依赖这个状态机,后端配合返回allowedActions字段。比如任务是待复查状态时,鉴定按钮和复查按钮同时出现,但用户一旦点了“复查通过”,按钮立即失效。前后端双重判断能减少大量误操作。

3.2 隐患上报、整改、复查的完整闭环

隐患处理流程比巡检流程更复杂,因为涉及多个角色协作。我设计的闭环是:

  1. 巡检员扫码后填报异常项,系统自动创建隐患记录,状态为待审核
  2. 由学院安全员审核隐患是否属实、级别定高还是定低,审核通过后进入待整改,并指定整改负责人和整改期限。
  3. 整改负责人收到通知后上传整改说明和照片,状态改为待复查
  4. 安全员去现场复查,通过后状态改为已销号,不通过则打回重新整改。

这里需要重点处理的是“整改期限超时”。我只在门户接口里判断时容易漏,因为只要没人查这条数据,超时状态就一直不存在。最终我是在设置整改期限时直接把rectify_deadline写入数据表,同时用一个定时任务每5分钟扫描一次:

@Scheduled(fixedDelay = 300000) public void scanRectifyDeadline() { LambdaQueryWrapper<HazardInfo> wrapper = Wrappers.lambdaQuery(); wrapper.in(HazardInfo::getStatus, Arrays.asList(1, 2)) .lt(HazardInfo::getRectifyDeadline, new Date()); List<HazardInfo> overdueList = list(wrapper); ... }

阈值扫描任务里没有写的很重,就是简单查询 + 预警提醒,但注意不能为了提醒而重复创建alarm_log。我加了alarm_type + hazard_id的唯一索引,并用Redis的setIfAbsent做防重判断,确保同一条隐患不会被重复告警。

3.3 预警规则引擎:定时扫描与WebSocket实时推送

预警模块是区分“普通管理系统”和“安全监管平台”的关键点。我的预警来源有三个:

  • 巡检任务超时未执行;
  • 隐患整改进度超过期限;
  • 设备异常数据(如温湿度传感器读数超过阈值,通过MQTT或模拟接口接入)。

前端采用WebSocket接收预警消息,后端在用户登录时把userId与WebSocket会话建立映射,推送时定向发送。这里有个问题:学校网络环境复杂,Wi-Fi切换、浏览器休眠都可能导致WebSocket连接断开,全站用长连接是不可靠的。我的方案是WebSocket只负责“你有新消息”的提醒,真正的消息列表还是靠HTTP接口拉取。这样断线重连后,前端会主动调一次未读接口,数据不会丢。

核心推送代码大致如下:

@Component public class AlarmPusher { @Autowired private SimpMessagingTemplate messagingTemplate; public void pushToUser(Long userId, AlarmMessageDto dto) { messagingTemplate.convertAndSendToUser( userId.toString(), "/queue/alarm", dto ); } }

定时扫描任务扫描到超时情况后,先检查Redis中是否存在该隐患的已推送标记,不存在则创建一条alarm_log、推送给相关用户,并把标记写入Redis,设定有效期为24小时。这套防重机制让我在演示时反复点击“手动触发预警”按钮也不会刷出多条重复消息。

预警列表里我额外加了一个“处理状态”字段,分为已推送、已读、已处理。这里的“已处理”能直接跳到隐患详情,形成从预警到整改到销号的完整闭环。很多初版系统预警推完就结束,没有后续动作,结果用户收到一堆提醒却不知道下一步该干什么,这个问题在答辩时经常被挑出来。

4. 我在真实部署中踩过的SpringBoot细节坑

4.1 JDK版本回退、IDEA创建项目失败这类环境问题的根因

打开IDEA创建SpringBoot项目时,我一开始默认选了SpringBoot 3.2并配了JDK 21,结果同事自己电脑用JDK 1.8跑我之前写的项目,启动直接报UnsupportedClassVersionError。这个坑在团队协作或毕业设计演示中特别常见,根因是SpringBoot 3.x强制要求JDK 17及以上,和SpringBoot 2.x的字节码版本完全不同。

后来我把项目统一回退到SpringBoot 2.7.18 + JDK 1.8。操作方法其实不难:

  1. pom.xml里修改spring-boot-starter-parent版本为2.7.18
  2. maven.compiler.sourcetarget设成1.8
  3. 在IDEA的Project Structure里把Project SDK改为JDK 1.8;
  4. 重新导入Maven项目,清理target目录后重启。

另外,IDEA新建SpringBoot项目时如果没有Spring Initializr服务器地址能选,或者只显示SpringBoot 3.x,可以直接打开https://start.spring.io/生成项目压缩包再导入。如果是教学环境内网受限,可以手动建Maven工程再添加依赖,骨架文件也可以自己补全。

这里我特别想说明:版本问题不是小事。如果你拿SpringBoot 3.x去答辩,老师一旦追问“为什么不用JDK 8”,你还能解释;但如果用到某个第三方库只支持JDK 8,项目起不来,就被动了。毕设项目稳妥比炫技重要,这是我从实际项目中得到的最大体会。

4.2 多数据源配置下的事务失效隐患

项目的日志表单独放了一个数据库,和业务库分开。一开始我用Spring配置了固定的DataSource,代码里用@Transactional管理事务。后来为了把业务库和日志库拆开,我引入了动态数据源。

这个改造过程踩了一个非常隐蔽的坑:@DynamicDataSource切换数据源后,@Transactional注解在某些场景下会失效。原因是Spring事务管理器默认绑定在主数据源上,切换数据源后事务仍然持有主库连接,导致用户操作日志写进了业务库而非日志库,数据隔离直接失败。

排查时我通过查看日志发现,虽然Mapper执行到了日志库的SQL,但事务提交始终在主库连接上。最终解决方案是:将日志写入逻辑单独拆成一个独立类,并且把事务隔离级别设为PROPAGATION_REQUIRES_NEW,让日志写入不参与外层业务事务。这也是很多系统把日志服务单独部署的原因之一——避免事务边界拖累核心业务。

给做毕设的同学一个建议:多数据源不是必须的,如果确实要做,先把事务传播行为搞懂,再动手写代码。别等数据错乱了才去补,调试成本远高于预防成本。

4.3 自动装配原理对排查问题的关键作用

我在项目里遇到一个诡异问题:明明引入了spring-boot-starter-data-redis,RedisTemplate也能注入,但连接一直超时。后来查看启动日志发现,SpringBoot自动配置时因为没有在application.yml里配置spring.redis.host,自动装配到了默认的localhost:6379

SpringBoot的自动配置原理这里体现得很明显:RedisAutoConfiguration会根据类路径下是否有RedisOperations来判断是否装配,同时从配置文件读取属性。配错了属性名(比如写成spring.redis.ip)并不会导致启动失败,只会悄悄用默认值,排查起来特别隐蔽。

我建议所有遇到类似问题的同学先看一眼spring-configuration-metadata.json或官方文档,确认配置属性名完全匹配,再加一行debug=true启动观察自动装配报告,看哪些AutoConfiguration匹配了、哪些没匹配。这一点对理解和排查其他组件(如数据源、JPA、Security)同样有效。

4.4 Redis缓存了“过期点位”导致的数据不一致

我缓存了检查点的基础信息到Redis里,key是lab:point:{id},这样扫码时能快速获取实验室名称、检查项清单。问题是当我修改实验室归属学院之后,Redis里的老缓存没有同步删除,导致巡检记录里面关联的学院信息还是旧值。

排查了两天后我决定不再手动删缓存,而是在点位信息更新的Service方法里直接调Redis删除接口,同时给所有缓存统一设置过期时间:

@CacheEvict(value = "lab:point", key = "#labId") public void updateLabInfo(LabInfo labInfo) { ... }

如果团队规范一点,还可以用消息通知机制做缓存双删,但毕设层面用@CacheEvict已经足够。这个坑的价值在于提醒我:缓存不是只有命中率问题,最常见的问题往往是数据一致性

5. 联调演示与答辩准备的经验补充

5.1 演示环境的数据准备与场景模拟

很多项目开发阶段没问题,一到答辩演示就露馅,主要原因不是代码,而是演示数据太假或太少。我建议在演示前构造一套连续、合理、能讲出故事的数据:

  • 创建3个学院、5名用户,分配不同角色;
  • 让一个任务处于“待执行”状态,一个处于“进行中”,一个“已逾期”;
  • 造2条隐患,一条未整改,一条在复查;
  • 手动触发一条预警,确保WebSocket能弹窗。

演示顺序也要提前排练:先登录展示角色权限差异(管理员看到全部、普通用户只看到本院),再扫码执行巡检任务,填报隐患,然后在后台走审核、派单、整改、复查流程,最后打开预警中心看推送记录。这个过程能完整覆盖系统的核心功能。

5.2 答辩时最容易被追问的几个技术点

我提前准备了一组高频问题,在答辩时果然被问到:

  • 为什么用JWT而不是Session?因为巡检端和管理端是前后端分离架构,JWT无状态、跨域友好、天然适合多端共享认证状态,当然也要说明JWT的吊销问题及我校内场景下可接受的取舍。
  • Redis在项目里用在哪?缓存点位信息、验证码、WebSocket在线映射、定时任务的防重标记。
  • 如果用户量变大,这套单机版本怎么扩展?将Redis缓存升级为集群、把定时任务改造成分布式任务调度平台(如XXL-Job)、文件存储转为对象存储OSS、数据库做主从分离,这些点说清楚就能体现架构视野。
  • 安全巡检的“闭环”如何保证?这个问题必须直接回答状态机设计、每个状态被卡住时的预警兜底、以及整改超时自动上抛机制。

答辩时不要背稿,关键是表明“我清楚每个设计背后的取舍”。比如问为什么要JSON存检查项,答案不是“方便”,而是“实验室差异巨大、动态表单是最优解;虽然牺牲了SQL统计能力,但通过后端聚合可以弥补”。

5.3 系统后续还能怎么扩展

其实这个系统的扩展方向非常多,我做完后自己也想了几个后续迭代点:

  • 接入摄像头或者图像识别,对气瓶存放区、通道占用做AI自动识别,异常时自动生成隐患;
  • 对接校园统一身份认证,减少重复登录成本;
  • 巡检记录增加语音转文字模块,现场填报时不用手动打字;
  • 把整改通知接入企业微信或钉钉机器人,提高消息触达率;
  • 实验室设备联动,比如环境传感器读数超过阈值直接预警并联动排风系统。

这些方向不一定在毕设周期内做完,但在文档里写清楚“现状与改进空间”,会让整个项目的思路显得更完整。

最后再分享一个我用得很顺手的小技巧:给所有Controller接口增加统一的返回格式Result<T>和全局异常处理器,前端拦截器能直接根据code判断是否弹出登录超时、权限不够等异常。项目调到后期,基本上不用打开前端控制台,看后端返回的code就能快速定位问题。这种细节总结起来都会成为后续开发里的长期资产。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询