# 全局报警 SSE 推送 — 后端实现文档 ## 1. SSE 端点 ```java GET /admin-api/warehousealarm/alarm-records/subscribe ``` ## 2. 数据传输格式 SSE `data:` 行内容为 JSON,支持三种消息类型: ### 2.1 新增报警 (`type: "alarm"`) ```json { "type": "alarm", "alarm_id": "ALM20260708143000123", "alarm_level": "2", "alarm_level_name": "报警", "alarm_time": "2026-07-08 14:30:00", "description": "仓库#3 温湿度传感器触发高温报警", "cameraId": "2", "pic": "http://localhost:48080/api/file/xxx.jpg", "sensor_data": [ { "sensor_code": "st_001", "sensor_name": "温湿度传感器", "data": [ { "name": "温度", "value": 38.5, "sensor_threshold": 30, "unit": "°C" }, { "name": "湿度", "value": 65.2, "sensor_threshold": 80, "unit": "%" } ] } ] } ``` | 字段 | 类型 | 说明 | |------|------|------| | alarm_id | String | **报警唯一标识,用于去重和清除** | | alarm_level | String | 报警等级:1=警告,2=报警,3=严重报警 | | alarm_level_name | String | 报警等级名称 | | alarm_time | String | 触发时间,格式 `yyyy-MM-dd HH:mm:ss` | | description | String | 报警描述 | | cameraId | String | 关联相机 ID | | pic | String | 报警截图 URL | | sensor_data | Array | 报警设备数组 | | sensor_data[].sensor_code | String | 传感器编码 | | sensor_data[].sensor_name | String | **传感器设备名称** | | sensor_data[].data | Array | 该传感器的监测指标数组 | | sensor_data[].data[].name | String | 指标名称(如"温度""湿度") | | sensor_data[].data[].value | Number | 当前值 | | sensor_data[].data[].sensor_threshold | Number | 报警阈值 | | sensor_data[].data[].unit | String | 单位(如 °C、%、ppm) | > **注意**:`alarm_id` 当新报警与已有报警的 `alarm_id` 相同时,前端会**自动去重**。 ### 2.2 清除指定报警 (`type: "clear"`) ```json { "type": "clear", "alarm_id": "ALM20260708143000123" } ``` 前端收到后,会删除 `alarm_id` 匹配的报警条目。 ### 2.3 清空全部报警 (`type: "clear_all"`) ```json { "type": "clear_all" } ``` 前端收到后,会清空当前面板中所有报警。 ## 3. Spring Boot 实现示例 ### 3.1 SseEmitter + 线程池推模式 ```java import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; @RestController @RequestMapping("/admin-api/warehousealarm/alarm-records") public class AlarmSseController { /** 保存所有在线 SSE 客户端 */ private final CopyOnWriteArrayList emitters = new CopyOnWriteArrayList<>(); /** * 客户端订阅 SSE */ @GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter subscribe() { SseEmitter emitter = new SseEmitter(0L); // 0L 表示永不超时 emitters.add(emitter); emitter.onCompletion(() -> emitters.remove(emitter)); emitter.onTimeout(() -> emitters.remove(emitter)); return emitter; } /** * 推送一条报警(后台触发) */ public void sendAlarm(AlarmDTO alarm) { Map data = Map.of( "type", "alarm", "alarm_id", alarm.getAlarmId(), "alarm_level", alarm.getAlarmLevel(), "alarm_level_name", alarm.getAlarmLevelName(), "alarm_time", alarm.getAlarmTime(), "description", alarm.getDescription(), "cameraId", alarm.getCameraId(), "pic", alarm.getPic(), "sensor_data", alarm.getSensorData() ); broadcast(data); } /** * 推送清除指定报警 */ public void sendClear(String alarmId) { broadcast(Map.of("type", "clear", "alarm_id", alarmId)); } /** * 推送清空全部 */ public void sendClearAll() { broadcast(Map.of("type", "clear_all")); } /** * 向所有客户端广播消息 */ private void broadcast(Object data) { for (SseEmitter emitter : emitters) { try { emitter.send(SseEmitter.event().data(data)); } catch (IOException e) { emitters.remove(emitter); } } } } ``` ### 3.2 使用示例(报警产生时调用) ```java @Service public class AlarmService { @Autowired private AlarmSseController alarmSseController; /** * 在报警创建逻辑末尾调用 */ public void triggerAlarm(AlarmRecord record) { // ... 保存数据库等业务逻辑 ... // 构建报警 DTO AlarmDTO dto = AlarmDTO.builder() .alarmId(record.getId().toString()) .alarmLevel(String.valueOf(record.getSeverity())) .alarmLevelName(getLevelName(record.getSeverity())) .alarmTime(DateUtil.format(record.getTriggerTime(), "yyyy-MM-dd HH:mm:ss")) .description(record.getDescription()) .cameraId(String.valueOf(record.getDeviceId())) .pic(record.getSnapshotUrl()) .sensorData(buildSensorData(record.getDetailJson())) .build(); // 推送 SSE alarmSseController.sendAlarm(dto); } } ``` ## 4. 传感器数据构建示例 `AlarmRecord` 中 `detailJson` 字段可能存的是如下格式: ```json [ { "sensorCode": "st_001", "sensorName": "温湿度传感器", "metrics": [ { "name": "温度", "value": 38.5, "threshold": 30, "unit": "°C" }, { "name": "湿度", "value": 65.2, "threshold": 80, "unit": "%" } ] } ] ``` 构建 `sensor_data` 的代码: ```java private List> buildSensorData(String detailJson) { try { JSONArray arr = JSON.parseArray(detailJson); List> result = new ArrayList<>(); for (int i = 0; i < arr.size(); i++) { JSONObject obj = arr.getJSONObject(i); Map sensor = new HashMap<>(); sensor.put("sensor_code", obj.getString("sensorCode")); sensor.put("sensor_name", obj.getString("sensorName")); JSONArray metrics = obj.getJSONArray("metrics"); List> dataList = new ArrayList<>(); for (int j = 0; j < metrics.size(); j++) { JSONObject m = metrics.getJSONObject(j); dataList.add(Map.of( "name", m.getString("name"), "value", m.getDouble("value"), "sensor_threshold", m.getDouble("threshold"), "unit", m.getString("unit") )); } sensor.put("data", dataList); result.add(sensor); } return result; } catch (Exception e) { return Collections.emptyList(); } } ``` ## 5. 注意事项 | 事项 | 说明 | |------|------| | Nginx 代理 | 如果使用了 Nginx,需配置 `proxy_buffering off;` 否则 SSE 会被缓冲 | | 超时 | `SseEmitter` 构造传入 `0L` 表示永不超时,或根据实际需求设置超时时间 | | 断线重连 | 前端在断开后会自动重连(10秒间隔),无需额外处理 | | 去重机制 | 前端以 `alarm_id` 作为唯一标识自动去重,**重发相同 `alarm_id` 不会重复显示** | | 并发 | 使用 `CopyOnWriteArrayList` 存储客户端,保证线程安全 |