公司动态
Spring Boot智慧社区老年人安全监护系统:从设备接入到告警闭环
在很多社区项目里老年人安全监护往往停留在“装一个摄像头”或者“买一块智能手表”的层面但真正落地时数据从哪来、异常怎么识别、告警怎么通知、家属和社区工作人员如何联动这些环节并没有被完整串起来。智慧社区老年人安全监护系统的核心工作不是做一个App而是把设备采集、业务处理、告警判断、通知送达和工单闭环这几个环节设计成一条可持续运维的链路。这篇文章围绕一个可落地的后端系统展开从需求拆解、技术选型、数据库设计、接口实现到告警推送和排错完整走一遍设计与实现过程适合有Spring Boot基础、想参与社区类项目或物联网后端开发的读者。1. 先拆分监护系统的需求再谈代码实现如果直接进入开发很容易把系统做成一张CRUD页面。老年人安全监护系统的难点在于数据采集频率高、异常场景多、告警时效要求高而且使用对象不仅包括老年人本人还包括子女、社区管理员、网格员和医护人员。因此第一步是把角色、业务场景和核心流程梳理清楚。1.1 核心角色和他们的真实诉求一个智慧社区老年人安全监护系统至少需要三类角色协同工作。第一类是监护对象也就是社区内的老年人通常佩戴智能手环、胸牌或使用家庭网关设备设备负责采集心率、血氧、步数、体温、位置和跌倒状态。第二类是家属或紧急联系人他们关心的是老人是否发生跌倒、健康状况是否出现明显波动以及告警后有没有人跟进。第三类是社区运营人员和网格员他们需要接收告警工单、上门核实、记录处理结果并对重点老人建立档案。数据库中不能只存一张用户表。老年人和家属之间的关系、老年人和设备的绑定关系、老年人和社区网格的归属关系都要单独建模。否则后续做“按网格员分派告警”“按家属批量通知”这类功能时SQL会非常难写。1.2 核心业务场景和异常类型监护系统的核心不是展示实时数据而是识别异常并触发闭环处理。常见的异常类型包括跌倒检测、心率异常、血氧过低、长时不动、SOS主动求救、设备离线。其中跌倒和SOS属于紧急事件需要立即通知心率、血氧异常属于健康风险事件需要连续观察后再告警设备离线属于设备事件不能反复骚扰家属但需要通知社区管理人员排查。每个异常类型都应该有自己的触发阈值、持续时间、通知对象和升级策略。例如心率异常不能单次超过阈值就告警要设置持续次数或者时间窗口防止运动后一过性升高产生误报。1.3 核心业务流程从传感器数据到工单闭环完整的数据流是这样的设备采集数据后通过MQTT或HTTP上报到服务端服务端先做数据格式校验再判断设备是否在线随后写入原始数据表指标分析模块按照老人档案中的阈值规则判断是否命中异常命中后生成告警事件并根据级别执行通知策略给家属发送短信或App推送给网格员创建工单网格员处理完成后回填处理结果系统记录整个生命周期。这里有一个容易忽略的点告警必须支持去重和合并。设备每5秒上报一次数据如果心率持续偏高不能每5秒生成一条告警。正确做法是在同一事件窗口内只生成一条告警后面的数据作为该告警的观察记录继续追加防止告警风暴。2. 技术选型和整体架构设计监护系统属于典型的数据采集类业务系统后端要求能够接收高频写入同时支持实时推送和异步任务处理。技术选型不应追求复杂而是让每个组件都有明确分工。2.1 后端框架和存储选型服务端以 Java Spring Boot 为主适合快速构建业务接口和任务调度。持久层使用 MySQL 存储老人档案、告警记录、工单等结构化业务数据。设备上报的原始体征数据写入量比较大可以先用 MySQL 分表或时序数据库保存数据量大的项目可引入 TDengine 或 InfluxDB如果项目处于起步阶段MySQL 按时间分表同样能支撑一段时间。缓存使用 Redis主要用来保存设备最新状态、在线状态、告警去重窗口和分布式锁。告警通知使用消息队列解耦例如 RocketMQ 或 RabbitMQ。设备接入协议优先选择 MQTT社区网关和设备端大多支持服务端可以用 EMQX 作为 Broker后端通过 MQTT 客户端订阅主题。2.2 前后端模块划分系统按功能模块划分为设备接入模块、老人档案模块、指标分析模块、告警模块、工单模块、通知模块和数据看板模块。设备接入模块负责接收设备消息解析不同厂商协议指标分析模块负责执行阈值规则告警模块负责生成事件和去重通知模块负责通过短信、App推送、微信模板消息等渠道触达用户。前端可以做成Web管理后台和家属端小程序两部分。社区工作人员使用Web后台家属使用小程序查看老人健康档案、接收告警推送。两者共用同一套后端接口通过不同角色权限控制访问范围。2.3 数据流和核心组件交互这里给出一个典型的数据交互流程设备通过MQTT发布消息到elder/{deviceId}/data主题EMQX 通过规则引擎将消息转发到后端 MQTT 消费服务后端解析并校验数据后先更新 Redis 中的设备状态原始数据异步写入数据库分析服务按老人阈值规则判断命中异常后生成告警事件告警服务通过消息队列发送通知通知服务调用短信平台和推送网关网格员在 Web 后台领取并处理工单。这个链路中消息队列的作用是削峰。如果几百台设备同时上报短信接口可能被瞬间打满。通过队列把通知任务排队发送可以保护下游渠道。3. 数据库设计要同时满足查询和告警去重数据库设计是监护系统最容易返工的部分。设计原则是业务表尽量满足范式设备原始数据表单独设计便于分表和归档告警去重依赖Redis但也要保留唯一索引作为兜底。3.1 核心数据表结构先设计五张核心表老年用户表、设备信息表、体征原始数据表、告警事件表、工单表。以下SQL用于说明字段设计思路实际项目要根据部署环境调整存储引擎和字符集。CREATE TABLE elder_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT DEFAULT NULL COMMENT 性别 1男 2女, birthday DATE DEFAULT NULL COMMENT 出生日期, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, community_id BIGINT DEFAULT NULL COMMENT 社区ID, grid_id BIGINT DEFAULT NULL COMMENT 网格ID, room_address VARCHAR(200) DEFAULT NULL COMMENT 居住地址, health_level TINYINT DEFAULT 1 COMMENT 健康等级 1正常 2关注 3高风险, emergency_contact VARCHAR(20) DEFAULT NULL COMMENT 紧急联系人电话, status TINYINT DEFAULT 1 COMMENT 状态 1正常 0注销, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_community_grid (community_id, grid_id) ) ENGINEInnoDB COMMENT老年用户档案表;老年用户表除了基本资料还要记录社区和网格归属。健康等级字段用于调整告警阈值例如高风险老人的心率上限会比正常老人更严格。紧急联系人电话是告警通知的兜底字段。CREATE TABLE device_info ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, device_code VARCHAR(64) NOT NULL COMMENT 设备编号, device_type TINYINT DEFAULT 1 COMMENT 设备类型 1手环 2胸牌 3网关, elder_id BIGINT DEFAULT NULL COMMENT 绑定的老人ID, status TINYINT DEFAULT 1 COMMENT 设备状态 1在线 0离线, last_report_time DATETIME DEFAULT NULL COMMENT 最后上报时间, bind_time DATETIME DEFAULT NULL COMMENT 绑定时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_device_code (device_code), KEY idx_elder_id (elder_id) ) ENGINEInnoDB COMMENT设备信息表;设备表和老人表解耦因为设备可能维修、更换或者重新绑定。last_report_time字段是判断设备离线的关键依据建议在Redis里也存一份避免每次都查数据库。CREATE TABLE health_record ( id BIGINT NOT NULL AUTO_INCREMENT, elder_id BIGINT NOT NULL, device_code VARCHAR(64) NOT NULL, heart_rate INT DEFAULT NULL COMMENT 心率, blood_oxygen INT DEFAULT NULL COMMENT 血氧百分比, body_temp DECIMAL(5,2) DEFAULT NULL COMMENT 体温, steps INT DEFAULT NULL COMMENT 步数, fall_status TINYINT DEFAULT 0 COMMENT 跌倒状态 0正常 1疑似跌倒, sos_flag TINYINT DEFAULT 0 COMMENT SOS 0正常 1触发, longitude DECIMAL(10,6) DEFAULT NULL, latitude DECIMAL(10,6) DEFAULT NULL, report_time DATETIME NOT NULL COMMENT 设备上报时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_elder_report_time (elder_id, report_time) ) ENGINEInnoDB COMMENT体征原始数据表;上报时间不能直接使用数据库当前时间必须使用设备端或解析后得到的时间戳。原因在于网络延迟会导致数据乱序按数据库时间排序会出现“更早的数据后写入”的情况影响指标分析和历史回放。CREATE TABLE alert_event ( id BIGINT NOT NULL AUTO_INCREMENT, elder_id BIGINT NOT NULL, device_code VARCHAR(64) DEFAULT NULL, alert_type TINYINT NOT NULL COMMENT 告警类型 1跌倒 2心率异常 3血氧低 4SOS 5长时不动 6设备离线, alert_level TINYINT NOT NULL COMMENT 级别 1紧急 2重要 3一般, status TINYINT DEFAULT 0 COMMENT 0待处理 1处理中 2已处理 3误报关闭, max_heart_rate INT DEFAULT NULL, min_blood_oxygen INT DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, location_desc VARCHAR(255) DEFAULT NULL, handle_user_id BIGINT DEFAULT NULL, handle_remark VARCHAR(500) DEFAULT NULL, handle_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_elder_status (elder_id, status), KEY idx_type_start_time (alert_type, start_time) ) ENGINEInnoDB COMMENT告警事件表;告警事件表保存的是“已经确认的异常事件”不是每次上报数据。max_heart_rate和min_blood_oxygen用于记录该事件窗口内的极端值方便后续分析。status字段兼容工单流程误报关闭也是一种处理结果。工单表可以和告警表合并也可以单独设计。如果社区需要处理过程有更完整记录单独建表更清晰。CREATE TABLE alert_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, alert_id BIGINT NOT NULL, elder_id BIGINT NOT NULL, grid_id BIGINT DEFAULT NULL, order_status TINYINT DEFAULT 0 COMMENT 0待接单 1处理中 2已完成 3已取消, assign_user_id BIGINT DEFAULT NULL, process_remark VARCHAR(500) DEFAULT NULL, finish_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_alert_id (alert_id) ) ENGINEInnoDB COMMENT告警工单表;uk_alert_id唯一索引保证一条告警只能生成一张工单避免重复分派。3.2 告警去重设计移动端设备经常因为网络弱或者GPS信号差重复上报同一条数据单纯依赖业务代码去重不够。Redis需要保存告警事件窗口的标记键结构可以设计为alert:dedup:{elderId}:{alertType}:{yyyyMMddHHmm}并设置过期时间。例如心率异常窗口为5分钟那么当同一老人同一类型在5分钟内已存在告警时就不再创建新告警而是向原告警追加数据。数据库层面也可以加唯一索引兜底。但唯一索引只能防止完全重复无法处理时间窗口内的合并。因此推荐用Redis做一级去重数据库唯一索引做最终兜底而不是反向设计。3.3 数据库分表和时间归档体征数据表的写入频率远高于业务表。社区规模扩大后health_record表很容易达到千万级。常见做法是按月分表例如health_record_202501。后端插入数据时根据report_time动态计算表名。查询历史趋势时先根据时间范围定位到对应月份表避免全表扫描。分表逻辑应该封装在数据访问层业务代码不感知具体表名。同时超过一年的原始数据可以迁移到冷存储或归档表热库只保留近期数据降低备份和查询成本。4. 后端工程实现与关键代码工程结构采用常见的 Spring Boot 分层方式controller、service、mapper、domain。下面对照一个最小闭环从依赖、配置、实体到告警判断逻辑逐步展示。4.1 Maven 依赖和项目结构项目使用 Spring Boot 2.7 或 3.x 均可本文示例以 2.7 为主。核心依赖包括 Web、MyBatis-Plus、Redis、MQTT 客户端、消息队列和定时任务。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.eclipse.paho/groupId artifactIdorg.eclipse.paho.client.mqttv3/artifactId version1.2.5/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-amqp/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency目录结构建议按模块分包com.community.elder ├── controller ├── service │ ├── device │ ├── alert │ ├── order │ └── notify ├── mapper ├── entity ├── mqtt ├── config └── job不建议把所有类都堆在common或utils包下面。监护系统的业务边界相对清晰按业务模块分包后后续扩展体温监测、用药提醒等功能时不会互相影响。4.2 核心配置文件application.yml中需要关注数据源、Redis、MQTT 和消息队列的配置。server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/elder_community?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 database: 0 rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest mqtt: broker: tcp://127.0.0.1:1883 client-id: elder-server username: emqx_user password: emqx_password topic-prefix: elder数据库连接串中必须设置serverTimezone否则设备上报的report_time和数据库时间可能相差8小时。生产环境数据库账号不要使用 root要单独创建只具备业务库权限的账号。4.3 设备数据接入与解析设备上报消息需要经过解码和校验。以手环上报数据为例消息可能是JSON格式{ deviceCode: HW1001, heartRate: 88, bloodOxygen: 97, bodyTemp: 36.5, steps: 1200, fallStatus: 0, sosFlag: 0, reportTime: 2025-01-20 14:30:00 }MQTT消费端收到消息后先解析JSON再判断设备是否在系统登记。未登记的设备消息不能直接入库应该记录日志后丢弃或放入死信队列。Component RequiredArgsConstructor public class DeviceDataHandler { private final DeviceInfoMapper deviceInfoMapper; private final HealthRecordMapper healthRecordMapper; private final RedisTemplateString, String redisTemplate; private final DeviceStatusService deviceStatusService; public void handle(String payload) { DeviceReportDTO dto JSON.parseObject(payload, DeviceReportDTO.class); DeviceInfo device deviceInfoMapper.selectByCode(dto.getDeviceCode()); if (device null) { log.warn(device not registered: {}, dto.getDeviceCode()); return; } // 更新在线状态 deviceStatusService.markOnline(dto.getDeviceCode(), dto.getReportTime()); // 写入原始数据 HealthRecord record new HealthRecord(); record.setElderId(device.getElderId()); record.setDeviceCode(dto.getDeviceCode()); record.setHeartRate(dto.getHeartRate()); // ... 其余字段赋值 healthRecordMapper.insert(record); } }设备在线状态不建议每次写数据库。可以在Redis中保存device:status:{deviceCode}值包含最后上报时间和在线标记。离线检测通过定时任务扫描 Redis如果超过设定时间未更新则把设备标记为离线并触发设备离线告警。4.4 告警判断逻辑告警判断既要支持简单阈值也要支持时间窗口。以心率异常为例不能一上报就判断需要同时结合老人健康等级和连续多次数据。public AlertEvent checkHeartRateAlert(ElderUser elder, HealthRecord record) { Integer heartRate record.getHeartRate(); if (heartRate null) { return null; } int highLimit getHighHeartRateLimit(elder.getHealthLevel()); int lowLimit getLowHeartRateLimit(elder.getHealthLevel()); if (heartRate highLimit || heartRate lowLimit) { String windowKey alert:dedup: elder.getId() :heart: DateUtil.format(record.getReportTime(), yyyyMMddHHmm); Boolean first redisTemplate.opsForValue().setIfAbsent(windowKey, 1, Duration.ofMinutes(5)); if (Boolean.TRUE.equals(first)) { AlertEvent event new AlertEvent(); event.setElderId(elder.getId()); event.setDeviceCode(record.getDeviceCode()); event.setAlertType(AlertTypeEnum.HEART_RATE.getCode()); event.setAlertLevel(AlertLevelEnum.IMPORTANT.getCode()); event.setStatus(AlertStatusEnum.PENDING.getCode()); event.setStartTime(record.getReportTime()); event.setMaxHeartRate(heartRate); event.setMinBloodOxygen(record.getBloodOxygen()); return event; } } return null; }setIfAbsent是Redis的原子操作多个线程同时处理同一老人的数据时只有一个线程能成功创建去重标记。这样可以避免高并发下重复生成告警。去重窗口的分钟粒度要与业务匹配如果窗口定为5分钟而使用当前时间的分钟取整容易出现边界问题。更稳妥的做法是记录首次异常时间每次判断当前时间与首次异常时间差是否超过窗口。4.5 已确认告警的事件分发告警生成后需要通知家属和创建工单。这里不能直接在服务里同步调用短信接口要通过消息队列异步处理。public void publishAlert(AlertEvent event) { alertEventMapper.insert(event); AlertNotifyMessage message new AlertNotifyMessage(); message.setAlertId(event.getId()); message.setElderId(event.getElderId()); message.setAlertType(event.getAlertType()); message.setAlertLevel(event.getAlertLevel()); rabbitTemplate.convertAndSend(alert.exchange, alert.notify, message); }通知消费者收到消息后根据告警级别决定通知渠道。紧急告警需要短信加电话语音提醒重要告警发送App推送和短信一般告警只发送App推送或站内信。通知失败时要有重试机制且重试次数有限最终进入失败日志表人工排查。5. 实时推送和告警通知链路社区工作人员查看后台时不希望一直刷新页面。看板上的告警数量和最新事件需要实时变化这可以通过WebSocket推送实现。而家属端的通知则需要走短信、App推送或微信服务号模板消息。5.1 服务端WebSocket推送Spring Boot 集成 WebSocket 最直接的方式是使用spring-boot-starter-websocket。服务端维护一个会话管理器将网格员ID与WebSocket Session绑定。告警事件创建后通过会话管理器推送给对应社区和网格的在线用户。Component public class AlertWebSocket { private final MapLong, Session sessionMap new ConcurrentHashMap(); public void bindUser(Long userId, Session session) { sessionMap.put(userId, session); } public void unbindUser(Long userId) { sessionMap.remove(userId); } public void sendToUser(Long userId, Object message) { Session session sessionMap.get(userId); if (session ! null session.isOpen()) { session.getBasicRemote().sendText(JSON.toJSONString(message)); } } }WebSocket推送要处理断线重连和重复绑定问题。同一用户从多个浏览器登录时后登录的Session会覆盖之前的Session需要按业务定义规则通常保留最新登录会话即可。5.2 通知渠道的降级策略短信、语音通知渠道存在限流和费用成本必须做降级。例如同一家属在10分钟内最多收到3条告警短信超过后只推送App消息。夜间紧急告警要允许配置静音时段避免非紧急心率异常打扰休息但跌倒和SOS不受静音限制。这个策略可以用Redis计数实现。通知服务发送前检查键notify:sms:{familyId}:{hour}如果数量超过限制则跳过短信只发送站内推送。5.3 消息队列的可靠投递RocketMQ 或 RabbitMQ 都需要考虑投递可靠性。生产者发送消息后要确认是否成功消费者处理完业务后手动ACK防止消息未处理就丢失。消息消费失败时不能无限重试应该进入死信队列后续通过界面人工触发补发。这里还要注意一个坑消息消费者中的业务方法必须保证幂等。因为网络闪断可能导致同一个通知消息被消费两次。消费者收到消息后先查询通知记录表是否存在相同alertId的通知记录存在则直接返回避免重复发送。6. 运行验证与联调测试只把代码写出来不够运行验证阶段要模拟设备上报、触发阈值、生成告警、推送通知、处理工单的完整链路。联调前要准备好 MySQL、Redis、MQTT Broker 和消息队列环境。6.1 本地环境准备清单组件推荐版本用途JDK1.8 或 11运行 Spring Boot 服务MySQL5.7 或 8.0存储业务数据Redis6.x设备状态与告警去重EMQX4.x 或 5.xMQTT 消息接入RabbitMQ3.x异步通知削峰学习环境可以用 Docker 快速启动这些中间件生产环境需要单独部署并配置访问控制。6.2 设备数据模拟脚本联调时不可能直接使用真实硬件可以用一个简单的 Python 脚本模拟设备上报。脚本每隔 5 秒发布一条消息MongoDB 数据由 MQTT 客户端发布到对应主题服务端会自动消费。import paho.mqtt.client as mqtt import json import time import random from datetime import datetime client mqtt.Client() client.username_pw_set(emqx_user, emqx_password) client.connect(127.0.0.1, 1883, 60) device_code HW1001 for i in range(20): payload { deviceCode: device_code, heartRate: random.randint(70, 120), bloodOxygen: random.randint(92, 99), bodyTemp: round(random.uniform(36.0, 37.0), 2), steps: random.randint(0, 30), fallStatus: 0, sosFlag: 0, reportTime: datetime.now().strftime(%Y-%m-%d %H:%M:%S) } client.publish(felder/{device_code}/data, json.dumps(payload)) time.sleep(5) client.disconnect()模拟时要注意设备编码必须已经在device_info表中存在否则服务端会丢弃消息。调试阶段可以把未登记设备日志调整成WARN级别便于在控制台直接看到。6.3 接口验证和预期结果以告警查询接口为例请求参数可以按老人、告警状态、时间范围查询返回结果中应包含告警类型、级别、处理状态和时间。正常结果示例如下{ code: 0, data: { total: 1, records: [ { alertId: 1001, elderId: 1, alertType: 2, alertLevel: 2, status: 0, startTime: 2025-01-20 14:30:00, maxHeartRate: 132 } ] } }验证时重点检查三点第一设备在线状态是否在Redis中更新第二异常数据是否只生成一条告警而不是多条第三通知消息是否进入消息队列并被消费者成功发送。6.4 验证误报和恢复场景除了正常告警链路还要验证误报和恢复。例如连续上报心率异常后老人心率恢复正常那么告警事件应该在后续数据到达时自动更新状态或追加记录。常见做法是维护一个“事件中”标志当窗口内数据恢复正常时关闭该事件窗口并将最终恢复时间写入告警记录。这个场景必须纳入测试否则线上会出现大量“永远待处理”的僵尸告警最终导致工作人员忽略真正紧急的事件。7. 常见问题排查与踩坑记录监护系统上线后大多数问题集中在设备离线、告警重复、数据时间偏差和通知不送达。以下排查路径基于实际项目中高频出现的问题整理。7.1 设备一直显示在线但实际已离线现象是设备已经断电后台仍显示在线。原因是服务端只在上报数据时更新在线状态没有做离线检测。解决方式是增加一个定时任务周期性检查 Redis 中每个设备的最后上报时间如果超过阈值例如3分钟则更新设备状态为离线并生成设备离线告警。排查命令可以用 Redis 查看设备状态键redis-cli GET device:status:HW1001如果该键不存在说明设备从未上报应先检查 MQTT 主题是否订阅、设备编码是否正确。如果键存在但最后上报时间很旧说明消息消费或入库链路存在阻塞需要排查 MQTT 消费线程池和数据库写入性能。7.2 告警重复或者告警风暴告警风暴通常有两个原因。一是去重键设计错误按分钟取整导致边界窗口内生成多条告警二是告警服务消费消息后重试但没有做幂等。处理方式参考前面介绍去重键使用事件首次时间而不是当前分钟取整消费者处理前先查询告警通知记录。如果 MySQL 中已经出现重复告警可以先用聚合查询找出同一老人、同一类型、同一时间窗口内的多条记录再由管理员在后台合并关闭。更重要是修复生成逻辑清理存量数据只能治标。7.3 设备上报时间比真实时间早8小时这个问题的根因一般是设备端时间戳使用的是UTC而服务端未做时区转换。处理方式是在接入层统一按设备协议解析时间统一转换为 Asia/Shanghai 时间。数据库连接串中也要设置serverTimezoneAsia/Shanghai。如果数据已经写错可以通过SQL批量修正但要先确认设备上报的原始时间语义不能盲目加8小时。7.4 通知发送失败和重试耗尽通知发送失败要观察失败原因。短信渠道常见原因是模板参数错误、签名不一致、手机号格式不对App推送失败则可能是用户未登录、推送Token过期。生产环境必须记录每次通知的渠道响应码和响应消息将三次重试仍失败的通知写入失败表并提供人工补发入口。推荐增加通知失败指标监控当失败率超过阈值时向运维人员发送告警而不是等到用户投诉才发现渠道异常。7.5 高频写入导致数据库锁等待体征数据高频写入时如果业务表索引设计不当会造成锁竞争。解决思路是原始数据按月分表降低单表热点批量插入代替逐条插入例如缓存10条数据后一次写入写入路由到独立的从库或专用库避免影响告警业务查询。实际项目里不建议在体征表上加过多二级索引只保留elder_id report_time的联合索引即可。查询历史趋势时使用时间范围作为第一条件可以有效利用索引而不会产生大量随机IO。8. 生产环境部署、安全和最佳实践开发环境跑通后距离稳定上线还有很长的路。监护系统直接关系到老年人生命安全生产环境必须从高可用、安全性、可观测性和运营效率四个维度加强。8.1 部署架构与高可用设计生产环境至少使用两台应用服务器放在Nginx后面MySQL 和 Redis 使用主从或云厂商高可用实例。MQTT Broker 可以采用集群部署避免单点故障导致所有设备离线。应用服务是无状态的可以水平扩容。任务调度建议使用独立的任务节点防止多节点同时执行离线检测导致重复告警。如果使用Spring自带的Scheduled需要引入分布式锁确保同一时刻只有一个节点执行任务。Scheduled(fixedDelay 60000) public void checkDeviceOffline() { String lockKey job:device:offline:lock; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofMinutes(2)); if (!Boolean.TRUE.equals(locked)) { return; } try { deviceStatusService.checkOffline(); } finally { redisTemplate.delete(lockKey); } }任务执行完必须释放锁释放时要确认锁还是自己的避免误删其他节点刚获取的锁。更稳妥的做法是使用 Redisson 的RLock它实现了看门狗续期和安全的释放逻辑。8.2 接口安全和数据权限后台接口必须登录鉴权建议使用 Spring Security 或 Sa-Token。角色权限需要区分家属只能看自己绑定的老人数据网格员只能看本网格的数据社区管理员可以看全部数据。查询参数不能信任前端传的elderId要根据当前登录用户动态拼接数据权限条件。设备上报接口虽然不要求登录但要有鉴权机制。可以使用设备级别Token或签名串防止陌生人向系统伪造老人体征数据触发大量误报告警。8.3 告警阈值和运营规则配置化阈值不能写死在代码里。心率上下限、血氧下限、长时不动时间、设备离线时间都要做成配置。通过管理后台配置时要支持按健康等级差异化设置。例如普通健康老人心率上限是120高风险老人是110。配置修改后要实时生效可以通过Redis发布订阅推送配置变更消息。规则配置还需要版本化管理。如果某次修改导致误报率上升可以快速回滚到上一个版本。生产环境建议把配置变更记录到操作日志表中方便审计。8.4 日志、监控和告警链路的可观测性日志统一采用JSON格式输出到ELK或Loki关键字包括deviceCode、elderId、alertId便于链路追踪。实时看板要监控设备在线数、今日告警数、通知成功率、接口耗时等指标。当短信失败率超过5%时需要及时触发运维告警。可以使用 Spring Boot Actuator 暴露健康检查接口配合 Prometheus 采集指标。Grafana 看板至少展示四个核心面板设备在线趋势、告警类型分布、工单处理时效、通知渠道成功率。有了这些数据运营人员才能判断阈值是否需要调整、哪些网格告警最多、哪个设备型号故障概率高。8.5 上线前检查清单检查项检查内容完成标准数据库表结构脚本、索引、分表策略已执行索引验证通过中间件MySQL、Redis、MQTT、MQ 高可用生产环境集群部署完成安全接口鉴权、设备鉴权、数据权限越权访问测试通过告警去重Redis去重键检查消息幂等模拟高频上报无重复告警离线检测定时任务分布式锁单点故障不影响任务执行通知渠道短信、语音、App推送测试各渠道均收到测试消息监控日志采集、指标看板、失败告警Grafana 面板可见回滚配置回滚、服务回滚演练通过8.6 扩展方向第一版跑通之后可以继续在三个方向扩展。第一个方向是健康趋势分析基于历史体征数据生成周报、月报识别血压、血糖趋势变化第二个方向是室内定位和活动轨迹利用社区网关判断老人是否长时间未离开房间第三个方向是引入算法模型对跌倒姿态、心率变异性做更精细的预测分析。数据积累越久监护系统越能从被动告警走向主动健康管理。从工程角度看智慧社区老年人安全监护系统并不是一个高不可攀的平台型项目。先用 Spring Boot 把设备接入、告警生成、通知和工单闭环跑通再逐步补充数据分析、算法判断和多端协同就能形成一个真正可用的社区安全基础设施。对开发者来说这个项目适合用来练习物联网接入、异步消息、Redis 去重、权限设计和生产部署是一套能完整覆盖后端常见问题的成长型项目。