|
|
|
|
|
# 全局报警 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<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 使用示例(报警产生时调用)
|
|
|
|
|
|
|
|
|
|
|
|
```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<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` 存储客户端,保证线程安全 |
|