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

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