Explorar el Código

feat: device monitor Stream

lihao16 hace 7 meses
padre
commit
a7a4b919b9
Se han modificado 19 ficheros con 394 adiciones y 93 borrados
  1. 12 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/controller/SmsbDeviceErrorRecordController.java
  2. 26 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/DeviceAlarmCountVo.java
  3. 6 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/SmsbDeviceErrorRecordVo.java
  4. 11 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/mapper/SmsbDeviceErrorRecordMapper.java
  5. 9 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbDeviceErrorRecordService.java
  6. 24 0
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDeviceErrorRecordServiceImpl.java
  7. 2 1
      smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDeviceServiceImpl.java
  8. 10 1
      smsb-modules/smsb-device/src/main/resources/mapper/device/SmsbDeviceErrorRecordMapper.xml
  9. 31 1
      smsb-modules/smsb-netty/src/main/java/com/inspur/netty/client/NettyClientController.java
  10. 15 13
      smsb-modules/smsb-netty/src/main/java/com/inspur/netty/controller/DeviceController.java
  11. 1 1
      smsb-modules/smsb-netty/src/main/java/com/inspur/netty/controller/StreamController.java
  12. 9 11
      smsb-modules/smsb-netty/src/main/java/com/inspur/netty/message/push/PushMessageType.java
  13. 4 1
      smsb-modules/smsb-netty/src/main/java/com/inspur/netty/stream/callback/MKNoReaderCallBack.java
  14. 12 10
      smsb-modules/smsb-netty/src/main/java/com/inspur/netty/stream/callback/MKPublishCallBack.java
  15. 18 12
      smsb-modules/smsb-netty/src/main/java/com/inspur/netty/stream/service/impl/StreamServiceImpl.java
  16. 8 0
      smsb-plus-ui/src/api/smsb/device/errorRecord.ts
  17. 6 0
      smsb-plus-ui/src/api/smsb/device/errorRecord_type.ts
  18. 65 12
      smsb-plus-ui/src/views/smsb/dashboard/device.vue
  19. 125 30
      smsb-plus-ui/src/views/smsb/device/index.vue

+ 12 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/controller/SmsbDeviceErrorRecordController.java

@@ -2,6 +2,7 @@ package com.inspur.device.controller;
 
 import java.util.List;
 
+import com.inspur.device.domain.vo.DeviceAlarmCountVo;
 import lombok.RequiredArgsConstructor;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.constraints.*;
@@ -96,4 +97,15 @@ public class SmsbDeviceErrorRecordController extends BaseController {
                           @PathVariable Long[] ids) {
         return toAjax(smsbDeviceErrorRecordService.deleteWithValidByIds(List.of(ids), true));
     }
+
+    /**
+     * 设备统计-告警类型统计
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    @GetMapping("/count/byType")
+    public R<DeviceAlarmCountVo> countByType(@RequestParam String startTime, @RequestParam String endTime) {
+        return R.ok(smsbDeviceErrorRecordService.countByType(startTime, endTime));
+    }
 }

+ 26 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/DeviceAlarmCountVo.java

@@ -0,0 +1,26 @@
+package com.inspur.device.domain.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 设备统计 - 告警问题统计
+ * @author lihao16
+ */
+@Data
+public class DeviceAlarmCountVo {
+
+    private List<String> alarmType;
+
+    private List<Integer> alarmCount;
+
+    private Integer onlineNum;
+
+    private Integer offlineNum;
+
+    private Integer bdNum;
+
+    private Integer thanNum;
+
+}

+ 6 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/domain/vo/SmsbDeviceErrorRecordVo.java

@@ -10,6 +10,7 @@ import org.dromara.common.excel.convert.ExcelDictConvert;
 
 import java.io.Serial;
 import java.io.Serializable;
+import java.util.Date;
 
 
 /**
@@ -58,5 +59,10 @@ public class SmsbDeviceErrorRecordVo implements Serializable {
     @ExcelDictFormat(dictType = "smsb_device_error_type")
     private Long errorType;
 
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
 
 }

+ 11 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/mapper/SmsbDeviceErrorRecordMapper.java

@@ -1,9 +1,13 @@
 package com.inspur.device.mapper;
 
 import com.inspur.device.domain.SmsbDeviceErrorRecord;
+import com.inspur.device.domain.vo.DeviceAlarmCountVo;
 import com.inspur.device.domain.vo.SmsbDeviceErrorRecordVo;
+import org.apache.ibatis.annotations.Param;
 import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
 
+import java.util.List;
+
 /**
  * 设备告警记录Mapper接口
  *
@@ -12,4 +16,11 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
  */
 public interface SmsbDeviceErrorRecordMapper extends BaseMapperPlus<SmsbDeviceErrorRecord, SmsbDeviceErrorRecordVo> {
 
+    /**
+     * 设备统计-告警类型统计
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    DeviceAlarmCountVo selectCountByType(@Param("startTime") String startTime, @Param("endTime") String endTime);
 }

+ 9 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/ISmsbDeviceErrorRecordService.java

@@ -1,6 +1,7 @@
 package com.inspur.device.service;
 
 import com.inspur.device.domain.bo.SmsbDeviceErrorRecordBo;
+import com.inspur.device.domain.vo.DeviceAlarmCountVo;
 import com.inspur.device.domain.vo.SmsbDeviceErrorRecordVo;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -65,4 +66,12 @@ public interface ISmsbDeviceErrorRecordService {
      * @return 是否删除成功
      */
     Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    /**
+     * 设备统计-告警类型统计
+     * @param startTime
+     * @param endTime
+     * @return
+     */
+    DeviceAlarmCountVo countByType(String startTime, String endTime);
 }

+ 24 - 0
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDeviceErrorRecordServiceImpl.java

@@ -5,16 +5,20 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.inspur.device.domain.SmsbDeviceErrorRecord;
 import com.inspur.device.domain.bo.SmsbDeviceErrorRecordBo;
+import com.inspur.device.domain.vo.DeviceAlarmCountVo;
 import com.inspur.device.domain.vo.SmsbDeviceErrorRecordVo;
 import com.inspur.device.mapper.SmsbDeviceErrorRecordMapper;
 import com.inspur.device.service.ISmsbDeviceErrorRecordService;
 import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.service.DictService;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -31,6 +35,11 @@ public class SmsbDeviceErrorRecordServiceImpl implements ISmsbDeviceErrorRecordS
 
     private final SmsbDeviceErrorRecordMapper baseMapper;
 
+    @Autowired
+    private DictService dictService;
+
+    private static final String ALARM_DICT_TYPE = "smsb_device_error_type";
+
     /**
      * 查询设备告警记录
      *
@@ -129,4 +138,19 @@ public class SmsbDeviceErrorRecordServiceImpl implements ISmsbDeviceErrorRecordS
         }
         return baseMapper.deleteByIds(ids) > 0;
     }
+
+    @Override
+    public DeviceAlarmCountVo countByType(String startTime, String endTime) {
+        DeviceAlarmCountVo dbResult = baseMapper.selectCountByType(startTime, endTime);
+        Map<String, String> dictAlarm = dictService.getAllDictByDictType(ALARM_DICT_TYPE);
+        List<String> alarmTypes = new ArrayList<>(dictAlarm.values());
+        dbResult.setAlarmType(alarmTypes);
+        List<Integer> alarmCounts = new ArrayList<>();
+        alarmCounts.add(dbResult.getOnlineNum());
+        alarmCounts.add(dbResult.getOfflineNum());
+        alarmCounts.add(dbResult.getBdNum());
+        alarmCounts.add(dbResult.getThanNum());
+        dbResult.setAlarmCount(alarmCounts);
+        return dbResult;
+    }
 }

+ 2 - 1
smsb-modules/smsb-device/src/main/java/com/inspur/device/service/impl/SmsbDeviceServiceImpl.java

@@ -255,7 +255,8 @@ public class SmsbDeviceServiceImpl implements ISmsbDeviceService {
     public SmsbDeviceVo updateDeviceStatus(SmsbDeviceVo smsbDeviceVo) {
         SmsbDeviceVo updateResult = getDeviceByIdentifier(smsbDeviceVo.getIdentifier());
         updateResult.setOnlineStatus(smsbDeviceVo.getOnlineStatus());
-
+        updateResult.setLastOnline(smsbDeviceVo.getLastOnline());
+        updateResult.setOfflineTime(smsbDeviceVo.getOfflineTime());
         baseMapper.update(null, new LambdaUpdateWrapper<SmsbDevice>()
             .eq(SmsbDevice::getIdentifier, updateResult.getIdentifier())
             .set(SmsbDevice::getOnlineStatus, updateResult.getOnlineStatus())

+ 10 - 1
smsb-modules/smsb-device/src/main/resources/mapper/device/SmsbDeviceErrorRecordMapper.xml

@@ -3,5 +3,14 @@
     PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.inspur.device.mapper.SmsbDeviceErrorRecordMapper">
-
+    <select id="selectCountByType" resultType="com.inspur.device.domain.vo.DeviceAlarmCountVo">
+        SELECT
+            IFNULL( SUM( CASE WHEN error_type = 1 THEN 1 ELSE 0 END ), 0 ) AS onlineNum,
+            IFNULL( SUM( CASE WHEN error_type = 2 THEN 1 ELSE 0 END ), 0 ) AS offlineNum,
+            IFNULL( SUM( CASE WHEN error_type = 3 THEN 1 ELSE 0 END ), 0 ) AS bdNum,
+            IFNULL( SUM( CASE WHEN error_type = 4 THEN 1 ELSE 0 END ), 0 ) AS thanNum
+        FROM
+            smsb_device_error_record
+        where create_time between #{startTime} and #{endTime}
+    </select>
 </mapper>

+ 31 - 1
smsb-modules/smsb-netty/src/main/java/com/inspur/netty/client/NettyClientController.java

@@ -4,9 +4,14 @@ import cn.dev33.satoken.annotation.SaIgnore;
 import com.inspur.netty.message.push.PushMessage;
 import com.inspur.netty.message.push.PushMessageType;
 import com.inspur.netty.util.PushMsgUtil;
+import org.dromara.common.core.config.ThreadPoolConfig;
 import org.dromara.common.core.domain.R;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.concurrent.ScheduledExecutorService;
+
 
 /**
  * netty client test controller
@@ -16,6 +21,12 @@ import org.springframework.web.bind.annotation.*;
 @RequestMapping("/netty")
 public class NettyClientController {
 
+    @Autowired
+    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
+
+    @Autowired
+    private ScheduledExecutorService scheduledExecutorService;
+
     private static ReusableNettyClient nettyClient = new ReusableNettyClient("127.0.0.1", 8900);
 
     @SaIgnore
@@ -29,11 +40,30 @@ public class NettyClientController {
     @SaIgnore
     @GetMapping("/sendMac/{mac}")
     public R<Void> sendMac(@PathVariable String mac)  {
-        String msg = mac + "/init";
+        String msg = mac + "/init####";
         nettyClient.sendMessage(msg);
+        pushHeartMessage(mac);
         return R.ok();
     }
 
+    private void pushHeartMessage(String mac) {
+        Runnable task = () -> {
+            // 任务逻辑
+            while (true) {
+                String heartbeat = mac + "/heartbeat####";
+                nettyClient.sendMessage(heartbeat);
+                try {
+                    Thread.sleep(3000);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        };
+
+        // 提交Runnable任务到线程池
+        threadPoolTaskExecutor.submit(task);
+    }
+
     @SaIgnore
     @GetMapping("/stop")
     public R<Void> stop() {

+ 15 - 13
smsb-modules/smsb-netty/src/main/java/com/inspur/netty/controller/DeviceController.java

@@ -2,8 +2,10 @@ package com.inspur.netty.controller;
 
 import com.inspur.device.domain.vo.SmsbDeviceVo;
 import com.inspur.device.mapper.SmsbDeviceMapper;
+import com.inspur.device.service.ISmsbDeviceService;
 import com.inspur.netty.message.push.PushMessage;
 import com.inspur.netty.message.push.PushMessageType;
+import com.inspur.netty.util.NettyConstants;
 import com.inspur.netty.util.PushMsgUtil;
 import org.dromara.common.core.domain.R;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -22,7 +24,7 @@ import org.springframework.web.bind.annotation.RestController;
 public class DeviceController {
 
     @Autowired
-    private SmsbDeviceMapper deviceMapper;
+    private ISmsbDeviceService smsbDeviceService;
 
     /**
      * 重启设备
@@ -33,13 +35,13 @@ public class DeviceController {
     @GetMapping("/reboot/{deviceId}")
     public R<String> reboot(@PathVariable Long deviceId) {
         // 查询设备信息
-        SmsbDeviceVo deviceVo = deviceMapper.selectVoById(deviceId);
+        SmsbDeviceVo deviceVo = smsbDeviceService.getDeviceCacheById(deviceId);
         if (deviceVo == null) {
             return R.fail("设备不存在");
         }
         // 组装重启命令
-        PushMessage pushMessage = new PushMessage(PushMessageType.CONTROL_REBOOT.getValue(), null);
-        boolean isSend = PushMsgUtil.send(deviceVo.getMac(), pushMessage);
+        String rebootCmd = deviceVo.getIdentifier() + PushMessageType.CONTROL_REBOOT.getValue() + NettyConstants.DATA_PACK_SEPARATOR;
+        boolean isSend = PushMsgUtil.sendV2(deviceVo.getIdentifier(), rebootCmd);
         return isSend ? R.ok() : R.fail("发送失败,设备长连接已断开");
     }
 
@@ -52,13 +54,13 @@ public class DeviceController {
     @GetMapping("/start/{deviceId}")
     public R<String> start(@PathVariable Long deviceId) {
         // 查询设备信息
-        SmsbDeviceVo deviceVo = deviceMapper.selectVoById(deviceId);
+        SmsbDeviceVo deviceVo = smsbDeviceService.getDeviceCacheById(deviceId);
         if (deviceVo == null) {
             return R.fail("设备不存在");
         }
         // 组装重启命令
-        PushMessage pushMessage = new PushMessage(PushMessageType.CONTROL_START.getValue(), null);
-        boolean isSend = PushMsgUtil.send(deviceVo.getMac(), pushMessage);
+        String startCmd = deviceVo.getIdentifier() + PushMessageType.CONTROL_START.getValue() + NettyConstants.DATA_PACK_SEPARATOR;
+        boolean isSend = PushMsgUtil.sendV2(deviceVo.getIdentifier(), startCmd);
         return isSend ? R.ok() : R.fail("发送失败,设备长连接已断开");
     }
 
@@ -71,13 +73,13 @@ public class DeviceController {
     @GetMapping("/shutdown/{deviceId}")
     public R<String> shutdown(@PathVariable Long deviceId) {
         // 查询设备信息
-        SmsbDeviceVo deviceVo = deviceMapper.selectVoById(deviceId);
+        SmsbDeviceVo deviceVo = smsbDeviceService.getDeviceCacheById(deviceId);
         if (deviceVo == null) {
             return R.fail("设备不存在");
         }
         // 组装重启命令
-        PushMessage pushMessage = new PushMessage(PushMessageType.CONTROL_SHUTDOWN.getValue(), null);
-        boolean isSend = PushMsgUtil.send(deviceVo.getMac(), pushMessage);
+        String shutdownCmd = deviceVo.getIdentifier() + PushMessageType.CONTROL_SHUTDOWN.getValue() + NettyConstants.DATA_PACK_SEPARATOR;
+        boolean isSend = PushMsgUtil.sendV2(deviceVo.getIdentifier(), shutdownCmd);
         return isSend ? R.ok() : R.fail("发送失败,设备长连接已断开");
     }
 
@@ -90,13 +92,13 @@ public class DeviceController {
     @GetMapping("/standby/{deviceId}")
     public R<String> standby(@PathVariable Long deviceId) {
         // 查询设备信息
-        SmsbDeviceVo deviceVo = deviceMapper.selectVoById(deviceId);
+        SmsbDeviceVo deviceVo = smsbDeviceService.getDeviceCacheById(deviceId);
         if (deviceVo == null) {
             return R.fail("设备不存在");
         }
         // 组装重启命令
-        PushMessage pushMessage = new PushMessage(PushMessageType.CONTROL_STANDBY.getValue(), null);
-        boolean isSend = PushMsgUtil.send(deviceVo.getMac(), pushMessage);
+        String standbyCmd = deviceVo.getIdentifier() + PushMessageType.CONTROL_STANDBY.getValue() + NettyConstants.DATA_PACK_SEPARATOR;
+        boolean isSend = PushMsgUtil.sendV2(deviceVo.getIdentifier(), standbyCmd);
         return isSend ? R.ok() : R.fail("发送失败,设备长连接已断开");
     }
 

+ 1 - 1
smsb-modules/smsb-netty/src/main/java/com/inspur/netty/controller/StreamController.java

@@ -36,7 +36,7 @@ public class StreamController {
      * 前端正常关闭观看
      * @param deviceId 主键
      */
-    @GetMapping("/stopView/{deviceId}")
+    @GetMapping("/stop/{deviceId}")
     public R<Void> stopView(@NotNull(message = "设备ID不能为空") @PathVariable Long deviceId) {
         streamService.stopView(deviceId);
         return R.ok();

+ 9 - 11
smsb-modules/smsb-netty/src/main/java/com/inspur/netty/message/push/PushMessageType.java

@@ -29,32 +29,32 @@ public enum PushMessageType {
     /**
      * 设备重启
      */
-    CONTROL_REBOOT("1001"),
+    CONTROL_REBOOT("/reboot"),
 
     /**
      * 设备开机
      */
-    CONTROL_START("1002"),
+    CONTROL_START("/power/on"),
 
     /**
      * 设备关机
      */
-    CONTROL_SHUTDOWN("1003"),
+    CONTROL_SHUTDOWN("/power/off"),
 
     /**
      * 设备待机
      */
-    CONTROL_STANDBY("1004"),
+    CONTROL_STANDBY("/standby"),
 
     /**
      * 调节设备音量
      */
-    CONTROL_VOLUME("1005"),
+    CONTROL_VOLUME("/volume"),
 
     /**
      * 调整设备亮度
      */
-    CONTROL_BRIGHTNESS("1006"),
+    CONTROL_BRIGHTNESS("/brightness"),
 
     /**
      * 开始推流
@@ -64,17 +64,15 @@ public enum PushMessageType {
     /**
      * 停止推流
      */
-    CONTROL_STOP_STREAM("1008");
+    CONTROL_STOP_STREAM("/stream/stop");
 
     private String value;
 
-    private PushMessageType(String value)
-    {
+    private PushMessageType(String value) {
         this.value = value;
     }
 
-    public String getValue()
-    {
+    public String getValue() {
         return value;
     }
 

+ 4 - 1
smsb-modules/smsb-netty/src/main/java/com/inspur/netty/stream/callback/MKNoReaderCallBack.java

@@ -4,6 +4,7 @@ import com.aizuda.zlm4j.callback.IMKNoReaderCallBack;
 import com.aizuda.zlm4j.structure.MK_MEDIA_SOURCE;
 import com.sun.jna.CallbackThreadInitializer;
 import com.sun.jna.Native;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
 import static com.inspur.netty.stream.Zlm4jServer.ZLM_API;
@@ -15,6 +16,7 @@ import static com.inspur.netty.stream.Zlm4jServer.ZLM_API;
  * @since 2025/01/23
  **/
 @Component
+@Slf4j
 public class MKNoReaderCallBack implements IMKNoReaderCallBack {
 
     public MKNoReaderCallBack() {
@@ -32,8 +34,9 @@ public class MKNoReaderCallBack implements IMKNoReaderCallBack {
         String stream = ZLM_API.mk_media_source_get_stream(sender);
         String app = ZLM_API.mk_media_source_get_app(sender);
         String schema = ZLM_API.mk_media_source_get_schema(sender);
+        log.info("stream :" + stream + "no reader call back,app:" + app + ",schema:" + schema);
         //无人观看时候可以调用下面的实现关流 不调用就代表不关流 需要配置protocol.auto_close 为 0 这里才会有回调
         ZLM_API.mk_media_source_close(sender,0);
-
+        log.info("close success stream : " + stream);
     }
 }

+ 12 - 10
smsb-modules/smsb-netty/src/main/java/com/inspur/netty/stream/callback/MKPublishCallBack.java

@@ -4,10 +4,8 @@ import com.aizuda.zlm4j.callback.IMKPublishCallBack;
 import com.aizuda.zlm4j.structure.MK_MEDIA_INFO;
 import com.aizuda.zlm4j.structure.MK_PUBLISH_AUTH_INVOKER;
 import com.aizuda.zlm4j.structure.MK_SOCK_INFO;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.inspur.device.domain.SmsbDevice;
 import com.inspur.device.domain.vo.SmsbDeviceVo;
-import com.inspur.device.mapper.SmsbDeviceMapper;
+import com.inspur.device.service.ISmsbDeviceService;
 import com.sun.jna.CallbackThreadInitializer;
 import com.sun.jna.Native;
 import lombok.extern.slf4j.Slf4j;
@@ -29,9 +27,9 @@ import static com.inspur.netty.stream.Zlm4jServer.ZLM_API;
 public class MKPublishCallBack implements IMKPublishCallBack {
 
     @Autowired
-    private SmsbDeviceMapper smsbDeviceMapper;
+    private ISmsbDeviceService smsbDeviceService;
 
-    private static final String DEVICE_STREAM_PUSH_KEY = "stream:device:push:";
+    private static final String DEVICE_STREAM_PUSH_KEY = "global:stream:device:push:";
 
     public MKPublishCallBack() {
         //回调使用同一个线程
@@ -49,18 +47,22 @@ public class MKPublishCallBack implements IMKPublishCallBack {
     public void invoke(MK_MEDIA_INFO urlInfo, MK_PUBLISH_AUTH_INVOKER invoker, MK_SOCK_INFO sender) {
         //这里拿到访问路径后(例如rtmp://xxxx/xxx/xxx?token=xxxx其中?后面就是拿到的参数)的参数
         // err_msg返回 空字符串表示鉴权成功 否则鉴权失败提示
-        String mac = ZLM_API.mk_media_info_get_params(urlInfo);
-        log.info("MKPublishCallBack : device mac = " + mac);
+        // String Identifier = ZLM_API.mk_media_info_get_params(urlInfo);
+        String Identifier = ZLM_API.mk_media_info_get_stream(urlInfo).split("_")[0];
+
+        // System.out.println("stream : " + stream);
+        // String Identifier = stream.split("/")[2];
+        log.info("MKPublishCallBack : device Identifier = " + Identifier);
         // 根据mac地址获取设备信息
-        SmsbDevice smsbDevice = smsbDeviceMapper.selectOne(new QueryWrapper<SmsbDevice>().eq("mac", mac));
+        SmsbDeviceVo smsbDevice = smsbDeviceService.getDeviceByIdentifier(Identifier);
         if (smsbDevice == null) {
-            ZLM_API.mk_publish_auth_invoker_do(invoker,"当前设备无权限接入",0,0);
+            ZLM_API.mk_publish_auth_invoker_do(invoker, "当前设备无权限接入", 0, 0);
             return;
         }
         // 鉴权成功 查看redis是否存在缓存的推流地址,如果没有禁止接入
         String pushUrl = RedisUtils.getCacheObject(DEVICE_STREAM_PUSH_KEY + smsbDevice.getId());
         if (StringUtils.isEmpty(pushUrl)) {
-            ZLM_API.mk_publish_auth_invoker_do(invoker,"当前设备接入流地址已失效",0,0);
+            ZLM_API.mk_publish_auth_invoker_do(invoker, "当前设备接入流地址已失效", 0, 0);
             return;
         }
         // 正常推流

+ 18 - 12
smsb-modules/smsb-netty/src/main/java/com/inspur/netty/stream/service/impl/StreamServiceImpl.java

@@ -2,7 +2,7 @@ package com.inspur.netty.stream.service.impl;
 
 import com.aizuda.zlm4j.structure.MK_MEDIA_SOURCE;
 import com.inspur.device.domain.vo.SmsbDeviceVo;
-import com.inspur.device.mapper.SmsbDeviceMapper;
+import com.inspur.device.service.ISmsbDeviceService;
 import com.inspur.netty.domain.MediaServerConstants;
 import com.inspur.netty.domain.vo.StartStreamResultVo;
 import com.inspur.netty.message.push.PushMessageType;
@@ -28,41 +28,47 @@ import static com.inspur.netty.stream.Zlm4jServer.ZLM_API;
 @Service
 public class StreamServiceImpl implements IStreamService {
 
-    private static final String DEVICE_STREAM_PUSH_KEY = "stream:device:push:";
+    private static final String DEVICE_STREAM_PUSH_KEY = "global:stream:device:push:";
 
-    private static final String DEVICE_STREAM_VIEW_KEY = "stream:device:view:";
+    private static final String DEVICE_STREAM_VIEW_KEY = "global:stream:device:view:";
 
     private static final String schema = "rtmp";
 
     private static final String app = "live";
 
     @Autowired
-    private SmsbDeviceMapper smsbDeviceMapper;
+    private ISmsbDeviceService smsbDeviceService;
+
 
     @Override
     public StartStreamResultVo startStream(Long deviceId) {
         StartStreamResultVo resultVo = new StartStreamResultVo();
         resultVo.setPushResult(true);
         // 根据设备ID查询设备信息
-        SmsbDeviceVo smsbDeviceVo = smsbDeviceMapper.selectVoById(deviceId);
+        SmsbDeviceVo smsbDeviceVo = smsbDeviceService.getDeviceCacheById(deviceId);
         // 根据设备ID,查询redis是否已经存在流地址
         String viewUrl = RedisUtils.getCacheObject(DEVICE_STREAM_VIEW_KEY + deviceId);
-        if (viewUrl != null) {
-            // 已开启推流
+        String pushUrl = RedisUtils.getCacheObject(DEVICE_STREAM_PUSH_KEY + deviceId);
+        if (viewUrl != null && pushUrl != null) {
+            // 已有推拉流地址
+            String nettyMessage = PushMessageType.CONTROL_START_STREAM.getValue() + "|" + pushUrl;
+            boolean pushResult = PushMsgUtil.sendV2(smsbDeviceVo.getIdentifier(), nettyMessage);
             resultVo.setViewUrl(viewUrl);
+            resultVo.setPushResult(pushResult);
             return resultVo;
         }
+        Long currentTimeMillis = System.currentTimeMillis();
         // 生成推流地址
-        String streamUrl = schema + "://127.0.0.1:1935/" + app + "/" + deviceId + "/" + System.currentTimeMillis() + "?" + smsbDeviceVo.getIdentifier();
+        String streamUrl = schema + "://127.0.0.1:1935/" + app + "/" + smsbDeviceVo.getIdentifier() + "_" + currentTimeMillis;
         // 前端看流地址为flv
-        viewUrl = "http://127.0.0.1:7788/" + app + "/" + deviceId + "/" + System.currentTimeMillis() + "?" + smsbDeviceVo.getIdentifier() + ".live.flv";
+        viewUrl = "http://127.0.0.1:7788/" + app + "/" + smsbDeviceVo.getIdentifier() + "_" + currentTimeMillis + ".live.flv";
         // 存入redis
-        RedisUtils.setCacheObject(DEVICE_STREAM_PUSH_KEY + deviceId, streamUrl, Duration.ofMinutes(1));
+        RedisUtils.setCacheObject(DEVICE_STREAM_PUSH_KEY + deviceId, streamUrl, Duration.ofMinutes(30));
         // 发送netty消息,通知设备开始推流
         String nettyMessage = PushMessageType.CONTROL_START_STREAM.getValue() + "|" + streamUrl;
         boolean pushResult = PushMsgUtil.sendV2(smsbDeviceVo.getIdentifier(), nettyMessage);
         if (pushResult) {
-            RedisUtils.setCacheObject(DEVICE_STREAM_VIEW_KEY + deviceId, viewUrl, Duration.ofMinutes(60));
+            RedisUtils.setCacheObject(DEVICE_STREAM_VIEW_KEY + deviceId, viewUrl, Duration.ofMinutes(30));
         }
         resultVo.setPushResult(pushResult);
         resultVo.setViewUrl(viewUrl);
@@ -76,7 +82,7 @@ public class StreamServiceImpl implements IStreamService {
             return;
         }
         // 根据设备ID查询设备信息
-        SmsbDeviceVo smsbDeviceVo = smsbDeviceMapper.selectVoById(deviceId);
+        SmsbDeviceVo smsbDeviceVo = smsbDeviceService.getDeviceCacheById(deviceId);
         String nettyMessage = PushMessageType.CONTROL_STOP_STREAM.getValue();
 
         // 根据当前流地址查询观看人数

+ 8 - 0
smsb-plus-ui/src/api/smsb/device/errorRecord.ts

@@ -16,6 +16,14 @@ export const listDeviceErrorRecord = (query?: DeviceErrorRecordQuery): AxiosProm
   });
 };
 
+export const alarmCountByType = (params?: any): AxiosPromise<DeviceErrorRecordVO> => {
+  return request({
+    url: '/device/errorRecord/count/byType',
+    method: 'get',
+    params: params
+  });
+};
+
 /**
  * 查询设备告警记录详细
  * @param id

+ 6 - 0
smsb-plus-ui/src/api/smsb/device/errorRecord_type.ts

@@ -23,6 +23,12 @@ export interface DeviceErrorRecordVO {
    * 告警类型
    */
   errorType: number;
+
+  createTime: string;
+
+  alarmType: string[];
+
+  alarmCount: number[];
 }
 
 export interface DeviceErrorRecordForm extends BaseEntity {

+ 65 - 12
smsb-plus-ui/src/views/smsb/dashboard/device.vue

@@ -51,7 +51,7 @@
             <el-col :span="8">
               <el-card shadow="hover">
                 <h3>告警问题统计</h3>
-                <div class="chart-placeholder"></div>
+                <div ref="alarmCount" class="chart-placeholder"></div>
               </el-card>
             </el-col>
           </el-row>
@@ -81,11 +81,19 @@
         <el-col :span="6">
           <el-card shadow="hover">
             <h3>告警清单</h3>
-            <el-table :data="alarmList" style="width: 100%">
-              <el-table-column prop="time" label="时间" width="160"></el-table-column>
-              <el-table-column prop="device" label="设备名称"></el-table-column>
-              <el-table-column prop="type" label="告警类型"></el-table-column>
-              <el-table-column prop="level" label="告警级别"></el-table-column>
+            <el-table v-loading="loading" :data="alarmList" row-key="id">
+              <el-table-column label="设备名称" align="left" prop="deviceName" :show-overflow-tooltip="true" />
+              <el-table-column label="告警等级" align="center" prop="errorLevel" width="100">
+                <template #default="scope">
+                  <dict-tag :options="smsb_device_error_level" :value="scope.row.errorLevel" />
+                </template>
+              </el-table-column>
+              <el-table-column label="告警类型" align="center" prop="errorType" width="100">
+                <template #default="scope">
+                  <dict-tag :options="smsb_device_error_type" :value="scope.row.errorType" />
+                </template>
+              </el-table-column>
+              <el-table-column label="创建时间" align="left" prop="createTime" width="160" />
             </el-table>
           </el-card>
         </el-col>
@@ -98,10 +106,28 @@
 import { reactive } from 'vue';
 import { deviceStatistics } from '@/api/smsb/device/device';
 import * as echarts from 'echarts';
+import { DeviceErrorRecordQuery, DeviceErrorRecordVO } from '@/api/smsb/device/errorRecord_type';
+import { alarmCountByType, listDeviceErrorRecord } from '@/api/smsb/device/errorRecord';
 
+const alarmList = ref<DeviceErrorRecordVO[]>([]);
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { smsb_device_error_level, smsb_device_error_type } = toRefs<any>(proxy?.useDict('smsb_device_error_level', 'smsb_device_error_type'));
+const total = ref(0);
+const loading = ref(true);
 const timeRadio = ref('today');
 const dateRange = ref(['2025-01-01', '2025-01-01']);
 const typePie = ref();
+const alarmCount = ref();
+const dialogData = reactive<DialogPageData<DeviceErrorRecordQuery>>({
+  dialogQueryParams: {
+    pageNum: 1,
+    pageSize: 11,
+    deviceId: undefined,
+    params: {}
+  }
+});
+const { dialogQueryParams } = toRefs(dialogData);
+
 const stats = reactive([
   { label: '设备总量', value: 0, class: '' },
   { label: '已上线设备', value: 0, class: 'success' },
@@ -121,12 +147,6 @@ const alarmRanking = reactive([
   { rank: 3, device: '设备C', value: '5800' }
 ]);
 
-const alarmList = reactive([
-  { time: '2025-02-20', device: '摄像头01', type: '紧急SOS', level: '设备报警' },
-  { time: '2025-02-20', device: '摄像头02', type: '紧急SOS', level: '设备报警' },
-  { time: '2025-02-20', device: '摄像头03', type: '紧急SOS', level: '设备报警' }
-]);
-
 const handleDateRangeChange = () => {
   const rangeType = timeRadio.value;
   const today = new Date();
@@ -145,6 +165,7 @@ const handleDateRangeChange = () => {
       throw new Error('Invalid range type');
   }
   dateRange.value = [formatDate(startDate), formatDate(endDate)];
+  alarmCountData();
 };
 
 const formatDate = (date: Date) => {
@@ -153,6 +174,29 @@ const formatDate = (date: Date) => {
   const day = String(date.getDate()).padStart(2, '0');
   return `${year}-${month}-${day}`;
 };
+const alarmCountData = async () => {
+  const params = {
+    startTime: dateRange.value[0],
+    endTime: dateRange.value[1]
+  };
+  const res = await alarmCountByType(params);
+  const alarmTypeInstance = echarts.init(alarmCount.value, 'macaroons');
+  alarmTypeInstance.setOption({
+    xAxis: {
+      type: 'category',
+      data: res.data.alarmType
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        data: res.data.alarmCount,
+        type: 'bar'
+      }
+    ]
+  });
+};
 const getDeviceStatistics = async () => {
   const res = await deviceStatistics();
   stats.forEach((item) => {
@@ -206,9 +250,18 @@ const getDeviceStatistics = async () => {
     ]
   });
 };
+const getAlarmList = async () => {
+  loading.value = true;
+  const res = await listDeviceErrorRecord(dialogQueryParams.value);
+  alarmList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+};
 onMounted(() => {
   handleDateRangeChange();
   getDeviceStatistics();
+  getAlarmList();
+  alarmCountData();
 });
 </script>
 

+ 125 - 30
smsb-plus-ui/src/views/smsb/device/index.vue

@@ -189,9 +189,9 @@
       </template>
     </el-dialog>
     <!--设备详情弹窗-->
-    <el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="900px" append-to-body>
+    <el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="1000px" append-to-body>
       <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClickTab">
-        <el-tab-pane label="状态检测" name="first">
+        <el-tab-pane label="状态检测" name="info">
           <div>
             <el-row :gutter="20" style="height: 100%; display: flex">
               <el-col :span="8">
@@ -207,7 +207,7 @@
             </el-row>
           </div>
         </el-tab-pane>
-        <el-tab-pane label="远程操作" name="second">
+        <el-tab-pane label="远程操作" name="control">
           <el-button :loading="buttonLoading" type="primary" @click="handleControl('reboot')">设备重启</el-button>
           <el-button :loading="buttonLoading" type="primary" @click="handleControl('start')">远程开机</el-button>
           <el-button :loading="buttonLoading" type="primary" @click="handleControl('shutdown')">远程关机</el-button>
@@ -215,7 +215,30 @@
           <el-button :loading="buttonLoading" type="primary" @click="handleVoice">音量调节</el-button>
           <el-button :loading="buttonLoading" type="primary" @click="handleBrightness">亮度调节</el-button>
         </el-tab-pane>
-        <el-tab-pane label="报警信息" name="third">报警信息</el-tab-pane>
+        <el-tab-pane label="报警信息" name="alarm">
+          <el-table v-loading="loading" :data="alarmList" row-key="id">
+            <el-table-column label="主键ID" align="left" prop="id" v-if="true" />
+            <el-table-column label="设备名称" align="left" prop="deviceName" :show-overflow-tooltip="true" />
+            <el-table-column label="告警等级" align="center" prop="errorLevel" width="100">
+              <template #default="scope">
+                <dict-tag :options="smsb_device_error_level" :value="scope.row.errorLevel" />
+              </template>
+            </el-table-column>
+            <el-table-column label="告警类型" align="center" prop="errorType" width="100">
+              <template #default="scope">
+                <dict-tag :options="smsb_device_error_type" :value="scope.row.errorType" />
+              </template>
+            </el-table-column>
+            <el-table-column label="创建时间" align="left" prop="createTime" width="160" />
+          </el-table>
+          <pagination
+            v-show="alarmTotal > 0"
+            :total="alarmTotal"
+            v-model:page="dialogQueryParams.pageNum"
+            v-model:limit="dialogQueryParams.pageSize"
+            @pagination="getAlarmList"
+          />
+        </el-tab-pane>
       </el-tabs>
       <template #footer>
         <div class="dialog-footer">
@@ -223,13 +246,13 @@
         </div>
       </template>
     </el-dialog>
-    <el-dialog :title="watchDialog.title" v-model="watchDialog.visible" width="900px" append-to-body>
+    <el-dialog :title="watchDialog.title" v-model="watchDialog.visible" width="900px" append-to-body @closed="onDialogClosed">
       <div v-if="watchDialog.visible" style="width: 100%; height: 500px">
-        <video id="flv-player" style="width: 100%; height: 100%" controls></video>
+        <video ref="flvPlayerRef" style="width: 100%; height: 100%" controls></video>
       </div>
       <template #footer>
         <div class="dialog-footer">
-          <el-button @click="cancel">取 消</el-button>
+          <el-button @click="cancelStream">取 消</el-button>
         </div>
       </template>
     </el-dialog>
@@ -249,7 +272,8 @@ import {
   shutdown,
   startStream,
   deviceStatistics,
-  getDeviceRunInfo
+  getDeviceRunInfo,
+  stopStream
 } from '@/api/smsb/device/device';
 import { DeviceVO, DeviceQuery, DeviceForm, DeviceStatisticsVo } from '@/api/smsb/device/device_type';
 import { ProductVO } from '@/api/smsb/device/product_types';
@@ -257,11 +281,17 @@ import { listProduct } from '@/api/smsb/device/product';
 import { DeviceManufacturerVO } from '@/api/smsb/device/deviceManufacturer_type';
 import { listDeviceManufacturer } from '@/api/smsb/device/deviceManufacturer';
 import type { TabsPaneContext } from 'element-plus';
+import { ref, onBeforeUnmount } from 'vue';
 import flvjs from 'flv.js';
 import { DeviceRunInfoVO } from '@/api/smsb/device/device_run_type';
+import { DeviceErrorRecordQuery, DeviceErrorRecordVO } from '@/api/smsb/device/errorRecord_type';
+import { listDeviceErrorRecord } from '@/api/smsb/device/errorRecord';
 
+const alarmList = ref<DeviceErrorRecordVO[]>([]);
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { sys_device_online, smsb_yes_no } = toRefs<any>(proxy?.useDict('sys_device_online', 'smsb_yes_no'));
+const { sys_device_online, smsb_yes_no, smsb_device_error_level, smsb_device_error_type } = toRefs<any>(
+  proxy?.useDict('sys_device_online', 'smsb_yes_no', 'smsb_device_error_level', 'smsb_device_error_type')
+);
 const deviceList = ref<DeviceVO[]>([]);
 const deviceStatisticsVo = ref<DeviceStatisticsVo>();
 const productList = ref<ProductVO[]>([]);
@@ -277,8 +307,10 @@ const queryFormRef = ref<ElFormInstance>();
 const deviceFormRef = ref<ElFormInstance>();
 const deviceId = ref<string | number>();
 const totalNum = ref(0);
+const alarmTotal = ref(0);
 const onlineNum = ref(0);
 const offlineNum = ref(0);
+const streamDeviceId = ref<string | number>();
 const initNum = ref(0);
 const deviceRunInfo = reactive<DeviceRunInfoVO>({
   deviceBase: undefined,
@@ -294,8 +326,6 @@ const deviceRunInfo = reactive<DeviceRunInfoVO>({
   appList: undefined,
   createTime: undefined
 });
-// 用于保存flv.js播放器实例
-let flvPlayer: flvjs.Player | null = null;
 const dialog = reactive<DialogOption>({
   visible: false,
   title: ''
@@ -305,11 +335,14 @@ const viewDialog = reactive<DialogOption>({
   visible: false,
   title: ''
 });
+// 播放器实例引用
+const flvPlayer = ref<flvjs.Player | null>(null);
+const flvPlayerRef = ref<HTMLVideoElement | null>(null);
 const watchDialog = reactive<DialogOption>({
   visible: false,
   title: ''
 });
-const activeName = ref('first');
+const activeName = ref('info');
 
 const initFormData: DeviceForm = {
   id: undefined,
@@ -361,8 +394,17 @@ const data = reactive<PageData<DeviceForm, DeviceQuery>>({
     ]
   }
 });
+const dialogData = reactive<DialogPageData<DeviceErrorRecordQuery>>({
+  dialogQueryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    deviceId: undefined,
+    params: {}
+  }
+});
 
 const { queryParams, form, rules } = toRefs(data);
+const { dialogQueryParams } = toRefs(dialogData);
 
 /** 查询设备列表 */
 const getList = async () => {
@@ -419,6 +461,10 @@ const cancel = () => {
   }
 };
 
+const cancelStream = () => {
+  stopMonitor();
+};
+
 /** 表单重置 */
 const reset = () => {
   form.value = { ...initFormData };
@@ -506,6 +552,7 @@ const handleExport = () => {
   );
 };
 const handleInfo = async (row?: DeviceVO) => {
+  activeName.value = 'info';
   deviceId.value = row?.id || ids.value[0];
   const runInfo = await getDeviceRunInfo(deviceId.value);
   Object.assign(deviceRunInfo, runInfo.data);
@@ -532,30 +579,78 @@ const handleControl = async (type: string) => {
 };
 
 const startMonitor = async (row?: DeviceVO) => {
-  const res = await startStream(ids.value[0]);
-  if (res.data.pushResult) {
-    watchDialog.visible = true;
-    watchDialog.title = '回采画面';
-    // 创建FLV播放器
-    const videoElement = document.getElementById('flv-player') as HTMLVideoElement;
-    const player = flvjs.createPlayer({
-      type: 'flv',
-      url: res.data.viewUrl
-    });
-    // 加载并播放
-    player.attachMediaElement(videoElement);
-    player.load();
-    player.play();
-    flvPlayer = player;
-  } else {
-    proxy?.$modal.msgError('开启推流失败!');
+  try {
+    const res = await startStream(ids.value[0]);
+    streamDeviceId.value = ids.value[0];
+
+    if (res.data.pushResult) {
+      // 先销毁已有播放器
+      destroyPlayer();
+      watchDialog.visible = true;
+      watchDialog.title = '回采画面';
+      // 确保DOM已更新
+      await nextTick();
+      if (flvPlayerRef.value && res.data.viewUrl) {
+        flvPlayer.value = flvjs.createPlayer({
+          type: 'flv',
+          url: res.data.viewUrl
+        });
+        // 错误处理
+        /*flvPlayer.value.on(flvjs.Events.ERROR, (errType, errDetail) => {
+          console.error('播放错误:', errType, errDetail);
+          proxy?.$modal.msgError('视频播放失败,请检查流地址');
+          destroyPlayer();
+        });*/
+        flvPlayer.value.attachMediaElement(flvPlayerRef.value);
+        flvPlayer.value.load();
+        // 处理浏览器自动播放策略
+        flvPlayer.value.play().catch(() => {
+          proxy?.$modal.msgWarning('请点击视频播放按钮以开始播放');
+        });
+      }
+    } else {
+      proxy?.$modal.msgError('开启推流失败!');
+    }
+  } catch (error) {
+    proxy?.$modal.msgError('请求推流失败');
+  }
+};
+// 清理播放器
+const destroyPlayer = () => {
+  if (flvPlayer.value) {
+    flvPlayer.value.pause();
+    flvPlayer.value.unload();
+    flvPlayer.value.detachMediaElement();
+    flvPlayer.value.destroy();
+    flvPlayer.value = null;
   }
 };
+// 对话框关闭时清理
+const onDialogClosed = () => {
+  destroyPlayer();
+};
+const stopMonitor = async () => {
+  const res = await stopStream(streamDeviceId.value);
+  watchDialog.visible = false;
+};
+
+const getAlarmList = async () => {
+  dialogQueryParams.value.deviceId = deviceId.value;
+  const res = await listDeviceErrorRecord(dialogQueryParams.value);
+  alarmList.value = res.rows;
+  alarmTotal.value = res.total;
+};
 
 const handleClickTab = (tab: TabsPaneContext, event: Event) => {
   console.log(tab, event);
+  if (tab.props.name === 'alarm') {
+    getAlarmList();
+  }
 };
-
+// 组件卸载前清理
+onBeforeUnmount(() => {
+  destroyPlayer();
+});
 onMounted(() => {
   getList();
   getManufacturerList();