You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
7.5 KiB
7.5 KiB
全局报警 SSE 推送 — 后端实现文档
1. SSE 端点
GET /admin-api/warehousealarm/alarm-records/subscribe
2. 数据传输格式
SSE data: 行内容为 JSON,支持三种消息类型:
2.1 新增报警 (type: "alarm")
{
"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")
{
"type": "clear",
"alarm_id": "ALM20260708143000123"
}
前端收到后,会删除 alarm_id 匹配的报警条目。
2.3 清空全部报警 (type: "clear_all")
{
"type": "clear_all"
}
前端收到后,会清空当前面板中所有报警。
3. Spring Boot 实现示例
3.1 SseEmitter + 线程池推模式
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<SseEmitter> 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<String, Object> 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 使用示例(报警产生时调用)
@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 字段可能存的是如下格式:
[
{
"sensorCode": "st_001",
"sensorName": "温湿度传感器",
"metrics": [
{ "name": "温度", "value": 38.5, "threshold": 30, "unit": "°C" },
{ "name": "湿度", "value": 65.2, "threshold": 80, "unit": "%" }
]
}
]
构建 sensor_data 的代码:
private List<Map<String, Object>> buildSensorData(String detailJson) {
try {
JSONArray arr = JSON.parseArray(detailJson);
List<Map<String, Object>> result = new ArrayList<>();
for (int i = 0; i < arr.size(); i++) {
JSONObject obj = arr.getJSONObject(i);
Map<String, Object> sensor = new HashMap<>();
sensor.put("sensor_code", obj.getString("sensorCode"));
sensor.put("sensor_name", obj.getString("sensorName"));
JSONArray metrics = obj.getJSONArray("metrics");
List<Map<String, Object>> 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 存储客户端,保证线程安全 |