带过好几届毕业设计,每年都能看到不少同学选“校园安全监测系统”这类题目。题目本身不新鲜,但大多数提交上来的成果其实只是一个换了皮肤的 CRUD 管理系统——用户、公告、楼栋信息增删改查,再套个 Bootstrap 模板,导师问一句“预警怎么触发、报警怎么流转”就卡住了。
今年带的一位学弟换了思路:技术栈锁死在 SSM,但把重心从“管理”挪到了“监测与预警”。他没有盲目上 Spring Boot,而是老老实实基于 SSM 框架把设备上报、规则匹配、报警事件流转、处理归档这条链路完整跑通。最后不仅系统演示效果扎实,答辩时评委连续追问了十几分钟都没被问倒。
这篇文章就把这个课题的完整设计与实现思路拆开讲一遍。我尽量按实际开发的顺序走,从框架选型的真实理由、需求模块边界、数据库设计、预警引擎核心实现,到交互对接和部署阶段的坑,全部覆盖。适合正在准备 SSM 毕设、或者想找一个有业务纵深的中型 Java Web 项目的同学参考。
1. 为什么是SSM:看清框架选型的真实逻辑
1.1 SSM三件套的分工
SSM 不是某个框架,而是 Spring、SpringMVC、MyBatis 三个开源框架的组合约定。很多刚接触 Java Web 的同学能说出“Spring 管 Bean、SpringMVC 管请求、MyBatis 管数据库”,但用到项目里就分不清边界了。
我用一个更直白的类比解释:一个公司要运转,得有人管人事和财务、有人当前台、有人管仓库。
- Spring 就是人事加财务部门,所有对象的创建、依赖关系的装配、事务提交回滚都归它管。在你没有任何 new 关键字的情况下,Service 和 Mapper 能被自动注入进来使用,靠的就是 Spring 的 IoC 容器。
- SpringMVC 是前台接待。浏览器发来的 HTTP 请求打到 DispatcherServlet,它根据 URL 找到对应的 Controller 方法,把请求参数绑定成 Java 对象,等 Controller 处理完再把结果渲染成 JSON 返回给前端。
- MyBatis 是仓库管理员,封装了 JDBC 的样板代码,你写 SQL 映射接口,它负责把 ResultSet 转换成 List
或者 AlarmEvent 对象。
三个框架各管一段,组合在一起就是一条完整的请求链路:浏览器发起请求 → SpringMVC 接收分发 → Controller 调 Service → Service 调 Mapper(MyBatis) → 数据库操作 → 结果逆向返回 → JSON 渲染。
1.2 在Spring Boot时代还选SSM,是不是逆潮流
这是这个课题最容易被答辩老师质疑的点。Spring Boot 已经成了 Java 后端开发的默认选择,为什么还要用 SSM?
以我带毕设的经验,选 SSM 通常有三个非常务实的理由。
第一,学校课题库和任务书的明确要求。 不少高校的选题列表里直接写着“基于 SSM 框架的XX系统”,或者导师指定要体现框架整合能力。严格按任务书执行,是最稳妥的选择。
第二,SSM 手动配置过程本身就是学习价值所在。 Spring Boot 提倡约定优于配置,一个注解自动装配搞定一切,反而掩盖了底层原理。用 SSM 手写 applicationContext.xml、spring-mvc.xml,配置数据源、SqlSessionFactory、MapperScannerConfigurer,你会被迫搞清楚每一个组件到底在做什么。这些知识对后续理解 Spring Boot 的自动装配机制有直接帮助。
第三,企业里存量系统维护的需求客观存在。 大量中小型企业内部系统还是十几年前的 SSM 架构,能看懂并维护 SSM 代码仍然是 Java 岗位的常见技能要求。
注意,答辩时千万别只说“题目要求”,要主动把 1.1 里的分工讲清楚,再补一句“SSM 手动配置让我理解了容器与拦截链的底层机制”,这比你吹自己写了多少行代码有效得多。
1.3 系统整体架构与数据流向
这个项目最终采用的架构并不是严格的前后端不分,而是用了“SSM 提供 REST 接口 + 前端页面独立渲染”的方式。
物理部署结构:
code复制浏览器 ──> Nginx(静态资源) ──> Tomcat(SSM 应用) ──> MySQL
└──────> Redis(可选,预警去重)
核心数据流是一条从设备到人的链路:
code复制传感器/摄像头网关 ──> HTTP POST JSON ──> Controller 接收 ──> 签名校验 ──>
解析指标数据 ──> 写入设备数据表 ──> 规则引擎匹配 ──> 触发报警事件 ──>
消息通知(站内信/短信) ──> 安保人员处理 ──> 事件状态流转 ──> 归档统计
这个链路设计帮我把系统分成了两个互相解耦的层次:数据接入层负责“把数据收上来”,业务处理层负责“把数据用起来”。很多同学做这类系统做成了“大杂烩”,问题就出在没把这两层分开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从需求到模块:校园安全监测系统的功能边界
2.1 核心角色与用例
动手编码之前,我习惯先把用户画出来。校园安全监测系统的使用者不是抽象的“用户”,而是四类有明确分工的角色:
| 角色 | 核心诉求 | 主要操作 |
|---|---|---|
| 系统管理员 | 维护基础数据 | 用户管理、区域管理、权限配置 |
| 安保值班员 | 日常监控与报警处理 | 查看实时状态、处理报警、上报情况 |
| 维修人员 | 处理设备故障 | 查看故障清单、更新维修结果 |
| 院系/部门负责人 | 查看管辖范围安全态势 | 查看统计报表、下载通报 |
角色设计决定了权限模型的复杂度。我这里没有引入 Spring Security 那一套重量级权限框架,而是用了最简单的 RBAC(用户-角色-菜单),三张表和两张关联表搞定。功能按钮级别的权限点按需校验,避免过度设计。
2.2 保留的七个功能模块
经过和学弟一轮轮讨论,最终把系统收敛为七个模块,每个模块都能对应到上面某个角色的真实工作场景:
- 用户与权限管理:账号维护、角色分配、菜单配置。这部分是 SSM 项目的基本盘,用来演示 SpringMVC 拦截器、MyBatis 多表关联很合适。
- 区域管理:校区 → 楼栋 → 楼层 → 房间/点位的多级树形结构。设备必须挂接在区域下,否则“哪个位置发生了报警”这个问题就回答不了。
- 设备台账管理:登记摄像头、烟感、门禁、水浸传感器等设备的基础信息、安装位置、在线状态。设备的增删改查不难,难的是和设备上报的数据表怎么关联。
- 实时数据监控:以列表和大屏卡片形式展示设备最近一次上报数据,支持按区域下钻查看。
- 预警中心:这是系统的核心模块,负责预警规则的配置、报警事件产生后的审核、分派、处理和归档。
- 统计报表:按时间、位置、报警类型多维度统计报警趋势,用 ECharts 展示柱状图和折线图。
- 系统日志:记录登录行为和关键操作,保证安全类系统可以回溯。
2.3 砍掉的需求:什么不该做
这节是给所有做毕设的同学提个醒:需求不是越多越好。
一开始学弟提出要加视频流分析,用 OpenCV 检测校园异常行为;还要做人脸识别门禁联动。我全部否决了。
原因是:视频流分析需要对接 RTSP 流媒体协议、处理高并发图像帧、要做模型推理,这套东西和 SSM 后端是完全不同的技术栈,在一个毕设项目里硬塞,只会两头不讨好。人脸识别同理,它涉及硬件 SDK 对接和数据合规问题,不是简单引入一个 SDK 就能演示好的。
最终保留的预警类型全部基于数值型传感器数据:烟感浓度、温度、湿度、门禁开关状态、水电参数。当某个指标超过阈值时触发报警。这类需求用规则引擎处理非常自然,逻辑清晰、可演示性强,又恰好能发挥 SSM 的技术优势。
建议:在系统设计文档里单列一节“需求取舍”,把砍掉的功能和理由写清楚。答辩老师看到这段会认为你做过完整的需求分析,而不是拿到题目就闷头写代码。
3. 数据库设计:让高频上报和实时预警不打架
3.1 核心表结构拆解
这个系统的表设计我把它分成四组:
- 权限组:sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu
- 基础数据组:base_campus、base_building、base_room、base_device
- 监测数据组:monitor_device_data、monitor_alarm_rule
- 预警业务组:alarm_event、alarm_event_handle、alarm_notice_log
其中最容易设计跑偏的是监测数据组和预警业务组。先看监测数据表:
sql复制CREATE TABLE monitor_device_data (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
device_id BIGINT NOT NULL COMMENT '设备ID',
device_type TINYINT NOT NULL COMMENT '设备类型:1-烟感 2-温湿度 3-门禁',
metric_code VARCHAR(32) NOT NULL COMMENT '指标编码,如 TEMP、HUMI、SMOKE',
metric_value DECIMAL(10, 2) NOT NULL COMMENT '指标数值',
collect_time DATETIME NOT NULL COMMENT '设备采集时间',
receive_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '系统接收时间',
status TINYINT DEFAULT 1 COMMENT '数据状态:1-正常 2-异常',
KEY idx_device_time (device_id, collect_time),
KEY idx_room_time (room_id, collect_time)
) COMMENT '设备原始上报数据';
这里有个容易犯的错误:把 device_type 和 metric_code 混在一起设计。比如有的人会设计成 data_type 字段存“温度”,然后又存一个“温度值”。一旦设备类型多起来,字段会越来越多,维护成本陡增。用“设备 ID + 指标编码 + 指标值”的宽表模式,无论接入多少种传感器,表结构都不用动。
再来看报警事件表:
sql复制CREATE TABLE alarm_event (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_no VARCHAR(32) NOT NULL COMMENT '报警编号',
device_id BIGINT NOT NULL COMMENT '触发设备',
room_id BIGINT NOT NULL COMMENT '所属位置',
rule_id BIGINT NOT NULL COMMENT '触发的规则ID',
alarm_type TINYINT NOT NULL COMMENT '报警类型:1-阈值超限 2-状态异常 3-离线',
alarm_level TINYINT NOT NULL COMMENT '级别:1-提示 2-一般 3-严重',
metric_code VARCHAR(32) NOT NULL,
metric_value DECIMAL(10,2) NOT NULL COMMENT '触发时数值',
threshold_value DECIMAL(10,2) NOT NULL COMMENT '当时的阈值',
status TINYINT DEFAULT 0 COMMENT '状态:0-待处理 1-处理中 2-已处理 3-误报 4-已关闭',
create_time DATETIME NOT NULL,
update_time DATETIME DEFAULT NULL,
UNIQUE KEY uk_event_no (event_no),
KEY idx_device_status (device_id, status),
KEY idx_room_create (room_id, create_time)
) COMMENT '报警事件表';
有一个设计细节特别值得注意:报警事件里冗余了 metric_value、threshold_value 和 room_id。报警事件一旦生成,即使后续修改了设备信息、调整了规则阈值,历史报警记录依然保留当时的快照。这是审计追踪的要求:你不能让一条已经产生的报警,因为规则参数变了就“变味”。
3.2 高频数据如何写入
设备数据上报的频率如果高起来,比如门禁系统每分钟上报几百条开关记录,直接把每条数据都落到 MySQL 里会导致两个问题:表膨胀速度过快、写入压力集中在单表。
这个项目里我给出的方案是分级存储:
- 原始设备数据:按天分表或者定期归档,项目里用一张表加创建时间索引先顶着,但后续完全可以把旧数据迁移到历史库。
- 聚合数据:定时任务每 5 分钟对设备数据做一次 MAX/MIN/AVG 聚合,生成监测趋势数据,供图表展示。
- 报警事件:只有当规则引擎判定需要触发时才会写入。
也就是说,高频数据查询走聚合表,明细数据保留但不参与日常统计。这样主业务表的数据量就可以控制在合理范围里。
3.3 报警事件的状态机设计
状态机是这个系统里最容易被忽略却最关键的环节。报警事件的 status 字段不是随便填的,它要能覆盖整个生命周期:
code复制0-待处理 → 1-处理中 → 2-已处理
↘ ↘
3-误报 4-已关闭
- 安保值班员看到新报警,点击“受理”后状态变为处理中,系统记录处理人和受理时间。
- 到达现场核实后,填写处理结果,状态变更为已处理,附件可以上传现场照片。
- 如果判断是传感器故障或误触,可以标记为误报。
- 超时未处理的报警,由定时任务扫描后自动升级,重新分配或通知上级。
实现时我建议不要只更新一个 status 字段,要配合一张报警处理记录表。每次状态变更都插入一条处理记录,保留完整的操作日志。这样统计模块就可以按“首次响应时长”“处理完成时长”做分析。
4. 预警引擎与监测链路:核心代码的实现细节
4.1 数据上报接口:先保证能安全收数据
设备上报接口是外部系统调用的入口,它和浏览器请求最大的区别是:没有登录 Session。所以接口必须做签名校验,否则任何人伪造一条 POST 请求就能制造虚假报警。
接口设计如下:
java复制@RestController
@RequestMapping("/api/v1/device")
public class DeviceDataController {
@Autowired
private AlertRuleService alertRuleService;
@PostMapping("/report")
public Result report(@RequestBody DeviceReportDTO dto,
@RequestHeader("sign") String sign,
@RequestHeader("timestamp") String timestamp) {
// 1. 校验时间戳,防止重放攻击(误差不得超过5分钟)
if (Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) > 300000) {
return Result.fail("请求过期");
}
// 2. 用设备的 secretKey 对参数拼接做 MD5 签名校验
Device device = deviceService.getById(dto.getDeviceId());
String baseStr = dto.getDeviceId() + dto.getData() + timestamp + device.getSecretKey();
String expectedSign = DigestUtils.md5Hex(baseStr);
if (!expectedSign.equals(sign)) {
return Result.fail("签名无效");
}
// 3. 解析 JSON 格式的上报数据
List<MetricItem> metrics = JSON.parseArray(dto.getData(), MetricItem.class);
deviceDataService.saveBatch(metrics);
// 4. 逐个指标走规则匹配
for (MetricItem item : metrics) {
alertRuleService.evaluate(device, item);
}
return Result.success();
}
}
这里有个教科书上不讲的细节:签名原串里必须带上时间戳,否则同一个签名可以无限重放。设备端每次请求生成一次签名,服务端只认 5 分钟内的时间戳,这是最朴素但最有效的防重放方案。
统一返回结构 Result 包含了 code、message、data 三个字段。前后端约定好 code 为 200 时表示成功,其余为失败或业务异常。这样前端 axios 拦截器统一处理错误提示,后端也不用为每个接口定制返回类型。
4.2 规则匹配:从“数据”到“报警”的关键一跳
规则引擎的核心不是一堆 if/else,而是把判断逻辑抽象成可配置的对象。预警规则表字段如下:
| 字段 | 含义 | 示例 |
|---|---|---|
| rule_name | 规则名称 | 实验室烟感浓度告警 |
| device_type | 适用设备类型 | 烟感(1) |
| metric_code | 监测指标 | SMOKE |
| operator | 比较操作符 | gt / lt / eq |
| threshold | 阈值 | 5.00 |
| duration_seconds | 持续时间 | 30 |
| alarm_level | 报警级别 | 3(严重) |
规则匹配流程要解决一个实际问题:单次上报超标不一定触发报警,因为传感器存在瞬时抖动。更稳妥的策略是“持续时间窗口内连续触发 N 次才报警”。
代码逻辑大致如下:
java复制public void evaluate(Device device, MetricItem item) {
List<AlertRule> rules = alertRuleMapper.selectByDeviceTypeAndMetric(
device.getDeviceType(), item.getMetricCode());
for (AlertRule rule : rules) {
boolean matched = compare(item.getValue(), rule.getOperator(), rule.getThreshold());
if (!matched) {
// 未匹配则清除该设备的连续超限计数
redisTemplate.delete(buildCountKey(device.getId(), rule.getId()));
continue;
}
// 匹配则计数 +1
String countKey = buildCountKey(device.getId(), rule.getId());
Long count = redisTemplate.opsForValue().increment(countKey);
redisTemplate.expire(countKey, Duration.ofSeconds(rule.getDurationSeconds() * 2));
if (count >= rule.getDurationSeconds()) {
createAlarmEvent(device, item, rule);
redisTemplate.delete(countKey);
}
}
}
阈值比较本身只是 operator 枚举的 switch 分发,价值不大。真正的价值在连续计数逻辑用 Redis 存储:既解决了多台设备并发触碰同一规则的计数问题,又避免了每次查询数据库记数的性能压力。
如果项目里没引入 Redis,也可以用数据库表连续判断,但要注意并发的正确性。实际演示中 Redis 方案的抗压表现要好很多,建议有条件就加上,只有 sm-cache 一个依赖,配置极其简单。
4.3 预警去重与升级:防止“报警轰炸”
报警去重是安全监测系统上线后必须处理的问题。试想烟感被持续触发,同一规则每 5 秒产生一条报警,一小时内就会生成 720 条报警事件,值班员手机直接被打爆。
我的去重策略是在 createAlarmEvent 入口加“时间窗口内不重复触发”判断:
java复制private void createAlarmEvent(Device device, MetricItem item, AlertRule rule) {
String lockKey = "alarm:dup:" + device.getId() + ":" + rule.getId();
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, "1",
Duration.ofSeconds(rule.getAlarmIntervalSeconds()));
if (!Boolean.TRUE.equals(success)) {
log.info("重复报警已过滤:device={}, rule={}", device.getId(), rule.getId());
return;
}
// 真正生成报警事件
alarmEventMapper.insert(buildEvent(device, item, rule));
}
setIfAbsent 就是 Redis 的 SETNX 命令,同一个设备同一个规则在 interval 时间内只能成功插入一次。这个方案天然支持分布式环境,即使未来把报警服务拆出去也能直接用。
报警升级则是另一个方向的保障:该触发的报警不能没被处理。我写了一个定时任务,每 30 秒扫描所有状态为“待处理”且创建时间超过 30 分钟的报警事件,自动将其级别提升、重新分配给在线值班员,并补发一条通知。
4.4 前端展示层:Vue3 连接 SSM 的正确姿势
现在做毕设,纯 JSP 页面已经不太够看了。我建议用前后端分离的方式,前端 Vue3 + Element Plus,后端 SSM 纯写接口。这样毕业设计视觉效果好,也避开 JSP 调试麻烦的问题。
Vue3 的 Vite 开发服务器端口是 5173,后端 Tomcat 端口是 8080,必然存在跨域。在 SpringMVC 中配置全局 CORS:
java复制@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("http://localhost:*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
注意 allowCredentials(true) 时不能使用 allowedOrigins("*"),必须用 allowedOriginPatterns 指定具体来源,这是很多同学会踩的坑,一旦配置错误浏览器报 CORS 错误特别难排查。
WebSocket 实时推送这个功能,毕设阶段我建议先不用,用轮询就够了。前端每 5 秒调用一次 /api/alarm/recent 获取最近的报警事件,视觉欺骗已经足够。WebSocket 在答辩时和 SpringMVC 拦截器结合容易出问题,不属于必要项。
5. 我在开发部署中踩过的五个具体坑
5.1 MyBatis 动态 SQL 导致索引失效
需求里有个功能:按时间段查询报警记录,时间段不从设备采集时间而是从报警创建时间算。第一版 SQL 写成了:
xml复制<if test="startTime != null">
AND DATE_FORMAT(create_time, '%Y-%m-%d') = DATE_FORMAT(#{startTime}, '%Y-%m-%d')
</if>
这个写法能查出正确结果,但 create_time 列上的索引完全失效,因为对列做了函数运算。数据量一上来查询直接超时。
正确的做法是把比较条件写成范围:
xml复制<if test="startTime != null">
AND create_time >= #{startTime}
</if>
<if test="endTime != null">
AND create_time <= #{endTime}
</if>
前端传的日期如果是“2025-04-01”,后端要拼上“00:00:00”到“23:59:59”。这个改动让查询从全表扫描降到了索引范围扫描,实测 50 万条数据下速度从 3 秒降到了 30 毫秒。
5.2 时间字段的时区偏移 8 小时
部署到服务器后,设备上报数据显示的设备采集时间和数据库存储时间相差 8 个小时。排查后发现是 JDBC URL 没有指定时区:
code复制jdbc:mysql://localhost:3306/campus_safety?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
加上 serverTimezone=Asia/Shanghai 后恢复正常。这个问题在本地开发时通常不会暴露,因为本机 MySQL 和 Java 默认时区一致,一上服务器就现形。建议配置数据库连接时把时区参数写死。
5.3 并发上报导致重复报警
某次压测模拟 50 个设备同时上报,结果同一规则的报警事件出现了 40 条重复。原因是最开始的报警生成逻辑是先查是否已有未处理的同设备同类报警,再决定是否插入,这个“先查后插”的流程在并发下必然出现竞态条件。
用 MySQL 唯一索引也可以解决,比如给 (device_id, rule_id, status) 加唯一约束,但 status 会变化,语义不对。最终的方案就是 4.3 里讲的 Redis SETNX 去重——简单、可靠、对现有表结构零侵入。
5.4 连接池耗尽:SQL 日志永远在等待
系统运行一段时间后,经常出现接口超时。查了一下 Tomcat JDBC 连接池的监控,active 连接数一直顶在 20 个(默认 maxActive),SQL 日志里全部是“正在等待获取数据库连接”。
问题出在两个地方:一是设备数据批量插入的事务范围没控制好,一个事务插了 1000 条数据,持锁时间太长;二是个别聚合查询 SQL 没有分页,一次返回几万行。
修复方式:把批量插入拆成每 200 条一个事务,慢查询全部走聚合表,连接池的 maxActive 也调到 100,maxWait 设置 5 秒后快速失败。改完后再也没出现连接池耗尽。
5.5 Multipart 文件上传被 1MB 限制挡下
报警处理时需要上传现场照片作为附件,开发时日志一直报 MaxUploadSizeExceededException。SpringMVC 的 MultipartResolver 默认限制上传文件大小为 1MB,手机拍的照片动不动就 3MB 以上。
配置如下,顺手把请求大小也调大:
xml复制<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="10485760"/>
<property name="maxInMemorySize" value="1048576"/>
</bean>
上传文件超过限制后要记得用 @ExceptionHandler 捕获并返回友好提示,否则用户看到的是整页 500 错误。
6. 演示准备与答辩加分的几个细节
代码和功能都做完了,演示效果同样重要。每年答辩都能看到有人在现场手忙脚乱造数据,体验很糟糕。我建议如下。
准备一份模拟数据生成器。 写一个独立的 Java 类,随机生成烟感、温湿度、门禁数据,以 HTTP 请求调用上报接口。演示前先启动模拟器,让系统自带实时数据流。注意模拟数据的范围要贴合实际,温度在 20 到 35 度之间波动,偶尔超温触发报警,不能全程都是报警,这样评委反而觉得假。
要重点演示“闭环”。 从模拟器触发一条报警,到系统弹窗提醒,到值班员受理、填写处理意见、上传照片,最后在统计报表里看到这条报警被归档。整个过程不超过三分钟,但把系统的每一个核心模块都串了一遍。提前写好这个演示脚本,反复排练两遍,比临时点菜单自然得多。
要把“做过的”和“没做的”边界讲清楚。 建议准备一张表:哪些模块是完整实现的,哪些只做了接口预留,哪些明确不做。比如“实时视频流分析暂未实现,系统预留了视频设备接入接口,当前报警以传感器数据为准”。这种坦诚反而会增加可信度。
我在带这个项目的过程中,最深的一点体会是:SSM 这个技术栈本身不会让系统高级,真正拉开差距的是有没有一条完整、自洽的业务链路。 你花三个小时把设备上报接口的签名校验写好,比花三天套一个花哨的前端模板,在答辩和实际能力上都要值得多。所以,少纠结“要不要把框架升级成微服务”,多想想“一条报警从产生到处置完,我的系统能不能一直追踪到底”,这才是这类毕设真正的分水岭。
