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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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