Sfoglia il codice sorgente

增加转录功能

wangliang 3 anni fa
parent
commit
3bc3e9cc92
71 ha cambiato i file con 1906 aggiunte e 186 eliminazioni
  1. 18 1
      pom.xml
  2. 2 2
      themis-admin/pom.xml
  3. 6 0
      themis-admin/src/main/java/com/qmth/themis/admin/api/TEExamController.java
  4. 0 2
      themis-admin/src/main/java/com/qmth/themis/admin/api/TEExamCourseController.java
  5. 143 0
      themis-admin/src/main/java/com/qmth/themis/admin/api/TENotifyController.java
  6. 1 0
      themis-admin/src/main/java/com/qmth/themis/admin/api/TIeInvigilateController.java
  7. 9 1
      themis-admin/src/main/resources/application.properties
  8. 2 2
      themis-business/pom.xml
  9. 11 0
      themis-business/src/main/java/com/qmth/themis/business/bean/admin/InvigilateListDetailBean.java
  10. 11 0
      themis-business/src/main/java/com/qmth/themis/business/bean/admin/InvigilateListVideoBean.java
  11. 11 0
      themis-business/src/main/java/com/qmth/themis/business/bean/mobile/MobileAuthorizationMonitorBean.java
  12. 23 1
      themis-business/src/main/java/com/qmth/themis/business/cache/ExamRecordCacheUtil.java
  13. 7 4
      themis-business/src/main/java/com/qmth/themis/business/cache/bean/ExamCacheBean.java
  14. 56 2
      themis-business/src/main/java/com/qmth/themis/business/constant/SystemConstant.java
  15. 16 0
      themis-business/src/main/java/com/qmth/themis/business/dao/TMTencentVideoMessageMapper.java
  16. 2 1
      themis-business/src/main/java/com/qmth/themis/business/domain/AliYunOssPrivateDomain.java
  17. 2 1
      themis-business/src/main/java/com/qmth/themis/business/domain/AliYunOssPublicDomain.java
  18. 10 0
      themis-business/src/main/java/com/qmth/themis/business/domain/PrefixUrlDomain.java
  19. 69 1
      themis-business/src/main/java/com/qmth/themis/business/domain/TencentYunDomain.java
  20. 1 1
      themis-business/src/main/java/com/qmth/themis/business/dto/MqDto.java
  21. 15 6
      themis-business/src/main/java/com/qmth/themis/business/dto/request/TEExamDto.java
  22. 13 1
      themis-business/src/main/java/com/qmth/themis/business/dto/response/MonitorStreamDto.java
  23. 33 6
      themis-business/src/main/java/com/qmth/themis/business/dto/response/TEExamActivityDto.java
  24. 11 0
      themis-business/src/main/java/com/qmth/themis/business/dto/response/TEExamQueryDto.java
  25. 34 0
      themis-business/src/main/java/com/qmth/themis/business/dto/response/TEStudentExamRecordDto.java
  26. 41 0
      themis-business/src/main/java/com/qmth/themis/business/dto/response/TEStudentMonitorRecordDto.java
  27. 19 8
      themis-business/src/main/java/com/qmth/themis/business/entity/TEExam.java
  28. 10 10
      themis-business/src/main/java/com/qmth/themis/business/entity/TEExamStudent.java
  29. 78 0
      themis-business/src/main/java/com/qmth/themis/business/entity/TMTencentVideoMessage.java
  30. 67 44
      themis-business/src/main/java/com/qmth/themis/business/entity/TOeExamRecord.java
  31. 5 1
      themis-business/src/main/java/com/qmth/themis/business/enums/ExamRecordFieldEnum.java
  32. 27 25
      themis-business/src/main/java/com/qmth/themis/business/enums/MonitorRecordEnum.java
  33. 12 4
      themis-business/src/main/java/com/qmth/themis/business/enums/MonitorVideoSourceEnum.java
  34. 3 1
      themis-business/src/main/java/com/qmth/themis/business/enums/MqExecTypeEnum.java
  35. 6 1
      themis-business/src/main/java/com/qmth/themis/business/enums/MqGroupEnum.java
  36. 2 1
      themis-business/src/main/java/com/qmth/themis/business/enums/MqTagEnum.java
  37. 16 0
      themis-business/src/main/java/com/qmth/themis/business/service/TMTencentVideoMessageService.java
  38. 5 3
      themis-business/src/main/java/com/qmth/themis/business/service/impl/CacheServiceImpl.java
  39. 3 1
      themis-business/src/main/java/com/qmth/themis/business/service/impl/MqDtoServiceImpl.java
  40. 1 1
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TBAttachmentServiceImpl.java
  41. 16 1
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TEExamActivityServiceImpl.java
  42. 12 0
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TEExamServiceImpl.java
  43. 14 2
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TEMobileServiceImpl.java
  44. 13 1
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TEStudentServiceImpl.java
  45. 1 1
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TIeReportServiceImpl.java
  46. 20 0
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TMTencentVideoMessageServiceImpl.java
  47. 5 1
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TOeExamRecordServiceImpl.java
  48. 225 0
      themis-business/src/main/java/com/qmth/themis/business/util/HttpUtil.java
  49. 2 2
      themis-business/src/main/java/com/qmth/themis/business/util/OssUtil.java
  50. 85 0
      themis-business/src/main/java/com/qmth/themis/business/util/QrCodeUtil.java
  51. 9 9
      themis-business/src/main/java/com/qmth/themis/business/util/RedisUtil.java
  52. 48 0
      themis-business/src/main/java/com/qmth/themis/business/util/SSLClient.java
  53. 269 0
      themis-business/src/main/java/com/qmth/themis/business/util/TencentCloudAPITC3Util.java
  54. 70 1
      themis-business/src/main/java/com/qmth/themis/business/util/TencentYunUtil.java
  55. 1 0
      themis-business/src/main/resources/mapper/TEExamActivityMapper.xml
  56. 3 0
      themis-business/src/main/resources/mapper/TEExamMapper.xml
  57. 3 1
      themis-business/src/main/resources/mapper/TEStudentMapper.xml
  58. 5 0
      themis-business/src/main/resources/mapper/TMTencentVideoMessageMapper.xml
  59. 2 1
      themis-business/src/main/resources/mapper/TOeExamRecordMapper.xml
  60. 14 2
      themis-common/pom.xml
  61. 2 2
      themis-exam/pom.xml
  62. 20 11
      themis-exam/src/main/java/com/qmth/themis/exam/listener/service/impl/MqOeLogicServiceImpl.java
  63. 8 0
      themis-exam/src/main/resources/application.properties
  64. 2 2
      themis-mq/pom.xml
  65. 11 1
      themis-mq/src/main/java/com/qmth/themis/mq/service/MqLogicService.java
  66. 197 10
      themis-mq/src/main/java/com/qmth/themis/mq/service/impl/MqLogicServiceImpl.java
  67. 29 0
      themis-mq/src/main/java/com/qmth/themis/mq/templete/impl/TencentVideoConcurrentlyImpl.java
  68. 2 2
      themis-task/pom.xml
  69. 6 4
      themis-task/src/main/java/com/qmth/themis/task/quartz/service/impl/QuartzLogicServiceImpl.java
  70. 4 0
      themis-task/src/main/java/com/qmth/themis/task/start/StartRunning.java
  71. 7 0
      themis-task/src/main/resources/application.properties

+ 18 - 1
pom.xml

@@ -4,7 +4,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.qmth.themis</groupId>
     <artifactId>themis-service</artifactId>
-    <version>1.1.0</version>
+    <version>1.1.1</version>
     <packaging>pom</packaging>
 
     <modules>
@@ -48,7 +48,9 @@
         <swagger2-bootstrap.version>1.9.6</swagger2-bootstrap.version>
         <jetbrains.version>13.0</jetbrains.version>
         <tencentyun.version>1.1</tencentyun.version>
+        <tencentyun.sdk.version>3.1.322</tencentyun.sdk.version>
         <version-plugin.version>2.8.1</version-plugin.version>
+        <googleBar.version>3.4.0</googleBar.version>
     </properties>
 
     <dependencyManagement>
@@ -244,6 +246,21 @@
                 <artifactId>tls-sig-api-v2</artifactId>
                 <version>${tencentyun.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.tencentcloudapi</groupId>
+                <artifactId>tencentcloud-sdk-java</artifactId>
+                <version>${tencentyun.sdk.version}</version>
+            </dependency>
+<!--            <dependency>-->
+<!--                <groupId>com.google.zxing</groupId>-->
+<!--                <artifactId>core</artifactId>-->
+<!--                <version>${googleBar.version}</version>-->
+<!--            </dependency>-->
+<!--            <dependency>-->
+<!--                <groupId>com.google.zxing</groupId>-->
+<!--                <artifactId>javase</artifactId>-->
+<!--                <version>${googleBar.version}</version>-->
+<!--            </dependency>-->
         </dependencies>
     </dependencyManagement>
 

+ 2 - 2
themis-admin/pom.xml

@@ -4,13 +4,13 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.qmth.themis.admin</groupId>
     <artifactId>themis-admin</artifactId>
-    <version>1.1.0</version>
+    <version>1.1.1</version>
     <packaging>jar</packaging>
 
     <parent>
         <groupId>com.qmth.themis</groupId>
         <artifactId>themis-service</artifactId>
-        <version>1.1.0</version>
+        <version>1.1.1</version>
     </parent>
 
     <dependencies>

+ 6 - 0
themis-admin/src/main/java/com/qmth/themis/admin/api/TEExamController.java

@@ -142,6 +142,12 @@ public class TEExamController {
             }
             teExamDto.setMonitorStatus(Objects.nonNull(oldTeExam) ? oldTeExam.getMonitorStatus() : InvigilateMonitorStatusEnum.NOT_START);
             teExam = new TEExam(teExamDto);
+            List<String> monitorRecordList = teExamDto.getMonitorRecord();
+            if (Objects.nonNull(monitorRecordList) && monitorRecordList.size() > 1) {
+                if (monitorRecordList.contains(MonitorVideoSourceEnum.CLIENT_CAMERA) && monitorRecordList.contains(MonitorVideoSourceEnum.CLIENT_SCREEN)) {
+                    throw new BusinessException("客户端摄像头和屏幕只能选一个");
+                }
+            }
             teExamService.saveOrUpdate(teExam);
             if (Objects.nonNull(oldTeExam) && !Objects
                     .equals(oldTeExam.getMode().name(), teExamDto.getMode().name())) {//如果模式改变,则删除之前模式的全部quartz

+ 0 - 2
themis-admin/src/main/java/com/qmth/themis/admin/api/TEExamCourseController.java

@@ -2,7 +2,6 @@ package com.qmth.themis.admin.api;
 
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.qmth.themis.business.constant.SystemConstant;
 import com.qmth.themis.business.entity.TBUser;
 import com.qmth.themis.business.entity.TEExamCourse;
 import com.qmth.themis.business.enums.FieldUniqueEnum;
@@ -20,7 +19,6 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
-import java.util.Collections;
 import java.util.Objects;
 
 /**

+ 143 - 0
themis-admin/src/main/java/com/qmth/themis/admin/api/TENotifyController.java

@@ -0,0 +1,143 @@
+package com.qmth.themis.admin.api;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.qmth.themis.business.cache.ExamRecordCacheUtil;
+import com.qmth.themis.business.constant.SystemConstant;
+import com.qmth.themis.business.entity.TOeExamRecord;
+import com.qmth.themis.business.service.TOeExamRecordService;
+import com.qmth.themis.business.util.RedisUtil;
+import com.qmth.themis.business.util.TencentYunUtil;
+import com.qmth.themis.common.exception.BusinessException;
+import com.qmth.themis.common.util.MD5Util;
+import com.qmth.themis.common.util.Result;
+import com.qmth.themis.common.util.ResultUtil;
+import io.swagger.annotations.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * @Description: 视频源回调接口
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2022/3/25
+ */
+@Api(tags = "视频源回调接口")
+@RestController
+@RequestMapping("/${prefix.url.notify}/monitor/record")
+public class TENotifyController {
+    private final static Logger log = LoggerFactory.getLogger(TENotifyController.class);
+
+    @Resource
+    TOeExamRecordService tOeExamRecordService;
+
+    @Resource
+    TencentYunUtil tencentYunUtil;
+
+    @Resource
+    RedisUtil redisUtil;
+
+    @ApiOperation(value = "腾讯云视频接口回调")
+    @RequestMapping(value = "/tencent", method = RequestMethod.POST)
+    @ApiResponses({@ApiResponse(code = 200, message = "结果信息")})
+    public Result tencentCallback(@ApiParam(value = "腾讯云视频接口回调信息", required = true) @RequestBody JSONObject jsonObject) throws NoSuchAlgorithmException {
+        log.info("腾讯云视频接口回调 jsonObject:{}", jsonObject.toJSONString());
+        String callbackPwd = tencentYunUtil.getTencentYunDomain().getCallbackPwd();
+        Optional.ofNullable(callbackPwd).orElseThrow(() -> new BusinessException("配置文件回调密码为空"));
+
+        String sign = (String) jsonObject.get("sign");
+        Optional.ofNullable(sign).orElseThrow(() -> new BusinessException("腾讯云签名为空"));
+
+        Long t = Long.parseLong(String.valueOf(jsonObject.get("t")));
+        Optional.ofNullable(t).orElseThrow(() -> new BusinessException("腾讯云回调时间为空"));
+
+        String localSign = MD5Util.encoder(callbackPwd + String.valueOf(t));
+        if (!Objects.equals(sign, localSign)) {
+            throw new BusinessException("腾讯云签名不匹配");
+        }
+
+        String eventType = String.valueOf(jsonObject.get("event_type"));
+        //说明是录制视频回调成功
+        if (Objects.nonNull(eventType) && Objects.equals(eventType, "100")) {
+            String streamId = String.valueOf(jsonObject.get("stream_id"));
+            Long recordId = null;
+            if (Objects.nonNull(streamId)) {
+                String[] str = streamId.split("_");
+                recordId = Long.parseLong(str[2]);
+                String tencentVideoUrl = ExamRecordCacheUtil.getTencentVideoUrl(recordId);
+                if (Objects.nonNull(tencentVideoUrl)) {//说明缓存还在
+                    JSONArray jsonArrayDb = JSONArray.parseArray(tencentVideoUrl);
+                    int count = jsonArrayDb.size();
+                    JSONArray jsonArray = this.getVideoUrl(tencentVideoUrl, streamId, jsonObject);
+                    if (Objects.nonNull(jsonArray) && jsonArray.size() > 0 && count != jsonArray.size()) {
+                        ExamRecordCacheUtil.setTencentVideoUrl(recordId, jsonArray.toJSONString());
+                        tOeExamRecordService.sendExamRecordDataSaveMq(recordId, System.currentTimeMillis());
+                    }
+                } else {
+                    TOeExamRecord tOeExamRecord = tOeExamRecordService.getById(recordId);
+                    int count = 0;
+                    JSONArray jsonArrayDb = null;
+                    if (Objects.nonNull(tOeExamRecord.getTencentVideoUrl())) {
+                        jsonArrayDb = JSONArray.parseArray(tOeExamRecord.getTencentVideoUrl());
+                        count = jsonArrayDb.size();
+                    }
+                    JSONArray jsonArray = this.getVideoUrl(tOeExamRecord.getTencentVideoUrl(), streamId, jsonObject);
+                    if (Objects.nonNull(jsonArray) && jsonArray.size() > 0 && count != jsonArray.size()) {
+                        tOeExamRecord.setTencentVideoUrl(jsonArray.toJSONString());
+                        tOeExamRecordService.updateById(tOeExamRecord);
+                    }
+                }
+            }
+        }
+        return ResultUtil.ok(true);
+    }
+
+    /**
+     * 获取视频源地址
+     *
+     * @param tencentVideoUrl
+     * @param streamId
+     * @param jsonObject
+     * @return
+     */
+    private JSONArray getVideoUrl(String tencentVideoUrl, String streamId, JSONObject jsonObject) {
+        Map<String, String> map = new HashMap<>();
+        JSONArray jsonArray = null;
+        if (Objects.isNull(tencentVideoUrl)) {
+            jsonArray = new JSONArray();
+        } else {
+            jsonArray = JSONArray.parseArray(tencentVideoUrl);
+        }
+        for (int i = 0; i < jsonArray.size(); i++) {
+            JSONObject json = (JSONObject) jsonArray.get(i);
+            String videoSource = SystemConstant.getMonitorRecordStreamId((String) json.get(SystemConstant.VIDEO_SOURCE));
+            map.put(videoSource, videoSource);
+        }
+        String videoSource = SystemConstant.getMonitorRecordStreamId(streamId);
+        boolean lock = redisUtil.lock(SystemConstant.REDIS_LOCK_TENCENT_VIDEO_PREFIX + videoSource, SystemConstant.REDIS_LOCK_TENCENT_VIDEO_TIME_OUT);
+        if (lock && Objects.isNull(map.get(videoSource))) {
+            try {
+                String videoUrl = String.valueOf(jsonObject.get("video_url"));
+                JSONObject json = new JSONObject();
+                json.put(SystemConstant.VIDEO_SOURCE, videoSource);
+                json.put(SystemConstant.VIDEO_URL, videoUrl);
+                jsonArray.add(json);
+            } finally {
+                redisUtil.releaseLock(SystemConstant.REDIS_LOCK_TENCENT_VIDEO_PREFIX + videoSource);
+            }
+        }
+        return jsonArray;
+    }
+}

+ 1 - 0
themis-admin/src/main/java/com/qmth/themis/admin/api/TIeInvigilateController.java

@@ -335,6 +335,7 @@ public class TIeInvigilateController {
             tIeInvigilateWarnInfoService.update(tIeExamInvigilateNoticeUpdateWrapper);
         }
         invigilateListDetailBean.setMonitorVideoSource(examCacheBean.getMonitorVideoSource());
+        invigilateListDetailBean.setMonitorRecord(examCacheBean.getMonitorRecord());
         return ResultUtil.ok(invigilateListDetailBean);
     }
 

+ 9 - 1
themis-admin/src/main/resources/application.properties

@@ -128,6 +128,12 @@ aliyun.oss.privateUrl=http://static-test.qmth.com.cn
 tencentyun.sdk.appId=1400411036
 tencentyun.sdk.key=d78004c94473cb1cf78af33d333e18b731132e527e829e44e2ab133945243b11
 tencentyun.sdk.urls=https://live1.qmth.com.cn,https://live2.qmth.com.cn,https://live3.qmth.com.cn,https://live4.qmth.com.cn,https://live5.qmth.com.cn,https://live6.qmth.com.cn
+tencentyun.sdk.service=vod
+tencentyun.sdk.queryUrl=${tencentyun.sdk.service}.tencentcloudapi.com
+tencentyun.sdk.secretId=AKIDKUO2PVLuCDxQcW9VaOuA8pFOGq9BwQdZ
+tencentyun.sdk.secretKey=g5D4dwxhByXrvjGWVDfqkPqzrSmqd9OM
+tencentyun.sdk.vodAppId=1500002365
+tencentyun.sdk.callbackPwd=123456
 
 #\u7CFB\u7EDF\u914D\u7F6E
 sys.config.datacenterId=1
@@ -189,13 +195,15 @@ mq.config.map.EXAM_BREAK_GROUP=themis-group-exam-examBreak
 mq.config.map.EXAM_BREAK_DELAY_GROUP=themis-group-exam-examBreakDelay
 mq.config.map.WEBSOCKET_OE_GROUP=themis-group-exam-websocketOe
 mq.config.map.WEBSOCKET_OE_MOBILE_GROUP=themis-group-exam-websocketOeMobile
+mq.config.map.TENCENT_VIDEO_GROUP=themis-group-exam-tencentVideo
 
 #api\u524D\u7F00
 prefix.url.admin=api/admin
 prefix.url.open=api/open
+prefix.url.notify=api/notify
 
 monitor.config.prefix=oe_test
 
 #\u65E0\u9700\u9274\u6743\u7684url
-no.auth.urls=/webjars/**,/druid/**,/swagger-ui.html,/doc.html,/swagger-resources/**,/v2/api-docs,/webjars/springfox-swagger-ui/**,/api/admin/user/login/account,/api/admin/sys/org/queryByOrgCode,/file/**,/upload/**,/client/**,/base_photo/**,/frontend/**,/api/admin/client/save,/api/admin/client/upload,/api/admin/client/query,/api/admin/app/save,/api/admin/app/query
+no.auth.urls=/webjars/**,/druid/**,/swagger-ui.html,/doc.html,/swagger-resources/**,/v2/api-docs,/webjars/springfox-swagger-ui/**,/api/admin/user/login/account,/api/admin/sys/org/queryByOrgCode,/file/**,/upload/**,/client/**,/base_photo/**,/frontend/**,/api/admin/client/save,/api/admin/client/upload,/api/admin/client/query,/api/admin/app/save,/api/admin/app/query,/api/notify/monitor/record/tencent
 common.system.urls=/api/admin/sys/getMenu,/api/admin/user/logout,/api/admin/sys/env,/api/admin/sys/file/upload,/api/admin/sys/file/download,/api/admin/sys/org/query,/api/admin/sys/role/query,/api/admin/sys/examActivity/query,/api/admin/sys/exam/query,/api/admin/sys/examRoom/query,/api/admin/sys/exam/privilegeQuery,/api/admin/student/photo/upload,/api/admin/sys/getPlayUrls

+ 2 - 2
themis-business/pom.xml

@@ -5,13 +5,13 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>com.qmth.themis.business</groupId>
 	<artifactId>themis-business</artifactId>
-	<version>1.1.0</version>
+	<version>1.1.1</version>
 	<packaging>jar</packaging>
 
 	<parent>
 		<groupId>com.qmth.themis</groupId>
 		<artifactId>themis-service</artifactId>
-		<version>1.1.0</version>
+		<version>1.1.1</version>
 	</parent>
 
 	<dependencies>

+ 11 - 0
themis-business/src/main/java/com/qmth/themis/business/bean/admin/InvigilateListDetailBean.java

@@ -84,6 +84,9 @@ public class InvigilateListDetailBean implements Serializable {
     @ApiModelProperty(name = "监控源")
     private String monitorVideoSource;
 
+    @ApiModelProperty(name = "转录源")
+    private String monitorRecord;
+
     public String getMonitorVideoSource() {
         return monitorVideoSource;
     }
@@ -125,6 +128,14 @@ public class InvigilateListDetailBean implements Serializable {
         this.basePhotoPath = basePhotoPath;
     }
 
+    public String getMonitorRecord() {
+        return monitorRecord;
+    }
+
+    public void setMonitorRecord(String monitorRecord) {
+        this.monitorRecord = monitorRecord;
+    }
+
     public String getExamName() {
         return examName;
     }

+ 11 - 0
themis-business/src/main/java/com/qmth/themis/business/bean/admin/InvigilateListVideoBean.java

@@ -115,6 +115,17 @@ public class InvigilateListVideoBean implements Serializable {
     @ApiModelProperty(name = "开启监控的视频源")
     private String monitorVideoSource;
 
+    @ApiModelProperty(name = "开启转录的视频源")
+    private String monitorRecord;
+
+    public String getMonitorRecord() {
+        return monitorRecord;
+    }
+
+    public void setMonitorRecord(String monitorRecord) {
+        this.monitorRecord = monitorRecord;
+    }
+
     public String getMonitorVideoSource() {
         return monitorVideoSource;
     }

+ 11 - 0
themis-business/src/main/java/com/qmth/themis/business/bean/mobile/MobileAuthorizationMonitorBean.java

@@ -28,6 +28,17 @@ public class MobileAuthorizationMonitorBean extends MobileAuthorizationBean {
     @ApiModelProperty("CDN线路id")
     private String monitorStreamId;
 
+    @ApiModelProperty("转录id")
+    private String monitorRecordId;
+
+    public String getMonitorRecordId() {
+        return monitorRecordId;
+    }
+
+    public void setMonitorRecordId(String monitorRecordId) {
+        this.monitorRecordId = monitorRecordId;
+    }
+
     public String getMonitorStreamId() {
         return monitorStreamId;
     }

+ 23 - 1
themis-business/src/main/java/com/qmth/themis/business/cache/ExamRecordCacheUtil.java

@@ -272,7 +272,8 @@ public class ExamRecordCacheUtil {
 
     public static String getMonitorKey(Long recordId) {
         if (Objects.isNull(redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId), ExamRecordFieldEnum.monitor_key.getCode()))) {
-            setMonitorKey(recordId, String.valueOf(redisUtil.getRedisSequence()));
+//            setMonitorKey(recordId, String.valueOf(redisUtil.getRedisSequence()));
+            setMonitorKey(recordId, String.valueOf(recordId));
         }
         return (String) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId), ExamRecordFieldEnum.monitor_key.getCode());
     }
@@ -719,6 +720,18 @@ public class ExamRecordCacheUtil {
         }
     }
 
+    public static void setMonitorRecord(Long recordId, String monitorRecord) {
+        if (Objects.nonNull(getId(recordId))) {
+            redisUtil.set(RedisKeyHelper.examRecordCacheKey(recordId), ExamRecordFieldEnum.monitor_record.getCode(), monitorRecord);
+        }
+    }
+
+    public static void setTencentVideoUrl(Long recordId, String tencentVideoUrl) {
+        if (Objects.nonNull(getId(recordId))) {
+            redisUtil.set(RedisKeyHelper.examRecordCacheKey(recordId), ExamRecordFieldEnum.tencent_video_url.getCode(), tencentVideoUrl);
+        }
+    }
+
     public static void setInProcessRealnessVerifyStatus(Long recordId, Integer inProcessRealnessVerifyStatus) {
         if (Objects.nonNull(getId(recordId))) {
             redisUtil.set(RedisKeyHelper.examRecordCacheKey(recordId), ExamRecordFieldEnum.in_process_realness_verify_status.getCode(), inProcessRealnessVerifyStatus);
@@ -781,6 +794,14 @@ public class ExamRecordCacheUtil {
         return (Integer) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId), ExamRecordFieldEnum.in_process_face_verify_status.getCode());
     }
 
+    public static String getMonitorRecord(Long recordId) {
+        return (String) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId), ExamRecordFieldEnum.monitor_record.getCode());
+    }
+
+    public static String getTencentVideoUrl(Long recordId) {
+        return (String) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId), ExamRecordFieldEnum.tencent_video_url.getCode());
+    }
+
     /**
      * 更新考试记录缓存
      *
@@ -803,6 +824,7 @@ public class ExamRecordCacheUtil {
         ExamRecordCacheUtil.setInProcessLivenessJudgePolicy(recordId, examCache.getInProcessLivenessJudgePolicy().name());
         ExamRecordCacheUtil.setInProcessLivenessFixedRange(recordId, examCache.getInProcessLivenessFixedRange());
         ExamRecordCacheUtil.setMonitorVideoSource(recordId, examCache.getMonitorVideoSource());
+        ExamRecordCacheUtil.setMonitorRecord(recordId, examCache.getMonitorRecord());
         TOeExamRecordService tOeExamRecordService = SpringContextHolder.getBean(TOeExamRecordService.class);
         tOeExamRecordService.sendExamRecordDataSaveMq(recordId, System.currentTimeMillis());
     }

+ 7 - 4
themis-business/src/main/java/com/qmth/themis/business/cache/bean/ExamCacheBean.java

@@ -143,8 +143,11 @@ public class ExamCacheBean implements Serializable {
 
     private Boolean monitorAudioEnable = false;//客户端监控是否启用音频与麦克风
 
-    //是否开始监控转录,off:禁用; mix:混流转录
-    private MonitorRecordEnum monitorRecord;
+//    //是否开始监控转录,off:禁用; mix:混流转录
+//    private MonitorRecordEnum monitorRecord;
+
+    //开启转录的视频源
+    private String monitorRecord;
 
     //是否允许使用移动端拍照答题,0:不开启,1:开启
     private Integer mobilePhotoUpload;
@@ -224,11 +227,11 @@ public class ExamCacheBean implements Serializable {
         this.monitorVideoSource = monitorVideoSource;
     }
 
-    public MonitorRecordEnum getMonitorRecord() {
+    public String getMonitorRecord() {
         return monitorRecord;
     }
 
-    public void setMonitorRecord(MonitorRecordEnum monitorRecord) {
+    public void setMonitorRecord(String monitorRecord) {
         this.monitorRecord = monitorRecord;
     }
 

+ 56 - 2
themis-business/src/main/java/com/qmth/themis/business/constant/SystemConstant.java

@@ -31,6 +31,8 @@ public class SystemConstant {
 
     public static final String ORG_INFO = "orgInfo";
 
+    public static final String CHARSET_NAME = "UTF-8";
+
     /**
      * 阿里云oss
      */
@@ -51,14 +53,42 @@ public class SystemConstant {
 
 //    public static final String TENCENT_KEY = "key";
 
-    public static final long TENCENT_EXPIRE_TIME = 24 * 3600 * 30;
+    public static final long TENCENT_EXPIRE_TIME = 24 * 3600 * 30;//30天过期
+
+    public static final String HEADER_AUTHORIZATION = "Authorization";
+
+    public static final String HEADER_TIME = "time";
+
+    public static final String X_TC_ACTION = "X-TC-Action";
+
+    public static final String X_TC_TIMESTAMP = "X-TC-Timestamp";
+
+    public static final String X_TC_VERSION = "X-TC-Version";
+
+    public static final String OFFSET = "Offset";
+
+    public static final String LIMIT = "Limit";
+
+    public static final String SORT_FIELD = "Sort.Field";
+
+    public static final String SORT_ORDER = "Sort.Order";
+
+    public static final String SUB_APP_ID = "SubAppId";
+
+    public static final String ACTION = "SearchMedia";
+
+    public static final String VIDEO_SOURCE = "videoSource";
+
+    public static final String VIDEO_URL = "videoUrl";
 
     /**
      * 系统相关
      */
+    public static final String LOG_ERROR = "请求出错:";
+
     public static final String EXTEND_COLUMN = "extendColumn";
 
-    public static final String MAP = "Map";
+    public static final String MOBILE_REMOVE_WEBSOCKET = "mobileRemoveWebsocket";
 
     public static final String IMPORT_INIT = "准备开始处理导入数据";
 
@@ -114,6 +144,8 @@ public class SystemConstant {
 
     public static final String OSS = "oss";
 
+    public static final String FILE_HOST = "fileHost";
+
     public static final String PATH = "path";
 
     public static final String ID = "id";
@@ -230,6 +262,8 @@ public class SystemConstant {
     //手机锁
     public static final String REDIS_LOCK_MOBILE_AUTHORIZATION_PREFIX = "lock:mobile:authorization_";
 
+    public static final String REDIS_LOCK_TENCENT_VIDEO_PREFIX = "lock:tencent:video:";//腾讯云视频锁
+
     /**
      * redis过期时间
      */
@@ -241,6 +275,8 @@ public class SystemConstant {
 
     public static final long REDIS_DEFAULT_EXPIRE_TIME = 24 * 60L * 60L;//过期时间24小时
 
+    public static final long REDIS_LOCK_TENCENT_VIDEO_TIME_OUT = 60L * 1;
+
     /**
      * rocket mq
      */
@@ -462,4 +498,22 @@ public class SystemConstant {
             throw new BusinessException(ExceptionResultEnum.EXAM_STATUS_ERROR);
         }
     }
+
+    /**
+     * 获取监控源
+     *
+     * @param videoSource
+     * @return
+     */
+    public static String getMonitorRecordStreamId(String videoSource) {
+        MonitorVideoSourceEnum monitorVideoSourceEnum = null;
+        if (videoSource.contains(MonitorVideoSourceEnum.CLIENT_CAMERA.name().toLowerCase())) {
+            monitorVideoSourceEnum = MonitorVideoSourceEnum.CLIENT_CAMERA;
+        } else if (videoSource.contains(MonitorVideoSourceEnum.MOBILE_FIRST.name().toLowerCase())) {
+            monitorVideoSourceEnum = MonitorVideoSourceEnum.MOBILE_FIRST;
+        } else if (videoSource.contains(MonitorVideoSourceEnum.MOBILE_SECOND.name().toLowerCase())) {
+            monitorVideoSourceEnum = MonitorVideoSourceEnum.MOBILE_SECOND;
+        }
+        return videoSource.substring(0, videoSource.indexOf(monitorVideoSourceEnum.name().toLowerCase()) + monitorVideoSourceEnum.name().length());
+    }
 }

+ 16 - 0
themis-business/src/main/java/com/qmth/themis/business/dao/TMTencentVideoMessageMapper.java

@@ -0,0 +1,16 @@
+package com.qmth.themis.business.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qmth.themis.business.entity.TMTencentVideoMessage;
+
+/**
+ * <p>
+ * 腾讯云视频回调消息 Mapper 接口
+ * </p>
+ *
+ * @author wangliang
+ * @since 2022-03-29
+ */
+public interface TMTencentVideoMessageMapper extends BaseMapper<TMTencentVideoMessage> {
+
+}

+ 2 - 1
themis-business/src/main/java/com/qmth/themis/business/domain/AliYunOssPrivateDomain.java

@@ -31,7 +31,7 @@ public class AliYunOssPrivateDomain implements Serializable {
 
     private Map<String, Object> map;
 
-    public AliYunOssPrivateDomain(String privateEndpoint, String privateName, String privateAccessKeyId, String privateAccessKeySecret, String privateBucket, String privateUrl, Boolean oss, List<String> attachmentType) {
+    public AliYunOssPrivateDomain(String privateEndpoint, String privateName, String privateAccessKeyId, String privateAccessKeySecret, String privateBucket, String privateUrl, Boolean oss, String fileHost, List<String> attachmentType) {
         this.privateEndpoint = privateEndpoint;
         this.privateName = privateName;
         this.privateAccessKeyId = privateAccessKeyId;
@@ -46,6 +46,7 @@ public class AliYunOssPrivateDomain implements Serializable {
         map.put(SystemConstant.NAME, this.privateName);
         map.put(SystemConstant.URL, this.privateUrl);
         map.put(SystemConstant.OSS, oss);
+        map.put(SystemConstant.FILE_HOST, fileHost);
         map.put(SystemConstant.ATTACHMENT_TYPE, attachmentType);
         map.put(SystemConstant.UPLOAD_TYPE, UploadFileEnum.file.name());
     }

+ 2 - 1
themis-business/src/main/java/com/qmth/themis/business/domain/AliYunOssPublicDomain.java

@@ -34,7 +34,7 @@ public class AliYunOssPublicDomain implements Serializable {
 
     }
 
-    public AliYunOssPublicDomain(String publicEndpoint, String publicName, String publicAccessKeyId, String publicAccessKeySecret, String publicBucket, String publicUrl, Boolean oss, List<String> attachmentType) {
+    public AliYunOssPublicDomain(String publicEndpoint, String publicName, String publicAccessKeyId, String publicAccessKeySecret, String publicBucket, String publicUrl, Boolean oss, String fileHost, List<String> attachmentType) {
         this.publicEndpoint = publicEndpoint;
         this.publicName = publicName;
         this.publicAccessKeyId = publicAccessKeyId;
@@ -49,6 +49,7 @@ public class AliYunOssPublicDomain implements Serializable {
         map.put(SystemConstant.NAME, this.publicName);
         map.put(SystemConstant.URL, this.publicUrl);
         map.put(SystemConstant.OSS, oss);
+        map.put(SystemConstant.FILE_HOST, fileHost);
         map.put(SystemConstant.ATTACHMENT_TYPE, attachmentType);
     }
 

+ 10 - 0
themis-business/src/main/java/com/qmth/themis/business/domain/PrefixUrlDomain.java

@@ -23,6 +23,16 @@ public class PrefixUrlDomain implements Serializable {
     
     String open;
 
+    String notify;
+
+    public String getNotify() {
+        return notify;
+    }
+
+    public void setNotify(String notify) {
+        this.notify = notify;
+    }
+
     public String getAdmin() {
         return admin;
     }

+ 69 - 1
themis-business/src/main/java/com/qmth/themis/business/domain/TencentYunDomain.java

@@ -15,15 +15,83 @@ public class TencentYunDomain implements Serializable {
     private String appId;
     private String key;
     private List<String> urls;
+    private String queryUrl;
+    private String service;
+    private String secretId;
+    private String secretKey;
+    private Long vodAppId;
+    private String callbackPwd;
 
     public TencentYunDomain() {
 
     }
 
-    public TencentYunDomain(String appId, String key, List<String> urls) {
+    public TencentYunDomain(String appId,
+                            String key,
+                            List<String> urls,
+                            String queryUrl,
+                            String service,
+                            String secretId,
+                            String secretKey,
+                            Long vodAppId,
+                            String callbackPwd) {
         this.appId = appId;
         this.key = key;
         this.urls = urls;
+        this.queryUrl = queryUrl;
+        this.service = service;
+        this.secretId = secretId;
+        this.secretKey = secretKey;
+        this.vodAppId = vodAppId;
+        this.callbackPwd = callbackPwd;
+    }
+
+    public String getCallbackPwd() {
+        return callbackPwd;
+    }
+
+    public void setCallbackPwd(String callbackPwd) {
+        this.callbackPwd = callbackPwd;
+    }
+
+    public Long getVodAppId() {
+        return vodAppId;
+    }
+
+    public void setVodAppId(Long vodAppId) {
+        this.vodAppId = vodAppId;
+    }
+
+    public String getService() {
+        return service;
+    }
+
+    public void setService(String service) {
+        this.service = service;
+    }
+
+    public String getSecretId() {
+        return secretId;
+    }
+
+    public void setSecretId(String secretId) {
+        this.secretId = secretId;
+    }
+
+    public String getSecretKey() {
+        return secretKey;
+    }
+
+    public void setSecretKey(String secretKey) {
+        this.secretKey = secretKey;
+    }
+
+    public String getQueryUrl() {
+        return queryUrl;
+    }
+
+    public void setQueryUrl(String queryUrl) {
+        this.queryUrl = queryUrl;
     }
 
     public List<String> getUrls() {

+ 1 - 1
themis-business/src/main/java/com/qmth/themis/business/dto/MqDto.java

@@ -74,7 +74,7 @@ public class MqDto implements Serializable {
         this.id = String.valueOf(UUID.randomUUID()).replaceAll("-", "");
     }
 
-    public MqDto(String topic, String tag, Object body, MqTagEnum type, String objId, Map properties, String objName) {
+    public MqDto(String topic, String tag, Object body, MqTagEnum type, String objId, Map<String, Object> properties, String objName) {
         this.topic = topic;
         this.tag = tag;
         this.body = body;

+ 15 - 6
themis-business/src/main/java/com/qmth/themis/business/dto/request/TEExamDto.java

@@ -164,9 +164,13 @@ public class TEExamDto extends BaseEntity {
     @TableField(value = "monitor_video_source")
     private List<String> monitorVideoSource;
 
-    @ApiModelProperty(value = "是否开始监控转录,off:禁用; mix:混流转录")
+//    @ApiModelProperty(value = "是否开始监控转录,off:禁用; mix:混流转录")
+//    @TableField(value = "monitor_record")
+//    private MonitorRecordEnum monitorRecord;
+
+    @ApiModelProperty(value = "开启转录的视频源")
     @TableField(value = "monitor_record")
-    private MonitorRecordEnum monitorRecord;
+    private List<String> monitorRecord;
 
     @ApiModelProperty(value = "算分进度")
     @TableField(value = "progress")
@@ -231,7 +235,7 @@ public class TEExamDto extends BaseEntity {
                 Integer integer = Integer.valueOf(longs[i].trim().replaceAll(" ", ""));
                 inProcessLivenessFixedRange.add(integer);
             }
-            if (Objects.equals(inProcessLivenessFixedRange.toString().trim().replaceAll(" ",""), "")) {
+            if (Objects.equals(inProcessLivenessFixedRange.toString().trim().replaceAll(" ", ""), "")) {
                 setInProcessLivenessFixedRange(null);
             } else {
                 setInProcessLivenessFixedRange(inProcessLivenessFixedRange);
@@ -248,7 +252,12 @@ public class TEExamDto extends BaseEntity {
         } else {
             setMonitorVideoSource(null);
         }
-        this.monitorRecord = teExam.getMonitorRecord();
+        if (Objects.nonNull(teExam.getMonitorRecord()) && !Objects.equals(teExam.getMonitorRecord().trim().replaceAll(" ", ""), "")) {
+            setMonitorRecord(Arrays.asList(teExam.getMonitorRecord().trim().toUpperCase().replaceAll(" ", "").split(",")));
+        } else {
+            setMonitorRecord(null);
+        }
+//        this.monitorRecord = teExam.getMonitorRecord();
         this.progress = teExam.getProgress();
         setCreateId(teExam.getCreateId());
         setCreateTime(teExam.getCreateTime());
@@ -577,11 +586,11 @@ public class TEExamDto extends BaseEntity {
         this.monitorVideoSource = monitorVideoSource;
     }
 
-    public MonitorRecordEnum getMonitorRecord() {
+    public List<String> getMonitorRecord() {
         return monitorRecord;
     }
 
-    public void setMonitorRecord(MonitorRecordEnum monitorRecord) {
+    public void setMonitorRecord(List<String> monitorRecord) {
         this.monitorRecord = monitorRecord;
     }
 

+ 13 - 1
themis-business/src/main/java/com/qmth/themis/business/dto/response/MonitorStreamDto.java

@@ -20,13 +20,25 @@ public class MonitorStreamDto implements Serializable {
     @ApiModelProperty(name = "监控源id")
     private String streamId;
 
+    @ApiModelProperty(name = "转录id")
+    private String recordId;
+
     public MonitorStreamDto() {
 
     }
 
-    public MonitorStreamDto(MonitorVideoSourceEnum name, String streamId) {
+    public MonitorStreamDto(MonitorVideoSourceEnum name, String streamId, String recordId) {
         this.name = name;
         this.streamId = streamId;
+        this.recordId = recordId;
+    }
+
+    public String getRecordId() {
+        return recordId;
+    }
+
+    public void setRecordId(String recordId) {
+        this.recordId = recordId;
     }
 
     public MonitorVideoSourceEnum getName() {

+ 33 - 6
themis-business/src/main/java/com/qmth/themis/business/dto/response/TEExamActivityDto.java

@@ -8,6 +8,7 @@ import com.qmth.themis.business.cache.bean.ExamCacheBean;
 import com.qmth.themis.business.cache.bean.ExamCourseCacheBean;
 import com.qmth.themis.business.cache.bean.ExamStudentCacheBean;
 import com.qmth.themis.business.constant.SystemConstant;
+import com.qmth.themis.business.entity.TEExamActivity;
 import com.qmth.themis.business.enums.EntryAuthenticationPolicyEnum;
 import com.qmth.themis.business.enums.ExamModeEnum;
 import com.qmth.themis.business.enums.HardwareTestEnum;
@@ -15,10 +16,9 @@ import com.qmth.themis.business.enums.MonitorVideoSourceEnum;
 import io.swagger.annotations.ApiModelProperty;
 
 import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * @Description: 考试场次dto
@@ -61,6 +61,9 @@ public class TEExamActivityDto implements Serializable {
     @ApiModelProperty(name = "监控源")
     private String monitorVideoSourceStr;
 
+    @ApiModelProperty(name = "转录源")
+    private String monitorRecordStr;
+
     @ApiModelProperty(name = "监控源集合")
     private List<MonitorStreamDto> monitorVideoSource;
 
@@ -157,7 +160,7 @@ public class TEExamActivityDto implements Serializable {
                 cameraPhotoUpload = null, mobilePhotoUpload = null, inProcessFaceStrangerIgnore = null, inProcessFaceVerify = null,
                 inProcessLivenessVerify = null, inProcessRealnessVerify = null;
         String entryAuthenticationPolicy = null, inProcessLivenessFixedRange = null, inProcessLivenessJudgePolicy = null,
-                monitorVideoSource = null;
+                monitorVideoSource = null, monitorRecord = null;
         if (cache) {
             startTime = ExamRecordCacheUtil.getStartTime(recordId);
             endTime = ExamRecordCacheUtil.getEndTime(recordId);
@@ -175,6 +178,7 @@ public class TEExamActivityDto implements Serializable {
             inProcessFaceVerify = ExamRecordCacheUtil.getInProcessFaceVerifyStatus(recordId);
             inProcessLivenessVerify = ExamRecordCacheUtil.getInProcessLivenessVerifyStatus(recordId);
             inProcessRealnessVerify = ExamRecordCacheUtil.getInProcessRealnessVerifyStatus(recordId);
+            monitorRecord = ExamRecordCacheUtil.getMonitorRecord(recordId);
         } else {
             startTime = examActivityCacheBean.getStartTime();
             endTime = ec.getEndTime();
@@ -192,6 +196,7 @@ public class TEExamActivityDto implements Serializable {
             inProcessFaceVerify = ec.getInProcessFaceVerify();
             inProcessLivenessVerify = ec.getInProcessLivenessVerify();
             inProcessRealnessVerify = ec.getInProcessRealnessVerify();
+            monitorRecord = ec.getMonitorRecord();
         }
         this.id = examActivityCacheBean.getId();
 //        this.code = examActivityCacheBean.getCode();
@@ -225,10 +230,23 @@ public class TEExamActivityDto implements Serializable {
         this.inProcessLivenessVerify = inProcessLivenessVerify;
         this.inProcessRealnessVerify = inProcessRealnessVerify;
         if (Objects.nonNull(monitorVideoSource) && !Objects.equals(monitorVideoSource.trim().replaceAll(" ", ""), "")) {
+            Map<String, String> monitorRecordMap = null;
+            if (Objects.nonNull(monitorRecord) && !Objects.equals(monitorRecord.trim().replaceAll(" ", ""), "")) {
+                List<String> monitorRecordList = Arrays.asList(monitorRecord.trim().toUpperCase().split(","));
+                monitorRecordMap = monitorRecordList.stream()
+                        .collect(Collectors.toMap(Function.identity(), s -> s));
+            }
+
             List<String> videoSources = Arrays.asList(monitorVideoSource.trim().toUpperCase().split(","));
             List<MonitorStreamDto> monitorStreamDtoList = new ArrayList<>();
+            Map<String, String> finalMonitorRecordMap = monitorRecordMap;
             videoSources.forEach(s -> {
-                monitorStreamDtoList.add(new MonitorStreamDto(MonitorVideoSourceEnum.valueOf(s), SystemConstant.setStreamId(prefix, recordId, MonitorVideoSourceEnum.valueOf(s))));
+                if (Objects.isNull(finalMonitorRecordMap) || finalMonitorRecordMap.size() == 0) {
+                    monitorStreamDtoList.add(new MonitorStreamDto(MonitorVideoSourceEnum.valueOf(s), SystemConstant.setStreamId(prefix, recordId, MonitorVideoSourceEnum.valueOf(s)), null));
+                } else {
+                    String monitorRecordStr = finalMonitorRecordMap.get(s);
+                    monitorStreamDtoList.add(new MonitorStreamDto(MonitorVideoSourceEnum.valueOf(s), SystemConstant.setStreamId(prefix, recordId, MonitorVideoSourceEnum.valueOf(s)), Objects.nonNull(monitorRecordStr) ? SystemConstant.setStreamId(prefix, recordId, MonitorVideoSourceEnum.valueOf(s)) : null));
+                }
             });
             this.setMonitorVideoSource(monitorStreamDtoList);
             //加入monitorAudioEnable逻辑
@@ -303,6 +321,7 @@ public class TEExamActivityDto implements Serializable {
         setMaxFinishTime(teExamActivityDto.getMaxFinishTime());
         setMonitorVideoSourceStr(teExamActivityDto.getMonitorVideoSourceStr());
         setMonitorVideoSource(teExamActivityDto.getMonitorVideoSource());
+        setMonitorRecordStr(teExamActivityDto.getMonitorRecordStr());
         setOpeningSeconds(teExamActivityDto.getOpeningSeconds());
         setActivityOpeningSeconds(teExamActivityDto.getActivityOpeningSeconds());
         setPrepareSeconds(teExamActivityDto.getPrepareSeconds());
@@ -331,6 +350,14 @@ public class TEExamActivityDto implements Serializable {
         setPreNoticeStaySeconds(teExamActivityDto.getPreNoticeStaySeconds());
     }
 
+    public String getMonitorRecordStr() {
+        return monitorRecordStr;
+    }
+
+    public void setMonitorRecordStr(String monitorRecordStr) {
+        this.monitorRecordStr = monitorRecordStr;
+    }
+
     public String getPreNotice() {
         return preNotice;
     }

+ 11 - 0
themis-business/src/main/java/com/qmth/themis/business/dto/response/TEExamQueryDto.java

@@ -59,6 +59,9 @@ public class TEExamQueryDto implements Serializable {
     @ApiModelProperty(name = "监控源")
     private String monitorVideoSource;
 
+    @ApiModelProperty(name = "转录源")
+    private String monitorRecord;
+
     @ApiModelProperty(value = "是否IP段限制,0:不允许,1:允许")
     private Integer enableIpLimit;
 
@@ -71,6 +74,14 @@ public class TEExamQueryDto implements Serializable {
     @ApiModelProperty(name = "监考状态,NOT_START:未开始,START:监考中,FINISHED:已结束")
     private String monitorStatus;
 
+    public String getMonitorRecord() {
+        return monitorRecord;
+    }
+
+    public void setMonitorRecord(String monitorRecord) {
+        this.monitorRecord = monitorRecord;
+    }
+
     public String getMonitorStatus() {
         return monitorStatus;
     }

+ 34 - 0
themis-business/src/main/java/com/qmth/themis/business/dto/response/TEStudentExamRecordDto.java

@@ -6,6 +6,7 @@ import com.qmth.themis.business.enums.ExamRecordStatusEnum;
 import io.swagger.annotations.ApiModelProperty;
 
 import java.io.Serializable;
+import java.util.List;
 
 /**
  * @Description: 学生考试记录 dto
@@ -49,6 +50,39 @@ public class TEStudentExamRecordDto implements Serializable {
     @ApiModelProperty(name = "考试状态")
     private ExamRecordStatusEnum status;//考试状态
 
+    @ApiModelProperty(name = "转录视频地址源")
+    private String tencentVideoUrl;
+
+    @ApiModelProperty(name = "转录视频地址")
+    private List<TEStudentMonitorRecordDto> monitorRecord;
+
+    @ApiModelProperty(name = "首次开考时间")
+    private Long firstStartTime;
+
+    public Long getFirstStartTime() {
+        return firstStartTime;
+    }
+
+    public void setFirstStartTime(Long firstStartTime) {
+        this.firstStartTime = firstStartTime;
+    }
+
+    public List<TEStudentMonitorRecordDto> getMonitorRecord() {
+        return monitorRecord;
+    }
+
+    public void setMonitorRecord(List<TEStudentMonitorRecordDto> monitorRecord) {
+        this.monitorRecord = monitorRecord;
+    }
+
+    public String getTencentVideoUrl() {
+        return tencentVideoUrl;
+    }
+
+    public void setTencentVideoUrl(String tencentVideoUrl) {
+        this.tencentVideoUrl = tencentVideoUrl;
+    }
+
     public Long getExamId() {
         return examId;
     }

+ 41 - 0
themis-business/src/main/java/com/qmth/themis/business/dto/response/TEStudentMonitorRecordDto.java

@@ -0,0 +1,41 @@
+package com.qmth.themis.business.dto.response;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.qmth.themis.business.enums.ExamRecordStatusEnum;
+import com.qmth.themis.business.enums.MonitorVideoSourceEnum;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.io.Serializable;
+
+/**
+ * @Description: 学生考试记录 dto
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2020/8/3
+ */
+public class TEStudentMonitorRecordDto implements Serializable {
+
+    @ApiModelProperty(name = "视频源")
+    private String videoSource;
+
+    @ApiModelProperty(name = "视频地址")
+    private String videoUrl;
+
+    public String getVideoSource() {
+        return videoSource;
+    }
+
+    public void setVideoSource(String videoSource) {
+        this.videoSource = videoSource;
+    }
+
+    public String getVideoUrl() {
+        return videoUrl;
+    }
+
+    public void setVideoUrl(String videoUrl) {
+        this.videoUrl = videoUrl;
+    }
+}

+ 19 - 8
themis-business/src/main/java/com/qmth/themis/business/entity/TEExam.java

@@ -1,7 +1,5 @@
 package com.qmth.themis.business.entity;
 
-import java.util.Objects;
-
 import com.baomidou.mybatisplus.annotation.FieldStrategy;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@@ -9,10 +7,11 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.qmth.themis.business.base.BaseEntity;
 import com.qmth.themis.business.dto.request.TEExamDto;
 import com.qmth.themis.business.enums.*;
-
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 
+import java.util.Objects;
+
 /**
  * @Description: 考试批次
  * @Param:
@@ -169,9 +168,13 @@ public class TEExam extends BaseEntity {
     @TableField(value = "monitor_video_source", updateStrategy = FieldStrategy.IGNORED)
     private String monitorVideoSource;
 
-    @ApiModelProperty(value = "是否开始监控转录,off:禁用; mix:混流转录")
+//    @ApiModelProperty(value = "是否开始监控转录,off:禁用; mix:混流转录")
+//    @TableField(value = "monitor_record")
+//    private MonitorRecordEnum monitorRecord;
+
+    @ApiModelProperty(value = "开启转录的视频源")
     @TableField(value = "monitor_record")
-    private MonitorRecordEnum monitorRecord;
+    private String monitorRecord;
 
     @ApiModelProperty(value = "算分进度")
     @TableField(value = "progress")
@@ -251,7 +254,15 @@ public class TEExam extends BaseEntity {
         } else {
             this.monitorVideoSource = null;
         }
-        this.monitorRecord = teExamDto.getMonitorRecord();
+        if (Objects.nonNull(teExamDto.getMonitorRecord()) && !Objects.equals(teExamDto.getMonitorRecord(), "")) {
+            this.monitorRecord = teExamDto.getMonitorRecord().toString().trim().replace("[", "").replace("]", "").replaceAll(" ", "");
+            if (Objects.equals(this.monitorRecord.trim().replaceAll(" ", ""), "")) {
+                this.monitorRecord = null;
+            }
+        } else {
+            this.monitorRecord = null;
+        }
+//        this.monitorRecord = teExamDto.getMonitorRecord();
         this.progress = teExamDto.getProgress();
         setCreateId(teExamDto.getCreateId());
         setCreateTime(teExamDto.getCreateTime());
@@ -317,11 +328,11 @@ public class TEExam extends BaseEntity {
         this.monitorVideoSource = monitorVideoSource;
     }
 
-    public MonitorRecordEnum getMonitorRecord() {
+    public String getMonitorRecord() {
         return monitorRecord;
     }
 
-    public void setMonitorRecord(MonitorRecordEnum monitorRecord) {
+    public void setMonitorRecord(String monitorRecord) {
         this.monitorRecord = monitorRecord;
     }
 

+ 10 - 10
themis-business/src/main/java/com/qmth/themis/business/entity/TEExamStudent.java

@@ -18,12 +18,12 @@ import io.swagger.annotations.ApiModelProperty;
 public class TEExamStudent extends BaseEntity {
 
     /**
-	 * 
-	 */
-	private static final long serialVersionUID = 4720242896522208986L;
+     *
+     */
+    private static final long serialVersionUID = 4720242896522208986L;
 
     @JsonSerialize(using = ToStringSerializer.class)
-	@ApiModelProperty(value = "批次id")
+    @ApiModelProperty(value = "批次id")
     @TableField(value = "exam_id")
     private Long examId;
 
@@ -211,11 +211,11 @@ public class TEExamStudent extends BaseEntity {
         this.selectRecordId = selectRecordId;
     }
 
-	public Integer getAlreadyExamCount() {
-		return alreadyExamCount;
-	}
+    public Integer getAlreadyExamCount() {
+        return alreadyExamCount;
+    }
 
-	public void setAlreadyExamCount(Integer alreadyExamCount) {
-		this.alreadyExamCount = alreadyExamCount;
-	}
+    public void setAlreadyExamCount(Integer alreadyExamCount) {
+        this.alreadyExamCount = alreadyExamCount;
+    }
 }

+ 78 - 0
themis-business/src/main/java/com/qmth/themis/business/entity/TMTencentVideoMessage.java

@@ -0,0 +1,78 @@
+package com.qmth.themis.business.entity;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.qmth.themis.business.util.UidUtil;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * 腾讯云视频回调消息
+ * </p>
+ *
+ * @author wangliang
+ * @since 2022-03-29
+ */
+@ApiModel(value = "TMTencentVideoMessage对象", description = "腾讯云视频回调消息")
+public class TMTencentVideoMessage implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    @ApiModelProperty(value = "主键")
+    private Long id;
+
+    @ApiModelProperty(value = "请求id")
+    private String requestId;
+
+    @ApiModelProperty(value = "请求内容")
+    private String object;
+
+    @ApiModelProperty(value = "创建时间")
+    private Long createTime;
+
+    public TMTencentVideoMessage() {
+
+    }
+
+    public TMTencentVideoMessage(String requestId, String object) {
+        this.id = UidUtil.nextId();
+        this.requestId = requestId;
+        this.object = object;
+        this.createTime = System.currentTimeMillis();
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getRequestId() {
+        return requestId;
+    }
+
+    public void setRequestId(String requestId) {
+        this.requestId = requestId;
+    }
+
+    public String getObject() {
+        return object;
+    }
+
+    public void setObject(String object) {
+        this.object = object;
+    }
+
+    public Long getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Long createTime) {
+        this.createTime = createTime;
+    }
+}

+ 67 - 44
themis-business/src/main/java/com/qmth/themis/business/entity/TOeExamRecord.java

@@ -181,27 +181,27 @@ public class TOeExamRecord implements Serializable {
     @ApiModelProperty(value = "考试开始时间")
     @TableField(value = "start_time")
     private Long startTime;
-    
+
     @ApiModelProperty(value = "考试结束时间")
     @TableField(value = "end_time")
     private Long endTime;
-    
+
     @ApiModelProperty(value = "允许开考开放时长,相当于迟到时间")
     @TableField(value = "opening_seconds")
     private Integer openingSeconds;
-    
+
     @ApiModelProperty(value = "最短考试时长,相当于考试冻结时间")
     @TableField(value = "min_duration_seconds")
     private Integer minDurationSeconds;
-    
+
     @ApiModelProperty(value = "最大考试时长")
     @TableField(value = "max_duration_seconds")
     private Integer maxDurationSeconds;
-    
+
     @ApiModelProperty(value = "是否在结束时间集中强制收卷,0:不强制,1:强制")
     @TableField(value = "force_finish")
     private Integer forceFinish;
-    
+
     @ApiModelProperty(value = "是否有标答,0:无,1:有")
     @TableField(value = "has_answer_file")
     private Integer hasAnswerFile;
@@ -238,6 +238,10 @@ public class TOeExamRecord implements Serializable {
     @TableField(value = "monitor_video_source", updateStrategy = FieldStrategy.IGNORED)
     private String monitorVideoSource;
 
+    @ApiModelProperty(value = "开启转录的视频源")
+    @TableField(value = "monitor_record", updateStrategy = FieldStrategy.IGNORED)
+    private String monitorRecord;
+
     @ApiModelProperty(value = "移动端第一机位websocket状态")
     @TableField(value = "mobile_first_websocket_status")
     private WebsocketStatusEnum mobileFirstWebsocketStatus;
@@ -270,6 +274,25 @@ public class TOeExamRecord implements Serializable {
     @TableField(value = "mobile_second_monitor_status")
     private MonitorStatusSourceEnum mobileSecondMonitorStatus;
 
+    @ApiModelProperty(name = "转录视频地址")
+    private String tencentVideoUrl;
+
+    public String getTencentVideoUrl() {
+        return tencentVideoUrl;
+    }
+
+    public void setTencentVideoUrl(String tencentVideoUrl) {
+        this.tencentVideoUrl = tencentVideoUrl;
+    }
+
+    public String getMonitorRecord() {
+        return monitorRecord;
+    }
+
+    public void setMonitorRecord(String monitorRecord) {
+        this.monitorRecord = monitorRecord;
+    }
+
     public MonitorStatusSourceEnum getCameraMonitorStatus() {
         return cameraMonitorStatus;
     }
@@ -634,45 +657,45 @@ public class TOeExamRecord implements Serializable {
         this.inProcessLivenessVerifyCount = inProcessLivenessVerifyCount;
     }
 
-	public Integer getAlreadyBreakCount() {
-		return alreadyBreakCount;
-	}
+    public Integer getAlreadyBreakCount() {
+        return alreadyBreakCount;
+    }
 
-	public void setAlreadyBreakCount(Integer alreadyBreakCount) {
-		this.alreadyBreakCount = alreadyBreakCount;
-	}
+    public void setAlreadyBreakCount(Integer alreadyBreakCount) {
+        this.alreadyBreakCount = alreadyBreakCount;
+    }
 
-	public Integer getOpeningSeconds() {
-		return openingSeconds;
-	}
+    public Integer getOpeningSeconds() {
+        return openingSeconds;
+    }
 
-	public void setOpeningSeconds(Integer openingSeconds) {
-		this.openingSeconds = openingSeconds;
-	}
+    public void setOpeningSeconds(Integer openingSeconds) {
+        this.openingSeconds = openingSeconds;
+    }
 
-	public Integer getMinDurationSeconds() {
-		return minDurationSeconds;
-	}
+    public Integer getMinDurationSeconds() {
+        return minDurationSeconds;
+    }
 
-	public void setMinDurationSeconds(Integer minDurationSeconds) {
-		this.minDurationSeconds = minDurationSeconds;
-	}
+    public void setMinDurationSeconds(Integer minDurationSeconds) {
+        this.minDurationSeconds = minDurationSeconds;
+    }
 
-	public Integer getMaxDurationSeconds() {
-		return maxDurationSeconds;
-	}
+    public Integer getMaxDurationSeconds() {
+        return maxDurationSeconds;
+    }
 
-	public void setMaxDurationSeconds(Integer maxDurationSeconds) {
-		this.maxDurationSeconds = maxDurationSeconds;
-	}
+    public void setMaxDurationSeconds(Integer maxDurationSeconds) {
+        this.maxDurationSeconds = maxDurationSeconds;
+    }
 
-	public Integer getForceFinish() {
-		return forceFinish;
-	}
+    public Integer getForceFinish() {
+        return forceFinish;
+    }
 
-	public void setForceFinish(Integer forceFinish) {
-		this.forceFinish = forceFinish;
-	}
+    public void setForceFinish(Integer forceFinish) {
+        this.forceFinish = forceFinish;
+    }
 
     public Long getFirstPrepareTime() {
         return firstPrepareTime;
@@ -746,13 +769,13 @@ public class TOeExamRecord implements Serializable {
         this.endTime = endTime;
     }
 
-	public Integer getHasAnswerFile() {
-		return hasAnswerFile;
-	}
+    public Integer getHasAnswerFile() {
+        return hasAnswerFile;
+    }
+
+    public void setHasAnswerFile(Integer hasAnswerFile) {
+        this.hasAnswerFile = hasAnswerFile;
+    }
+
 
-	public void setHasAnswerFile(Integer hasAnswerFile) {
-		this.hasAnswerFile = hasAnswerFile;
-	}
-    
-    
 }

+ 5 - 1
themis-business/src/main/java/com/qmth/themis/business/enums/ExamRecordFieldEnum.java

@@ -129,7 +129,11 @@ public enum ExamRecordFieldEnum {
 
     mobile_first_monitor_status("mobileFirstMonitorStatus"),
 
-    mobile_second_monitor_status("mobileSecondMonitorStatus");
+    mobile_second_monitor_status("mobileSecondMonitorStatus"),
+
+    monitor_record("monitorRecord"),
+
+    tencent_video_url("tencentVideoUrl");
 
     private String code;
 

+ 27 - 25
themis-business/src/main/java/com/qmth/themis/business/enums/MonitorRecordEnum.java

@@ -1,25 +1,27 @@
-package com.qmth.themis.business.enums;
-
-/**
- * @Description: 监控转录 enum
- * @Param:
- * @return:
- * @Author: wangliang
- * @Date: 2020/11/30
- */
-public enum MonitorRecordEnum {
-
-    OFF("禁用"),
-
-    MIX("混流转录");
-
-    private String code;
-
-    private MonitorRecordEnum(String code) {
-        this.code = code;
-    }
-
-    public String getCode() {
-        return code;
-    }
-}
+//package com.qmth.themis.business.enums;
+//
+///**
+// * @Description: 监控转录 enum
+// * @Param:
+// * @return:
+// * @Author: wangliang
+// * @Date: 2020/11/30
+// */
+//public enum MonitorRecordEnum {
+//
+//    OFF("禁用"),
+//
+//    LIVE("直播"),
+//
+//    VIDEO("直播+回放");
+//
+//    private String code;
+//
+//    private MonitorRecordEnum(String code) {
+//        this.code = code;
+//    }
+//
+//    public String getCode() {
+//        return code;
+//    }
+//}

+ 12 - 4
themis-business/src/main/java/com/qmth/themis/business/enums/MonitorVideoSourceEnum.java

@@ -2,6 +2,8 @@ package com.qmth.themis.business.enums;
 
 import com.qmth.themis.common.enums.Source;
 
+import java.util.Objects;
+
 /**
  * @Description: 监控源 enum
  * @Param:
@@ -28,10 +30,16 @@ public enum MonitorVideoSourceEnum {
         return code;
     }
 
-    public static MonitorVideoSourceEnum findByName(String name) {
-        for (MonitorVideoSourceEnum value : MonitorVideoSourceEnum.values()) {
-            if (value.toString().equalsIgnoreCase(name)) {
-                return value;
+    /**
+     * 状态转换 toName
+     *
+     * @param title
+     * @return
+     */
+    public static MonitorVideoSourceEnum convertToName(String title) {
+        for (MonitorVideoSourceEnum e : MonitorVideoSourceEnum.values()) {
+            if (Objects.equals(title, e.name())) {
+                return e;
             }
         }
         return null;

+ 3 - 1
themis-business/src/main/java/com/qmth/themis/business/enums/MqExecTypeEnum.java

@@ -39,7 +39,9 @@ public enum MqExecTypeEnum {
 
     EXEC_MQ_WEBSOCKET_UN_NORMAL_LOGIC("websocket非正常退出,延时消息逻辑", "execMqWebsocketUnNormalLogic"),
 
-    EXEC_MQ_EXAM_BREAK_DELAY_LOGIC("考试断点,延时消息逻辑", "execMqExamBreakDelayLogic");
+    EXEC_MQ_EXAM_BREAK_DELAY_LOGIC("考试断点,延时消息逻辑", "execMqExamBreakDelayLogic"),
+
+    EXEC_MQ_TENCENT_VIDEO_LOGIC("腾讯云视频回调,延时消息逻辑", "execMqTencentVideoLogic");
 
     private String code;
     private String desc;

+ 6 - 1
themis-business/src/main/java/com/qmth/themis/business/enums/MqGroupEnum.java

@@ -95,5 +95,10 @@ public enum MqGroupEnum {
     /**
      * 考试断点延时消息
      */
-    EXAM_BREAK_DELAY_GROUP;
+    EXAM_BREAK_DELAY_GROUP,
+
+    /**
+     * tencent video 回调 group
+     */
+    TENCENT_VIDEO_GROUP;
 }

+ 2 - 1
themis-business/src/main/java/com/qmth/themis/business/enums/MqTagEnum.java

@@ -53,7 +53,8 @@ public enum MqTagEnum {
     MONITOR_START("监控开始标签", "监控开始","broadcast", 45),
     MONITOR_STOP("监控结束标签", "监控结束","broadcast", 46),
     EXAM_START("考试移动端监控开始标签", "考试移动端开始", "broadcast", 47),
-    OE_WEBSOCKET_MOBILE_MONITOR_STATUS("通知客户端移动端当前监控状态标签", "通知客户端移动端当前监控状态", "broadcast", 48);
+    OE_WEBSOCKET_MOBILE_MONITOR_STATUS("通知客户端移动端当前监控状态标签", "通知客户端移动端当前监控状态", "broadcast", 48),
+    TENCENT_VIDEO("腾讯云视频回调标签", "腾讯云视频回调", "delay", 49);
 
     private MqTagEnum(String desc, String code, String type, int id) {
         this.desc = desc;

+ 16 - 0
themis-business/src/main/java/com/qmth/themis/business/service/TMTencentVideoMessageService.java

@@ -0,0 +1,16 @@
+package com.qmth.themis.business.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.qmth.themis.business.entity.TMTencentVideoMessage;
+
+/**
+ * <p>
+ * 腾讯云视频回调消息 服务类
+ * </p>
+ *
+ * @author wangliang
+ * @since 2022-03-29
+ */
+public interface TMTencentVideoMessageService extends IService<TMTencentVideoMessage> {
+
+}

+ 5 - 3
themis-business/src/main/java/com/qmth/themis/business/service/impl/CacheServiceImpl.java

@@ -87,9 +87,11 @@ public class CacheServiceImpl implements CacheService {
     @Override
     @Cacheable(value = SystemConstant.orgCodeCache, key = "#p0")
     public TBOrg addOrgCodeCache(String code) {
-        QueryWrapper<TBOrg> tbOrgQueryWrapper = new QueryWrapper<>();
-        tbOrgQueryWrapper.lambda().eq(TBOrg::getCode, code);
-        return tbOrgService.getOne(tbOrgQueryWrapper);
+        TBOrg tbOrg = tbOrgService.getOne(new QueryWrapper<TBOrg>().lambda().eq(TBOrg::getCode, code));
+        if (Objects.isNull(tbOrg)) {
+            throw new BusinessException(ExceptionResultEnum.ORG_NO);
+        }
+        return tbOrg;
     }
 
     /**

+ 3 - 1
themis-business/src/main/java/com/qmth/themis/business/service/impl/MqDtoServiceImpl.java

@@ -201,8 +201,10 @@ public class MqDtoServiceImpl implements MqDtoService {
         LocalDateTime dt = LocalDateTime.now();
         if (level.contains("m")) {
             dt = dt.plusMinutes(Long.parseLong(level.replace("m", "")));
-        } else {
+        } else if (level.contains("s")) {
             dt = dt.plusSeconds(Long.parseLong(level.replace("s", "")));
+        } else if (level.contains("h")) {
+            dt = dt.plusHours(Long.parseLong(level.replace("h", "")));
         }
         tranMap.put("timeOut", time);
         tranMap.put("mqExecTime", dt.toInstant(ZoneOffset.of("+8")).toEpochMilli());

+ 1 - 1
themis-business/src/main/java/com/qmth/themis/business/service/impl/TBAttachmentServiceImpl.java

@@ -113,7 +113,7 @@ public class TBAttachmentServiceImpl extends ServiceImpl<TBAttachmentMapper, TBA
                 stringJoiner.add(uploadType).add(File.separator).add(String.valueOf(orgId));
             } else if (Objects.equals(uploadType, UploadFileEnum.client.name())) {
                 stringJoiner.add(uploadType).add(map.get(SystemConstant.VERSION).toString()).add(File.separator);
-            } else if (Objects.equals(uploadType, UploadFileEnum.file.name())) {
+            } else if (Objects.equals(uploadType, UploadFileEnum.file.name()) || Objects.equals(uploadType, UploadFileEnum.upload.name())) {
                 stringJoiner.add(uploadType).add(File.separator);
             }
             stringJoiner.add(String.valueOf(nowTime.getYear())).add(File.separator)

+ 16 - 1
themis-business/src/main/java/com/qmth/themis/business/service/impl/TEExamActivityServiceImpl.java

@@ -33,6 +33,8 @@ import javax.annotation.Resource;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * @Description: 考试场次 服务实现类
@@ -187,10 +189,23 @@ public class TEExamActivityServiceImpl extends ServiceImpl<TEExamActivityMapper,
         }
         if (Objects.nonNull(teExamActivityDto.getMonitorVideoSourceStr()) && !Objects
                 .equals(teExamActivityDto.getMonitorVideoSourceStr().toString().trim().replaceAll(" ", ""), "")) {
+            Map<String, String> monitorRecordMap = null;
+            if (Objects.nonNull(teExamActivityDto.getMonitorRecordStr()) && !Objects.equals(teExamActivityDto.getMonitorRecordStr().trim().replaceAll(" ", ""), "")) {
+                List<String> monitorRecordList = Arrays.asList(teExamActivityDto.getMonitorRecordStr().trim().toUpperCase().split(","));
+                monitorRecordMap = monitorRecordList.stream()
+                        .collect(Collectors.toMap(Function.identity(), s -> s));
+            }
+
             List<String> videoSources = Arrays.asList(teExamActivityDto.getMonitorVideoSourceStr().trim().toUpperCase().replaceAll(" ", "").split(","));
             List<MonitorStreamDto> monitorStreamDtoList = new ArrayList<>();
+            Map<String, String> finalMonitorRecordMap = monitorRecordMap;
             videoSources.forEach(s -> {
-                monitorStreamDtoList.add(new MonitorStreamDto(MonitorVideoSourceEnum.valueOf(s), SystemConstant.setStreamId(prefix, recordId, MonitorVideoSourceEnum.valueOf(s))));
+                if (Objects.isNull(finalMonitorRecordMap) || finalMonitorRecordMap.size() == 0) {
+                    monitorStreamDtoList.add(new MonitorStreamDto(MonitorVideoSourceEnum.valueOf(s), SystemConstant.setStreamId(prefix, recordId, MonitorVideoSourceEnum.valueOf(s)), null));
+                } else {
+                    String monitorRecordStr = finalMonitorRecordMap.get(s);
+                    monitorStreamDtoList.add(new MonitorStreamDto(MonitorVideoSourceEnum.valueOf(s), SystemConstant.setStreamId(prefix, recordId, MonitorVideoSourceEnum.valueOf(s)), Objects.nonNull(monitorRecordStr) ? SystemConstant.setStreamId(prefix, recordId, MonitorVideoSourceEnum.valueOf(s)) : null));
+                }
             });
             teExamActivityDto.setMonitorVideoSource(monitorStreamDtoList);
             teExamActivityDto.setMonitorAudioEnable(examCache.getMonitorAudioEnable());

+ 12 - 0
themis-business/src/main/java/com/qmth/themis/business/service/impl/TEExamServiceImpl.java

@@ -1057,6 +1057,7 @@ public class TEExamServiceImpl extends ServiceImpl<TEExamMapper, TEExam> impleme
 
     private ExamFinishBean diposeFinish(Long studentId, Long recordId, String type, Integer durationSeconds) {
         Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
+        String monitorRecord = ExamRecordCacheUtil.getMonitorRecord(recordId);
         // 校验当前登录用户和参数一致性
         if (examStudentId == null) {
             throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_RECORD);
@@ -1186,6 +1187,17 @@ public class TEExamServiceImpl extends ServiceImpl<TEExamMapper, TEExam> impleme
                 teStudentCacheDto.getIdentity());
         this.sendOeLogMessage(SystemOperationEnum.FINISHED, examStudentId, recordId, mqDto);
         //mq发送消息end
+
+        if (Objects.nonNull(monitorRecord) && !Objects.equals(monitorRecord.trim().replaceAll(" ", ""), "")) {
+            //发送腾讯云回调延时mq消息start
+//            Map<String, Object> tranMap = mqDtoService.buildMqDelayMsg("1h");
+            //TODO 测试用
+            Map<String, Object> tranMap = mqDtoService.buildMqDelayMsg("6m");
+            tranMap.put("recordId", recordId);
+            MqDto mqDtoTencentVideo = new MqDto(mqUtil.getMqGroupDomain().getTopic(), MqTagEnum.TENCENT_VIDEO.name(), MqTagEnum.TENCENT_VIDEO.name(), MqTagEnum.TENCENT_VIDEO, String.valueOf(recordId), tranMap, String.valueOf(recordId));
+            mqDtoService.assembleSendAsyncDelayMsg(mqDtoTencentVideo);
+            //发送腾讯云回调延时mq消息end
+        }
         return ret;
     }
 

+ 14 - 2
themis-business/src/main/java/com/qmth/themis/business/service/impl/TEMobileServiceImpl.java

@@ -16,6 +16,7 @@ import com.qmth.themis.business.constant.SystemConstant;
 import com.qmth.themis.business.dto.AuthDto;
 import com.qmth.themis.business.dto.ExpireTimeDTO;
 import com.qmth.themis.business.dto.MqDto;
+import com.qmth.themis.business.dto.response.MonitorStreamDto;
 import com.qmth.themis.business.entity.TBAppVersion;
 import com.qmth.themis.business.entity.TBSession;
 import com.qmth.themis.business.enums.*;
@@ -29,8 +30,9 @@ import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
 import java.security.NoSuchAlgorithmException;
-import java.util.Objects;
-import java.util.UUID;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 @Service
 public class TEMobileServiceImpl implements TEMobileService {
@@ -126,6 +128,7 @@ public class TEMobileServiceImpl implements TEMobileService {
             throws NoSuchAlgorithmException {
         Long recordId = MobileAuthCacheUtil.getRecordId(mode, code);
         MonitorVideoSourceEnum monitorVideoSource = MobileAuthCacheUtil.getMonitorVideoSource(mode, code);
+        String monitorRecord = ExamRecordCacheUtil.getMonitorRecord(recordId);
         if (MobileAuthCacheUtil.getMode(mode, code) == null) {
             throw new BusinessException(ExceptionResultEnum.QR_CODE_EXPIRE);
         }
@@ -135,6 +138,15 @@ public class TEMobileServiceImpl implements TEMobileService {
         ret.setMonitorVideoSource(monitorVideoSource);
         ret.setMonitorAudioEnable(getMonitorAudioEnable(recordId, monitorVideoSource));
         ret.setMonitorStreamId(SystemConstant.setStreamId(monitorUtil.getMonitorDomain().getPrefix(), recordId, monitorVideoSource));
+        Map<String, String> monitorRecordMap = null;
+        if (Objects.nonNull(monitorRecord) && !Objects.equals(monitorRecord.trim().replaceAll(" ", ""), "")) {
+            List<String> monitorRecordList = Arrays.asList(monitorRecord.trim().toUpperCase().split(","));
+            monitorRecordMap = monitorRecordList.stream()
+                    .collect(Collectors.toMap(Function.identity(), s -> s));
+            String monitorRecordStr = monitorRecordMap.get(monitorVideoSource.name());
+            ret.setMonitorRecordId(Objects.nonNull(monitorRecordStr) ? ret.getMonitorStreamId() : null);
+        }
+
         Source sourceEnum = monitorVideoSource.getSessionSource();
         ExamStudentCacheBean es = examStudentService
                 .getExamStudentCacheBean(ExamRecordCacheUtil.getExamStudentId(ret.getRecordId()));

+ 13 - 1
themis-business/src/main/java/com/qmth/themis/business/service/impl/TEStudentServiceImpl.java

@@ -4,13 +4,17 @@ import com.aliyun.oss.common.utils.BinaryUtil;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.google.common.reflect.TypeToken;
+import com.google.gson.Gson;
 import com.qmth.themis.business.bean.admin.StudentPhotoUploadResponseBean;
 import com.qmth.themis.business.dao.TEStudentMapper;
 import com.qmth.themis.business.dto.response.TEStudentDto;
 import com.qmth.themis.business.dto.response.TEStudentExamRecordDto;
+import com.qmth.themis.business.dto.response.TEStudentMonitorRecordDto;
 import com.qmth.themis.business.entity.TEStudent;
 import com.qmth.themis.business.service.CacheService;
 import com.qmth.themis.business.service.TEStudentService;
+import com.qmth.themis.business.util.JacksonUtil;
 import com.qmth.themis.business.util.OssUtil;
 import com.qmth.themis.common.exception.BusinessException;
 import com.qmth.themis.common.util.HexUtils;
@@ -22,6 +26,7 @@ import javax.annotation.Resource;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -69,7 +74,14 @@ public class TEStudentServiceImpl extends ServiceImpl<TEStudentMapper, TEStudent
      */
     @Override
     public IPage<TEStudentExamRecordDto> studentExamRecordQuery(IPage<Map> iPage, Long studentId, Long examId) {
-        return teStudentMapper.studentExamRecordQuery(iPage, studentId, examId);
+        IPage<TEStudentExamRecordDto> teStudentExamRecordDtoIPage = teStudentMapper.studentExamRecordQuery(iPage, studentId, examId);
+        Gson gson = new Gson();
+        for (TEStudentExamRecordDto t : teStudentExamRecordDtoIPage.getRecords()) {
+            List<TEStudentMonitorRecordDto> monitorRecordList = gson.fromJson(t.getTencentVideoUrl(), new TypeToken<List<TEStudentMonitorRecordDto>>() {
+            }.getType());
+            t.setMonitorRecord(monitorRecordList);
+        }
+        return teStudentExamRecordDtoIPage;
     }
 
     @Transactional

+ 1 - 1
themis-business/src/main/java/com/qmth/themis/business/service/impl/TIeReportServiceImpl.java

@@ -406,7 +406,7 @@ public class TIeReportServiceImpl implements TIeReportService {
         teExamQueryWrapper.lambda().eq(TEExam::getOrgId, orgId);
         List<TEExam> teExamList = examService.list(teExamQueryWrapper);
         List<TEExamActivity> teExamActivityList = null;
-        if (Objects.isNull(teExamList)) {
+        if (Objects.isNull(teExamList) || teExamList.size() == 0) {
             return ret;
         } else {
             Set<Long> examIdSet = teExamList.stream().map(TEExam::getId).collect(Collectors.toSet());

+ 20 - 0
themis-business/src/main/java/com/qmth/themis/business/service/impl/TMTencentVideoMessageServiceImpl.java

@@ -0,0 +1,20 @@
+package com.qmth.themis.business.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.qmth.themis.business.dao.TMTencentVideoMessageMapper;
+import com.qmth.themis.business.entity.TMTencentVideoMessage;
+import com.qmth.themis.business.service.TMTencentVideoMessageService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 腾讯云视频回调消息 服务实现类
+ * </p>
+ *
+ * @author wangliang
+ * @since 2022-03-29
+ */
+@Service
+public class TMTencentVideoMessageServiceImpl extends ServiceImpl<TMTencentVideoMessageMapper, TMTencentVideoMessage> implements TMTencentVideoMessageService {
+
+}

+ 5 - 1
themis-business/src/main/java/com/qmth/themis/business/service/impl/TOeExamRecordServiceImpl.java

@@ -162,7 +162,8 @@ public class TOeExamRecordServiceImpl extends ServiceImpl<TOeExamRecordMapper, T
         er.setFirstPrepareTime(System.currentTimeMillis());
         er.setStatus(ExamRecordStatusEnum.FIRST_PREPARE);
         er.setObjectiveScore(0.0);
-        er.setMonitorKey(String.valueOf(redisUtil.getRedisSequence()));
+//        er.setMonitorKey(String.valueOf(redisUtil.getRedisSequence()));
+        er.setMonitorKey(String.valueOf(er.getId()));
         er.setAlreadyBreakCount(0);
         er.setStartTime(ac.getStartTime());
         er.setEndTime(ac.getFinishTime());
@@ -180,6 +181,7 @@ public class TOeExamRecordServiceImpl extends ServiceImpl<TOeExamRecordMapper, T
         er.setInProcessLivenessFixedRange(exam.getInProcessLivenessFixedRange());
         er.setInProcessLivenessJudgePolicy(exam.getInProcessLivenessJudgePolicy());
         er.setMonitorVideoSource(exam.getMonitorVideoSource());
+        er.setMonitorRecord(exam.getMonitorRecord());
         ExamPaperCacheBean ep = examPaperService.getExamPaperCacheBean(paperId);
         if (StringUtils.isBlank(ep.getAnswerPath())) {
             er.setHasAnswerFile(0);
@@ -551,6 +553,7 @@ public class TOeExamRecordServiceImpl extends ServiceImpl<TOeExamRecordMapper, T
             String inProcessLivenessFixedRange = ExamRecordCacheUtil.getInProcessLivenessFixedRange(recordId);
             String inProcessLivenessJudgePolicy = ExamRecordCacheUtil.getInProcessLivenessJudgePolicy(recordId);
             String monitorVideoSource = ExamRecordCacheUtil.getMonitorVideoSource(recordId);
+            String monitorRecord = ExamRecordCacheUtil.getMonitorRecord(recordId);
             Integer inProcessRealnessVerifyStatus = ExamRecordCacheUtil.getInProcessRealnessVerifyStatus(recordId);
             Integer inProcessLivenessVerifyStatus = ExamRecordCacheUtil.getInProcessLivenessVerifyStatus(recordId);
             Integer inProcessFaceVerifyStatus = ExamRecordCacheUtil.getInProcessFaceVerifyStatus(recordId);
@@ -632,6 +635,7 @@ public class TOeExamRecordServiceImpl extends ServiceImpl<TOeExamRecordMapper, T
             er.setInProcessLivenessFixedRange(inProcessLivenessFixedRange);
             er.setInProcessLivenessJudgePolicy(InProcessLivenessJudgePolicyEnum.valueOf(inProcessLivenessJudgePolicy));
             er.setMonitorVideoSource(monitorVideoSource);
+            er.setMonitorRecord(monitorRecord);
             er.setCameraMonitorStatus(cameraMonitorStatus);
             er.setScreenMonitorStatus(screenMonitorStatus);
             er.setMobileFirstMonitorStatus(mobileFirstMonitorStatus);

+ 225 - 0
themis-business/src/main/java/com/qmth/themis/business/util/HttpUtil.java

@@ -0,0 +1,225 @@
+package com.qmth.themis.business.util;
+
+import com.qmth.themis.business.constant.SystemConstant;
+import com.qmth.themis.common.exception.BusinessException;
+import org.apache.commons.io.FileUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.protocol.HTTP;
+import org.apache.http.util.EntityUtils;
+import org.apache.tomcat.util.http.fileupload.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+/**
+ * @Description: http util
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2020/12/11
+ */
+public class HttpUtil {
+    private final static Logger log = LoggerFactory.getLogger(HttpUtil.class);
+
+    /**
+     * ssl post
+     *
+     * @param url
+     * @param json
+     * @param treeMap
+     * @return
+     */
+    public static String sslPost(String url, String json, TreeMap<String, String> treeMap) {
+        HttpClient httpClient = null;
+        HttpPost httpPost = null;
+        String result = null;
+        try {
+            httpClient = new SSLClient();
+            httpPost = new HttpPost(url);
+            for (Map.Entry<String, String> entry : treeMap.entrySet()) {
+                httpPost.addHeader(entry.getKey(), entry.getValue());
+            }
+//            StringEntity se = new StringEntity(json);
+//            se.setContentType("text/json");
+//            se.setContentEncoding(new BasicHeader("Content-Type", "application/json"));
+//            httpPost.setEntity(se);
+            HttpResponse response = httpClient.execute(httpPost);
+            if (response != null) {
+                HttpEntity resEntity = response.getEntity();
+                if (resEntity != null) {
+                    result = EntityUtils.toString(resEntity, SystemConstant.CHARSET_NAME);
+                }
+            }
+        } catch (Exception e) {
+            log.error(SystemConstant.LOG_ERROR, e);
+        }
+        return result;
+    }
+
+    /**
+     * post json
+     *
+     * @param url
+     * @param json
+     * @param secret
+     * @param timestamp
+     * @return
+     * @throws IOException
+     */
+    public static String postJson(String url, String json, String secret, Long timestamp) throws IOException {
+        // 构建post请求
+        HttpPost post = new HttpPost(url);
+        post.setHeader("authorization-token", secret);
+        post.setHeader(SystemConstant.HEADER_TIME, String.valueOf(timestamp));
+        post.setHeader(HTTP.CONTENT_TYPE, "application/json; charset=utf-8");
+        post.setHeader("Accept", "application/json");
+
+        String encoderJson = URLEncoder.encode(json, SystemConstant.CHARSET_NAME);
+        StringEntity se = new StringEntity(encoderJson);
+        se.setContentType("text/json");
+        se.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, "application/json"));
+        post.setEntity(se);
+        // 执行请求,获取响应
+        return getRespString(post);
+    }
+
+    /**
+     * post 请求
+     *
+     * @param url
+     * @param params
+     * @param secret
+     * @param timestamp
+     * @return
+     */
+    public static String post(String url, Map<String, Object> params, String secret, Long timestamp) throws IOException {
+        // 构建post请求
+        HttpPost post = new HttpPost(url);
+        post.setHeader(SystemConstant.HEADER_AUTHORIZATION, secret);
+        post.setHeader(SystemConstant.HEADER_TIME, String.valueOf(timestamp));
+        post.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
+        // 构建请求参数
+        List<BasicNameValuePair> pairs = new ArrayList<BasicNameValuePair>();
+        if (params != null) {
+            for (String key : params.keySet()) {
+                pairs.add(new BasicNameValuePair(key, String.valueOf(params.get(key))));
+            }
+        }
+        HttpEntity entity = null;
+        try {
+            entity = new UrlEncodedFormEntity(pairs, SystemConstant.CHARSET_NAME);
+        } catch (UnsupportedEncodingException e) {
+            log.error(SystemConstant.LOG_ERROR, e);
+        }
+        post.setEntity(entity);
+        // 执行请求,获取响应
+        return getRespString(post);
+    }
+
+    /**
+     * 获取响应信息
+     *
+     * @param request
+     * @return
+     */
+    public static String getRespString(HttpUriRequest request) throws IOException {
+        // 获取响应流
+        InputStream in = null;
+        ByteArrayOutputStream ou = null;
+        try {
+            in = getRespInputStream(request);
+            ou = new ByteArrayOutputStream();
+            IOUtils.copy(in, ou);
+        } catch (Exception e) {
+            log.error(SystemConstant.LOG_ERROR, e);
+        } finally {
+            if (Objects.nonNull(in)) {
+                in.close();
+            }
+            if (Objects.nonNull(ou)) {
+                ou.close();
+            }
+        }
+        return Objects.nonNull(ou) ? new String(ou.toByteArray(), StandardCharsets.UTF_8) : null;
+    }
+
+    /**
+     * 获取输入流
+     *
+     * @param request
+     * @return
+     */
+    public static InputStream getRespInputStream(HttpUriRequest request) {
+        // 获取响应对象
+        HttpResponse response = null;
+        try {
+            response = HttpClients.createDefault().execute(request);
+        } catch (Exception e) {
+            log.error(SystemConstant.LOG_ERROR, e);
+        }
+        if (response == null) {
+            return null;
+        }
+        // 获取Entity对象
+        HttpEntity entity = response.getEntity();
+        // 获取响应信息流
+        InputStream in = null;
+        if (entity != null) {
+            try {
+                in = entity.getContent();
+            } catch (Exception e) {
+                log.error(SystemConstant.LOG_ERROR, e);
+            }
+        }
+        return in;
+    }
+
+    /**
+     * 根据url下载文件,保存到filePath中
+     *
+     * @param url
+     * @param filePath
+     * @return
+     */
+    public static File httpDownload(String url, String filePath) throws IOException {
+        InputStream is = null;
+        File file = null;
+        try {
+            HttpClient client = HttpClients.createDefault();
+            HttpGet httpget = new HttpGet(url);
+            HttpResponse response = client.execute(httpget);
+
+            HttpEntity entity = response.getEntity();
+            is = entity.getContent();
+
+            Optional.ofNullable(is).orElseThrow(() -> new BusinessException("所在路径不存在"));
+            file = new File(filePath);
+            if (!file.exists()) {
+                file.getParentFile().mkdirs();
+                file.createNewFile();
+            }
+            FileUtils.copyInputStreamToFile(is, file);
+        } catch (Exception e) {
+            log.error(SystemConstant.LOG_ERROR, e);
+        } finally {
+            if (Objects.nonNull(is)) {
+                is.close();
+            }
+        }
+        return file;
+    }
+}

+ 2 - 2
themis-business/src/main/java/com/qmth/themis/business/util/OssUtil.java

@@ -54,7 +54,7 @@ public class OssUtil {
         aliYunOssPublicDomain = new AliYunOssPublicDomain(aliYunOssDomain.getPublicEndpoint(),
                 aliYunOssDomain.getPublicName(), aliYunOssDomain.getPublicAccessKeyId(),
                 aliYunOssDomain.getPublicAccessKeySecret(), aliYunOssDomain.getPublicBucket(),
-                aliYunOssDomain.getPublicUrl(), sysDomain.isOss(), sysDomain.getAttachmentType());
+                aliYunOssDomain.getPublicUrl(), sysDomain.isOss(), sysDomain.getFileHost(), sysDomain.getAttachmentType());
         publicClient = new OSSClientBuilder()
                 .build(aliYunOssDomain.getPublicEndpoint(), aliYunOssDomain.getPublicAccessKeyId(),
                         aliYunOssDomain.getPublicAccessKeySecret());
@@ -66,7 +66,7 @@ public class OssUtil {
         aliYunOssPrivateDomain = new AliYunOssPrivateDomain(aliYunOssDomain.getPrivateEndpoint(),
                 aliYunOssDomain.getPrivateName(), aliYunOssDomain.getPrivateAccessKeyId(),
                 aliYunOssDomain.getPrivateAccessKeySecret(), aliYunOssDomain.getPrivateBucket(),
-                aliYunOssDomain.getPrivateUrl(), sysDomain.isOss(), sysDomain.getAttachmentType());
+                aliYunOssDomain.getPrivateUrl(), sysDomain.isOss(), sysDomain.getFileHost(), sysDomain.getAttachmentType());
         privateClient = new OSSClientBuilder()
                 .build(aliYunOssDomain.getPrivateEndpoint(), aliYunOssDomain.getPrivateAccessKeyId(),
                         aliYunOssDomain.getPrivateAccessKeySecret());

+ 85 - 0
themis-business/src/main/java/com/qmth/themis/business/util/QrCodeUtil.java

@@ -0,0 +1,85 @@
+//package com.qmth.themis.business.util;
+//
+//import com.alibaba.fastjson.JSONObject;
+//import com.aliyun.oss.common.utils.BinaryUtil;
+//import com.google.zxing.BarcodeFormat;
+//import com.google.zxing.EncodeHintType;
+//import com.google.zxing.MultiFormatWriter;
+//import com.google.zxing.WriterException;
+//import com.google.zxing.common.BitMatrix;
+//import com.qmth.themis.business.constant.SystemConstant;
+//
+//import javax.imageio.ImageIO;
+//import java.awt.image.BufferedImage;
+//import java.io.*;
+//import java.time.LocalDateTime;
+//import java.util.*;
+//import java.io.IOException;
+//import java.io.OutputStream;
+//
+//import com.google.zxing.BarcodeFormat;
+//import com.google.zxing.EncodeHintType;
+//import com.google.zxing.MultiFormatWriter;
+//import com.google.zxing.WriterException;
+//import com.google.zxing.client.j2se.MatrixToImageWriter;
+//import com.google.zxing.common.BitMatrix;
+//import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+//import com.qmth.themis.business.domain.AliYunOssPublicDomain;
+//import com.qmth.themis.business.enums.UploadFileEnum;
+//import com.qmth.themis.common.util.HexUtils;
+//import org.apache.commons.codec.digest.DigestUtils;
+//import org.apache.commons.io.FileUtils;
+//
+///**
+// * @Description: Qrcode
+// * @Param:
+// * @return:
+// * @Author: wangliang
+// * @Date: 2022/3/30
+// */
+//public class QrCodeUtil {
+//
+//    /**
+//     * 生成一个二维码图片
+//     *
+//     * @param width
+//     * @param height
+//     * @param content
+//     * @return
+//     * @throws WriterException
+//     * @throws IOException
+//     */
+//    public static byte[] createQRCode(int width, int height, String content) throws WriterException, IOException {
+//        // 二维码基本参数设置
+//        Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>();
+//        hints.put(EncodeHintType.CHARACTER_SET, SystemConstant.CHARSET_NAME);// 设置编码字符集utf-8
+//        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);// 设置纠错等级L/M/Q/H,纠错等级越高越不易识别,当前设置等级为最高等级H
+//        hints.put(EncodeHintType.MARGIN, 0);// 可设置范围为0-10,但仅四个变化0 1(2) 3(4 5 6) 7(8 9 10)
+//        //生成图片类型为QRCode
+//        BarcodeFormat format = BarcodeFormat.QR_CODE;
+//        // 创建位矩阵对象
+//        BitMatrix bitMatrix = new MultiFormatWriter().encode(content, format, width, height, hints);
+//        // 设置位矩阵转图片的参数
+//        //MatrixToImageConfig config = new MatrixToImageConfig(Color.black.getRGB(), Color.white.getRGB());
+//        //位矩阵对象转流对象
+//        ByteArrayOutputStream os = new ByteArrayOutputStream();
+//        MatrixToImageWriter.writeToStream(bitMatrix, "png", os);
+//        return os.toByteArray();
+//    }
+//
+//    public static void main(String[] args) throws WriterException, IOException {
+//        byte[] b = createQRCode(300, 300, "遇见最好的自己!123");
+//        InputStream inputStream = new ByteArrayInputStream(b);
+//        String md5 = DigestUtils.md5Hex(inputStream);
+//        System.out.println("md5:" + md5);
+////        String filePath = "upload" + File.separator + sdf.format(new Date()) + File.separator + uuid() + "." + suffix;
+////        InputStream in = null;
+////        try {
+////            ossUtil.upload(true, filePath, inputStream, BinaryUtil.toBase64String(HexUtils.decodeHex(md5)));
+////        }
+//        OutputStream os = new FileOutputStream("/Users/king/Downloads/1.png");
+//        os.write(b);
+//        os.close();
+//        inputStream.close();
+//    }
+//}

+ 9 - 9
themis-business/src/main/java/com/qmth/themis/business/util/RedisUtil.java

@@ -235,15 +235,15 @@ public class RedisUtil {
         redisTemplate.expire(key, timeOut, timeUnit);
     }
 
-    /**
-     * 获取redis序列
-     *
-     * @return
-     */
-    public Integer getRedisSequence() {
-        RedisAtomicInteger entityIdCounter = new RedisAtomicInteger(SystemConstant.REDIS_MONITOR_SEQUENCE, redisTemplate.getConnectionFactory());
-        return entityIdCounter.incrementAndGet();
-    }
+//    /**
+//     * 获取redis序列
+//     *
+//     * @return
+//     */
+//    public Integer getRedisSequence() {
+//        RedisAtomicInteger entityIdCounter = new RedisAtomicInteger(SystemConstant.REDIS_MONITOR_SEQUENCE, redisTemplate.getConnectionFactory());
+//        return entityIdCounter.incrementAndGet();
+//    }
 
     /**
      * 获取redis activity code序列

+ 48 - 0
themis-business/src/main/java/com/qmth/themis/business/util/SSLClient.java

@@ -0,0 +1,48 @@
+package com.qmth.themis.business.util;
+
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * @Description: 443 ssl
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2022/3/30
+ */
+public class SSLClient extends DefaultHttpClient {
+    public SSLClient() throws Exception {
+        super();
+        SSLContext ctx = SSLContext.getInstance("TLS");
+        X509TrustManager tm = new X509TrustManager() {
+            @Override
+            public void checkClientTrusted(X509Certificate[] chain,
+                                           String authType) throws CertificateException {
+            }
+
+            @Override
+            public void checkServerTrusted(X509Certificate[] chain,
+                                           String authType) throws CertificateException {
+            }
+
+            @Override
+            public X509Certificate[] getAcceptedIssuers() {
+                return null;
+            }
+        };
+        ctx.init(null, new TrustManager[]{tm}, null);
+        SSLSocketFactory ssf = new SSLSocketFactory(ctx, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+        ClientConnectionManager ccm = this.getConnectionManager();
+        SchemeRegistry sr = ccm.getSchemeRegistry();
+        sr.register(new Scheme("https", 443, ssf));
+    }
+}

+ 269 - 0
themis-business/src/main/java/com/qmth/themis/business/util/TencentCloudAPITC3Util.java

@@ -0,0 +1,269 @@
+package com.qmth.themis.business.util;
+
+import com.alibaba.fastjson.JSONObject;
+import com.qmth.themis.business.constant.SystemConstant;
+import org.apache.http.protocol.HTTP;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.xml.bind.DatatypeConverter;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.TreeMap;
+
+/**
+ * @Description: 腾讯云视频回调
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2022/3/30
+ */
+public class TencentCloudAPITC3Util {
+    private final static Logger log = LoggerFactory.getLogger(TencentCloudAPITC3Util.class);
+
+    public final static String DATE_PATTERN = "yyyy-MM-dd";
+    public final static String UTC = "UTC";
+    private final static String TC3 = "TC3";
+    private final static String TC3_REQUEST = "tc3_request";
+    private final static String TC3_HMAC_SHA256 = "TC3-HMAC-SHA256";
+    private final static String SIGNED_HEADERS = "content-type;host";
+    private final static Charset UTF8 = StandardCharsets.UTF_8;
+    //    private final static String SECRET_ID = "AKIDz8krbsJ5yKBZQpn74WFkmLPx3*******";
+//    private final static String SECRET_KEY = "Gu5t9xGARNpq86cd98joQYCN3*******";
+    private final static String CT_JSON = "application/json";
+    private final static String HMACSHA256 = "HmacSHA256";
+    private final static String SHA256 = "SHA-256";
+    private final static String VERSION = "2018-07-17";
+
+    /**
+     * hmac256算法
+     *
+     * @param key
+     * @param msg
+     * @return
+     * @throws Exception
+     */
+    public static byte[] hmac256(byte[] key, String msg) throws Exception {
+        Mac mac = Mac.getInstance(HMACSHA256);
+        SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
+        mac.init(secretKeySpec);
+        return mac.doFinal(msg.getBytes(UTF8));
+    }
+
+    /**
+     * sha256Hex 算法
+     *
+     * @param input
+     * @return
+     * @throws Exception
+     */
+    public static String sha256Hex(String input) throws Exception {
+        MessageDigest md = MessageDigest.getInstance(SHA256);
+        byte[] d = md.digest(input.getBytes(UTF8));
+        return DatatypeConverter.printHexBinary(d).toLowerCase();
+    }
+
+    /**
+     * 拼接规范请求串
+     *
+     * @param requestMethod
+     * @param offset
+     * @param limit
+     * @param subAppId
+     * @param host
+     * @return
+     * @throws Exception
+     */
+    public static String requestStr(String requestMethod, Long offset, Long limit, Long subAppId, String host) throws Exception {
+        String canonicalUri = "/";
+        String canonicalQueryString = "";
+        String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + host + "\n";
+        String signedHeaders = "content-type;host";
+
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put(SystemConstant.LIMIT, limit);
+        jsonObject.put(SystemConstant.OFFSET, offset);
+        jsonObject.put(SystemConstant.SUB_APP_ID, subAppId);
+//        String payload = "{\"Limit\": " + limit + ", \"Offset\": " + offset + ", \"SubAppId\": " + subAppId + "}";
+        String hashedRequestPayload = sha256Hex(jsonObject.toJSONString());
+        String canonicalRequest = requestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
+                + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
+        log.info("canonicalRequest:{}", canonicalRequest);
+        return canonicalRequest;
+    }
+
+    /**
+     * 拼接待签名字符串
+     *
+     * @param date
+     * @param timestamp
+     * @param service
+     * @param canonicalRequest
+     * @return
+     * @throws Exception
+     */
+    public static String signStr(String date, String timestamp, String service, String canonicalRequest) throws Exception {
+        String credentialScope = date + "/" + service + "/" + TC3_REQUEST;
+        String hashedCanonicalRequest = sha256Hex(canonicalRequest);
+        String stringToSign = TC3_HMAC_SHA256 + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
+        log.info("stringToSign:{}", stringToSign);
+        return stringToSign;
+    }
+
+    /**
+     * 计算签名
+     *
+     * @param secretKey
+     * @param date
+     * @param service
+     * @param stringToSign
+     * @return
+     * @throws Exception
+     */
+    public static String calculateSign(String secretKey, String date, String service, String stringToSign) throws Exception {
+        byte[] secretDate = hmac256((TC3 + secretKey).getBytes(UTF8), date);
+        byte[] secretService = hmac256(secretDate, service);
+        byte[] secretSigning = hmac256(secretService, TC3_REQUEST);
+        String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
+        log.info("signature:{}", signature);
+        return signature;
+    }
+
+    /**
+     * 拼接参数
+     *
+     * @param secretId
+     * @param date
+     * @param timestamp
+     * @param service
+     * @param signature
+     * @param action
+     * @param host
+     * @param subAppId
+     * @return
+     */
+    public static TreeMap<String, String> jointParam(String secretId,
+                                                     String date,
+                                                     String timestamp,
+                                                     String service,
+                                                     String signature,
+                                                     String action,
+                                                     String host,
+                                                     Long subAppId) {
+        String credentialScope = date + "/" + service + "/" + TC3_REQUEST;
+        String authorization = TC3_HMAC_SHA256 + " " + "Credential=" + secretId + "/" + credentialScope + ", "
+                + "SignedHeaders=" + SIGNED_HEADERS + ", " + "Signature=" + signature;
+        log.info("authorization:{}", authorization);
+
+        TreeMap<String, String> headers = new TreeMap<String, String>();
+        headers.put(SystemConstant.HEADER_AUTHORIZATION, authorization);
+        headers.put(HTTP.CONTENT_TYPE, CT_JSON);
+        headers.put(HTTP.TARGET_HOST, host);
+        headers.put(SystemConstant.X_TC_ACTION, action);
+        headers.put(SystemConstant.X_TC_TIMESTAMP, timestamp);
+        headers.put(SystemConstant.X_TC_VERSION, VERSION);
+        log.info("headers:{}", JacksonUtil.parseJson(headers));
+
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put(SystemConstant.LIMIT, 10L);
+        jsonObject.put(SystemConstant.OFFSET, 0L);
+        jsonObject.put(SystemConstant.SUB_APP_ID, subAppId);
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("curl -X POST https://").append(host)
+                .append(" -H \"Authorization: ").append(authorization).append("\"")
+                .append(" -H Content-Type: application/json")
+                .append(" -H \"Host: ").append(host).append("\"")
+                .append(" -H \"X-TC-Action: ").append(action).append("\"")
+                .append(" -H \"X-TC-Timestamp: ").append(timestamp).append("\"")
+                .append(" -H \"X-TC-Version: ").append(VERSION).append("\"")
+                .append(" -d '").append(jsonObject.toJSONString()).append("'");
+        log.info("sb:{}", sb.toString());
+
+//        try {
+//            Process process = Runtime.getRuntime().exec(sb.toString());
+//            process.getInputStream();
+//            BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
+//            String output;
+//            StringBuffer response = new StringBuffer();
+//            while ((output = br.readLine()) != null) {
+//                response.append(output);
+//            }
+//            br.close();
+//            System.out.println(response.toString());
+//        } catch (IOException ioException) {
+//            ioException.printStackTrace();
+//        }
+        return headers;
+    }
+
+//    public static void main(String[] args) throws Exception {
+////        String service = "cvm";
+////        String host = "cvm.tencentcloudapi.com";
+////        String region = "ap-guangzhou";
+//        String action = "DescribeInstances";
+//        String version = "2017-03-12";
+//        String algorithm = "TC3-HMAC-SHA256";
+//        String timestamp = "1551113065";
+//        //String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+//        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+//        // 注意时区,否则容易出错
+//        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+//        String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
+//
+//        // ************* 步骤 1:拼接规范请求串 *************
+//        String httpRequestMethod = "POST";
+//        String canonicalUri = "/";
+//        String canonicalQueryString = "";
+//        String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + host + "\n";
+//        String signedHeaders = "content-type;host";
+//
+//        String payload = "{\"Limit\": 1, \"Filters\": [{\"Values\": [\"\\u672a\\u547d\\u540d\"], \"Name\": \"instance-name\"}]}";
+//        String hashedRequestPayload = sha256Hex(payload);
+//        String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
+//                + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
+//        System.out.println(canonicalRequest);
+//
+//        // ************* 步骤 2:拼接待签名字符串 *************
+//        String credentialScope = date + "/" + service + "/" + "tc3_request";
+//        String hashedCanonicalRequest = sha256Hex(canonicalRequest);
+//        String stringToSign = algorithm + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
+//        System.out.println(stringToSign);
+//
+//        // ************* 步骤 3:计算签名 *************
+//        byte[] secretDate = hmac256(("TC3" + SECRET_KEY).getBytes(UTF8), date);
+//        byte[] secretService = hmac256(secretDate, service);
+//        byte[] secretSigning = hmac256(secretService, "tc3_request");
+//        String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
+//        System.out.println(signature);
+//
+//        // ************* 步骤 4:拼接 Authorization *************
+//        String authorization = algorithm + " " + "Credential=" + SECRET_ID + "/" + credentialScope + ", "
+//                + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
+//        System.out.println(authorization);
+//
+//        TreeMap<String, String> headers = new TreeMap<String, String>();
+//        headers.put("Authorization", authorization);
+//        headers.put("Content-Type", CT_JSON);
+//        headers.put("Host", host);
+//        headers.put("X-TC-Action", action);
+//        headers.put("X-TC-Timestamp", timestamp);
+//        headers.put("X-TC-Version", version);
+////        headers.put("X-TC-Region", region);
+//
+//        StringBuilder sb = new StringBuilder();
+//        sb.append("curl -X POST https://").append(host)
+//                .append(" -H \"Authorization: ").append(authorization).append("\"")
+//                .append(" -H \"Content-Type: application/json; charset=utf-8\"")
+//                .append(" -H \"Host: ").append(host).append("\"")
+//                .append(" -H \"X-TC-Action: ").append(action).append("\"")
+//                .append(" -H \"X-TC-Timestamp: ").append(timestamp).append("\"")
+//                .append(" -H \"X-TC-Version: ").append(version).append("\"")
+//                .append(" -H \"X-TC-Region: ").append(region).append("\"")
+//                .append(" -d '").append(payload).append("'");
+//        System.out.println(sb.toString());
+//    }
+}

+ 70 - 1
themis-business/src/main/java/com/qmth/themis/business/util/TencentYunUtil.java

@@ -1,10 +1,24 @@
 package com.qmth.themis.business.util;
 
+import com.qmth.themis.business.constant.SystemConstant;
 import com.qmth.themis.business.domain.TencentYunDomain;
+import com.tencentcloudapi.common.Credential;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
+import com.tencentcloudapi.vod.v20180717.VodClient;
+import com.tencentcloudapi.vod.v20180717.models.SearchMediaRequest;
+import com.tencentcloudapi.vod.v20180717.models.SearchMediaResponse;
 import com.tencentyun.TLSSigAPIv2;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.context.annotation.Bean;
 import org.springframework.stereotype.Component;
 
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
 /**
  * @Description: 腾讯云util
  * @Param:
@@ -14,12 +28,21 @@ import org.springframework.stereotype.Component;
  */
 @Component
 public class TencentYunUtil {
+    private final static Logger log = LoggerFactory.getLogger(TencentYunUtil.class);
 
     private TencentYunDomain tencentYunDomain;
 
     @Bean
     public TencentYunDomain tencentYunEnv(TencentYunDomain tencentYunDomain) {
-        this.tencentYunDomain = new TencentYunDomain(tencentYunDomain.getAppId(), tencentYunDomain.getKey(), tencentYunDomain.getUrls());
+        this.tencentYunDomain = new TencentYunDomain(tencentYunDomain.getAppId(),
+                tencentYunDomain.getKey(),
+                tencentYunDomain.getUrls(),
+                tencentYunDomain.getQueryUrl(),
+                tencentYunDomain.getService(),
+                tencentYunDomain.getSecretId(),
+                tencentYunDomain.getSecretKey(),
+                tencentYunDomain.getVodAppId(),
+                tencentYunDomain.getCallbackPwd());
         return this.tencentYunDomain;
     }
 
@@ -38,4 +61,50 @@ public class TencentYunUtil {
     public TencentYunDomain getTencentYunDomain() {
         return tencentYunDomain;
     }
+
+    /**
+     * 腾讯云vodsdk
+     *
+     * @param secretId
+     * @param secretKey
+     * @param endPoint
+     * @param subAppId
+     * @param streamIds
+     * @return
+     */
+    public Map<String, Object> tencentVodSdk(String secretId, String secretKey, String endPoint, Long subAppId, String... streamIds) {
+        Map<String, Object> resultMap = null;
+        try {
+            // 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密
+            // 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取
+            Credential cred = new Credential(secretId, secretKey);
+            // 实例化一个http选项,可选的,没有特殊需求可以跳过
+            HttpProfile httpProfile = new HttpProfile();
+            httpProfile.setEndpoint(endPoint);
+            // 实例化一个client选项,可选的,没有特殊需求可以跳过
+            ClientProfile clientProfile = new ClientProfile();
+            clientProfile.setHttpProfile(httpProfile);
+            // 实例化要请求产品的client对象,clientProfile是可选的
+            VodClient client = new VodClient(cred, "", clientProfile);
+            // 实例化一个请求对象,每个接口都会对应一个request对象
+            SearchMediaRequest req = new SearchMediaRequest();
+            req.setSubAppId(subAppId);
+            if (Objects.nonNull(streamIds) && streamIds.length > 0) {
+                req.setStreamIds(streamIds);
+            }
+
+            // 返回的resp是一个SearchMediaResponse的实例,与请求对象对应
+            SearchMediaResponse resp = client.SearchMedia(req);
+            if (Objects.nonNull(resp)) {
+                resultMap = new HashMap<>();
+                // 输出json格式的字符串回包
+                String result = SearchMediaResponse.toJsonString(resp);
+                resultMap.put("object", resp);
+                log.info("result:{}", result);
+            }
+        } catch (TencentCloudSDKException e) {
+            log.error(SystemConstant.LOG_ERROR, e);
+        }
+        return resultMap;
+    }
 }

+ 1 - 0
themis-business/src/main/resources/mapper/TEExamActivityMapper.xml

@@ -158,6 +158,7 @@
         ,IFNULL(teea.opening_seconds,tee.opening_seconds) as openingSeconds,
         teea.opening_seconds as activityOpeningSeconds,
         tee.monitor_video_source as monitorVideoSourceStr,
+        tee.monitor_record as monitorRecordStr,
         IFNULL(teea.prepare_seconds,tee.prepare_seconds) as prepareSeconds,
         teea.prepare_seconds as activityPrepareSeconds,
         teea.max_duration_seconds as activityMaxDurationSeconds,

+ 3 - 0
themis-business/src/main/resources/mapper/TEExamMapper.xml

@@ -45,6 +45,7 @@
         if(t.updateTime is not null, t.updateTime, t.createTime) as updateTime,
         t.enableIpLimit,
         t.monitorVideoSource,
+        t.monitorRecord,
         t.monitorStatus
         <if test="type != null and type != '' and type == 'monitor'">
             ,group_concat(distinct tees.room_code) as roomCode,
@@ -63,6 +64,7 @@
         t.update_time as updateTime,
         t.enable_ip_limit as enableIpLimit,
         t.monitor_video_source as monitorVideoSource,
+        t.monitor_record as monitorRecord,
         t.monitor_status as monitorStatus,
         TRUNCATE(t.progress,2) as progress,
         t.score_status,
@@ -133,6 +135,7 @@
         t.updateTime,
         t.enableIpLimit,
         t.monitorVideoSource,
+        t.monitorRecord,
         t.monitorStatus
         order by t.createTime desc
     </select>

+ 3 - 1
themis-business/src/main/resources/mapper/TEStudentMapper.xml

@@ -56,7 +56,9 @@
             tees.`identity`,
             tees.name,
             toer.objective_score as objectiveScore,
-            toer.status
+            toer.status,
+            toer.tencent_video_url as tencentVideoUrl,
+            toer.first_start_time as firstStartTime
         from
             t_e_exam_student tees
         left join t_e_exam tee on

+ 5 - 0
themis-business/src/main/resources/mapper/TMTencentVideoMessageMapper.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.qmth.themis.business.dao.TMTencentVideoMessageMapper">
+
+</mapper>

+ 2 - 1
themis-business/src/main/resources/mapper/TOeExamRecordMapper.xml

@@ -103,7 +103,8 @@
 		IFNULL(t.breach_status,1) as breachStatus,
 		IFNULL(t.client_websocket_status,'OFF_LINE') as clientWebsocketStatus,
 		t.client_last_sync_time as updateTime,
-		t.monitor_video_source as monitorVideoSource
+		t.monitor_video_source as monitorVideoSource,
+        t.monitor_record as monitorRecord
 	</sql>
 
     <sql id="invigilatePageMiddle">

+ 14 - 2
themis-common/pom.xml

@@ -5,13 +5,13 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.qmth.themis.common</groupId>
     <artifactId>themis-common</artifactId>
-    <version>1.1.0</version>
+    <version>1.1.1</version>
     <packaging>jar</packaging>
 
     <parent>
         <groupId>com.qmth.themis</groupId>
         <artifactId>themis-service</artifactId>
-        <version>1.1.0</version>
+        <version>1.1.1</version>
     </parent>
 
     <dependencies>
@@ -78,6 +78,18 @@
             <groupId>com.github.tencentyun</groupId>
             <artifactId>tls-sig-api-v2</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.tencentcloudapi</groupId>
+            <artifactId>tencentcloud-sdk-java</artifactId>
+        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>com.google.zxing</groupId>-->
+<!--            <artifactId>core</artifactId>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>com.google.zxing</groupId>-->
+<!--            <artifactId>javase</artifactId>-->
+<!--        </dependency>-->
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>

+ 2 - 2
themis-exam/pom.xml

@@ -4,13 +4,13 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.qmth.themis.exam</groupId>
     <artifactId>themis-exam</artifactId>
-    <version>1.1.0</version>
+    <version>1.1.1</version>
     <packaging>jar</packaging>
 
     <parent>
         <groupId>com.qmth.themis</groupId>
         <artifactId>themis-service</artifactId>
-        <version>1.1.0</version>
+        <version>1.1.1</version>
     </parent>
 
     <dependencies>

+ 20 - 11
themis-exam/src/main/java/com/qmth/themis/exam/listener/service/impl/MqOeLogicServiceImpl.java

@@ -264,24 +264,29 @@ public class MqOeLogicServiceImpl implements MqOeLogicService {
     public void execMqOeMobileLogic(MqDto mqDto, String key) {
         Gson gson = new Gson();
         String tag = mqDto.getTag();
-//        AtomicBoolean match = new AtomicBoolean(false);
         ConcurrentHashMap<String, WebSocketMobileServer> webSocketMap = WebSocketMobileServer.getWebSocketMap();
         if (Objects.equals(MqTagEnum.EXAM_STOP.name(), tag)//考试退出
                 || Objects.equals(MqTagEnum.EXAM_START.name(), tag)) {//考试开始
             Long recordId = Long.parseLong(String.valueOf(mqDto.getBody()));
-            String mobileWebsocketId = ExamRecordCacheUtil.getMobileFirstWebsocketId(recordId);
-            if (Objects.nonNull(mobileWebsocketId) && Objects.nonNull(webSocketMap.get(mobileWebsocketId + "-" + MonitorVideoSourceEnum.MOBILE_FIRST.name()))) {
-                WebSocketMobileServer webSocketMobileServer = webSocketMap.get(mobileWebsocketId + "-" + MonitorVideoSourceEnum.MOBILE_FIRST.name());
+            String mobileFirstWebsocketId = ExamRecordCacheUtil.getMobileFirstWebsocketId(recordId);
+            if (Objects.nonNull(mobileFirstWebsocketId) && Objects.nonNull(webSocketMap.get(mobileFirstWebsocketId + "-" + MonitorVideoSourceEnum.MOBILE_FIRST.name()))) {
+                WebSocketMobileServer webSocketMobileServer = webSocketMap.get(mobileFirstWebsocketId + "-" + MonitorVideoSourceEnum.MOBILE_FIRST.name());
                 ExamRecordStatusEnum examRecordStatusEnum = ExamRecordCacheUtil.getStatus(recordId);
                 if (Objects.nonNull(examRecordStatusEnum)
                         && Objects.nonNull(webSocketMobileServer.getRecordId())
                         && webSocketMobileServer.getRecordId().longValue() == recordId.longValue()) {
-//                    match.set(true);
                     WebsocketDto websocketDto = null;
                     switch (tag.toUpperCase()) {
                         case "EXAM_STOP":
                             if (!Objects.equals(ExamRecordStatusEnum.ANSWERING, examRecordStatusEnum)) {
                                 websocketDto = new WebsocketDto(WebsocketTypeEnum.EXAM_STOP.name(), mqDto.getProperties());
+                                Map<String, Object> properties = mqDto.getProperties();
+                                if (Objects.nonNull(properties) && properties.size() > 0) {
+                                    Boolean mobileRemoveWebsocket = (Boolean) properties.get(SystemConstant.MOBILE_REMOVE_WEBSOCKET);
+                                    if (Objects.nonNull(mobileRemoveWebsocket) && mobileRemoveWebsocket) {
+                                        webSocketMap.remove(mobileFirstWebsocketId);
+                                    }
+                                }
                             }
                             break;
                         case "EXAM_START":
@@ -296,19 +301,25 @@ public class MqOeLogicServiceImpl implements MqOeLogicService {
                     webSocketMobileServer.sendMessage(websocketDto);
                 }
             }
-            mobileWebsocketId = ExamRecordCacheUtil.getMobileSecondWebsocketId(recordId);
-            if (Objects.nonNull(mobileWebsocketId) && Objects.nonNull(webSocketMap.get(mobileWebsocketId + "-" + MonitorVideoSourceEnum.MOBILE_SECOND.name()))) {
-                WebSocketMobileServer webSocketMobileServer = webSocketMap.get(mobileWebsocketId + "-" + MonitorVideoSourceEnum.MOBILE_SECOND.name());
+            String mobileSecondWebsocketId = ExamRecordCacheUtil.getMobileSecondWebsocketId(recordId);
+            if (Objects.nonNull(mobileSecondWebsocketId) && Objects.nonNull(webSocketMap.get(mobileSecondWebsocketId + "-" + MonitorVideoSourceEnum.MOBILE_SECOND.name()))) {
+                WebSocketMobileServer webSocketMobileServer = webSocketMap.get(mobileSecondWebsocketId + "-" + MonitorVideoSourceEnum.MOBILE_SECOND.name());
                 ExamRecordStatusEnum examRecordStatusEnum = ExamRecordCacheUtil.getStatus(recordId);
                 if (Objects.nonNull(examRecordStatusEnum)
                         && Objects.nonNull(webSocketMobileServer.getRecordId())
                         && webSocketMobileServer.getRecordId().longValue() == recordId.longValue()) {
-//                    match.set(true);
                     WebsocketDto websocketDto = null;
                     switch (tag.toUpperCase()) {
                         case "EXAM_STOP":
                             if (!Objects.equals(ExamRecordStatusEnum.ANSWERING, examRecordStatusEnum)) {
                                 websocketDto = new WebsocketDto(WebsocketTypeEnum.EXAM_STOP.name(), mqDto.getProperties());
+                                Map<String, Object> properties = mqDto.getProperties();
+                                if (Objects.nonNull(properties) && properties.size() > 0) {
+                                    Boolean mobileRemoveWebsocket = (Boolean) properties.get(SystemConstant.MOBILE_REMOVE_WEBSOCKET);
+                                    if (Objects.nonNull(mobileRemoveWebsocket) && mobileRemoveWebsocket) {
+                                        webSocketMap.remove(mobileSecondWebsocketId);
+                                    }
+                                }
                             }
                             break;
                         case "EXAM_START":
@@ -324,12 +335,10 @@ public class MqOeLogicServiceImpl implements MqOeLogicService {
                 }
             }
         }
-//        if (match.get()) {
         mqDto.setAck(SystemConstant.STANDARD_ACK_TYPE);
         TMRocketMessage tmRocketMessage = gson.fromJson(gson.toJson(mqDto), TMRocketMessage.class);
         tmRocketMessage.setBody(JacksonUtil.parseJson(tmRocketMessage.getBody()));
         tmRocketMessageService.saveOrUpdate(tmRocketMessage);
         redisUtil.delete(key, mqDto.getId());
-//        }
     }
 }

+ 8 - 0
themis-exam/src/main/resources/application.properties

@@ -141,6 +141,7 @@ mq.config.map.EXAM_BREAK_GROUP=themis-group-exam-examBreak
 mq.config.map.EXAM_BREAK_DELAY_GROUP=themis-group-exam-examBreakDelay
 mq.config.map.WEBSOCKET_OE_GROUP=themis-group-exam-websocketOe
 mq.config.map.WEBSOCKET_OE_MOBILE_GROUP=themis-group-exam-websocketOeMobile
+mq.config.map.TENCENT_VIDEO_GROUP=themis-group-exam-tencentVideo
 
 #\u963F\u91CC\u4E91OSS\u914D\u7F6E
 aliyun.oss.publicName=oss-cn-shenzhen.aliyuncs.com
@@ -161,6 +162,12 @@ aliyun.oss.privateUrl=http://static-test.qmth.com.cn
 tencentyun.sdk.appId=1400411036
 tencentyun.sdk.key=d78004c94473cb1cf78af33d333e18b731132e527e829e44e2ab133945243b11
 tencentyun.sdk.urls=https://live1.qmth.com.cn,https://live2.qmth.com.cn,https://live3.qmth.com.cn,https://live4.qmth.com.cn,https://live5.qmth.com.cn,https://live6.qmth.com.cn
+tencentyun.sdk.service=vod
+tencentyun.sdk.queryUrl=${tencentyun.sdk.service}.tencentcloudapi.com
+tencentyun.sdk.secretId=AKIDKUO2PVLuCDxQcW9VaOuA8pFOGq9BwQdZ
+tencentyun.sdk.secretKey=g5D4dwxhByXrvjGWVDfqkPqzrSmqd9OM
+tencentyun.sdk.vodAppId=1500002365
+tencentyun.sdk.callbackPwd=123456
 
 #\u7CFB\u7EDF\u914D\u7F6E
 sys.config.datacenterId=2
@@ -180,6 +187,7 @@ spring.resources.static-locations=file:${sys.config.serverUpload},classpath:/MET
 prefix.url.exam=api/oe
 prefix.url.mobile=api/mobile
 
+#wxapp.upload.url=http://192.168.10.86:6002
 wxapp.upload.url=https://mobile.online-exam-test.cn
 client.config.url=https://cdn.online-exam.cn/client
 monitor.config.prefix=oe_test

+ 2 - 2
themis-mq/pom.xml

@@ -4,13 +4,13 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.qmth.themis.mq</groupId>
     <artifactId>themis-mq</artifactId>
-    <version>1.1.0</version>
+    <version>1.1.1</version>
     <packaging>jar</packaging>
 
     <parent>
         <groupId>com.qmth.themis</groupId>
         <artifactId>themis-service</artifactId>
-        <version>1.1.0</version>
+        <version>1.1.1</version>
     </parent>
 
     <dependencies>

+ 11 - 1
themis-mq/src/main/java/com/qmth/themis/mq/service/MqLogicService.java

@@ -6,6 +6,8 @@ import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
 import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
 import org.apache.rocketmq.common.message.MessageExt;
 
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
 import java.util.List;
 
 /**
@@ -55,7 +57,15 @@ public interface MqLogicService {
      * @param mqDto
      * @param key
      */
-    public void execMqWebsocketUnNormalLogic(MqDto mqDto, String key);
+    public void execMqWebsocketUnNormalLogic(MqDto mqDto, String key) throws NoSuchAlgorithmException;
+
+    /**
+     * 腾讯云视频回调逻辑
+     *
+     * @param mqDto
+     * @param key
+     */
+    public void execMqTencentVideoLogic(MqDto mqDto, String key) throws NoSuchAlgorithmException, IOException;
 
     /**
      * 计算客观分逻辑

+ 197 - 10
themis-mq/src/main/java/com/qmth/themis/mq/service/impl/MqLogicServiceImpl.java

@@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.google.common.collect.Sets;
 import com.google.gson.Gson;
 import com.qmth.themis.business.cache.ExamRecordCacheUtil;
 import com.qmth.themis.business.cache.RedisKeyHelper;
@@ -12,20 +13,23 @@ import com.qmth.themis.business.constant.SpringContextHolder;
 import com.qmth.themis.business.constant.SystemConstant;
 import com.qmth.themis.business.dto.MqDto;
 import com.qmth.themis.business.dto.WarningDto;
+import com.qmth.themis.business.dto.cache.TEStudentCacheDto;
 import com.qmth.themis.business.entity.*;
 import com.qmth.themis.business.enums.*;
 import com.qmth.themis.business.service.*;
 import com.qmth.themis.business.templete.TaskExportTemplete;
 import com.qmth.themis.business.templete.TaskImportTemplete;
 import com.qmth.themis.business.templete.impl.*;
-import com.qmth.themis.business.util.JacksonUtil;
-import com.qmth.themis.business.util.MqUtil;
-import com.qmth.themis.business.util.RedisUtil;
-import com.qmth.themis.business.util.UidUtil;
+import com.qmth.themis.business.util.*;
 import com.qmth.themis.common.contanst.Constants;
+import com.qmth.themis.common.enums.ExceptionResultEnum;
+import com.qmth.themis.common.enums.Source;
 import com.qmth.themis.common.exception.BusinessException;
 import com.qmth.themis.common.util.SimpleBeanUtil;
 import com.qmth.themis.mq.service.MqLogicService;
+import com.tencentcloudapi.vod.v20180717.models.MediaBasicInfo;
+import com.tencentcloudapi.vod.v20180717.models.MediaInfo;
+import com.tencentcloudapi.vod.v20180717.models.SearchMediaResponse;
 import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
 import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
 import org.apache.rocketmq.common.message.MessageExt;
@@ -37,10 +41,8 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.io.IOException;
 import java.lang.reflect.Method;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.UUID;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -118,6 +120,24 @@ public class MqLogicServiceImpl implements MqLogicService {
     @Resource
     MqUtil mqUtil;
 
+    @Resource
+    CacheService cacheService;
+
+    @Resource
+    TencentYunUtil tencentYunUtil;
+
+    @Resource
+    TMTencentVideoMessageService tencentVideoMessageService;
+
+    @Resource
+    MonitorUtil monitorUtil;
+
+    @Resource
+    OssUtil ossUtil;
+
+    @Resource
+    TOeExamRecordService tOeExamRecordService;
+
     /**
      * mq最大重试次数逻辑
      *
@@ -265,13 +285,14 @@ public class MqLogicServiceImpl implements MqLogicService {
      */
     @Override
     @Transactional
-    public void execMqWebsocketUnNormalLogic(MqDto mqDto, String key) {
+    public void execMqWebsocketUnNormalLogic(MqDto mqDto, String key) throws NoSuchAlgorithmException {
         Gson gson = new Gson();
         mqDto.setAck(SystemConstant.STANDARD_ACK_TYPE);//表示成功处理
         Map<String, Object> tranMap = mqDto.getProperties();
         Long recordId = Long.parseLong(String.valueOf(tranMap.get("recordId")));
         ExamRecordStatusEnum status = ExamRecordCacheUtil.getStatus(recordId);
         WebsocketStatusEnum websocketStatusEnum = ExamRecordCacheUtil.getClientWebsocketStatus(recordId);
+        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
         if (Objects.nonNull(websocketStatusEnum) && (Objects.equals(websocketStatusEnum, WebsocketStatusEnum.OFF_LINE)
                 && Objects.equals(status, ExamRecordStatusEnum.ANSWERING))) {
             examRecordService.examBreakLogic(recordId, true);
@@ -292,10 +313,47 @@ public class MqLogicServiceImpl implements MqLogicService {
                         .setMonitorStatus(recordId, MonitorVideoSourceEnum.CLIENT_SCREEN, MonitorStatusSourceEnum.STOP,
                                 timestamp);
             }
+
+            ExamStudentCacheBean examStudentCacheBean = teExamStudentService.getExamStudentCacheBean(examStudentId);
+            Optional.ofNullable(examStudentCacheBean).orElseThrow(() -> new BusinessException("考生数据为空"));
+
+            TEStudentCacheDto teStudent = cacheService.addStudentAccountCache(examStudentCacheBean.getStudentId());
+            Optional.ofNullable(teStudent).orElseThrow(() -> new BusinessException("学生数据为空"));
+
+            //2022/03/28加入更新移动端第一、第二机位推流状态
+            //更新移动端第一机位推流状态为stop
+            MonitorStatusSourceEnum mobileFirstStatusSourceEnum = ExamRecordCacheUtil
+                    .getMonitorStatus(recordId, MonitorVideoSourceEnum.MOBILE_FIRST);
+            if (Objects.nonNull(mobileFirstStatusSourceEnum)) {
+                ExamRecordCacheUtil
+                        .setMonitorStatus(recordId, MonitorVideoSourceEnum.MOBILE_FIRST, MonitorStatusSourceEnum.STOP,
+                                timestamp);
+                ExamRecordCacheUtil.setMobileFirstWebsocketStatus(recordId, WebsocketStatusEnum.OFF_LINE, timestamp);
+                String sessionId = SessionUtil.digest(teStudent.getId(),
+                        Math.abs(Sets.newHashSet(RoleEnum.STUDENT.name()).toString().hashCode()), Source.MOBILE_MONITOR_FIRST.name());
+                redisUtil.deleteUserSession(sessionId);
+            }
+
+            //更新移动端第二机位推流状态为stop
+            MonitorStatusSourceEnum mobileSecondStatusSourceEnum = ExamRecordCacheUtil
+                    .getMonitorStatus(recordId, MonitorVideoSourceEnum.MOBILE_SECOND);
+            if (Objects.nonNull(mobileSecondStatusSourceEnum)) {
+                ExamRecordCacheUtil
+                        .setMonitorStatus(recordId, MonitorVideoSourceEnum.MOBILE_SECOND, MonitorStatusSourceEnum.STOP,
+                                timestamp);
+                ExamRecordCacheUtil.setMobileSecondWebsocketStatus(recordId, WebsocketStatusEnum.OFF_LINE, timestamp);
+                String sessionId = SessionUtil.digest(teStudent.getId(),
+                        Math.abs(Sets.newHashSet(RoleEnum.STUDENT.name()).toString().hashCode()), Source.MOBILE_MONITOR_SECOND.name());
+                redisUtil.deleteUserSession(sessionId);
+            }
+
             examRecordService.sendExamRecordDataSaveMq(recordId, timestamp);
+
+            Map<String, Object> properties = new HashMap<>();
+            properties.put(SystemConstant.MOBILE_REMOVE_WEBSOCKET, true);
             //发送移动端监考退出考试mq消息 start
             MqDto mqDtoExamStop = new MqDto(mqUtil.getMqGroupDomain().getTopic(), MqTagEnum.EXAM_STOP.name(), recordId,
-                    MqTagEnum.EXAM_STOP, String.valueOf(recordId), String.valueOf(recordId));
+                    MqTagEnum.EXAM_STOP, String.valueOf(recordId), properties, String.valueOf(recordId));
             mqDtoService.assembleSendOneOrderMsg(mqDtoExamStop);
             //发送移动端监考退出考试mq消息 end
         }
@@ -305,6 +363,80 @@ public class MqLogicServiceImpl implements MqLogicService {
         redisUtil.delete(key, mqDto.getId());
     }
 
+    /**
+     * 腾讯云视频回调逻辑
+     *
+     * @param mqDto
+     * @param key
+     * @throws NoSuchAlgorithmException
+     */
+    @Override
+    @Transactional
+    public void execMqTencentVideoLogic(MqDto mqDto, String key) throws NoSuchAlgorithmException, IOException {
+        Gson gson = new Gson();
+        Map<String, Object> tranMap = mqDto.getProperties();
+        Long recordId = Long.parseLong(String.valueOf(tranMap.get("recordId")));
+        TOeExamRecord tOeExamRecord = tOeExamRecordService.getById(recordId);
+        List<String> monitorRecordList = Arrays.asList(tOeExamRecord.getMonitorRecord().trim().toUpperCase().split(","));
+        String[] streamIds = new String[monitorRecordList.size()];
+        for (int i = 0; i < monitorRecordList.size(); i++) {
+            streamIds[i] = SystemConstant.setStreamId(monitorUtil.getMonitorDomain().getPrefix(), recordId, MonitorVideoSourceEnum.convertToName(monitorRecordList.get(i)));
+        }
+
+        Map<String, Object> resultMap = tencentYunUtil.tencentVodSdk(tencentYunUtil.getTencentYunDomain().getSecretId(),
+                tencentYunUtil.getTencentYunDomain().getSecretKey(),
+                tencentYunUtil.getTencentYunDomain().getQueryUrl(),
+                tencentYunUtil.getTencentYunDomain().getVodAppId(),
+                streamIds);
+
+        if (Objects.nonNull(resultMap) && resultMap.size() > 0) {
+            SearchMediaResponse response = (SearchMediaResponse) resultMap.get("object");
+            if (Objects.nonNull(response) && response.getTotalCount().intValue() > 0) {
+                Map<String, String> map = new HashMap<>();
+                JSONArray jsonArray = null;
+                int countDb = 0;
+                if (Objects.nonNull(tOeExamRecord.getTencentVideoUrl())) {
+                    jsonArray = JSONArray.parseArray(tOeExamRecord.getTencentVideoUrl());
+                    countDb = jsonArray.size();
+                    for (int i = 0; i < countDb; i++) {
+                        JSONObject jsonObject = (JSONObject) jsonArray.get(i);
+                        String videoSource = SystemConstant.getMonitorRecordStreamId((String) jsonObject.get(SystemConstant.VIDEO_SOURCE));
+                        map.put(videoSource, videoSource);
+                    }
+                } else {
+                    jsonArray = new JSONArray();
+                }
+                MediaInfo[] mediaInfos = response.getMediaInfoSet();
+                for (int i = 0; i < mediaInfos.length; i++) {
+                    MediaBasicInfo mediaBasicInfo = mediaInfos[i].getBasicInfo();
+                    String videoSource = SystemConstant.getMonitorRecordStreamId(mediaBasicInfo.getName());
+                    boolean lock = redisUtil.lock(SystemConstant.REDIS_LOCK_TENCENT_VIDEO_PREFIX + videoSource, SystemConstant.REDIS_LOCK_TENCENT_VIDEO_TIME_OUT);
+                    if (lock && Objects.isNull(map.get(videoSource))) {
+                        try {
+                            JSONObject jsonObject = new JSONObject();
+                            jsonObject.put(SystemConstant.VIDEO_SOURCE, videoSource);
+                            jsonObject.put(SystemConstant.VIDEO_URL, mediaBasicInfo.getMediaUrl());
+                            jsonArray.add(jsonObject);
+                        } finally {
+                            redisUtil.releaseLock(SystemConstant.REDIS_LOCK_TENCENT_VIDEO_PREFIX + videoSource);
+                        }
+                    }
+                }
+                if (Objects.nonNull(jsonArray) && jsonArray.size() > 0 && countDb != jsonArray.size()) {
+                    tOeExamRecord.setTencentVideoUrl(jsonArray.toJSONString());
+                    tOeExamRecordService.updateById(tOeExamRecord);
+                }
+                TMTencentVideoMessage tencentVideoMessage = new TMTencentVideoMessage(response.getRequestId(), JacksonUtil.parseJson(response));
+                tencentVideoMessageService.save(tencentVideoMessage);
+            }
+        }
+        mqDto.setAck(SystemConstant.STANDARD_ACK_TYPE);//表示成功处理
+        TMRocketMessage tmRocketMessage = gson.fromJson(gson.toJson(mqDto), TMRocketMessage.class);
+        tmRocketMessage.setBody(JacksonUtil.parseJson(tmRocketMessage.getBody()));
+        tmRocketMessageService.saveOrUpdate(tmRocketMessage);
+        redisUtil.delete(key, mqDto.getId());
+    }
+
     /**
      * 计算客观分逻辑
      *
@@ -1014,4 +1146,59 @@ public class MqLogicServiceImpl implements MqLogicService {
         }
         return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;//成功
     }
+
+//    /**
+//     * 创建qrcode
+//     *
+//     * @param videoUrl
+//     * @param jsonObject
+//     * @return
+//     * @throws IOException
+//     */
+//    private JSONObject createQrCode(String videoUrl, JSONObject jsonObject) throws IOException {
+//        InputStream inputStream = null;
+//        try {
+//            byte[] b = QrCodeUtil.createQRCode(300, 300, videoUrl);
+//            inputStream = new ByteArrayInputStream(b);
+//            String md5 = DigestUtils.md5Hex(new ByteArrayInputStream(b));
+//
+//            AliYunOssPublicDomain aliYunOssPublicDomain = ossUtil.getAliYunOssPublicDomain();
+//            Boolean oss = (Boolean) aliYunOssPublicDomain.getMap().get(SystemConstant.OSS);
+//
+//            LocalDateTime nowTime = LocalDateTime.now();
+//            StringJoiner stringJoiner = new StringJoiner("");
+//            if (!oss) {
+//                stringJoiner.add(SystemConstant.FILES_DIR).add(File.separator);
+//            }
+//            String fileType = ".png";
+//            UploadFileEnum type = UploadFileEnum.upload;
+//            stringJoiner.add(type.name()).add(File.separator);
+//            stringJoiner.add(String.valueOf(nowTime.getYear())).add(File.separator)
+//                    .add(String.format("%02d", nowTime.getMonthValue())).add(File.separator)
+//                    .add(String.format("%02d", nowTime.getDayOfMonth()));
+//            if (oss) {//上传至oss
+//                stringJoiner.add(File.separator).add(String.valueOf(UUID.randomUUID()).replaceAll("-", "")).add(fileType);
+//                ossUtil.upload(ossUtil.isPublic(type), stringJoiner.toString(), inputStream, md5);
+//                jsonObject.put("qrCodeUrl", aliYunOssPublicDomain.getPublicUrl() + File.separator + stringJoiner.toString());
+//            } else {//上传至服务器
+//                File finalFile = new File(
+//                        stringJoiner.add(File.separator).add(String.valueOf(UUID.randomUUID()).replaceAll("-", ""))
+//                                .add(fileType).toString());
+//                if (!finalFile.exists()) {
+//                    finalFile.getParentFile().mkdirs();
+//                    finalFile.createNewFile();
+//                }
+//                String fileHost = (String) aliYunOssPublicDomain.getMap().get(SystemConstant.FILE_HOST);
+//                jsonObject.put("qrCodeUrl", "http://" + fileHost + File.separator + finalFile.getPath());
+//                FileUtils.copyInputStreamToFile(inputStream, finalFile);
+//            }
+//        } catch (Exception e) {
+//            log.error(SystemConstant.LOG_ERROR, e);
+//        } finally {
+//            if (Objects.nonNull(inputStream)) {
+//                inputStream.close();
+//            }
+//        }
+//        return jsonObject;
+//    }
 }

+ 29 - 0
themis-mq/src/main/java/com/qmth/themis/mq/templete/impl/TencentVideoConcurrentlyImpl.java

@@ -0,0 +1,29 @@
+package com.qmth.themis.mq.templete.impl;
+
+import com.qmth.themis.business.constant.SpringContextHolder;
+import com.qmth.themis.business.enums.MqExecTypeEnum;
+import com.qmth.themis.mq.service.MqLogicService;
+import com.qmth.themis.mq.templete.Concurrently;
+import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
+import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
+import org.apache.rocketmq.common.message.MessageExt;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * @Description: mq 延时消息监听 tencent video 并行消费监听
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2020/7/28
+ */
+@Service
+public class TencentVideoConcurrentlyImpl implements Concurrently {
+
+    @Override
+    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
+        MqLogicService mqLogicService = SpringContextHolder.getBean(MqLogicService.class);
+        return mqLogicService.consumeMessageDelay(msgs, consumeConcurrentlyContext, MqExecTypeEnum.EXEC_MQ_TENCENT_VIDEO_LOGIC);
+    }
+}

+ 2 - 2
themis-task/pom.xml

@@ -4,13 +4,13 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.qmth.themis.task</groupId>
     <artifactId>themis-task</artifactId>
-    <version>1.1.0</version>
+    <version>1.1.1</version>
     <packaging>jar</packaging>
 
     <parent>
         <groupId>com.qmth.themis</groupId>
         <artifactId>themis-service</artifactId>
-        <version>1.1.0</version>
+        <version>1.1.1</version>
     </parent>
 
     <dependencies>

+ 6 - 4
themis-task/src/main/java/com/qmth/themis/task/quartz/service/impl/QuartzLogicServiceImpl.java

@@ -5,13 +5,14 @@ import com.qmth.themis.business.cache.ExamRecordCacheUtil;
 import com.qmth.themis.business.cache.bean.ExamActivityCacheBean;
 import com.qmth.themis.business.cache.bean.ExamCacheBean;
 import com.qmth.themis.business.cache.bean.ExamStudentCacheBean;
+import com.qmth.themis.business.constant.SpringContextHolder;
+import com.qmth.themis.business.dto.MqDto;
 import com.qmth.themis.business.entity.TOeExamRecord;
 import com.qmth.themis.business.enums.ExamRecordStatusEnum;
 import com.qmth.themis.business.enums.FinishTypeEnum;
-import com.qmth.themis.business.service.TEExamActivityService;
-import com.qmth.themis.business.service.TEExamService;
-import com.qmth.themis.business.service.TEExamStudentService;
-import com.qmth.themis.business.service.TOeExamRecordService;
+import com.qmth.themis.business.enums.MqTagEnum;
+import com.qmth.themis.business.service.*;
+import com.qmth.themis.business.util.MqUtil;
 import com.qmth.themis.task.quartz.service.QuartzLogicService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -20,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 /**

+ 4 - 0
themis-task/src/main/java/com/qmth/themis/task/start/StartRunning.java

@@ -119,6 +119,10 @@ public class StartRunning implements CommandLineRunner {
         //考试断点
         rocketMessageConsumer.setRocketMQConsumer(nameServer, dictionaryConfig.mqConfigDomain().getMap().get(MqGroupEnum.EXAM_BREAK_GROUP.name()), dictionaryConfig.mqConfigDomain().getTopic(), MqTagEnum.EXAM_BREAK.name(), MessageModel.CLUSTERING, SpringContextHolder.getBean(ExamBreakConcurrentlyImpl.class));
         rocketMessageConsumer.setRocketMQConsumer(nameServer, dictionaryConfig.mqConfigDomain().getMap().get(MqGroupEnum.EXAM_BREAK_DELAY_GROUP.name()), dictionaryConfig.mqConfigDomain().getTopic(), MqTagEnum.EXAM_BREAK_DELAY.name(), MessageModel.CLUSTERING, SpringContextHolder.getBean(ExamBreakDelayConcurrentlyImpl.class));
+        /**
+         * tencent video mq start
+         */
+        rocketMessageConsumer.setRocketMQConsumer(nameServer, dictionaryConfig.mqConfigDomain().getMap().get(MqGroupEnum.TENCENT_VIDEO_GROUP.name()), dictionaryConfig.mqConfigDomain().getTopic(), MqTagEnum.TENCENT_VIDEO.name(), MessageModel.CLUSTERING, SpringContextHolder.getBean(TencentVideoConcurrentlyImpl.class));
         SystemConstant.initTempFiles();
         log.info("服务器启动时执行 end");
     }

+ 7 - 0
themis-task/src/main/resources/application.properties

@@ -199,10 +199,17 @@ mq.config.map.EXAM_BREAK_GROUP=themis-group-exam-examBreak
 mq.config.map.EXAM_BREAK_DELAY_GROUP=themis-group-exam-examBreakDelay
 mq.config.map.WEBSOCKET_OE_GROUP=themis-group-exam-websocketOe
 mq.config.map.WEBSOCKET_OE_MOBILE_GROUP=themis-group-exam-websocketOeMobile
+mq.config.map.TENCENT_VIDEO_GROUP=themis-group-exam-tencentVideo
 
 #\u817E\u8BAF\u4E91\u914D\u7F6E
 tencentyun.sdk.appId=1400411036
 tencentyun.sdk.key=d78004c94473cb1cf78af33d333e18b731132e527e829e44e2ab133945243b11
 tencentyun.sdk.urls=https://live1.qmth.com.cn,https://live2.qmth.com.cn,https://live3.qmth.com.cn,https://live4.qmth.com.cn,https://live5.qmth.com.cn,https://live6.qmth.com.cn
+tencentyun.sdk.service=vod
+tencentyun.sdk.queryUrl=${tencentyun.sdk.service}.tencentcloudapi.com
+tencentyun.sdk.secretId=AKIDKUO2PVLuCDxQcW9VaOuA8pFOGq9BwQdZ
+tencentyun.sdk.secretKey=g5D4dwxhByXrvjGWVDfqkPqzrSmqd9OM
+tencentyun.sdk.vodAppId=1500002365
+tencentyun.sdk.callbackPwd=123456
 
 monitor.config.prefix=oe_test