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.
yudao-web/SSE实现文档.md

254 lines
7.5 KiB
Markdown

# 全局报警 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` 存储客户端,保证线程安全 |