From 0517ab3be9ef119196f0c38ff31ecb3d00f6c394 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Fri, 13 Dec 2024 14:35:44 +0800
Subject: [PATCH 01/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0RPC=E5=BD=95=E5=83=8F?=
 =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=9F=A5=E8=AF=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../genersoft/iot/vmp/conf/UserSetting.java   |   5 +
 .../iot/vmp/conf/redis/RedisRpcConfig.java    |   5 +-
 .../controller/GBRecordController.java        |  40 ++--
 .../iot/vmp/gb28181/event/EventPublisher.java |   8 +-
 ...dEndEvent.java => RecordInfoEndEvent.java} |  17 +-
 .../gb28181/event/record/RecordInfoEvent.java |  27 +++
 ...ener.java => RecordInfoEventListener.java} |  11 +-
 .../service/IDeviceChannelService.java        |   9 +-
 .../vmp/gb28181/service/IDeviceService.java   |   5 +-
 .../service/IGbChannelPlayService.java        |   2 +-
 .../gb28181/service/IGbChannelService.java    |   2 +
 .../impl/DeviceChannelServiceImpl.java        |  86 +++++++-
 .../service/impl/DeviceServiceImpl.java       |  11 +-
 .../impl/GbChannelPlayServiceImpl.java        |   7 +-
 .../service/impl/GbChannelServiceImpl.java    |  28 +++
 .../vmp/gb28181/session/RecordDataCatch.java  |   6 +-
 .../cmd/RecordInfoQueryMessageHandler.java    |   6 +-
 .../cmd/RecordInfoResponseMessageHandler.java | 204 +++++++++---------
 .../redisMsg/IRedisRpcPlayService.java        |   6 +-
 .../RedisRpcChannelPlayController.java        |  70 ++++--
 .../control/RedisRpcSendRtpController.java    |  12 +-
 .../control/RedisRpcStreamPushController.java |  13 +-
 .../service/RedisRpcPlayServiceImpl.java      |  30 ++-
 .../iot/vmp/vmanager/bean/ErrorCode.java      |   1 +
 src/main/resources/配置详情.yml               |   2 +
 25 files changed, 409 insertions(+), 204 deletions(-)
 rename src/main/java/com/genersoft/iot/vmp/gb28181/event/record/{RecordEndEvent.java => RecordInfoEndEvent.java} (54%)
 create mode 100755 src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java
 rename src/main/java/com/genersoft/iot/vmp/gb28181/event/record/{RecordEndEventListener.java => RecordInfoEventListener.java} (85%)

diff --git a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
index 604c8c56a..b9356592f 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
@@ -37,6 +37,11 @@ public class UserSetting {
      */
     private Integer playTimeout = 10000;
 
+    /**
+     * 获取设备录像数据超时时间,单位:毫秒
+     */
+    private Integer recordInfoTimeout = 15000;
+
     /**
      * 上级点播等待超时时间,单位:毫秒
      */
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
index 02293685a..29e090ad4 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
@@ -8,6 +8,7 @@ import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -129,7 +130,7 @@ public class RedisRpcConfig implements MessageListener {
                 if (method == null) {
                     // 回复404结果
                     RedisRpcResponse response = request.getResponse();
-                    response.setStatusCode(Response.NOT_FOUND);
+                    response.setStatusCode(ErrorCode.ERROR404.getCode());
                     sendResponse(response);
                     return;
                 }
@@ -185,7 +186,7 @@ public class RedisRpcConfig implements MessageListener {
         } catch (InterruptedException e) {
             log.warn("[redis rpc timeout] uri: {}, sn: {}", request.getUri(), request.getSn(), e);
             RedisRpcResponse redisRpcResponse = new RedisRpcResponse();
-            redisRpcResponse.setStatusCode(Response.BUSY_HERE);
+            redisRpcResponse.setStatusCode(ErrorCode.ERROR486.getCode());
             return redisRpcResponse;
         } finally {
             this.unsubscribe(request.getSn());
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java
index e48ae616e..87357d27a 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java
@@ -36,6 +36,7 @@ import javax.sip.InvalidArgumentException;
 import javax.sip.SipException;
 import java.text.ParseException;
 import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 
 @Tag(name  = "国标录像")
 @Slf4j
@@ -72,7 +73,7 @@ public class GBRecordController {
 		if (log.isDebugEnabled()) {
 			log.debug(String.format("录像信息查询 API调用,deviceId:%s ,startTime:%s, endTime:%s",deviceId, startTime, endTime));
 		}
-		DeferredResult<WVPResult<RecordInfo>> result = new DeferredResult<>();
+		DeferredResult<WVPResult<RecordInfo>> result = new DeferredResult<>(Long.valueOf(userSetting.getRecordInfoTimeout()), TimeUnit.MILLISECONDS);
 		if (!DateUtil.verification(startTime, DateUtil.formatter)){
 			throw new ControllerException(ErrorCode.ERROR100.getCode(), "startTime格式为" + DateUtil.PATTERN);
 		}
@@ -81,35 +82,24 @@ public class GBRecordController {
 		}
 
 		Device device = deviceService.getDeviceByDeviceId(deviceId);
-		// 指定超时时间 1分钟30秒
-		String uuid = UUID.randomUUID().toString();
-		int sn  =  (int)((Math.random()*9+1)*100000);
-		String key = DeferredResultHolder.CALLBACK_CMD_RECORDINFO + deviceId + sn;
-		RequestMessage msg = new RequestMessage();
-		msg.setId(uuid);
-		msg.setKey(key);
-		try {
-			cmder.recordInfoQuery(device, channelId, startTime, endTime, sn, null, null, null, (eventResult -> {
-				WVPResult<RecordInfo> wvpResult = new WVPResult<>();
-				wvpResult.setCode(ErrorCode.ERROR100.getCode());
-				wvpResult.setMsg("查询录像失败, status: " +  eventResult.statusCode + ", message: " + eventResult.msg);
-				msg.setData(wvpResult);
-				resultHolder.invokeResult(msg);
-			}));
-		} catch (InvalidArgumentException | SipException | ParseException e) {
-			log.error("[命令发送失败] 查询录像: {}", e.getMessage());
-			throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " +  e.getMessage());
+		if (device == null) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), deviceId + " 不存在");
 		}
-
-		// 录像查询以channelId作为deviceId查询
-		resultHolder.put(key, uuid, result);
+		DeviceChannel channel = channelService.getOneForSource(device.getId(), channelId);
+		if (channel == null) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), channelId + " 不存在");
+		}
+		channelService.queryRecordInfo(device, channel, startTime, endTime, (code, msg, data)->{
+			WVPResult<RecordInfo> wvpResult = new WVPResult<>();
+			wvpResult.setCode(code);
+			wvpResult.setMsg(msg);
+			result.setResult(wvpResult);
+		});
 		result.onTimeout(()->{
-			msg.setData("timeout");
 			WVPResult<RecordInfo> wvpResult = new WVPResult<>();
 			wvpResult.setCode(ErrorCode.ERROR100.getCode());
 			wvpResult.setMsg("timeout");
-			msg.setData(wvpResult);
-			resultHolder.invokeResult(msg);
+			result.setResult(wvpResult);
 		});
         return result;
 	}
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java
index 7d4c876a9..a19a1009b 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java
@@ -3,7 +3,7 @@ package com.genersoft.iot.vmp.gb28181.event;
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEvent;
 import com.genersoft.iot.vmp.gb28181.event.device.RequestTimeoutEvent;
-import com.genersoft.iot.vmp.gb28181.event.record.RecordEndEvent;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEvent;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.mobilePosition.MobilePositionEvent;
 import com.genersoft.iot.vmp.media.bean.MediaServer;
@@ -101,9 +101,5 @@ public class EventPublisher {
 		applicationEventPublisher.publishEvent(event);
 	}
 
-	public void recordEndEventPush(RecordInfo recordInfo) {
-		RecordEndEvent outEvent = new RecordEndEvent(this);
-		outEvent.setRecordInfo(recordInfo);
-		applicationEventPublisher.publishEvent(outEvent);
-	}
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java
similarity index 54%
rename from src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEvent.java
rename to src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java
index cfd2985c5..4788eb622 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEvent.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java
@@ -1,7 +1,8 @@
 package com.genersoft.iot.vmp.gb28181.event.record;
 
-import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm;
 import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
+import lombok.Getter;
+import lombok.Setter;
 import org.springframework.context.ApplicationEvent;
 
 /**
@@ -9,24 +10,18 @@ import org.springframework.context.ApplicationEvent;
  * @author: pan
  * @data: 2022-02-23
  */
-
-public class RecordEndEvent extends ApplicationEvent {
+@Setter
+@Getter
+public class RecordInfoEndEvent extends ApplicationEvent {
     /**
      *
      */
     private static final long serialVersionUID = 1L;
 
-    public RecordEndEvent(Object source) {
+    public RecordInfoEndEvent(Object source) {
         super(source);
     }
 
     private RecordInfo recordInfo;
 
-    public RecordInfo getRecordInfo() {
-        return recordInfo;
-    }
-
-    public void setRecordInfo(RecordInfo recordInfo) {
-        this.recordInfo = recordInfo;
-    }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java
new file mode 100755
index 000000000..d78a22b28
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java
@@ -0,0 +1,27 @@
+package com.genersoft.iot.vmp.gb28181.event.record;
+
+import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * @description: 录像查询结束时间
+ * @author: pan
+ * @data: 2022-02-23
+ */
+
+@Setter
+@Getter
+public class RecordInfoEvent extends ApplicationEvent {
+    /**
+     *
+     */
+    private static final long serialVersionUID = 1L;
+
+    public RecordInfoEvent(Object source) {
+        super(source);
+    }
+
+    private RecordInfo recordInfo;
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEventListener.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java
similarity index 85%
rename from src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEventListener.java
rename to src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java
index 411b54d5c..f2a0986d6 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEventListener.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java
@@ -15,15 +15,15 @@ import java.util.concurrent.ConcurrentHashMap;
  */
 @Slf4j
 @Component
-public class RecordEndEventListener implements ApplicationListener<RecordEndEvent> {
+public class RecordInfoEventListener implements ApplicationListener<RecordInfoEvent> {
 
-    private Map<String, RecordEndEventHandler> handlerMap = new ConcurrentHashMap<>();
+    private final Map<String, RecordEndEventHandler> handlerMap = new ConcurrentHashMap<>();
     public interface RecordEndEventHandler{
         void  handler(RecordInfo recordInfo);
     }
 
     @Override
-    public void onApplicationEvent(RecordEndEvent event) {
+    public void onApplicationEvent(RecordInfoEvent event) {
         String deviceId = event.getRecordInfo().getDeviceId();
         String channelId = event.getRecordInfo().getChannelId();
         int count = event.getRecordInfo().getCount();
@@ -45,9 +45,6 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
 
     /**
      * 添加
-     * @param device
-     * @param channelId
-     * @param recordEndEventHandler
      */
     public void addEndEventHandler(String device, String channelId, RecordEndEventHandler recordEndEventHandler) {
         log.info("录像查询事件添加监听,deviceId:{}, channelId: {}", device, channelId);
@@ -55,8 +52,6 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
     }
     /**
      * 添加
-     * @param device
-     * @param channelId
      */
     public void delEndEventHandler(String device, String channelId) {
         log.info("录像查询事件移除监听,deviceId:{}, channelId: {}", device, channelId);
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
index bb21e3214..8ef7ee52a 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
@@ -1,9 +1,8 @@
 package com.genersoft.iot.vmp.gb28181.service;
 
-import com.genersoft.iot.vmp.gb28181.bean.Device;
-import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
-import com.genersoft.iot.vmp.gb28181.bean.MobilePosition;
+import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.controller.bean.ChannelReduce;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo;
 import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend;
 import com.github.pagehelper.PageInfo;
@@ -125,4 +124,8 @@ public interface IDeviceChannelService {
     List<DeviceChannel> queryChaneListByDeviceDbId(Integer deviceDbId);
 
     List<Integer> queryChaneIdListByDeviceDbIds(List<Integer> deviceDbId);
+
+    void queryRecordInfo(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> object);
+
+    void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> object);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java
index a8f48a476..43a965f4c 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java
@@ -1,9 +1,8 @@
 package com.genersoft.iot.vmp.gb28181.service;
 
 import com.genersoft.iot.vmp.common.CommonCallback;
-import com.genersoft.iot.vmp.gb28181.bean.Device;
-import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo;
-import com.genersoft.iot.vmp.gb28181.bean.SyncStatus;
+import com.genersoft.iot.vmp.gb28181.bean.*;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo;
 import com.github.pagehelper.PageInfo;
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
index 967661a84..61c0b8cbf 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
@@ -5,6 +5,7 @@ import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo;
 import com.genersoft.iot.vmp.gb28181.bean.Platform;
+import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 
 public interface IGbChannelPlayService {
@@ -25,6 +26,5 @@ public interface IGbChannelPlayService {
 
     void playPush(CommonGBChannel channel, String platformDeviceId, String platformName, ErrorCallback<StreamInfo> callback);
 
-
     void  stopPlayPush(CommonGBChannel channel);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java
index 8dc67df20..441b524fc 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java
@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.gb28181.service;
 
 import com.genersoft.iot.vmp.gb28181.bean.*;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.streamPush.bean.StreamPush;
 import com.github.pagehelper.PageInfo;
 
@@ -87,4 +88,5 @@ public interface IGbChannelService {
 
     PageInfo<CommonGBChannel> queryList(int page, int count, String query, Boolean online, Boolean hasRecordPlan, Integer channelType);
 
+    void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> callback);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
index b397f4d4e..062fd1d56 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
@@ -5,37 +5,48 @@ import com.genersoft.iot.vmp.common.InviteInfo;
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
-import com.genersoft.iot.vmp.gb28181.bean.Device;
-import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
-import com.genersoft.iot.vmp.gb28181.bean.GbCode;
-import com.genersoft.iot.vmp.gb28181.bean.MobilePosition;
+import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.controller.bean.ChannelReduce;
 import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper;
 import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper;
 import com.genersoft.iot.vmp.gb28181.dao.DeviceMobilePositionMapper;
 import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper;
 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEndEvent;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
 import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
 import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService;
 import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService;
+import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
 import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
+import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
+import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo;
+import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
 
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import javax.sip.message.Response;
+import java.text.ParseException;
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.TimeUnit;
 
 /**
  * @author lin
@@ -71,6 +82,26 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
     @Autowired
     private IPlatformChannelService platformChannelService;
 
+    @Autowired
+    private IRedisRpcPlayService redisRpcPlayService;
+
+    @Autowired
+    private ISIPCommander commander;
+
+    // 记录录像查询的结果等待
+    private final Map<String, SynchronousQueue<RecordInfo>> topicSubscribers = new ConcurrentHashMap<>();
+
+    /**
+     * 监听录像查询结束事件
+     */
+    @Async("taskExecutor")
+    @org.springframework.context.event.EventListener
+    public void onApplicationEvent(RecordInfoEndEvent event) {
+        SynchronousQueue<RecordInfo> queue = topicSubscribers.get("record" + event.getRecordInfo().getSn());
+        if (queue != null) {
+            queue.offer(event.getRecordInfo());
+        }
+    }
 
     @Override
     public int updateChannels(Device device, List<DeviceChannel> channels) {
@@ -164,10 +195,8 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
 
     @Override
     public ResourceBaseInfo getOverview() {
-
         int online = channelMapper.getOnlineCount();
         int total = channelMapper.getAllChannelCount();
-
         return new ResourceBaseInfo(total, online);
     }
 
@@ -705,4 +734,49 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
     public void updateChannelForNotify(DeviceChannel channel) {
         channelMapper.updateChannelForNotify(channel);
     }
+
+    @Override
+    public void queryRecordInfo(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> callback) {
+        if (!userSetting.getServerId().equals(device.getServerId())){
+            redisRpcPlayService.queryRecordInfo(device.getServerId(), channel.getId(), startTime, endTime, callback);
+            return;
+        }
+        try {
+            int sn  =  (int)((Math.random()*9+1)*100000);
+            commander.recordInfoQuery(device, channel.getDeviceId(), startTime, endTime, sn, null, null, eventResult -> {
+                try {
+                    // 消息发送成功, 监听等待数据到来
+                    SynchronousQueue<RecordInfo> queue = new SynchronousQueue<>();
+                    topicSubscribers.put("record" + sn, queue);
+                    RecordInfo recordInfo = queue.poll(userSetting.getRecordInfoTimeout(), TimeUnit.MILLISECONDS);
+                    if (recordInfo == null) {
+                        callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), recordInfo);
+                    }
+                } catch (InterruptedException e) {
+                    callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null);
+                } finally {
+                    this.topicSubscribers.remove("record" + sn);
+                }
+
+            }, (eventResult -> {
+                callback.run(ErrorCode.ERROR100.getCode(), "查询录像失败, status: " +  eventResult.statusCode + ", message: " + eventResult.msg, null);
+            }));
+        } catch (InvalidArgumentException | SipException | ParseException e) {
+            log.error("[命令发送失败] 查询录像: {}", e.getMessage());
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " +  e.getMessage());
+        }
+    }
+
+    @Override
+    public void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> callback) {
+        Device device = deviceMapper.query(channel.getGbDeviceDbId());
+        if (device == null) {
+            log.warn("[点播] 未找到通道{}的设备信息", channel);
+            callback.run(ErrorCode.ERROR100.getCode(), "设备不存在", null);
+            return;
+        }
+        DeviceChannel deviceChannel = getOneForSourceById(channel.getGbId());
+        queryRecordInfo(device, deviceChannel, startTime, endTime, callback);
+
+    }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java
index 4773b9fda..3c69da757 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java
@@ -6,6 +6,7 @@ import com.genersoft.iot.vmp.common.VideoManagerConstants;
 import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper;
 import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper;
@@ -23,10 +24,13 @@ import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.respons
 import com.genersoft.iot.vmp.media.bean.MediaServer;
 import com.genersoft.iot.vmp.media.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.ISendRtpServerService;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
+import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo;
+import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import lombok.extern.slf4j.Slf4j;
@@ -36,9 +40,13 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.sip.InvalidArgumentException;
 import javax.sip.SipException;
+import javax.sip.message.Response;
 import java.text.ParseException;
 import java.time.Instant;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -69,9 +77,6 @@ public class DeviceServiceImpl implements IDeviceService {
     @Autowired
     private PlatformChannelMapper platformChannelMapper;
 
-    @Autowired
-    private IDeviceChannelService deviceChannelService;
-
     @Autowired
     private DeviceChannelMapper deviceChannelMapper;
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
index c1c009d32..cd3bf2129 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
@@ -2,10 +2,7 @@ package com.genersoft.iot.vmp.gb28181.service.impl;
 
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
-import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
-import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo;
-import com.genersoft.iot.vmp.gb28181.bean.Platform;
-import com.genersoft.iot.vmp.gb28181.bean.PlayException;
+import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
 import com.genersoft.iot.vmp.gb28181.service.IPlayService;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
@@ -223,4 +220,6 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
             callback.run(Response.BUSY_HERE, "busy here", null);
         }
     }
+
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java
index 792fd88bd..5fb01d4b8 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java
@@ -8,11 +8,15 @@ import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper;
 import com.genersoft.iot.vmp.gb28181.dao.RegionMapper;
 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
+import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
+import com.genersoft.iot.vmp.gb28181.service.IDeviceService;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
 import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.streamPush.bean.StreamPush;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import lombok.extern.slf4j.Slf4j;
@@ -22,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
+import javax.sip.message.Response;
 import java.util.*;
 
 @Slf4j
@@ -46,6 +51,9 @@ public class GbChannelServiceImpl implements IGbChannelService {
     @Autowired
     private GroupMapper groupMapper;
 
+    @Autowired
+    private IDeviceChannelService deviceChannelService;
+
     @Override
     public CommonGBChannel queryByDeviceId(String gbDeviceId) {
         return commonGBChannelMapper.queryByDeviceId(gbDeviceId);
@@ -726,4 +734,24 @@ public class GbChannelServiceImpl implements IGbChannelService {
         List<CommonGBChannel> all = commonGBChannelMapper.queryList(query, online,  hasRecordPlan, channelType);
         return new PageInfo<>(all);
     }
+
+    @Override
+    public void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> callback) {
+        if (channel.getGbDeviceDbId() != null) {
+
+            deviceChannelService.queryRecordInfo(channel, startTime, endTime, callback);
+        } else if (channel.getStreamProxyId() != null) {
+            // 拉流代理
+            log.warn("[下载通用通道录像] 不支持下载拉流代理的录像: {}({})", channel.getGbName(), channel.getGbDeviceId());
+            throw new PlayException(Response.FORBIDDEN, "forbidden");
+        } else if (channel.getStreamPushId() != null) {
+            // 推流
+            log.warn("[下载通用通道录像] 不支持下载推流的录像: {}({})", channel.getGbName(), channel.getGbDeviceId());
+            throw new PlayException(Response.FORBIDDEN, "forbidden");
+        } else {
+            // 通道数据异常
+            log.error("[回放通用通道] 通道数据异常,无法识别通道来源: {}({})", channel.getGbName(), channel.getGbDeviceId());
+            throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error");
+        }
+    }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/RecordDataCatch.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/RecordDataCatch.java
index 3f24dbee4..e89cedf2c 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/session/RecordDataCatch.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/RecordDataCatch.java
@@ -1,10 +1,9 @@
 package com.genersoft.iot.vmp.gb28181.session;
 
 import com.genersoft.iot.vmp.gb28181.bean.*;
-import com.genersoft.iot.vmp.gb28181.event.record.RecordEndEventListener;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEventListener;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
-import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
@@ -24,8 +23,9 @@ public class RecordDataCatch {
 
     @Autowired
     private DeferredResultHolder deferredResultHolder;
+
     @Autowired
-    private RecordEndEventListener recordEndEventListener;
+    private RecordInfoEventListener recordEndEventListener;
 
 
     public int put(String deviceId,String channelId, String sn, int sumNum, List<RecordItem> recordItems) {
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java
index 79deb0423..ccb1b2c9c 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java
@@ -4,7 +4,7 @@ import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
 import com.genersoft.iot.vmp.gb28181.bean.Platform;
-import com.genersoft.iot.vmp.gb28181.event.record.RecordEndEventListener;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEventListener;
 import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
 import com.genersoft.iot.vmp.gb28181.service.IDeviceService;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
@@ -52,7 +52,7 @@ public class RecordInfoQueryMessageHandler extends SIPRequestProcessorParent imp
     private SIPCommander commander;
 
     @Autowired
-    private RecordEndEventListener recordEndEventListener;
+    private RecordInfoEventListener recordInfoEventListener;
 
     @Override
     public void afterPropertiesSet() throws Exception {
@@ -126,7 +126,7 @@ public class RecordInfoQueryMessageHandler extends SIPRequestProcessorParent imp
         // 获取通道的原始信息
         DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
         // 接收录像数据
-        recordEndEventListener.addEndEventHandler(device.getDeviceId(), deviceChannel.getDeviceId(), (recordInfo)->{
+        recordInfoEventListener.addEndEventHandler(device.getDeviceId(), deviceChannel.getDeviceId(), (recordInfo)->{
             try {
                 log.info("[国标级联] 录像查询收到数据, 通道: {},准备转发===", channelId);
                 cmderFroPlatform.recordInfo(channel, platform, request.getFromTag(), recordInfo);
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java
index 655c05511..0ab97e87f 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java
@@ -6,6 +6,8 @@ import com.genersoft.iot.vmp.gb28181.bean.Platform;
 import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
 import com.genersoft.iot.vmp.gb28181.bean.RecordItem;
 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEndEvent;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEvent;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
 import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
@@ -19,6 +21,7 @@ import org.dom4j.Element;
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Component;
@@ -48,14 +51,7 @@ public class RecordInfoResponseMessageHandler extends SIPRequestProcessorParent
     private ResponseMessageHandler responseMessageHandler;
 
     @Autowired
-    private DeferredResultHolder deferredResultHolder;
-
-    @Autowired
-    private EventPublisher eventPublisher;
-
-    @Qualifier("taskExecutor")
-    @Autowired
-    private ThreadPoolTaskExecutor taskExecutor;
+    private ApplicationEventPublisher applicationEventPublisher;
 
     @Autowired
     private RedisTemplate<Object, Object> redisTemplate;
@@ -75,88 +71,89 @@ public class RecordInfoResponseMessageHandler extends SIPRequestProcessorParent
         }catch (SipException | InvalidArgumentException | ParseException e) {
             log.error("[命令发送失败] 国标级联 国标录像: {}", e.getMessage());
         }
-        taskExecutor.execute(()->{
-            try {
-                String sn = getText(rootElement, "SN");
-                String channelId = getText(rootElement, "DeviceID");
-                RecordInfo recordInfo = new RecordInfo();
-                recordInfo.setChannelId(channelId);
-                recordInfo.setDeviceId(device.getDeviceId());
-                recordInfo.setSn(sn);
-                recordInfo.setName(getText(rootElement, "Name"));
-                String sumNumStr = getText(rootElement, "SumNum");
-                int sumNum = 0;
-                if (!ObjectUtils.isEmpty(sumNumStr)) {
-                    sumNum = Integer.parseInt(sumNumStr);
-                }
-                recordInfo.setSumNum(sumNum);
-                Element recordListElement = rootElement.element("RecordList");
-                if (recordListElement == null || sumNum == 0) {
-                    log.info("无录像数据");
-                    recordInfo.setCount(sumNum);
-                    eventPublisher.recordEndEventPush(recordInfo);
-                    releaseRequest(device.getDeviceId(), sn,recordInfo);
-                } else {
-                    Iterator<Element> recordListIterator = recordListElement.elementIterator();
-                    if (recordListIterator != null) {
-                        List<RecordItem> recordList = new ArrayList<>();
-                        // 遍历DeviceList
-                        while (recordListIterator.hasNext()) {
-                            Element itemRecord = recordListIterator.next();
-                            Element recordElement = itemRecord.element("DeviceID");
-                            if (recordElement == null) {
-                                log.info("记录为空,下一个...");
-                                continue;
-                            }
-                            RecordItem record = new RecordItem();
-                            record.setDeviceId(getText(itemRecord, "DeviceID"));
-                            record.setName(getText(itemRecord, "Name"));
-                            record.setFilePath(getText(itemRecord, "FilePath"));
-                            record.setFileSize(getText(itemRecord, "FileSize"));
-                            record.setAddress(getText(itemRecord, "Address"));
-
-                            String startTimeStr = getText(itemRecord, "StartTime");
-                            record.setStartTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTimeStr));
-
-                            String endTimeStr = getText(itemRecord, "EndTime");
-                            record.setEndTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTimeStr));
-
-                            record.setSecrecy(itemRecord.element("Secrecy") == null ? 0
-                                    : Integer.parseInt(getText(itemRecord, "Secrecy")));
-                            record.setType(getText(itemRecord, "Type"));
-                            record.setRecorderId(getText(itemRecord, "RecorderID"));
-                            recordList.add(record);
-                        }
-                        Map<String, String> map = recordList.stream()
-                                .filter(record -> record.getDeviceId() != null)
-                                .collect(Collectors.toMap(record -> record.getStartTime()+ record.getEndTime(), UJson::writeJson));
-                        // 获取任务结果数据
-                        String resKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_PRE + channelId + sn;
-                        redisTemplate.opsForHash().putAll(resKey, map);
-                        redisTemplate.expire(resKey, recordInfoTtl, TimeUnit.SECONDS);
-                        String resCountKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_COUNT_PRE + channelId + sn;
-                        long incr = redisTemplate.opsForValue().increment(resCountKey, map.size());
-                        redisTemplate.expire(resCountKey, recordInfoTtl, TimeUnit.SECONDS);
-                        recordInfo.setRecordList(recordList);
-                        recordInfo.setCount(Math.toIntExact(incr));
-                        eventPublisher.recordEndEventPush(recordInfo);
-                        if (incr < sumNum) {
-                            return;
-                        }
-                        // 已接收完成
-                        List<RecordItem> resList = redisTemplate.opsForHash().entries(resKey).values().stream().map(e -> UJson.readJson(e.toString(), RecordItem.class)).collect(Collectors.toList());
-                        if (resList.size() < sumNum) {
-                            return;
-                        }
-                        recordInfo.setRecordList(resList);
-                        releaseRequest(device.getDeviceId(), sn,recordInfo);
-                    }
-                }
-            } catch (Exception e) {
-                log.error("[国标录像] 发现未处理的异常, \r\n{}", evt.getRequest());
-                log.error("[国标录像] 异常内容: ", e);
+        try {
+            String sn = getText(rootElement, "SN");
+            String channelId = getText(rootElement, "DeviceID");
+            RecordInfo recordInfo = new RecordInfo();
+            recordInfo.setChannelId(channelId);
+            recordInfo.setDeviceId(device.getDeviceId());
+            recordInfo.setSn(sn);
+            recordInfo.setName(getText(rootElement, "Name"));
+            String sumNumStr = getText(rootElement, "SumNum");
+            int sumNum = 0;
+            if (!ObjectUtils.isEmpty(sumNumStr)) {
+                sumNum = Integer.parseInt(sumNumStr);
             }
-        });
+            recordInfo.setSumNum(sumNum);
+            Element recordListElement = rootElement.element("RecordList");
+            if (recordListElement == null || sumNum == 0) {
+                log.info("无录像数据");
+                recordInfo.setCount(sumNum);
+                recordInfoEventPush(recordInfo);
+                recordInfoEndEventPush(recordInfo);
+            } else {
+                Iterator<Element> recordListIterator = recordListElement.elementIterator();
+                if (recordListIterator != null) {
+                    List<RecordItem> recordList = new ArrayList<>();
+                    // 遍历DeviceList
+                    while (recordListIterator.hasNext()) {
+                        Element itemRecord = recordListIterator.next();
+                        Element recordElement = itemRecord.element("DeviceID");
+                        if (recordElement == null) {
+                            log.info("记录为空,下一个...");
+                            continue;
+                        }
+                        RecordItem record = new RecordItem();
+                        record.setDeviceId(getText(itemRecord, "DeviceID"));
+                        record.setName(getText(itemRecord, "Name"));
+                        record.setFilePath(getText(itemRecord, "FilePath"));
+                        record.setFileSize(getText(itemRecord, "FileSize"));
+                        record.setAddress(getText(itemRecord, "Address"));
+
+                        String startTimeStr = getText(itemRecord, "StartTime");
+                        record.setStartTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTimeStr));
+
+                        String endTimeStr = getText(itemRecord, "EndTime");
+                        record.setEndTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTimeStr));
+
+                        record.setSecrecy(itemRecord.element("Secrecy") == null ? 0
+                                : Integer.parseInt(getText(itemRecord, "Secrecy")));
+                        record.setType(getText(itemRecord, "Type"));
+                        record.setRecorderId(getText(itemRecord, "RecorderID"));
+                        recordList.add(record);
+                    }
+                    Map<String, String> map = recordList.stream()
+                            .filter(record -> record.getDeviceId() != null)
+                            .collect(Collectors.toMap(record -> record.getStartTime()+ record.getEndTime(), UJson::writeJson));
+                    // 获取任务结果数据
+                    String resKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_PRE + channelId + sn;
+                    redisTemplate.opsForHash().putAll(resKey, map);
+                    redisTemplate.expire(resKey, recordInfoTtl, TimeUnit.SECONDS);
+                    String resCountKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_COUNT_PRE + channelId + sn;
+                    Long incr = redisTemplate.opsForValue().increment(resCountKey, map.size());
+                    if (incr == null) {
+                        incr = 0L;
+                    }
+                    redisTemplate.expire(resCountKey, recordInfoTtl, TimeUnit.SECONDS);
+                    recordInfo.setRecordList(recordList);
+                    recordInfo.setCount(Math.toIntExact(incr));
+                    recordInfoEventPush(recordInfo);
+                    if (incr < sumNum) {
+                        return;
+                    }
+                    // 已接收完成
+                    List<RecordItem> resList = redisTemplate.opsForHash().entries(resKey).values().stream().map(e -> UJson.readJson(e.toString(), RecordItem.class)).collect(Collectors.toList());
+                    if (resList.size() < sumNum) {
+                        return;
+                    }
+                    recordInfo.setRecordList(resList);
+                    recordInfoEndEventPush(recordInfo);
+                }
+            }
+        } catch (Exception e) {
+            log.error("[国标录像] 发现未处理的异常, \r\n{}", evt.getRequest());
+            log.error("[国标录像] 异常内容: ", e);
+        }
     }
 
     @Override
@@ -164,18 +161,31 @@ public class RecordInfoResponseMessageHandler extends SIPRequestProcessorParent
 
     }
 
-    public void releaseRequest(String deviceId, String sn,RecordInfo recordInfo){
-        String key = DeferredResultHolder.CALLBACK_CMD_RECORDINFO + deviceId + sn;
-        // 对数据进行排序
-        if(recordInfo!=null && recordInfo.getRecordList()!=null) {
+    private void recordInfoEventPush(RecordInfo recordInfo) {
+        if (recordInfo == null) {
+            return;
+        }
+        if(recordInfo.getRecordList() != null) {
             Collections.sort(recordInfo.getRecordList());
         }else{
             recordInfo.setRecordList(new ArrayList<>());
         }
+        RecordInfoEvent outEvent = new RecordInfoEvent(this);
+        outEvent.setRecordInfo(recordInfo);
+        applicationEventPublisher.publishEvent(outEvent);
+    }
 
-        RequestMessage msg = new RequestMessage();
-        msg.setKey(key);
-        msg.setData(recordInfo);
-        deferredResultHolder.invokeAllResult(msg);
+    private void recordInfoEndEventPush(RecordInfo recordInfo) {
+        if (recordInfo == null) {
+            return;
+        }
+        if(recordInfo.getRecordList() != null) {
+            Collections.sort(recordInfo.getRecordList());
+        }else{
+            recordInfo.setRecordList(new ArrayList<>());
+        }
+        RecordInfoEndEvent outEvent = new RecordInfoEndEvent(this);
+        outEvent.setRecordInfo(recordInfo);
+        applicationEventPublisher.publishEvent(outEvent);
     }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
index 4c1bc0661..11f80ee86 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
@@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.service.redisMsg;
 
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
+import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 
 public interface IRedisRpcPlayService {
@@ -13,5 +14,8 @@ public interface IRedisRpcPlayService {
 
     void playback(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback<StreamInfo> callback);
 
-    void download(String serverId, Integer id, String startTime, String endTime, int downloadSpeed, ErrorCallback<StreamInfo> callback);
+    void download(String serverId, Integer channelId, String startTime, String endTime, int downloadSpeed, ErrorCallback<StreamInfo> callback);
+
+    void queryRecordInfo(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback<RecordInfo> callback);
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
index f28aa6f11..3eb9c1e31 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
@@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONObject;
 import com.genersoft.iot.vmp.common.InviteInfo;
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.conf.UserSetting;
+import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
@@ -17,6 +18,7 @@ import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
 import com.genersoft.iot.vmp.utils.DateUtil;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -60,14 +62,14 @@ public class RedisRpcChannelPlayController extends RpcController {
         RedisRpcResponse response = request.getResponse();
 
         if (channelId <= 0) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
         // 获取对应的设备和通道信息
         CommonGBChannel channel = channelService.getOne(channelId);
         if (channel == null) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
@@ -76,7 +78,7 @@ public class RedisRpcChannelPlayController extends RpcController {
         inviteInfo.setSessionName("Play");
         channelPlayService.start(channel, inviteInfo, null, (code, msg, data) ->{
             if (code == InviteErrorCode.SUCCESS.getCode()) {
-                response.setStatusCode(Response.OK);
+                response.setStatusCode(ErrorCode.SUCCESS.getCode());
                 response.setBody(data);
             }else {
                 response.setStatusCode(code);
@@ -88,6 +90,50 @@ public class RedisRpcChannelPlayController extends RpcController {
     }
 
 
+    /**
+     * 点播国标设备
+     */
+    @RedisRpcMapping("queryRecordInfo")
+    public RedisRpcResponse queryRecordInfo(RedisRpcRequest request) {
+        JSONObject paramJson = JSONObject.parseObject(request.getParam().toString());
+        int channelId = paramJson.getIntValue("channelId");
+        String startTime = paramJson.getString("startTime");
+        String endTime = paramJson.getString("endTime");
+        RedisRpcResponse response = request.getResponse();
+
+        if (channelId <= 0) {
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
+            response.setBody("param error");
+            return response;
+        }
+        // 获取对应的设备和通道信息
+        CommonGBChannel channel = channelService.getOne(channelId);
+        if (channel == null) {
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
+            response.setBody("param error");
+            return response;
+        }
+        
+        try {
+            channelService.queryRecordInfo(channel, startTime, endTime, (code, msg, data) ->{
+                if (code == InviteErrorCode.SUCCESS.getCode()) {
+                    response.setStatusCode(ErrorCode.SUCCESS.getCode());
+                    response.setBody(data);
+                }else {
+                    response.setStatusCode(code);
+                }
+                // 手动发送结果
+                sendResponse(response);
+            });
+        }catch (ControllerException e) {
+            response.setStatusCode(ErrorCode.ERROR100.getCode());
+            response.setBody(e.getMessage());
+        }
+        
+        return null;
+    }
+
+
     /**
      * 停止点播国标设备
      */
@@ -99,7 +145,7 @@ public class RedisRpcChannelPlayController extends RpcController {
 
         Integer channelId = jsonObject.getIntValue("channelId");
         if (channelId == null || channelId <= 0) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
@@ -110,13 +156,13 @@ public class RedisRpcChannelPlayController extends RpcController {
         // 获取对应的设备和通道信息
         CommonGBChannel channel = channelService.getOne(channelId);
         if (channel == null) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
         try {
             channelPlayService.stopPlay(type, channel, stream);
-            response.setStatusCode(Response.OK);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
         }catch (Exception e){
             response.setStatusCode(Response.SERVER_INTERNAL_ERROR);
             response.setBody(e.getMessage());
@@ -136,14 +182,14 @@ public class RedisRpcChannelPlayController extends RpcController {
         RedisRpcResponse response = request.getResponse();
 
         if (channelId <= 0) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
         // 获取对应的设备和通道信息
         CommonGBChannel channel = channelService.getOne(channelId);
         if (channel == null) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
@@ -154,7 +200,7 @@ public class RedisRpcChannelPlayController extends RpcController {
         inviteInfo.setStopTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime));
         channelPlayService.start(channel, inviteInfo, null, (code, msg, data) ->{
             if (code == InviteErrorCode.SUCCESS.getCode()) {
-                response.setStatusCode(Response.OK);
+                response.setStatusCode(ErrorCode.SUCCESS.getCode());
                 response.setBody(data);
             }else {
                 response.setStatusCode(code);
@@ -178,14 +224,14 @@ public class RedisRpcChannelPlayController extends RpcController {
         RedisRpcResponse response = request.getResponse();
 
         if (channelId <= 0) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
         // 获取对应的设备和通道信息
         CommonGBChannel channel = channelService.getOne(channelId);
         if (channel == null) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
@@ -197,7 +243,7 @@ public class RedisRpcChannelPlayController extends RpcController {
         inviteInfo.setDownloadSpeed(downloadSpeed + "");
         channelPlayService.start(channel, inviteInfo, null, (code, msg, data) ->{
             if (code == InviteErrorCode.SUCCESS.getCode()) {
-                response.setStatusCode(Response.OK);
+                response.setStatusCode(ErrorCode.SUCCESS.getCode());
                 response.setBody(data);
             }else {
                 response.setStatusCode(code);
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
index ee5d1e18e..fa8180252 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
@@ -49,7 +49,7 @@ public class RedisRpcSendRtpController extends RpcController {
         if (sendRtpItem == null) {
             log.info("[redis-rpc] 获取发流的信息, 未找到redis中的发流信息, callId:{}", callId);
             RedisRpcResponse response = request.getResponse();
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
             return response;
         }
         log.info("[redis-rpc] 获取发流的信息: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort());
@@ -57,14 +57,14 @@ public class RedisRpcSendRtpController extends RpcController {
         MediaServer mediaServerItem = mediaServerService.getMediaServerByAppAndStream(sendRtpItem.getApp(), sendRtpItem.getStream());
         if (mediaServerItem == null) {
             RedisRpcResponse response = request.getResponse();
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
         }
         // 自平台内容
         int localPort = sendRtpServerService.getNextPort(mediaServerItem);
         if (localPort == 0) {
             log.info("[redis-rpc] getSendRtpItem->服务器端口资源不足" );
             RedisRpcResponse response = request.getResponse();
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
         }
         // 写入redis, 超时时回复
         sendRtpItem.setStatus(1);
@@ -77,7 +77,7 @@ public class RedisRpcSendRtpController extends RpcController {
         }
         sendRtpServerService.update(sendRtpItem);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
+        response.setStatusCode(ErrorCode.SUCCESS.getCode());
         response.setBody(callId);
         return response;
     }
@@ -90,7 +90,7 @@ public class RedisRpcSendRtpController extends RpcController {
         String callId = request.getParam().toString();
         SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
+        response.setStatusCode(ErrorCode.SUCCESS.getCode());
         if (sendRtpItem == null) {
             log.info("[redis-rpc] 开始发流, 未找到redis中的发流信息, callId:{}", callId);
             WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息");
@@ -134,7 +134,7 @@ public class RedisRpcSendRtpController extends RpcController {
         String callId = request.getParam().toString();
         SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(Response.OK);
+        response.setStatusCode(ErrorCode.SUCCESS.getCode());
         if (sendRtpItem == null) {
             log.info("[redis-rpc] 停止推流, 未找到redis中的发流信息, key:{}", callId);
             WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息");
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
index e1ef57b5c..1cf468905 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
@@ -18,6 +18,7 @@ import com.genersoft.iot.vmp.service.ISendRtpServerService;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
@@ -79,7 +80,7 @@ public class RedisRpcStreamPushController extends RpcController {
             sendRtpServerService.update(sendRtpItem);
             RedisRpcResponse response = request.getResponse();
             response.setBody(sendRtpItem.getChannelId());
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
         }
         // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者
         Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null);
@@ -98,7 +99,7 @@ public class RedisRpcStreamPushController extends RpcController {
             redisTemplate.opsForValue().set(sendRtpItem.getChannelId(), sendRtpItem);
             RedisRpcResponse response = request.getResponse();
             response.setBody(sendRtpItem.getChannelId());
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
             // 手动发送结果
             sendResponse(response);
             hookSubscribe.removeSubscribe(hook);
@@ -120,7 +121,7 @@ public class RedisRpcStreamPushController extends RpcController {
             log.info("[redis-rpc] 监听流上线时发现流已存在直接返回: {}/{}", streamInfo.getApp(), streamInfo.getStream());
             RedisRpcResponse response = request.getResponse();
             response.setBody(JSONObject.toJSONString(streamInfoInServer));
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
             return response;
         }
         // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者
@@ -133,7 +134,7 @@ public class RedisRpcStreamPushController extends RpcController {
                     streamInfo.getApp(), streamInfo.getStream(), hookData.getMediaInfo(),
                     hookData.getMediaInfo() != null ? hookData.getMediaInfo().getCallId() : null);
             response.setBody(JSONObject.toJSONString(streamInfoByAppAndStream));
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
             // 手动发送结果
             sendResponse(response);
             hookSubscribe.removeSubscribe(hook);
@@ -152,7 +153,7 @@ public class RedisRpcStreamPushController extends RpcController {
         Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null);
         hookSubscribe.removeSubscribe(hook);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
+        response.setStatusCode(ErrorCode.SUCCESS.getCode());
         return response;
     }
 
@@ -167,7 +168,7 @@ public class RedisRpcStreamPushController extends RpcController {
         Hook hook = Hook.getInstance(HookType.on_media_arrival, streamInfo.getApp(), streamInfo.getStream(), null);
         hookSubscribe.removeSubscribe(hook);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
+        response.setStatusCode(ErrorCode.SUCCESS.getCode());
         return response;
     }
 
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
index 623aae13d..a22e9dad4 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
@@ -9,6 +9,7 @@ import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
+import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
@@ -48,7 +49,7 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
         if (response == null) {
             callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
         }else {
-            if (response.getStatusCode() == Response.OK) {
+            if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) {
                 StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class);
                 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
             }else {
@@ -69,12 +70,33 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
         if (response == null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg());
         }else {
-            if (response.getStatusCode() != Response.OK) {
+            if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) {
                 throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg());
             }
         }
     }
 
+    @Override
+    public void queryRecordInfo(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback<RecordInfo> callback) {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("channelId", channelId);
+        jsonObject.put("startTime", startTime);
+        jsonObject.put("endTime", endTime);
+        RedisRpcRequest request = buildRequest("channel/queryRecordInfo", jsonObject);
+        request.setToId(serverId);
+        RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getRecordInfoTimeout(), TimeUnit.MILLISECONDS);
+        if (response == null) {
+            callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
+        }else {
+            if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) {
+                RecordInfo recordInfo = JSON.parseObject(response.getBody().toString(), RecordInfo.class);
+                callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), recordInfo);
+            }else {
+                callback.run(response.getStatusCode(), response.getBody().toString(), null);
+            }
+        }
+    }
+
     @Override
     public void playback(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback<StreamInfo> callback) {
 
@@ -88,7 +110,7 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
         if (response == null) {
             callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
         }else {
-            if (response.getStatusCode() == Response.OK) {
+            if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) {
                 StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class);
                 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
             }else {
@@ -111,7 +133,7 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
         if (response == null) {
             callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
         }else {
-            if (response.getStatusCode() == Response.OK) {
+            if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) {
                 StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class);
                 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
             }else {
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java
index e2e3879bd..53b8be605 100755
--- a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java
@@ -9,6 +9,7 @@ public enum ErrorCode {
     ERROR400(400, "参数或方法错误"),
     ERROR404(404, "资源未找到"),
     ERROR403(403, "无权限操作"),
+    ERROR486(486, "超时或无响应"),
     ERROR401(401, "请登录后重新请求"),
     ERROR500(500, "系统异常");
 
diff --git a/src/main/resources/配置详情.yml b/src/main/resources/配置详情.yml
index cef721516..167dd8340 100644
--- a/src/main/resources/配置详情.yml
+++ b/src/main/resources/配置详情.yml
@@ -198,6 +198,8 @@ user-settings:
     save-position-history: false
     # 点播/录像回放 等待超时时间,单位:毫秒
     play-timeout: 18000
+    # 获取设备录像数据超时时间,单位:毫秒
+    record-info-timeout: 10000
     # 上级点播等待超时时间,单位:毫秒
     platform-play-timeout: 60000
     # 是否开启接口鉴权

From af7344225413729e2ccf8f3b83b68dc097fa5bcd Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Mon, 9 Dec 2024 14:27:54 +0800
Subject: [PATCH 02/38] =?UTF-8?q?=E6=8C=87=E5=AE=9A=E8=AF=BB=E5=8F=96?=
 =?UTF-8?q?=E8=A1=8C=E6=94=BF=E5=8C=BA=E5=88=92=E6=96=87=E4=BB=B6=E7=BC=96?=
 =?UTF-8?q?=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java     | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java b/src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java
index 878e01c5d..28e68fb3d 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java
@@ -15,6 +15,7 @@ import java.io.BufferedReader;
 import java.io.File;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 
 /**
@@ -54,7 +55,7 @@ public class CivilCodeFileConf implements CommandLineRunner {
             inputStream = Files.newInputStream(civilCodeFile.toPath());
         }
 
-        BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(inputStream));
+        BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
         int index = -1;
         String line;
         while ((line = inputStreamReader.readLine()) != null) {

From 4354a532bbe430fd776c890232fe31dec70d8469 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Mon, 18 Nov 2024 18:00:21 +0800
Subject: [PATCH 03/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BD=95=E5=88=B6?=
 =?UTF-8?q?=E8=AE=A1=E5=88=92=E7=B1=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../iot/vmp/service/bean/RecordPlan.java      | 27 +++++++++++++++++++
 1 file changed, 27 insertions(+)
 create mode 100644 src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java

diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java
new file mode 100644
index 000000000..0ed21513e
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java
@@ -0,0 +1,27 @@
+package com.genersoft.iot.vmp.service.bean;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@Schema(description = "录制计划")
+public class RecordPlan {
+
+    @Schema(description = "计划数据库ID")
+    private int id;
+
+    @Schema(description = "计划关联的通道ID")
+    private Integer channelId;
+
+    @Schema(description = "计划开始时间")
+    private Long startTime;
+
+    @Schema(description = "计划结束时间")
+    private Long stopTime;
+
+    @Schema(description = "计划周几执行")
+    private Integer weekDay;
+
+    @Schema(description = "是否开启定时截图")
+    private Boolean snap;
+}

From b6bce3b02998455f9402e744905b96e7a7f74756 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Tue, 19 Nov 2024 16:42:11 +0800
Subject: [PATCH 04/38] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BD=95=E5=88=B6?=
 =?UTF-8?q?=E8=AE=A1=E5=88=92=E7=9A=84=E9=A1=B5=E9=9D=A2=E4=B8=8E=E9=80=9A?=
 =?UTF-8?q?=E9=81=93=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../iot/vmp/gb28181/bean/CommonGBChannel.java |   3 +
 .../controller/CommonChannelController.java   |  21 ++
 .../gb28181/dao/CommonGBChannelMapper.java    |   2 +
 .../gb28181/dao/provider/ChannelProvider.java |  32 +++
 .../gb28181/service/IGbChannelService.java    |   3 +
 .../service/impl/GbChannelServiceImpl.java    |  12 +
 .../recordPlan/RecordPlanController.java      |  15 ++
 web_src/src/components/RecordPLan.vue         | 241 ++++++++++++++++++
 web_src/src/layout/UiHeader.vue               |   1 +
 web_src/src/router/index.js                   |   5 +
 数据库/2.7.3/初始化-mysql-2.7.3.sql           |   1 +
 .../2.7.3/初始化-postgresql-kingbase-2.7.3.sql |   1 +
 12 files changed, 337 insertions(+)
 create mode 100644 src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
 create mode 100755 web_src/src/components/RecordPLan.vue

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonGBChannel.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonGBChannel.java
index 6327688b2..ea6066920 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonGBChannel.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CommonGBChannel.java
@@ -126,6 +126,9 @@ public class CommonGBChannel {
     @Schema(description = "关联的国标设备数据库ID")
     private Integer gbDeviceDbId;
 
+    @Schema(description = "二进制保存的录制计划, 每一位表示每个小时的前半个小时")
+    private Long recordPLan;
+
     @Schema(description = "关联的推流Id(流来源是推流时有效)")
     private Integer streamPushId;
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/CommonChannelController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/CommonChannelController.java
index 44d11b0ba..3db52a437 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/CommonChannelController.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/CommonChannelController.java
@@ -101,11 +101,31 @@ public class CommonChannelController {
         return channel;
     }
 
+    @Operation(summary = "获取通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "page", description = "当前页", required = true)
+    @Parameter(name = "count", description = "每页查询数量", required = true)
+    @Parameter(name = "query", description = "查询内容")
+    @Parameter(name = "online", description = "是否在线")
+    @Parameter(name = "hasRecordPlan", description = "是否已设置录制计划")
+    @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理")
+    @GetMapping("/list")
+    public PageInfo<CommonGBChannel> queryList(int page, int count,
+                                                          @RequestParam(required = false) String query,
+                                                          @RequestParam(required = false) Boolean online,
+                                                          @RequestParam(required = false) Boolean hasRecordPlan,
+                                                          @RequestParam(required = false) Integer channelType){
+        if (ObjectUtils.isEmpty(query)){
+            query = null;
+        }
+        return channelService.queryList(page, count, query, online, hasRecordPlan, channelType);
+    }
+
     @Operation(summary = "获取关联行政区划通道列表", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "page", description = "当前页", required = true)
     @Parameter(name = "count", description = "每页查询数量", required = true)
     @Parameter(name = "query", description = "查询内容")
     @Parameter(name = "online", description = "是否在线")
+    @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理")
     @Parameter(name = "civilCode", description = "行政区划")
     @GetMapping("/civilcode/list")
     public PageInfo<CommonGBChannel> queryListByCivilCode(int page, int count,
@@ -124,6 +144,7 @@ public class CommonChannelController {
     @Parameter(name = "count", description = "每页查询数量", required = true)
     @Parameter(name = "query", description = "查询内容")
     @Parameter(name = "online", description = "是否在线")
+    @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理")
     @Parameter(name = "groupDeviceId", description = "业务分组下的父节点ID")
     @GetMapping("/parent/list")
     public PageInfo<CommonGBChannel> queryListByParentId(int page, int count,
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
index 664329e48..99aff9730 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
@@ -457,4 +457,6 @@ public interface CommonGBChannelMapper {
             " </script>"})
     void updateGpsByDeviceIdForStreamPush(List<CommonGBChannel> channels);
 
+    @SelectProvider(type = ChannelProvider.class, method = "queryList")
+    List<CommonGBChannel> queryList(@Param("query") String query, @Param("online") Boolean online, @Param("hasRecordPlan") Boolean hasRecordPlan, @Param("channelType") Integer channelType);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java
index 092c90dc6..3c481cdcb 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java
@@ -17,6 +17,7 @@ public class ChannelProvider {
             "    stream_proxy_id,\n" +
             "    create_time,\n" +
             "    update_time,\n" +
+            "    record_plan,\n" +
             "    coalesce(gb_device_id, device_id) as gb_device_id,\n" +
             "    coalesce(gb_name, name) as gb_name,\n" +
             "    coalesce(gb_manufacturer, manufacturer) as gb_manufacturer,\n" +
@@ -182,6 +183,37 @@ public class ChannelProvider {
         return sqlBuild.toString();
     }
 
+    public String queryList(Map<String, Object> params ){
+        StringBuilder sqlBuild = new StringBuilder();
+        sqlBuild.append(BASE_SQL);
+        sqlBuild.append(" where channel_type = 0 ");
+        if (params.get("query") != null) {
+            sqlBuild.append(" AND (coalesce(gb_device_id, device_id) LIKE concat('%',#{query},'%') escape '/'" +
+                    " OR coalesce(gb_name, name) LIKE concat('%',#{query},'%') escape '/' )")
+            ;
+        }
+        if (params.get("online") != null && (Boolean)params.get("online")) {
+            sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'");
+        }
+        if (params.get("online") != null && !(Boolean)params.get("online")) {
+            sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'");
+        }
+        if (params.get("hasRecordPlan") != null && !(Boolean)params.get("hasRecordPlan")) {
+            sqlBuild.append(" AND record_plan == 0");
+        }
+
+        if (params.get("channelType") != null) {
+            if ((Integer)params.get("channelType") == 0) {
+                sqlBuild.append(" AND device_db_id is not null");
+            }else if ((Integer)params.get("channelType") == 1) {
+                sqlBuild.append(" AND stream_push_id is not null");
+            }else if ((Integer)params.get("channelType") == 2) {
+                sqlBuild.append(" AND stream_proxy_id is not null");
+            }
+        }
+        return sqlBuild.toString();
+    }
+
     public String queryInListByStatus(Map<String, Object> params ){
         StringBuilder sqlBuild = new StringBuilder();
         sqlBuild.append(BASE_SQL);
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java
index 2d8f95315..8dc67df20 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java
@@ -84,4 +84,7 @@ public interface IGbChannelService {
     List<CommonGBChannel> queryListByStreamPushList(List<StreamPush> streamPushList);
 
     void updateGpsByDeviceIdForStreamPush(List<CommonGBChannel> channels);
+
+    PageInfo<CommonGBChannel> queryList(int page, int count, String query, Boolean online, Boolean hasRecordPlan, Integer channelType);
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java
index f19f4f9ce..792fd88bd 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java
@@ -714,4 +714,16 @@ public class GbChannelServiceImpl implements IGbChannelService {
     public void updateGpsByDeviceIdForStreamPush(List<CommonGBChannel> channels) {
         commonGBChannelMapper.updateGpsByDeviceIdForStreamPush(channels);
     }
+
+    @Override
+    public PageInfo<CommonGBChannel> queryList(int page, int count, String query, Boolean online, Boolean hasRecordPlan, Integer channelType) {
+        PageHelper.startPage(page, count);
+        if (query != null) {
+            query = query.replaceAll("/", "//")
+                    .replaceAll("%", "/%")
+                    .replaceAll("_", "/_");
+        }
+        List<CommonGBChannel> all = commonGBChannelMapper.queryList(query, online,  hasRecordPlan, channelType);
+        return new PageInfo<>(all);
+    }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
new file mode 100644
index 000000000..9c8b4687b
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
@@ -0,0 +1,15 @@
+package com.genersoft.iot.vmp.vmanager.recordPlan;
+
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "录制计划")
+@Slf4j
+@RestController
+@RequestMapping("/api/record/plan")
+public class RecordPlanController {
+
+
+}
diff --git a/web_src/src/components/RecordPLan.vue b/web_src/src/components/RecordPLan.vue
new file mode 100755
index 000000000..ed4215fdf
--- /dev/null
+++ b/web_src/src/components/RecordPLan.vue
@@ -0,0 +1,241 @@
+<template>
+  <div id="recordPLan" style="width: 100%">
+    <div class="page-header">
+        <div class="page-title">
+          <div >录像计划</div>
+        </div>
+        <div class="page-header-btn">
+          <div style="display: inline;">
+            搜索:
+            <el-input @input="search" style="margin-right: 1rem; width: auto;" size="mini" placeholder="关键字"
+                      prefix-icon="el-icon-search" v-model="searchSrt" clearable></el-input>
+
+            在线状态:
+            <el-select size="mini" style="width: 8rem; margin-right: 1rem;" @change="search" v-model="online"
+                       placeholder="请选择"
+                       default-first-option>
+              <el-option label="全部" value=""></el-option>
+              <el-option label="在线" value="true"></el-option>
+              <el-option label="离线" value="false"></el-option>
+            </el-select>
+            录制计划:
+            <el-select size="mini" style="width: 8rem; margin-right: 1rem;" @change="search" v-model="hasRecordPlan"
+                       placeholder="请选择"
+                       default-first-option>
+              <el-option label="全部" value=""></el-option>
+              <el-option label="已设置" value="true"></el-option>
+              <el-option label="未设置" value="false"></el-option>
+            </el-select>
+            类型:
+            <el-select size="mini" style="width: 8rem; margin-right: 1rem;" @change="getChannelList"
+                       v-model="channelType" placeholder="请选择"
+                       default-first-option>
+              <el-option label="全部" value=""></el-option>
+              <el-option label="国标设备" :value="0"></el-option>
+              <el-option label="推流设备" :value="1"></el-option>
+              <el-option label="拉流代理" :value="2"></el-option>
+            </el-select>
+            <el-button size="mini" type="primary" @click="add()">
+              按国标设备添加
+            </el-button>
+            <el-button size="mini" type="danger" @click="remove()">
+              按国标设备移除
+            </el-button>
+            <el-button icon="el-icon-refresh-right" circle size="mini" @click="getChannelList()"></el-button>
+          </div>
+        </div>
+      </div>
+      <el-table size="medium" ref="channelListTable" :data="channelList" :height="winHeight" style="width: 100%"
+                header-row-class-name="table-header" >
+        <el-table-column type="selection" width="55" >
+        </el-table-column>
+        <el-table-column prop="gbName" label="名称" min-width="180">
+        </el-table-column>
+        <el-table-column prop="gbDeviceId" label="编号" min-width="180">
+        </el-table-column>
+        <el-table-column prop="gbManufacturer" label="厂家" min-width="100">
+        </el-table-column>
+
+        <el-table-column label="类型" min-width="100">
+          <template v-slot:default="scope">
+            <div slot="reference" class="name-wrapper">
+              <el-tag size="medium" effect="plain" v-if="scope.row.gbDeviceDbId">国标设备</el-tag>
+              <el-tag size="medium" effect="plain" type="success" v-if="scope.row.streamPushId">推流设备</el-tag>
+              <el-tag size="medium" effect="plain" type="warning" v-if="scope.row.streamProxyId">拉流代理</el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" min-width="100">
+          <template v-slot:default="scope">
+            <div slot="reference" class="name-wrapper">
+              <el-tag size="medium" v-if="scope.row.gbStatus === 'ON'">在线</el-tag>
+              <el-tag size="medium" type="info" v-if="scope.row.gbStatus !== 'ON'">离线</el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="录制计划" min-width="100">
+          <template v-slot:default="scope">
+            <div slot="reference" class="name-wrapper">
+              <el-tag size="medium" effect="dark" v-if="scope.row.recordPlan">已设置</el-tag>
+              <el-tag size="medium" effect="dark" v-else>未设置</el-tag>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        style="text-align: right"
+        @size-change="handleSizeChange"
+        @current-change="currentChange"
+        :current-page="currentPage"
+        :page-size="count"
+        :page-sizes="[15, 25, 35, 50]"
+        layout="total, sizes, prev, pager, next"
+        :total="total">
+      </el-pagination>
+  </div>
+</template>
+
+<script>
+import uiHeader from '../layout/UiHeader.vue'
+
+export default {
+  name: 'recordPLan',
+  components: {
+    uiHeader,
+  },
+  data() {
+    return {
+      channelList: [],
+      searchSrt: "",
+      channelType: "",
+      online: "",
+      hasRecordPlan: "",
+      hasGroup: "false",
+      winHeight: window.innerHeight - 180,
+      currentPage: 1,
+      count: 15,
+      total: 0,
+      loading: false,
+      loadSnap: {},
+      groupId: "",
+      businessGroup: "",
+      regionParents: ["请选择虚拟组织"],
+      multipleSelection: []
+    };
+  },
+
+  created() {
+    this.initData();
+  },
+  destroyed() {
+  },
+  methods: {
+    initData: function () {
+      this.getChannelList();
+    },
+    currentChange: function (val) {
+      this.currentPage = val;
+      this.initData();
+    },
+    handleSizeChange: function (val) {
+      this.count = val;
+      this.getChannelList();
+    },
+    getChannelList: function () {
+      this.$axios({
+        method: 'get',
+        url: `/api/common/channel/list`,
+        params: {
+          page: this.currentPage,
+          count: this.count,
+          query: this.searchSrt,
+          online: this.online,
+          hasRecordPlan: this.hasRecordPlan,
+          channelType: this.channelType,
+        }
+      }).then((res) => {
+        if (res.data.code === 0) {
+          this.total = res.data.data.total;
+          this.channelList = res.data.data.list;
+          // 防止出现表格错位
+          this.$nextTick(() => {
+            this.$refs.channelListTable.doLayout();
+          })
+        }
+
+      }).catch((error) => {
+        console.log(error);
+      });
+    },
+    getSnap: function (row) {
+      let baseUrl = window.baseUrl ? window.baseUrl : "";
+      return ((process.env.NODE_ENV === 'development') ? process.env.BASE_API : baseUrl) + '/api/device/query/snap/' + this.deviceId + '/' + row.deviceId;
+    },
+    search: function () {
+      this.currentPage = 1;
+      this.total = 0;
+      this.initData();
+    },
+    refresh: function () {
+      this.initData();
+    },
+    onChannelChange: function (deviceId) {
+      //
+    },
+  }
+};
+</script>
+
+<style>
+.videoList {
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+}
+
+.video-item {
+  position: relative;
+  width: 15rem;
+  height: 10rem;
+  margin-right: 1rem;
+  background-color: #000000;
+}
+
+.video-item-img {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+  width: 100%;
+  height: 100%;
+}
+
+.video-item-img:after {
+  content: "";
+  display: inline-block;
+  position: absolute;
+  z-index: 2;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+  width: 3rem;
+  height: 3rem;
+  background-image: url("../assets/loading.png");
+  background-size: cover;
+  background-color: #000000;
+}
+
+.video-item-title {
+  position: absolute;
+  bottom: 0;
+  color: #000000;
+  background-color: #ffffff;
+  line-height: 1.5rem;
+  padding: 0.3rem;
+  width: 14.4rem;
+}
+</style>
diff --git a/web_src/src/layout/UiHeader.vue b/web_src/src/layout/UiHeader.vue
index 0561bea7f..dda1a8eae 100755
--- a/web_src/src/layout/UiHeader.vue
+++ b/web_src/src/layout/UiHeader.vue
@@ -15,6 +15,7 @@
         <el-menu-item index="/channel/region">行政区划</el-menu-item>
         <el-menu-item index="/channel/group">业务分组</el-menu-item>
       </el-submenu>
+      <el-menu-item index="/recordPlan">录制计划</el-menu-item>
       <el-menu-item index="/cloudRecord">云端录像</el-menu-item>
       <el-menu-item index="/mediaServerManger">节点管理</el-menu-item>
       <el-menu-item index="/platformList/15/1">国标级联</el-menu-item>
diff --git a/web_src/src/router/index.js b/web_src/src/router/index.js
index f85c5a5ab..5d609c4ef 100755
--- a/web_src/src/router/index.js
+++ b/web_src/src/router/index.js
@@ -26,6 +26,7 @@ import rtcPlayer from '../components/dialog/rtcPlayer.vue'
 import region from '../components/region.vue'
 import group from '../components/group.vue'
 import operations from '../components/operations.vue'
+import recordPLan from '../components/RecordPLan.vue'
 
 const originalPush = VueRouter.prototype.push
 VueRouter.prototype.push = function push(location) {
@@ -148,6 +149,10 @@ export default new VueRouter({
           path: '/operations',
           component: operations,
         },
+        {
+          path: '/recordPLan',
+          component: recordPLan,
+        },
         ]
     },
     {
diff --git a/数据库/2.7.3/初始化-mysql-2.7.3.sql b/数据库/2.7.3/初始化-mysql-2.7.3.sql
index d572843dd..0d9322a59 100644
--- a/数据库/2.7.3/初始化-mysql-2.7.3.sql
+++ b/数据库/2.7.3/初始化-mysql-2.7.3.sql
@@ -147,6 +147,7 @@ create table wvp_device_channel
     gb_download_speed            character varying(255),
     gb_svc_space_support_mod     integer,
     gb_svc_time_support_mode     integer,
+    record_plan                  integer,
     stream_push_id               integer,
     stream_proxy_id              integer,
     constraint uk_wvp_device_channel_unique_device_channel unique (device_db_id, device_id),
diff --git a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
index cb06d2eea..223d80aba 100644
--- a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
+++ b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
@@ -163,6 +163,7 @@ create table wvp_device_channel
     gb_download_speed            character varying(255),
     gb_svc_space_support_mod     integer,
     gb_svc_time_support_mode     integer,
+    record_plan                  integer,
     stream_push_id               integer,
     stream_proxy_id              integer,
     constraint uk_wvp_device_channel_unique_device_channel unique (device_db_id, device_id),

From 3da263493f1eeda2803e985c3fb7cd4ab2c74973 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Tue, 19 Nov 2024 18:03:43 +0800
Subject: [PATCH 05/38] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BD=95=E5=88=B6?=
 =?UTF-8?q?=E8=AE=A1=E5=88=92=E8=AE=BE=E7=BD=AE=E9=A1=B5=E9=9D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 web_src/package.json                          |  1 +
 web_src/src/components/RecordPLan.vue         | 14 +++
 .../src/components/dialog/editRecordPlan.vue  | 98 +++++++++++++++++++
 3 files changed, 113 insertions(+)
 create mode 100644 web_src/src/components/dialog/editRecordPlan.vue

diff --git a/web_src/package.json b/web_src/package.json
index c314b9ba8..78582b871 100644
--- a/web_src/package.json
+++ b/web_src/package.json
@@ -15,6 +15,7 @@
     "@liveqing/liveplayer": "^2.7.10",
     "@wchbrad/vue-easy-tree": "^1.0.12",
     "axios": "^0.24.0",
+    "byte-weektime-picker": "^1.1.1",
     "core-js": "^2.6.5",
     "echarts": "^4.9.0",
     "element-ui": "^2.15.14",
diff --git a/web_src/src/components/RecordPLan.vue b/web_src/src/components/RecordPLan.vue
index ed4215fdf..a31917ce9 100755
--- a/web_src/src/components/RecordPLan.vue
+++ b/web_src/src/components/RecordPLan.vue
@@ -81,6 +81,12 @@
             </div>
           </template>
         </el-table-column>
+        <el-table-column label="操作" width="200" fixed="right">
+          <template v-slot:default="scope">
+            <el-button size="medium" icon="el-icon-edit" type="text" v-if="scope.row.recordPlan" @click="edit(scope.row)">编辑</el-button>
+            <el-button size="medium" icon="el-icon-plus" type="text" v-else @click="edit(scope.row)">添加</el-button>
+          </template>
+        </el-table-column>
       </el-table>
       <el-pagination
         style="text-align: right"
@@ -92,15 +98,18 @@
         layout="total, sizes, prev, pager, next"
         :total="total">
       </el-pagination>
+    <editRecordPlan ref="editRecordPlan"></editRecordPlan>
   </div>
 </template>
 
 <script>
 import uiHeader from '../layout/UiHeader.vue'
+import EditRecordPlan from "./dialog/editRecordPlan.vue";
 
 export default {
   name: 'recordPLan',
   components: {
+    EditRecordPlan,
     uiHeader,
   },
   data() {
@@ -182,6 +191,11 @@ export default {
     onChannelChange: function (deviceId) {
       //
     },
+    edit: function (channel) {
+      this.$refs.editRecordPlan.openDialog(channel, ()=>{
+        this.initData()
+      })
+    },
   }
 };
 </script>
diff --git a/web_src/src/components/dialog/editRecordPlan.vue b/web_src/src/components/dialog/editRecordPlan.vue
new file mode 100644
index 000000000..3b54b1b6d
--- /dev/null
+++ b/web_src/src/components/dialog/editRecordPlan.vue
@@ -0,0 +1,98 @@
+<template>
+  <div id="editRecordPlan" v-loading="loading" style="text-align: left;">
+    <el-dialog
+      title="录制计划"
+      width="700px"
+      top="2rem"
+      :close-on-click-modal="false"
+      :visible.sync="showDialog"
+      :destroy-on-close="true"
+      @close="close()"
+    >
+      <div id="shared" style="margin-right: 20px;">
+        <ByteWeektimePicker v-model="byteTime" name="name"/>
+        <el-form status-icon label-width="80px">
+          <el-form-item>
+            <div style="float: right;">
+              <el-button type="primary" @click="onSubmit">保存</el-button>
+              <el-button @click="close">取消</el-button>
+            </div>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { ByteWeektimePicker } from 'byte-weektime-picker'
+
+
+export default {
+  name: "editRecordPlan",
+  props: {},
+  components: {ByteWeektimePicker},
+  created() {
+  },
+  data() {
+    return {
+      value:"",
+      options: [],
+      loading: false,
+      showDialog: false,
+      channel: "",
+      deviceDbId: "",
+      endCallback: "",
+      byteTime: "",
+    };
+  },
+  methods: {
+    openDialog: function (channel, deviceDbId, endCallback) {
+      this.channel = channel;
+      this.deviceDbId = deviceDbId;
+      this.endCallback = endCallback;
+      this.showDialog = true;
+    },
+    onSubmit: function () {
+      this.$axios({
+        method: 'post',
+        url: "/api/user/add",
+        params: {
+          username: this.username,
+          password: this.password,
+          roleId: this.roleId
+        }
+      }).then((res) => {
+        if (res.data.code === 0) {
+          this.$message({
+            showClose: true,
+            message: '添加成功',
+            type: 'success',
+
+          });
+          this.showDialog = false;
+          this.listChangeCallback()
+
+        } else {
+          this.$message({
+            showClose: true,
+            message: res.data.msg,
+            type: 'error'
+          });
+        }
+      }).catch((error) => {
+        console.error(error)
+      });
+    },
+    close: function () {
+      console.log(this.byteTime)
+      this.channel = "";
+      this.deviceDbId = "";
+      this.showDialog = false;
+      if(this.endCallback) {
+        this.endCallback();
+      }
+    },
+  },
+};
+</script>

From c7e33f655efea9d7f564c87b67f6b5b226307217 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Wed, 20 Nov 2024 18:37:39 +0800
Subject: [PATCH 06/38] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B7=BB=E5=8A=A0?=
 =?UTF-8?q?=E5=BD=95=E5=88=B6=E8=AE=A1=E5=88=92=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gb28181/dao/provider/ChannelProvider.java |  6 +-
 .../recordPlan/RecordPlanController.java      | 27 ++++++-
 web_src/src/components/RecordPLan.vue         |  2 +-
 .../src/components/dialog/editRecordPlan.vue  | 79 ++++++++++++++++---
 数据库/2.7.3/初始化-mysql-2.7.3.sql           |  2 +-
 .../2.7.3/初始化-postgresql-kingbase-2.7.3.sql |  2 +-
 6 files changed, 99 insertions(+), 19 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java
index 3c481cdcb..8a941d3c5 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/provider/ChannelProvider.java
@@ -17,7 +17,7 @@ public class ChannelProvider {
             "    stream_proxy_id,\n" +
             "    create_time,\n" +
             "    update_time,\n" +
-            "    record_plan,\n" +
+            "    record_plan_id,\n" +
             "    coalesce(gb_device_id, device_id) as gb_device_id,\n" +
             "    coalesce(gb_name, name) as gb_name,\n" +
             "    coalesce(gb_manufacturer, manufacturer) as gb_manufacturer,\n" +
@@ -198,8 +198,8 @@ public class ChannelProvider {
         if (params.get("online") != null && !(Boolean)params.get("online")) {
             sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'");
         }
-        if (params.get("hasRecordPlan") != null && !(Boolean)params.get("hasRecordPlan")) {
-            sqlBuild.append(" AND record_plan == 0");
+        if (params.get("hasRecordPlan") != null && (Boolean)params.get("hasRecordPlan")) {
+            sqlBuild.append(" AND record_plan_id > 0");
         }
 
         if (params.get("channelType") != null) {
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
index 9c8b4687b..2a2698364 100644
--- a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
@@ -1,9 +1,17 @@
 package com.genersoft.iot.vmp.vmanager.recordPlan;
 
+import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
+import com.genersoft.iot.vmp.service.bean.RecordPlan;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
 
 @Tag(name = "录制计划")
 @Slf4j
@@ -11,5 +19,20 @@ import org.springframework.web.bind.annotation.RestController;
 @RequestMapping("/api/record/plan")
 public class RecordPlanController {
 
+    @ResponseBody
+    @PostMapping("/add")
+    @Operation(summary = "添加录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "channelId", description = "通道ID", required = true)
+    @Parameter(name = "deviceDbId", description = "国标设备ID", required = true)
+    @Parameter(name = "planList", description = "录制计划, 为空则清空计划", required = false)
+    public void openRtpServer(@RequestParam(required = false) Integer channelId, @RequestParam(required = false) Integer deviceDbId, @RequestParam(required = false) List<RecordPlan> planList
 
+    ) {
+        if (channelId == null && deviceDbId == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道ID和国标设备ID不可都为NULL");
+        }
+
+
+
+    }
 }
diff --git a/web_src/src/components/RecordPLan.vue b/web_src/src/components/RecordPLan.vue
index a31917ce9..bba30ea4f 100755
--- a/web_src/src/components/RecordPLan.vue
+++ b/web_src/src/components/RecordPLan.vue
@@ -76,7 +76,7 @@
         <el-table-column label="录制计划" min-width="100">
           <template v-slot:default="scope">
             <div slot="reference" class="name-wrapper">
-              <el-tag size="medium" effect="dark" v-if="scope.row.recordPlan">已设置</el-tag>
+              <el-tag size="medium" effect="dark" v-if="scope.row.recordPlanId">已设置</el-tag>
               <el-tag size="medium" effect="dark" v-else>未设置</el-tag>
             </div>
           </template>
diff --git a/web_src/src/components/dialog/editRecordPlan.vue b/web_src/src/components/dialog/editRecordPlan.vue
index 3b54b1b6d..7f12794f3 100644
--- a/web_src/src/components/dialog/editRecordPlan.vue
+++ b/web_src/src/components/dialog/editRecordPlan.vue
@@ -11,9 +11,9 @@
     >
       <div id="shared" style="margin-right: 20px;">
         <ByteWeektimePicker v-model="byteTime" name="name"/>
-        <el-form status-icon label-width="80px">
+        <el-form >
           <el-form-item>
-            <div style="float: right;">
+            <div style="float: right; margin-top: 20px">
               <el-button type="primary" @click="onSubmit">保存</el-button>
               <el-button @click="close">取消</el-button>
             </div>
@@ -36,7 +36,6 @@ export default {
   },
   data() {
     return {
-      value:"",
       options: [],
       loading: false,
       showDialog: false,
@@ -44,6 +43,7 @@ export default {
       deviceDbId: "",
       endCallback: "",
       byteTime: "",
+      planList: [],
     };
   },
   methods: {
@@ -52,15 +52,22 @@ export default {
       this.deviceDbId = deviceDbId;
       this.endCallback = endCallback;
       this.showDialog = true;
+      this.byteTime= "";
+      if (channel.recordPlanId) {
+        // 请求plan信息
+
+      }
     },
     onSubmit: function () {
+      let planList = this.byteTime2PlanList();
+      console.log(planList)
       this.$axios({
         method: 'post',
-        url: "/api/user/add",
+        url: "/api/record/plan/add",
         params: {
-          username: this.username,
-          password: this.password,
-          roleId: this.roleId
+          channelId: this.channel?this.channel.id:null,
+          deviceDbId: this.deviceDbId,
+          planList: planList
         }
       }).then((res) => {
         if (res.data.code === 0) {
@@ -68,11 +75,9 @@ export default {
             showClose: true,
             message: '添加成功',
             type: 'success',
-
           });
           this.showDialog = false;
-          this.listChangeCallback()
-
+          this.endCallback()
         } else {
           this.$message({
             showClose: true,
@@ -85,7 +90,6 @@ export default {
       });
     },
     close: function () {
-      console.log(this.byteTime)
       this.channel = "";
       this.deviceDbId = "";
       this.showDialog = false;
@@ -93,6 +97,59 @@ export default {
         this.endCallback();
       }
     },
+    byteTime2PlanList() {
+      this.planList = []
+      if (this.byteTime.length === 0) {
+        return;
+      }
+      const DayTimes = 24 * 2;
+      let planList = []
+      let week = 1;
+      // 把 336长度的 list 分成 7 组,每组 48 个
+      for (let i = 0; i < this.byteTime.length; i += DayTimes) {
+        let planArray = this.byteTime2Plan(this.byteTime.slice(i, i + DayTimes));
+        console.log(planArray)
+        if(!planArray || planArray.length === 0) {
+          week ++;
+          continue
+        }
+        for (let j = 0; j < planArray.length; j++) {
+          console.log(planArray[j])
+          planList.push({
+            startTime: planArray[j].startTime,
+            stopTime: planArray[j].stopTime,
+            weekDay: week
+          })
+        }
+        week ++;
+      }
+      return planList
+    },
+    byteTime2Plan(weekItem){
+      let startTime = 0;
+      let endTime = 0;
+      let result = []
+
+      for (let i = 0; i < weekItem.length; i++) {
+        let item = weekItem[i]
+        if (item === '1') {
+          endTime = 30*i
+          if (startTime === 0 ) {
+            startTime = 30*i
+          }
+        } else {
+          if (endTime !== 0){
+            result.push({
+              startTime: startTime * 60 * 1000,
+              stopTime: endTime * 60 * 1000,
+            })
+            startTime = 0
+            endTime = 0
+          }
+        }
+      }
+      return result;
+    }
   },
 };
 </script>
diff --git a/数据库/2.7.3/初始化-mysql-2.7.3.sql b/数据库/2.7.3/初始化-mysql-2.7.3.sql
index 0d9322a59..705eb301d 100644
--- a/数据库/2.7.3/初始化-mysql-2.7.3.sql
+++ b/数据库/2.7.3/初始化-mysql-2.7.3.sql
@@ -147,7 +147,7 @@ create table wvp_device_channel
     gb_download_speed            character varying(255),
     gb_svc_space_support_mod     integer,
     gb_svc_time_support_mode     integer,
-    record_plan                  integer,
+    record_plan_id               integer,
     stream_push_id               integer,
     stream_proxy_id              integer,
     constraint uk_wvp_device_channel_unique_device_channel unique (device_db_id, device_id),
diff --git a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
index 223d80aba..f589329e1 100644
--- a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
+++ b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
@@ -163,7 +163,7 @@ create table wvp_device_channel
     gb_download_speed            character varying(255),
     gb_svc_space_support_mod     integer,
     gb_svc_time_support_mode     integer,
-    record_plan                  integer,
+    record_plan_id               integer,
     stream_push_id               integer,
     stream_proxy_id              integer,
     constraint uk_wvp_device_channel_unique_device_channel unique (device_db_id, device_id),

From 988845f246cfc90ff9cfdcda50b018a125dd42ef Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Wed, 20 Nov 2024 22:33:21 +0800
Subject: [PATCH 07/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE?=
 =?UTF-8?q?=E5=BA=93=E7=BB=93=E6=9E=84=E4=BB=A5=E5=8F=8A=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vmp/gb28181/dao/DeviceChannelMapper.java  |  3 +
 .../service/IDeviceChannelService.java        |  3 +
 .../impl/DeviceChannelServiceImpl.java        | 10 ++
 .../cmd/BroadcastResponseMessageHandler.java  |  2 +
 .../iot/vmp/service/IRecordPlanService.java   | 23 +++++
 .../iot/vmp/service/bean/RecordPlan.java      | 25 +++--
 .../iot/vmp/service/bean/RecordPlanItem.java  | 25 +++++
 .../service/impl/RecordPlanServiceImpl.java   | 79 ++++++++++++++++
 .../vmp/storager/dao/RecordPlanMapper.java    | 40 ++++++++
 .../recordPlan/RecordPlanController.java      | 93 +++++++++++++++++--
 .../recordPlan/bean/RecordPlanParam.java      | 22 +++++
 src/main/resources/index.html                 | 10 ++
 数据库/2.7.3/初始化-mysql-2.7.3.sql           | 20 ++++
 .../2.7.3/初始化-postgresql-kingbase-2.7.3.sql | 20 ++++
 14 files changed, 356 insertions(+), 19 deletions(-)
 create mode 100644 src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
 create mode 100644 src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java
 create mode 100644 src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
 create mode 100644 src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
 create mode 100644 src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java
 create mode 100644 src/main/resources/index.html

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
index a52632354..75131ca60 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
@@ -93,6 +93,9 @@ public interface DeviceChannelMapper {
     @SelectProvider(type = DeviceChannelProvider.class, method = "queryChannelsByDeviceDbId")
     List<DeviceChannel> queryChannelsByDeviceDbId(@Param("deviceDbId") int deviceDbId);
 
+    @Select("select id from wvp_device_channel where device_db_id = #{deviceDbId}")
+    List<Integer> queryChaneIdListByDeviceDbId(@Param("deviceDbId") int deviceDbId);
+
     @Delete("DELETE FROM wvp_device_channel WHERE device_db_id=#{deviceId}")
     int cleanChannelsByDeviceId(@Param("deviceId") int deviceId);
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
index 81d9f9aff..4f0ee8e13 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
@@ -122,4 +122,7 @@ public interface IDeviceChannelService {
 
     DeviceChannel getOneBySourceId(int deviceDbId, String channelId);
 
+    List<DeviceChannel> queryChaneListByDeviceDbId(Integer deviceDbId);
+
+    List<Integer> queryChaneIdListByDeviceDbId(Integer deviceDbId);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
index 4536c1ec8..8fecc7f41 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
@@ -348,6 +348,16 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
         return channelMapper.queryChannelsByDeviceDbId(device.getId());
     }
 
+    @Override
+    public List<DeviceChannel> queryChaneListByDeviceDbId(Integer deviceDbId) {
+        return channelMapper.queryChannelsByDeviceDbId(deviceDbId);
+    }
+
+    @Override
+    public List<Integer> queryChaneIdListByDeviceDbId(Integer deviceDbId) {
+        return channelMapper.queryChaneIdListByDeviceDbId(deviceDbId);
+    }
+
     @Override
     public void updateChannelGPS(Device device, DeviceChannel deviceChannel, MobilePosition mobilePosition) {
         if (userSetting.getSavePositionHistory()) {
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java
index 977482224..2567b7628 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/BroadcastResponseMessageHandler.java
@@ -94,4 +94,6 @@ public class BroadcastResponseMessageHandler extends SIPRequestProcessorParent i
     public void handForPlatform(RequestEvent evt, Platform parentPlatform, Element element) {
 
     }
+
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
new file mode 100644
index 000000000..641ddb180
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
@@ -0,0 +1,23 @@
+package com.genersoft.iot.vmp.service;
+
+import com.genersoft.iot.vmp.service.bean.RecordPlan;
+import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
+import com.github.pagehelper.PageInfo;
+
+import java.util.List;
+
+public interface IRecordPlanService {
+
+
+    RecordPlan get(Integer planId);
+
+    void update(RecordPlan plan);
+
+    void delete(Integer planId);
+
+    PageInfo<RecordPlan> query(Integer page, Integer count, String query);
+
+    void add(RecordPlan plan);
+
+    void linke(List<Integer> channelIds, Integer planId);
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java
index 0ed21513e..5333b2c3e 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlan.java
@@ -3,6 +3,8 @@ package com.genersoft.iot.vmp.service.bean;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.util.List;
+
 @Data
 @Schema(description = "录制计划")
 public class RecordPlan {
@@ -10,18 +12,21 @@ public class RecordPlan {
     @Schema(description = "计划数据库ID")
     private int id;
 
-    @Schema(description = "计划关联的通道ID")
-    private Integer channelId;
+    @Schema(description = "计划名称")
+    private String name;
 
-    @Schema(description = "计划开始时间")
-    private Long startTime;
-
-    @Schema(description = "计划结束时间")
-    private Long stopTime;
-
-    @Schema(description = "计划周几执行")
-    private Integer weekDay;
+    @Schema(description = "计划关联通道数量")
+    private int channelCount;
 
     @Schema(description = "是否开启定时截图")
     private Boolean snap;
+
+    @Schema(description = "创建时间")
+    private String createTime;
+
+    @Schema(description = "更新时间")
+    private String updateTime;
+
+    @Schema(description = "计划内容")
+    private List<RecordPlanItem> planItemList;
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java
new file mode 100644
index 000000000..14383cb15
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java
@@ -0,0 +1,25 @@
+package com.genersoft.iot.vmp.service.bean;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@Schema(description = "录制计划项")
+public class RecordPlanItem {
+
+    @Schema(description = "计划项数据库ID")
+    private int id;
+
+    @Schema(description = "计划开始时间")
+    private Long startTime;
+
+    @Schema(description = "计划结束时间")
+    private Long stopTime;
+
+    @Schema(description = "计划周几执行")
+    private Integer weekDay;
+
+    @Schema(description = "所属计划ID")
+    private Integer planId;
+
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
new file mode 100644
index 000000000..661bea1bc
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
@@ -0,0 +1,79 @@
+package com.genersoft.iot.vmp.service.impl;
+
+import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
+import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper;
+import com.genersoft.iot.vmp.service.IRecordPlanService;
+import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
+import com.genersoft.iot.vmp.service.bean.RecordPlan;
+import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
+import com.genersoft.iot.vmp.storager.dao.RecordPlanMapper;
+import com.genersoft.iot.vmp.utils.DateUtil;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Slf4j
+public class RecordPlanServiceImpl implements IRecordPlanService {
+
+    @Autowired
+    private RecordPlanMapper recordPlanMapper;
+
+    @Autowired
+    private CommonGBChannelMapper channelMapper;
+
+    @Override
+    @Transactional
+    public void add(RecordPlan plan) {
+        plan.setCreateTime(DateUtil.getNow());
+        plan.setUpdateTime(DateUtil.getNow());
+        recordPlanMapper.add(plan);
+        if (plan.getId() > 0) {
+            recordPlanMapper.batchAddItem(plan.getId(), plan.getPlanItemList());
+        }
+    }
+
+    @Override
+    public RecordPlan get(Integer planId) {
+        return recordPlanMapper.get(planId);
+    }
+
+    @Override
+    public void update(RecordPlan plan) {
+        plan.setUpdateTime(DateUtil.getNow());
+        recordPlanMapper.update(plan);
+    }
+
+    @Override
+    public void delete(Integer planId) {
+        recordPlanMapper.delete(planId);
+    }
+
+    @Override
+    public PageInfo<RecordPlan> query(Integer page, Integer count, String query) {
+        PageHelper.startPage(page, count);
+        if (query != null) {
+            query = query.replaceAll("/", "//")
+                    .replaceAll("%", "/%")
+                    .replaceAll("_", "/_");
+        }
+        List<RecordPlan> all = recordPlanMapper.query(query);
+        return new PageInfo<>(all);
+    }
+
+    @Override
+    public void linke(List<Integer> channelIds, Integer planId) {
+        if (planId == null) {
+            log.info("[录制计划] 移除通道关联的计划");
+            channelMapper.removeRecordPlan(channelIds);
+        }else {
+            channelMapper.addRecordPlan(channelIds, planId);
+        }
+
+    }
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
new file mode 100644
index 000000000..cd4f3173d
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
@@ -0,0 +1,40 @@
+package com.genersoft.iot.vmp.storager.dao;
+
+import com.genersoft.iot.vmp.service.bean.RecordPlan;
+import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Options;
+
+import java.util.List;
+
+@Mapper
+public interface RecordPlanMapper {
+
+    @Insert(" <script>" +
+            "INSERT INTO wvp_cloud_record (" +
+            " name," +
+            " snap," +
+            " create_time," +
+            " update_time) " +
+            "VALUES (" +
+            " #{name}," +
+            " #{snap}," +
+            " #{createTime}," +
+            " #{updateTime})" +
+            " </script>")
+    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
+    void add(RecordPlan plan);
+
+    RecordPlan get(Integer planId);
+
+    List<RecordPlan> query(String query);
+
+    void update(RecordPlan plan);
+
+    void delete(Integer planId);
+
+
+    void batchAddItem(int planId, List<RecordPlanItem> planItemList);
+
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
index 2a2698364..6f3fdfa37 100644
--- a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
@@ -2,15 +2,22 @@ package com.genersoft.iot.vmp.vmanager.recordPlan;
 
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.security.JwtUtils;
+import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
+import com.genersoft.iot.vmp.service.IRecordPlanService;
 import com.genersoft.iot.vmp.service.bean.RecordPlan;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.genersoft.iot.vmp.vmanager.recordPlan.bean.RecordPlanParam;
+import com.github.pagehelper.PageInfo;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @Tag(name = "录制计划")
@@ -19,20 +26,88 @@ import java.util.List;
 @RequestMapping("/api/record/plan")
 public class RecordPlanController {
 
+    @Autowired
+    private IRecordPlanService recordPlanService;
+
+    @Autowired
+    private IDeviceChannelService deviceChannelService;
+
+
     @ResponseBody
     @PostMapping("/add")
     @Operation(summary = "添加录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER))
-    @Parameter(name = "channelId", description = "通道ID", required = true)
-    @Parameter(name = "deviceDbId", description = "国标设备ID", required = true)
-    @Parameter(name = "planList", description = "录制计划, 为空则清空计划", required = false)
-    public void openRtpServer(@RequestParam(required = false) Integer channelId, @RequestParam(required = false) Integer deviceDbId, @RequestParam(required = false) List<RecordPlan> planList
+    @Parameter(name = "plan", description = "计划", required = true)
+    public void add(@RequestBody RecordPlan plan) {
+        if (plan.getPlanItemList() == null || plan.getPlanItemList().isEmpty()) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "添加录制计划时,录制计划不可为空");
+        }
+        recordPlanService.add(plan);
+    }
 
-    ) {
-        if (channelId == null && deviceDbId == null) {
+    @ResponseBody
+    @PostMapping("/linke")
+    @Operation(summary = "通道关联录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "param", description = "通道关联录制计划", required = false)
+    public void linke(@RequestBody RecordPlanParam param) {
+        if (param.getChannelId() == null && param.getDeviceDbId() == null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道ID和国标设备ID不可都为NULL");
         }
-
-
-
+        List<Integer> channelIds = new ArrayList<>();
+        if (param.getChannelId() != null) {
+            channelIds.add(param.getChannelId());
+        }else {
+            List<Integer> chanelIdList = deviceChannelService.queryChaneIdListByDeviceDbId(param.getDeviceDbId());
+            if (chanelIdList == null || chanelIdList.isEmpty()) {
+                channelIds = chanelIdList;
+            }
+        }
+        recordPlanService.linke(channelIds, param.getPlanId());
     }
+
+    @ResponseBody
+    @GetMapping("/get")
+    @Operation(summary = "查询录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "planId", description = "计划ID", required = true)
+    public RecordPlan get(Integer planId) {
+        if (planId == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "计划ID不可为NULL");
+        }
+        return recordPlanService.get(planId);
+    }
+
+    @ResponseBody
+    @GetMapping("/query")
+    @Operation(summary = "查询录制计划列表", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "query", description = "检索内容", required = false)
+    @Parameter(name = "page", description = "当前页", required = true)
+    @Parameter(name = "count", description = "每页查询数量", required = true)
+    public PageInfo<RecordPlan> query(@RequestParam(required = false) String query, @RequestParam Integer page, @RequestParam Integer count) {
+        if (query != null && ObjectUtils.isEmpty(query.trim())) {
+            query = null;
+        }
+        return recordPlanService.query(page, count, query);
+    }
+
+    @ResponseBody
+    @PostMapping("/edit")
+    @Operation(summary = "编辑录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "plan", description = "计划", required = true)
+    public void edit(@RequestBody RecordPlan plan) {
+        if (plan == null || plan.getId() == 0) {
+            throw new ControllerException(ErrorCode.ERROR400);
+        }
+        recordPlanService.update(plan);
+    }
+
+    @ResponseBody
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "planId", description = "计划ID", required = true)
+    public void delete(Integer planId) {
+        if (planId == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "计划IDID不可为NULL");
+        }
+        recordPlanService.delete(planId);
+    }
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java
new file mode 100644
index 000000000..11cc1a0ab
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java
@@ -0,0 +1,22 @@
+package com.genersoft.iot.vmp.vmanager.recordPlan.bean;
+
+import com.genersoft.iot.vmp.service.bean.RecordPlan;
+import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Schema(description = "录制计划-添加/编辑参数")
+public class RecordPlanParam {
+
+    @Schema(description = "关联的通道ID")
+    private Integer channelId;
+
+    @Schema(description = "关联的设备ID,会为设备下的所有通道关联此录制计划,channelId存在是此项不生效,")
+    private Integer deviceDbId;
+
+    @Schema(description = "录制计划ID, ID为空是删除关联的计划")
+    private Integer planId;
+}
diff --git a/src/main/resources/index.html b/src/main/resources/index.html
new file mode 100644
index 000000000..9d2fdca2a
--- /dev/null
+++ b/src/main/resources/index.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+</head>
+<body>
+111
+</body>
+</html>
\ No newline at end of file
diff --git a/数据库/2.7.3/初始化-mysql-2.7.3.sql b/数据库/2.7.3/初始化-mysql-2.7.3.sql
index 705eb301d..faada9cd4 100644
--- a/数据库/2.7.3/初始化-mysql-2.7.3.sql
+++ b/数据库/2.7.3/初始化-mysql-2.7.3.sql
@@ -428,3 +428,23 @@ CREATE TABLE wvp_common_region
     constraint uk_common_region_device_id unique (device_id)
 );
 
+create table wvp_record_plan
+(
+    id              serial primary key,
+    snap            bool default false,
+    name            varchar(255) NOT NULL,
+    create_time     character varying(50),
+    update_time     character varying(50)
+);
+
+create table wvp_record_plan_item
+(
+    id              serial primary key,
+    start_time      bigint,
+    stop_time       bigint,
+    week_day        int,
+    plan_id        int,
+    create_time     character varying(50),
+    update_time     character varying(50)
+);
+
diff --git a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
index f589329e1..9ef3e926b 100644
--- a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
+++ b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
@@ -445,3 +445,23 @@ CREATE TABLE wvp_common_region
     constraint uk_common_region_device_id unique (device_id)
 );
 
+create table wvp_record_plan
+(
+    id              serial primary key,
+    snap            bool default false,
+    name            varchar(255) NOT NULL,
+    create_time     character varying(50),
+    update_time     character varying(50)
+);
+
+create table wvp_record_plan_item
+(
+    id              serial primary key,
+    start_time      int8,
+    stop_time       int8,
+    week_day        int,
+    plan_id        int,
+    create_time     character varying(50),
+    update_time     character varying(50)
+);
+

From 0ad1aceeb58f7bdd95439e48748c54788c76577e Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 21 Nov 2024 20:50:22 +0800
Subject: [PATCH 08/38] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/impl/RecordPlanServiceImpl.java   | 10 +++++-
 .../vmp/storager/dao/RecordPlanMapper.java    | 34 ++++++++++++++-----
 2 files changed, 35 insertions(+), 9 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
index 661bea1bc..aa6d922d3 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
@@ -40,7 +40,15 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
 
     @Override
     public RecordPlan get(Integer planId) {
-        return recordPlanMapper.get(planId);
+        RecordPlan recordPlan = recordPlanMapper.get(planId);
+        if (recordPlan == null) {
+            return null;
+        }
+        List<RecordPlanItem> recordPlanItemList = recordPlanMapper.getItemList(planId);
+        if (!recordPlanItemList.isEmpty()) {
+            recordPlan.setPlanItemList(recordPlanItemList);
+        }
+        return recordPlan;
     }
 
     @Override
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
index cd4f3173d..9fb4d588b 100644
--- a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
@@ -2,9 +2,7 @@ package com.genersoft.iot.vmp.storager.dao;
 
 import com.genersoft.iot.vmp.service.bean.RecordPlan;
 import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
-import org.apache.ibatis.annotations.Insert;
-import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Options;
+import org.apache.ibatis.annotations.*;
 
 import java.util.List;
 
@@ -26,15 +24,35 @@ public interface RecordPlanMapper {
     @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
     void add(RecordPlan plan);
 
-    RecordPlan get(Integer planId);
+    @Insert(" <script>" +
+            "INSERT INTO wvp_device_channel (" +
+            "start_time," +
+            "stop_time, " +
+            "week_day," +
+            "create_time," +
+            "update_time) " +
+            "VALUES" +
+            "<foreach collection='commonGBChannels' index='index' item='item' separator=','> " +
+            "(#{item.startTime}, #{item.stopTime}, #{item.weekDay},#{item.planId},#{item.createTime},#{item.updateTime})" +
+            "</foreach> " +
+            " </script>")
+    void batchAddItem(@Param("planId") int planId, List<RecordPlanItem> planItemList);
 
-    List<RecordPlan> query(String query);
+    @Select("select * from wvp_record_plan where  id = #{planId}")
+    RecordPlan get(@Param("planId") Integer planId);
 
+    @Select(" <script>" +
+            "select * from wvp_record_plan where  1=1" +
+            " <if test='query != null'> AND (name LIKE concat('%',#{query},'%') escape '/' )</if> " +
+            " </script>")
+    List<RecordPlan> query(@Param("query") String query);
+
+    @Update("UPDATE wvp_record_plan SET update_time=#{updateTime}, name=#{name}, snap=#{snap} WHERE id=#{id}")
     void update(RecordPlan plan);
 
-    void delete(Integer planId);
+    @Delete("DELETE FROM wvp_record_plan WHERE id=#{id}")
+    void delete(@Param("planId") Integer planId);
 
 
-    void batchAddItem(int planId, List<RecordPlanItem> planItemList);
-
+    List<RecordPlanItem> getItemList(Integer planId);
 }

From a35c1193f2bffe845585f05a4dcbac40063c0e40 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 21 Nov 2024 23:20:56 +0800
Subject: [PATCH 09/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=80=9A=E9=81=93?=
 =?UTF-8?q?=E5=85=B3=E8=81=94=E4=BB=A5=E5=8F=8A=E5=BD=95=E5=88=B6=E8=AE=A1?=
 =?UTF-8?q?=E5=88=92=E6=8E=A5=E5=8F=A3=E5=AE=8C=E5=96=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gb28181/dao/CommonGBChannelMapper.java    |  89 ++++
 .../vmp/gb28181/dao/DeviceChannelMapper.java  |   5 +-
 .../service/IDeviceChannelService.java        |   2 +-
 .../impl/DeviceChannelServiceImpl.java        |   4 +-
 .../impl/PlatformChannelServiceImpl.java      |   5 +
 .../iot/vmp/service/IRecordPlanService.java   |  11 +-
 .../service/impl/RecordPlanServiceImpl.java   |  49 +-
 .../vmp/storager/dao/RecordPlanMapper.java    |  21 +-
 .../recordPlan/RecordPlanController.java      |  56 +-
 .../recordPlan/bean/RecordPlanParam.java      |   7 +-
 web_src/src/components/RecordPLan.vue         | 157 +++---
 .../src/components/dialog/editRecordPlan.vue  | 152 ++++--
 .../components/dialog/linkChannelRecord.vue   | 500 ++++++++++++++++++
 13 files changed, 899 insertions(+), 159 deletions(-)
 create mode 100755 web_src/src/components/dialog/linkChannelRecord.vue

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
index 99aff9730..2f636a4d5 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
@@ -459,4 +459,93 @@ public interface CommonGBChannelMapper {
 
     @SelectProvider(type = ChannelProvider.class, method = "queryList")
     List<CommonGBChannel> queryList(@Param("query") String query, @Param("online") Boolean online, @Param("hasRecordPlan") Boolean hasRecordPlan, @Param("channelType") Integer channelType);
+
+    @Update(value = {" <script>" +
+            " UPDATE wvp_device_channel " +
+            " SET record_plan_id = null" +
+            " WHERE id in "+
+            " <foreach collection='channelIds'  item='item'  open='(' separator=',' close=')' > #{item}</foreach>" +
+            " </script>"})
+    void removeRecordPlan(List<Integer> channelIds);
+
+    @Update(value = {" <script>" +
+            " UPDATE wvp_device_channel " +
+            " SET record_plan_id = #{planId}" +
+            " WHERE id in "+
+            " <foreach collection='channelIds'  item='item'  open='(' separator=',' close=')' > #{item}</foreach>" +
+            " </script>"})
+    void addRecordPlan(List<Integer> channelIds, @Param("planId") Integer planId);
+
+    @Update(value = {" <script>" +
+            " UPDATE wvp_device_channel " +
+            " SET record_plan_id = #{planId}" +
+            " </script>"})
+    void addRecordPlanForAll(@Param("planId") Integer planId);
+
+    @Update(value = {" <script>" +
+            " UPDATE wvp_device_channel " +
+            " SET record_plan_id = null" +
+            " WHERE record_plan_id = #{planId} "+
+            " </script>"})
+    void removeRecordPlanByPlanId( @Param("planId") Integer planId);
+
+
+    @Select("<script>" +
+            " select " +
+            "    wdc.id as gb_id,\n" +
+            "    wdc.device_db_id as gb_device_db_id,\n" +
+            "    wdc.stream_push_id,\n" +
+            "    wdc.stream_proxy_id,\n" +
+            "    wdc.create_time,\n" +
+            "    wdc.update_time,\n" +
+            "    coalesce( wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" +
+            "    coalesce( wdc.gb_name, wdc.name) as gb_name,\n" +
+            "    coalesce( wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" +
+            "    coalesce( wdc.gb_model, wdc.model) as gb_model,\n" +
+            "    coalesce( wdc.gb_owner, wdc.owner) as gb_owner,\n" +
+            "    coalesce( wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" +
+            "    coalesce( wdc.gb_block, wdc.block) as gb_block,\n" +
+            "    coalesce( wdc.gb_address, wdc.address) as gb_address,\n" +
+            "    coalesce( wdc.gb_parental, wdc.parental) as gb_parental,\n" +
+            "    coalesce( wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" +
+            "    coalesce( wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" +
+            "    coalesce( wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" +
+            "    coalesce( wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" +
+            "    coalesce( wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" +
+            "    coalesce( wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" +
+            "    coalesce( wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" +
+            "    coalesce( wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" +
+            "    coalesce( wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" +
+            "    coalesce( wdc.gb_port, wdc.port) as gb_port,\n" +
+            "    coalesce( wdc.gb_password, wdc.password) as gb_password,\n" +
+            "    coalesce( wdc.gb_status, wdc.status) as gb_status,\n" +
+            "    coalesce( wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" +
+            "    coalesce( wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" +
+            "    coalesce( wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" +
+            "    coalesce( wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" +
+            "    coalesce( wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" +
+            "    coalesce( wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" +
+            "    coalesce( wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" +
+            "    coalesce( wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" +
+            "    coalesce( wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" +
+            "    coalesce( wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" +
+            "    coalesce( wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" +
+            "    coalesce( wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" +
+            "    coalesce( wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode \n" +
+            " from wvp_device_channel wdc" +
+            " where wdc.channel_type = 0 " +
+            " <if test='query != null'> " +
+            " AND (coalesce(wdc.gb_device_id, wdc.device_id) LIKE concat('%',#{query},'%') escape '/' " +
+            "      OR coalesce(wdc.gb_name, wdc.name)  LIKE concat('%',#{query},'%') escape '/')</if> " +
+            " <if test='online == true'> AND coalesce(wdc.gb_status, wdc.status) = 'ON'</if> " +
+            " <if test='online == false'> AND coalesce(wdc.gb_status, wdc.status) = 'OFF'</if> " +
+            " <if test='hasLink == true'> AND wdc.record_plan_id = #{planId}</if> " +
+            " <if test='hasLink == false'> AND wdc.record_plan_id is null</if> " +
+            " <if test='channelType == 0'> AND wdc.device_db_id is not null</if> " +
+            " <if test='channelType == 1'> AND wdc.stream_push_id is not null</if> " +
+            " <if test='channelType == 2'> AND wdc.stream_proxy_id is not null</if> " +
+            "</script>")
+    List<CommonGBChannel> queryForRecordPlanForWebList(@Param("planId") Integer planId, @Param("query") String query,
+                                                       @Param("channelType") Integer channelType, @Param("online") Boolean online,
+                                                       @Param("hasLink") Boolean hasLink);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
index 75131ca60..13f1976a8 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
@@ -93,8 +93,9 @@ public interface DeviceChannelMapper {
     @SelectProvider(type = DeviceChannelProvider.class, method = "queryChannelsByDeviceDbId")
     List<DeviceChannel> queryChannelsByDeviceDbId(@Param("deviceDbId") int deviceDbId);
 
-    @Select("select id from wvp_device_channel where device_db_id = #{deviceDbId}")
-    List<Integer> queryChaneIdListByDeviceDbId(@Param("deviceDbId") int deviceDbId);
+    @Select("select id from wvp_device_channel where device_db_id in  " +
+            " <foreach item='item' index='index' collection='deviceDbIds' open='(' separator=',' close=')'> #{item} </foreach> </if>")
+    List<Integer> queryChaneIdListByDeviceDbIds(List<Integer> deviceDbIds);
 
     @Delete("DELETE FROM wvp_device_channel WHERE device_db_id=#{deviceId}")
     int cleanChannelsByDeviceId(@Param("deviceId") int deviceId);
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
index 4f0ee8e13..bb21e3214 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
@@ -124,5 +124,5 @@ public interface IDeviceChannelService {
 
     List<DeviceChannel> queryChaneListByDeviceDbId(Integer deviceDbId);
 
-    List<Integer> queryChaneIdListByDeviceDbId(Integer deviceDbId);
+    List<Integer> queryChaneIdListByDeviceDbIds(List<Integer> deviceDbId);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
index 8fecc7f41..c18510c3f 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
@@ -354,8 +354,8 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
     }
 
     @Override
-    public List<Integer> queryChaneIdListByDeviceDbId(Integer deviceDbId) {
-        return channelMapper.queryChaneIdListByDeviceDbId(deviceDbId);
+    public List<Integer> queryChaneIdListByDeviceDbIds(List<Integer> deviceDbIds) {
+        return channelMapper.queryChaneIdListByDeviceDbIds(deviceDbIds);
     }
 
     @Override
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformChannelServiceImpl.java
index 62125e7c5..6b51054b5 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformChannelServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlatformChannelServiceImpl.java
@@ -52,6 +52,11 @@ public class PlatformChannelServiceImpl implements IPlatformChannelService {
     @Override
     public PageInfo<PlatformChannel> queryChannelList(int page, int count, String query, Integer channelType, Boolean online, Integer platformId, Boolean hasShare) {
         PageHelper.startPage(page, count);
+        if (query != null) {
+            query = query.replaceAll("/", "//")
+                    .replaceAll("%", "/%")
+                    .replaceAll("_", "/_");
+        }
         List<PlatformChannel> all = platformChannelMapper.queryForPlatformForWebList(platformId, query, channelType, online, hasShare);
         return new PageInfo<>(all);
     }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
index 641ddb180..359e0072a 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
@@ -1,7 +1,8 @@
 package com.genersoft.iot.vmp.service;
 
+import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
+import com.genersoft.iot.vmp.gb28181.bean.PlatformChannel;
 import com.genersoft.iot.vmp.service.bean.RecordPlan;
-import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
 import com.github.pagehelper.PageInfo;
 
 import java.util.List;
@@ -19,5 +20,11 @@ public interface IRecordPlanService {
 
     void add(RecordPlan plan);
 
-    void linke(List<Integer> channelIds, Integer planId);
+    void link(List<Integer> channelIds, Integer planId);
+
+    PageInfo<CommonGBChannel> queryChannelList(int page, int count, String query, Integer channelType, Boolean online, Integer planId, Boolean hasLink);
+
+    void linkAll(Integer planId);
+
+    void cleanAll(Integer planId);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
index aa6d922d3..f8f7e9c35 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
@@ -1,6 +1,8 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
+import com.genersoft.iot.vmp.gb28181.bean.PlatformChannel;
 import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper;
 import com.genersoft.iot.vmp.service.IRecordPlanService;
 import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
@@ -8,6 +10,7 @@ import com.genersoft.iot.vmp.service.bean.RecordPlan;
 import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
 import com.genersoft.iot.vmp.storager.dao.RecordPlanMapper;
 import com.genersoft.iot.vmp.utils.DateUtil;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import lombok.extern.slf4j.Slf4j;
@@ -33,9 +36,13 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
         plan.setCreateTime(DateUtil.getNow());
         plan.setUpdateTime(DateUtil.getNow());
         recordPlanMapper.add(plan);
-        if (plan.getId() > 0) {
+        if (plan.getId() > 0 && !plan.getPlanItemList().isEmpty()) {
+            for (RecordPlanItem recordPlanItem : plan.getPlanItemList()) {
+                recordPlanItem.setPlanId(plan.getId());
+            }
             recordPlanMapper.batchAddItem(plan.getId(), plan.getPlanItemList());
         }
+        // TODO  更新录像队列
     }
 
     @Override
@@ -52,14 +59,30 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
     }
 
     @Override
+    @Transactional
     public void update(RecordPlan plan) {
         plan.setUpdateTime(DateUtil.getNow());
         recordPlanMapper.update(plan);
+        recordPlanMapper.cleanItems(plan.getId());
+        if (plan.getPlanItemList() != null){
+            recordPlanMapper.batchAddItem(plan.getId(), plan.getPlanItemList());
+        }
+        // TODO  更新录像队列
+       
     }
 
     @Override
+    @Transactional
     public void delete(Integer planId) {
+        RecordPlan recordPlan = recordPlanMapper.get(planId);
+        if (recordPlan == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "录制计划不存在");
+        }
+        // 清理关联的通道
+        channelMapper.removeRecordPlanByPlanId(recordPlan.getId());
+        recordPlanMapper.cleanItems(planId);
         recordPlanMapper.delete(planId);
+        // TODO  更新录像队列
     }
 
     @Override
@@ -75,13 +98,35 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
     }
 
     @Override
-    public void linke(List<Integer> channelIds, Integer planId) {
+    public void link(List<Integer> channelIds, Integer planId) {
         if (planId == null) {
             log.info("[录制计划] 移除通道关联的计划");
             channelMapper.removeRecordPlan(channelIds);
         }else {
             channelMapper.addRecordPlan(channelIds, planId);
         }
+        // TODO  更新录像队列
+    }
 
+    @Override
+    public PageInfo<CommonGBChannel> queryChannelList(int page, int count, String query, Integer channelType, Boolean online, Integer planId, Boolean hasLink) {
+        PageHelper.startPage(page, count);
+        if (query != null) {
+            query = query.replaceAll("/", "//")
+                    .replaceAll("%", "/%")
+                    .replaceAll("_", "/_");
+        }
+        List<CommonGBChannel> all = channelMapper.queryForRecordPlanForWebList(planId, query, channelType, online, hasLink);
+        return new PageInfo<>(all);
+    }
+
+    @Override
+    public void linkAll(Integer planId) {
+        channelMapper.addRecordPlanForAll(planId);
+    }
+
+    @Override
+    public void cleanAll(Integer planId) {
+        channelMapper.removeRecordPlanByPlanId(planId);
     }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
index 9fb4d588b..705e59aae 100644
--- a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
@@ -10,7 +10,7 @@ import java.util.List;
 public interface RecordPlanMapper {
 
     @Insert(" <script>" +
-            "INSERT INTO wvp_cloud_record (" +
+            "INSERT INTO wvp_record_plan (" +
             " name," +
             " snap," +
             " create_time," +
@@ -25,15 +25,14 @@ public interface RecordPlanMapper {
     void add(RecordPlan plan);
 
     @Insert(" <script>" +
-            "INSERT INTO wvp_device_channel (" +
+            "INSERT INTO wvp_record_plan_item (" +
             "start_time," +
             "stop_time, " +
             "week_day," +
-            "create_time," +
-            "update_time) " +
+            "plan_id) " +
             "VALUES" +
-            "<foreach collection='commonGBChannels' index='index' item='item' separator=','> " +
-            "(#{item.startTime}, #{item.stopTime}, #{item.weekDay},#{item.planId},#{item.createTime},#{item.updateTime})" +
+            "<foreach collection='planItemList' index='index' item='item' separator=','> " +
+            "(#{item.startTime}, #{item.stopTime}, #{item.weekDay},#{planId})" +
             "</foreach> " +
             " </script>")
     void batchAddItem(@Param("planId") int planId, List<RecordPlanItem> planItemList);
@@ -42,7 +41,8 @@ public interface RecordPlanMapper {
     RecordPlan get(@Param("planId") Integer planId);
 
     @Select(" <script>" +
-            "select * from wvp_record_plan where  1=1" +
+            " SELECT wrp.*, (select count(1) from wvp_device_channel where record_plan_id = wrp.id) AS channelCount\n" +
+            " FROM wvp_record_plan wrp where  1=1" +
             " <if test='query != null'> AND (name LIKE concat('%',#{query},'%') escape '/' )</if> " +
             " </script>")
     List<RecordPlan> query(@Param("query") String query);
@@ -50,9 +50,12 @@ public interface RecordPlanMapper {
     @Update("UPDATE wvp_record_plan SET update_time=#{updateTime}, name=#{name}, snap=#{snap} WHERE id=#{id}")
     void update(RecordPlan plan);
 
-    @Delete("DELETE FROM wvp_record_plan WHERE id=#{id}")
+    @Delete("DELETE FROM wvp_record_plan WHERE id=#{planId}")
     void delete(@Param("planId") Integer planId);
 
+    @Select("select * from wvp_record_plan_item where  plan_id = #{planId}")
+    List<RecordPlanItem> getItemList(@Param("planId") Integer planId);
 
-    List<RecordPlanItem> getItemList(Integer planId);
+    @Delete("DELETE FROM wvp_record_plan_item WHERE plan_id = #{planId}")
+    void cleanItems(@Param("planId") Integer planId);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
index 6f3fdfa37..3675acbb8 100644
--- a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
@@ -2,6 +2,8 @@ package com.genersoft.iot.vmp.vmanager.recordPlan;
 
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.security.JwtUtils;
+import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
+import com.genersoft.iot.vmp.gb28181.bean.PlatformChannel;
 import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
 import com.genersoft.iot.vmp.service.IRecordPlanService;
 import com.genersoft.iot.vmp.service.bean.RecordPlan;
@@ -15,6 +17,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.ObjectUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.Assert;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.ArrayList;
@@ -45,23 +48,31 @@ public class RecordPlanController {
     }
 
     @ResponseBody
-    @PostMapping("/linke")
+    @PostMapping("/link")
     @Operation(summary = "通道关联录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "param", description = "通道关联录制计划", required = false)
-    public void linke(@RequestBody RecordPlanParam param) {
-        if (param.getChannelId() == null && param.getDeviceDbId() == null) {
+    public void link(@RequestBody RecordPlanParam param) {
+        if (param.getChannelIds() == null && param.getDeviceDbIds() == null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道ID和国标设备ID不可都为NULL");
         }
+        if (param.getAll() != null) {
+            if (param.getAll()) {
+                recordPlanService.linkAll(param.getPlanId());
+            }else {
+                recordPlanService.cleanAll(param.getPlanId());
+            }
+            return;
+        }
         List<Integer> channelIds = new ArrayList<>();
-        if (param.getChannelId() != null) {
-            channelIds.add(param.getChannelId());
+        if (param.getChannelIds() != null) {
+            channelIds.addAll(param.getChannelIds());
         }else {
-            List<Integer> chanelIdList = deviceChannelService.queryChaneIdListByDeviceDbId(param.getDeviceDbId());
+            List<Integer> chanelIdList = deviceChannelService.queryChaneIdListByDeviceDbIds(param.getDeviceDbIds());
             if (chanelIdList == null || chanelIdList.isEmpty()) {
                 channelIds = chanelIdList;
             }
         }
-        recordPlanService.linke(channelIds, param.getPlanId());
+        recordPlanService.link(channelIds, param.getPlanId());
     }
 
     @ResponseBody
@@ -88,11 +99,36 @@ public class RecordPlanController {
         return recordPlanService.query(page, count, query);
     }
 
+    @Operation(summary = "分页查询级联平台的所有所有通道", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "page", description = "当前页", required = true)
+    @Parameter(name = "count", description = "每页条数", required = true)
+    @Parameter(name = "planId", description = "录制计划ID")
+    @Parameter(name = "channelType", description = "通道类型, 0:国标设备,1:推流设备,2:拉流代理")
+    @Parameter(name = "query", description = "查询内容")
+    @Parameter(name = "online", description = "是否在线")
+    @Parameter(name = "hasLink", description = "是否已经关联")
+    @GetMapping("/channel/list")
     @ResponseBody
-    @PostMapping("/edit")
-    @Operation(summary = "编辑录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    public PageInfo<CommonGBChannel> queryChannelList(int page, int count,
+                                                      @RequestParam(required = false) Integer planId,
+                                                      @RequestParam(required = false) String query,
+                                                      @RequestParam(required = false) Integer channelType,
+                                                      @RequestParam(required = false) Boolean online,
+                                                      @RequestParam(required = false) Boolean hasLink) {
+
+        Assert.notNull(planId, "录制计划ID不可为NULL");
+        if (org.springframework.util.ObjectUtils.isEmpty(query)) {
+            query = null;
+        }
+
+        return recordPlanService.queryChannelList(page, count, query, channelType,  online, planId, hasLink);
+    }
+
+    @ResponseBody
+    @PostMapping("/update")
+    @Operation(summary = "更新录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "plan", description = "计划", required = true)
-    public void edit(@RequestBody RecordPlan plan) {
+    public void update(@RequestBody RecordPlan plan) {
         if (plan == null || plan.getId() == 0) {
             throw new ControllerException(ErrorCode.ERROR400);
         }
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java
index 11cc1a0ab..e74314098 100644
--- a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java
@@ -12,10 +12,13 @@ import java.util.List;
 public class RecordPlanParam {
 
     @Schema(description = "关联的通道ID")
-    private Integer channelId;
+    private List<Integer> channelIds;
 
     @Schema(description = "关联的设备ID,会为设备下的所有通道关联此录制计划,channelId存在是此项不生效,")
-    private Integer deviceDbId;
+    private List<Integer> deviceDbIds;
+
+    @Schema(description = "全部关联/全部取消关联")
+    private Boolean all;
 
     @Schema(description = "录制计划ID, ID为空是删除关联的计划")
     private Integer planId;
diff --git a/web_src/src/components/RecordPLan.vue b/web_src/src/components/RecordPLan.vue
index bba30ea4f..4e649fa55 100755
--- a/web_src/src/components/RecordPLan.vue
+++ b/web_src/src/components/RecordPLan.vue
@@ -9,82 +9,30 @@
             搜索:
             <el-input @input="search" style="margin-right: 1rem; width: auto;" size="mini" placeholder="关键字"
                       prefix-icon="el-icon-search" v-model="searchSrt" clearable></el-input>
-
-            在线状态:
-            <el-select size="mini" style="width: 8rem; margin-right: 1rem;" @change="search" v-model="online"
-                       placeholder="请选择"
-                       default-first-option>
-              <el-option label="全部" value=""></el-option>
-              <el-option label="在线" value="true"></el-option>
-              <el-option label="离线" value="false"></el-option>
-            </el-select>
-            录制计划:
-            <el-select size="mini" style="width: 8rem; margin-right: 1rem;" @change="search" v-model="hasRecordPlan"
-                       placeholder="请选择"
-                       default-first-option>
-              <el-option label="全部" value=""></el-option>
-              <el-option label="已设置" value="true"></el-option>
-              <el-option label="未设置" value="false"></el-option>
-            </el-select>
-            类型:
-            <el-select size="mini" style="width: 8rem; margin-right: 1rem;" @change="getChannelList"
-                       v-model="channelType" placeholder="请选择"
-                       default-first-option>
-              <el-option label="全部" value=""></el-option>
-              <el-option label="国标设备" :value="0"></el-option>
-              <el-option label="推流设备" :value="1"></el-option>
-              <el-option label="拉流代理" :value="2"></el-option>
-            </el-select>
             <el-button size="mini" type="primary" @click="add()">
-              按国标设备添加
+              添加
             </el-button>
-            <el-button size="mini" type="danger" @click="remove()">
-              按国标设备移除
-            </el-button>
-            <el-button icon="el-icon-refresh-right" circle size="mini" @click="getChannelList()"></el-button>
+            <el-button icon="el-icon-refresh-right" circle size="mini" @click="getRecordPlanList()"></el-button>
           </div>
         </div>
       </div>
-      <el-table size="medium" ref="channelListTable" :data="channelList" :height="winHeight" style="width: 100%"
+      <el-table size="medium" ref="recordPlanListTable" :data="recordPlanList" :height="winHeight" style="width: 100%"
                 header-row-class-name="table-header" >
         <el-table-column type="selection" width="55" >
         </el-table-column>
-        <el-table-column prop="gbName" label="名称" min-width="180">
+        <el-table-column prop="name" label="名称" >
         </el-table-column>
-        <el-table-column prop="gbDeviceId" label="编号" min-width="180">
+        <el-table-column prop="channelCount" label="关联通道" >
         </el-table-column>
-        <el-table-column prop="gbManufacturer" label="厂家" min-width="100">
+        <el-table-column prop="updateTime" label="更新时间">
         </el-table-column>
-
-        <el-table-column label="类型" min-width="100">
+        <el-table-column prop="createTime" label="创建时间">
+        </el-table-column>
+        <el-table-column label="操作" width="300" fixed="right">
           <template v-slot:default="scope">
-            <div slot="reference" class="name-wrapper">
-              <el-tag size="medium" effect="plain" v-if="scope.row.gbDeviceDbId">国标设备</el-tag>
-              <el-tag size="medium" effect="plain" type="success" v-if="scope.row.streamPushId">推流设备</el-tag>
-              <el-tag size="medium" effect="plain" type="warning" v-if="scope.row.streamProxyId">拉流代理</el-tag>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="状态" min-width="100">
-          <template v-slot:default="scope">
-            <div slot="reference" class="name-wrapper">
-              <el-tag size="medium" v-if="scope.row.gbStatus === 'ON'">在线</el-tag>
-              <el-tag size="medium" type="info" v-if="scope.row.gbStatus !== 'ON'">离线</el-tag>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="录制计划" min-width="100">
-          <template v-slot:default="scope">
-            <div slot="reference" class="name-wrapper">
-              <el-tag size="medium" effect="dark" v-if="scope.row.recordPlanId">已设置</el-tag>
-              <el-tag size="medium" effect="dark" v-else>未设置</el-tag>
-            </div>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="200" fixed="right">
-          <template v-slot:default="scope">
-            <el-button size="medium" icon="el-icon-edit" type="text" v-if="scope.row.recordPlan" @click="edit(scope.row)">编辑</el-button>
-            <el-button size="medium" icon="el-icon-plus" type="text" v-else @click="edit(scope.row)">添加</el-button>
+            <el-button size="medium" icon="el-icon-link" type="text" @click="link(scope.row)">关联通道</el-button>
+            <el-button size="medium" icon="el-icon-edit" type="text" @click="edit(scope.row)">编辑</el-button>
+            <el-button size="medium" icon="el-icon-delete" style="color: #f56c6c" type="text" @click="deletePlan(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -99,37 +47,31 @@
         :total="total">
       </el-pagination>
     <editRecordPlan ref="editRecordPlan"></editRecordPlan>
+    <LinkChannelRecord ref="linkChannelRecord"></LinkChannelRecord>
   </div>
 </template>
 
 <script>
 import uiHeader from '../layout/UiHeader.vue'
 import EditRecordPlan from "./dialog/editRecordPlan.vue";
+import LinkChannelRecord from "./dialog/linkChannelRecord.vue";
 
 export default {
   name: 'recordPLan',
   components: {
     EditRecordPlan,
+    LinkChannelRecord,
     uiHeader,
   },
   data() {
     return {
-      channelList: [],
+      recordPlanList: [],
       searchSrt: "",
-      channelType: "",
-      online: "",
-      hasRecordPlan: "",
-      hasGroup: "false",
       winHeight: window.innerHeight - 180,
       currentPage: 1,
       count: 15,
       total: 0,
       loading: false,
-      loadSnap: {},
-      groupId: "",
-      businessGroup: "",
-      regionParents: ["请选择虚拟组织"],
-      multipleSelection: []
     };
   },
 
@@ -140,7 +82,7 @@ export default {
   },
   methods: {
     initData: function () {
-      this.getChannelList();
+      this.getRecordPlanList();
     },
     currentChange: function (val) {
       this.currentPage = val;
@@ -148,27 +90,24 @@ export default {
     },
     handleSizeChange: function (val) {
       this.count = val;
-      this.getChannelList();
+      this.getRecordPlanList();
     },
-    getChannelList: function () {
+    getRecordPlanList: function () {
       this.$axios({
         method: 'get',
-        url: `/api/common/channel/list`,
+        url: `/api/record/plan/query`,
         params: {
           page: this.currentPage,
           count: this.count,
           query: this.searchSrt,
-          online: this.online,
-          hasRecordPlan: this.hasRecordPlan,
-          channelType: this.channelType,
         }
       }).then((res) => {
         if (res.data.code === 0) {
           this.total = res.data.data.total;
-          this.channelList = res.data.data.list;
+          this.recordPlanList = res.data.data.list;
           // 防止出现表格错位
           this.$nextTick(() => {
-            this.$refs.channelListTable.doLayout();
+            this.$refs.recordPlanListTable.doLayout();
           })
         }
 
@@ -188,14 +127,56 @@ export default {
     refresh: function () {
       this.initData();
     },
-    onChannelChange: function (deviceId) {
-      //
-    },
-    edit: function (channel) {
-      this.$refs.editRecordPlan.openDialog(channel, ()=>{
+    add: function () {
+      this.$refs.editRecordPlan.openDialog(null, ()=>{
         this.initData()
       })
     },
+    edit: function (plan) {
+      this.$refs.editRecordPlan.openDialog(plan, ()=>{
+        this.initData()
+      })
+    },
+    link: function (plan) {
+      this.$refs.linkChannelRecord.openDialog(plan.id, ()=>{
+        this.initData()
+      })
+    },
+    deletePlan: function (plan) {
+      this.$confirm('确定删除?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.$axios({
+          method: 'delete',
+          url: "/api/record/plan/delete",
+          params: {
+            planId: plan.id,
+          }
+        }).then((res) => {
+          if (res.data.code === 0) {
+            this.$message({
+              showClose: true,
+              message: '删除成功',
+              type: 'success',
+            });
+            this.initData();
+          } else {
+            this.$message({
+              showClose: true,
+              message: res.data.msg,
+              type: 'error'
+            });
+          }
+        }).catch((error) => {
+          console.error(error)
+        });
+      }).catch(() => {
+
+      });
+
+    },
   }
 };
 </script>
diff --git a/web_src/src/components/dialog/editRecordPlan.vue b/web_src/src/components/dialog/editRecordPlan.vue
index 7f12794f3..3772c4909 100644
--- a/web_src/src/components/dialog/editRecordPlan.vue
+++ b/web_src/src/components/dialog/editRecordPlan.vue
@@ -10,8 +10,13 @@
       @close="close()"
     >
       <div id="shared" style="margin-right: 20px;">
-        <ByteWeektimePicker v-model="byteTime" name="name"/>
         <el-form >
+          <el-form-item label="名称">
+            <el-input type="text" v-model="planName"></el-input>
+          </el-form-item>
+          <el-form-item>
+            <ByteWeektimePicker v-model="byteTime" name="name"/>
+          </el-form-item>
           <el-form-item>
             <div style="float: right; margin-top: 20px">
               <el-button type="primary" @click="onSubmit">保存</el-button>
@@ -38,67 +43,111 @@ export default {
     return {
       options: [],
       loading: false,
+      edit: false,
+      planName: null,
+      id: null,
       showDialog: false,
-      channel: "",
-      deviceDbId: "",
       endCallback: "",
       byteTime: "",
-      planList: [],
     };
   },
   methods: {
-    openDialog: function (channel, deviceDbId, endCallback) {
-      this.channel = channel;
-      this.deviceDbId = deviceDbId;
+    openDialog: function (recordPlan, endCallback) {
+      console.log(recordPlan);
       this.endCallback = endCallback;
       this.showDialog = true;
       this.byteTime= "";
-      if (channel.recordPlanId) {
-        // 请求plan信息
+      if (recordPlan) {
+        this.edit = true
+        this.planName = recordPlan.name
+        this.id = recordPlan.id
+        this.$axios({
+          method: 'get',
+          url: "/api/record/plan/get",
+          params: {
+            planId: recordPlan.id,
+          }
+        }).then((res) => {
+          if (res.data.code === 0) {
+            this.byteTime = this.plan2Byte(res.data.data.planItemList)
+          }
+        }).catch((error) => {
+          console.error(error)
+        });
 
       }
     },
     onSubmit: function () {
       let planList = this.byteTime2PlanList();
-      console.log(planList)
-      this.$axios({
-        method: 'post',
-        url: "/api/record/plan/add",
-        params: {
-          channelId: this.channel?this.channel.id:null,
-          deviceDbId: this.deviceDbId,
-          planList: planList
-        }
-      }).then((res) => {
-        if (res.data.code === 0) {
-          this.$message({
-            showClose: true,
-            message: '添加成功',
-            type: 'success',
-          });
-          this.showDialog = false;
-          this.endCallback()
-        } else {
-          this.$message({
-            showClose: true,
-            message: res.data.msg,
-            type: 'error'
-          });
-        }
-      }).catch((error) => {
-        console.error(error)
-      });
+      if (!this.edit) {
+        this.$axios({
+          method: 'post',
+          url: "/api/record/plan/add",
+          data: {
+            name: this.planName,
+            planItemList: planList
+          }
+        }).then((res) => {
+          if (res.data.code === 0) {
+            this.$message({
+              showClose: true,
+              message: '添加成功',
+              type: 'success',
+            });
+            this.showDialog = false;
+            this.endCallback()
+          } else {
+            this.$message({
+              showClose: true,
+              message: res.data.msg,
+              type: 'error'
+            });
+          }
+        }).catch((error) => {
+          console.error(error)
+        });
+      }else {
+        this.$axios({
+          method: 'post',
+          url: "/api/record/plan/update",
+          data: {
+            id: this.id,
+            name: this.planName,
+            planItemList: planList
+          }
+        }).then((res) => {
+          if (res.data.code === 0) {
+            this.$message({
+              showClose: true,
+              message: '更新成功',
+              type: 'success',
+            });
+            this.showDialog = false;
+            this.endCallback()
+          } else {
+            this.$message({
+              showClose: true,
+              message: res.data.msg,
+              type: 'error'
+            });
+          }
+        }).catch((error) => {
+          console.error(error)
+        });
+      }
+
     },
     close: function () {
-      this.channel = "";
-      this.deviceDbId = "";
       this.showDialog = false;
+      this.id = null
+      this.planName = null
+      this.byteTime = ""
+      this.endCallback = ""
       if(this.endCallback) {
         this.endCallback();
       }
     },
     byteTime2PlanList() {
-      this.planList = []
       if (this.byteTime.length === 0) {
         return;
       }
@@ -114,8 +163,8 @@ export default {
           continue
         }
         for (let j = 0; j < planArray.length; j++) {
-          console.log(planArray[j])
           planList.push({
+            id: this.id,
             startTime: planArray[j].startTime,
             stopTime: planArray[j].stopTime,
             weekDay: week
@@ -149,6 +198,27 @@ export default {
         }
       }
       return result;
+    },
+    plan2Byte(planList) {
+      console.log(planList);
+      let byte = ""
+      let indexArray = {}
+      for (let i = 0; i < planList.length; i++) {
+        let index = planList[i].startTime/1000/60/30
+        let endIndex = planList[i].stopTime/1000/60/30
+        for (let j = index; j <= endIndex; j++) {
+          indexArray[j + (planList[i].weekDay - 1 )*48] = j + i*48
+        }
+      }
+      console.log(indexArray)
+      for (let i = 0; i < 336; i++) {
+        if (indexArray[i]){
+          byte += "1"
+        }else {
+          byte += "0"
+        }
+      }
+      return byte
     }
   },
 };
diff --git a/web_src/src/components/dialog/linkChannelRecord.vue b/web_src/src/components/dialog/linkChannelRecord.vue
new file mode 100755
index 000000000..173b5008b
--- /dev/null
+++ b/web_src/src/components/dialog/linkChannelRecord.vue
@@ -0,0 +1,500 @@
+<template>
+  <div id="linkChannelRecord" style="width: 100%;  background-color: #FFFFFF; display: grid; grid-template-columns: 200px auto;">
+    <el-dialog title="通道共享" v-loading="dialogLoading" v-if="showDialog" top="2rem" width="80%" :close-on-click-modal="false" :visible.sync="showDialog" :destroy-on-close="true" @close="close()">
+      <div style="display: grid; grid-template-columns: 100px auto;">
+        <el-tabs tab-position="left" style="" v-model="hasLink" @tab-click="search">
+          <el-tab-pane label="未关联" name="false"></el-tab-pane>
+          <el-tab-pane label="已关联" name="true"></el-tab-pane>
+        </el-tabs>
+        <div>
+          <div class="page-header">
+            <div class="page-header-btn" >
+              <div  style="display: inline;">
+                搜索:
+                <el-input @input="search" style="margin-right: 1rem; width: auto;" size="mini" placeholder="关键字"
+                          prefix-icon="el-icon-search" v-model="searchSrt" clearable></el-input>
+
+                在线状态:
+                <el-select size="mini" style="width: 8rem; margin-right: 1rem;" @change="search" v-model="online" placeholder="请选择"
+                           default-first-option>
+                  <el-option label="全部" value=""></el-option>
+                  <el-option label="在线" value="true"></el-option>
+                  <el-option label="离线" value="false"></el-option>
+                </el-select>
+                类型:
+                <el-select size="mini" style="width: 8rem; margin-right: 1rem;" @change="search" v-model="channelType" placeholder="请选择"
+                           default-first-option>
+                  <el-option label="全部" value=""></el-option>
+                  <el-option label="国标设备" :value="0"></el-option>
+                  <el-option label="推流设备" :value="1"></el-option>
+                  <el-option label="拉流代理" :value="2"></el-option>
+                </el-select>
+                <el-button v-if="hasLink !=='true'" size="mini" type="primary" @click="add()">
+                  添加
+                </el-button>
+                <el-button v-if="hasLink ==='true'" size="mini" type="danger" @click="remove()">
+                  移除
+                </el-button>
+                <el-button size="mini" v-if="hasLink !=='true'" @click="addByDevice()">按设备添加</el-button>
+                <el-button size="mini" v-if="hasLink ==='true'" @click="removeByDevice()">按设备移除</el-button>
+                <el-button size="mini" v-if="hasLink !=='true'" @click="addAll()">全部添加</el-button>
+                <el-button size="mini" v-if="hasLink ==='true'" @click="removeAll()">全部移除</el-button>
+                <el-button size="mini" @click="getChannelList()">刷新</el-button>
+              </div>
+            </div>
+          </div>
+          <el-table size="small"  ref="channelListTable" :data="channelList" :height="winHeight"
+                    header-row-class-name="table-header" @selection-change="handleSelectionChange" >
+            <el-table-column type="selection" width="55" >
+            </el-table-column>
+            <el-table-column prop="gbName" label="名称" min-width="180">
+            </el-table-column>
+            <el-table-column prop="gbDeviceId" label="编号" min-width="180">
+            </el-table-column>
+            <el-table-column prop="gbManufacturer" label="厂家" min-width="100">
+            </el-table-column>
+            <el-table-column label="类型" min-width="100">
+              <template v-slot:default="scope">
+                <div slot="reference" class="name-wrapper">
+                  <el-tag size="medium" effect="plain" v-if="scope.row.gbDeviceDbId">国标设备</el-tag>
+                  <el-tag size="medium" effect="plain" type="success" v-if="scope.row.streamPushId">推流设备</el-tag>
+                  <el-tag size="medium" effect="plain" type="warning" v-if="scope.row.streamProxyId">拉流代理</el-tag>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="状态" min-width="100">
+              <template v-slot:default="scope">
+                <div slot="reference" class="name-wrapper">
+                  <el-tag size="medium" v-if="scope.row.gbStatus === 'ON'">在线</el-tag>
+                  <el-tag size="medium" type="info" v-if="scope.row.gbStatus !== 'ON'">离线</el-tag>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+          <el-pagination
+            style="text-align: right"
+            @size-change="handleSizeChange"
+            @current-change="currentChange"
+            :current-page="currentPage"
+            :page-size="count"
+            :page-sizes="[15, 25, 35, 50]"
+            layout="total, sizes, prev, pager, next"
+            :total="total">
+          </el-pagination>
+          <gbDeviceSelect ref="gbDeviceSelect"></gbDeviceSelect>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+
+import gbDeviceSelect from "./GbDeviceSelect.vue";
+
+export default {
+  name: 'linkChannelRecord',
+  components: {gbDeviceSelect},
+  data() {
+    return {
+      dialogLoading: false,
+      showDialog: false,
+      chooseData: {},
+      channelList: [],
+      searchSrt: "",
+      channelType: "",
+      online: "",
+      hasLink: "false",
+      winHeight: window.innerHeight - 250,
+      currentPage: 1,
+      count: 15,
+      total: 0,
+      loading: false,
+      planId: null,
+      loadSnap: {},
+      multipleSelection: []
+    };
+  },
+
+  created() {
+    this.initData();
+  },
+  destroyed() {},
+  methods: {
+    openDialog(planId, closeCallback) {
+      this.planId = planId
+      this.showDialog = true
+      this.closeCallback = closeCallback
+    },
+    initData: function () {
+      this.getChannelList();
+    },
+    currentChange: function (val) {
+      this.currentPage = val;
+      this.initData();
+    },
+    handleSizeChange: function (val) {
+      this.count = val;
+      this.getChannelList();
+    },
+    getChannelList: function () {
+      this.$axios({
+        method: 'get',
+        url: `/api/record/plan/channel/list`,
+        params: {
+          page: this.currentPage,
+          count: this.count,
+          query: this.searchSrt,
+          online: this.online,
+          channelType: this.channelType,
+          planId: this.planId,
+          hasLink: this.hasLink
+        }
+      }).then((res)=> {
+        if (res.data.code === 0) {
+          this.total = res.data.data.total;
+          this.channelList = res.data.data.list;
+          // 防止出现表格错位
+          this.$nextTick(() => {
+            this.$refs.channelListTable.doLayout();
+          })
+        }
+
+      }).catch((error)=> {
+
+        console.log(error);
+      });
+    },
+    handleSelectionChange: function (val){
+      this.multipleSelection = val;
+    },
+    add: function (row) {
+      let channels = []
+      for (let i = 0; i < this.multipleSelection.length; i++) {
+        channels.push(this.multipleSelection[i].gbId)
+      }
+      if (channels.length === 0) {
+        this.$message.info({
+          showClose: true,
+          message: "请选择通道"
+        })
+        return;
+      }
+      this.loading = true
+
+      this.$axios({
+        method: 'post',
+        url: `/api/record/plan/link`,
+        data: {
+          planId: this.planId,
+          all: true,
+          channelIds: channels
+        }
+      }).then((res)=> {
+        if (res.data.code === 0) {
+          this.$message.success({
+            showClose: true,
+            message: "保存成功"
+          })
+          this.getChannelList()
+        }else {
+          this.$message.error({
+              showClose: true,
+              message: res.data.msg
+            })
+        }
+        this.loading = false
+      }).catch((error)=> {
+        this.$message.error({
+            showClose: true,
+            message: error
+          })
+        this.loading = false
+      });
+    },
+    addAll: function (row) {
+      this.$confirm("确定全部添加?", '提示', {
+        dangerouslyUseHTMLString: true,
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.loading = true
+
+        this.$axios({
+          method: 'post',
+          url: `/api/platform/channel/add`,
+          data: {
+            platformId: this.platformId,
+            all: true
+          }
+        }).then((res)=> {
+          if (res.data.code === 0) {
+            this.$message.success({
+            showClose: true,
+            message: "保存成功"
+          })
+            this.getChannelList()
+          }else {
+            this.$message.error({
+              showClose: true,
+              message: res.data.msg
+            })
+          }
+          this.loading = false
+        }).catch((error)=> {
+          this.$message.error({
+            showClose: true,
+            message: error
+          })
+          this.loading = false
+        });
+      }).catch(() => {
+      });
+    },
+
+    addByDevice: function (row) {
+      this.$refs.gbDeviceSelect.openDialog((rows)=>{
+        let deviceIds = []
+        for (let i = 0; i < rows.length; i++) {
+          deviceIds.push(rows[i].id)
+        }
+        this.$axios({
+          method: 'post',
+          url: `/api/platform/channel/device/add`,
+          data: {
+            platformId: this.platformId,
+            deviceIds: deviceIds,
+          }
+        }).then((res)=> {
+          if (res.data.code === 0) {
+            this.$message.success({
+              showClose: true,
+              message: "保存成功"
+            })
+            this.initData()
+          }else {
+            this.$message.error({
+              showClose: true,
+              message: res.data.msg
+            })
+          }
+        }).catch((error)=> {
+          this.$message.error({
+            showClose: true,
+            message: error
+          })
+          this.loading = false
+        });
+      })
+    },
+
+    removeByDevice: function (row) {
+      this.$refs.gbDeviceSelect.openDialog((rows)=>{
+        let deviceIds = []
+        for (let i = 0; i < rows.length; i++) {
+          deviceIds.push(rows[i].id)
+        }
+        this.$axios({
+          method: 'post',
+          url: `/api/platform/channel/device/remove`,
+          data: {
+            platformId: this.platformId,
+            deviceIds: deviceIds,
+          }
+        }).then((res)=> {
+          if (res.data.code === 0) {
+            this.$message.success({
+              showClose: true,
+              message: "保存成功"
+            })
+            this.initData()
+          }else {
+            this.$message.error({
+              showClose: true,
+              message: res.data.msg
+            })
+          }
+        }).catch((error)=> {
+          this.$message.error({
+            showClose: true,
+            message: error
+          })
+          this.loading = false
+        });
+      })
+    },
+    remove: function (row) {
+      let channels = []
+      for (let i = 0; i < this.multipleSelection.length; i++) {
+        channels.push(this.multipleSelection[i].gbId)
+      }
+      if (channels.length === 0) {
+        this.$message.info({
+          showClose: true,
+          message: "请选择通道"
+        })
+        return;
+      }
+      this.loading = true
+
+      this.$axios({
+        method: 'delete',
+        url: `/api/platform/channel/remove`,
+        data: {
+          platformId: this.platformId,
+          channelIds: channels
+        }
+      }).then((res)=> {
+        if (res.data.code === 0) {
+          this.$message.success({
+            showClose: true,
+            message: "保存成功"
+          })
+          this.getChannelList()
+        }else {
+          this.$message.error({
+              showClose: true,
+              message: res.data.msg
+            })
+        }
+        this.loading = false
+      }).catch((error)=> {
+        this.$message.error({
+            showClose: true,
+            message: error
+          })
+        this.loading = false
+      });
+    },
+    removeAll: function (row) {
+
+      this.$confirm("确定全部移除?", '提示', {
+        dangerouslyUseHTMLString: true,
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.loading = true
+        this.$axios({
+          method: 'delete',
+          url: `/api/platform/channel/remove`,
+          data: {
+            platformId: this.platformId,
+            all: true
+          }
+        }).then((res)=> {
+          if (res.data.code === 0) {
+            this.$message.success({
+            showClose: true,
+            message: "保存成功"
+          })
+            this.getChannelList()
+          }else {
+            this.$message.error({
+              showClose: true,
+              message: res.data.msg
+            })
+          }
+          this.loading = false
+        }).catch((error)=> {
+          this.$message.error({
+            showClose: true,
+            message: error
+          })
+          this.loading = false
+        });
+      }).catch(() => {
+      });
+
+    },
+    saveCustom: function (row) {
+      this.$axios({
+        method: 'post',
+        url: `/api/platform/channel/custom/update`,
+        data: row
+      }).then((res)=> {
+        if (res.data.code === 0) {
+          this.$message.success({
+            showClose: true,
+            message: "保存成功"
+          })
+          this.initData()
+        }else {
+          this.$message.error({
+            showClose: true,
+            message: res.data.msg
+          })
+        }
+      }).catch((error)=> {
+        this.$message.error({
+          showClose: true,
+          message: error
+        })
+      });
+    },
+    search: function () {
+      this.currentPage = 1;
+      this.total = 0;
+      this.initData();
+    },
+    refresh: function () {
+      this.initData();
+    },
+  }
+};
+</script>
+
+<style>
+.videoList {
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+}
+
+.video-item {
+  position: relative;
+  width: 15rem;
+  height: 10rem;
+  margin-right: 1rem;
+  background-color: #000000;
+}
+
+.video-item-img {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+  width: 100%;
+  height: 100%;
+}
+
+.video-item-img:after {
+  content: "";
+  display: inline-block;
+  position: absolute;
+  z-index: 2;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+  width: 3rem;
+  height: 3rem;
+  background-image: url("../../assets/loading.png");
+  background-size: cover;
+  background-color: #000000;
+}
+
+.video-item-title {
+  position: absolute;
+  bottom: 0;
+  color: #000000;
+  background-color: #ffffff;
+  line-height: 1.5rem;
+  padding: 0.3rem;
+  width: 14.4rem;
+}
+</style>

From c2ad767d89505695912af352657d5c88e480459f Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Sat, 23 Nov 2024 14:51:13 +0800
Subject: [PATCH 10/38] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../components/dialog/linkChannelRecord.vue   | 36 ++++++++++---------
 1 file changed, 20 insertions(+), 16 deletions(-)

diff --git a/web_src/src/components/dialog/linkChannelRecord.vue b/web_src/src/components/dialog/linkChannelRecord.vue
index 173b5008b..6f2a299a3 100755
--- a/web_src/src/components/dialog/linkChannelRecord.vue
+++ b/web_src/src/components/dialog/linkChannelRecord.vue
@@ -1,6 +1,6 @@
 <template>
   <div id="linkChannelRecord" style="width: 100%;  background-color: #FFFFFF; display: grid; grid-template-columns: 200px auto;">
-    <el-dialog title="通道共享" v-loading="dialogLoading" v-if="showDialog" top="2rem" width="80%" :close-on-click-modal="false" :visible.sync="showDialog" :destroy-on-close="true" @close="close()">
+    <el-dialog title="通道关联" v-loading="dialogLoading" v-if="showDialog" top="2rem" width="80%" :close-on-click-modal="false" :visible.sync="showDialog" :destroy-on-close="true" @close="close()">
       <div style="display: grid; grid-template-columns: 100px auto;">
         <el-tabs tab-position="left" style="" v-model="hasLink" @tab-click="search">
           <el-tab-pane label="未关联" name="false"></el-tab-pane>
@@ -181,13 +181,17 @@ export default {
         return;
       }
       this.loading = true
+      this.linkPlan({
+        planId: this.planId,
+        channelIds: channels
+      }).cache
+
 
       this.$axios({
         method: 'post',
         url: `/api/record/plan/link`,
         data: {
           planId: this.planId,
-          all: true,
           channelIds: channels
         }
       }).then((res)=> {
@@ -220,20 +224,19 @@ export default {
         type: 'warning'
       }).then(() => {
         this.loading = true
-
         this.$axios({
           method: 'post',
-          url: `/api/platform/channel/add`,
+          url: `/api/record/plan/link`,
           data: {
-            platformId: this.platformId,
+            planId: this.planId,
             all: true
           }
         }).then((res)=> {
           if (res.data.code === 0) {
             this.$message.success({
-            showClose: true,
-            message: "保存成功"
-          })
+              showClose: true,
+              message: "保存成功"
+            })
             this.getChannelList()
           }else {
             this.$message.error({
@@ -261,10 +264,10 @@ export default {
         }
         this.$axios({
           method: 'post',
-          url: `/api/platform/channel/device/add`,
+          url: `/api/record/plan/link`,
           data: {
-            platformId: this.platformId,
-            deviceIds: deviceIds,
+            planId: this.planId,
+            deviceDbIds: deviceIds
           }
         }).then((res)=> {
           if (res.data.code === 0) {
@@ -272,13 +275,14 @@ export default {
               showClose: true,
               message: "保存成功"
             })
-            this.initData()
+            this.getChannelList()
           }else {
             this.$message.error({
               showClose: true,
               message: res.data.msg
             })
           }
+          this.loading = false
         }).catch((error)=> {
           this.$message.error({
             showClose: true,
@@ -297,10 +301,9 @@ export default {
         }
         this.$axios({
           method: 'post',
-          url: `/api/platform/channel/device/remove`,
+          url: `/api/record/plan/link`,
           data: {
-            platformId: this.platformId,
-            deviceIds: deviceIds,
+            deviceDbIds: deviceIds
           }
         }).then((res)=> {
           if (res.data.code === 0) {
@@ -308,13 +311,14 @@ export default {
               showClose: true,
               message: "保存成功"
             })
-            this.initData()
+            this.getChannelList()
           }else {
             this.$message.error({
               showClose: true,
               message: res.data.msg
             })
           }
+          this.loading = false
         }).catch((error)=> {
           this.$message.error({
             showClose: true,

From 812ddd3bbc713a71dfa9f1a652b0aeccfe7a78e3 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Wed, 27 Nov 2024 17:59:54 +0800
Subject: [PATCH 11/38] =?UTF-8?q?[=E5=BD=95=E5=88=B6=E8=AE=A1=E5=88=92]=20?=
 =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=B3=E8=81=94=E9=80=9A=E9=81=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../iot/vmp/gb28181/bean/CatalogData.java     |   1 +
 .../vmp/gb28181/dao/DeviceChannelMapper.java  |   6 +-
 .../gb28181/session/CatalogDataManager.java   |   2 +-
 .../service/impl/RecordPlanServiceImpl.java   |   7 +-
 .../recordPlan/RecordPlanController.java      |  17 +-
 .../recordPlan/bean/RecordPlanParam.java      |   4 +-
 .../components/dialog/linkChannelRecord.vue   | 259 ++++--------------
 7 files changed, 75 insertions(+), 221 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java
index 25407ed4a..fb687b355 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/CatalogData.java
@@ -20,6 +20,7 @@ public class CatalogData {
     private Device device;
     private String errorMsg;
     private Set<String> redisKeysForChannel = new HashSet<>();
+    private Set<String> errorChannel = new HashSet<>();
     private Set<String> redisKeysForRegion = new HashSet<>();
     private Set<String> redisKeysForGroup = new HashSet<>();
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
index 13f1976a8..99bb6e36f 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
@@ -93,8 +93,10 @@ public interface DeviceChannelMapper {
     @SelectProvider(type = DeviceChannelProvider.class, method = "queryChannelsByDeviceDbId")
     List<DeviceChannel> queryChannelsByDeviceDbId(@Param("deviceDbId") int deviceDbId);
 
-    @Select("select id from wvp_device_channel where device_db_id in  " +
-            " <foreach item='item' index='index' collection='deviceDbIds' open='(' separator=',' close=')'> #{item} </foreach> </if>")
+    @Select(value = {" <script> " +
+            "select id from wvp_device_channel where device_db_id in  " +
+            " <foreach item='item' index='index' collection='deviceDbIds' open='(' separator=',' close=')'> #{item} </foreach>" +
+            " </script>"})
     List<Integer> queryChaneIdListByDeviceDbIds(List<Integer> deviceDbIds);
 
     @Delete("DELETE FROM wvp_device_channel WHERE device_db_id=#{deviceId}")
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataManager.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataManager.java
index 049fc00e4..9be9a519a 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataManager.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataManager.java
@@ -283,7 +283,7 @@ public class CatalogDataManager implements CommandLineRunner {
         if (catalogData == null) {
             return 0;
         }
-        return catalogData.getRedisKeysForChannel().size();
+        return catalogData.getRedisKeysForChannel().size() + catalogData.getErrorChannel().size();
     }
 
     public int sumNum(String deviceId, int sn) {
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
index f8f7e9c35..44b49ddde 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
@@ -2,10 +2,8 @@ package com.genersoft.iot.vmp.service.impl;
 
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
-import com.genersoft.iot.vmp.gb28181.bean.PlatformChannel;
 import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper;
 import com.genersoft.iot.vmp.service.IRecordPlanService;
-import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
 import com.genersoft.iot.vmp.service.bean.RecordPlan;
 import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
 import com.genersoft.iot.vmp.storager.dao.RecordPlanMapper;
@@ -99,8 +97,11 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
 
     @Override
     public void link(List<Integer> channelIds, Integer planId) {
+        if (channelIds == null || channelIds.isEmpty()) {
+            log.info("[录制计划] 关联/移除关联时, 通道编号必须存在");
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道编号必须存在");
+        }
         if (planId == null) {
-            log.info("[录制计划] 移除通道关联的计划");
             channelMapper.removeRecordPlan(channelIds);
         }else {
             channelMapper.addRecordPlan(channelIds, planId);
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
index 3675acbb8..f01c11e5f 100644
--- a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/RecordPlanController.java
@@ -3,7 +3,6 @@ package com.genersoft.iot.vmp.vmanager.recordPlan;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
-import com.genersoft.iot.vmp.gb28181.bean.PlatformChannel;
 import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
 import com.genersoft.iot.vmp.service.IRecordPlanService;
 import com.genersoft.iot.vmp.service.bean.RecordPlan;
@@ -50,25 +49,27 @@ public class RecordPlanController {
     @ResponseBody
     @PostMapping("/link")
     @Operation(summary = "通道关联录制计划", security = @SecurityRequirement(name = JwtUtils.HEADER))
-    @Parameter(name = "param", description = "通道关联录制计划", required = false)
+    @Parameter(name = "param", description = "通道关联录制计划", required = true)
     public void link(@RequestBody RecordPlanParam param) {
-        if (param.getChannelIds() == null && param.getDeviceDbIds() == null) {
-            throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道ID和国标设备ID不可都为NULL");
-        }
-        if (param.getAll() != null) {
-            if (param.getAll()) {
+        if (param.getAllLink() != null) {
+            if (param.getAllLink()) {
                 recordPlanService.linkAll(param.getPlanId());
             }else {
                 recordPlanService.cleanAll(param.getPlanId());
             }
             return;
         }
+
+        if (param.getChannelIds() == null && param.getDeviceDbIds() == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道ID和国标设备ID不可都为NULL");
+        }
+
         List<Integer> channelIds = new ArrayList<>();
         if (param.getChannelIds() != null) {
             channelIds.addAll(param.getChannelIds());
         }else {
             List<Integer> chanelIdList = deviceChannelService.queryChaneIdListByDeviceDbIds(param.getDeviceDbIds());
-            if (chanelIdList == null || chanelIdList.isEmpty()) {
+            if (chanelIdList != null && !chanelIdList.isEmpty()) {
                 channelIds = chanelIdList;
             }
         }
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java
index e74314098..9f56a0fbb 100644
--- a/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/recordPlan/bean/RecordPlanParam.java
@@ -1,7 +1,5 @@
 package com.genersoft.iot.vmp.vmanager.recordPlan.bean;
 
-import com.genersoft.iot.vmp.service.bean.RecordPlan;
-import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -18,7 +16,7 @@ public class RecordPlanParam {
     private List<Integer> deviceDbIds;
 
     @Schema(description = "全部关联/全部取消关联")
-    private Boolean all;
+    private Boolean allLink;
 
     @Schema(description = "录制计划ID, ID为空是删除关联的计划")
     private Integer planId;
diff --git a/web_src/src/components/dialog/linkChannelRecord.vue b/web_src/src/components/dialog/linkChannelRecord.vue
index 6f2a299a3..2046c1eb7 100755
--- a/web_src/src/components/dialog/linkChannelRecord.vue
+++ b/web_src/src/components/dialog/linkChannelRecord.vue
@@ -116,17 +116,19 @@ export default {
     };
   },
 
-  created() {
-    this.initData();
-  },
+  created() {},
   destroyed() {},
   methods: {
     openDialog(planId, closeCallback) {
       this.planId = planId
       this.showDialog = true
       this.closeCallback = closeCallback
+      this.initData()
     },
     initData: function () {
+      this.currentPage= 1;
+      this.count= 15;
+      this.total= 0;
       this.getChannelList();
     },
     currentChange: function (val) {
@@ -168,6 +170,36 @@ export default {
     handleSelectionChange: function (val){
       this.multipleSelection = val;
     },
+
+    linkPlan: function (data){
+      this.loading = true
+      return this.$axios({
+        method: 'post',
+        url: `/api/record/plan/link`,
+        data: data
+      }).then((res)=> {
+        if (res.data.code === 0) {
+          this.$message.success({
+            showClose: true,
+            message: "保存成功"
+          })
+          this.getChannelList()
+        }else {
+          this.$message.error({
+            showClose: true,
+            message: res.data.msg
+          })
+        }
+        this.loading = false
+      }).catch((error)=> {
+        this.$message.error({
+          showClose: true,
+          message: error
+        })
+        this.loading = false
+      })
+    },
+
     add: function (row) {
       let channels = []
       for (let i = 0; i < this.multipleSelection.length; i++) {
@@ -180,41 +212,10 @@ export default {
         })
         return;
       }
-      this.loading = true
       this.linkPlan({
         planId: this.planId,
         channelIds: channels
-      }).cache
-
-
-      this.$axios({
-        method: 'post',
-        url: `/api/record/plan/link`,
-        data: {
-          planId: this.planId,
-          channelIds: channels
-        }
-      }).then((res)=> {
-        if (res.data.code === 0) {
-          this.$message.success({
-            showClose: true,
-            message: "保存成功"
-          })
-          this.getChannelList()
-        }else {
-          this.$message.error({
-              showClose: true,
-              message: res.data.msg
-            })
-        }
-        this.loading = false
-      }).catch((error)=> {
-        this.$message.error({
-            showClose: true,
-            message: error
-          })
-        this.loading = false
-      });
+      })
     },
     addAll: function (row) {
       this.$confirm("确定全部添加?", '提示', {
@@ -223,36 +224,11 @@ export default {
         cancelButtonText: '取消',
         type: 'warning'
       }).then(() => {
-        this.loading = true
-        this.$axios({
-          method: 'post',
-          url: `/api/record/plan/link`,
-          data: {
-            planId: this.planId,
-            all: true
-          }
-        }).then((res)=> {
-          if (res.data.code === 0) {
-            this.$message.success({
-              showClose: true,
-              message: "保存成功"
-            })
-            this.getChannelList()
-          }else {
-            this.$message.error({
-              showClose: true,
-              message: res.data.msg
-            })
-          }
-          this.loading = false
-        }).catch((error)=> {
-          this.$message.error({
-            showClose: true,
-            message: error
-          })
-          this.loading = false
-        });
-      }).catch(() => {
+        this.linkPlan({
+          planId: this.planId,
+          allLink: true
+        })
+        }).catch(() => {
       });
     },
 
@@ -262,34 +238,10 @@ export default {
         for (let i = 0; i < rows.length; i++) {
           deviceIds.push(rows[i].id)
         }
-        this.$axios({
-          method: 'post',
-          url: `/api/record/plan/link`,
-          data: {
-            planId: this.planId,
-            deviceDbIds: deviceIds
-          }
-        }).then((res)=> {
-          if (res.data.code === 0) {
-            this.$message.success({
-              showClose: true,
-              message: "保存成功"
-            })
-            this.getChannelList()
-          }else {
-            this.$message.error({
-              showClose: true,
-              message: res.data.msg
-            })
-          }
-          this.loading = false
-        }).catch((error)=> {
-          this.$message.error({
-            showClose: true,
-            message: error
-          })
-          this.loading = false
-        });
+        this.linkPlan({
+          planId: this.planId,
+          deviceDbIds: deviceIds
+        })
       })
     },
 
@@ -299,33 +251,9 @@ export default {
         for (let i = 0; i < rows.length; i++) {
           deviceIds.push(rows[i].id)
         }
-        this.$axios({
-          method: 'post',
-          url: `/api/record/plan/link`,
-          data: {
-            deviceDbIds: deviceIds
-          }
-        }).then((res)=> {
-          if (res.data.code === 0) {
-            this.$message.success({
-              showClose: true,
-              message: "保存成功"
-            })
-            this.getChannelList()
-          }else {
-            this.$message.error({
-              showClose: true,
-              message: res.data.msg
-            })
-          }
-          this.loading = false
-        }).catch((error)=> {
-          this.$message.error({
-            showClose: true,
-            message: error
-          })
-          this.loading = false
-        });
+        this.linkPlan({
+          deviceDbIds: deviceIds
+        })
       })
     },
     remove: function (row) {
@@ -340,36 +268,10 @@ export default {
         })
         return;
       }
-      this.loading = true
 
-      this.$axios({
-        method: 'delete',
-        url: `/api/platform/channel/remove`,
-        data: {
-          platformId: this.platformId,
-          channelIds: channels
-        }
-      }).then((res)=> {
-        if (res.data.code === 0) {
-          this.$message.success({
-            showClose: true,
-            message: "保存成功"
-          })
-          this.getChannelList()
-        }else {
-          this.$message.error({
-              showClose: true,
-              message: res.data.msg
-            })
-        }
-        this.loading = false
-      }).catch((error)=> {
-        this.$message.error({
-            showClose: true,
-            message: error
-          })
-        this.loading = false
-      });
+      this.linkPlan({
+        channelIds: channels
+      })
     },
     removeAll: function (row) {
 
@@ -379,62 +281,11 @@ export default {
         cancelButtonText: '取消',
         type: 'warning'
       }).then(() => {
-        this.loading = true
-        this.$axios({
-          method: 'delete',
-          url: `/api/platform/channel/remove`,
-          data: {
-            platformId: this.platformId,
-            all: true
-          }
-        }).then((res)=> {
-          if (res.data.code === 0) {
-            this.$message.success({
-            showClose: true,
-            message: "保存成功"
-          })
-            this.getChannelList()
-          }else {
-            this.$message.error({
-              showClose: true,
-              message: res.data.msg
-            })
-          }
-          this.loading = false
-        }).catch((error)=> {
-          this.$message.error({
-            showClose: true,
-            message: error
-          })
-          this.loading = false
-        });
-      }).catch(() => {
-      });
-
-    },
-    saveCustom: function (row) {
-      this.$axios({
-        method: 'post',
-        url: `/api/platform/channel/custom/update`,
-        data: row
-      }).then((res)=> {
-        if (res.data.code === 0) {
-          this.$message.success({
-            showClose: true,
-            message: "保存成功"
-          })
-          this.initData()
-        }else {
-          this.$message.error({
-            showClose: true,
-            message: res.data.msg
-          })
-        }
-      }).catch((error)=> {
-        this.$message.error({
-          showClose: true,
-          message: error
+        this.linkPlan({
+          planId: this.planId,
+          allLink: false
         })
+      }).catch(() => {
       });
     },
     search: function () {

From cb12bd731d3c449d359dd1252892b0e7bfeb30df Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Wed, 27 Nov 2024 22:44:22 +0800
Subject: [PATCH 12/38] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../iot/vmp/service/bean/RecordPlanItem.java  |  8 +--
 .../service/impl/RecordPlanServiceImpl.java   | 55 ++++++++++++++++++-
 .../vmp/storager/dao/RecordPlanMapper.java    |  6 +-
 .../src/components/dialog/editRecordPlan.vue  | 47 +++++++++-------
 数据库/2.7.3/初始化-mysql-2.7.3.sql           |  6 +-
 .../2.7.3/初始化-postgresql-kingbase-2.7.3.sql |  4 +-
 6 files changed, 91 insertions(+), 35 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java
index 14383cb15..31fa3214e 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/bean/RecordPlanItem.java
@@ -10,11 +10,11 @@ public class RecordPlanItem {
     @Schema(description = "计划项数据库ID")
     private int id;
 
-    @Schema(description = "计划开始时间")
-    private Long startTime;
+    @Schema(description = "计划开始时间的序号, 从0点开始,每半个小时增加1")
+    private Integer start;
 
-    @Schema(description = "计划结束时间")
-    private Long stopTime;
+    @Schema(description = "计划结束时间的序号, 从0点开始,每半个小时增加1")
+    private Integer stop;
 
     @Schema(description = "计划周几执行")
     private Integer weekDay;
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
index 44b49ddde..dc3de6834 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
@@ -1,8 +1,14 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.genersoft.iot.vmp.common.InviteInfo;
+import com.genersoft.iot.vmp.common.InviteSessionStatus;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
-import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
+import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
+import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper;
+import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
+import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent;
+import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent;
 import com.genersoft.iot.vmp.service.IRecordPlanService;
 import com.genersoft.iot.vmp.service.bean.RecordPlan;
 import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
@@ -13,9 +19,15 @@ import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import java.text.ParseException;
 import java.util.List;
 
 @Service
@@ -28,6 +40,43 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
     @Autowired
     private CommonGBChannelMapper channelMapper;
 
+    @Autowired
+    private IGbChannelService channelService;
+
+
+    /**
+     * 流到来的处理
+     */
+    @Async("taskExecutor")
+    @org.springframework.context.event.EventListener
+    public void onApplicationEvent(MediaArrivalEvent event) {
+
+    }
+
+    /**
+     * 流离开的处理
+     */
+    @Async("taskExecutor")
+    @EventListener
+    public void onApplicationEvent(MediaDepartureEvent event) {
+        // 流断开,检查是否还处于录像状态, 如果是则继续录像
+
+    }
+
+    @Scheduled(cron = "0 */30 * * * *")
+    public void execution() {
+        // 执行计划
+        // 查询startTime等于现在的, 开始录像
+
+        // 查询stopTime等于现在的,结束录像
+        // 查询处于中间的,验证录像是否正在进行
+
+
+        // TODO 无人观看要确保处于录像状态的通道不被移除
+    }
+
+    // 系统启动时
+
     @Override
     @Transactional
     public void add(RecordPlan plan) {
@@ -61,8 +110,8 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
     public void update(RecordPlan plan) {
         plan.setUpdateTime(DateUtil.getNow());
         recordPlanMapper.update(plan);
-        recordPlanMapper.cleanItems(plan.getId());
-        if (plan.getPlanItemList() != null){
+        if (plan.getPlanItemList() != null && !plan.getPlanItemList().isEmpty()){
+            recordPlanMapper.cleanItems(plan.getId());
             recordPlanMapper.batchAddItem(plan.getId(), plan.getPlanItemList());
         }
         // TODO  更新录像队列
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
index 705e59aae..2cdc6468e 100644
--- a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
@@ -26,13 +26,13 @@ public interface RecordPlanMapper {
 
     @Insert(" <script>" +
             "INSERT INTO wvp_record_plan_item (" +
-            "start_time," +
-            "stop_time, " +
+            "start," +
+            "stop, " +
             "week_day," +
             "plan_id) " +
             "VALUES" +
             "<foreach collection='planItemList' index='index' item='item' separator=','> " +
-            "(#{item.startTime}, #{item.stopTime}, #{item.weekDay},#{planId})" +
+            "(#{item.start}, #{item.stop}, #{item.weekDay},#{planId})" +
             "</foreach> " +
             " </script>")
     void batchAddItem(@Param("planId") int planId, List<RecordPlanItem> planItemList);
diff --git a/web_src/src/components/dialog/editRecordPlan.vue b/web_src/src/components/dialog/editRecordPlan.vue
index 3772c4909..5ae900c3b 100644
--- a/web_src/src/components/dialog/editRecordPlan.vue
+++ b/web_src/src/components/dialog/editRecordPlan.vue
@@ -157,7 +157,6 @@ export default {
       // 把 336长度的 list 分成 7 组,每组 48 个
       for (let i = 0; i < this.byteTime.length; i += DayTimes) {
         let planArray = this.byteTime2Plan(this.byteTime.slice(i, i + DayTimes));
-        console.log(planArray)
         if(!planArray || planArray.length === 0) {
           week ++;
           continue
@@ -165,8 +164,8 @@ export default {
         for (let j = 0; j < planArray.length; j++) {
           planList.push({
             id: this.id,
-            startTime: planArray[j].startTime,
-            stopTime: planArray[j].stopTime,
+            start: planArray[j].start,
+            stop: planArray[j].stop,
             weekDay: week
           })
         }
@@ -175,44 +174,52 @@ export default {
       return planList
     },
     byteTime2Plan(weekItem){
-      let startTime = 0;
-      let endTime = 0;
+      let start = null;
+      let stop = null;
       let result = []
 
       for (let i = 0; i < weekItem.length; i++) {
         let item = weekItem[i]
-        if (item === '1') {
-          endTime = 30*i
-          if (startTime === 0 ) {
-            startTime = 30*i
+        if (item === '1') { // 表示选中
+          stop = i
+          if (start === null ) {
+            start = i
+          }
+          if (i === weekItem.length - 1) {
+            result.push({
+              start: start,
+              stop: stop,
+            })
           }
         } else {
-          if (endTime !== 0){
+          if (stop !== 0){
             result.push({
-              startTime: startTime * 60 * 1000,
-              stopTime: endTime * 60 * 1000,
+              start: start,
+              stop: stop,
             })
-            startTime = 0
-            endTime = 0
+            start = 0
+            stop = 0
           }
         }
       }
       return result;
     },
     plan2Byte(planList) {
-      console.log(planList);
       let byte = ""
       let indexArray = {}
       for (let i = 0; i < planList.length; i++) {
-        let index = planList[i].startTime/1000/60/30
-        let endIndex = planList[i].stopTime/1000/60/30
+
+        let weekDay = planList[i].weekDay
+        let index = planList[i].start
+        let endIndex = planList[i].stop
+        console.log(index + "===" + endIndex)
         for (let j = index; j <= endIndex; j++) {
-          indexArray[j + (planList[i].weekDay - 1 )*48] = j + i*48
+          indexArray["key_" + (j + (weekDay - 1 )*48)] = 1
+          console.log("key_" + (j + (weekDay - 1 )*48))
         }
       }
-      console.log(indexArray)
       for (let i = 0; i < 336; i++) {
-        if (indexArray[i]){
+        if (indexArray["key_" + i]){
           byte += "1"
         }else {
           byte += "0"
diff --git a/数据库/2.7.3/初始化-mysql-2.7.3.sql b/数据库/2.7.3/初始化-mysql-2.7.3.sql
index faada9cd4..e12677da5 100644
--- a/数据库/2.7.3/初始化-mysql-2.7.3.sql
+++ b/数据库/2.7.3/初始化-mysql-2.7.3.sql
@@ -440,10 +440,10 @@ create table wvp_record_plan
 create table wvp_record_plan_item
 (
     id              serial primary key,
-    start_time      bigint,
-    stop_time       bigint,
+    start           int,
+    stop            int,
     week_day        int,
-    plan_id        int,
+    plan_id         int,
     create_time     character varying(50),
     update_time     character varying(50)
 );
diff --git a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
index 9ef3e926b..c632f9a83 100644
--- a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
+++ b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
@@ -457,8 +457,8 @@ create table wvp_record_plan
 create table wvp_record_plan_item
 (
     id              serial primary key,
-    start_time      int8,
-    stop_time       int8,
+    start           int,
+    stop            int,
     week_day        int,
     plan_id        int,
     create_time     character varying(50),

From 25008d50c78816095f20f5f14b0b8dead34869d8 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 28 Nov 2024 10:15:03 +0800
Subject: [PATCH 13/38] =?UTF-8?q?[=E5=BD=95=E5=88=B6=E8=AE=A1=E5=88=92]=20?=
 =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=A1=E5=88=92=E9=80=89=E6=8B=A9=E4=B8=8E?=
 =?UTF-8?q?=E5=9B=9E=E6=98=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/impl/RecordPlanServiceImpl.java   | 25 ++++++++++++-------
 .../src/components/dialog/editRecordPlan.vue  | 18 ++++++-------
 2 files changed, 24 insertions(+), 19 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
index dc3de6834..061769b82 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
@@ -1,10 +1,7 @@
 package com.genersoft.iot.vmp.service.impl;
 
-import com.genersoft.iot.vmp.common.InviteInfo;
-import com.genersoft.iot.vmp.common.InviteSessionStatus;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
-import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
-import com.genersoft.iot.vmp.gb28181.bean.*;
+import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
 import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent;
@@ -25,9 +22,7 @@ import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.sip.InvalidArgumentException;
-import javax.sip.SipException;
-import java.text.ParseException;
+import java.util.ArrayList;
 import java.util.List;
 
 @Service
@@ -110,9 +105,21 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
     public void update(RecordPlan plan) {
         plan.setUpdateTime(DateUtil.getNow());
         recordPlanMapper.update(plan);
+        recordPlanMapper.cleanItems(plan.getId());
         if (plan.getPlanItemList() != null && !plan.getPlanItemList().isEmpty()){
-            recordPlanMapper.cleanItems(plan.getId());
-            recordPlanMapper.batchAddItem(plan.getId(), plan.getPlanItemList());
+            List<RecordPlanItem> planItemList = new ArrayList<>();
+            for (RecordPlanItem recordPlanItem : plan.getPlanItemList()) {
+                if (recordPlanItem.getStart() == null || recordPlanItem.getStop() == null || recordPlanItem.getWeekDay() == null){
+                    continue;
+                }
+                if (recordPlanItem.getPlanId() == null) {
+                    recordPlanItem.setPlanId(plan.getId());
+                }
+                planItemList.add(recordPlanItem);
+            }
+            if(!planItemList.isEmpty()) {
+                recordPlanMapper.batchAddItem(plan.getId(), planItemList);
+            }
         }
         // TODO  更新录像队列
        
diff --git a/web_src/src/components/dialog/editRecordPlan.vue b/web_src/src/components/dialog/editRecordPlan.vue
index 5ae900c3b..1de115cca 100644
--- a/web_src/src/components/dialog/editRecordPlan.vue
+++ b/web_src/src/components/dialog/editRecordPlan.vue
@@ -53,7 +53,6 @@ export default {
   },
   methods: {
     openDialog: function (recordPlan, endCallback) {
-      console.log(recordPlan);
       this.endCallback = endCallback;
       this.showDialog = true;
       this.byteTime= "";
@@ -68,7 +67,7 @@ export default {
             planId: recordPlan.id,
           }
         }).then((res) => {
-          if (res.data.code === 0) {
+          if (res.data.code === 0 && res.data.data.planItemList) {
             this.byteTime = this.plan2Byte(res.data.data.planItemList)
           }
         }).catch((error) => {
@@ -163,7 +162,7 @@ export default {
         }
         for (let j = 0; j < planArray.length; j++) {
           planList.push({
-            id: this.id,
+            planId: this.id,
             start: planArray[j].start,
             stop: planArray[j].stop,
             weekDay: week
@@ -177,28 +176,29 @@ export default {
       let start = null;
       let stop = null;
       let result = []
-
+      console.log("===================")
       for (let i = 0; i < weekItem.length; i++) {
         let item = weekItem[i]
+        console.log(item)
         if (item === '1') { // 表示选中
           stop = i
           if (start === null ) {
             start = i
           }
-          if (i === weekItem.length - 1) {
+          if (i === weekItem.length - 1 && start != null && stop != null) {
             result.push({
               start: start,
               stop: stop,
             })
           }
         } else {
-          if (stop !== 0){
+          if (stop !== null){
             result.push({
               start: start,
               stop: stop,
             })
-            start = 0
-            stop = 0
+            start = null
+            stop = null
           }
         }
       }
@@ -212,10 +212,8 @@ export default {
         let weekDay = planList[i].weekDay
         let index = planList[i].start
         let endIndex = planList[i].stop
-        console.log(index + "===" + endIndex)
         for (let j = index; j <= endIndex; j++) {
           indexArray["key_" + (j + (weekDay - 1 )*48)] = 1
-          console.log("key_" + (j + (weekDay - 1 )*48))
         }
       }
       for (let i = 0; i < 336; i++) {

From ae339269403712fe9be941f5d620370b41d3b609 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 28 Nov 2024 18:00:52 +0800
Subject: [PATCH 14/38] =?UTF-8?q?[=E5=BD=95=E5=88=B6=E8=AE=A1=E5=88=92]=20?=
 =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BD=95=E5=88=B6=E8=AE=A1=E5=88=92=E6=89=A7?=
 =?UTF-8?q?=E8=A1=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gb28181/dao/CommonGBChannelMapper.java    |  50 +++++++++
 .../zlm/dto/hook/HookResultForOnPublish.java  |  52 +--------
 .../iot/vmp/service/IRecordPlanService.java   |   3 +-
 .../vmp/service/impl/MediaServiceImpl.java    |   7 ++
 .../service/impl/RecordPlanServiceImpl.java   | 100 ++++++++++++++----
 .../vmp/storager/dao/RecordPlanMapper.java    |   3 +
 .../src/components/dialog/editRecordPlan.vue  |   2 -
 7 files changed, 148 insertions(+), 69 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
index 2f636a4d5..d14ba09c2 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
@@ -498,6 +498,7 @@ public interface CommonGBChannelMapper {
             "    wdc.stream_proxy_id,\n" +
             "    wdc.create_time,\n" +
             "    wdc.update_time,\n" +
+            "    wdc.record_plan_id,\n" +
             "    coalesce( wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" +
             "    coalesce( wdc.gb_name, wdc.name) as gb_name,\n" +
             "    coalesce( wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" +
@@ -548,4 +549,53 @@ public interface CommonGBChannelMapper {
     List<CommonGBChannel> queryForRecordPlanForWebList(@Param("planId") Integer planId, @Param("query") String query,
                                                        @Param("channelType") Integer channelType, @Param("online") Boolean online,
                                                        @Param("hasLink") Boolean hasLink);
+
+    @Select("<script>" +
+            " select " +
+            "    wdc.id as gb_id,\n" +
+            "    wdc.device_db_id as gb_device_db_id,\n" +
+            "    wdc.stream_push_id,\n" +
+            "    wdc.stream_proxy_id,\n" +
+            "    wdc.create_time,\n" +
+            "    wdc.update_time,\n" +
+            "    wdc.record_plan_id,\n" +
+            "    coalesce( wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" +
+            "    coalesce( wdc.gb_name, wdc.name) as gb_name,\n" +
+            "    coalesce( wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" +
+            "    coalesce( wdc.gb_model, wdc.model) as gb_model,\n" +
+            "    coalesce( wdc.gb_owner, wdc.owner) as gb_owner,\n" +
+            "    coalesce( wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" +
+            "    coalesce( wdc.gb_block, wdc.block) as gb_block,\n" +
+            "    coalesce( wdc.gb_address, wdc.address) as gb_address,\n" +
+            "    coalesce( wdc.gb_parental, wdc.parental) as gb_parental,\n" +
+            "    coalesce( wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" +
+            "    coalesce( wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" +
+            "    coalesce( wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" +
+            "    coalesce( wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" +
+            "    coalesce( wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" +
+            "    coalesce( wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" +
+            "    coalesce( wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" +
+            "    coalesce( wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" +
+            "    coalesce( wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" +
+            "    coalesce( wdc.gb_port, wdc.port) as gb_port,\n" +
+            "    coalesce( wdc.gb_password, wdc.password) as gb_password,\n" +
+            "    coalesce( wdc.gb_status, wdc.status) as gb_status,\n" +
+            "    coalesce( wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" +
+            "    coalesce( wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" +
+            "    coalesce( wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" +
+            "    coalesce( wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" +
+            "    coalesce( wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" +
+            "    coalesce( wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" +
+            "    coalesce( wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" +
+            "    coalesce( wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" +
+            "    coalesce( wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" +
+            "    coalesce( wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" +
+            "    coalesce( wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" +
+            "    coalesce( wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" +
+            "    coalesce( wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode \n" +
+            " from wvp_device_channel wdc" +
+            " where wdc.record_plan_id in " +
+            " <foreach collection='planIdList'  item='item'  open='(' separator=',' close=')' > #{item}</foreach>" +
+            "</script>")
+    List<CommonGBChannel> queryForRecordPlan(List<Integer> planIdList);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java
index 33f9856f4..3777e19ec 100755
--- a/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java
+++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java
@@ -1,7 +1,11 @@
 package com.genersoft.iot.vmp.media.zlm.dto.hook;
 
 import com.genersoft.iot.vmp.media.bean.ResultForOnPublish;
+import lombok.Getter;
+import lombok.Setter;
 
+@Setter
+@Getter
 public class HookResultForOnPublish extends HookResult{
 
     private boolean enable_audio;
@@ -34,54 +38,6 @@ public class HookResultForOnPublish extends HookResult{
         setMsg(msg);
     }
 
-    public boolean isEnable_audio() {
-        return enable_audio;
-    }
-
-    public void setEnable_audio(boolean enable_audio) {
-        this.enable_audio = enable_audio;
-    }
-
-    public boolean isEnable_mp4() {
-        return enable_mp4;
-    }
-
-    public void setEnable_mp4(boolean enable_mp4) {
-        this.enable_mp4 = enable_mp4;
-    }
-
-    public int getMp4_max_second() {
-        return mp4_max_second;
-    }
-
-    public void setMp4_max_second(int mp4_max_second) {
-        this.mp4_max_second = mp4_max_second;
-    }
-
-    public String getMp4_save_path() {
-        return mp4_save_path;
-    }
-
-    public void setMp4_save_path(String mp4_save_path) {
-        this.mp4_save_path = mp4_save_path;
-    }
-
-    public String getStream_replace() {
-        return stream_replace;
-    }
-
-    public void setStream_replace(String stream_replace) {
-        this.stream_replace = stream_replace;
-    }
-
-    public Integer getModify_stamp() {
-        return modify_stamp;
-    }
-
-    public void setModify_stamp(Integer modify_stamp) {
-        this.modify_stamp = modify_stamp;
-    }
-
     @Override
     public String toString() {
         return "HookResultForOnPublish{" +
diff --git a/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
index 359e0072a..abec8e0cb 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
@@ -1,7 +1,6 @@
 package com.genersoft.iot.vmp.service;
 
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
-import com.genersoft.iot.vmp.gb28181.bean.PlatformChannel;
 import com.genersoft.iot.vmp.service.bean.RecordPlan;
 import com.github.pagehelper.PageInfo;
 
@@ -27,4 +26,6 @@ public interface IRecordPlanService {
     void linkAll(Integer planId);
 
     void cleanAll(Integer planId);
+
+    boolean recording(String app, String stream);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java
index ad575042b..0959c7843 100755
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java
@@ -15,6 +15,7 @@ import com.genersoft.iot.vmp.media.bean.MediaServer;
 import com.genersoft.iot.vmp.media.bean.ResultForOnPublish;
 import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo;
 import com.genersoft.iot.vmp.service.IMediaService;
+import com.genersoft.iot.vmp.service.IRecordPlanService;
 import com.genersoft.iot.vmp.service.IUserService;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.streamProxy.bean.StreamProxy;
@@ -59,6 +60,9 @@ public class MediaServiceImpl implements IMediaService {
     @Autowired
     private SipInviteSessionManager sessionManager;
 
+    @Autowired
+    private IRecordPlanService recordPlanService;
+
     @Override
     public boolean authenticatePlay(String app, String stream, String callId) {
         if (app == null || stream == null) {
@@ -205,6 +209,9 @@ public class MediaServiceImpl implements IMediaService {
     @Override
     public boolean closeStreamOnNoneReader(String mediaServerId, String app, String stream, String schema) {
         boolean result = false;
+        if (recordPlanService.recording(app, stream)) {
+            return false;
+        }
         // 国标类型的流
         if ("rtp".equals(app)) {
             result = userSetting.getStreamOnDemand();
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
index 061769b82..4bcc945a0 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
@@ -1,12 +1,15 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.dao.CommonGBChannelMapper;
-import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
-import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent;
+import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
+import com.genersoft.iot.vmp.media.bean.MediaInfo;
 import com.genersoft.iot.vmp.media.event.media.MediaDepartureEvent;
+import com.genersoft.iot.vmp.media.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.IRecordPlanService;
+import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.service.bean.RecordPlan;
 import com.genersoft.iot.vmp.service.bean.RecordPlanItem;
 import com.genersoft.iot.vmp.storager.dao.RecordPlanMapper;
@@ -22,8 +25,8 @@ import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.time.LocalDateTime;
+import java.util.*;
 
 @Service
 @Slf4j
@@ -36,17 +39,12 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
     private CommonGBChannelMapper channelMapper;
 
     @Autowired
-    private IGbChannelService channelService;
+    private IGbChannelPlayService channelPlayService;
+
+    @Autowired
+    private IMediaServerService mediaServerService;
 
 
-    /**
-     * 流到来的处理
-     */
-    @Async("taskExecutor")
-    @org.springframework.context.event.EventListener
-    public void onApplicationEvent(MediaArrivalEvent event) {
-
-    }
 
     /**
      * 流离开的处理
@@ -55,23 +53,89 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
     @EventListener
     public void onApplicationEvent(MediaDepartureEvent event) {
         // 流断开,检查是否还处于录像状态, 如果是则继续录像
+        if (recording(event.getApp(), event.getStream())) {
+            // 重新拉起
 
+        }
     }
 
+    Map<Integer, StreamInfo> recordStreamMap = new HashMap<>();
+
     @Scheduled(cron = "0 */30 * * * *")
     public void execution() {
         // 执行计划
+
+        // 获取当前时间在一周内的序号
+        LocalDateTime now = LocalDateTime.now();
+        int week = now.getDayOfWeek().getValue();
+        int index = now.getHour() * 2 + (now.getMinute() > 30?1:0);
         // 查询startTime等于现在的, 开始录像
+        List<Integer> startPlanList = recordPlanMapper.queryStart(week, index);
 
-        // 查询stopTime等于现在的,结束录像
-        // 查询处于中间的,验证录像是否正在进行
-
-
-        // TODO 无人观看要确保处于录像状态的通道不被移除
+        Map<Integer, StreamInfo> channelMapWithoutRecord = new HashMap<>();
+        if (startPlanList.isEmpty()) {
+            // 停止所有正在录像的
+            if(recordStreamMap.isEmpty()) {
+                // 暂无录像任务
+                return;
+            }else {
+                channelMapWithoutRecord.putAll(recordStreamMap);
+                recordStreamMap.clear();
+            }
+        }else {
+            channelMapWithoutRecord.putAll(recordStreamMap);
+            // 获取所有的关联的通道
+            List<CommonGBChannel> channelList = channelMapper.queryForRecordPlan(startPlanList);
+            if (channelList.isEmpty()) {
+                recordStreamMap.clear();
+            }else {
+                // 查找是否已经开启录像, 如果没有则开启录像
+                for (CommonGBChannel channel : channelList) {
+                    if (recordStreamMap.get(channel.getGbId()) != null) {
+                        channelMapWithoutRecord.remove(channel.getGbId());
+                    }else {
+                        // 开启点播,
+                        channelPlayService.play(channel, null, ((code, msg, streamInfo) -> {
+                            if (code == InviteErrorCode.SUCCESS.getCode() && streamInfo != null) {
+                                log.info("[录像] 开启成功, 通道ID: {}", channel.getGbId());
+                                recordStreamMap.put(channel.getGbId(), streamInfo);
+                                channelMapWithoutRecord.remove(channel.getGbId(), streamInfo);
+                            }
+                        }));
+                    }
+                }
+            }
+        }
+        // 结束录像
+        if(!channelMapWithoutRecord.isEmpty()) {
+            for (Integer channelId : channelMapWithoutRecord.keySet()) {
+                StreamInfo streamInfo = channelMapWithoutRecord.get(channelId);
+                if (streamInfo == null) {
+                    continue;
+                }
+                // 查看是否有人观看,存在则不做处理,等待后续自然处理,如果无人观看,则关闭该流
+                MediaInfo mediaInfo = mediaServerService.getMediaInfo(streamInfo.getMediaServer(), streamInfo.getApp(), streamInfo.getStream());
+                if (mediaInfo.getReaderCount() == null ||  mediaInfo.getReaderCount() == 0) {
+                    mediaServerService.closeStreams(streamInfo.getMediaServer(), streamInfo.getApp(), streamInfo.getStream());
+                    log.info("[录像] 停止, 通道ID: {}", channelId);
+                }
+            }
+        }
     }
 
     // 系统启动时
 
+
+    @Override
+    public boolean recording(String app, String stream) {
+        for (StreamInfo streamInfo : recordStreamMap.values()) {
+            if (streamInfo.getApp().equals(app) && streamInfo.getStream().equals(stream)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @Override
     @Transactional
     public void add(RecordPlan plan) {
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
index 2cdc6468e..e9304df05 100644
--- a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
@@ -58,4 +58,7 @@ public interface RecordPlanMapper {
 
     @Delete("DELETE FROM wvp_record_plan_item WHERE plan_id = #{planId}")
     void cleanItems(@Param("planId") Integer planId);
+
+    @Select("select plan_id from wvp_record_plan_item where  week_day = #{week} and start &gt;= #{index} and stop &lt;= #{index} group by plan_id")
+    List<Integer> queryStart(@Param("week") int week, @Param("index") int index);
 }
diff --git a/web_src/src/components/dialog/editRecordPlan.vue b/web_src/src/components/dialog/editRecordPlan.vue
index 1de115cca..4d425779a 100644
--- a/web_src/src/components/dialog/editRecordPlan.vue
+++ b/web_src/src/components/dialog/editRecordPlan.vue
@@ -176,10 +176,8 @@ export default {
       let start = null;
       let stop = null;
       let result = []
-      console.log("===================")
       for (let i = 0; i < weekItem.length; i++) {
         let item = weekItem[i]
-        console.log(item)
         if (item === '1') { // 表示选中
           stop = i
           if (start === null ) {

From 0f0be7d7c77d4382961ca0f48ee7d290efca0c3b Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Fri, 29 Nov 2024 15:38:55 +0800
Subject: [PATCH 15/38] =?UTF-8?q?[=E5=BD=95=E5=88=B6=E8=AE=A1=E5=88=92]=20?=
 =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=92=8C=E4=BC=98=E5=8C=96=E5=BD=95=E5=88=B6?=
 =?UTF-8?q?=E8=AE=A1=E5=88=92=E6=89=A7=E8=A1=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gb28181/dao/CommonGBChannelMapper.java    |  48 -------
 .../iot/vmp/service/IRecordPlanService.java   |   2 +-
 .../vmp/service/impl/MediaServiceImpl.java    |   2 +-
 .../service/impl/RecordPlanServiceImpl.java   | 132 ++++++++++++------
 .../vmp/storager/dao/RecordPlanMapper.java    |   7 +-
 5 files changed, 93 insertions(+), 98 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
index d14ba09c2..64f7dad85 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/CommonGBChannelMapper.java
@@ -550,52 +550,4 @@ public interface CommonGBChannelMapper {
                                                        @Param("channelType") Integer channelType, @Param("online") Boolean online,
                                                        @Param("hasLink") Boolean hasLink);
 
-    @Select("<script>" +
-            " select " +
-            "    wdc.id as gb_id,\n" +
-            "    wdc.device_db_id as gb_device_db_id,\n" +
-            "    wdc.stream_push_id,\n" +
-            "    wdc.stream_proxy_id,\n" +
-            "    wdc.create_time,\n" +
-            "    wdc.update_time,\n" +
-            "    wdc.record_plan_id,\n" +
-            "    coalesce( wdc.gb_device_id, wdc.device_id) as gb_device_id,\n" +
-            "    coalesce( wdc.gb_name, wdc.name) as gb_name,\n" +
-            "    coalesce( wdc.gb_manufacturer, wdc.manufacturer) as gb_manufacturer,\n" +
-            "    coalesce( wdc.gb_model, wdc.model) as gb_model,\n" +
-            "    coalesce( wdc.gb_owner, wdc.owner) as gb_owner,\n" +
-            "    coalesce( wdc.gb_civil_code, wdc.civil_code) as gb_civil_code,\n" +
-            "    coalesce( wdc.gb_block, wdc.block) as gb_block,\n" +
-            "    coalesce( wdc.gb_address, wdc.address) as gb_address,\n" +
-            "    coalesce( wdc.gb_parental, wdc.parental) as gb_parental,\n" +
-            "    coalesce( wdc.gb_parent_id, wdc.parent_id) as gb_parent_id,\n" +
-            "    coalesce( wdc.gb_safety_way, wdc.safety_way) as gb_safety_way,\n" +
-            "    coalesce( wdc.gb_register_way, wdc.register_way) as gb_register_way,\n" +
-            "    coalesce( wdc.gb_cert_num, wdc.cert_num) as gb_cert_num,\n" +
-            "    coalesce( wdc.gb_certifiable, wdc.certifiable) as gb_certifiable,\n" +
-            "    coalesce( wdc.gb_err_code, wdc.err_code) as gb_err_code,\n" +
-            "    coalesce( wdc.gb_end_time, wdc.end_time) as gb_end_time,\n" +
-            "    coalesce( wdc.gb_secrecy, wdc.secrecy) as gb_secrecy,\n" +
-            "    coalesce( wdc.gb_ip_address, wdc.ip_address) as gb_ip_address,\n" +
-            "    coalesce( wdc.gb_port, wdc.port) as gb_port,\n" +
-            "    coalesce( wdc.gb_password, wdc.password) as gb_password,\n" +
-            "    coalesce( wdc.gb_status, wdc.status) as gb_status,\n" +
-            "    coalesce( wdc.gb_longitude, wdc.longitude) as gb_longitude,\n" +
-            "    coalesce( wdc.gb_latitude, wdc.latitude) as gb_latitude,\n" +
-            "    coalesce( wdc.gb_ptz_type, wdc.ptz_type) as gb_ptz_type,\n" +
-            "    coalesce( wdc.gb_position_type, wdc.position_type) as gb_position_type,\n" +
-            "    coalesce( wdc.gb_room_type, wdc.room_type) as gb_room_type,\n" +
-            "    coalesce( wdc.gb_use_type, wdc.use_type) as gb_use_type,\n" +
-            "    coalesce( wdc.gb_supply_light_type, wdc.supply_light_type) as gb_supply_light_type,\n" +
-            "    coalesce( wdc.gb_direction_type, wdc.direction_type) as gb_direction_type,\n" +
-            "    coalesce( wdc.gb_resolution, wdc.resolution) as gb_resolution,\n" +
-            "    coalesce( wdc.gb_business_group_id, wdc.business_group_id) as gb_business_group_id,\n" +
-            "    coalesce( wdc.gb_download_speed, wdc.download_speed) as gb_download_speed,\n" +
-            "    coalesce( wdc.gb_svc_space_support_mod, wdc.svc_space_support_mod) as gb_svc_space_support_mod,\n" +
-            "    coalesce( wdc.gb_svc_time_support_mode, wdc.svc_time_support_mode) as gb_svc_time_support_mode \n" +
-            " from wvp_device_channel wdc" +
-            " where wdc.record_plan_id in " +
-            " <foreach collection='planIdList'  item='item'  open='(' separator=',' close=')' > #{item}</foreach>" +
-            "</script>")
-    List<CommonGBChannel> queryForRecordPlan(List<Integer> planIdList);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
index abec8e0cb..f3b34912c 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/IRecordPlanService.java
@@ -27,5 +27,5 @@ public interface IRecordPlanService {
 
     void cleanAll(Integer planId);
 
-    boolean recording(String app, String stream);
+    Integer recording(String app, String stream);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java
index 0959c7843..fcb7570fb 100755
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java
@@ -209,7 +209,7 @@ public class MediaServiceImpl implements IMediaService {
     @Override
     public boolean closeStreamOnNoneReader(String mediaServerId, String app, String stream, String schema) {
         boolean result = false;
-        if (recordPlanService.recording(app, stream)) {
+        if (recordPlanService.recording(app, stream) != null) {
             return false;
         }
         // 国标类型的流
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
index 4bcc945a0..ac146e867 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
@@ -17,6 +17,7 @@ import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
+import com.google.common.base.Joiner;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.event.EventListener;
@@ -27,6 +28,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 
 @Service
 @Slf4j
@@ -53,63 +55,94 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
     @EventListener
     public void onApplicationEvent(MediaDepartureEvent event) {
         // 流断开,检查是否还处于录像状态, 如果是则继续录像
-        if (recording(event.getApp(), event.getStream())) {
-            // 重新拉起
-
+        Integer channelId = recording(event.getApp(), event.getStream());
+        if(channelId == null) {
+            return;
         }
+        // 重新拉起
+        CommonGBChannel channel = channelMapper.queryById(channelId);
+        if (channel == null) {
+            log.warn("[录制计划] 流离开时拉起需要录像的流时, 发现通道不存在, id: {}", channelId);
+            return;
+        }
+        // 开启点播,
+        channelPlayService.play(channel, null, ((code, msg, streamInfo) -> {
+            if (code == InviteErrorCode.SUCCESS.getCode() && streamInfo != null) {
+                log.info("[录像] 流离开时拉起需要录像的流, 开启成功, 通道ID: {}", channel.getGbId());
+                recordStreamMap.put(channel.getGbId(), streamInfo);
+            } else {
+                recordStreamMap.remove(channelId);
+                log.info("[录像] 流离开时拉起需要录像的流, 开启失败, 十分钟后重试,  通道ID: {}", channel.getGbId());
+            }
+        }));
     }
 
     Map<Integer, StreamInfo> recordStreamMap = new HashMap<>();
 
-    @Scheduled(cron = "0 */30 * * * *")
+//    @Scheduled(cron = "0 */30 * * * *")
+    @Scheduled(fixedRate = 10, timeUnit = TimeUnit.MINUTES)
     public void execution() {
-        // 执行计划
+        log.info("[录制计划] 执行");
+        // 查询现在需要录像的通道Id
+        List<Integer> startChannelIdList = queryCurrentChannelRecord();
 
-        // 获取当前时间在一周内的序号
-        LocalDateTime now = LocalDateTime.now();
-        int week = now.getDayOfWeek().getValue();
-        int index = now.getHour() * 2 + (now.getMinute() > 30?1:0);
-        // 查询startTime等于现在的, 开始录像
-        List<Integer> startPlanList = recordPlanMapper.queryStart(week, index);
-
-        Map<Integer, StreamInfo> channelMapWithoutRecord = new HashMap<>();
-        if (startPlanList.isEmpty()) {
-            // 停止所有正在录像的
-            if(recordStreamMap.isEmpty()) {
-                // 暂无录像任务
-                return;
-            }else {
-                channelMapWithoutRecord.putAll(recordStreamMap);
+        if (startChannelIdList.isEmpty()) {
+            // 当前没有录像任务, 如果存在旧的正在录像的就移除
+            if(!recordStreamMap.isEmpty()) {
+                stopStreams(recordStreamMap.keySet(), recordStreamMap);
                 recordStreamMap.clear();
             }
         }else {
-            channelMapWithoutRecord.putAll(recordStreamMap);
-            // 获取所有的关联的通道
-            List<CommonGBChannel> channelList = channelMapper.queryForRecordPlan(startPlanList);
-            if (channelList.isEmpty()) {
-                recordStreamMap.clear();
-            }else {
-                // 查找是否已经开启录像, 如果没有则开启录像
-                for (CommonGBChannel channel : channelList) {
-                    if (recordStreamMap.get(channel.getGbId()) != null) {
-                        channelMapWithoutRecord.remove(channel.getGbId());
-                    }else {
+            // 当前存在录像任务, 获取正在录像中存在但是当前录制列表不存在的内容,进行停止; 获取正在录像中没有但是当前需录制的列表中存在的进行开启.
+            Set<Integer> recordStreamSet = new HashSet<>(recordStreamMap.keySet());
+            startChannelIdList.forEach(recordStreamSet::remove);
+            if (!recordStreamSet.isEmpty()) {
+                // 正在录像中存在但是当前录制列表不存在的内容,进行停止;
+                stopStreams(recordStreamSet, recordStreamMap);
+            }
+
+            // 移除startChannelIdList中已经在录像的部分, 剩下的都是需要新添加的(正在录像中没有但是当前需录制的列表中存在的进行开启)
+            recordStreamMap.keySet().forEach(startChannelIdList::remove);
+            if (!startChannelIdList.isEmpty()) {
+                // 获取所有的关联的通道
+                List<CommonGBChannel> channelList = channelMapper.queryByIds(startChannelIdList);
+                if (!channelList.isEmpty()) {
+                    // 查找是否已经开启录像, 如果没有则开启录像
+                    for (CommonGBChannel channel : channelList) {
                         // 开启点播,
                         channelPlayService.play(channel, null, ((code, msg, streamInfo) -> {
                             if (code == InviteErrorCode.SUCCESS.getCode() && streamInfo != null) {
                                 log.info("[录像] 开启成功, 通道ID: {}", channel.getGbId());
                                 recordStreamMap.put(channel.getGbId(), streamInfo);
-                                channelMapWithoutRecord.remove(channel.getGbId(), streamInfo);
+                            } else {
+                                log.info("[录像] 开启失败, 十分钟后重试,  通道ID: {}", channel.getGbId());
                             }
                         }));
                     }
+                } else {
+                    log.error("[录制计划] 数据异常, 这些关联的通道已经不存在了: {}", Joiner.on(",").join(startChannelIdList));
                 }
             }
         }
-        // 结束录像
-        if(!channelMapWithoutRecord.isEmpty()) {
-            for (Integer channelId : channelMapWithoutRecord.keySet()) {
-                StreamInfo streamInfo = channelMapWithoutRecord.get(channelId);
+    }
+
+    /**
+     * 获取当前时间段应该录像的通道Id列表
+     */
+    private List<Integer> queryCurrentChannelRecord(){
+        // 获取当前时间在一周内的序号, 数据库存储的从第几个30分钟开始, 0-47, 包括首尾
+        LocalDateTime now = LocalDateTime.now();
+        int week = now.getDayOfWeek().getValue();
+        int index = now.getHour() * 2 + (now.getMinute() > 30?1:0);
+
+        // 查询现在需要录像的通道Id
+        return recordPlanMapper.queryRecordIng(week, index);
+    }
+
+    private void stopStreams(Collection<Integer> channelIds, Map<Integer, StreamInfo> recordStreamMap) {
+        for (Integer channelId : channelIds) {
+            try {
+                StreamInfo streamInfo = recordStreamMap.get(channelId);
                 if (streamInfo == null) {
                     continue;
                 }
@@ -117,23 +150,25 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
                 MediaInfo mediaInfo = mediaServerService.getMediaInfo(streamInfo.getMediaServer(), streamInfo.getApp(), streamInfo.getStream());
                 if (mediaInfo.getReaderCount() == null ||  mediaInfo.getReaderCount() == 0) {
                     mediaServerService.closeStreams(streamInfo.getMediaServer(), streamInfo.getApp(), streamInfo.getStream());
-                    log.info("[录像] 停止, 通道ID: {}", channelId);
+                    log.info("[录制计划] 停止, 通道ID: {}", channelId);
                 }
+            }catch (Exception e) {
+                log.error("[录制计划] 停止时异常", e);
+            }finally {
+                recordStreamMap.remove(channelId);
             }
         }
     }
 
-    // 系统启动时
-
-
     @Override
-    public boolean recording(String app, String stream) {
-        for (StreamInfo streamInfo : recordStreamMap.values()) {
-            if (streamInfo.getApp().equals(app) && streamInfo.getStream().equals(stream)) {
-                return true;
+    public Integer recording(String app, String stream) {
+        for (Integer channelId : recordStreamMap.keySet()) {
+            StreamInfo streamInfo = recordStreamMap.get(channelId);
+            if (streamInfo != null && streamInfo.getApp().equals(app) && streamInfo.getStream().equals(stream)) {
+                return channelId;
             }
         }
-        return false;
+        return null;
     }
 
     @Override
@@ -226,7 +261,12 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
         }else {
             channelMapper.addRecordPlan(channelIds, planId);
         }
-        // TODO  更新录像队列
+        // 查看当前的待录制列表是否变化,如果变化,则调用录制计划马上开始录制
+        List<Integer> currentChannelRecord = queryCurrentChannelRecord();
+        recordStreamMap.keySet().forEach(currentChannelRecord::remove);
+        if (!currentChannelRecord.isEmpty()) {
+            execution();
+        }
     }
 
     @Override
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
index e9304df05..ae0649aa2 100644
--- a/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/RecordPlanMapper.java
@@ -59,6 +59,9 @@ public interface RecordPlanMapper {
     @Delete("DELETE FROM wvp_record_plan_item WHERE plan_id = #{planId}")
     void cleanItems(@Param("planId") Integer planId);
 
-    @Select("select plan_id from wvp_record_plan_item where  week_day = #{week} and start &gt;= #{index} and stop &lt;= #{index} group by plan_id")
-    List<Integer> queryStart(@Param("week") int week, @Param("index") int index);
+    @Select(" <script>" +
+            " select wdc.id from wvp_device_channel wdc left join wvp_record_plan_item wrpi on wrpi.plan_id = wdc.record_plan_id " +
+            " where  wrpi.week_day = #{week} and wrpi.start &lt;= #{index} and stop &gt;= #{index} group by wdc.id" +
+            " </script>")
+    List<Integer> queryRecordIng(@Param("week") int week, @Param("index") int index);
 }

From 6004c512284b8a831d6e57e03c8f47805186c781 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Mon, 9 Dec 2024 15:46:40 +0800
Subject: [PATCH 16/38] =?UTF-8?q?=E6=9B=B4=E6=96=B0README?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index bbe1975ee..1116581f7 100644
--- a/README.md
+++ b/README.md
@@ -8,16 +8,16 @@
 [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-yellow.svg)](https://github.com/xia-chu/ZLMediaKit/pulls)
 
 
-WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网络视频平台,负责实现核心信令与设备管理后台部分,支持NAT穿透,支持海康、大华、宇视等品牌的IPC、NVR接入。支持国标级联,支持将不带国标功能的摄像机/直播流/直播推流转发到其他国标平台。   
+WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网络视频平台,负责实现核心信令与设备管理后台部分,支持NAT穿透,支持海康、大华、宇视等品牌的IPC、NVR接入。支持国标级联,支持将不带国标功能的摄像机/直播流/直播推流转发到其他国标平台。
 
 流媒体服务基于@夏楚 ZLMediaKit [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)   
 播放器使用@dexter jessibuca [https://github.com/langhuihui/jessibuca/tree/v3](https://github.com/langhuihui/jessibuca/tree/v3)  
-前端页面基于@Kyle MediaServerUI [https://gitee.com/kkkkk5G/MediaServerUI](https://gitee.com/kkkkk5G/MediaServerUI) 进行修改.  
+前端页面基于@Kyle MediaServerUI [https://gitee.com/kkkkk5G/MediaServerUI](https://gitee.com/kkkkk5G/MediaServerUI) 进行修改.
 
 # 应用场景:
 支持浏览器无插件播放摄像头视频。
 支持国标设备(摄像机、平台、NVR等)设备接入
-支持非国标(onvif, rtsp, rtmp,直播设备等等)设备接入,充分利旧。 
+支持非国标(onvif, rtsp, rtmp,直播设备等等)设备接入,充分利旧。
 支持国标级联。多平台级联。跨网视频预览。
 支持跨网网闸平台互联。
 
@@ -43,7 +43,7 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
 ![build_1](https://images.gitee.com/uploads/images/2022/0304/101919_ee5b8c79_1018729.png "2022-03-04_10-13.png")
 ![运维中心](doc/_media/log.jpg "log.jpg")
 
-# 功能特性 
+# 功能特性
 - [X] 集成web界面
 - [X] 兼容性良好
 - [X] 接入设备
@@ -97,10 +97,10 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
     - [X] GPS订阅与通知(直播推流)
     - [X] 语音对讲
   - [X] 支持同时级联到多个上级平台
-- [X] 支持自动配置ZLM媒体服务, 减少因配置问题所出现的问题;  
+- [X] 支持自动配置ZLM媒体服务, 减少因配置问题所出现的问题;
 - [X] 多流媒体节点,自动选择负载最低的节点使用。
 - [X] 支持启用udp多端口模式, 提高udp模式下媒体传输性能;
-- [X] 支持公网部署; 
+- [X] 支持公网部署;
 - [X] 支持wvp与zlm分开部署,提升平台并发能力
 - [X] 支持拉流RTSP/RTMP,分发为各种流格式,或者推送到其他国标平台
 - [X] 支持推流RTSP/RTMP,分发为各种流格式,或者推送到其他国标平台
@@ -110,6 +110,7 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
 - [X] 支持打包可执行jar和war
 - [X] 支持跨域请求,支持前后端分离部署
 - [X] 支持Mysql,Postgresql,金仓等数据库
+- [X] 支持录制计划, 根据设定的时间对通道进行录制. 暂不支持将录制的内容转发到国标上级
 - [X] 支持Onvif, 目前付费提供, 永久免费试用包在知识星球获取
 - [X] 支持国标28181-2022协议, 目前付费提供, 永久免费试用包在知识星球获取
 
@@ -122,7 +123,7 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
 # 授权协议
 本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议
 
-# 技术支持  
+# 技术支持
 
 [知识星球](https://t.zsxq.com/0d8VAD3Dm)专栏列表:,
 - [使用入门系列一:WVP-PRO能做什么](https://t.zsxq.com/0dLguVoSp)
@@ -134,7 +135,7 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
 感谢作者[dexter langhuihui](https://github.com/langhuihui) 开源这么好用的WEB播放器。     
 感谢作者[Kyle](https://gitee.com/kkkkk5G) 开源了好用的前端页面     
 感谢各位大佬的赞助以及对项目的指正与帮助。包括但不限于代码贡献、问题反馈、资金捐赠等各种方式的支持!以下排名不分先后:  
-[lawrencehj](https://github.com/lawrencehj) [Smallwhitepig](https://github.com/Smallwhitepig) [swwhaha](https://github.com/swwheihei) 
+[lawrencehj](https://github.com/lawrencehj) [Smallwhitepig](https://github.com/Smallwhitepig) [swwhaha](https://github.com/swwheihei)
 [hotcoffie](https://github.com/hotcoffie) [xiaomu](https://github.com/nikmu) [TristingChen](https://github.com/TristingChen)
 [chenparty](https://github.com/chenparty) [Hotleave](https://github.com/hotleave) [ydwxb](https://github.com/ydwxb)
 [ydpd](https://github.com/ydpd) [szy833](https://github.com/szy833) [ydwxb](https://github.com/ydwxb) [Albertzhu666](https://github.com/Albertzhu666)

From 0f12c240e84826fa0c1bf9d592ba85eb9751888b Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Tue, 10 Dec 2024 21:59:40 +0800
Subject: [PATCH 17/38] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BD=95=E5=88=B6?=
 =?UTF-8?q?=E8=AE=A1=E5=88=92=E5=85=B3=E8=81=94=E5=85=A8=E9=83=A8=E9=80=9A?=
 =?UTF-8?q?=E9=81=93=E6=8F=90=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 web_src/src/components/dialog/linkChannelRecord.vue | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/web_src/src/components/dialog/linkChannelRecord.vue b/web_src/src/components/dialog/linkChannelRecord.vue
index 2046c1eb7..d30e25bcf 100755
--- a/web_src/src/components/dialog/linkChannelRecord.vue
+++ b/web_src/src/components/dialog/linkChannelRecord.vue
@@ -37,8 +37,8 @@
                 </el-button>
                 <el-button size="mini" v-if="hasLink !=='true'" @click="addByDevice()">按设备添加</el-button>
                 <el-button size="mini" v-if="hasLink ==='true'" @click="removeByDevice()">按设备移除</el-button>
-                <el-button size="mini" v-if="hasLink !=='true'" @click="addAll()">全部添加</el-button>
-                <el-button size="mini" v-if="hasLink ==='true'" @click="removeAll()">全部移除</el-button>
+                <el-button size="mini" v-if="hasLink !=='true'" @click="addAll()">添加所有通道</el-button>
+                <el-button size="mini" v-if="hasLink ==='true'" @click="removeAll()">移除所有通道</el-button>
                 <el-button size="mini" @click="getChannelList()">刷新</el-button>
               </div>
             </div>
@@ -218,7 +218,7 @@ export default {
       })
     },
     addAll: function (row) {
-      this.$confirm("确定全部添加?", '提示', {
+      this.$confirm("添加所有通道将包括已经添加到其他计划的通道,确定添加所有通道?", '提示', {
         dangerouslyUseHTMLString: true,
         confirmButtonText: '确定',
         cancelButtonText: '取消',
@@ -275,7 +275,7 @@ export default {
     },
     removeAll: function (row) {
 
-      this.$confirm("确定全部移除?", '提示', {
+      this.$confirm("确定移除所有通道?", '提示', {
         dangerouslyUseHTMLString: true,
         confirmButtonText: '确定',
         cancelButtonText: '取消',

From f340eb33322027730579fa48aa64bea35e71d96f Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Tue, 10 Dec 2024 22:02:47 +0800
Subject: [PATCH 18/38] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A1=8C=E6=94=BF?=
 =?UTF-8?q?=E5=8C=BA=E5=88=92=E6=A0=91=E4=BB=A5=E5=8F=8A=E4=B8=9A=E5=8A=A1?=
 =?UTF-8?q?=E5=88=86=E7=BB=84=E6=A0=91=E7=9A=84=E6=93=8D=E4=BD=9C=E6=8F=90?=
 =?UTF-8?q?=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 web_src/src/components/common/GroupTree.vue  | 5 +++++
 web_src/src/components/common/RegionTree.vue | 7 ++++++-
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/web_src/src/components/common/GroupTree.vue b/web_src/src/components/common/GroupTree.vue
index aded33161..9e0993552 100755
--- a/web_src/src/components/common/GroupTree.vue
+++ b/web_src/src/components/common/GroupTree.vue
@@ -14,6 +14,7 @@
     </div>
     <div v-if="showHeader" style="height: 2rem; background-color: #FFFFFF"></div>
     <div>
+      <el-alert v-if="showAlert && edit" title="操作提示" description="你可以使用右键菜单管理节点" type="info" style="text-align: left"></el-alert>
       <vue-easy-tree
         class="flow-tree"
         ref="veTree"
@@ -65,6 +66,7 @@ export default {
         id: "treeId"
       },
       showCode: false,
+      showAlert: true,
       searchSrt: "",
       chooseId: "",
       treeData: [],
@@ -101,6 +103,9 @@ export default {
           }
         }).then((res) => {
           if (res.data.code === 0) {
+            if (res.data.data.length > 0) {
+              this.showAlert = false
+            }
             resolve(res.data.data);
           }
 
diff --git a/web_src/src/components/common/RegionTree.vue b/web_src/src/components/common/RegionTree.vue
index 6dd38fb3a..f45bc8ba9 100755
--- a/web_src/src/components/common/RegionTree.vue
+++ b/web_src/src/components/common/RegionTree.vue
@@ -12,7 +12,8 @@
       </div>
     </div>
     <div v-if="showHeader" style="height: 2rem; background-color: #FFFFFF" ></div>
-    <div >
+    <div>
+      <el-alert v-if="showAlert && edit" title="操作提示" description="你可以使用右键菜单管理节点" type="info" style="text-align: left"></el-alert>
       <vue-easy-tree
         class="flow-tree"
         ref="veTree"
@@ -63,6 +64,7 @@ export default {
         label: "name",
       },
       showCode: false,
+      showAlert: true,
       searchSrt: "",
       chooseId: "",
       treeData: [],
@@ -99,6 +101,9 @@ export default {
           }
         }).then((res) => {
           if (res.data.code === 0) {
+            if (res.data.data.length > 0) {
+              this.showAlert = false
+            }
             resolve(res.data.data);
           }
 

From f08566096d094d5367b8d30182d3225042964f8f Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Tue, 10 Dec 2024 22:21:45 +0800
Subject: [PATCH 19/38] =?UTF-8?q?=E4=B8=BA=E8=A1=8C=E6=94=BF=E5=8C=BA?=
 =?UTF-8?q?=E5=88=92=E6=A0=91=E4=BB=A5=E5=8F=8A=E4=B8=9A=E5=8A=A1=E5=88=86?=
 =?UTF-8?q?=E7=BB=84=E6=A0=91=E6=B7=BB=E5=8A=A0=E6=93=8D=E4=BD=9C=E6=8F=90?=
 =?UTF-8?q?=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 web_src/src/components/group.vue  | 11 ++++++++---
 web_src/src/components/region.vue |  7 ++++---
 2 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/web_src/src/components/group.vue b/web_src/src/components/group.vue
index bce8d9567..85fb134e0 100755
--- a/web_src/src/components/group.vue
+++ b/web_src/src/components/group.vue
@@ -8,9 +8,10 @@
       <el-main style="padding: 5px;">
         <div class="page-header">
           <div class="page-title">
-            <el-breadcrumb separator="/">
+            <el-breadcrumb separator="/" v-if="regionParents.length > 0">
               <el-breadcrumb-item v-for="key in regionParents" key="key">{{ key }}</el-breadcrumb-item>
             </el-breadcrumb>
+            <div v-else style="color: #00c6ff">未选择虚拟组织</div>
           </div>
           <div class="page-header-btn">
             <div style="display: inline;">
@@ -121,7 +122,7 @@ export default {
       groupDeviceId: "",
       groupId: "",
       businessGroup: "",
-      regionParents: ["请选择虚拟组织"],
+      regionParents: [],
       multipleSelection: []
     };
   },
@@ -289,7 +290,11 @@ export default {
     treeNodeClickEvent: function (group) {
       if (group.deviceId === "" || group.deviceId === group.businessGroup) {
         this.channelList = []
-        this.regionParents = ["请选择虚拟组织"];
+        this.regionParents = [];
+        this.$message.info({
+          showClose: true,
+          message: "当前为业务分组,挂载通道请选择其下的虚拟组织,如不存在可右键新建"
+        })
         return
       }
       this.groupDeviceId = group.deviceId;
diff --git a/web_src/src/components/region.vue b/web_src/src/components/region.vue
index 046fdce8d..b2c80dbe4 100755
--- a/web_src/src/components/region.vue
+++ b/web_src/src/components/region.vue
@@ -8,9 +8,10 @@
       <el-main style="padding: 5px;">
         <div class="page-header">
           <div class="page-title">
-            <el-breadcrumb separator="/">
+            <el-breadcrumb separator="/" v-if="regionParents.length > 0">
               <el-breadcrumb-item v-for="key in regionParents" key="key">{{ key }}</el-breadcrumb-item>
             </el-breadcrumb>
+            <div v-else style="color: #00c6ff">未选择行政区划</div>
           </div>
           <div class="page-header-btn">
             <div style="display: inline;">
@@ -116,7 +117,7 @@ export default {
       loadSnap: {},
       regionId: "",
       regionDeviceId: "",
-      regionParents: ["请选择行政区划"],
+      regionParents: [],
       multipleSelection: []
     };
   },
@@ -285,7 +286,7 @@ export default {
       this.regionDeviceId = region.deviceId;
       if (region.deviceId === "") {
         this.channelList = []
-        this.regionParents = ["请选择行政区划"];
+        this.regionParents = [];
       }
       this.initData();
       // 获取regionDeviceId对应的节点信息

From 4c7050e78cf2f41c46490302d0bd02b2a0f85fd9 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Tue, 10 Dec 2024 22:45:08 +0800
Subject: [PATCH 20/38] =?UTF-8?q?=E4=B8=BA=E8=A1=8C=E6=94=BF=E5=8C=BA?=
 =?UTF-8?q?=E5=88=92=E6=A0=91=E4=BB=A5=E5=8F=8A=E4=B8=9A=E5=8A=A1=E5=88=86?=
 =?UTF-8?q?=E7=BB=84=E6=A0=91=E6=B7=BB=E5=8A=A0=E9=80=9A=E9=81=93=E5=A2=9E?=
 =?UTF-8?q?=E5=8A=A0=E6=8F=90=E7=A4=BA=E4=BB=A5=E5=8F=8A=E6=B7=BB=E5=8A=A0?=
 =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E5=88=86=E7=BB=84=E6=94=AF=E6=8C=81=E9=80=89?=
 =?UTF-8?q?=E6=8B=A9=E8=A1=8C=E6=94=BF=E5=8C=BA=E5=88=92?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/components/dialog/GbChannelSelect.vue | 26 ++++++++++++-------
 web_src/src/components/dialog/groupEdit.vue   | 14 +++++-----
 2 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/web_src/src/components/dialog/GbChannelSelect.vue b/web_src/src/components/dialog/GbChannelSelect.vue
index 62a09066f..258e313ec 100644
--- a/web_src/src/components/dialog/GbChannelSelect.vue
+++ b/web_src/src/components/dialog/GbChannelSelect.vue
@@ -64,16 +64,22 @@
           </template>
         </el-table-column>
       </el-table>
-      <el-pagination
-        style="text-align: right"
-        @size-change="handleSizeChange"
-        @current-change="currentChange"
-        :current-page="currentPage"
-        :page-size="count"
-        :page-sizes="[10, 25, 35, 50, 200, 1000, 50000]"
-        layout="total, sizes, prev, pager, next"
-        :total="total">
-      </el-pagination>
+      <div style="display: grid; grid-template-columns: 1fr 1fr">
+        <div style="text-align: left; line-height: 32px">
+          <i class="el-icon-info"></i>未找到通道,可在国标设备/通道中选择编辑按钮, 选择{{dataType === 'civilCode'?'行政区划':'父节点编码'}}
+        </div>
+        <el-pagination
+          style="text-align: right"
+          @size-change="handleSizeChange"
+          @current-change="currentChange"
+          :current-page="currentPage"
+          :page-size="count"
+          :page-sizes="[10, 25, 35, 50, 200, 1000, 50000]"
+          layout="total, sizes, prev, pager, next"
+          :total="total">
+        </el-pagination>
+      </div>
+
     </el-dialog>
   </div>
 </template>
diff --git a/web_src/src/components/dialog/groupEdit.vue b/web_src/src/components/dialog/groupEdit.vue
index 72795cb4d..1c77bc841 100755
--- a/web_src/src/components/dialog/groupEdit.vue
+++ b/web_src/src/components/dialog/groupEdit.vue
@@ -22,7 +22,7 @@
           </el-form-item>
           <el-form-item label="行政区划" prop="name">
             <el-input v-model="group.civilCode" >
-              <el-button slot="append" @click="buildCivilCode(group.civilCode)">生成</el-button>
+              <el-button slot="append" @click="buildCivilCode(group.civilCode)">选择</el-button>
             </el-input>
           </el-form-item>
 
@@ -37,17 +37,17 @@
       </div>
     </el-dialog>
     <channelCode ref="channelCode"></channelCode>
-    <regionCode ref="regionCode"></regionCode>
+    <chooseCivilCode ref="chooseCivilCode"></chooseCivilCode>
   </div>
 </template>
 
 <script>
 import channelCode from "./channelCode.vue";
-import regionCode from "./regionCode.vue";
+import ChooseCivilCode from "./chooseCivilCode.vue";
 
 export default {
   name: "groupEdit",
-  components: {channelCode, regionCode},
+  components: {ChooseCivilCode, channelCode},
   computed: {},
   props: [],
   created() {},
@@ -116,11 +116,9 @@ export default {
       }, deviceId, 5 , lockContent);
     },
     buildCivilCode: function (deviceId){
-      this.$refs.regionCode.openDialog(code=>{
-        console.log("2222")
-        console.log(code)
+      this.$refs.chooseCivilCode.openDialog(code=>{
         this.group.civilCode = code;
-      }, deviceId)
+      });
     },
     close: function () {
       this.showDialog = false;

From b037f082064667e5499730a14890233196a31121 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Tue, 10 Dec 2024 22:55:02 +0800
Subject: [PATCH 21/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8E=86=E5=8F=B2?=
 =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=96=87=E4=BB=B6=E4=B8=8B=E8=BD=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../components/operationsForHistoryLog.vue    | 50 +++++++++++++++----
 1 file changed, 41 insertions(+), 9 deletions(-)

diff --git a/web_src/src/components/operationsForHistoryLog.vue b/web_src/src/components/operationsForHistoryLog.vue
index 870f1509e..196cf49d1 100755
--- a/web_src/src/components/operationsForHistoryLog.vue
+++ b/web_src/src/components/operationsForHistoryLog.vue
@@ -82,6 +82,7 @@ import uiHeader from '../layout/UiHeader.vue'
 import MediaServer from './service/MediaServer'
 import operationsFoShowLog from './dialog/operationsFoShowLog.vue'
 import moment from 'moment'
+import userService from "./service/UserService";
 
 export default {
   name: 'app',
@@ -154,16 +155,47 @@ export default {
 
     },
     downloadFile(file) {
-      const link = document.createElement('a');
-      link.target = "_blank";
-      link.download = file.fileName;
-      if (process.env.NODE_ENV === 'development') {
-        link.href = `/debug/api/log/file/${file.fileName}`
-      }else {
-        link.href = `/api/log/file/${file.fileName}`
-      }
+      // const link = document.createElement('a');
+      // link.target = "_blank";
+      // link.download = file.fileName;
+      // if (process.env.NODE_ENV === 'development') {
+      //   link.href = `/debug/api/log/file/${file.fileName}`
+      // }else {
+      //   link.href = `/api/log/file/${file.fileName}`
+      // }
+      //
+      // link.click();
 
-      link.click();
+
+      // 文件下载地址
+      const fileUrl = ((process.env.NODE_ENV === 'development') ? process.env.BASE_API : baseUrl) + `/api/log/file/${file.fileName}`;
+
+      // 设置请求头
+      const headers = new Headers();
+      headers.append('access-token', userService.getToken()); // 设置授权头,替换YourAccessToken为实际的访问令牌
+      // 发起  请求
+      fetch(fileUrl, {
+        method: 'GET',
+        headers: headers,
+      })
+        .then(response => response.blob())
+        .then(blob => {
+          console.log(blob)
+          // 创建一个虚拟的链接元素,模拟点击下载
+          const link = document.createElement('a');
+          link.target = "_blank";
+          link.href = window.URL.createObjectURL(blob);
+          link.download = file.fileName; // 设置下载文件名,替换filename.ext为实际的文件名和扩展名
+          document.body.appendChild(link);
+
+          // 模拟点击
+          link.click();
+
+          // 移除虚拟链接元素
+          document.body.removeChild(link);
+          this.$message.success("已申请截图",{closed: true})
+        })
+        .catch(error => console.error('下载失败:', error));
     },
     loadEnd() {
       this.playerTitle = this.file.fileName

From 425bf0459144b65cb353448a49e40ec4dac8bb85 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Wed, 11 Dec 2024 09:56:09 +0800
Subject: [PATCH 22/38] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../iot/vmp/gb28181/bean/Device.java          |  3 ++
 .../gb28181/controller/PlayController.java    |  3 ++
 .../iot/vmp/gb28181/dao/DeviceMapper.java     | 39 +++++++++++++++++++
 .../vmp/gb28181/service/IDeviceService.java   |  2 +-
 .../service/impl/DeviceServiceImpl.java       |  4 +-
 .../iot/vmp/gb28181/task/SipRunner.java       |  9 ++++-
 .../impl/RegisterRequestProcessor.java        |  2 +-
 7 files changed, 56 insertions(+), 6 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java
index 1e783e867..8d673dbc6 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java
@@ -195,4 +195,7 @@ public class Device {
 
 	@Schema(description = "控制语音对讲流程,释放收到ACK后发流")
 	private boolean broadcastPushAfterAck;
+
+	@Schema(description = "所属服务Id")
+	private String serverId;
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
index e7a8b1c87..003478f90 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
@@ -88,9 +88,12 @@ public class PlayController {
 		Assert.notNull(channelId, "通道国标编号不可为NULL");
 		// 获取可用的zlm
 		Device device = deviceService.getDeviceByDeviceId(deviceId);
+
 		Assert.notNull(deviceId, "设备不存在");
 		DeviceChannel channel = deviceChannelService.getOne(deviceId, channelId);
 		Assert.notNull(channel, "通道不存在");
+
+
 		MediaServer newMediaServerItem = playService.getNewMediaServerItem(device);
 
 		RequestMessage requestMessage = new RequestMessage();
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java
index abac64bb5..07b186763 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java
@@ -78,6 +78,7 @@ public interface DeviceMapper {
                 "as_message_channel,"+
                 "broadcast_push_after_ack,"+
                 "geo_coord_sys,"+
+                "server_id,"+
                 "on_line"+
             ") VALUES (" +
                 "#{deviceId}," +
@@ -108,6 +109,7 @@ public interface DeviceMapper {
                 "#{asMessageChannel}," +
                 "#{broadcastPushAfterAck}," +
                 "#{geoCoordSys}," +
+                "#{serverId}," +
                 "#{onLine}" +
             ")")
     @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
@@ -130,6 +132,7 @@ public interface DeviceMapper {
                 "<if test=\"keepaliveTime != null\">, keepalive_time=#{keepaliveTime}</if>" +
                 "<if test=\"keepaliveIntervalTime != null\">, keepalive_interval_time=#{keepaliveIntervalTime}</if>" +
                 "<if test=\"expires != null\">, expires=#{expires}</if>" +
+                "<if test=\"serverId != null\">, server_id=#{serverId}</if>" +
                 "WHERE device_id=#{deviceId}"+
             " </script>"})
     int update(Device device);
@@ -207,9 +210,43 @@ public interface DeviceMapper {
             "as_message_channel,"+
             "broadcast_push_after_ack,"+
             "geo_coord_sys,"+
+            "server_id,"+
             "on_line"+
             " FROM wvp_device WHERE on_line = true")
     List<Device> getOnlineDevices();
+    @Select("SELECT " +
+            "id, " +
+            "device_id, " +
+            "coalesce(custom_name, name) as name, " +
+            "password, " +
+            "manufacturer, " +
+            "model, " +
+            "firmware, " +
+            "transport," +
+            "stream_mode," +
+            "ip," +
+            "sdp_ip,"+
+            "local_ip,"+
+            "port,"+
+            "host_address,"+
+            "expires,"+
+            "register_time,"+
+            "keepalive_time,"+
+            "create_time,"+
+            "update_time,"+
+            "charset,"+
+            "subscribe_cycle_for_catalog,"+
+            "subscribe_cycle_for_mobile_position,"+
+            "mobile_position_submission_interval,"+
+            "subscribe_cycle_for_alarm,"+
+            "ssrc_check,"+
+            "as_message_channel,"+
+            "broadcast_push_after_ack,"+
+            "geo_coord_sys,"+
+            "server_id,"+
+            "on_line"+
+            " FROM wvp_device WHERE on_line = true and server_id = #{serverId}")
+    List<Device> getOnlineDevicesByServerId(@Param("serverId") String serverId);
 
     @Select("SELECT " +
             "id,"+
@@ -269,6 +306,7 @@ public interface DeviceMapper {
             "geo_coord_sys,"+
             "on_line,"+
             "stream_mode," +
+            "server_id," +
             "media_server_id"+
             ") VALUES (" +
             "#{deviceId}," +
@@ -284,6 +322,7 @@ public interface DeviceMapper {
             "#{geoCoordSys}," +
             "#{onLine}," +
             "#{streamMode}," +
+            "#{serverId}," +
             "#{mediaServerId}" +
             ")")
     void addCustomDevice(Device device);
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java
index 0812f8cfa..a8f48a476 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java
@@ -86,7 +86,7 @@ public interface IDeviceService {
      * 获取所有在线设备
      * @return 设备列表
      */
-    List<Device> getAllOnlineDevice();
+    List<Device> getAllOnlineDevice(String serverId);
 
     List<Device> getAllByStatus(Boolean status);
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java
index c3ede305a..4773b9fda 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java
@@ -353,8 +353,8 @@ public class DeviceServiceImpl implements IDeviceService {
     }
 
     @Override
-    public List<Device> getAllOnlineDevice() {
-        return deviceMapper.getOnlineDevices();
+    public List<Device> getAllOnlineDevice(String serverId) {
+        return deviceMapper.getOnlineDevicesByServerId(serverId);
     }
 
     @Override
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java b/src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java
index 102f9d0c9..56a649bc5 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java
@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.gb28181.task;
 
+import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.Platform;
@@ -60,9 +61,12 @@ public class SipRunner implements CommandLineRunner {
     @Autowired
     private ISendRtpServerService sendRtpServerService;
 
+    @Autowired
+    private UserSetting userSetting;
+
     @Override
     public void run(String... args) throws Exception {
-        List<Device> deviceList = deviceService.getAllOnlineDevice();
+        List<Device> deviceList = deviceService.getAllOnlineDevice(userSetting.getServerId());
 
         for (Device device : deviceList) {
             if (deviceService.expire(device)){
@@ -86,7 +90,8 @@ public class SipRunner implements CommandLineRunner {
                     deviceMapInDb.put(device.getDeviceId(), device);
                 });
                 devicesInRedis.parallelStream().forEach(device -> {
-                    if (deviceMapInDb.get(device.getDeviceId()) == null) {
+                    if (deviceMapInDb.get(device.getDeviceId()) == null
+                            && userSetting.getServerId().equals(device.getServerId())) {
                         redisCatchStorage.removeDevice(device.getDeviceId());
                     }
                 });
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java
index 44a3eb1ed..8ad37f1ed 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java
@@ -183,7 +183,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
                     device.setGeoCoordSys("WGS84");
                 }
             }
-
+            device.setServerId(userSetting.getServerId());
             device.setIp(remoteAddressInfo.getIp());
             device.setPort(remoteAddressInfo.getPort());
             device.setHostAddress(remoteAddressInfo.getIp().concat(":").concat(String.valueOf(remoteAddressInfo.getPort())));

From 6258bf69692407fa484160cff5826bdcd4546900 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Wed, 11 Dec 2024 11:43:41 +0800
Subject: [PATCH 23/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=89=B9=E9=87=8F?=
 =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=AD=90=E7=A0=81=E6=B5=81=E7=B1=BB=E5=9E=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java  | 4 ++++
 .../vmp/gb28181/service/impl/DeviceChannelServiceImpl.java  | 6 +++++-
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
index 99bb6e36f..068eb4b79 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceChannelMapper.java
@@ -413,6 +413,10 @@ public interface DeviceChannelMapper {
             "</script>")
     void updateChannelStreamIdentification(DeviceChannel channel);
 
+    @Update("<script>" +
+            "UPDATE wvp_device_channel SET stream_identification=#{streamIdentification}" +
+            "</script>")
+    void updateAllChannelStreamIdentification(@Param("streamIdentification") String streamIdentification);
 
     @Update({"<script>" +
             "<foreach collection='channelList' item='item' separator=';'>" +
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
index c18510c3f..b397f4d4e 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
@@ -336,7 +336,11 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
             log.info("[更新通道码流类型] 设备: {}, 通道:{}, 码流: {}", channel.getDeviceId(), channel.getDeviceId(),
                     channel.getStreamIdentification());
         }
-        channelMapper.updateChannelStreamIdentification(channel);
+        if (channel.getId() > 0) {
+            channelMapper.updateChannelStreamIdentification(channel);
+        }else {
+            channelMapper.updateAllChannelStreamIdentification(channel.getStreamIdentification());
+        }
     }
 
     @Override

From deaa84aafd1dd98b57d647327f9684c8b6dd671a Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Wed, 11 Dec 2024 14:04:55 +0800
Subject: [PATCH 24/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=B0=E5=A2=9Ezlm?=
 =?UTF-8?q?=E8=8A=82=E7=82=B9=E5=88=9D=E5=A7=8B=E5=8C=96=E7=AB=AF=E5=8F=A3?=
 =?UTF-8?q?=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../iot/vmp/media/zlm/ZLMMediaNodeServerService.java        | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java
index ac01944ae..802b7eb6e 100644
--- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java
+++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java
@@ -107,9 +107,9 @@ public class ZLMMediaNodeServerService implements IMediaNodeServerService {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), "读取配置失败");
         }
         mediaServer.setId(zlmServerConfig.getGeneralMediaServerId());
-        mediaServer.setHttpSSlPort(zlmServerConfig.getHttpPort());
-        mediaServer.setFlvSSLPort(zlmServerConfig.getHttpPort());
-        mediaServer.setWsFlvSSLPort(zlmServerConfig.getHttpPort());
+        mediaServer.setHttpSSlPort(zlmServerConfig.getHttpSSLport());
+        mediaServer.setFlvSSLPort(zlmServerConfig.getHttpSSLport());
+        mediaServer.setWsFlvSSLPort(zlmServerConfig.getHttpSSLport());
         mediaServer.setRtmpPort(zlmServerConfig.getRtmpPort());
         mediaServer.setRtmpSSlPort(zlmServerConfig.getRtmpSslPort());
         mediaServer.setRtspPort(zlmServerConfig.getRtspPort());

From 3c90533999facb19b3b030099b1fd97b8e31dee2 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Wed, 11 Dec 2024 15:17:17 +0800
Subject: [PATCH 25/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BD=95=E5=88=B6?=
 =?UTF-8?q?=E8=AE=A1=E5=88=92=E5=8F=96=E6=B6=88=E9=80=9A=E9=81=93=E5=85=B3?=
 =?UTF-8?q?=E8=81=94=E6=97=B6=E6=9C=AA=E5=8F=8A=E6=97=B6=E5=81=9C=E6=AD=A2?=
 =?UTF-8?q?=E9=80=9A=E9=81=93=E7=82=B9=E6=92=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../iot/vmp/service/impl/RecordPlanServiceImpl.java         | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
index ac146e867..ca55b4d3a 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/RecordPlanServiceImpl.java
@@ -262,11 +262,7 @@ public class RecordPlanServiceImpl implements IRecordPlanService {
             channelMapper.addRecordPlan(channelIds, planId);
         }
         // 查看当前的待录制列表是否变化,如果变化,则调用录制计划马上开始录制
-        List<Integer> currentChannelRecord = queryCurrentChannelRecord();
-        recordStreamMap.keySet().forEach(currentChannelRecord::remove);
-        if (!currentChannelRecord.isEmpty()) {
-            execution();
-        }
+        execution();
     }
 
     @Override

From 3afdfff5b29df492a4913d02e49999c0646193c7 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Wed, 11 Dec 2024 16:59:48 +0800
Subject: [PATCH 26/38] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=82=B9=E6=92=AD?=
 =?UTF-8?q?=E5=90=8C=E4=B8=80=E4=B8=AAredis=E4=B8=8B=E7=9A=84=E5=85=B6?=
 =?UTF-8?q?=E4=BB=96wvp?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gb28181/controller/PlayController.java    | 27 +++++-----
 .../service/IGbChannelRpcPlayService.java     |  8 +++
 .../iot/vmp/gb28181/service/IPlayService.java |  3 ++
 .../gb28181/service/impl/PlayRpcService.java  | 54 +++++++++++++++++++
 .../gb28181/service/impl/PlayServiceImpl.java | 11 +++-
 .../redisMsg/control/RedisRpcController.java  | 47 +++++++++++++++-
 6 files changed, 136 insertions(+), 14 deletions(-)
 create mode 100644 src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelRpcPlayService.java
 create mode 100644 src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayRpcService.java

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
index 003478f90..d9f7e7317 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
@@ -10,15 +10,13 @@ import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
 import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction;
-import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
-import com.genersoft.iot.vmp.gb28181.service.IDeviceService;
-import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService;
-import com.genersoft.iot.vmp.gb28181.service.IPlayService;
+import com.genersoft.iot.vmp.gb28181.service.*;
 import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
 import com.genersoft.iot.vmp.media.bean.MediaServer;
 import com.genersoft.iot.vmp.media.service.IMediaServerService;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult;
@@ -64,6 +62,9 @@ public class PlayController {
 	@Autowired
 	private IPlayService playService;
 
+	@Autowired
+	private IGbChannelRpcPlayService playRpcService;
+
 	@Autowired
 	private IMediaServerService mediaServerService;
 
@@ -88,14 +89,10 @@ public class PlayController {
 		Assert.notNull(channelId, "通道国标编号不可为NULL");
 		// 获取可用的zlm
 		Device device = deviceService.getDeviceByDeviceId(deviceId);
-
 		Assert.notNull(deviceId, "设备不存在");
 		DeviceChannel channel = deviceChannelService.getOne(deviceId, channelId);
 		Assert.notNull(channel, "通道不存在");
 
-
-		MediaServer newMediaServerItem = playService.getNewMediaServerItem(device);
-
 		RequestMessage requestMessage = new RequestMessage();
 		String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId;
 		requestMessage.setKey(key);
@@ -118,7 +115,7 @@ public class PlayController {
 		// 录像查询以channelId作为deviceId查询
 		resultHolder.put(key, uuid, result);
 
-		playService.play(newMediaServerItem, deviceId, channelId, null, (code, msg, streamInfo) -> {
+		ErrorCallback<StreamInfo> callback  = (code, msg, streamInfo) -> {
 			WVPResult<StreamContent> wvpResult = new WVPResult<>();
 			if (code == InviteErrorCode.SUCCESS.getCode()) {
 				wvpResult.setCode(ErrorCode.SUCCESS.getCode());
@@ -136,8 +133,8 @@ public class PlayController {
 						}
 						streamInfo.channgeStreamIp(host);
 					}
-					if (!ObjectUtils.isEmpty(newMediaServerItem.getTranscodeSuffix()) && !"null".equalsIgnoreCase(newMediaServerItem.getTranscodeSuffix())) {
-						streamInfo.setStream(streamInfo.getStream() + "_" + newMediaServerItem.getTranscodeSuffix());
+					if (!ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) {
+						streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix());
 					}
 					wvpResult.setData(new StreamContent(streamInfo));
 				}else {
@@ -151,7 +148,13 @@ public class PlayController {
 			requestMessage.setData(wvpResult);
 			// 此处必须释放所有请求
 			resultHolder.invokeAllResult(requestMessage);
-		});
+		};
+		// 判断设备是否属于当前平台, 如果不属于则发起自动调用
+		if (userSetting.getServerId().equals(device.getServerId())) {
+			playRpcService.play(device.getServerId(), channel.getId(), callback);
+		}else {
+			playService.play(device, channel, callback);
+		}
 		return result;
 	}
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelRpcPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelRpcPlayService.java
new file mode 100644
index 000000000..1847e4307
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelRpcPlayService.java
@@ -0,0 +1,8 @@
+package com.genersoft.iot.vmp.gb28181.service;
+
+import com.genersoft.iot.vmp.common.StreamInfo;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
+
+public interface IGbChannelRpcPlayService {
+    void play(String serverId, Integer channelId, ErrorCallback<StreamInfo> callback);
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java
index aa2ca3ab6..4a1403a04 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java
@@ -25,6 +25,8 @@ public interface IPlayService {
 
     SSRCInfo play(MediaServer mediaServerItem, String deviceId, String channelId, String ssrc, ErrorCallback<StreamInfo> callback);
 
+    void play(Device device, DeviceChannel channel, ErrorCallback<StreamInfo> callback);
+
     StreamInfo onPublishHandlerForPlay(MediaServer mediaServerItem, MediaInfo mediaInfo, Device device, DeviceChannel channel);
 
     MediaServer getNewMediaServerItem(Device device);
@@ -70,4 +72,5 @@ public interface IPlayService {
 
     void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback<StreamInfo> callback);
 
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayRpcService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayRpcService.java
new file mode 100644
index 000000000..96eb3ced6
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayRpcService.java
@@ -0,0 +1,54 @@
+package com.genersoft.iot.vmp.gb28181.service.impl;
+
+import com.alibaba.fastjson2.JSON;
+import com.genersoft.iot.vmp.common.StreamInfo;
+import com.genersoft.iot.vmp.conf.UserSetting;
+import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
+import com.genersoft.iot.vmp.gb28181.service.IGbChannelRpcPlayService;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.sip.message.Response;
+
+@Slf4j
+@Service("playRpcService")
+public class PlayRpcService implements IGbChannelRpcPlayService {
+
+    @Autowired
+    private RedisRpcConfig redisRpcConfig;
+
+    @Autowired
+    private UserSetting userSetting;
+
+
+    private RedisRpcRequest buildRequest(String uri, Object param) {
+        RedisRpcRequest request = new RedisRpcRequest();
+        request.setFromId(userSetting.getServerId());
+        request.setParam(param);
+        request.setUri(uri);
+        return request;
+    }
+
+    @Override
+    public void play(String serverId, Integer channelId, ErrorCallback<StreamInfo> callback) {
+        log.info("[点播其他WVP的设备] 通道Id:{}", channelId);
+        RedisRpcRequest request = buildRequest("playChannel", channelId);
+        request.setToId(serverId);
+        RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout());
+        if (response == null) {
+            callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
+        }else {
+            if (response.getStatusCode() == Response.OK) {
+                StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class);
+                callback.run(response.getStatusCode(), "success", streamInfo);
+            }else {
+                callback.run(response.getStatusCode(), response.getBody().toString(), null);
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
index 2c9b777b8..8b3d9f067 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
@@ -63,7 +63,7 @@ import java.util.Vector;
 
 @SuppressWarnings(value = {"rawtypes", "unchecked"})
 @Slf4j
-@Service
+@Service("playService")
 public class PlayServiceImpl implements IPlayService {
 
     @Autowired
@@ -285,6 +285,15 @@ public class PlayServiceImpl implements IPlayService {
         }
     }
 
+    @Override
+    public void play(Device device, DeviceChannel channel, ErrorCallback<StreamInfo> callback) {
+        MediaServer mediaServerItem = getNewMediaServerItem(device);
+        if (mediaServerItem == null) {
+            log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", device.getDeviceId(), channel.getDeviceId());
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm");
+        }
+        play(mediaServerItem, device, channel, null, callback);
+    }
 
     @Override
     public SSRCInfo play(MediaServer mediaServerItem, String deviceId, String channelId, String ssrc, ErrorCallback<StreamInfo> callback) {
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcController.java
index b74df842f..946178bdf 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcController.java
@@ -8,7 +8,10 @@ import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
+import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo;
+import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
+import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
 import com.genersoft.iot.vmp.gb28181.session.SSRCFactory;
 import com.genersoft.iot.vmp.media.bean.MediaInfo;
 import com.genersoft.iot.vmp.media.bean.MediaServer;
@@ -17,6 +20,7 @@ import com.genersoft.iot.vmp.media.event.hook.HookSubscribe;
 import com.genersoft.iot.vmp.media.event.hook.HookType;
 import com.genersoft.iot.vmp.media.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.ISendRtpServerService;
+import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import lombok.extern.slf4j.Slf4j;
@@ -24,6 +28,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Component;
 
+import javax.sip.message.Response;
+
 /**
  * 其他wvp发起的rpc调用,这里的方法被 RedisRpcConfig 通过反射寻找对应的方法名称调用
  */
@@ -49,6 +55,12 @@ public class RedisRpcController {
     @Autowired
     private RedisTemplate<Object, Object> redisTemplate;
 
+    @Autowired
+    private IGbChannelService channelService;
+
+    @Autowired
+    private IGbChannelPlayService channelPlayService;
+
 
     /**
      * 获取发流的信息
@@ -255,7 +267,7 @@ public class RedisRpcController {
         String callId = request.getParam().toString();
         SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
+        response.setStatusCode(Response.OK);
         if (sendRtpItem == null) {
             log.info("[redis-rpc] 停止推流, 未找到redis中的发流信息, key:{}", callId);
             WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息");
@@ -290,4 +302,37 @@ public class RedisRpcController {
         message.setResponse(response);
         redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message);
     }
+
+    /**
+     * 点播国标设备
+     */
+    public RedisRpcResponse playChannel(RedisRpcRequest request) {
+        int channelId = Integer.parseInt(request.getParam().toString());
+        RedisRpcResponse response = request.getResponse();
+
+        if (channelId <= 0) {
+            response.setStatusCode(Response.BAD_REQUEST);
+            response.setBody("param error");
+            return response;
+        }
+        // 获取对应的设备和通道信息
+        CommonGBChannel channel = channelService.getOne(channelId);
+        if (channel == null) {
+            response.setStatusCode(Response.BAD_REQUEST);
+            response.setBody("param error");
+            return response;
+        }
+
+        channelPlayService.play(channel, null, (code, msg, data) ->{
+            if (code == InviteErrorCode.SUCCESS.getCode()) {
+                response.setStatusCode(Response.OK);
+                response.setBody(data);
+            }else {
+                response.setStatusCode(code);
+            }
+            // 手动发送结果
+            sendResponse(response);
+        });
+        return null;
+    }
 }

From 8ef71b0f2d6dd5e9c7d2bb39b9766ec177a90898 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Wed, 11 Dec 2024 18:26:18 +0800
Subject: [PATCH 27/38] =?UTF-8?q?[redis=E4=B8=8B=E7=9A=84=E5=A4=9Awvp]=20?=
 =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9C=8D=E5=8A=A1=E9=97=B4=E6=96=B9=E6=B3=95?=
 =?UTF-8?q?=E8=B0=83=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../iot/vmp/conf/redis/RedisRpcConfig.java    |  61 ++++--
 .../conf/redis/bean/RedisRpcClassHandler.java |  17 ++
 .../gb28181/controller/PlayController.java    |  10 +-
 .../iot/vmp/media/MediaServerConfig.java      |   5 +
 .../iot/vmp/media/bean/MediaServer.java       |  11 +
 .../service/impl/MediaServerServiceImpl.java  |  22 +-
 .../media/zlm/ZLMMediaNodeServerService.java  |   1 +
 .../redisMsg/IRedisRpcPlayService.java}       |   6 +-
 .../service/redisMsg/IRedisRpcService.java    |   1 +
 .../control/RedisRpcSendRtpController.java    | 177 +++++++++++++++++
 ...java => RedisRpcStreamPushController.java} | 188 ++----------------
 .../redisMsg/dto/RedisRpcController.java      |  16 ++
 .../service/redisMsg/dto/RedisRpcMapping.java |  13 ++
 .../service/RedisRpcPlayServiceImpl.java}     |  11 +-
 .../redisMsg/service/RedisRpcServiceImpl.java |  16 +-
 .../vmp/storager/dao/MediaServerMapper.java   |  34 ++--
 数据库/2.7.3/初始化-mysql-2.7.3.sql           |   2 +
 .../2.7.3/初始化-postgresql-kingbase-2.7.3.sql |   2 +
 18 files changed, 354 insertions(+), 239 deletions(-)
 create mode 100644 src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java
 rename src/main/java/com/genersoft/iot/vmp/{gb28181/service/IGbChannelRpcPlayService.java => service/redisMsg/IRedisRpcPlayService.java} (68%)
 create mode 100644 src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
 rename src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/{RedisRpcController.java => RedisRpcStreamPushController.java} (51%)
 create mode 100644 src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java
 create mode 100644 src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcMapping.java
 rename src/main/java/com/genersoft/iot/vmp/{gb28181/service/impl/PlayRpcService.java => service/redisMsg/service/RedisRpcPlayServiceImpl.java} (86%)

diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
index b762838c6..6607acecd 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
@@ -3,10 +3,13 @@ package com.genersoft.iot.vmp.conf.redis;
 import com.alibaba.fastjson2.JSON;
 import com.genersoft.iot.vmp.common.CommonCallback;
 import com.genersoft.iot.vmp.conf.UserSetting;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcClassHandler;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
-import com.genersoft.iot.vmp.service.redisMsg.control.RedisRpcController;
+import com.genersoft.iot.vmp.jt1078.util.ClassUtil;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -16,8 +19,11 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Component;
 
+import javax.annotation.PostConstruct;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.ConcurrentHashMap;
@@ -36,9 +42,6 @@ public class RedisRpcConfig implements MessageListener {
     @Autowired
     private UserSetting userSetting;
 
-    @Autowired
-    private RedisRpcController redisRpcController;
-
     @Autowired
     private RedisTemplate<Object, Object> redisTemplate;
 
@@ -48,6 +51,37 @@ public class RedisRpcConfig implements MessageListener {
     @Autowired
     private ThreadPoolTaskExecutor taskExecutor;
 
+    private final static Map<String, RedisRpcClassHandler> protocolHash = new HashMap<>();
+
+    // 启动时执行
+    @PostConstruct
+    public void init(){
+        List<Class<?>> classList = ClassUtil.getClassList("com.genersoft.iot.vmp.service.redisMsg.control", RedisRpcController.class);
+        for (Class<?> handlerClass : classList) {
+            String controllerPath = handlerClass.getAnnotation(RedisRpcController.class).value();
+            // 扫描其下的方法
+            Method[] methods = handlerClass.getDeclaredMethods();
+            for (Method method : methods) {
+                RedisRpcMapping annotation = method.getAnnotation(RedisRpcMapping.class);
+                if (annotation != null) {
+                    String methodPath =  annotation.value();
+                    if (methodPath != null) {
+                        protocolHash.put(controllerPath + "/" + methodPath, new RedisRpcClassHandler(handlerClass, method));
+                    }
+                }
+
+            }
+
+        }
+        for (String s : protocolHash.keySet()) {
+            System.out.println(s);
+        }
+        if (log.isDebugEnabled()) {
+            log.debug("消息ID缓存表 protocolHash:{}", protocolHash);
+        }
+    }
+
+
     @Override
     public void onMessage(Message message, byte[] pattern) {
         boolean isEmpty = taskQueue.isEmpty();
@@ -87,7 +121,9 @@ public class RedisRpcConfig implements MessageListener {
                 return;
             }
             log.info("[redis-rpc] << {}", request);
-            Method method = getMethod(request.getUri());
+            RedisRpcClassHandler redisRpcClassHandler = protocolHash.get(request.getUri());
+            Class<?> objectClass = redisRpcClassHandler.getObjectClass();
+            Method method = redisRpcClassHandler.getMethod();
             // 没有携带目标ID的可以理解为哪个wvp有结果就哪个回复,携带目标ID,但是如果是不存在的uri则直接回复404
             if (userSetting.getServerId().equals(request.getToId())) {
                 if (method == null) {
@@ -97,7 +133,7 @@ public class RedisRpcConfig implements MessageListener {
                     sendResponse(response);
                     return;
                 }
-                RedisRpcResponse response = (RedisRpcResponse)method.invoke(redisRpcController, request);
+                RedisRpcResponse response = (RedisRpcResponse)method.invoke(objectClass, request);
                 if(response != null) {
                     sendResponse(response);
                 }
@@ -105,7 +141,7 @@ public class RedisRpcConfig implements MessageListener {
                 if (method == null) {
                     return;
                 }
-                RedisRpcResponse response = (RedisRpcResponse)method.invoke(redisRpcController, request);
+                RedisRpcResponse response = (RedisRpcResponse)method.invoke(objectClass, request);
                 if (response != null) {
                     sendResponse(response);
                 }
@@ -116,17 +152,6 @@ public class RedisRpcConfig implements MessageListener {
 
     }
 
-    private Method getMethod(String name) {
-        // 启动后扫描所有的路径注解
-        Method[] methods = redisRpcController.getClass().getMethods();
-        for (Method method : methods) {
-            if (method.getName().equals(name)) {
-                return method;
-            }
-        }
-        return null;
-    }
-
     private void sendResponse(RedisRpcResponse response){
         log.info("[redis-rpc] >> {}", response);
         response.setToId(userSetting.getServerId());
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java
new file mode 100644
index 000000000..8c49a79f1
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java
@@ -0,0 +1,17 @@
+package com.genersoft.iot.vmp.conf.redis.bean;
+
+import lombok.Data;
+
+import java.lang.reflect.Method;
+
+@Data
+public class RedisRpcClassHandler {
+
+    private Class<?> objectClass;
+    private Method method;
+
+    public RedisRpcClassHandler(Class<?> objectClass, Method method) {
+        this.objectClass = objectClass;
+        this.method = method;
+    }
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
index d9f7e7317..e5087cbaa 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
@@ -10,7 +10,10 @@ import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
 import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction;
-import com.genersoft.iot.vmp.gb28181.service.*;
+import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
+import com.genersoft.iot.vmp.gb28181.service.IDeviceService;
+import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService;
+import com.genersoft.iot.vmp.gb28181.service.IPlayService;
 import com.genersoft.iot.vmp.gb28181.session.SipInviteSessionManager;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
@@ -18,6 +21,7 @@ import com.genersoft.iot.vmp.media.bean.MediaServer;
 import com.genersoft.iot.vmp.media.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
+import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
@@ -63,7 +67,7 @@ public class PlayController {
 	private IPlayService playService;
 
 	@Autowired
-	private IGbChannelRpcPlayService playRpcService;
+	private IRedisRpcPlayService redisRpcPlayService;
 
 	@Autowired
 	private IMediaServerService mediaServerService;
@@ -151,7 +155,7 @@ public class PlayController {
 		};
 		// 判断设备是否属于当前平台, 如果不属于则发起自动调用
 		if (userSetting.getServerId().equals(device.getServerId())) {
-			playRpcService.play(device.getServerId(), channel.getId(), callback);
+			redisRpcPlayService.play(device.getServerId(), channel.getId(), callback);
 		}else {
 			playService.play(device, channel, callback);
 		}
diff --git a/src/main/java/com/genersoft/iot/vmp/media/MediaServerConfig.java b/src/main/java/com/genersoft/iot/vmp/media/MediaServerConfig.java
index b9f7c08ef..ba27fb350 100755
--- a/src/main/java/com/genersoft/iot/vmp/media/MediaServerConfig.java
+++ b/src/main/java/com/genersoft/iot/vmp/media/MediaServerConfig.java
@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.media;
 
 import com.genersoft.iot.vmp.conf.MediaConfig;
+import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.media.bean.MediaServer;
 import com.genersoft.iot.vmp.media.event.mediaServer.MediaServerChangeEvent;
 import com.genersoft.iot.vmp.media.service.IMediaServerService;
@@ -30,6 +31,9 @@ public class MediaServerConfig implements CommandLineRunner {
     @Autowired
     private MediaConfig mediaConfig;
 
+    @Autowired
+    private UserSetting userSetting;
+
 
     @Override
     public void run(String... strings) throws Exception {
@@ -37,6 +41,7 @@ public class MediaServerConfig implements CommandLineRunner {
         mediaServerService.clearMediaServerForOnline();
         MediaServer defaultMediaServer = mediaServerService.getDefaultMediaServer();
         MediaServer mediaSerItemInConfig = mediaConfig.getMediaSerItem();
+        mediaSerItemInConfig.setServerId(userSetting.getServerId());
         if (defaultMediaServer != null && mediaSerItemInConfig.getId().equals(defaultMediaServer.getId())) {
             mediaServerService.update(mediaSerItemInConfig);
         }else {
diff --git a/src/main/java/com/genersoft/iot/vmp/media/bean/MediaServer.java b/src/main/java/com/genersoft/iot/vmp/media/bean/MediaServer.java
index e6faac364..2660cb6a9 100755
--- a/src/main/java/com/genersoft/iot/vmp/media/bean/MediaServer.java
+++ b/src/main/java/com/genersoft/iot/vmp/media/bean/MediaServer.java
@@ -103,6 +103,9 @@ public class MediaServer {
     @Schema(description = "转码的前缀")
     private String transcodeSuffix;
 
+    @Schema(description = "服务Id")
+    private String serverId;
+
     public MediaServer() {
     }
 
@@ -388,4 +391,12 @@ public class MediaServer {
     public void setTranscodeSuffix(String transcodeSuffix) {
         this.transcodeSuffix = transcodeSuffix;
     }
+
+    public String getServerId() {
+        return serverId;
+    }
+
+    public void setServerId(String serverId) {
+        this.serverId = serverId;
+    }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/media/service/impl/MediaServerServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/media/service/impl/MediaServerServiceImpl.java
index f755c5f52..3c38035e8 100755
--- a/src/main/java/com/genersoft/iot/vmp/media/service/impl/MediaServerServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/media/service/impl/MediaServerServiceImpl.java
@@ -303,7 +303,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
         mediaServerMapper.update(mediaSerItem);
         MediaServer mediaServerInRedis = getOne(mediaSerItem.getId());
         // 获取完整数据
-        MediaServer mediaServerInDataBase = mediaServerMapper.queryOne(mediaSerItem.getId());
+        MediaServer mediaServerInDataBase = mediaServerMapper.queryOne(mediaSerItem.getId(), userSetting.getServerId());
         if (mediaServerInDataBase == null) {
             return;
         }
@@ -350,7 +350,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
     @Override
     public List<MediaServer> getAll() {
-        List<MediaServer> mediaServerList = mediaServerMapper.queryAll();
+        List<MediaServer> mediaServerList = mediaServerMapper.queryAll(userSetting.getServerId());
         if (mediaServerList.isEmpty()) {
             return new ArrayList<>();
         }
@@ -366,7 +366,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
     @Override
     public List<MediaServer> getAllFromDatabase() {
-        return mediaServerMapper.queryAll();
+        return mediaServerMapper.queryAll(userSetting.getServerId());
     }
 
     @Override
@@ -403,7 +403,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
     @Override
     public MediaServer getDefaultMediaServer() {
-        return mediaServerMapper.queryDefault();
+        return mediaServerMapper.queryDefault(userSetting.getServerId());
     }
 
     @Override
@@ -423,7 +423,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
             log.info("[添加媒体节点] 失败, mediaServer的类型:为空");
             return;
         }
-        if (mediaServerMapper.queryOne(mediaServer.getId()) != null) {
+        if (mediaServerMapper.queryOne(mediaServer.getId(), userSetting.getServerId()) != null) {
             log.info("[添加媒体节点] 失败, 媒体服务ID已存在,请修改媒体服务器配置, {}", mediaServer.getId());
             throw new ControllerException(ErrorCode.ERROR100.getCode(),"保存失败,媒体服务ID [ " + mediaServer.getId() + " ] 已存在,请修改媒体服务器配置");
         }
@@ -521,7 +521,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
     @Override
     public MediaServer checkMediaServer(String ip, int port, String secret, String type) {
-        if (mediaServerMapper.queryOneByHostAndPort(ip, port) != null) {
+        if (mediaServerMapper.queryOneByHostAndPort(ip, port, userSetting.getServerId()) != null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), "此连接已存在");
         }
 
@@ -532,7 +532,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
         }
         MediaServer mediaServer = mediaNodeServerService.checkMediaServer(ip, port, secret);
         if (mediaServer != null) {
-            if (mediaServerMapper.queryOne(mediaServer.getId()) != null) {
+            if (mediaServerMapper.queryOne(mediaServer.getId(), userSetting.getServerId()) != null) {
                 throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体服务ID [" + mediaServer.getId() + " ] 已存在,请修改媒体服务器配置");
             }
         }
@@ -560,7 +560,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
     @Override
     public void delete(MediaServer mediaServer) {
-        mediaServerMapper.delOne(mediaServer.getId());
+        mediaServerMapper.delOne(mediaServer.getId(), userSetting.getServerId());
         redisTemplate.opsForZSet().remove(VideoManagerConstants.ONLINE_MEDIA_SERVERS_PREFIX + userSetting.getServerId(), mediaServer.getId());
         String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + ":" + mediaServer.getId();
         redisTemplate.delete(key);
@@ -572,13 +572,13 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
     @Override
     public MediaServer getOneFromDatabase(String mediaServerId) {
-        return mediaServerMapper.queryOne(mediaServerId);
+        return mediaServerMapper.queryOne(mediaServerId, userSetting.getServerId());
     }
 
     @Override
     public void syncCatchFromDatabase() {
         List<MediaServer> allInCatch = getAllOnlineList();
-        List<MediaServer> allInDatabase = mediaServerMapper.queryAll();
+        List<MediaServer> allInDatabase = mediaServerMapper.queryAll(userSetting.getServerId());
         Map<String, MediaServer> mediaServerMap = new HashMap<>();
 
         for (MediaServer mediaServer : allInDatabase) {
@@ -606,7 +606,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
     @Override
     public List<MediaServer> getAllWithAssistPort() {
-        return mediaServerMapper.queryAllWithAssistPort();
+        return mediaServerMapper.queryAllWithAssistPort(userSetting.getServerId());
     }
 
 
diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java
index 802b7eb6e..da44d23da 100644
--- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java
+++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaNodeServerService.java
@@ -89,6 +89,7 @@ public class ZLMMediaNodeServerService implements IMediaNodeServerService {
     @Override
     public MediaServer checkMediaServer(String ip, int port, String secret) {
         MediaServer mediaServer = new MediaServer();
+        mediaServer.setServerId(userSetting.getServerId());
         mediaServer.setIp(ip);
         mediaServer.setHttpPort(port);
         mediaServer.setFlvPort(port);
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelRpcPlayService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
similarity index 68%
rename from src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelRpcPlayService.java
rename to src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
index 1847e4307..1ae1268ce 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelRpcPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
@@ -1,8 +1,10 @@
-package com.genersoft.iot.vmp.gb28181.service;
+package com.genersoft.iot.vmp.service.redisMsg;
 
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 
-public interface IGbChannelRpcPlayService {
+public interface IRedisRpcPlayService {
+
+
     void play(String serverId, Integer channelId, ErrorCallback<StreamInfo> callback);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcService.java
index 7e23e2767..6dcfb5fb8 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcService.java
@@ -23,4 +23,5 @@ public interface IRedisRpcService {
 
     long onStreamOnlineEvent(String app, String stream, CommonCallback<StreamInfo> callback);
     void unPushStreamOnlineEvent(String app, String stream);
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
new file mode 100644
index 000000000..1077f5782
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
@@ -0,0 +1,177 @@
+package com.genersoft.iot.vmp.service.redisMsg.control;
+
+import com.genersoft.iot.vmp.conf.UserSetting;
+import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
+import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo;
+import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
+import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
+import com.genersoft.iot.vmp.gb28181.session.SSRCFactory;
+import com.genersoft.iot.vmp.media.bean.MediaInfo;
+import com.genersoft.iot.vmp.media.bean.MediaServer;
+import com.genersoft.iot.vmp.media.event.hook.HookSubscribe;
+import com.genersoft.iot.vmp.media.service.IMediaServerService;
+import com.genersoft.iot.vmp.service.ISendRtpServerService;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import javax.sip.message.Response;
+
+@Slf4j
+@RedisRpcController("sendRtp")
+public class RedisRpcSendRtpController {
+
+    @Autowired
+    private SSRCFactory ssrcFactory;
+
+    @Autowired
+    private IMediaServerService mediaServerService;
+
+    @Autowired
+    private ISendRtpServerService sendRtpServerService;
+
+    @Autowired
+    private UserSetting userSetting;
+
+    @Autowired
+    private HookSubscribe hookSubscribe;
+
+    @Autowired
+    private RedisTemplate<Object, Object> redisTemplate;
+
+    @Autowired
+    private IGbChannelService channelService;
+
+    @Autowired
+    private IGbChannelPlayService channelPlayService;
+
+    /**
+     * 获取发流的信息
+     */
+    @RedisRpcMapping("getSendRtpItem")
+    public RedisRpcResponse getSendRtpItem(RedisRpcRequest request) {
+        String callId = request.getParam().toString();
+        SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
+        if (sendRtpItem == null) {
+            log.info("[redis-rpc] 获取发流的信息, 未找到redis中的发流信息, callId:{}", callId);
+            RedisRpcResponse response = request.getResponse();
+            response.setStatusCode(200);
+            return response;
+        }
+        log.info("[redis-rpc] 获取发流的信息: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort());
+        // 查询本级是否有这个流
+        MediaServer mediaServerItem = mediaServerService.getMediaServerByAppAndStream(sendRtpItem.getApp(), sendRtpItem.getStream());
+        if (mediaServerItem == null) {
+            RedisRpcResponse response = request.getResponse();
+            response.setStatusCode(200);
+        }
+        // 自平台内容
+        int localPort = sendRtpServerService.getNextPort(mediaServerItem);
+        if (localPort == 0) {
+            log.info("[redis-rpc] getSendRtpItem->服务器端口资源不足" );
+            RedisRpcResponse response = request.getResponse();
+            response.setStatusCode(200);
+        }
+        // 写入redis, 超时时回复
+        sendRtpItem.setStatus(1);
+        sendRtpItem.setServerId(userSetting.getServerId());
+        sendRtpItem.setLocalIp(mediaServerItem.getSdpIp());
+        if (sendRtpItem.getSsrc() == null) {
+            // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式
+            String ssrc = "Play".equalsIgnoreCase(sendRtpItem.getSessionName()) ? ssrcFactory.getPlaySsrc(mediaServerItem.getId()) : ssrcFactory.getPlayBackSsrc(mediaServerItem.getId());
+            sendRtpItem.setSsrc(ssrc);
+        }
+        sendRtpServerService.update(sendRtpItem);
+        RedisRpcResponse response = request.getResponse();
+        response.setStatusCode(200);
+        response.setBody(callId);
+        return response;
+    }
+
+    /**
+     * 开始发流
+     */
+    @RedisRpcMapping("startSendRtp")
+    public RedisRpcResponse startSendRtp(RedisRpcRequest request) {
+        String callId = request.getParam().toString();
+        SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
+        RedisRpcResponse response = request.getResponse();
+        response.setStatusCode(200);
+        if (sendRtpItem == null) {
+            log.info("[redis-rpc] 开始发流, 未找到redis中的发流信息, callId:{}", callId);
+            WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息");
+            response.setBody(wvpResult);
+            return response;
+        }
+        log.info("[redis-rpc] 开始发流: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort());
+        MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId());
+        if (mediaServer == null) {
+            log.info("[redis-rpc] startSendRtp->未找到MediaServer: {}", sendRtpItem.getMediaServerId() );
+            WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到MediaServer");
+            response.setBody(wvpResult);
+            return response;
+        }
+        MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, sendRtpItem.getApp(), sendRtpItem.getStream());
+        if (mediaInfo == null) {
+            log.info("[redis-rpc] startSendRtp->流不在线: {}/{}", sendRtpItem.getApp(), sendRtpItem.getStream() );
+            WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "流不在线");
+            response.setBody(wvpResult);
+            return response;
+        }
+        try {
+            mediaServerService.startSendRtp(mediaServer, sendRtpItem);
+        }catch (ControllerException exception) {
+            log.info("[redis-rpc] 发流失败: {}/{}, 目标地址: {}:{}, {}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort(), exception.getMsg());
+            WVPResult wvpResult = WVPResult.fail(exception.getCode(), exception.getMsg());
+            response.setBody(wvpResult);
+            return response;
+        }
+        log.info("[redis-rpc] 发流成功: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort());
+        WVPResult wvpResult = WVPResult.success();
+        response.setBody(wvpResult);
+        return response;
+    }
+
+    /**
+     * 停止发流
+     */
+    @RedisRpcMapping("stopSendRtp")
+    public RedisRpcResponse stopSendRtp(RedisRpcRequest request) {
+        String callId = request.getParam().toString();
+        SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
+        RedisRpcResponse response = request.getResponse();
+        response.setStatusCode(Response.OK);
+        if (sendRtpItem == null) {
+            log.info("[redis-rpc] 停止推流, 未找到redis中的发流信息, key:{}", callId);
+            WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息");
+            response.setBody(wvpResult);
+            return response;
+        }
+        log.info("[redis-rpc] 停止推流: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() );
+        MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId());
+        if (mediaServer == null) {
+            log.info("[redis-rpc] stopSendRtp->未找到MediaServer: {}", sendRtpItem.getMediaServerId() );
+            WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到MediaServer");
+            response.setBody(wvpResult);
+            return response;
+        }
+        try {
+            mediaServerService.stopSendRtp(mediaServer, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc());
+        }catch (ControllerException exception) {
+            log.info("[redis-rpc] 停止推流失败: {}/{}, 目标地址: {}:{}, code: {}, msg: {}", sendRtpItem.getApp(),
+                    sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort(), exception.getCode(), exception.getMsg() );
+            response.setBody(WVPResult.fail(exception.getCode(), exception.getMsg()));
+            return response;
+        }
+        log.info("[redis-rpc] 停止推流成功: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() );
+        response.setBody(WVPResult.success());
+        return response;
+    }
+
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
similarity index 51%
rename from src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcController.java
rename to src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
index 946178bdf..3465bdc8e 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
@@ -3,39 +3,29 @@ package com.genersoft.iot.vmp.service.redisMsg.control;
 import com.alibaba.fastjson2.JSONObject;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.conf.UserSetting;
-import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
-import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
 import com.genersoft.iot.vmp.gb28181.session.SSRCFactory;
-import com.genersoft.iot.vmp.media.bean.MediaInfo;
 import com.genersoft.iot.vmp.media.bean.MediaServer;
 import com.genersoft.iot.vmp.media.event.hook.Hook;
 import com.genersoft.iot.vmp.media.event.hook.HookSubscribe;
 import com.genersoft.iot.vmp.media.event.hook.HookType;
 import com.genersoft.iot.vmp.media.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.ISendRtpServerService;
-import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
-import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
-import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.stereotype.Component;
 
-import javax.sip.message.Response;
-
-/**
- * 其他wvp发起的rpc调用,这里的方法被 RedisRpcConfig 通过反射寻找对应的方法名称调用
- */
 @Slf4j
-@Component
-public class RedisRpcController {
+@RedisRpcController("streamPush")
+public class RedisRpcStreamPushController {
 
     @Autowired
     private SSRCFactory ssrcFactory;
@@ -61,52 +51,18 @@ public class RedisRpcController {
     @Autowired
     private IGbChannelPlayService channelPlayService;
 
-
-    /**
-     * 获取发流的信息
-     */
-    public RedisRpcResponse getSendRtpItem(RedisRpcRequest request) {
-        String callId = request.getParam().toString();
-        SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
-        if (sendRtpItem == null) {
-            log.info("[redis-rpc] 获取发流的信息, 未找到redis中的发流信息, callId:{}", callId);
-            RedisRpcResponse response = request.getResponse();
-            response.setStatusCode(200);
-            return response;
-        }
-        log.info("[redis-rpc] 获取发流的信息: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort());
-        // 查询本级是否有这个流
-        MediaServer mediaServerItem = mediaServerService.getMediaServerByAppAndStream(sendRtpItem.getApp(), sendRtpItem.getStream());
-        if (mediaServerItem == null) {
-            RedisRpcResponse response = request.getResponse();
-            response.setStatusCode(200);
-        }
-        // 自平台内容
-        int localPort = sendRtpServerService.getNextPort(mediaServerItem);
-        if (localPort == 0) {
-            log.info("[redis-rpc] getSendRtpItem->服务器端口资源不足" );
-            RedisRpcResponse response = request.getResponse();
-            response.setStatusCode(200);
-        }
-        // 写入redis, 超时时回复
-        sendRtpItem.setStatus(1);
-        sendRtpItem.setServerId(userSetting.getServerId());
-        sendRtpItem.setLocalIp(mediaServerItem.getSdpIp());
-        if (sendRtpItem.getSsrc() == null) {
-            // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式
-            String ssrc = "Play".equalsIgnoreCase(sendRtpItem.getSessionName()) ? ssrcFactory.getPlaySsrc(mediaServerItem.getId()) : ssrcFactory.getPlayBackSsrc(mediaServerItem.getId());
-            sendRtpItem.setSsrc(ssrc);
-        }
-        sendRtpServerService.update(sendRtpItem);
-        RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
-        response.setBody(callId);
-        return response;
+    private void sendResponse(RedisRpcResponse response){
+        log.info("[redis-rpc] >> {}", response);
+        response.setToId(userSetting.getServerId());
+        RedisRpcMessage message = new RedisRpcMessage();
+        message.setResponse(response);
+        redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message);
     }
 
     /**
      * 监听流上线
      */
+    @RedisRpcMapping("waitePushStreamOnline")
     public RedisRpcResponse waitePushStreamOnline(RedisRpcRequest request) {
         SendRtpInfo sendRtpItem = JSONObject.parseObject(request.getParam().toString(), SendRtpInfo.class);
         log.info("[redis-rpc] 监听流上线: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort());
@@ -158,6 +114,7 @@ public class RedisRpcController {
     /**
      * 监听流上线
      */
+    @RedisRpcMapping("onStreamOnlineEvent")
     public RedisRpcResponse onStreamOnlineEvent(RedisRpcRequest request) {
         StreamInfo streamInfo = JSONObject.parseObject(request.getParam().toString(), StreamInfo.class);
         log.info("[redis-rpc] 监听流信息,等待流上线: {}/{}", streamInfo.getApp(), streamInfo.getStream());
@@ -191,6 +148,7 @@ public class RedisRpcController {
     /**
      * 停止监听流上线
      */
+    @RedisRpcMapping("stopWaitePushStreamOnline")
     public RedisRpcResponse stopWaitePushStreamOnline(RedisRpcRequest request) {
         SendRtpInfo sendRtpItem = JSONObject.parseObject(request.getParam().toString(), SendRtpInfo.class);
         log.info("[redis-rpc] 停止监听流上线: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() );
@@ -205,6 +163,7 @@ public class RedisRpcController {
     /**
      * 停止监听流上线
      */
+    @RedisRpcMapping("unPushStreamOnlineEvent")
     public RedisRpcResponse unPushStreamOnlineEvent(RedisRpcRequest request) {
         StreamInfo streamInfo = JSONObject.parseObject(request.getParam().toString(), StreamInfo.class);
         log.info("[redis-rpc] 停止监听流上线: {}/{}", streamInfo.getApp(), streamInfo.getStream());
@@ -216,123 +175,4 @@ public class RedisRpcController {
         return response;
     }
 
-
-    /**
-     * 开始发流
-     */
-    public RedisRpcResponse startSendRtp(RedisRpcRequest request) {
-        String callId = request.getParam().toString();
-        SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
-        RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
-        if (sendRtpItem == null) {
-            log.info("[redis-rpc] 开始发流, 未找到redis中的发流信息, callId:{}", callId);
-            WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息");
-            response.setBody(wvpResult);
-            return response;
-        }
-        log.info("[redis-rpc] 开始发流: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort());
-        MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId());
-        if (mediaServer == null) {
-            log.info("[redis-rpc] startSendRtp->未找到MediaServer: {}", sendRtpItem.getMediaServerId() );
-            WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到MediaServer");
-            response.setBody(wvpResult);
-            return response;
-        }
-        MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, sendRtpItem.getApp(), sendRtpItem.getStream());
-        if (mediaInfo == null) {
-            log.info("[redis-rpc] startSendRtp->流不在线: {}/{}", sendRtpItem.getApp(), sendRtpItem.getStream() );
-            WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "流不在线");
-            response.setBody(wvpResult);
-            return response;
-        }
-        try {
-            mediaServerService.startSendRtp(mediaServer, sendRtpItem);
-        }catch (ControllerException exception) {
-            log.info("[redis-rpc] 发流失败: {}/{}, 目标地址: {}:{}, {}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort(), exception.getMsg());
-            WVPResult wvpResult = WVPResult.fail(exception.getCode(), exception.getMsg());
-            response.setBody(wvpResult);
-            return response;
-        }
-        log.info("[redis-rpc] 发流成功: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort());
-        WVPResult wvpResult = WVPResult.success();
-        response.setBody(wvpResult);
-        return response;
-    }
-
-    /**
-     * 停止发流
-     */
-    public RedisRpcResponse stopSendRtp(RedisRpcRequest request) {
-        String callId = request.getParam().toString();
-        SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
-        RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(Response.OK);
-        if (sendRtpItem == null) {
-            log.info("[redis-rpc] 停止推流, 未找到redis中的发流信息, key:{}", callId);
-            WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息");
-            response.setBody(wvpResult);
-            return response;
-        }
-        log.info("[redis-rpc] 停止推流: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() );
-        MediaServer mediaServer = mediaServerService.getOne(sendRtpItem.getMediaServerId());
-        if (mediaServer == null) {
-            log.info("[redis-rpc] stopSendRtp->未找到MediaServer: {}", sendRtpItem.getMediaServerId() );
-            WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到MediaServer");
-            response.setBody(wvpResult);
-            return response;
-        }
-        try {
-            mediaServerService.stopSendRtp(mediaServer, sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getSsrc());
-        }catch (ControllerException exception) {
-            log.info("[redis-rpc] 停止推流失败: {}/{}, 目标地址: {}:{}, code: {}, msg: {}", sendRtpItem.getApp(),
-                    sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort(), exception.getCode(), exception.getMsg() );
-            response.setBody(WVPResult.fail(exception.getCode(), exception.getMsg()));
-            return response;
-        }
-        log.info("[redis-rpc] 停止推流成功: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort() );
-        response.setBody(WVPResult.success());
-        return response;
-    }
-
-    private void sendResponse(RedisRpcResponse response){
-        log.info("[redis-rpc] >> {}", response);
-        response.setToId(userSetting.getServerId());
-        RedisRpcMessage message = new RedisRpcMessage();
-        message.setResponse(response);
-        redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message);
-    }
-
-    /**
-     * 点播国标设备
-     */
-    public RedisRpcResponse playChannel(RedisRpcRequest request) {
-        int channelId = Integer.parseInt(request.getParam().toString());
-        RedisRpcResponse response = request.getResponse();
-
-        if (channelId <= 0) {
-            response.setStatusCode(Response.BAD_REQUEST);
-            response.setBody("param error");
-            return response;
-        }
-        // 获取对应的设备和通道信息
-        CommonGBChannel channel = channelService.getOne(channelId);
-        if (channel == null) {
-            response.setStatusCode(Response.BAD_REQUEST);
-            response.setBody("param error");
-            return response;
-        }
-
-        channelPlayService.play(channel, null, (code, msg, data) ->{
-            if (code == InviteErrorCode.SUCCESS.getCode()) {
-                response.setStatusCode(Response.OK);
-                response.setBody(data);
-            }else {
-                response.setStatusCode(code);
-            }
-            // 手动发送结果
-            sendResponse(response);
-        });
-        return null;
-    }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java
new file mode 100644
index 000000000..79cdcb370
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java
@@ -0,0 +1,16 @@
+package com.genersoft.iot.vmp.service.redisMsg.dto;
+
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.*;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Component
+public @interface RedisRpcController {
+    /**
+     * 请求路径
+     */
+    String value() default "";
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcMapping.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcMapping.java
new file mode 100644
index 000000000..61f51bb68
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcMapping.java
@@ -0,0 +1,13 @@
+package com.genersoft.iot.vmp.service.redisMsg.dto;
+
+import java.lang.annotation.*;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RedisRpcMapping {
+    /**
+     * 请求路径
+     */
+    String value() default "";
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayRpcService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
similarity index 86%
rename from src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayRpcService.java
rename to src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
index 96eb3ced6..93f218a6c 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayRpcService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
@@ -1,4 +1,4 @@
-package com.genersoft.iot.vmp.gb28181.service.impl;
+package com.genersoft.iot.vmp.service.redisMsg.service;
 
 import com.alibaba.fastjson2.JSON;
 import com.genersoft.iot.vmp.common.StreamInfo;
@@ -6,8 +6,8 @@ import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
-import com.genersoft.iot.vmp.gb28181.service.IGbChannelRpcPlayService;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
+import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -16,8 +16,9 @@ import org.springframework.stereotype.Service;
 import javax.sip.message.Response;
 
 @Slf4j
-@Service("playRpcService")
-public class PlayRpcService implements IGbChannelRpcPlayService {
+@Service
+public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
+
 
     @Autowired
     private RedisRpcConfig redisRpcConfig;
@@ -36,7 +37,6 @@ public class PlayRpcService implements IGbChannelRpcPlayService {
 
     @Override
     public void play(String serverId, Integer channelId, ErrorCallback<StreamInfo> callback) {
-        log.info("[点播其他WVP的设备] 通道Id:{}", channelId);
         RedisRpcRequest request = buildRequest("playChannel", channelId);
         request.setToId(serverId);
         RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout());
@@ -52,3 +52,4 @@ public class PlayRpcService implements IGbChannelRpcPlayService {
         }
     }
 }
+
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcServiceImpl.java
index 861821353..939dcb05e 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcServiceImpl.java
@@ -58,7 +58,7 @@ public class RedisRpcServiceImpl implements IRedisRpcService {
 
     @Override
     public SendRtpInfo getSendRtpItem(String callId) {
-        RedisRpcRequest request = buildRequest("getSendRtpItem", callId);
+        RedisRpcRequest request = buildRequest("sendRtp/getSendRtpItem", callId);
         RedisRpcResponse response = redisRpcConfig.request(request, 10);
         if (response.getBody() == null) {
             return null;
@@ -69,7 +69,7 @@ public class RedisRpcServiceImpl implements IRedisRpcService {
     @Override
     public WVPResult startSendRtp(String callId, SendRtpInfo sendRtpItem) {
         log.info("[请求其他WVP] 开始推流,wvp:{}, {}/{}", sendRtpItem.getServerId(), sendRtpItem.getApp(), sendRtpItem.getStream());
-        RedisRpcRequest request = buildRequest("startSendRtp", callId);
+        RedisRpcRequest request = buildRequest("sendRtp/startSendRtp", callId);
         request.setToId(sendRtpItem.getServerId());
         RedisRpcResponse response = redisRpcConfig.request(request, 10);
         return JSON.parseObject(response.getBody().toString(), WVPResult.class);
@@ -83,7 +83,7 @@ public class RedisRpcServiceImpl implements IRedisRpcService {
             return WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到发流信息");
         }
         log.info("[请求其他WVP] 停止推流,wvp:{}, {}/{}", sendRtpItem.getServerId(), sendRtpItem.getApp(), sendRtpItem.getStream());
-        RedisRpcRequest request = buildRequest("stopSendRtp", callId);
+        RedisRpcRequest request = buildRequest("sendRtp/stopSendRtp", callId);
         request.setToId(sendRtpItem.getServerId());
         RedisRpcResponse response = redisRpcConfig.request(request, 10);
         return JSON.parseObject(response.getBody().toString(), WVPResult.class);
@@ -94,7 +94,7 @@ public class RedisRpcServiceImpl implements IRedisRpcService {
         log.info("[请求所有WVP监听流上线] {}/{}", sendRtpItem.getApp(), sendRtpItem.getStream());
         // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者
         Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null);
-        RedisRpcRequest request = buildRequest("waitePushStreamOnline", sendRtpItem);
+        RedisRpcRequest request = buildRequest("streamPush/waitePushStreamOnline", sendRtpItem);
         request.setToId(sendRtpItem.getServerId());
         hookSubscribe.addSubscribe(hook, (hookData) -> {
 
@@ -135,7 +135,7 @@ public class RedisRpcServiceImpl implements IRedisRpcService {
         log.info("[停止WVP监听流上线] {}/{}", sendRtpItem.getApp(), sendRtpItem.getStream());
         Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null);
         hookSubscribe.removeSubscribe(hook);
-        RedisRpcRequest request = buildRequest("stopWaitePushStreamOnline", sendRtpItem);
+        RedisRpcRequest request = buildRequest("streamPush/stopWaitePushStreamOnline", sendRtpItem);
         request.setToId(sendRtpItem.getServerId());
         redisRpcConfig.request(request, 10);
     }
@@ -147,7 +147,7 @@ public class RedisRpcServiceImpl implements IRedisRpcService {
             log.info("[停止WVP监听流上线] 未找到redis中的发流信息, key:{}", callId);
             return;
         }
-        RedisRpcRequest request = buildRequest("rtpSendStopped", callId);
+        RedisRpcRequest request = buildRequest("streamPush/rtpSendStopped", callId);
         request.setToId(sendRtpItem.getServerId());
         redisRpcConfig.request(request, 10);
     }
@@ -166,7 +166,7 @@ public class RedisRpcServiceImpl implements IRedisRpcService {
         StreamInfo streamInfoParam = new StreamInfo();
         streamInfoParam.setApp(app);
         streamInfoParam.setStream(stream);
-        RedisRpcRequest request = buildRequest("onStreamOnlineEvent", streamInfoParam);
+        RedisRpcRequest request = buildRequest("streamPush/onStreamOnlineEvent", streamInfoParam);
         hookSubscribe.addSubscribe(hook, (hookData) -> {
             log.info("[请求所有WVP监听流上线] 监听流上线 {}/{}", app, stream);
             if (callback != null) {
@@ -198,7 +198,7 @@ public class RedisRpcServiceImpl implements IRedisRpcService {
         StreamInfo streamInfoParam = new StreamInfo();
         streamInfoParam.setApp(app);
         streamInfoParam.setStream(stream);
-        RedisRpcRequest request = buildRequest("unPushStreamOnlineEvent", streamInfoParam);
+        RedisRpcRequest request = buildRequest("streamPush/unPushStreamOnlineEvent", streamInfoParam);
         redisRpcConfig.request(request, 10);
     }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java
index 2f2bfb6d7..8682461fe 100755
--- a/src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java
@@ -42,6 +42,7 @@ public interface MediaServerMapper {
             "create_time,"+
             "update_time,"+
             "transcode_suffix,"+
+            "server_id,"+
             "hook_alive_interval"+
             ") VALUES " +
             "(" +
@@ -74,6 +75,7 @@ public interface MediaServerMapper {
             "#{createTime}, " +
             "#{updateTime}, " +
             "#{transcodeSuffix}, " +
+            "#{serverId}, " +
             "#{hookAliveInterval})")
     int add(MediaServer mediaServerItem);
 
@@ -105,6 +107,7 @@ public interface MediaServerMapper {
             "<if test=\"recordDay != null\">, record_day=#{recordDay}</if>" +
             "<if test=\"recordPath != null\">, record_path=#{recordPath}</if>" +
             "<if test=\"transcodeSuffix != null\">, transcode_suffix=#{transcodeSuffix}</if>" +
+            "<if test=\"serverId != null\">, server_id=#{serverId}</if>" +
             "<if test=\"type != null\">, type=#{type}</if>" +
             "WHERE id=#{id}"+
             " </script>"})
@@ -138,32 +141,27 @@ public interface MediaServerMapper {
             "<if test=\"type != null\">, type=#{type}</if>" +
             "<if test=\"transcodeSuffix != null\">, transcode_suffix=#{transcodeSuffix}</if>" +
             "<if test=\"hookAliveInterval != null\">, hook_alive_interval=#{hookAliveInterval}</if>" +
+            "<if test=\"serverId != null\">, server_id=#{serverId}</if>" +
             "WHERE ip=#{ip} and http_port=#{httpPort}"+
             " </script>"})
     int updateByHostAndPort(MediaServer mediaServerItem);
 
-    @Select("SELECT * FROM wvp_media_server WHERE id=#{id}")
-    MediaServer queryOne(String id);
+    @Select("SELECT * FROM wvp_media_server WHERE id=#{id} and server_id = #{serverId}")
+    MediaServer queryOne(@Param("id") String id, @Param("serverId") String serverId);
 
-    @Select("SELECT * FROM wvp_media_server")
-    List<MediaServer> queryAll();
+    @Select("SELECT * FROM wvp_media_server where server_id = #{serverId}")
+    List<MediaServer> queryAll(@Param("serverId") String serverId);
 
-    @Delete("DELETE FROM wvp_media_server WHERE id=#{id}")
-    void delOne(String id);
+    @Delete("DELETE FROM wvp_media_server WHERE id=#{id} and server_id = #{serverId}")
+    void delOne(String id, @Param("serverId") String serverId);
 
-    @Select("DELETE FROM wvp_media_server WHERE ip=#{host} and http_port=#{port}")
-    void delOneByIPAndPort(@Param("host") String host, @Param("port") int port);
+    @Select("SELECT * FROM wvp_media_server WHERE ip=#{host} and http_port=#{port} and server_id = #{serverId}")
+    MediaServer queryOneByHostAndPort(@Param("host") String host, @Param("port") int port, @Param("serverId") String serverId);
 
-    @Delete("DELETE FROM wvp_media_server WHERE default_server=true")
-    int delDefault();
+    @Select("SELECT * FROM wvp_media_server WHERE default_server=true and server_id = #{serverId}")
+    MediaServer queryDefault(@Param("serverId") String serverId);
 
-    @Select("SELECT * FROM wvp_media_server WHERE ip=#{host} and http_port=#{port}")
-    MediaServer queryOneByHostAndPort(@Param("host") String host, @Param("port") int port);
-
-    @Select("SELECT * FROM wvp_media_server WHERE default_server=true")
-    MediaServer queryDefault();
-
-    @Select("SELECT * FROM wvp_media_server WHERE record_assist_port > 0")
-    List<MediaServer> queryAllWithAssistPort();
+    @Select("SELECT * FROM wvp_media_server WHERE record_assist_port > 0 and server_id = #{serverId}")
+    List<MediaServer> queryAllWithAssistPort(@Param("serverId") String serverId);
 
 }
diff --git a/数据库/2.7.3/初始化-mysql-2.7.3.sql b/数据库/2.7.3/初始化-mysql-2.7.3.sql
index e12677da5..5ffb722c6 100644
--- a/数据库/2.7.3/初始化-mysql-2.7.3.sql
+++ b/数据库/2.7.3/初始化-mysql-2.7.3.sql
@@ -29,6 +29,7 @@ create table wvp_device
     custom_name                         character varying(255),
     sdp_ip                              character varying(50),
     local_ip                            character varying(50),
+    server_id                           character varying(50),
     password                            character varying(255),
     as_message_channel                  bool    default false,
     keepalive_interval_time             integer,
@@ -190,6 +191,7 @@ create table wvp_media_server
     record_path         character varying(255),
     record_day          integer               default 7,
     transcode_suffix    character varying(255),
+    server_id           character varying(50),
     constraint uk_media_server_unique_ip_http_port unique (ip, http_port)
 );
 
diff --git a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
index c632f9a83..9789b4632 100644
--- a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
+++ b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
@@ -29,6 +29,7 @@ create table wvp_device
     custom_name                         character varying(255),
     sdp_ip                              character varying(50),
     local_ip                            character varying(50),
+    server_id                           character varying(50),
     password                            character varying(255),
     as_message_channel                  bool    default false,
     keepalive_interval_time             integer,
@@ -207,6 +208,7 @@ create table wvp_media_server
     record_path         character varying(255),
     record_day          integer               default 7,
     transcode_suffix    character varying(255),
+    server_id           character varying(50),
     constraint uk_media_server_unique_ip_http_port unique (ip, http_port)
 );
 

From 8372bfec6884b4078721b8239d7d67e381d28f69 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 12 Dec 2024 09:29:49 +0800
Subject: [PATCH 28/38] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AA=92=E4=BD=93?=
 =?UTF-8?q?=E8=8A=82=E7=82=B9=E8=A1=A8=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 数据库/2.7.3/初始化-mysql-2.7.3.sql               | 2 +-
 数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/数据库/2.7.3/初始化-mysql-2.7.3.sql b/数据库/2.7.3/初始化-mysql-2.7.3.sql
index 5ffb722c6..f294e62e0 100644
--- a/数据库/2.7.3/初始化-mysql-2.7.3.sql
+++ b/数据库/2.7.3/初始化-mysql-2.7.3.sql
@@ -192,7 +192,7 @@ create table wvp_media_server
     record_day          integer               default 7,
     transcode_suffix    character varying(255),
     server_id           character varying(50),
-    constraint uk_media_server_unique_ip_http_port unique (ip, http_port)
+    constraint uk_media_server_unique_ip_http_port unique (ip, http_port, server_id)
 );
 
 create table wvp_platform
diff --git a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
index 9789b4632..67fb1445f 100644
--- a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
+++ b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
@@ -209,7 +209,7 @@ create table wvp_media_server
     record_day          integer               default 7,
     transcode_suffix    character varying(255),
     server_id           character varying(50),
-    constraint uk_media_server_unique_ip_http_port unique (ip, http_port)
+    constraint uk_media_server_unique_ip_http_port unique (ip, http_port, server_id)
 );
 
 create table wvp_platform

From c4025d7a073bf0a47ec8680ef0916159011345ce Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 12 Dec 2024 09:37:12 +0800
Subject: [PATCH 29/38] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=80=9A=E7=94=A8?=
 =?UTF-8?q?=E7=82=B9=E6=92=ADRPC=E8=B0=83=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../RedisRpcChannelPlayController.java        | 101 ++++++++++++++++++
 .../service/RedisRpcPlayServiceImpl.java      |   2 +-
 数据库/2.7.3/初始化-mysql-2.7.3.sql           |   1 +
 .../2.7.3/初始化-postgresql-kingbase-2.7.3.sql |   1 +
 4 files changed, 104 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java

diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
new file mode 100644
index 000000000..adeda445a
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
@@ -0,0 +1,101 @@
+package com.genersoft.iot.vmp.service.redisMsg.control;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.genersoft.iot.vmp.common.StreamInfo;
+import com.genersoft.iot.vmp.conf.UserSetting;
+import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
+import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
+import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo;
+import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
+import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
+import com.genersoft.iot.vmp.gb28181.session.SSRCFactory;
+import com.genersoft.iot.vmp.media.bean.MediaServer;
+import com.genersoft.iot.vmp.media.event.hook.Hook;
+import com.genersoft.iot.vmp.media.event.hook.HookSubscribe;
+import com.genersoft.iot.vmp.media.event.hook.HookType;
+import com.genersoft.iot.vmp.media.service.IMediaServerService;
+import com.genersoft.iot.vmp.service.ISendRtpServerService;
+import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import javax.sip.message.Response;
+
+@Slf4j
+@RedisRpcController("chanel")
+public class RedisRpcChannelPlayController {
+
+    @Autowired
+    private SSRCFactory ssrcFactory;
+
+    @Autowired
+    private IMediaServerService mediaServerService;
+
+    @Autowired
+    private ISendRtpServerService sendRtpServerService;
+
+    @Autowired
+    private UserSetting userSetting;
+
+    @Autowired
+    private HookSubscribe hookSubscribe;
+
+    @Autowired
+    private RedisTemplate<Object, Object> redisTemplate;
+
+    @Autowired
+    private IGbChannelService channelService;
+
+    @Autowired
+    private IGbChannelPlayService channelPlayService;
+
+    private void sendResponse(RedisRpcResponse response){
+        log.info("[redis-rpc] >> {}", response);
+        response.setToId(userSetting.getServerId());
+        RedisRpcMessage message = new RedisRpcMessage();
+        message.setResponse(response);
+        redisTemplate.convertAndSend(RedisRpcConfig.REDIS_REQUEST_CHANNEL_KEY, message);
+    }
+
+
+    /**
+     * 点播国标设备
+     */
+    @RedisRpcMapping("play")
+    public RedisRpcResponse playChannel(RedisRpcRequest request) {
+        int channelId = Integer.parseInt(request.getParam().toString());
+        RedisRpcResponse response = request.getResponse();
+
+        if (channelId <= 0) {
+            response.setStatusCode(Response.BAD_REQUEST);
+            response.setBody("param error");
+            return response;
+        }
+        // 获取对应的设备和通道信息
+        CommonGBChannel channel = channelService.getOne(channelId);
+        if (channel == null) {
+            response.setStatusCode(Response.BAD_REQUEST);
+            response.setBody("param error");
+            return response;
+        }
+
+        channelPlayService.play(channel, null, (code, msg, data) ->{
+            if (code == InviteErrorCode.SUCCESS.getCode()) {
+                response.setStatusCode(Response.OK);
+                response.setBody(data);
+            }else {
+                response.setStatusCode(code);
+            }
+            // 手动发送结果
+            sendResponse(response);
+        });
+        return null;
+    }
+
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
index 93f218a6c..0e1c0b1ee 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
@@ -37,7 +37,7 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
 
     @Override
     public void play(String serverId, Integer channelId, ErrorCallback<StreamInfo> callback) {
-        RedisRpcRequest request = buildRequest("playChannel", channelId);
+        RedisRpcRequest request = buildRequest("channel/play", channelId);
         request.setToId(serverId);
         RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout());
         if (response == null) {
diff --git a/数据库/2.7.3/初始化-mysql-2.7.3.sql b/数据库/2.7.3/初始化-mysql-2.7.3.sql
index f294e62e0..81658c3d0 100644
--- a/数据库/2.7.3/初始化-mysql-2.7.3.sql
+++ b/数据库/2.7.3/初始化-mysql-2.7.3.sql
@@ -34,6 +34,7 @@ create table wvp_device
     as_message_channel                  bool    default false,
     keepalive_interval_time             integer,
     broadcast_push_after_ack            bool    default false,
+    server_id                           character varying(50),
     constraint uk_device_device unique (device_id)
 );
 
diff --git a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
index 67fb1445f..d7c4c1f0a 100644
--- a/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
+++ b/数据库/2.7.3/初始化-postgresql-kingbase-2.7.3.sql
@@ -34,6 +34,7 @@ create table wvp_device
     as_message_channel                  bool    default false,
     keepalive_interval_time             integer,
     broadcast_push_after_ack            bool    default false,
+    server_id                           character varying(50),
     constraint uk_device_device unique (device_id)
 );
 

From 1c2709163d97833276c56e2403c0168ef3df2a05 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 12 Dec 2024 09:43:24 +0800
Subject: [PATCH 30/38] =?UTF-8?q?=E4=BF=AE=E6=94=B9RedisRpcChannelPlayCont?=
 =?UTF-8?q?roller=E8=B7=AF=E5=BE=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../control/RedisRpcChannelPlayController.java     | 14 +-------------
 1 file changed, 1 insertion(+), 13 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
index adeda445a..d6f3a5af2 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
@@ -28,24 +28,12 @@ import org.springframework.data.redis.core.RedisTemplate;
 import javax.sip.message.Response;
 
 @Slf4j
-@RedisRpcController("chanel")
+@RedisRpcController("channel")
 public class RedisRpcChannelPlayController {
 
-    @Autowired
-    private SSRCFactory ssrcFactory;
-
-    @Autowired
-    private IMediaServerService mediaServerService;
-
-    @Autowired
-    private ISendRtpServerService sendRtpServerService;
-
     @Autowired
     private UserSetting userSetting;
 
-    @Autowired
-    private HookSubscribe hookSubscribe;
-
     @Autowired
     private RedisTemplate<Object, Object> redisTemplate;
 

From fe341036a8f38f58563b3d6bbc8b2d39827e9acf Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 12 Dec 2024 10:15:04 +0800
Subject: [PATCH 31/38] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=9B=BD=E6=A0=87?=
 =?UTF-8?q?=E7=BA=A7=E8=81=94=E7=82=B9=E6=92=AD=E5=85=B6=E4=BB=96wvp?=
 =?UTF-8?q?=E7=9A=84=E5=9B=BD=E6=A0=87=E8=AE=BE=E5=A4=87?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gb28181/controller/PlayController.java    | 11 +-------
 .../iot/vmp/gb28181/dao/DeviceMapper.java     |  1 +
 .../gb28181/service/impl/PlayServiceImpl.java | 27 ++++++++++--------
 web_src/src/components/DeviceList.vue         | 28 +++++++++----------
 4 files changed, 32 insertions(+), 35 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
index e5087cbaa..b99a00bf6 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/PlayController.java
@@ -21,7 +21,6 @@ import com.genersoft.iot.vmp.media.bean.MediaServer;
 import com.genersoft.iot.vmp.media.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
-import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
@@ -66,9 +65,6 @@ public class PlayController {
 	@Autowired
 	private IPlayService playService;
 
-	@Autowired
-	private IRedisRpcPlayService redisRpcPlayService;
-
 	@Autowired
 	private IMediaServerService mediaServerService;
 
@@ -153,12 +149,7 @@ public class PlayController {
 			// 此处必须释放所有请求
 			resultHolder.invokeAllResult(requestMessage);
 		};
-		// 判断设备是否属于当前平台, 如果不属于则发起自动调用
-		if (userSetting.getServerId().equals(device.getServerId())) {
-			redisRpcPlayService.play(device.getServerId(), channel.getId(), callback);
-		}else {
-			playService.play(device, channel, callback);
-		}
+		playService.play(device, channel, callback);
 		return result;
 	}
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java
index 07b186763..b33f906b5 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/dao/DeviceMapper.java
@@ -365,6 +365,7 @@ public interface DeviceMapper {
             "geo_coord_sys,"+
             "on_line,"+
             "media_server_id,"+
+            "server_id,"+
             "(SELECT count(0) FROM wvp_device_channel dc WHERE dc.device_db_id= de.id) as channel_count " +
             " FROM wvp_device de" +
             " where 1 = 1 "+
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
index 8b3d9f067..e631a6cdc 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
@@ -31,6 +31,7 @@ import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo;
 import com.genersoft.iot.vmp.service.IReceiveRtpServerService;
 import com.genersoft.iot.vmp.service.ISendRtpServerService;
 import com.genersoft.iot.vmp.service.bean.*;
+import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.utils.CloudRecordUtils;
 import com.genersoft.iot.vmp.utils.DateUtil;
@@ -123,6 +124,9 @@ public class PlayServiceImpl implements IPlayService {
     @Autowired
     private ICloudRecordService cloudRecordService;
 
+    @Autowired
+    private IRedisRpcPlayService redisRpcPlayService;
+
     /**
      * 流到来的处理
      */
@@ -287,12 +291,18 @@ public class PlayServiceImpl implements IPlayService {
 
     @Override
     public void play(Device device, DeviceChannel channel, ErrorCallback<StreamInfo> callback) {
-        MediaServer mediaServerItem = getNewMediaServerItem(device);
-        if (mediaServerItem == null) {
-            log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", device.getDeviceId(), channel.getDeviceId());
-            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm");
+
+        // 判断设备是否属于当前平台, 如果不属于则发起自动调用
+        if (userSetting.getServerId().equals(device.getServerId())) {
+            redisRpcPlayService.play(device.getServerId(), channel.getId(), callback);
+        }else {
+            MediaServer mediaServerItem = getNewMediaServerItem(device);
+            if (mediaServerItem == null) {
+                log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", device.getDeviceId(), channel.getDeviceId());
+                throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm");
+            }
+            play(mediaServerItem, device, channel, null, callback);
         }
-        play(mediaServerItem, device, channel, null, callback);
     }
 
     @Override
@@ -1660,13 +1670,8 @@ public class PlayServiceImpl implements IPlayService {
             log.warn("[点播] 未找到通道{}的设备信息", channel);
             throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error");
         }
-        MediaServer mediaServer = getNewMediaServerItem(device);
-        if (mediaServer == null) {
-            log.warn("[点播] 未找到可用媒体节点");
-            throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error");
-        }
         DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
-        play(mediaServer, device, deviceChannel, null, callback);
+        play(device, deviceChannel, callback);
     }
 
     @Override
diff --git a/web_src/src/components/DeviceList.vue b/web_src/src/components/DeviceList.vue
index 7525def58..f9240f64d 100755
--- a/web_src/src/components/DeviceList.vue
+++ b/web_src/src/components/DeviceList.vue
@@ -15,7 +15,7 @@
         </el-select>
         <el-button icon="el-icon-plus" size="mini" style="margin-right: 1rem;" type="primary" @click="add">添加设备
         </el-button>
-        <el-button icon="el-icon-info" size="mini" style="margin-right: 1rem;" type="primary" @click="showInfo">平台信息
+        <el-button icon="el-icon-info" size="mini" style="margin-right: 1rem;" type="primary" @click="showInfo(true)">平台信息
         </el-button>
         <el-button icon="el-icon-refresh-right" circle size="mini" :loading="getDeviceListLoading"
                    @click="getDeviceList()"></el-button>
@@ -53,7 +53,8 @@
       <el-table-column label="状态" min-width="120">
         <template v-slot:default="scope">
           <div slot="reference" class="name-wrapper">
-            <el-tag size="medium" v-if="scope.row.onLine">在线</el-tag>
+            <el-tag size="medium" v-if="scope.row.onLine && serverId !== scope.row.serverId" style="border-color: #ecf1af">在线</el-tag>
+            <el-tag size="medium" v-if="scope.row.onLine && serverId === scope.row.serverId">在线</el-tag>
             <el-tag size="medium" type="info" v-if="!scope.row.onLine">离线</el-tag>
           </div>
         </template>
@@ -62,11 +63,6 @@
       </el-table-column>
       <el-table-column prop="registerTime" label="最近注册"  min-width="160">
       </el-table-column>
-<!--      <el-table-column prop="updateTime" label="更新时间"  width="140">-->
-<!--      </el-table-column>-->
-<!--      <el-table-column prop="createTime" label="创建时间"  width="140">-->
-<!--      </el-table-column>-->
-
       <el-table-column label="操作" min-width="380" fixed="right">
         <template v-slot:default="scope">
           <el-button type="text" size="medium" v-bind:disabled="scope.row.online==0" icon="el-icon-refresh" @click="refDevice(scope.row)"
@@ -133,11 +129,12 @@ export default {
       online: null,
       videoComponentList: [],
       updateLooper: 0, //数据刷新轮训标志
-      currentDeviceChannelsLenth: 0,
+      currentDeviceChannelsLength: 0,
       winHeight: window.innerHeight - 200,
       currentPage: 1,
       count: 15,
       total: 0,
+      serverId: null,
       getDeviceListLoading: false,
     };
   },
@@ -149,7 +146,7 @@ export default {
         channels = Object.keys(data).map(key => {
           return data[key];
         });
-        this.currentDeviceChannelsLenth = channels.length;
+        this.currentDeviceChannelsLength = channels.length;
       }
       return channels;
     }
@@ -164,6 +161,8 @@ export default {
   },
   methods: {
     initData: function () {
+      // 获取平台信息
+      this.showInfo(false)
       this.getDeviceList();
     },
     currentChange: function (val) {
@@ -326,17 +325,18 @@ export default {
 
       })
     },
-    showInfo: function (){
+    showInfo: function (showConfigInfo){
 
       this.$axios({
         method: 'get',
         url: `/api/server/system/configInfo`,
       }).then( (res)=> {
-        console.log(res)
         if (res.data.code === 0) {
-          console.log(2222)
-          console.log(this.$refs.configInfo)
-          this.$refs.configInfo.openDialog(res.data.data)
+          this.serverId = res.data.data.addOn.serverId;
+          console.log(this.serverId);
+          if(showConfigInfo) {
+            this.$refs.configInfo.openDialog(res.data.data)
+          }
         }
       }).catch( (error)=> {
       });

From b4f28859b9002d981eeeb278030fc1747e451873 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 12 Dec 2024 12:42:57 +0800
Subject: [PATCH 32/38] =?UTF-8?q?=E4=BC=98=E5=8C=96RPC=E8=B0=83=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../genersoft/iot/vmp/VManageBootstrap.java   |  3 +-
 .../iot/vmp/conf/redis/RedisRpcConfig.java    | 71 ++++++++++---------
 .../conf/redis/bean/RedisRpcClassHandler.java |  7 +-
 .../iot/vmp/jt1078/util/ClassUtil.java        |  7 ++
 .../RedisRpcChannelPlayController.java        | 15 ++--
 .../control/RedisRpcSendRtpController.java    | 20 ++----
 .../control/RedisRpcStreamPushController.java | 12 ++--
 .../redisMsg/dto/RedisRpcController.java      |  3 -
 .../service/redisMsg/dto/RpcController.java   | 33 +++++++++
 web_src/src/components/StreamPushList.vue     |  4 +-
 10 files changed, 96 insertions(+), 79 deletions(-)
 create mode 100644 src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RpcController.java

diff --git a/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java b/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java
index acd6092fd..9a7912d20 100644
--- a/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java
+++ b/src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java
@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp;
 
+import com.genersoft.iot.vmp.jt1078.util.ClassUtil;
 import com.genersoft.iot.vmp.utils.GitUtil;
 import com.genersoft.iot.vmp.utils.SpringBeanFactory;
 import lombok.extern.slf4j.Slf4j;
@@ -33,6 +34,7 @@ public class VManageBootstrap extends SpringBootServletInitializer {
 	public static void main(String[] args) {
 		VManageBootstrap.args = args;
 		VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args);
+		ClassUtil.context = VManageBootstrap.context;
 		GitUtil gitUtil = SpringBeanFactory.getBean("gitUtil");
 		if (gitUtil == null) {
 			log.info("获取版本信息失败");
@@ -62,6 +64,5 @@ public class VManageBootstrap extends SpringBootServletInitializer {
 		);
 		SessionCookieConfig sessionCookieConfig = servletContext.getSessionCookieConfig();
 		sessionCookieConfig.setHttpOnly(true);
-
 	}
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
index 6607acecd..a70563fb0 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
@@ -7,9 +7,7 @@ import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcClassHandler;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
-import com.genersoft.iot.vmp.jt1078.util.ClassUtil;
-import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
-import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -19,11 +17,9 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.PostConstruct;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.ConcurrentHashMap;
@@ -53,34 +49,37 @@ public class RedisRpcConfig implements MessageListener {
 
     private final static Map<String, RedisRpcClassHandler> protocolHash = new HashMap<>();
 
-    // 启动时执行
-    @PostConstruct
-    public void init(){
-        List<Class<?>> classList = ClassUtil.getClassList("com.genersoft.iot.vmp.service.redisMsg.control", RedisRpcController.class);
-        for (Class<?> handlerClass : classList) {
-            String controllerPath = handlerClass.getAnnotation(RedisRpcController.class).value();
-            // 扫描其下的方法
-            Method[] methods = handlerClass.getDeclaredMethods();
-            for (Method method : methods) {
-                RedisRpcMapping annotation = method.getAnnotation(RedisRpcMapping.class);
-                if (annotation != null) {
-                    String methodPath =  annotation.value();
-                    if (methodPath != null) {
-                        protocolHash.put(controllerPath + "/" + methodPath, new RedisRpcClassHandler(handlerClass, method));
-                    }
-                }
-
-            }
-
-        }
-        for (String s : protocolHash.keySet()) {
-            System.out.println(s);
-        }
-        if (log.isDebugEnabled()) {
-            log.debug("消息ID缓存表 protocolHash:{}", protocolHash);
-        }
+    public void addHandler(String path, RedisRpcClassHandler handler) {
+        protocolHash.put(path, handler);
     }
 
+//    @Override
+//    public void run(String... args) throws Exception {
+//        List<Class<?>> classList = ClassUtil.getClassList("com.genersoft.iot.vmp.service.redisMsg.control", RedisRpcController.class);
+//        for (Class<?> handlerClass : classList) {
+//            String controllerPath = handlerClass.getAnnotation(RedisRpcController.class).value();
+//            Object bean = ClassUtil.getBean(controllerPath, handlerClass);
+//            // 扫描其下的方法
+//            Method[] methods = handlerClass.getDeclaredMethods();
+//            for (Method method : methods) {
+//                RedisRpcMapping annotation = method.getAnnotation(RedisRpcMapping.class);
+//                if (annotation != null) {
+//                    String methodPath =  annotation.value();
+//                    if (methodPath != null) {
+//                        protocolHash.put(controllerPath + "/" + methodPath, new RedisRpcClassHandler(bean, method));
+//                    }
+//                }
+//
+//            }
+//
+//        }
+//        for (String s : protocolHash.keySet()) {
+//            System.out.println(s);
+//        }
+//        if (log.isDebugEnabled()) {
+//            log.debug("消息ID缓存表 protocolHash:{}", protocolHash);
+//        }
+//    }
 
     @Override
     public void onMessage(Message message, byte[] pattern) {
@@ -100,7 +99,7 @@ public class RedisRpcConfig implements MessageListener {
                             log.error("[redis rpc 解析失败] {}", JSON.toJSONString(redisRpcMessage));
                         }
                     } catch (Exception e) {
-                        log.error("[redis rpc 解析异常] ", e);
+                        log.error("[redis rpc 解析异常] {}",new String(msg.getBody()), e);
                     }
                 }
             });
@@ -122,7 +121,7 @@ public class RedisRpcConfig implements MessageListener {
             }
             log.info("[redis-rpc] << {}", request);
             RedisRpcClassHandler redisRpcClassHandler = protocolHash.get(request.getUri());
-            Class<?> objectClass = redisRpcClassHandler.getObjectClass();
+            RpcController controller = redisRpcClassHandler.getController();
             Method method = redisRpcClassHandler.getMethod();
             // 没有携带目标ID的可以理解为哪个wvp有结果就哪个回复,携带目标ID,但是如果是不存在的uri则直接回复404
             if (userSetting.getServerId().equals(request.getToId())) {
@@ -133,7 +132,7 @@ public class RedisRpcConfig implements MessageListener {
                     sendResponse(response);
                     return;
                 }
-                RedisRpcResponse response = (RedisRpcResponse)method.invoke(objectClass, request);
+                RedisRpcResponse response = (RedisRpcResponse)method.invoke(controller, request);
                 if(response != null) {
                     sendResponse(response);
                 }
@@ -141,7 +140,7 @@ public class RedisRpcConfig implements MessageListener {
                 if (method == null) {
                     return;
                 }
-                RedisRpcResponse response = (RedisRpcResponse)method.invoke(objectClass, request);
+                RedisRpcResponse response = (RedisRpcResponse)method.invoke(controller, request);
                 if (response != null) {
                     sendResponse(response);
                 }
@@ -234,6 +233,8 @@ public class RedisRpcConfig implements MessageListener {
         return callbacks.size();
     }
 
+
+
 //    @Scheduled(fixedRate = 1000)   //每1秒执行一次
 //    public void execute(){
 //        logger.info("callbacks的长度: " + callbacks.size());
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java
index 8c49a79f1..1fab24bbe 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/bean/RedisRpcClassHandler.java
@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.conf.redis.bean;
 
+import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
 import lombok.Data;
 
 import java.lang.reflect.Method;
@@ -7,11 +8,11 @@ import java.lang.reflect.Method;
 @Data
 public class RedisRpcClassHandler {
 
-    private Class<?> objectClass;
+    private RpcController controller;
     private Method method;
 
-    public RedisRpcClassHandler(Class<?> objectClass, Method method) {
-        this.objectClass = objectClass;
+    public RedisRpcClassHandler(RpcController controller, Method method) {
+        this.controller = controller;
         this.method = method;
     }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java b/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java
index 9c3fd2868..def0c0eb0 100644
--- a/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java
+++ b/src/main/java/com/genersoft/iot/vmp/jt1078/util/ClassUtil.java
@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.jt1078.util;
 
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
 import org.springframework.core.io.support.ResourcePatternResolver;
@@ -12,6 +13,12 @@ import java.util.List;
 @Slf4j
 public class ClassUtil {
 
+    public static ConfigurableApplicationContext context;
+
+    public static <T>  T  getBean(String beanName, Class<T> clazz) {
+        return context.getBean(beanName, clazz);
+    }
+
     public static Object getBean(Class<?> clazz) {
         if (clazz != null) {
             try {
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
index d6f3a5af2..bcd58c3eb 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
@@ -1,35 +1,28 @@
 package com.genersoft.iot.vmp.service.redisMsg.control;
 
-import com.alibaba.fastjson2.JSONObject;
-import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
-import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
-import com.genersoft.iot.vmp.gb28181.session.SSRCFactory;
-import com.genersoft.iot.vmp.media.bean.MediaServer;
-import com.genersoft.iot.vmp.media.event.hook.Hook;
-import com.genersoft.iot.vmp.media.event.hook.HookSubscribe;
-import com.genersoft.iot.vmp.media.event.hook.HookType;
-import com.genersoft.iot.vmp.media.service.IMediaServerService;
-import com.genersoft.iot.vmp.service.ISendRtpServerService;
 import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
 
 import javax.sip.message.Response;
 
+@Component
 @Slf4j
 @RedisRpcController("channel")
-public class RedisRpcChannelPlayController {
+public class RedisRpcChannelPlayController extends RpcController {
 
     @Autowired
     private UserSetting userSetting;
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
index 1077f5782..ee5d1e18e 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
@@ -5,27 +5,26 @@ import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
 import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo;
-import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
-import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
 import com.genersoft.iot.vmp.gb28181.session.SSRCFactory;
 import com.genersoft.iot.vmp.media.bean.MediaInfo;
 import com.genersoft.iot.vmp.media.bean.MediaServer;
-import com.genersoft.iot.vmp.media.event.hook.HookSubscribe;
 import com.genersoft.iot.vmp.media.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.ISendRtpServerService;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
 
 import javax.sip.message.Response;
 
+@Component
 @Slf4j
 @RedisRpcController("sendRtp")
-public class RedisRpcSendRtpController {
+public class RedisRpcSendRtpController extends RpcController {
 
     @Autowired
     private SSRCFactory ssrcFactory;
@@ -39,17 +38,6 @@ public class RedisRpcSendRtpController {
     @Autowired
     private UserSetting userSetting;
 
-    @Autowired
-    private HookSubscribe hookSubscribe;
-
-    @Autowired
-    private RedisTemplate<Object, Object> redisTemplate;
-
-    @Autowired
-    private IGbChannelService channelService;
-
-    @Autowired
-    private IGbChannelPlayService channelPlayService;
 
     /**
      * 获取发流的信息
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
index 3465bdc8e..e1ef57b5c 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
@@ -8,8 +8,6 @@ import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
 import com.genersoft.iot.vmp.gb28181.bean.SendRtpInfo;
-import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
-import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
 import com.genersoft.iot.vmp.gb28181.session.SSRCFactory;
 import com.genersoft.iot.vmp.media.bean.MediaServer;
 import com.genersoft.iot.vmp.media.event.hook.Hook;
@@ -19,13 +17,16 @@ import com.genersoft.iot.vmp.media.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.ISendRtpServerService;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
+import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
 
+@Component
 @Slf4j
 @RedisRpcController("streamPush")
-public class RedisRpcStreamPushController {
+public class RedisRpcStreamPushController extends RpcController {
 
     @Autowired
     private SSRCFactory ssrcFactory;
@@ -45,11 +46,6 @@ public class RedisRpcStreamPushController {
     @Autowired
     private RedisTemplate<Object, Object> redisTemplate;
 
-    @Autowired
-    private IGbChannelService channelService;
-
-    @Autowired
-    private IGbChannelPlayService channelPlayService;
 
     private void sendResponse(RedisRpcResponse response){
         log.info("[redis-rpc] >> {}", response);
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java
index 79cdcb370..f314b0c01 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RedisRpcController.java
@@ -1,13 +1,10 @@
 package com.genersoft.iot.vmp.service.redisMsg.dto;
 
-import org.springframework.stereotype.Component;
-
 import java.lang.annotation.*;
 
 @Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
-@Component
 public @interface RedisRpcController {
     /**
      * 请求路径
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RpcController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RpcController.java
new file mode 100644
index 000000000..4544812f5
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/dto/RpcController.java
@@ -0,0 +1,33 @@
+package com.genersoft.iot.vmp.service.redisMsg.dto;
+
+
+import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcClassHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.annotation.PostConstruct;
+import java.lang.reflect.Method;
+
+public class RpcController {
+
+    @Autowired
+    private RedisRpcConfig redisRpcConfig;
+
+
+    @PostConstruct
+    public void init() {
+        String controllerPath = this.getClass().getAnnotation(RedisRpcController.class).value();
+        // 扫描其下的方法
+        Method[] methods = this.getClass().getDeclaredMethods();
+        for (Method method : methods) {
+            RedisRpcMapping annotation = method.getAnnotation(RedisRpcMapping.class);
+            if (annotation != null) {
+                String methodPath =  annotation.value();
+                if (methodPath != null) {
+                    redisRpcConfig.addHandler(controllerPath + "/" + methodPath, new RedisRpcClassHandler(this, method));
+                }
+            }
+
+        }
+    }
+}
diff --git a/web_src/src/components/StreamPushList.vue b/web_src/src/components/StreamPushList.vue
index 6ee690efa..230af4c68 100755
--- a/web_src/src/components/StreamPushList.vue
+++ b/web_src/src/components/StreamPushList.vue
@@ -33,9 +33,9 @@
                download='推流通道导入.zip'>下载模板</a>
           </el-button>
           <el-button icon="el-icon-delete" size="mini" style="margin-right: 1rem;"
-                     :disabled="multipleSelection.length === 0" type="danger" @click="batchDel">批量移除
+                     :disabled="multipleSelection.length === 0" type="danger" @click="batchDel">移除
           </el-button>
-          <el-button icon="el-icon-plus" size="mini" style="margin-right: 1rem;" type="primary" @click="addStream">添加通道
+          <el-button icon="el-icon-plus" size="mini" style="margin-right: 1rem;" type="primary" @click="addStream">添加
           </el-button>
           <el-button icon="el-icon-refresh-right" circle size="mini" @click="refresh()"></el-button>
         </div>

From 0dda8298bf89f54ac4463af89e288e4a9109a63c Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 12 Dec 2024 12:44:11 +0800
Subject: [PATCH 33/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=82=B9=E6=92=AD?=
 =?UTF-8?q?=E8=B0=83=E7=94=A8=E5=88=A4=E6=96=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java    | 6 +++---
 .../iot/vmp/gb28181/service/impl/PlayServiceImpl.java       | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
index a70563fb0..1ccaf0118 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
@@ -96,10 +96,10 @@ public class RedisRpcConfig implements MessageListener {
                         } else if (redisRpcMessage.getResponse() != null){
                             handlerResponse(redisRpcMessage.getResponse());
                         } else {
-                            log.error("[redis rpc 解析失败] {}", JSON.toJSONString(redisRpcMessage));
+                            log.error("[redis-rpc]解析失败 {}", JSON.toJSONString(redisRpcMessage));
                         }
                     } catch (Exception e) {
-                        log.error("[redis rpc 解析异常] {}",new String(msg.getBody()), e);
+                        log.error("[redis-rpc]解析异常 {}",new String(msg.getBody()), e);
                     }
                 }
             });
@@ -146,7 +146,7 @@ public class RedisRpcConfig implements MessageListener {
                 }
             }
         }catch (InvocationTargetException | IllegalAccessException e) {
-            log.error("[redis rpc ] 处理请求失败 ", e);
+            log.error("[redis-rpc ] 处理请求失败 ", e);
         }
 
     }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
index e631a6cdc..f6392ba2e 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
@@ -293,7 +293,7 @@ public class PlayServiceImpl implements IPlayService {
     public void play(Device device, DeviceChannel channel, ErrorCallback<StreamInfo> callback) {
 
         // 判断设备是否属于当前平台, 如果不属于则发起自动调用
-        if (userSetting.getServerId().equals(device.getServerId())) {
+        if (!userSetting.getServerId().equals(device.getServerId())) {
             redisRpcPlayService.play(device.getServerId(), channel.getId(), callback);
         }else {
             MediaServer mediaServerItem = getNewMediaServerItem(device);

From 2e254c6c8c068b82421733fa2f00f634236dc0d7 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 12 Dec 2024 16:30:40 +0800
Subject: [PATCH 34/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0RPC=E8=B0=83=E7=94=A8?=
 =?UTF-8?q?=E5=81=9C=E6=AD=A2=E5=AE=9E=E6=97=B6=E6=B5=81=E6=92=AD=E6=94=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../iot/vmp/conf/redis/RedisRpcConfig.java    |  3 +-
 .../service/IGbChannelPlayService.java        | 12 +++-
 .../iot/vmp/gb28181/service/IPlayService.java |  3 +
 .../impl/GbChannelPlayServiceImpl.java        | 51 +++++++++++++++++
 .../gb28181/service/impl/PlayServiceImpl.java | 55 ++++++++++++-------
 .../redisMsg/IRedisRpcPlayService.java        |  3 +
 .../RedisRpcChannelPlayController.java        | 40 ++++++++++++++
 .../service/RedisRpcPlayServiceImpl.java      | 21 +++++++
 .../service/IStreamPushPlayService.java       |  2 +
 .../impl/StreamPushPlayServiceImpl.java       |  5 ++
 10 files changed, 173 insertions(+), 22 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
index 1ccaf0118..308bb07f8 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
@@ -17,6 +17,7 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Component;
 
+import javax.sip.message.Response;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.HashMap;
@@ -128,7 +129,7 @@ public class RedisRpcConfig implements MessageListener {
                 if (method == null) {
                     // 回复404结果
                     RedisRpcResponse response = request.getResponse();
-                    response.setStatusCode(404);
+                    response.setStatusCode(Response.NOT_FOUND);
                     sendResponse(response);
                     return;
                 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
index 4f5779396..7658f6353 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.gb28181.service;
 
+import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.bean.InviteInfo;
@@ -10,11 +11,20 @@ public interface IGbChannelPlayService {
 
     void start(CommonGBChannel channel, InviteInfo inviteInfo, Platform platform, ErrorCallback<StreamInfo> callback);
 
-    void play(CommonGBChannel channel, Platform platform,  ErrorCallback<StreamInfo> callback);
+    void stopPlay(InviteSessionType type, CommonGBChannel channel, String stream);
+
+    void play(CommonGBChannel channel, Platform platform, ErrorCallback<StreamInfo> callback);
 
     void playGbDeviceChannel(CommonGBChannel channel, ErrorCallback<StreamInfo> callback);
 
+    void stopPlayDeviceChannel(CommonGBChannel channel, String stream);
+
     void playProxy(CommonGBChannel channel, ErrorCallback<StreamInfo> callback);
 
+    void stopPlayProxy(CommonGBChannel channel);
+
     void playPush(CommonGBChannel channel, String platformDeviceId, String platformName, ErrorCallback<StreamInfo> callback);
+
+
+    void  stopPlayPush(CommonGBChannel channel);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java
index 4a1403a04..b65fc4702 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IPlayService.java
@@ -68,9 +68,12 @@ public interface IPlayService {
 
     void play(CommonGBChannel channel, ErrorCallback<StreamInfo> callback);
 
+    void stop(InviteSessionType inviteSessionType, CommonGBChannel channel, String stream);
+
     void playBack(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback<StreamInfo> callback);
 
     void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback<StreamInfo> callback);
 
 
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
index e0a736c7b..618b72a15 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.gb28181.service.impl;
 
+import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.bean.InviteInfo;
@@ -88,6 +89,24 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
         }
     }
 
+    @Override
+    public void stopPlay(InviteSessionType type, CommonGBChannel channel, String stream) {
+        if (channel.getGbDeviceDbId() != null) {
+            // 国标通道
+            stopPlayDeviceChannel(channel, stream);
+        } else if (channel.getStreamProxyId() != null) {
+            // 拉流代理
+            stopPlayProxy(channel);
+        } else if (channel.getStreamPushId() != null) {
+            // 推流
+            stopPlayPush(channel);
+        } else {
+            // 通道数据异常
+            log.error("[点播通用通道] 通道数据异常,无法识别通道来源: {}({})", channel.getGbName(), channel.getGbDeviceId());
+            throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error");
+        }
+    }
+
     @Override
     public void play(CommonGBChannel channel, Platform platform, ErrorCallback<StreamInfo> callback) {
         if (channel.getGbDeviceDbId() != null) {
@@ -124,6 +143,18 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
         }
     }
 
+    @Override
+    public void stopPlayDeviceChannel(CommonGBChannel channel, String stream) {
+        // 国标通道
+        try {
+            deviceChannelPlayService.stop(InviteSessionType.PLAY, channel, stream);
+        }  catch (Exception e) {
+            log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e);
+        }
+    }
+
+
+
     @Override
     public void playProxy(CommonGBChannel channel, ErrorCallback<StreamInfo> callback){
         // 拉流代理通道
@@ -139,6 +170,16 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
         }
     }
 
+    @Override
+    public void stopPlayProxy(CommonGBChannel channel) {
+        // 拉流代理通道
+        try {
+            streamProxyPlayService.stop(channel.getStreamProxyId());
+        }catch (Exception e) {
+            log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e);
+        }
+    }
+
     @Override
     public void playPush(CommonGBChannel channel, String platformDeviceId, String platformName, ErrorCallback<StreamInfo> callback){
         // 推流
@@ -152,6 +193,16 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
         }
     }
 
+    @Override
+    public void stopPlayPush(CommonGBChannel channel) {
+        // 推流
+        try {
+            streamPushPlayService.stop(channel.getStreamPushId());
+        }catch (Exception e) {
+            log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e);
+        }
+    }
+
     private void playbackGbDeviceChannel(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback<StreamInfo> callback){
         try {
             deviceChannelPlayService.playBack(channel, startTime, stopTime, callback);
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
index f6392ba2e..70bd6d817 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
@@ -1606,30 +1606,34 @@ public class PlayServiceImpl implements IPlayService {
 
     @Override
     public void stop(InviteSessionType type, Device device, DeviceChannel channel, String stream) {
-        InviteInfo inviteInfo = inviteStreamService.getInviteInfo(type, channel.getId(), stream);
-        if (inviteInfo == null) {
-            if (type == InviteSessionType.PLAY) {
+        if (!userSetting.getServerId().equals(device.getServerId())) {
+            redisRpcPlayService.stop(device.getServerId(), type,  channel.getId(), stream);
+        }else {
+            InviteInfo inviteInfo = inviteStreamService.getInviteInfo(type, channel.getId(), stream);
+            if (inviteInfo == null) {
+                if (type == InviteSessionType.PLAY) {
+                    deviceChannelService.stopPlay(channel.getId());
+                }
+                return;
+            }
+            inviteStreamService.removeInviteInfo(inviteInfo);
+            if (InviteSessionStatus.ok == inviteInfo.getStatus()) {
+                try {
+                    log.info("[停止点播/回放/下载] {}/{}", device.getDeviceId(), channel.getDeviceId());
+                    cmder.streamByeCmd(device, channel.getDeviceId(), inviteInfo.getStream(), null, null);
+                } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) {
+                    log.error("[命令发送失败] 停止点播/回放/下载, 发送BYE: {}", e.getMessage());
+                    throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
+                }
+            }
+
+            if (inviteInfo.getType() == InviteSessionType.PLAY) {
                 deviceChannelService.stopPlay(channel.getId());
             }
-            return;
-        }
-        inviteStreamService.removeInviteInfo(inviteInfo);
-        if (InviteSessionStatus.ok == inviteInfo.getStatus()) {
-            try {
-                log.info("[停止点播/回放/下载] {}/{}", device.getDeviceId(), channel.getDeviceId());
-                cmder.streamByeCmd(device, channel.getDeviceId(), inviteInfo.getStream(), null, null);
-            } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) {
-                log.error("[命令发送失败] 停止点播/回放/下载, 发送BYE: {}", e.getMessage());
-                throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
+            if (inviteInfo.getStreamInfo() != null) {
+                receiveRtpServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServer(), inviteInfo.getSsrcInfo());
             }
         }
-
-        if (inviteInfo.getType() == InviteSessionType.PLAY) {
-            deviceChannelService.stopPlay(channel.getId());
-        }
-        if (inviteInfo.getStreamInfo() != null) {
-            receiveRtpServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServer(), inviteInfo.getSsrcInfo());
-        }
     }
 
     @Override
@@ -1674,6 +1678,17 @@ public class PlayServiceImpl implements IPlayService {
         play(device, deviceChannel, callback);
     }
 
+    @Override
+    public void stop(InviteSessionType inviteSessionType, CommonGBChannel channel, String stream) {
+        Device device = deviceService.getDevice(channel.getGbDeviceDbId());
+        if (device == null) {
+            log.warn("[停止播放] 未找到通道{}的设备信息", channel);
+            throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error");
+        }
+        DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
+        stop(InviteSessionType.PLAY, device, deviceChannel, stream);
+    }
+
     @Override
     public void playBack(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback<StreamInfo> callback) {
         if (startTime == null || stopTime == null) {
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
index 1ae1268ce..42957aa5e 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service.redisMsg;
 
+import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 
@@ -7,4 +8,6 @@ public interface IRedisRpcPlayService {
 
 
     void play(String serverId, Integer channelId, ErrorCallback<StreamInfo> callback);
+
+    void stop(String serverId, InviteSessionType type, int channelId, String stream);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
index bcd58c3eb..587f21987 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
@@ -1,5 +1,7 @@
 package com.genersoft.iot.vmp.service.redisMsg.control;
 
+import com.alibaba.fastjson2.JSONObject;
+import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
@@ -79,4 +81,42 @@ public class RedisRpcChannelPlayController extends RpcController {
         return null;
     }
 
+
+    /**
+     * 停止点播国标设备
+     */
+    @RedisRpcMapping("stop")
+    public RedisRpcResponse stop(RedisRpcRequest request) {
+        System.out.println(request.getParam().toString());
+        JSONObject jsonObject = JSONObject.parseObject(request.getParam().toString());
+
+        RedisRpcResponse response = request.getResponse();
+
+        Integer channelId = jsonObject.getIntValue("channelId");
+        if (channelId == null || channelId <= 0) {
+            response.setStatusCode(Response.BAD_REQUEST);
+            response.setBody("param error");
+            return response;
+        }
+
+        String stream = jsonObject.getString("stream");
+        InviteSessionType type = jsonObject.getObject("inviteSessionType", InviteSessionType.class);
+
+        // 获取对应的设备和通道信息
+        CommonGBChannel channel = channelService.getOne(channelId);
+        if (channel == null) {
+            response.setStatusCode(Response.BAD_REQUEST);
+            response.setBody("param error");
+            return response;
+        }
+        try {
+            channelPlayService.stopPlay(type, channel, stream);
+            response.setStatusCode(Response.OK);
+        }catch (Exception e){
+            response.setStatusCode(Response.SERVER_INTERNAL_ERROR);
+            response.setBody(e.getMessage());
+        }
+        return response;
+    }
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
index 0e1c0b1ee..f2b1b74ee 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
@@ -1,8 +1,11 @@
 package com.genersoft.iot.vmp.service.redisMsg.service;
 
 import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.conf.UserSetting;
+import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
@@ -51,5 +54,23 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
             }
         }
     }
+
+    @Override
+    public void stop(String serverId, InviteSessionType type, int channelId, String stream) {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("channelId", channelId);
+        jsonObject.put("stream", stream);
+        jsonObject.put("inviteSessionType", type);
+        RedisRpcRequest request = buildRequest("channel/stop", jsonObject.toJSONString());
+        request.setToId(serverId);
+        RedisRpcResponse response = redisRpcConfig.request(request, 50);
+        if (response == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg());
+        }else {
+            if (response.getStatusCode() != Response.OK) {
+                throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg());
+            }
+        }
+    }
 }
 
diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushPlayService.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushPlayService.java
index 1881c19b9..9b75ffc7a 100644
--- a/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/IStreamPushPlayService.java
@@ -5,4 +5,6 @@ import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 
 public interface IStreamPushPlayService {
     void start(Integer id, ErrorCallback<StreamInfo> callback, String platformDeviceId, String platformName );
+
+    void stop(Integer streamPushId);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushPlayServiceImpl.java
index 6fa3d65d0..d56cc286a 100644
--- a/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/streamPush/service/impl/StreamPushPlayServiceImpl.java
@@ -98,4 +98,9 @@ public class StreamPushPlayServiceImpl implements IStreamPushPlayService {
             }
         });
     }
+
+    @Override
+    public void stop(Integer streamPushId) {
+        // 推流无需主动停止
+    }
 }

From b0a1a8d141f230cd0823ebdd5cb8af6a4ad6e6b5 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 12 Dec 2024 17:00:58 +0800
Subject: [PATCH 35/38] =?UTF-8?q?=E4=BC=98=E5=8C=96RPC=E6=97=B6=E9=97=B4?=
 =?UTF-8?q?=E5=AE=9A=E4=B9=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../iot/vmp/conf/redis/RedisRpcConfig.java          | 13 ++++++++++---
 .../redisMsg/service/RedisRpcPlayServiceImpl.java   |  8 +++++---
 2 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
index 308bb07f8..02293685a 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
@@ -171,19 +171,25 @@ public class RedisRpcConfig implements MessageListener {
     private final Map<Long, SynchronousQueue<RedisRpcResponse>> topicSubscribers = new ConcurrentHashMap<>();
     private final Map<Long, CommonCallback<RedisRpcResponse>> callbacks = new ConcurrentHashMap<>();
 
-    public RedisRpcResponse request(RedisRpcRequest request, int timeOut) {
+    public RedisRpcResponse request(RedisRpcRequest request, long timeOut) {
+        return request(request, timeOut, TimeUnit.SECONDS);
+    }
+
+    public RedisRpcResponse request(RedisRpcRequest request, long timeOut, TimeUnit timeUnit) {
         request.setSn((long) random.nextInt(1000) + 1);
         SynchronousQueue<RedisRpcResponse> subscribe = subscribe(request.getSn());
 
         try {
             sendRequest(request);
-            return subscribe.poll(timeOut, TimeUnit.SECONDS);
+            return subscribe.poll(timeOut, timeUnit);
         } catch (InterruptedException e) {
             log.warn("[redis rpc timeout] uri: {}, sn: {}", request.getUri(), request.getSn(), e);
+            RedisRpcResponse redisRpcResponse = new RedisRpcResponse();
+            redisRpcResponse.setStatusCode(Response.BUSY_HERE);
+            return redisRpcResponse;
         } finally {
             this.unsubscribe(request.getSn());
         }
-        return null;
     }
 
     public void request(RedisRpcRequest request, CommonCallback<RedisRpcResponse> callback) {
@@ -236,6 +242,7 @@ public class RedisRpcConfig implements MessageListener {
 
 
 
+
 //    @Scheduled(fixedRate = 1000)   //每1秒执行一次
 //    public void execute(){
 //        logger.info("callbacks的长度: " + callbacks.size());
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
index f2b1b74ee..3aa859098 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
@@ -10,6 +10,7 @@ import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
+import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import lombok.extern.slf4j.Slf4j;
@@ -17,6 +18,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import javax.sip.message.Response;
+import java.util.concurrent.TimeUnit;
 
 @Slf4j
 @Service
@@ -42,13 +44,13 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
     public void play(String serverId, Integer channelId, ErrorCallback<StreamInfo> callback) {
         RedisRpcRequest request = buildRequest("channel/play", channelId);
         request.setToId(serverId);
-        RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout());
+        RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MICROSECONDS);
         if (response == null) {
             callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
         }else {
             if (response.getStatusCode() == Response.OK) {
                 StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class);
-                callback.run(response.getStatusCode(), "success", streamInfo);
+                callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
             }else {
                 callback.run(response.getStatusCode(), response.getBody().toString(), null);
             }
@@ -63,7 +65,7 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
         jsonObject.put("inviteSessionType", type);
         RedisRpcRequest request = buildRequest("channel/stop", jsonObject.toJSONString());
         request.setToId(serverId);
-        RedisRpcResponse response = redisRpcConfig.request(request, 50);
+        RedisRpcResponse response = redisRpcConfig.request(request, 50, TimeUnit.MICROSECONDS);
         if (response == null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg());
         }else {

From 26feb7a9b457d00706af32349b95671c8e0de9c6 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 12 Dec 2024 17:04:08 +0800
Subject: [PATCH 36/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8DPRC=E7=82=B9=E6=92=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
index 3aa859098..e6f8a0405 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
@@ -44,7 +44,7 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
     public void play(String serverId, Integer channelId, ErrorCallback<StreamInfo> callback) {
         RedisRpcRequest request = buildRequest("channel/play", channelId);
         request.setToId(serverId);
-        RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MICROSECONDS);
+        RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS);
         if (response == null) {
             callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
         }else {

From 5a63e7f95821fadb75ec917880b381b0276727b4 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Thu, 12 Dec 2024 17:46:51 +0800
Subject: [PATCH 37/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0RPC=E5=BD=95=E5=83=8F?=
 =?UTF-8?q?=E5=9B=9E=E6=94=BE=E5=92=8C=E5=BD=95=E5=83=8F=E4=B8=8B=E8=BD=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 ...InviteInfo.java => InviteMessageInfo.java} |  2 +-
 .../service/IGbChannelPlayService.java        |  4 +-
 .../impl/GbChannelPlayServiceImpl.java        |  4 +-
 .../gb28181/service/impl/PlayServiceImpl.java | 24 +++--
 .../request/impl/InviteRequestProcessor.java  | 10 +-
 .../redisMsg/IRedisRpcPlayService.java        |  4 +
 .../RedisRpcChannelPlayController.java        | 93 ++++++++++++++++++-
 .../service/RedisRpcPlayServiceImpl.java      | 45 +++++++++
 8 files changed, 167 insertions(+), 19 deletions(-)
 rename src/main/java/com/genersoft/iot/vmp/gb28181/bean/{InviteInfo.java => InviteMessageInfo.java} (93%)

diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteInfo.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteMessageInfo.java
similarity index 93%
rename from src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteInfo.java
rename to src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteMessageInfo.java
index 57e83bd10..beadb6901 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteInfo.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/InviteMessageInfo.java
@@ -4,7 +4,7 @@ import lombok.Data;
 
 // 从INVITE消息中解析需要的信息
 @Data
-public class InviteInfo {
+public class InviteMessageInfo {
     private String requesterId;
     private String targetChannelId;
     private String sourceChannelId;
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
index 7658f6353..967661a84 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
@@ -3,13 +3,13 @@ package com.genersoft.iot.vmp.gb28181.service;
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
-import com.genersoft.iot.vmp.gb28181.bean.InviteInfo;
+import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo;
 import com.genersoft.iot.vmp.gb28181.bean.Platform;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 
 public interface IGbChannelPlayService {
 
-    void start(CommonGBChannel channel, InviteInfo inviteInfo, Platform platform, ErrorCallback<StreamInfo> callback);
+    void start(CommonGBChannel channel, InviteMessageInfo inviteInfo, Platform platform, ErrorCallback<StreamInfo> callback);
 
     void stopPlay(InviteSessionType type, CommonGBChannel channel, String stream);
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
index 618b72a15..c1c009d32 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
@@ -3,7 +3,7 @@ package com.genersoft.iot.vmp.gb28181.service.impl;
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
-import com.genersoft.iot.vmp.gb28181.bean.InviteInfo;
+import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo;
 import com.genersoft.iot.vmp.gb28181.bean.Platform;
 import com.genersoft.iot.vmp.gb28181.bean.PlayException;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
@@ -33,7 +33,7 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
 
 
     @Override
-    public void start(CommonGBChannel channel, InviteInfo inviteInfo, Platform platform, ErrorCallback<StreamInfo> callback) {
+    public void start(CommonGBChannel channel, InviteMessageInfo inviteInfo, Platform platform, ErrorCallback<StreamInfo> callback) {
         if (channel == null || inviteInfo == null || callback == null) {
             log.warn("[通用通道点播] 参数异常, channel: {}, inviteInfo: {}, callback: {}", channel != null, inviteInfo != null, callback != null);
             throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error");
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
index 70bd6d817..868b600ca 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/PlayServiceImpl.java
@@ -295,14 +295,14 @@ public class PlayServiceImpl implements IPlayService {
         // 判断设备是否属于当前平台, 如果不属于则发起自动调用
         if (!userSetting.getServerId().equals(device.getServerId())) {
             redisRpcPlayService.play(device.getServerId(), channel.getId(), callback);
-        }else {
-            MediaServer mediaServerItem = getNewMediaServerItem(device);
-            if (mediaServerItem == null) {
-                log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", device.getDeviceId(), channel.getDeviceId());
-                throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm");
-            }
-            play(mediaServerItem, device, channel, null, callback);
+            return;
         }
+        MediaServer mediaServerItem = getNewMediaServerItem(device);
+        if (mediaServerItem == null) {
+            log.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", device.getDeviceId(), channel.getDeviceId());
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm");
+        }
+        play(mediaServerItem, device, channel, null, callback);
     }
 
     @Override
@@ -746,6 +746,11 @@ public class PlayServiceImpl implements IPlayService {
         if (channel == null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), "通道不存在");
         }
+        if (!userSetting.getServerId().equals(device.getServerId())) {
+            redisRpcPlayService.playback(device.getServerId(), channel.getId(), startTime, endTime, callback);
+            return;
+        }
+
         MediaServer newMediaServerItem = getNewMediaServerItem(device);
         if (newMediaServerItem == null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的节点");
@@ -954,6 +959,11 @@ public class PlayServiceImpl implements IPlayService {
     @Override
     public void download(Device device, DeviceChannel channel, String startTime, String endTime, int downloadSpeed, ErrorCallback<StreamInfo> callback) {
 
+        if (!userSetting.getServerId().equals(device.getServerId())) {
+            redisRpcPlayService.download(device.getServerId(), channel.getId(), startTime, endTime, downloadSpeed, callback);
+            return;
+        }
+
         MediaServer newMediaServerItem = this.getNewMediaServerItem(device);
         if (newMediaServerItem == null) {
             callback.run(InviteErrorCode.ERROR_FOR_ASSIST_NOT_READY.getCode(),
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
index 6459841ed..aed5d8736 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
@@ -121,7 +121,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
 
         SIPRequest request = (SIPRequest)evt.getRequest();
         try {
-            InviteInfo inviteInfo = decode(evt);
+            InviteMessageInfo inviteInfo = decode(evt);
 
             // 查询请求是否来自上级平台\设备
             Platform platform = platformService.queryPlatformByServerGBId(inviteInfo.getRequesterId());
@@ -247,9 +247,9 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
         }
     }
 
-    private InviteInfo decode(RequestEvent evt) throws SdpException {
+    private InviteMessageInfo decode(RequestEvent evt) throws SdpException {
 
-        InviteInfo inviteInfo = new InviteInfo();
+        InviteMessageInfo inviteInfo = new InviteMessageInfo();
         SIPRequest request = (SIPRequest)evt.getRequest();
         String[] channelIdArrayFromSub = SipUtils.getChannelIdFromRequest(request);
 
@@ -349,7 +349,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
 
     }
 
-    private String createSendSdp(SendRtpInfo sendRtpItem, InviteInfo inviteInfo, String sdpIp) {
+    private String createSendSdp(SendRtpInfo sendRtpItem, InviteMessageInfo inviteInfo, String sdpIp) {
         StringBuilder content = new StringBuilder(200);
         content.append("v=0\r\n");
         content.append("o=" + inviteInfo.getTargetChannelId() + " 0 0 IN IP4 " + sdpIp + "\r\n");
@@ -393,7 +393,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
         }
     }
 
-    public void inviteFromDeviceHandle(SIPRequest request, InviteInfo inviteInfo) {
+    public void inviteFromDeviceHandle(SIPRequest request, InviteMessageInfo inviteInfo) {
 
         if (inviteInfo.getSourceChannelId() == null) {
             log.warn("来自设备的Invite请求,无法从请求信息中确定请求来自的通道,已忽略,requesterId: {}", inviteInfo.getRequesterId());
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
index 42957aa5e..4c1bc0661 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
@@ -10,4 +10,8 @@ public interface IRedisRpcPlayService {
     void play(String serverId, Integer channelId, ErrorCallback<StreamInfo> callback);
 
     void stop(String serverId, InviteSessionType type, int channelId, String stream);
+
+    void playback(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback<StreamInfo> callback);
+
+    void download(String serverId, Integer id, String startTime, String endTime, int downloadSpeed, ErrorCallback<StreamInfo> callback);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
index 587f21987..f28aa6f11 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.service.redisMsg.control;
 
 import com.alibaba.fastjson2.JSONObject;
+import com.genersoft.iot.vmp.common.InviteInfo;
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
@@ -8,12 +9,15 @@ import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
+import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
 import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
+import com.genersoft.iot.vmp.utils.DateUtil;
+import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
@@ -68,7 +72,9 @@ public class RedisRpcChannelPlayController extends RpcController {
             return response;
         }
 
-        channelPlayService.play(channel, null, (code, msg, data) ->{
+        InviteMessageInfo inviteInfo = new InviteMessageInfo();
+        inviteInfo.setSessionName("Play");
+        channelPlayService.start(channel, inviteInfo, null, (code, msg, data) ->{
             if (code == InviteErrorCode.SUCCESS.getCode()) {
                 response.setStatusCode(Response.OK);
                 response.setBody(data);
@@ -87,7 +93,6 @@ public class RedisRpcChannelPlayController extends RpcController {
      */
     @RedisRpcMapping("stop")
     public RedisRpcResponse stop(RedisRpcRequest request) {
-        System.out.println(request.getParam().toString());
         JSONObject jsonObject = JSONObject.parseObject(request.getParam().toString());
 
         RedisRpcResponse response = request.getResponse();
@@ -119,4 +124,88 @@ public class RedisRpcChannelPlayController extends RpcController {
         return response;
     }
 
+    /**
+     * 录像回放国标设备
+     */
+    @RedisRpcMapping("playback")
+    public RedisRpcResponse playbackChannel(RedisRpcRequest request) {
+        JSONObject paramJson = JSONObject.parseObject(request.getParam().toString());
+        int channelId = paramJson.getIntValue("channelId");
+        String startTime = paramJson.getString("startTime");
+        String endTime = paramJson.getString("endTime");
+        RedisRpcResponse response = request.getResponse();
+
+        if (channelId <= 0) {
+            response.setStatusCode(Response.BAD_REQUEST);
+            response.setBody("param error");
+            return response;
+        }
+        // 获取对应的设备和通道信息
+        CommonGBChannel channel = channelService.getOne(channelId);
+        if (channel == null) {
+            response.setStatusCode(Response.BAD_REQUEST);
+            response.setBody("param error");
+            return response;
+        }
+
+        InviteMessageInfo inviteInfo = new InviteMessageInfo();
+        inviteInfo.setSessionName("Playback");
+        inviteInfo.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime));
+        inviteInfo.setStopTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime));
+        channelPlayService.start(channel, inviteInfo, null, (code, msg, data) ->{
+            if (code == InviteErrorCode.SUCCESS.getCode()) {
+                response.setStatusCode(Response.OK);
+                response.setBody(data);
+            }else {
+                response.setStatusCode(code);
+            }
+            // 手动发送结果
+            sendResponse(response);
+        });
+        return null;
+    }
+
+    /**
+     * 录像回放国标设备
+     */
+    @RedisRpcMapping("download")
+    public RedisRpcResponse downloadChannel(RedisRpcRequest request) {
+        JSONObject paramJson = JSONObject.parseObject(request.getParam().toString());
+        int channelId = paramJson.getIntValue("channelId");
+        String startTime = paramJson.getString("startTime");
+        String endTime = paramJson.getString("endTime");
+        int downloadSpeed = paramJson.getIntValue("downloadSpeed");
+        RedisRpcResponse response = request.getResponse();
+
+        if (channelId <= 0) {
+            response.setStatusCode(Response.BAD_REQUEST);
+            response.setBody("param error");
+            return response;
+        }
+        // 获取对应的设备和通道信息
+        CommonGBChannel channel = channelService.getOne(channelId);
+        if (channel == null) {
+            response.setStatusCode(Response.BAD_REQUEST);
+            response.setBody("param error");
+            return response;
+        }
+
+        InviteMessageInfo inviteInfo = new InviteMessageInfo();
+        inviteInfo.setSessionName("Download");
+        inviteInfo.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime));
+        inviteInfo.setStopTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime));
+        inviteInfo.setDownloadSpeed(downloadSpeed + "");
+        channelPlayService.start(channel, inviteInfo, null, (code, msg, data) ->{
+            if (code == InviteErrorCode.SUCCESS.getCode()) {
+                response.setStatusCode(Response.OK);
+                response.setBody(data);
+            }else {
+                response.setStatusCode(code);
+            }
+            // 手动发送结果
+            sendResponse(response);
+        });
+        return null;
+    }
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
index e6f8a0405..623aae13d 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
@@ -74,5 +74,50 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
             }
         }
     }
+
+    @Override
+    public void playback(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback<StreamInfo> callback) {
+
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("channelId", channelId);
+        jsonObject.put("startTime", startTime);
+        jsonObject.put("endTime", endTime);
+        RedisRpcRequest request = buildRequest("channel/playback", jsonObject.toString());
+        request.setToId(serverId);
+        RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS);
+        if (response == null) {
+            callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
+        }else {
+            if (response.getStatusCode() == Response.OK) {
+                StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class);
+                callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
+            }else {
+                callback.run(response.getStatusCode(), response.getBody().toString(), null);
+            }
+        }
+    }
+
+    @Override
+    public void download(String serverId, Integer channelId, String startTime, String endTime, int downloadSpeed, ErrorCallback<StreamInfo> callback) {
+
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("channelId", channelId);
+        jsonObject.put("startTime", startTime);
+        jsonObject.put("endTime", endTime);
+        jsonObject.put("downloadSpeed", downloadSpeed);
+        RedisRpcRequest request = buildRequest("channel/download", jsonObject.toString());
+        request.setToId(serverId);
+        RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getPlayTimeout(), TimeUnit.MILLISECONDS);
+        if (response == null) {
+            callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
+        }else {
+            if (response.getStatusCode() == Response.OK) {
+                StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class);
+                callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
+            }else {
+                callback.run(response.getStatusCode(), response.getBody().toString(), null);
+            }
+        }
+    }
 }
 

From a9099e0412aa5ba9b00ef5d0dcc0dc70e9b3627d Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: Fri, 13 Dec 2024 14:35:44 +0800
Subject: [PATCH 38/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0RPC=E5=BD=95=E5=83=8F?=
 =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=9F=A5=E8=AF=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../genersoft/iot/vmp/conf/UserSetting.java   |   5 +
 .../iot/vmp/conf/redis/RedisRpcConfig.java    |   5 +-
 .../controller/GBRecordController.java        |  40 ++--
 .../iot/vmp/gb28181/event/EventPublisher.java |   8 +-
 ...dEndEvent.java => RecordInfoEndEvent.java} |  17 +-
 .../gb28181/event/record/RecordInfoEvent.java |  27 +++
 ...ener.java => RecordInfoEventListener.java} |  11 +-
 .../service/IDeviceChannelService.java        |   9 +-
 .../vmp/gb28181/service/IDeviceService.java   |   5 +-
 .../service/IGbChannelPlayService.java        |   2 +-
 .../gb28181/service/IGbChannelService.java    |   2 +
 .../impl/DeviceChannelServiceImpl.java        |  86 +++++++-
 .../service/impl/DeviceServiceImpl.java       |  11 +-
 .../impl/GbChannelPlayServiceImpl.java        |   7 +-
 .../service/impl/GbChannelServiceImpl.java    |  28 +++
 .../vmp/gb28181/session/RecordDataCatch.java  |   6 +-
 .../cmd/RecordInfoQueryMessageHandler.java    |   6 +-
 .../cmd/RecordInfoResponseMessageHandler.java | 204 +++++++++---------
 .../redisMsg/IRedisRpcPlayService.java        |   6 +-
 .../RedisRpcChannelPlayController.java        |  70 ++++--
 .../control/RedisRpcSendRtpController.java    |  12 +-
 .../control/RedisRpcStreamPushController.java |  13 +-
 .../service/RedisRpcPlayServiceImpl.java      |  30 ++-
 .../iot/vmp/vmanager/bean/ErrorCode.java      |   1 +
 src/main/resources/配置详情.yml               |   2 +
 25 files changed, 409 insertions(+), 204 deletions(-)
 rename src/main/java/com/genersoft/iot/vmp/gb28181/event/record/{RecordEndEvent.java => RecordInfoEndEvent.java} (54%)
 create mode 100755 src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java
 rename src/main/java/com/genersoft/iot/vmp/gb28181/event/record/{RecordEndEventListener.java => RecordInfoEventListener.java} (85%)

diff --git a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
index 604c8c56a..b9356592f 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
@@ -37,6 +37,11 @@ public class UserSetting {
      */
     private Integer playTimeout = 10000;
 
+    /**
+     * 获取设备录像数据超时时间,单位:毫秒
+     */
+    private Integer recordInfoTimeout = 15000;
+
     /**
      * 上级点播等待超时时间,单位:毫秒
      */
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
index 02293685a..29e090ad4 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/redis/RedisRpcConfig.java
@@ -8,6 +8,7 @@ import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -129,7 +130,7 @@ public class RedisRpcConfig implements MessageListener {
                 if (method == null) {
                     // 回复404结果
                     RedisRpcResponse response = request.getResponse();
-                    response.setStatusCode(Response.NOT_FOUND);
+                    response.setStatusCode(ErrorCode.ERROR404.getCode());
                     sendResponse(response);
                     return;
                 }
@@ -185,7 +186,7 @@ public class RedisRpcConfig implements MessageListener {
         } catch (InterruptedException e) {
             log.warn("[redis rpc timeout] uri: {}, sn: {}", request.getUri(), request.getSn(), e);
             RedisRpcResponse redisRpcResponse = new RedisRpcResponse();
-            redisRpcResponse.setStatusCode(Response.BUSY_HERE);
+            redisRpcResponse.setStatusCode(ErrorCode.ERROR486.getCode());
             return redisRpcResponse;
         } finally {
             this.unsubscribe(request.getSn());
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java
index e48ae616e..87357d27a 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/controller/GBRecordController.java
@@ -36,6 +36,7 @@ import javax.sip.InvalidArgumentException;
 import javax.sip.SipException;
 import java.text.ParseException;
 import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 
 @Tag(name  = "国标录像")
 @Slf4j
@@ -72,7 +73,7 @@ public class GBRecordController {
 		if (log.isDebugEnabled()) {
 			log.debug(String.format("录像信息查询 API调用,deviceId:%s ,startTime:%s, endTime:%s",deviceId, startTime, endTime));
 		}
-		DeferredResult<WVPResult<RecordInfo>> result = new DeferredResult<>();
+		DeferredResult<WVPResult<RecordInfo>> result = new DeferredResult<>(Long.valueOf(userSetting.getRecordInfoTimeout()), TimeUnit.MILLISECONDS);
 		if (!DateUtil.verification(startTime, DateUtil.formatter)){
 			throw new ControllerException(ErrorCode.ERROR100.getCode(), "startTime格式为" + DateUtil.PATTERN);
 		}
@@ -81,35 +82,24 @@ public class GBRecordController {
 		}
 
 		Device device = deviceService.getDeviceByDeviceId(deviceId);
-		// 指定超时时间 1分钟30秒
-		String uuid = UUID.randomUUID().toString();
-		int sn  =  (int)((Math.random()*9+1)*100000);
-		String key = DeferredResultHolder.CALLBACK_CMD_RECORDINFO + deviceId + sn;
-		RequestMessage msg = new RequestMessage();
-		msg.setId(uuid);
-		msg.setKey(key);
-		try {
-			cmder.recordInfoQuery(device, channelId, startTime, endTime, sn, null, null, null, (eventResult -> {
-				WVPResult<RecordInfo> wvpResult = new WVPResult<>();
-				wvpResult.setCode(ErrorCode.ERROR100.getCode());
-				wvpResult.setMsg("查询录像失败, status: " +  eventResult.statusCode + ", message: " + eventResult.msg);
-				msg.setData(wvpResult);
-				resultHolder.invokeResult(msg);
-			}));
-		} catch (InvalidArgumentException | SipException | ParseException e) {
-			log.error("[命令发送失败] 查询录像: {}", e.getMessage());
-			throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " +  e.getMessage());
+		if (device == null) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), deviceId + " 不存在");
 		}
-
-		// 录像查询以channelId作为deviceId查询
-		resultHolder.put(key, uuid, result);
+		DeviceChannel channel = channelService.getOneForSource(device.getId(), channelId);
+		if (channel == null) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), channelId + " 不存在");
+		}
+		channelService.queryRecordInfo(device, channel, startTime, endTime, (code, msg, data)->{
+			WVPResult<RecordInfo> wvpResult = new WVPResult<>();
+			wvpResult.setCode(code);
+			wvpResult.setMsg(msg);
+			result.setResult(wvpResult);
+		});
 		result.onTimeout(()->{
-			msg.setData("timeout");
 			WVPResult<RecordInfo> wvpResult = new WVPResult<>();
 			wvpResult.setCode(ErrorCode.ERROR100.getCode());
 			wvpResult.setMsg("timeout");
-			msg.setData(wvpResult);
-			resultHolder.invokeResult(msg);
+			result.setResult(wvpResult);
 		});
         return result;
 	}
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java
index 7d4c876a9..a19a1009b 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/EventPublisher.java
@@ -3,7 +3,7 @@ package com.genersoft.iot.vmp.gb28181.event;
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEvent;
 import com.genersoft.iot.vmp.gb28181.event.device.RequestTimeoutEvent;
-import com.genersoft.iot.vmp.gb28181.event.record.RecordEndEvent;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEvent;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.mobilePosition.MobilePositionEvent;
 import com.genersoft.iot.vmp.media.bean.MediaServer;
@@ -101,9 +101,5 @@ public class EventPublisher {
 		applicationEventPublisher.publishEvent(event);
 	}
 
-	public void recordEndEventPush(RecordInfo recordInfo) {
-		RecordEndEvent outEvent = new RecordEndEvent(this);
-		outEvent.setRecordInfo(recordInfo);
-		applicationEventPublisher.publishEvent(outEvent);
-	}
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java
similarity index 54%
rename from src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEvent.java
rename to src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java
index cfd2985c5..4788eb622 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEvent.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEndEvent.java
@@ -1,7 +1,8 @@
 package com.genersoft.iot.vmp.gb28181.event.record;
 
-import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm;
 import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
+import lombok.Getter;
+import lombok.Setter;
 import org.springframework.context.ApplicationEvent;
 
 /**
@@ -9,24 +10,18 @@ import org.springframework.context.ApplicationEvent;
  * @author: pan
  * @data: 2022-02-23
  */
-
-public class RecordEndEvent extends ApplicationEvent {
+@Setter
+@Getter
+public class RecordInfoEndEvent extends ApplicationEvent {
     /**
      *
      */
     private static final long serialVersionUID = 1L;
 
-    public RecordEndEvent(Object source) {
+    public RecordInfoEndEvent(Object source) {
         super(source);
     }
 
     private RecordInfo recordInfo;
 
-    public RecordInfo getRecordInfo() {
-        return recordInfo;
-    }
-
-    public void setRecordInfo(RecordInfo recordInfo) {
-        this.recordInfo = recordInfo;
-    }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java
new file mode 100755
index 000000000..d78a22b28
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEvent.java
@@ -0,0 +1,27 @@
+package com.genersoft.iot.vmp.gb28181.event.record;
+
+import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * @description: 录像查询结束时间
+ * @author: pan
+ * @data: 2022-02-23
+ */
+
+@Setter
+@Getter
+public class RecordInfoEvent extends ApplicationEvent {
+    /**
+     *
+     */
+    private static final long serialVersionUID = 1L;
+
+    public RecordInfoEvent(Object source) {
+        super(source);
+    }
+
+    private RecordInfo recordInfo;
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEventListener.java b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java
similarity index 85%
rename from src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEventListener.java
rename to src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java
index 411b54d5c..f2a0986d6 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEventListener.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordInfoEventListener.java
@@ -15,15 +15,15 @@ import java.util.concurrent.ConcurrentHashMap;
  */
 @Slf4j
 @Component
-public class RecordEndEventListener implements ApplicationListener<RecordEndEvent> {
+public class RecordInfoEventListener implements ApplicationListener<RecordInfoEvent> {
 
-    private Map<String, RecordEndEventHandler> handlerMap = new ConcurrentHashMap<>();
+    private final Map<String, RecordEndEventHandler> handlerMap = new ConcurrentHashMap<>();
     public interface RecordEndEventHandler{
         void  handler(RecordInfo recordInfo);
     }
 
     @Override
-    public void onApplicationEvent(RecordEndEvent event) {
+    public void onApplicationEvent(RecordInfoEvent event) {
         String deviceId = event.getRecordInfo().getDeviceId();
         String channelId = event.getRecordInfo().getChannelId();
         int count = event.getRecordInfo().getCount();
@@ -45,9 +45,6 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
 
     /**
      * 添加
-     * @param device
-     * @param channelId
-     * @param recordEndEventHandler
      */
     public void addEndEventHandler(String device, String channelId, RecordEndEventHandler recordEndEventHandler) {
         log.info("录像查询事件添加监听,deviceId:{}, channelId: {}", device, channelId);
@@ -55,8 +52,6 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
     }
     /**
      * 添加
-     * @param device
-     * @param channelId
      */
     public void delEndEventHandler(String device, String channelId) {
         log.info("录像查询事件移除监听,deviceId:{}, channelId: {}", device, channelId);
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
index bb21e3214..8ef7ee52a 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceChannelService.java
@@ -1,9 +1,8 @@
 package com.genersoft.iot.vmp.gb28181.service;
 
-import com.genersoft.iot.vmp.gb28181.bean.Device;
-import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
-import com.genersoft.iot.vmp.gb28181.bean.MobilePosition;
+import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.controller.bean.ChannelReduce;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo;
 import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend;
 import com.github.pagehelper.PageInfo;
@@ -125,4 +124,8 @@ public interface IDeviceChannelService {
     List<DeviceChannel> queryChaneListByDeviceDbId(Integer deviceDbId);
 
     List<Integer> queryChaneIdListByDeviceDbIds(List<Integer> deviceDbId);
+
+    void queryRecordInfo(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> object);
+
+    void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> object);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java
index a8f48a476..43a965f4c 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IDeviceService.java
@@ -1,9 +1,8 @@
 package com.genersoft.iot.vmp.gb28181.service;
 
 import com.genersoft.iot.vmp.common.CommonCallback;
-import com.genersoft.iot.vmp.gb28181.bean.Device;
-import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo;
-import com.genersoft.iot.vmp.gb28181.bean.SyncStatus;
+import com.genersoft.iot.vmp.gb28181.bean.*;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo;
 import com.github.pagehelper.PageInfo;
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
index 967661a84..61c0b8cbf 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelPlayService.java
@@ -5,6 +5,7 @@ import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo;
 import com.genersoft.iot.vmp.gb28181.bean.Platform;
+import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 
 public interface IGbChannelPlayService {
@@ -25,6 +26,5 @@ public interface IGbChannelPlayService {
 
     void playPush(CommonGBChannel channel, String platformDeviceId, String platformName, ErrorCallback<StreamInfo> callback);
 
-
     void  stopPlayPush(CommonGBChannel channel);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java
index 8dc67df20..441b524fc 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/IGbChannelService.java
@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.gb28181.service;
 
 import com.genersoft.iot.vmp.gb28181.bean.*;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.streamPush.bean.StreamPush;
 import com.github.pagehelper.PageInfo;
 
@@ -87,4 +88,5 @@ public interface IGbChannelService {
 
     PageInfo<CommonGBChannel> queryList(int page, int count, String query, Boolean online, Boolean hasRecordPlan, Integer channelType);
 
+    void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> callback);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
index b397f4d4e..062fd1d56 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceChannelServiceImpl.java
@@ -5,37 +5,48 @@ import com.genersoft.iot.vmp.common.InviteInfo;
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
-import com.genersoft.iot.vmp.gb28181.bean.Device;
-import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
-import com.genersoft.iot.vmp.gb28181.bean.GbCode;
-import com.genersoft.iot.vmp.gb28181.bean.MobilePosition;
+import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.controller.bean.ChannelReduce;
 import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper;
 import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper;
 import com.genersoft.iot.vmp.gb28181.dao.DeviceMobilePositionMapper;
 import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper;
 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEndEvent;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
 import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
 import com.genersoft.iot.vmp.gb28181.service.IInviteStreamService;
 import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService;
+import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
 import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
+import com.genersoft.iot.vmp.media.event.media.MediaArrivalEvent;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
+import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo;
+import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
 
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import javax.sip.message.Response;
+import java.text.ParseException;
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.TimeUnit;
 
 /**
  * @author lin
@@ -71,6 +82,26 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
     @Autowired
     private IPlatformChannelService platformChannelService;
 
+    @Autowired
+    private IRedisRpcPlayService redisRpcPlayService;
+
+    @Autowired
+    private ISIPCommander commander;
+
+    // 记录录像查询的结果等待
+    private final Map<String, SynchronousQueue<RecordInfo>> topicSubscribers = new ConcurrentHashMap<>();
+
+    /**
+     * 监听录像查询结束事件
+     */
+    @Async("taskExecutor")
+    @org.springframework.context.event.EventListener
+    public void onApplicationEvent(RecordInfoEndEvent event) {
+        SynchronousQueue<RecordInfo> queue = topicSubscribers.get("record" + event.getRecordInfo().getSn());
+        if (queue != null) {
+            queue.offer(event.getRecordInfo());
+        }
+    }
 
     @Override
     public int updateChannels(Device device, List<DeviceChannel> channels) {
@@ -164,10 +195,8 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
 
     @Override
     public ResourceBaseInfo getOverview() {
-
         int online = channelMapper.getOnlineCount();
         int total = channelMapper.getAllChannelCount();
-
         return new ResourceBaseInfo(total, online);
     }
 
@@ -705,4 +734,49 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
     public void updateChannelForNotify(DeviceChannel channel) {
         channelMapper.updateChannelForNotify(channel);
     }
+
+    @Override
+    public void queryRecordInfo(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> callback) {
+        if (!userSetting.getServerId().equals(device.getServerId())){
+            redisRpcPlayService.queryRecordInfo(device.getServerId(), channel.getId(), startTime, endTime, callback);
+            return;
+        }
+        try {
+            int sn  =  (int)((Math.random()*9+1)*100000);
+            commander.recordInfoQuery(device, channel.getDeviceId(), startTime, endTime, sn, null, null, eventResult -> {
+                try {
+                    // 消息发送成功, 监听等待数据到来
+                    SynchronousQueue<RecordInfo> queue = new SynchronousQueue<>();
+                    topicSubscribers.put("record" + sn, queue);
+                    RecordInfo recordInfo = queue.poll(userSetting.getRecordInfoTimeout(), TimeUnit.MILLISECONDS);
+                    if (recordInfo == null) {
+                        callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), recordInfo);
+                    }
+                } catch (InterruptedException e) {
+                    callback.run(ErrorCode.ERROR100.getCode(), e.getMessage(), null);
+                } finally {
+                    this.topicSubscribers.remove("record" + sn);
+                }
+
+            }, (eventResult -> {
+                callback.run(ErrorCode.ERROR100.getCode(), "查询录像失败, status: " +  eventResult.statusCode + ", message: " + eventResult.msg, null);
+            }));
+        } catch (InvalidArgumentException | SipException | ParseException e) {
+            log.error("[命令发送失败] 查询录像: {}", e.getMessage());
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " +  e.getMessage());
+        }
+    }
+
+    @Override
+    public void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> callback) {
+        Device device = deviceMapper.query(channel.getGbDeviceDbId());
+        if (device == null) {
+            log.warn("[点播] 未找到通道{}的设备信息", channel);
+            callback.run(ErrorCode.ERROR100.getCode(), "设备不存在", null);
+            return;
+        }
+        DeviceChannel deviceChannel = getOneForSourceById(channel.getGbId());
+        queryRecordInfo(device, deviceChannel, startTime, endTime, callback);
+
+    }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java
index 4773b9fda..3c69da757 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/DeviceServiceImpl.java
@@ -6,6 +6,7 @@ import com.genersoft.iot.vmp.common.VideoManagerConstants;
 import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.dao.DeviceChannelMapper;
 import com.genersoft.iot.vmp.gb28181.dao.DeviceMapper;
@@ -23,10 +24,13 @@ import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.respons
 import com.genersoft.iot.vmp.media.bean.MediaServer;
 import com.genersoft.iot.vmp.media.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.ISendRtpServerService;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
+import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.ResourceBaseInfo;
+import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import lombok.extern.slf4j.Slf4j;
@@ -36,9 +40,13 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.sip.InvalidArgumentException;
 import javax.sip.SipException;
+import javax.sip.message.Response;
 import java.text.ParseException;
 import java.time.Instant;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -69,9 +77,6 @@ public class DeviceServiceImpl implements IDeviceService {
     @Autowired
     private PlatformChannelMapper platformChannelMapper;
 
-    @Autowired
-    private IDeviceChannelService deviceChannelService;
-
     @Autowired
     private DeviceChannelMapper deviceChannelMapper;
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
index c1c009d32..cd3bf2129 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelPlayServiceImpl.java
@@ -2,10 +2,7 @@ package com.genersoft.iot.vmp.gb28181.service.impl;
 
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
-import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
-import com.genersoft.iot.vmp.gb28181.bean.InviteMessageInfo;
-import com.genersoft.iot.vmp.gb28181.bean.Platform;
-import com.genersoft.iot.vmp.gb28181.bean.PlayException;
+import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
 import com.genersoft.iot.vmp.gb28181.service.IPlayService;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
@@ -223,4 +220,6 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
             callback.run(Response.BUSY_HERE, "busy here", null);
         }
     }
+
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java
index 792fd88bd..5fb01d4b8 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/service/impl/GbChannelServiceImpl.java
@@ -8,11 +8,15 @@ import com.genersoft.iot.vmp.gb28181.dao.PlatformChannelMapper;
 import com.genersoft.iot.vmp.gb28181.dao.RegionMapper;
 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
+import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
+import com.genersoft.iot.vmp.gb28181.service.IDeviceService;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
 import com.genersoft.iot.vmp.gb28181.service.IPlatformChannelService;
+import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.streamPush.bean.StreamPush;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import lombok.extern.slf4j.Slf4j;
@@ -22,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
+import javax.sip.message.Response;
 import java.util.*;
 
 @Slf4j
@@ -46,6 +51,9 @@ public class GbChannelServiceImpl implements IGbChannelService {
     @Autowired
     private GroupMapper groupMapper;
 
+    @Autowired
+    private IDeviceChannelService deviceChannelService;
+
     @Override
     public CommonGBChannel queryByDeviceId(String gbDeviceId) {
         return commonGBChannelMapper.queryByDeviceId(gbDeviceId);
@@ -726,4 +734,24 @@ public class GbChannelServiceImpl implements IGbChannelService {
         List<CommonGBChannel> all = commonGBChannelMapper.queryList(query, online,  hasRecordPlan, channelType);
         return new PageInfo<>(all);
     }
+
+    @Override
+    public void queryRecordInfo(CommonGBChannel channel, String startTime, String endTime, ErrorCallback<RecordInfo> callback) {
+        if (channel.getGbDeviceDbId() != null) {
+
+            deviceChannelService.queryRecordInfo(channel, startTime, endTime, callback);
+        } else if (channel.getStreamProxyId() != null) {
+            // 拉流代理
+            log.warn("[下载通用通道录像] 不支持下载拉流代理的录像: {}({})", channel.getGbName(), channel.getGbDeviceId());
+            throw new PlayException(Response.FORBIDDEN, "forbidden");
+        } else if (channel.getStreamPushId() != null) {
+            // 推流
+            log.warn("[下载通用通道录像] 不支持下载推流的录像: {}({})", channel.getGbName(), channel.getGbDeviceId());
+            throw new PlayException(Response.FORBIDDEN, "forbidden");
+        } else {
+            // 通道数据异常
+            log.error("[回放通用通道] 通道数据异常,无法识别通道来源: {}({})", channel.getGbName(), channel.getGbDeviceId());
+            throw new PlayException(Response.SERVER_INTERNAL_ERROR, "server internal error");
+        }
+    }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/session/RecordDataCatch.java b/src/main/java/com/genersoft/iot/vmp/gb28181/session/RecordDataCatch.java
index 3f24dbee4..e89cedf2c 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/session/RecordDataCatch.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/session/RecordDataCatch.java
@@ -1,10 +1,9 @@
 package com.genersoft.iot.vmp.gb28181.session;
 
 import com.genersoft.iot.vmp.gb28181.bean.*;
-import com.genersoft.iot.vmp.gb28181.event.record.RecordEndEventListener;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEventListener;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
-import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
@@ -24,8 +23,9 @@ public class RecordDataCatch {
 
     @Autowired
     private DeferredResultHolder deferredResultHolder;
+
     @Autowired
-    private RecordEndEventListener recordEndEventListener;
+    private RecordInfoEventListener recordEndEventListener;
 
 
     public int put(String deviceId,String channelId, String sn, int sumNum, List<RecordItem> recordItems) {
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java
index 79deb0423..ccb1b2c9c 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java
@@ -4,7 +4,7 @@ import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
 import com.genersoft.iot.vmp.gb28181.bean.Platform;
-import com.genersoft.iot.vmp.gb28181.event.record.RecordEndEventListener;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEventListener;
 import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
 import com.genersoft.iot.vmp.gb28181.service.IDeviceService;
 import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
@@ -52,7 +52,7 @@ public class RecordInfoQueryMessageHandler extends SIPRequestProcessorParent imp
     private SIPCommander commander;
 
     @Autowired
-    private RecordEndEventListener recordEndEventListener;
+    private RecordInfoEventListener recordInfoEventListener;
 
     @Override
     public void afterPropertiesSet() throws Exception {
@@ -126,7 +126,7 @@ public class RecordInfoQueryMessageHandler extends SIPRequestProcessorParent imp
         // 获取通道的原始信息
         DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
         // 接收录像数据
-        recordEndEventListener.addEndEventHandler(device.getDeviceId(), deviceChannel.getDeviceId(), (recordInfo)->{
+        recordInfoEventListener.addEndEventHandler(device.getDeviceId(), deviceChannel.getDeviceId(), (recordInfo)->{
             try {
                 log.info("[国标级联] 录像查询收到数据, 通道: {},准备转发===", channelId);
                 cmderFroPlatform.recordInfo(channel, platform, request.getFromTag(), recordInfo);
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java
index 655c05511..0ab97e87f 100755
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/RecordInfoResponseMessageHandler.java
@@ -6,6 +6,8 @@ import com.genersoft.iot.vmp.gb28181.bean.Platform;
 import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
 import com.genersoft.iot.vmp.gb28181.bean.RecordItem;
 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEndEvent;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordInfoEvent;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
 import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
@@ -19,6 +21,7 @@ import org.dom4j.Element;
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Component;
@@ -48,14 +51,7 @@ public class RecordInfoResponseMessageHandler extends SIPRequestProcessorParent
     private ResponseMessageHandler responseMessageHandler;
 
     @Autowired
-    private DeferredResultHolder deferredResultHolder;
-
-    @Autowired
-    private EventPublisher eventPublisher;
-
-    @Qualifier("taskExecutor")
-    @Autowired
-    private ThreadPoolTaskExecutor taskExecutor;
+    private ApplicationEventPublisher applicationEventPublisher;
 
     @Autowired
     private RedisTemplate<Object, Object> redisTemplate;
@@ -75,88 +71,89 @@ public class RecordInfoResponseMessageHandler extends SIPRequestProcessorParent
         }catch (SipException | InvalidArgumentException | ParseException e) {
             log.error("[命令发送失败] 国标级联 国标录像: {}", e.getMessage());
         }
-        taskExecutor.execute(()->{
-            try {
-                String sn = getText(rootElement, "SN");
-                String channelId = getText(rootElement, "DeviceID");
-                RecordInfo recordInfo = new RecordInfo();
-                recordInfo.setChannelId(channelId);
-                recordInfo.setDeviceId(device.getDeviceId());
-                recordInfo.setSn(sn);
-                recordInfo.setName(getText(rootElement, "Name"));
-                String sumNumStr = getText(rootElement, "SumNum");
-                int sumNum = 0;
-                if (!ObjectUtils.isEmpty(sumNumStr)) {
-                    sumNum = Integer.parseInt(sumNumStr);
-                }
-                recordInfo.setSumNum(sumNum);
-                Element recordListElement = rootElement.element("RecordList");
-                if (recordListElement == null || sumNum == 0) {
-                    log.info("无录像数据");
-                    recordInfo.setCount(sumNum);
-                    eventPublisher.recordEndEventPush(recordInfo);
-                    releaseRequest(device.getDeviceId(), sn,recordInfo);
-                } else {
-                    Iterator<Element> recordListIterator = recordListElement.elementIterator();
-                    if (recordListIterator != null) {
-                        List<RecordItem> recordList = new ArrayList<>();
-                        // 遍历DeviceList
-                        while (recordListIterator.hasNext()) {
-                            Element itemRecord = recordListIterator.next();
-                            Element recordElement = itemRecord.element("DeviceID");
-                            if (recordElement == null) {
-                                log.info("记录为空,下一个...");
-                                continue;
-                            }
-                            RecordItem record = new RecordItem();
-                            record.setDeviceId(getText(itemRecord, "DeviceID"));
-                            record.setName(getText(itemRecord, "Name"));
-                            record.setFilePath(getText(itemRecord, "FilePath"));
-                            record.setFileSize(getText(itemRecord, "FileSize"));
-                            record.setAddress(getText(itemRecord, "Address"));
-
-                            String startTimeStr = getText(itemRecord, "StartTime");
-                            record.setStartTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTimeStr));
-
-                            String endTimeStr = getText(itemRecord, "EndTime");
-                            record.setEndTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTimeStr));
-
-                            record.setSecrecy(itemRecord.element("Secrecy") == null ? 0
-                                    : Integer.parseInt(getText(itemRecord, "Secrecy")));
-                            record.setType(getText(itemRecord, "Type"));
-                            record.setRecorderId(getText(itemRecord, "RecorderID"));
-                            recordList.add(record);
-                        }
-                        Map<String, String> map = recordList.stream()
-                                .filter(record -> record.getDeviceId() != null)
-                                .collect(Collectors.toMap(record -> record.getStartTime()+ record.getEndTime(), UJson::writeJson));
-                        // 获取任务结果数据
-                        String resKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_PRE + channelId + sn;
-                        redisTemplate.opsForHash().putAll(resKey, map);
-                        redisTemplate.expire(resKey, recordInfoTtl, TimeUnit.SECONDS);
-                        String resCountKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_COUNT_PRE + channelId + sn;
-                        long incr = redisTemplate.opsForValue().increment(resCountKey, map.size());
-                        redisTemplate.expire(resCountKey, recordInfoTtl, TimeUnit.SECONDS);
-                        recordInfo.setRecordList(recordList);
-                        recordInfo.setCount(Math.toIntExact(incr));
-                        eventPublisher.recordEndEventPush(recordInfo);
-                        if (incr < sumNum) {
-                            return;
-                        }
-                        // 已接收完成
-                        List<RecordItem> resList = redisTemplate.opsForHash().entries(resKey).values().stream().map(e -> UJson.readJson(e.toString(), RecordItem.class)).collect(Collectors.toList());
-                        if (resList.size() < sumNum) {
-                            return;
-                        }
-                        recordInfo.setRecordList(resList);
-                        releaseRequest(device.getDeviceId(), sn,recordInfo);
-                    }
-                }
-            } catch (Exception e) {
-                log.error("[国标录像] 发现未处理的异常, \r\n{}", evt.getRequest());
-                log.error("[国标录像] 异常内容: ", e);
+        try {
+            String sn = getText(rootElement, "SN");
+            String channelId = getText(rootElement, "DeviceID");
+            RecordInfo recordInfo = new RecordInfo();
+            recordInfo.setChannelId(channelId);
+            recordInfo.setDeviceId(device.getDeviceId());
+            recordInfo.setSn(sn);
+            recordInfo.setName(getText(rootElement, "Name"));
+            String sumNumStr = getText(rootElement, "SumNum");
+            int sumNum = 0;
+            if (!ObjectUtils.isEmpty(sumNumStr)) {
+                sumNum = Integer.parseInt(sumNumStr);
             }
-        });
+            recordInfo.setSumNum(sumNum);
+            Element recordListElement = rootElement.element("RecordList");
+            if (recordListElement == null || sumNum == 0) {
+                log.info("无录像数据");
+                recordInfo.setCount(sumNum);
+                recordInfoEventPush(recordInfo);
+                recordInfoEndEventPush(recordInfo);
+            } else {
+                Iterator<Element> recordListIterator = recordListElement.elementIterator();
+                if (recordListIterator != null) {
+                    List<RecordItem> recordList = new ArrayList<>();
+                    // 遍历DeviceList
+                    while (recordListIterator.hasNext()) {
+                        Element itemRecord = recordListIterator.next();
+                        Element recordElement = itemRecord.element("DeviceID");
+                        if (recordElement == null) {
+                            log.info("记录为空,下一个...");
+                            continue;
+                        }
+                        RecordItem record = new RecordItem();
+                        record.setDeviceId(getText(itemRecord, "DeviceID"));
+                        record.setName(getText(itemRecord, "Name"));
+                        record.setFilePath(getText(itemRecord, "FilePath"));
+                        record.setFileSize(getText(itemRecord, "FileSize"));
+                        record.setAddress(getText(itemRecord, "Address"));
+
+                        String startTimeStr = getText(itemRecord, "StartTime");
+                        record.setStartTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(startTimeStr));
+
+                        String endTimeStr = getText(itemRecord, "EndTime");
+                        record.setEndTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(endTimeStr));
+
+                        record.setSecrecy(itemRecord.element("Secrecy") == null ? 0
+                                : Integer.parseInt(getText(itemRecord, "Secrecy")));
+                        record.setType(getText(itemRecord, "Type"));
+                        record.setRecorderId(getText(itemRecord, "RecorderID"));
+                        recordList.add(record);
+                    }
+                    Map<String, String> map = recordList.stream()
+                            .filter(record -> record.getDeviceId() != null)
+                            .collect(Collectors.toMap(record -> record.getStartTime()+ record.getEndTime(), UJson::writeJson));
+                    // 获取任务结果数据
+                    String resKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_PRE + channelId + sn;
+                    redisTemplate.opsForHash().putAll(resKey, map);
+                    redisTemplate.expire(resKey, recordInfoTtl, TimeUnit.SECONDS);
+                    String resCountKey = VideoManagerConstants.REDIS_RECORD_INFO_RES_COUNT_PRE + channelId + sn;
+                    Long incr = redisTemplate.opsForValue().increment(resCountKey, map.size());
+                    if (incr == null) {
+                        incr = 0L;
+                    }
+                    redisTemplate.expire(resCountKey, recordInfoTtl, TimeUnit.SECONDS);
+                    recordInfo.setRecordList(recordList);
+                    recordInfo.setCount(Math.toIntExact(incr));
+                    recordInfoEventPush(recordInfo);
+                    if (incr < sumNum) {
+                        return;
+                    }
+                    // 已接收完成
+                    List<RecordItem> resList = redisTemplate.opsForHash().entries(resKey).values().stream().map(e -> UJson.readJson(e.toString(), RecordItem.class)).collect(Collectors.toList());
+                    if (resList.size() < sumNum) {
+                        return;
+                    }
+                    recordInfo.setRecordList(resList);
+                    recordInfoEndEventPush(recordInfo);
+                }
+            }
+        } catch (Exception e) {
+            log.error("[国标录像] 发现未处理的异常, \r\n{}", evt.getRequest());
+            log.error("[国标录像] 异常内容: ", e);
+        }
     }
 
     @Override
@@ -164,18 +161,31 @@ public class RecordInfoResponseMessageHandler extends SIPRequestProcessorParent
 
     }
 
-    public void releaseRequest(String deviceId, String sn,RecordInfo recordInfo){
-        String key = DeferredResultHolder.CALLBACK_CMD_RECORDINFO + deviceId + sn;
-        // 对数据进行排序
-        if(recordInfo!=null && recordInfo.getRecordList()!=null) {
+    private void recordInfoEventPush(RecordInfo recordInfo) {
+        if (recordInfo == null) {
+            return;
+        }
+        if(recordInfo.getRecordList() != null) {
             Collections.sort(recordInfo.getRecordList());
         }else{
             recordInfo.setRecordList(new ArrayList<>());
         }
+        RecordInfoEvent outEvent = new RecordInfoEvent(this);
+        outEvent.setRecordInfo(recordInfo);
+        applicationEventPublisher.publishEvent(outEvent);
+    }
 
-        RequestMessage msg = new RequestMessage();
-        msg.setKey(key);
-        msg.setData(recordInfo);
-        deferredResultHolder.invokeAllResult(msg);
+    private void recordInfoEndEventPush(RecordInfo recordInfo) {
+        if (recordInfo == null) {
+            return;
+        }
+        if(recordInfo.getRecordList() != null) {
+            Collections.sort(recordInfo.getRecordList());
+        }else{
+            recordInfo.setRecordList(new ArrayList<>());
+        }
+        RecordInfoEndEvent outEvent = new RecordInfoEndEvent(this);
+        outEvent.setRecordInfo(recordInfo);
+        applicationEventPublisher.publishEvent(outEvent);
     }
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
index 4c1bc0661..11f80ee86 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/IRedisRpcPlayService.java
@@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.service.redisMsg;
 
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
+import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 
 public interface IRedisRpcPlayService {
@@ -13,5 +14,8 @@ public interface IRedisRpcPlayService {
 
     void playback(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback<StreamInfo> callback);
 
-    void download(String serverId, Integer id, String startTime, String endTime, int downloadSpeed, ErrorCallback<StreamInfo> callback);
+    void download(String serverId, Integer channelId, String startTime, String endTime, int downloadSpeed, ErrorCallback<StreamInfo> callback);
+
+    void queryRecordInfo(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback<RecordInfo> callback);
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
index f28aa6f11..3eb9c1e31 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcChannelPlayController.java
@@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONObject;
 import com.genersoft.iot.vmp.common.InviteInfo;
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.conf.UserSetting;
+import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcMessage;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
@@ -17,6 +18,7 @@ import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
 import com.genersoft.iot.vmp.utils.DateUtil;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -60,14 +62,14 @@ public class RedisRpcChannelPlayController extends RpcController {
         RedisRpcResponse response = request.getResponse();
 
         if (channelId <= 0) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
         // 获取对应的设备和通道信息
         CommonGBChannel channel = channelService.getOne(channelId);
         if (channel == null) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
@@ -76,7 +78,7 @@ public class RedisRpcChannelPlayController extends RpcController {
         inviteInfo.setSessionName("Play");
         channelPlayService.start(channel, inviteInfo, null, (code, msg, data) ->{
             if (code == InviteErrorCode.SUCCESS.getCode()) {
-                response.setStatusCode(Response.OK);
+                response.setStatusCode(ErrorCode.SUCCESS.getCode());
                 response.setBody(data);
             }else {
                 response.setStatusCode(code);
@@ -88,6 +90,50 @@ public class RedisRpcChannelPlayController extends RpcController {
     }
 
 
+    /**
+     * 点播国标设备
+     */
+    @RedisRpcMapping("queryRecordInfo")
+    public RedisRpcResponse queryRecordInfo(RedisRpcRequest request) {
+        JSONObject paramJson = JSONObject.parseObject(request.getParam().toString());
+        int channelId = paramJson.getIntValue("channelId");
+        String startTime = paramJson.getString("startTime");
+        String endTime = paramJson.getString("endTime");
+        RedisRpcResponse response = request.getResponse();
+
+        if (channelId <= 0) {
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
+            response.setBody("param error");
+            return response;
+        }
+        // 获取对应的设备和通道信息
+        CommonGBChannel channel = channelService.getOne(channelId);
+        if (channel == null) {
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
+            response.setBody("param error");
+            return response;
+        }
+        
+        try {
+            channelService.queryRecordInfo(channel, startTime, endTime, (code, msg, data) ->{
+                if (code == InviteErrorCode.SUCCESS.getCode()) {
+                    response.setStatusCode(ErrorCode.SUCCESS.getCode());
+                    response.setBody(data);
+                }else {
+                    response.setStatusCode(code);
+                }
+                // 手动发送结果
+                sendResponse(response);
+            });
+        }catch (ControllerException e) {
+            response.setStatusCode(ErrorCode.ERROR100.getCode());
+            response.setBody(e.getMessage());
+        }
+        
+        return null;
+    }
+
+
     /**
      * 停止点播国标设备
      */
@@ -99,7 +145,7 @@ public class RedisRpcChannelPlayController extends RpcController {
 
         Integer channelId = jsonObject.getIntValue("channelId");
         if (channelId == null || channelId <= 0) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
@@ -110,13 +156,13 @@ public class RedisRpcChannelPlayController extends RpcController {
         // 获取对应的设备和通道信息
         CommonGBChannel channel = channelService.getOne(channelId);
         if (channel == null) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
         try {
             channelPlayService.stopPlay(type, channel, stream);
-            response.setStatusCode(Response.OK);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
         }catch (Exception e){
             response.setStatusCode(Response.SERVER_INTERNAL_ERROR);
             response.setBody(e.getMessage());
@@ -136,14 +182,14 @@ public class RedisRpcChannelPlayController extends RpcController {
         RedisRpcResponse response = request.getResponse();
 
         if (channelId <= 0) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
         // 获取对应的设备和通道信息
         CommonGBChannel channel = channelService.getOne(channelId);
         if (channel == null) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
@@ -154,7 +200,7 @@ public class RedisRpcChannelPlayController extends RpcController {
         inviteInfo.setStopTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime));
         channelPlayService.start(channel, inviteInfo, null, (code, msg, data) ->{
             if (code == InviteErrorCode.SUCCESS.getCode()) {
-                response.setStatusCode(Response.OK);
+                response.setStatusCode(ErrorCode.SUCCESS.getCode());
                 response.setBody(data);
             }else {
                 response.setStatusCode(code);
@@ -178,14 +224,14 @@ public class RedisRpcChannelPlayController extends RpcController {
         RedisRpcResponse response = request.getResponse();
 
         if (channelId <= 0) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
         // 获取对应的设备和通道信息
         CommonGBChannel channel = channelService.getOne(channelId);
         if (channel == null) {
-            response.setStatusCode(Response.BAD_REQUEST);
+            response.setStatusCode(ErrorCode.ERROR400.getCode());
             response.setBody("param error");
             return response;
         }
@@ -197,7 +243,7 @@ public class RedisRpcChannelPlayController extends RpcController {
         inviteInfo.setDownloadSpeed(downloadSpeed + "");
         channelPlayService.start(channel, inviteInfo, null, (code, msg, data) ->{
             if (code == InviteErrorCode.SUCCESS.getCode()) {
-                response.setStatusCode(Response.OK);
+                response.setStatusCode(ErrorCode.SUCCESS.getCode());
                 response.setBody(data);
             }else {
                 response.setStatusCode(code);
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
index ee5d1e18e..fa8180252 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcSendRtpController.java
@@ -49,7 +49,7 @@ public class RedisRpcSendRtpController extends RpcController {
         if (sendRtpItem == null) {
             log.info("[redis-rpc] 获取发流的信息, 未找到redis中的发流信息, callId:{}", callId);
             RedisRpcResponse response = request.getResponse();
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
             return response;
         }
         log.info("[redis-rpc] 获取发流的信息: {}/{}, 目标地址: {}:{}", sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getIp(), sendRtpItem.getPort());
@@ -57,14 +57,14 @@ public class RedisRpcSendRtpController extends RpcController {
         MediaServer mediaServerItem = mediaServerService.getMediaServerByAppAndStream(sendRtpItem.getApp(), sendRtpItem.getStream());
         if (mediaServerItem == null) {
             RedisRpcResponse response = request.getResponse();
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
         }
         // 自平台内容
         int localPort = sendRtpServerService.getNextPort(mediaServerItem);
         if (localPort == 0) {
             log.info("[redis-rpc] getSendRtpItem->服务器端口资源不足" );
             RedisRpcResponse response = request.getResponse();
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
         }
         // 写入redis, 超时时回复
         sendRtpItem.setStatus(1);
@@ -77,7 +77,7 @@ public class RedisRpcSendRtpController extends RpcController {
         }
         sendRtpServerService.update(sendRtpItem);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
+        response.setStatusCode(ErrorCode.SUCCESS.getCode());
         response.setBody(callId);
         return response;
     }
@@ -90,7 +90,7 @@ public class RedisRpcSendRtpController extends RpcController {
         String callId = request.getParam().toString();
         SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
+        response.setStatusCode(ErrorCode.SUCCESS.getCode());
         if (sendRtpItem == null) {
             log.info("[redis-rpc] 开始发流, 未找到redis中的发流信息, callId:{}", callId);
             WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息");
@@ -134,7 +134,7 @@ public class RedisRpcSendRtpController extends RpcController {
         String callId = request.getParam().toString();
         SendRtpInfo sendRtpItem = sendRtpServerService.queryByCallId(callId);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(Response.OK);
+        response.setStatusCode(ErrorCode.SUCCESS.getCode());
         if (sendRtpItem == null) {
             log.info("[redis-rpc] 停止推流, 未找到redis中的发流信息, key:{}", callId);
             WVPResult wvpResult = WVPResult.fail(ErrorCode.ERROR100.getCode(), "未找到redis中的发流信息");
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
index e1ef57b5c..1cf468905 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/control/RedisRpcStreamPushController.java
@@ -18,6 +18,7 @@ import com.genersoft.iot.vmp.service.ISendRtpServerService;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcController;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RedisRpcMapping;
 import com.genersoft.iot.vmp.service.redisMsg.dto.RpcController;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
@@ -79,7 +80,7 @@ public class RedisRpcStreamPushController extends RpcController {
             sendRtpServerService.update(sendRtpItem);
             RedisRpcResponse response = request.getResponse();
             response.setBody(sendRtpItem.getChannelId());
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
         }
         // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者
         Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null);
@@ -98,7 +99,7 @@ public class RedisRpcStreamPushController extends RpcController {
             redisTemplate.opsForValue().set(sendRtpItem.getChannelId(), sendRtpItem);
             RedisRpcResponse response = request.getResponse();
             response.setBody(sendRtpItem.getChannelId());
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
             // 手动发送结果
             sendResponse(response);
             hookSubscribe.removeSubscribe(hook);
@@ -120,7 +121,7 @@ public class RedisRpcStreamPushController extends RpcController {
             log.info("[redis-rpc] 监听流上线时发现流已存在直接返回: {}/{}", streamInfo.getApp(), streamInfo.getStream());
             RedisRpcResponse response = request.getResponse();
             response.setBody(JSONObject.toJSONString(streamInfoInServer));
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
             return response;
         }
         // 监听流上线。 流上线直接发送sendRtpItem消息给实际的信令处理者
@@ -133,7 +134,7 @@ public class RedisRpcStreamPushController extends RpcController {
                     streamInfo.getApp(), streamInfo.getStream(), hookData.getMediaInfo(),
                     hookData.getMediaInfo() != null ? hookData.getMediaInfo().getCallId() : null);
             response.setBody(JSONObject.toJSONString(streamInfoByAppAndStream));
-            response.setStatusCode(200);
+            response.setStatusCode(ErrorCode.SUCCESS.getCode());
             // 手动发送结果
             sendResponse(response);
             hookSubscribe.removeSubscribe(hook);
@@ -152,7 +153,7 @@ public class RedisRpcStreamPushController extends RpcController {
         Hook hook = Hook.getInstance(HookType.on_media_arrival, sendRtpItem.getApp(), sendRtpItem.getStream(), null);
         hookSubscribe.removeSubscribe(hook);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
+        response.setStatusCode(ErrorCode.SUCCESS.getCode());
         return response;
     }
 
@@ -167,7 +168,7 @@ public class RedisRpcStreamPushController extends RpcController {
         Hook hook = Hook.getInstance(HookType.on_media_arrival, streamInfo.getApp(), streamInfo.getStream(), null);
         hookSubscribe.removeSubscribe(hook);
         RedisRpcResponse response = request.getResponse();
-        response.setStatusCode(200);
+        response.setStatusCode(ErrorCode.SUCCESS.getCode());
         return response;
     }
 
diff --git a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
index 623aae13d..a22e9dad4 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/redisMsg/service/RedisRpcPlayServiceImpl.java
@@ -9,6 +9,7 @@ import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.redis.RedisRpcConfig;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcRequest;
 import com.genersoft.iot.vmp.conf.redis.bean.RedisRpcResponse;
+import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.service.redisMsg.IRedisRpcPlayService;
@@ -48,7 +49,7 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
         if (response == null) {
             callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
         }else {
-            if (response.getStatusCode() == Response.OK) {
+            if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) {
                 StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class);
                 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
             }else {
@@ -69,12 +70,33 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
         if (response == null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg());
         }else {
-            if (response.getStatusCode() != Response.OK) {
+            if (response.getStatusCode() != ErrorCode.SUCCESS.getCode()) {
                 throw new ControllerException(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg());
             }
         }
     }
 
+    @Override
+    public void queryRecordInfo(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback<RecordInfo> callback) {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("channelId", channelId);
+        jsonObject.put("startTime", startTime);
+        jsonObject.put("endTime", endTime);
+        RedisRpcRequest request = buildRequest("channel/queryRecordInfo", jsonObject);
+        request.setToId(serverId);
+        RedisRpcResponse response = redisRpcConfig.request(request, userSetting.getRecordInfoTimeout(), TimeUnit.MILLISECONDS);
+        if (response == null) {
+            callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
+        }else {
+            if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) {
+                RecordInfo recordInfo = JSON.parseObject(response.getBody().toString(), RecordInfo.class);
+                callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), recordInfo);
+            }else {
+                callback.run(response.getStatusCode(), response.getBody().toString(), null);
+            }
+        }
+    }
+
     @Override
     public void playback(String serverId, Integer channelId, String startTime, String endTime, ErrorCallback<StreamInfo> callback) {
 
@@ -88,7 +110,7 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
         if (response == null) {
             callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
         }else {
-            if (response.getStatusCode() == Response.OK) {
+            if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) {
                 StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class);
                 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
             }else {
@@ -111,7 +133,7 @@ public class RedisRpcPlayServiceImpl implements IRedisRpcPlayService {
         if (response == null) {
             callback.run(ErrorCode.ERROR100.getCode(), ErrorCode.ERROR100.getMsg(), null);
         }else {
-            if (response.getStatusCode() == Response.OK) {
+            if (response.getStatusCode() == ErrorCode.SUCCESS.getCode()) {
                 StreamInfo streamInfo = JSON.parseObject(response.getBody().toString(), StreamInfo.class);
                 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
             }else {
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java
index e2e3879bd..53b8be605 100755
--- a/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java
@@ -9,6 +9,7 @@ public enum ErrorCode {
     ERROR400(400, "参数或方法错误"),
     ERROR404(404, "资源未找到"),
     ERROR403(403, "无权限操作"),
+    ERROR486(486, "超时或无响应"),
     ERROR401(401, "请登录后重新请求"),
     ERROR500(500, "系统异常");
 
diff --git a/src/main/resources/配置详情.yml b/src/main/resources/配置详情.yml
index cef721516..167dd8340 100644
--- a/src/main/resources/配置详情.yml
+++ b/src/main/resources/配置详情.yml
@@ -198,6 +198,8 @@ user-settings:
     save-position-history: false
     # 点播/录像回放 等待超时时间,单位:毫秒
     play-timeout: 18000
+    # 获取设备录像数据超时时间,单位:毫秒
+    record-info-timeout: 10000
     # 上级点播等待超时时间,单位:毫秒
     platform-play-timeout: 60000
     # 是否开启接口鉴权