Jelajahi Sumber

治疗监控

G 10 jam lalu
induk
melakukan
69e56eb355
13 mengubah file dengan 690 tambahan dan 1 penghapusan
  1. TEMPAT SAMPAH
      logs/2026-01/hdis-2026-01-27-1.log.gz
  2. 34 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/controller/DeviceSseController.java
  3. 92 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/controller/SseTestController.java
  4. 25 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/dto/DeviceRealTimePushDTO.java
  5. 54 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/dto/DeviceStatusPushDTO.java
  6. 47 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/dto/PatientMonitorQueryDTO.java
  7. 43 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/dto/TherapyDurationPushDTO.java
  8. 2 1
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/enums/TherapyStatusEnum.java
  9. 43 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/service/DeviceEventService.java
  10. 35 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/service/DeviceSseService.java
  11. 96 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/service/impl/DeviceEventServiceImpl.java
  12. 143 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/service/impl/DeviceSseServiceImpl.java
  13. 76 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/vo/PatientMonitorCardVO.java

TEMPAT SAMPAH
logs/2026-01/hdis-2026-01-27-1.log.gz


+ 34 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/controller/DeviceSseController.java

@@ -0,0 +1,34 @@
+package cn.tr.module.phototherapy.common.controller;
+
+import cn.tr.module.phototherapy.common.service.DeviceSseService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+/**
+ * @ClassName DeviceSseController
+ * @Description 设备SSE控制器
+ * @Date 2026/1/28 9:30
+ * @Version 1.0.0
+ */
+@RestController
+@RequestMapping("/sse")
+public class DeviceSseController {
+
+    @Autowired
+    private DeviceSseService deviceSseService;
+
+    /**
+     * 前端建立SSE连接的接口
+     * @param patientUniqueId 患者唯一标识
+     * @return SseEmitter
+     */
+    @GetMapping("/connect/{patientUniqueId}")
+    public SseEmitter connect(@PathVariable String patientUniqueId) {
+        return deviceSseService.createConnection(patientUniqueId);
+    }
+}
+

+ 92 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/controller/SseTestController.java

@@ -0,0 +1,92 @@
+package cn.tr.module.phototherapy.common.controller; // 替换成你项目的实际包名
+
+import cn.tr.module.phototherapy.common.dto.DeviceStatusPushDTO;
+import cn.tr.module.phototherapy.common.dto.TherapyDurationPushDTO;
+import cn.tr.module.phototherapy.common.service.DeviceSseService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * SSE测试专用Controller
+ * 作用:模拟业务触发,推送测试数据到SSE连接(供APIfox测试)
+ */
+@Slf4j
+@RestController
+@RequestMapping("/test/sse") // 测试接口前缀,避免和业务接口冲突
+public class SseTestController {
+
+    // 注入你已有的SSE服务(核心,复用现有逻辑)
+    @Autowired
+    private DeviceSseService deviceSseService;
+
+    /**
+     * 测试1:推送【设备开机】数据到SSE
+     * 调用路径:GET http://localhost:8080/test/sse/push/device/startup
+     */
+    @GetMapping("/push/device/startup")
+    public String pushDeviceStartup() {
+        // 1. 构建测试用的设备状态DTO(和你业务逻辑的字段一致)
+        DeviceStatusPushDTO testDto = new DeviceStatusPushDTO();
+        testDto.setPatientUniqueId("P001");       // 测试患者ID
+        testDto.setDeviceId("D001");              // 测试设备ID
+        testDto.setDeviceStatus(0);               // 0=开机(和你代码逻辑一致)
+        testDto.setDeviceAlarm(0);                // 0=无报警
+        testDto.setDeviceRunTime(System.currentTimeMillis()); // 开机时间戳
+        testDto.setEventTime(System.currentTimeMillis());      // 事件时间戳
+
+        // 2. 调用SSE服务推送(同时推给单个患者+全局连接)
+        deviceSseService.pushDeviceStatus("P001", testDto);
+
+        log.info("已推送【设备开机】测试数据到SSE,患者ID:P001");
+        return "✅ 设备开机测试数据推送成功!";
+    }
+
+    /**
+     * 测试2:推送【治疗开始】数据到SSE
+     * 调用路径:GET http://localhost:8080/test/sse/push/therapy/start
+     */
+    @GetMapping("/push/therapy/start")
+    public String pushTherapyStart() {
+        // 1. 构建测试用的治疗时长DTO
+        TherapyDurationPushDTO testDto = new TherapyDurationPushDTO();
+        testDto.setPatientUniqueId("P001");       // 测试患者ID
+        testDto.setTherapyStatus(0);              // 0=治疗中(和你代码逻辑一致)
+        testDto.setEventTime(System.currentTimeMillis());      // 事件时间戳
+
+        // 2. 调用SSE服务推送
+        deviceSseService.pushTherapyDuration("P001", testDto);
+
+        log.info("已推送【治疗开始】测试数据到SSE,患者ID:P001");
+        return "✅ 治疗开始测试数据推送成功!";
+    }
+
+    /**
+     * 测试3:推送【脱帽报警+治疗结束】数据到SSE
+     * 调用路径:GET http://localhost:8080/test/sse/push/device/alarm
+     */
+    @GetMapping("/push/device/alarm")
+    public String pushDeviceAlarm() {
+        long currentTime = System.currentTimeMillis();
+
+        // 第一步:推送设备报警状态
+        DeviceStatusPushDTO alarmDto = new DeviceStatusPushDTO();
+        alarmDto.setPatientUniqueId("P001");
+        alarmDto.setDeviceAlarm(1);               // 1=脱帽报警
+        alarmDto.setEventTime(currentTime);
+        deviceSseService.pushDeviceStatus("P001", alarmDto);
+
+        // 第二步:推送治疗结束状态
+        TherapyDurationPushDTO endDto = new TherapyDurationPushDTO();
+        endDto.setPatientUniqueId("P001");
+        endDto.setTherapyStatus(1);               // 1=治疗结束
+        endDto.setTherapyDuration(30);            // 治疗时长30分钟
+        endDto.setEventTime(currentTime);
+        deviceSseService.pushTherapyDuration("P001", endDto);
+
+        log.info("已推送【脱帽报警+治疗结束】测试数据到SSE,患者ID:P001");
+        return "✅ 脱帽报警+治疗结束测试数据推送成功!";
+    }
+}

+ 25 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/dto/DeviceRealTimePushDTO.java

@@ -0,0 +1,25 @@
+package cn.tr.module.phototherapy.common.dto;
+
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 设备实时数据推送通用DTO(兼容设备状态/治疗时长)
+ */
+@Data
+public class DeviceRealTimePushDTO implements Serializable {
+    // 基础通用字段(必传)
+    private String patientUniqueId; // 患者ID(必传)
+    private Long eventTime; // 事件发生时间戳(必传)
+    private String eventType; // 事件类型:DEVICE_STATUS / THERAPY_DURATION(必传)
+
+    // 设备状态相关字段(仅DEVICE_STATUS事件传)
+    private String deviceId; // 设备ID
+    private Integer deviceStatus; // 设备状态:0-开机 1-关机
+    private Integer deviceAlarm; // 报警状态:0-无 1-脱帽
+    private Long deviceRunTime; // 设备开机时间戳
+
+    // 治疗时长相关字段(仅THERAPY_DURATION事件传)
+    private Integer therapyDuration; // 本次治疗时长(分钟)
+    private Integer therapyStatus; // 治疗状态:1-进行中 2-结束
+}

+ 54 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/dto/DeviceStatusPushDTO.java

@@ -0,0 +1,54 @@
+package cn.tr.module.phototherapy.common.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @ClassName DeviceStatusPushDTO
+ * @Description   设备状态推送DTO
+ *                WebSocket推送设备状态变化(开机/离线/报警)时使用
+ * @Date 2026/1/26 15:54
+ * @Version 1.0.0
+ */
+@Data
+public class DeviceStatusPushDTO implements Serializable {
+
+    /**
+     * 推送类型(固定值:device_status)
+     */
+    private String type = "device_status";
+
+    /**
+     * 设备唯一编码
+     */
+    private String deviceId;
+
+    /**
+     * 患者唯一标识
+     */
+    private String patientUniqueId;
+
+    /**
+     * 设备状态(0=开机,1=离线,2=报警)
+     */
+    private Integer deviceStatus;
+
+    /**
+     * 报警类型
+     */
+    private Integer deviceAlarm;
+
+    /**
+     * 设备开机时间戳(毫秒,仅开机状态返回,用于前端计算运行时间)
+     */
+    private Long deviceRunTime;
+
+
+    /**
+     * 事件发生时间戳
+     */
+    private Long eventTime;
+
+}

+ 47 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/dto/PatientMonitorQueryDTO.java

@@ -0,0 +1,47 @@
+package cn.tr.module.phototherapy.common.dto;
+
+import jakarta.validation.constraints.Pattern;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @ClassName PatientMonitorQueryDTO
+ * @Description   患者治疗监控查询DTO
+ *                分页查询患者卡片列表时的入参,含筛选/排序/分页参数
+ * @Date 2026/1/26 15:51
+ * @Version 1.0.0
+ */
+@Data
+public class PatientMonitorQueryDTO implements Serializable {
+
+    /**
+     * 设备状态筛选(0=正常,1=异常)
+     */
+    @Pattern(regexp = "^[012]$", message = "设备状态只能是0/1/2")
+    private String deviceStatus;
+
+    /**
+     * 治疗状态筛选(0=进行中,1=治疗结束,2=未进行)
+     */
+    @Pattern(regexp = "^[012]$", message = "治疗状态只能是0/1/2")
+    private String therapyStatus;
+
+    /**
+     * 是否仅看报警患者(true/false)
+     */
+    private Boolean onlyAlarm;
+
+    /**
+     * 患者姓名(模糊搜索)
+     */
+    private String patientName;
+
+    /**
+     * 排序类型(0=默认排序,1=年龄降序,2=上次治疗时间升序)
+     */
+    @Pattern(regexp = "^[012]$", message = "排序类型只能是0/1/2")
+    private String sortType = "0"; // 默认排序
+
+
+}

+ 43 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/dto/TherapyDurationPushDTO.java

@@ -0,0 +1,43 @@
+package cn.tr.module.phototherapy.common.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @ClassName TherapyDurationPushDTO
+ * @Description   治疗时长推送DTO
+ *                WebSocket推送治疗时长更新时使用
+ * @Date 2026/1/26 15:56
+ * @Version 1.0.0
+ */
+
+@Data
+public class TherapyDurationPushDTO implements Serializable {
+
+    /**
+     * 推送类型(固定值:therapy_duration)
+     */
+    private String type = "therapy_duration";
+
+    /**
+     * 患者唯一标识
+     */
+    private String patientUniqueId;
+
+    /**
+     * 今日治疗时长(分钟,前端可直接展示)
+     */
+    private Integer therapyDuration;
+
+    /**
+     * 治疗状态(0=进行中,1=治疗结束,2=未进行)
+     */
+    private Integer therapyStatus;
+
+    /**
+     * 事件发生时间戳
+     */
+    private Long eventTime;
+}

+ 2 - 1
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/enums/TherapyStatusEnum.java

@@ -15,7 +15,8 @@ public enum TherapyStatusEnum implements IEnum<Integer> {
 
     IN_PROGRESS(0, "进行中"),
     THERAPY_END(1, "治疗结束"),
-    NOT_STARTED(2, "未进行");
+    NOT_STARTED(2, "未进行"),
+    PAUSED(3, "治疗暂停");
 
     @Getter
     @Schema(description = "治疗状态编码")

+ 43 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/service/DeviceEventService.java

@@ -0,0 +1,43 @@
+package cn.tr.module.phototherapy.common.service;
+
+/**
+ * @ClassName DeviceEventService
+ * @Description 设备事件处理服务接口
+ * @Date 2026/1/28 10:00
+ * @Version 1.0.0
+ */
+public interface DeviceEventService {
+
+    /**
+     * 处理设备开机事件
+     * @param deviceId 设备ID
+     * @param patientId 患者ID
+     */
+    void handleDeviceStartup(String deviceId, String patientId);
+
+    /**
+     * 处理治疗开始事件
+     * @param patientId 患者ID
+     */
+    void handleTherapyStart(String patientId);
+
+    /**
+     * 处理脱离报警(治疗结束)事件
+     * @param patientId 患者ID
+     * @param duration 治疗时长(分钟)
+     */
+    void handleDeviceAlarm(String patientId, Integer duration);
+
+    /**
+     * 处理正常治疗结束事件
+     * @param patientId 患者ID
+     * @param duration 治疗时长(分钟)
+     */
+    void handleTherapyEnd(String patientId, Integer duration);
+
+    /**
+     * 处理设备关机事件
+     * @param patientId 患者ID
+     */
+    void handleDeviceShutdown(String patientId);
+}

+ 35 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/service/DeviceSseService.java

@@ -0,0 +1,35 @@
+package cn.tr.module.phototherapy.common.service;
+
+import cn.tr.module.phototherapy.common.dto.DeviceStatusPushDTO;
+import cn.tr.module.phototherapy.common.dto.TherapyDurationPushDTO;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+/**
+ * @ClassName DeviceSseService
+ * @Description 设备SSE服务接口
+ * @Date 2026/1/28 9:33
+ * @Version 1.0.0
+ */
+public interface DeviceSseService {
+
+    /**
+     * 建立SSE连接
+     * @param patientUniqueId 患者唯一标识
+     * @return SseEmitter SSE连接实例
+     */
+    SseEmitter createConnection(String patientUniqueId);
+
+    /**
+     * 推送设备状态
+     * @param patientUniqueId 患者唯一标识
+     * @param data 设备状态数据
+     */
+    void pushDeviceStatus(String patientUniqueId, DeviceStatusPushDTO data);
+
+    /**
+     * 推送治疗时长
+     * @param patientUniqueId 患者唯一标识
+     * @param data 治疗时长数据
+     */
+    void pushTherapyDuration(String patientUniqueId, TherapyDurationPushDTO data);
+}

+ 96 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/service/impl/DeviceEventServiceImpl.java

@@ -0,0 +1,96 @@
+package cn.tr.module.phototherapy.common.service.impl;
+
+import cn.tr.module.phototherapy.common.dto.DeviceStatusPushDTO;
+import cn.tr.module.phototherapy.common.dto.TherapyDurationPushDTO;
+import cn.tr.module.phototherapy.common.service.DeviceEventService;
+import cn.tr.module.phototherapy.common.service.DeviceSseService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+//TODO 入库操作待完善
+@Slf4j
+@Service
+public class DeviceEventServiceImpl implements DeviceEventService {
+
+    @Autowired
+    private DeviceSseService deviceSseService;
+
+    // 处理设备开机
+    public void handleDeviceStartup(String deviceId, String patientUniqueId) {
+        long currentTime = System.currentTimeMillis();
+
+        log.info("处理设备开机事件,设备ID:{},患者ID:{}", deviceId, patientUniqueId);
+        DeviceStatusPushDTO statusDto = new DeviceStatusPushDTO();
+        statusDto.setDeviceId(deviceId);
+        statusDto.setPatientUniqueId(patientUniqueId);
+        statusDto.setDeviceStatus(0); // 开机
+        statusDto.setDeviceAlarm(0);
+        statusDto.setDeviceRunTime(currentTime);
+        statusDto.setEventTime(currentTime);
+
+        deviceSseService.pushDeviceStatus(patientUniqueId, statusDto);
+
+        TherapyDurationPushDTO durationDto = new TherapyDurationPushDTO();
+        durationDto.setPatientUniqueId(patientUniqueId);
+        durationDto.setTherapyStatus(2);//未进行
+        durationDto.setEventTime(currentTime);
+
+        deviceSseService.pushTherapyDuration(patientUniqueId, durationDto);
+    }
+
+    // 处理治疗开始
+    public void handleTherapyStart(String patientUniqueId) {
+        log.info("处理治疗开始事件,患者ID:{}", patientUniqueId);
+
+        TherapyDurationPushDTO durationDto = new TherapyDurationPushDTO();
+        durationDto.setPatientUniqueId(patientUniqueId);
+        durationDto.setTherapyStatus(0); // 治疗中
+        durationDto.setEventTime(System.currentTimeMillis());
+
+        deviceSseService.pushTherapyDuration(patientUniqueId, durationDto);
+    }
+
+    // 处理脱离报警(治疗结束)
+    public void handleDeviceAlarm(String patientUniqueId, Integer duration) {
+        log.info("处理脱离报警事件,患者ID:{},治疗时长:{}分钟", patientUniqueId, duration);
+        long currentTime = System.currentTimeMillis();
+        // 1. 推送报警状态
+        DeviceStatusPushDTO alarmDto = new DeviceStatusPushDTO();
+        alarmDto.setPatientUniqueId(patientUniqueId);
+        alarmDto.setDeviceAlarm(1); // 脱帽报警
+        alarmDto.setEventTime(currentTime);
+        deviceSseService.pushDeviceStatus(patientUniqueId, alarmDto);
+
+        // 2. 推送治疗时长
+        TherapyDurationPushDTO durationDto = new TherapyDurationPushDTO();
+        durationDto.setPatientUniqueId(patientUniqueId);
+        durationDto.setTherapyDuration(duration);
+        durationDto.setTherapyStatus(3); // 治疗暂停
+        durationDto.setEventTime(currentTime);
+        deviceSseService.pushTherapyDuration(patientUniqueId, durationDto);
+    }
+
+    // 处理正常治疗结束
+    public void handleTherapyEnd(String patientUniqueId, Integer duration) {
+        log.info("处理正常治疗结束事件,患者ID:{},治疗时长:{}分钟", patientUniqueId, duration);
+        TherapyDurationPushDTO durationDto = new TherapyDurationPushDTO();
+        durationDto.setPatientUniqueId(patientUniqueId);
+        durationDto.setTherapyDuration(duration);
+        durationDto.setTherapyStatus(1); // 治疗结束
+        durationDto.setEventTime(System.currentTimeMillis());
+
+        deviceSseService.pushTherapyDuration(patientUniqueId, durationDto);
+    }
+
+    // 处理设备关机
+    public void handleDeviceShutdown(String patientUniqueId) {
+        log.info("处理设备关机事件,患者ID:{}", patientUniqueId);
+        DeviceStatusPushDTO statusDto = new DeviceStatusPushDTO();
+        statusDto.setPatientUniqueId(patientUniqueId);
+        statusDto.setDeviceStatus(1); // 关机
+        statusDto.setEventTime(System.currentTimeMillis());
+
+        deviceSseService.pushDeviceStatus(patientUniqueId, statusDto);
+    }
+}

+ 143 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/service/impl/DeviceSseServiceImpl.java

@@ -0,0 +1,143 @@
+package cn.tr.module.phototherapy.common.service.impl;
+
+import cn.tr.module.phototherapy.common.dto.DeviceStatusPushDTO;
+import cn.tr.module.phototherapy.common.dto.TherapyDurationPushDTO;
+import cn.tr.module.phototherapy.common.service.DeviceSseService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @ClassName DeviceSseServiceImpl
+ * @Description 设备SSE服务实现类
+ * @Date 2026/1/28 9:33
+ * @Version 1.0.0
+ */
+@Slf4j
+@Service
+public class DeviceSseServiceImpl implements DeviceSseService {
+    private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
+    // 心跳线程池
+    private final ScheduledExecutorService heartbeatExecutor = Executors.newScheduledThreadPool(1);
+    // 超时时间:30分钟(避免Long.MAX_VALUE导致内存泄漏)
+    private static final long SSE_TIMEOUT = 30 * 60 * 1000L;
+    // 心跳间隔:40秒(维持连接不被断开)
+    private static final long HEARTBEAT_INTERVAL = 40 * 1000L;
+
+    // 全局连接标识
+    private static final String GLOBAL_ADMIN_ID = "hospital_admin";
+
+    @Override
+    public SseEmitter createConnection(String uniqueId) {
+        // 先移除旧连接(避免重复连接)
+        if (emitters.containsKey(uniqueId)) {
+            SseEmitter oldEmitter = emitters.remove(uniqueId);
+            oldEmitter.complete(); // 关闭旧连接
+        }
+
+        // 创建新的Emitter,设置30分钟超时
+        SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
+        log.info("创建SSE连接,标识:{}", uniqueId);
+
+        // 连接回调:清理资源
+        emitter.onCompletion(() -> {
+            emitters.remove(uniqueId);
+            log.info("SSE连接完成,清理标识{}连接", uniqueId);
+        });
+        emitter.onTimeout(() -> {
+            emitters.remove(uniqueId);
+            log.warn("SSE连接超时,清理标识{}连接", uniqueId);
+        });
+        emitter.onError((ex) -> {
+            emitters.remove(uniqueId);
+            log.error("SSE连接异常,清理标识{}连接", uniqueId, ex);
+        });
+
+        // 启动心跳任务,维持连接
+        startHeartbeat(uniqueId, emitter);
+
+        // 存入连接池
+        emitters.put(uniqueId, emitter);
+
+        return emitter;
+    }
+
+    @Override
+    public void pushDeviceStatus(String patientUniqueId, DeviceStatusPushDTO data) {
+        sendEvent(patientUniqueId, "DEVICE_STATUS_CHANGE", data);
+
+        // 同步推送给全局连接
+        sendGlobalEvent("DEVICE_STATUS_CHANGE", data);
+    }
+
+    @Override
+    public void pushTherapyDuration(String patientUniqueId, TherapyDurationPushDTO data) {
+        sendEvent(patientUniqueId, "THERAPY_DURATION_UPDATE", data);
+
+        // 同步推送给全局连接
+        sendGlobalEvent("THERAPY_DURATION_UPDATE", data);
+    }
+
+    /**
+     * 通用发送方法(抽离重复逻辑)
+     * @param uniqueId 患者ID/全局标识
+     * @param eventName 事件名称
+     * @param data 推送数据
+     * @param <T> 数据类型
+     */
+    private <T> void sendEvent(String uniqueId, String eventName, T data) {
+        SseEmitter emitter = emitters.get(uniqueId);
+        if (emitter == null) {
+            log.warn("标识{}无活跃SSE连接,推送{}失败", uniqueId, eventName);
+            return;
+        }
+
+        try {
+            emitter.send(SseEmitter.event()
+                    .name(eventName)
+                    .data(data));
+            log.info("标识{}推送{}成功,数据:{}", uniqueId, eventName, data);
+        } catch (IOException e) {
+            log.error("标识{}推送{}失败", uniqueId, eventName, e);
+            emitters.remove(uniqueId); // 移除失效连接
+        }
+    }
+
+    /**
+     * 新增:推送给全局管理员连接的通用方法
+     * @param eventName 事件名称
+     * @param data 推送数据
+     * @param <T> 数据类型
+     */
+    private <T> void sendGlobalEvent(String eventName, T data) {
+        // 直接调用通用发送方法,目标标识为全局管理员ID
+        sendEvent(GLOBAL_ADMIN_ID, eventName, data);
+    }
+
+    /**
+     * 心跳任务:每30秒发送空事件维持连接
+     * @param uniqueId 唯一标识(患者ID/全局标识)
+     * @param emitter SSE连接实例
+     */
+    private void startHeartbeat(String uniqueId, SseEmitter emitter) {
+        heartbeatExecutor.scheduleAtFixedRate(() -> {
+            if (!emitters.containsKey(uniqueId)) {
+                return; // 连接已清理,停止心跳
+            }
+            try {
+                // 发送注释类型的心跳(前端不解析,仅维持连接)
+                emitter.send(SseEmitter.event().comment("heartbeat"));
+            } catch (IOException e) {
+                log.error("标识{}心跳发送失败,清理连接", uniqueId, e);
+                emitters.remove(uniqueId);
+            }
+        }, 0, HEARTBEAT_INTERVAL, TimeUnit.MILLISECONDS);
+    }
+}

+ 76 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/vo/PatientMonitorCardVO.java

@@ -0,0 +1,76 @@
+package cn.tr.module.phototherapy.common.vo;
+
+import cn.tr.module.phototherapy.common.enums.DeviceStatusEnum;
+import cn.tr.module.phototherapy.common.enums.IsCurrentBindEnum;
+import cn.tr.module.phototherapy.common.enums.PatientGenderEnum;
+import cn.tr.module.phototherapy.common.enums.TherapyStatusEnum;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @ClassName PatientMonitorVO
+ * @Description 患者治疗监控VO
+ * @Date 2026/1/26 15:15
+ * @Version 1.0.0
+ */
+@Data
+@Schema(description = "患者治疗监控VO")
+@ToString
+public class PatientMonitorCardVO implements Serializable {
+
+    // 患者基础信息
+    @Schema(description = "患者唯一标识")
+    private  String patientUniqueId;
+
+    @Schema(description = "患者姓名")
+    private  String patientName;
+
+    @Schema(description = "患者年龄")
+    private  Integer patientAge;
+
+    @Schema(description = "患者性别")
+    private PatientGenderEnum patientGender;
+
+    @Schema(description = "上次治疗时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
+    private Date lastTreatmentTime;
+
+    @Schema(description = "是否当前绑定")
+    private IsCurrentBindEnum isCurrentBind;
+
+    // 治疗方案信息
+    @Schema(description = "治疗方案阶段")
+    private String phaseType;
+
+    @Schema(description = "治疗方案频率")
+    private Integer phaseFreq;
+
+    @Schema(description = "治疗方案时长(分钟)")
+    private Integer phaseDurationMin;
+
+    //设备相关信息
+    @Schema(description = "设备ID")
+    private String deviceId;
+
+    @Schema(description = "设备状态")
+    private DeviceStatusEnum deviceStatus;
+
+    @Schema(description = "设备开始运行时间")
+    private Date deviceRunTime;
+
+    @Schema(description = "报警类型")
+    private String deviceAlarm;
+
+    //治疗相关信息
+    @Schema(description = "治疗状态")
+    private TherapyStatusEnum therapyStatus;
+
+    @Schema(description = "累计治疗时长")
+    private Integer therapyDuration;
+
+}