deason 5 年之前
當前提交
b20a6ce434
共有 66 個文件被更改,包括 6194 次插入0 次删除
  1. 12 0
      .gitignore
  2. 25 0
      examcloud-core-oe-task-api-provider/pom.xml
  3. 189 0
      examcloud-core-oe-task-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/task/controller/ExamCaptureController.java
  4. 72 0
      examcloud-core-oe-task-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/task/controller/bean/GetExamCaptureResultDomain.java
  5. 70 0
      examcloud-core-oe-task-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/task/provider/ExamCaptureCloudServiceProvider.java
  6. 135 0
      examcloud-core-oe-task-base/pom.xml
  7. 152 0
      examcloud-core-oe-task-base/src/main/java/cn/com/qmth/examcloud/core/oe/task/base/ExamCaptureProcessStatisticController.java
  8. 34 0
      examcloud-core-oe-task-base/src/main/java/cn/com/qmth/examcloud/core/oe/task/base/UniqueRuleHolder.java
  9. 20 0
      examcloud-core-oe-task-dao/pom.xml
  10. 114 0
      examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/ExamCaptureQueueRepo.java
  11. 31 0
      examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/ExamCaptureRepo.java
  12. 17 0
      examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/ExamSyncCaptureRepo.java
  13. 218 0
      examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/entity/ExamCaptureEntity.java
  14. 267 0
      examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/entity/ExamCaptureQueueEntity.java
  15. 218 0
      examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/entity/ExamSyncCaptureEntity.java
  16. 45 0
      examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/enums/ExamCaptureQueueStatus.java
  17. 19 0
      examcloud-core-oe-task-service/pom.xml
  18. 32 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamBossService.java
  19. 31 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamCaptureQueueService.java
  20. 50 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamCaptureService.java
  21. 55 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamRecordDataService.java
  22. 13 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamSyncCaptureService.java
  23. 30 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamingSessionService.java
  24. 126 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/CalculateFaceCheckResultInfo.java
  25. 129 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/CompareFaceSyncInfo.java
  26. 296 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/ExamCaptureQueueInfo.java
  27. 83 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/SaveExamCaptureQueueInfo.java
  28. 39 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamBossServiceImpl.java
  29. 84 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamCaptureQueueServiceImpl.java
  30. 671 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamCaptureServiceImpl.java
  31. 176 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamRecordDataServiceImpl.java
  32. 68 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamSyncCaptureServiceImpl.java
  33. 39 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamingSessionServiceImpl.java
  34. 132 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/pipeline/AfterHandInExamExecutor.java
  35. 151 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/pipeline/ClearExamDataCacheExecutor.java
  36. 90 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/pipeline/DataGainExamExecutor.java
  37. 215 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/pipeline/HandInExamExecutor.java
  38. 449 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/pipeline/SyncExamDataExecutor.java
  39. 77 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/worker/BaiduFaceLivenessWorker.java
  40. 84 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/worker/FacePPCompareWorker.java
  41. 31 0
      examcloud-core-oe-task-starter/assembly.xml
  42. 66 0
      examcloud-core-oe-task-starter/pom.xml
  43. 1 0
      examcloud-core-oe-task-starter/shell/start.args
  44. 36 0
      examcloud-core-oe-task-starter/shell/start.sh
  45. 1 0
      examcloud-core-oe-task-starter/shell/start.vmoptions
  46. 18 0
      examcloud-core-oe-task-starter/shell/stop.sh
  47. 71 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/OETaskApp.java
  48. 12 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/Tester.java
  49. 129 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ExamCloudResourceManager.java
  50. 53 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ExamCloudWebMvcConfigurer.java
  51. 149 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessBaiduFaceLivenessAlarmTask.java
  52. 143 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessBaiduFacelivenessTask.java
  53. 158 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessFaceCompareAlarmTask.java
  54. 146 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessFaceCompareQueueTask.java
  55. 92 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/StreamTaskExecutor.java
  56. 44 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/SwaggerConfig.java
  57. 49 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/SystemStartup.java
  58. 5 0
      examcloud-core-oe-task-starter/src/main/resources/application.properties
  59. 1 0
      examcloud-core-oe-task-starter/src/main/resources/classpath.location
  60. 21 0
      examcloud-core-oe-task-starter/src/main/resources/limited.properties
  61. 146 0
      examcloud-core-oe-task-starter/src/main/resources/log4j2.xml
  62. 0 0
      examcloud-core-oe-task-starter/src/main/resources/security.properties
  63. 20 0
      jenkins-dev.sh
  64. 4 0
      jenkins-prod.sh
  65. 20 0
      jenkins-test.sh
  66. 20 0
      pom.xml

+ 12 - 0
.gitignore

@@ -0,0 +1,12 @@
+.project
+.classpath
+.settings
+target/
+.idea/
+*.iml
+*test/
+# Package Files #
+*.jar
+logs/
+
+

+ 25 - 0
examcloud-core-oe-task-api-provider/pom.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>cn.com.qmth.examcloud</groupId>
+		<artifactId>examcloud-core-oe-task</artifactId>
+		<version>2019-SNAPSHOT</version>
+	</parent>
+	<artifactId>examcloud-core-oe-task-api-provider</artifactId>
+
+	<dependencies>
+		<dependency>
+			<groupId>cn.com.qmth.examcloud</groupId>
+			<artifactId>examcloud-core-oe-task-service</artifactId>
+			<version>${examcloud.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>cn.com.qmth.examcloud.rpc</groupId>
+			<artifactId>examcloud-core-oe-task-api</artifactId>
+			<version>${examcloud.version}</version>
+		</dependency>
+	</dependencies>
+
+</project>

+ 189 - 0
examcloud-core-oe-task-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/task/controller/ExamCaptureController.java

@@ -0,0 +1,189 @@
+package cn.com.qmth.examcloud.core.oe.task.controller;
+
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.util.UrlUtil;
+import cn.com.qmth.examcloud.core.oe.task.controller.bean.GetExamCaptureResultDomain;
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureQueueService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamingSessionService;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CompareFaceSyncInfo;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.SaveExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.StudentCacheBean;
+import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+import cn.com.qmth.examcloud.support.examing.ExamingStatus;
+import cn.com.qmth.examcloud.web.filestorage.YunHttpRequest;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+
+/**
+ * @author chenken
+ * @date 2018年9月6日 上午10:14:23
+ * @company QMTH
+ * @description ExamCaptureController.java
+ */
+@Api(tags = "考试抓拍")
+@RestController
+@RequestMapping("${app.api.oe.student.face}/examCaptureQueue")
+public class ExamCaptureController extends ControllerSupport {
+
+    @Autowired
+    private ExamCaptureService examCaptureService;
+    @Autowired
+    private ExamingSessionService examingSessionService;
+    @Autowired
+    private ExamCaptureQueueService examCaptureQueueService;
+    @Autowired
+    private RedisClient redisClient;
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+
+//	private static AES aes = new AES();
+
+    @ApiOperation(value = "同步比较人脸:用于进入考试")
+    @PostMapping("/compareFaceSync")
+    public CompareFaceSyncInfo compareFaceSync(@RequestParam String fileUrl, @RequestParam(required = false) String signIdentifier) {
+        Long startTime = System.currentTimeMillis();
+        User user = getAccessUser();
+        if (StringUtils.isBlank(fileUrl)) {
+            throw new StatusException("301001", "文件Url不能为空");
+        }
+        validateUpyunSign(signIdentifier, fileUrl, user.getUserId());
+
+        StudentCacheBean studentCache = CacheHelper.getStudent(user.getUserId());
+        String baseFaceToken = studentCache.getFaceToken();
+        if (StringUtils.isBlank(baseFaceToken)) {
+            throw new StatusException("301002", "学生底照的faceToken为空");
+        }
+//		fileUrl = aes.decrypt(fileUrl);
+        fileUrl = UrlUtil.decode(fileUrl);
+        CompareFaceSyncInfo compareFaceSyncInfo = examCaptureService.compareFaceSyncByFileUrl(user.getUserId(), baseFaceToken, fileUrl);
+
+        //将人脸同步比较的结果临时存储到redis中.开考后会清除
+        String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
+        compareFaceSyncInfo.setFileName(fileName);
+        compareFaceSyncInfo.setFileUrl(fileUrl);
+        compareFaceSyncInfo.setProcessTime(System.currentTimeMillis() - startTime);
+
+        redisClient.set(Constants.FACE_SYNC_COMPARE_RESULT_PREFIX + user.getUserId(), compareFaceSyncInfo, 5 * 60);
+        return compareFaceSyncInfo;
+    }
+
+    /**
+     * 校验又拍云签名
+     *
+     * @param signIdentifier 签名标识
+     * @param fileUrl        文件路径
+     * @param userId         用户id
+     */
+    private void validateUpyunSign(String signIdentifier, String fileUrl, Long userId) {
+//		if (StringUtils.isBlank(signIdentifier)){
+//			throw new StatusException("300001", "签名标识不能为空");
+//		}
+        if (!StringUtils.isBlank(signIdentifier)) {
+            String upyunSignRedisKey = Constants.EXAM_CAPTURE_PHOTO_UPYUN_SIGN_PREFIX + userId + "_" + signIdentifier;
+            YunHttpRequest upYunHttpRequest = redisClient.get(upyunSignRedisKey, YunHttpRequest.class);
+            if (upYunHttpRequest == null) {
+                throw new StatusException("301003", "无效的请求,请检查签名标识");
+            }
+            if (!upYunHttpRequest.getAccessUrl().equals(fileUrl)) {
+                throw new StatusException("301004", "文件路径格式不正确");
+            }
+        }
+
+    }
+
+    @ApiOperation(value = "获取抓拍结果")
+    @GetMapping("/getExamCaptureResult")
+    public GetExamCaptureResultDomain getExamCaptureResult(@RequestParam Long examRecordDataId, @RequestParam String fileName) {
+        if (null == examRecordDataId) {
+            throw new StatusException("301005", "examRecordDataId不能为空");
+        }
+        if (StringUtils.isBlank(fileName)) {
+            throw new StatusException("301006", "fileName不能为空");
+        }
+        ExamCaptureEntity examCaptureEntity = examCaptureService.getExamCaptureResult(examRecordDataId, fileName);
+        GetExamCaptureResultDomain domain = new GetExamCaptureResultDomain();
+        if (examCaptureEntity == null) {
+            domain.setIsCompleted(false);
+        } else {
+            domain.setIsCompleted(true);
+            domain.setIsPass(examCaptureEntity.getIsPass());
+            domain.setIsStranger(examCaptureEntity.getIsStranger());
+            domain.setExamRecordDataId(examRecordDataId);
+            domain.setFileName(fileName);
+        }
+        return domain;
+    }
+
+    @ApiOperation(value = "保存考试抓拍照片队列")
+    @PostMapping("/uploadExamCapture")
+    public String uploadExamCapture(@RequestBody SaveExamCaptureQueueInfo saveExamCaptureQueueInfo) {
+        User user = getAccessUser();
+
+        ExamingSession examingSession = examingSessionService.getExamingSession(user.getUserId());
+        //不存在考试会话,或者会话状态不正确,不允许上传图片直接返回
+        if (null == examingSession || examingSession.getExamingStatus() == ExamingStatus.INFORMAL) {
+            return null;
+        }
+
+        //参数校验
+        if (saveExamCaptureQueueInfo == null) {
+            throw new StatusException("301001", "对象不能为空");
+        }
+
+        Long examRecordDataId = saveExamCaptureQueueInfo.getExamRecordDataId();
+        if (null == examRecordDataId) {
+            throw new StatusException("301002", "examRecordDataId不能为空");
+        }
+
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+        if (null == examRecordData) {
+            throw new StatusException("301004", "无效的考试记录id");
+        }
+        //非考试中不允许上传图片
+        if (ExamRecordStatus.EXAM_ING != examRecordData.getExamRecordStatus()) {
+            return null;
+        }
+
+        if (StringUtils.isBlank(saveExamCaptureQueueInfo.getFileUrl())) {
+            throw new StatusException("301005", "fileUrl不能为空");
+        }
+
+        //校验虚拟摄像头格式,必须 是Json数组,如果格式不正确,则置为null
+        if (StringUtils.isNoneBlank(saveExamCaptureQueueInfo.getCameraInfos())) {
+            try {
+                JSONArray jsonArray = new JSONArray(saveExamCaptureQueueInfo.getCameraInfos());
+                if (jsonArray.length() == 0) {
+                    saveExamCaptureQueueInfo.setCameraInfos(null);
+                }
+            } catch (JSONException e) {
+                saveExamCaptureQueueInfo.setCameraInfos(null);
+                log.error("ExamCaptureQueueController-uploadExamCapture-004:虚拟摄像头信息格式不正确", e);
+            }
+        }
+        validateUpyunSign(saveExamCaptureQueueInfo.getSignIdentifier(), saveExamCaptureQueueInfo.getFileUrl(), user.getUserId());
+
+        return examCaptureQueueService.saveExamCaptureQueue(saveExamCaptureQueueInfo, user.getUserId());
+    }
+}

+ 72 - 0
examcloud-core-oe-task-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/task/controller/bean/GetExamCaptureResultDomain.java

@@ -0,0 +1,72 @@
+package cn.com.qmth.examcloud.core.oe.task.controller.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class GetExamCaptureResultDomain implements JsonSerializable {
+	
+	
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -7258224010169602773L;
+	
+	/**
+	 * 是否处理完成
+	 */
+	private Boolean isCompleted;
+
+	private Long examRecordDataId;
+	
+	/**
+	 * 是否通过
+	 */
+	private Boolean isPass;
+	
+	/**
+	 * 是否有陌生人
+	 */
+	private Boolean isStranger;
+	
+	private String fileName;
+	
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Boolean getIsPass() {
+		return isPass;
+	}
+
+	public void setIsPass(Boolean isPass) {
+		this.isPass = isPass;
+	}
+
+	public Boolean getIsStranger() {
+		return isStranger;
+	}
+
+	public void setIsStranger(Boolean isStranger) {
+		this.isStranger = isStranger;
+	}
+
+	public String getFileName() {
+		return fileName;
+	}
+
+	public void setFileName(String fileName) {
+		this.fileName = fileName;
+	}
+
+	public Boolean getIsCompleted() {
+		return isCompleted;
+	}
+
+	public void setIsCompleted(Boolean isCompleted) {
+		this.isCompleted = isCompleted;
+	}
+
+}

+ 70 - 0
examcloud-core-oe-task-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/task/provider/ExamCaptureCloudServiceProvider.java

@@ -0,0 +1,70 @@
+package cn.com.qmth.examcloud.core.oe.task.provider;
+
+
+import cn.com.qmth.examcloud.core.oe.task.api.ExamCaptureCloudService;
+import cn.com.qmth.examcloud.core.oe.task.api.request.ExistUnhandledCaptureQueueReq;
+import cn.com.qmth.examcloud.core.oe.task.api.request.SaveExamCaptureSyncCompareResultReq;
+import cn.com.qmth.examcloud.core.oe.task.api.request.UpdateExamCaptureQueuePriorityReq;
+import cn.com.qmth.examcloud.core.oe.task.api.response.ExistUnhandledCaptureQueueResp;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamSyncCaptureService;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @Description
+ * @Author lideyin
+ * @Date 2019/10/12 11:01
+ * @Version 1.0
+ */
+@RestController
+@RequestMapping("${$rmp.cloud.oe.student.face}/examCapture")
+public class ExamCaptureCloudServiceProvider extends ControllerSupport
+        implements ExamCaptureCloudService {
+
+    @Autowired
+    private ExamSyncCaptureService examSyncCaptureService;
+    @Autowired
+    private ExamCaptureQueueRepo examCaptureQueueRepo;
+
+    @Override
+    @ApiOperation(value = "保存抓拍照片的同步比较结果")
+    @PostMapping("/saveExamCaptureSyncCompareResult")
+    public void saveExamCaptureSyncCompareResult(@RequestBody SaveExamCaptureSyncCompareResultReq request) {
+        examSyncCaptureService.saveExamCaptureSyncCompareResult(request.getStudentId(),
+                request.getExamRecordDataId());
+    }
+
+    /**
+     * @param req 更新优先级请求参数
+     * @description 更新考试抓拍照片在队列优中的先级
+     * @date 2019/7/31 16:46
+     * @author lideyin
+     */
+    @Override
+    @ApiOperation(value = "更新考试抓拍照片在队列优中的先级")
+    @PostMapping("/updateExamCaptureQueuePriority")
+    public void updateExamCaptureQueuePriority(@RequestBody UpdateExamCaptureQueuePriorityReq req) {
+        examCaptureQueueRepo.updateExamCaptureQueuePriority(req.getPriority(), req.getExamRecordDataId());
+    }
+
+    /**
+     * 是否存在未处理的图片抓拍队列(处理完成的队列数据都会清理掉)
+     *
+     * @param req
+     * @return
+     */
+    @Override
+    @ApiOperation(value = "是否存在未处理的图片抓拍队列")
+    @PostMapping("/existsUnhandledCaptureQueue")
+    public ExistUnhandledCaptureQueueResp existsUnhandledCaptureQueue(@RequestBody ExistUnhandledCaptureQueueReq req) {
+        ExistUnhandledCaptureQueueResp resp = new ExistUnhandledCaptureQueueResp();
+        resp.setExist(examCaptureQueueRepo.existsUnhandledByExamRecordDataId(req.getExamRecordDataId()) != null);
+        return resp;
+    }
+}

+ 135 - 0
examcloud-core-oe-task-base/pom.xml

@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cn.com.qmth.examcloud</groupId>
+        <artifactId>examcloud-core-oe-task</artifactId>
+        <version>2019-SNAPSHOT</version>
+    </parent>
+    <artifactId>examcloud-core-oe-task-base</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-mongodb</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud</groupId>
+            <artifactId>examcloud-web</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud</groupId>
+            <artifactId>examcloud-support</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.com.qmth.examcloud.rpc</groupId>
+            <artifactId>examcloud-exchange-inner-api-client</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud.rpc</groupId>
+            <artifactId>examcloud-core-basic-api-client</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud.rpc</groupId>
+            <artifactId>examcloud-core-examwork-api-client</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud.rpc</groupId>
+            <artifactId>examcloud-core-questions-api-client</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud.rpc</groupId>
+            <artifactId>examcloud-core-oe-admin-api-client</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud.rpc</groupId>
+            <artifactId>examcloud-core-oe-student-api-client</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud.rpc</groupId>
+            <artifactId>examcloud-ws-api-client</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud.rpc</groupId>
+            <artifactId>examcloud-core-marking-api-client</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud.rpc</groupId>
+            <artifactId>examcloud-global-api</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpmime</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+            <version>20140107</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hibernate</groupId>
+            <artifactId>hibernate-validator</artifactId>
+            <version>5.3.6.Final</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-okhttp</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>logging-interceptor</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.upyun</groupId>
+            <artifactId>java-sdk</artifactId>
+            <version>3.16</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baidu.aip</groupId>
+            <artifactId>java-sdk</artifactId>
+            <version>4.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.esotericsoftware</groupId>
+            <artifactId>reflectasm</artifactId>
+            <version>1.11.3</version>
+        </dependency>
+    </dependencies>
+
+
+</project>

+ 152 - 0
examcloud-core-oe-task-base/src/main/java/cn/com/qmth/examcloud/core/oe/task/base/ExamCaptureProcessStatisticController.java

@@ -0,0 +1,152 @@
+package cn.com.qmth.examcloud.core.oe.task.base;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @Description 考试照片统计控制器
+ * @Author lideyin
+ * @Date 2019/9/9 17:05
+ * @Version 1.0
+ */
+public class ExamCaptureProcessStatisticController {
+    //人脸比对处理数量
+    private static AtomicInteger faceCompareCount = new AtomicInteger(0);
+    //人脸比对处理失败数量
+    private static AtomicInteger faceCompareFailedCount = new AtomicInteger(0);
+
+    private static Object facePPLock = new Object();
+    private static Object faceLivenessLock = new Object();
+
+    /**
+     * 人脸比对总数量自增
+     */
+    public static void increaseFaceCompareCount() {
+        synchronized (facePPLock) {
+            faceCompareCount.incrementAndGet();
+        }
+    }
+
+    /**
+     * 人脸比对错误数量自增
+     */
+    public static void increaseFaceCompareFailedCount() {
+        synchronized (facePPLock) {
+            faceCompareFailedCount.incrementAndGet();
+        }
+    }
+
+    /**
+     * 同步重置人脸比对所有数量
+     */
+    public static void resetAllFaceCompareCount() {
+        synchronized (facePPLock) {
+            faceCompareCount.set(0);
+            faceCompareFailedCount.set(0);
+        }
+    }
+
+    /**
+     * 人脸比对总数量
+     *
+     * @return int
+     */
+    public static int getFaceCompareCount() {
+        synchronized (facePPLock) {
+            return faceCompareCount.get();
+        }
+    }
+
+    /**
+     * 人脸比对失败数量
+     *
+     * @return int
+     */
+    public static int getFaceCompareFailedCount() {
+        synchronized (facePPLock) {
+            return faceCompareFailedCount.get();
+        }
+    }
+
+    /**
+     * 人脸比对失败率
+     *
+     * @return int
+     */
+    public static int getFaceCompareFailurePercent() {
+        synchronized (facePPLock) {
+            if (faceCompareCount.get() == 0) {
+                return 0;
+            }
+            return faceCompareFailedCount.get() * 100 / faceCompareCount.get();
+        }
+    }
+
+    //活体检测处理数量
+    private static AtomicInteger faceLivenessDetectCount = new AtomicInteger(0);
+    //活体检测处理失败数量
+    private static AtomicInteger faceLivenessDetectFailedCount = new AtomicInteger(0);
+
+    /**
+     * 活体检测总数量自增
+     */
+    public static void increaseFaceLivenessDetectCount() {
+        synchronized (faceLivenessLock) {
+            faceLivenessDetectCount.incrementAndGet();
+        }
+    }
+
+    /**
+     * 活体检测错误数量自增
+     */
+    public static void increaseFaceLivenessDetectFailedCount() {
+        synchronized (faceLivenessLock) {
+            faceLivenessDetectFailedCount.incrementAndGet();
+        }
+    }
+
+    /**
+     * 重置活体检测所有数量
+     */
+    public static void resetAllFaceLivenessDetectCount() {
+        synchronized (faceLivenessLock) {
+            faceLivenessDetectCount.set(0);
+            faceLivenessDetectFailedCount.set(0);
+        }
+    }
+
+    /**
+     * 活体检测总数量
+     *
+     * @return
+     */
+    public static int getFaceLivenessDetectCount() {
+        synchronized (faceLivenessLock) {
+            return faceLivenessDetectCount.get();
+        }
+    }
+
+    /**
+     * 活体检测失败数量
+     *
+     * @return
+     */
+    public static int getFaceLivenessDetectFailedCount() {
+        synchronized (faceLivenessLock) {
+            return faceLivenessDetectFailedCount.get();
+        }
+    }
+
+    /**
+     * 活体检测失败率
+     *
+     * @return int
+     */
+    public static int getFaceLivenessDetectFailurePercent() {
+        synchronized (faceLivenessLock) {
+            if (faceLivenessDetectCount.get() == 0) {
+                return 0;
+            }
+            return faceLivenessDetectFailedCount.get() * 100 / faceLivenessDetectCount.get();
+        }
+    }
+}

+ 34 - 0
examcloud-core-oe-task-base/src/main/java/cn/com/qmth/examcloud/core/oe/task/base/UniqueRuleHolder.java

@@ -0,0 +1,34 @@
+package cn.com.qmth.examcloud.core.oe.task.base;
+
+import cn.com.qmth.examcloud.web.jpa.UniqueRule;
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * @Description 唯一约束holder 状态码范围110XXX
+ * @Author lideyin
+ * @Date 2019/9/7 11:43
+ * @Version 1.0
+ */
+public class UniqueRuleHolder {
+    private static List<UniqueRule> LIST = Lists.newArrayList();
+
+    public static List<UniqueRule> getUniqueRuleList() {
+        return LIST;
+    }
+
+    static {
+        // ResourceEntity
+        LIST.add(new UniqueRule("IDX_E_O_E_A_001", "110001", "考试审核记录已存在"));
+        LIST.add(new UniqueRule("IDX_E_O_E_C_001", "110002", "抓拍照片处理结果已存在"));
+        LIST.add(new UniqueRule("IDX_E_O_E_C_Q_001", "110003", "拍拍照片队列已存在"));
+        LIST.add(new UniqueRule("IDX_E_O_E_F_A_T_001", "110004", "文件作答结果已存在"));
+        LIST.add(new UniqueRule("IDX_E_O_E_R_002", "110005", "考生已存在进行中的考试"));
+        LIST.add(new UniqueRule("IDX_E_O_E_R_D_001", "110006", "考试记录已存在"));
+        LIST.add(new UniqueRule("IDX_E_O_E_R_4_M_001", "110007", "待阅卷的考试记录已存在"));
+        LIST.add(new UniqueRule("IDX_E_O_E_S_001", "110008", "考试分数已存在"));
+        LIST.add(new UniqueRule("IDX_E_O_E_S_001", "110009", "考生已存在"));
+        LIST.add(new UniqueRule("IDX_E_O_E_O_S_H_001", "110010", "机构推分队列已存在"));
+    }
+}

+ 20 - 0
examcloud-core-oe-task-dao/pom.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>cn.com.qmth.examcloud</groupId>
+        <artifactId>examcloud-core-oe-task</artifactId>
+        <version>2019-SNAPSHOT</version>
+    </parent>
+    <artifactId>examcloud-core-oe-task-dao</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.com.qmth.examcloud</groupId>
+            <artifactId>examcloud-core-oe-task-base</artifactId>
+            <version>${examcloud.version}</version>
+        </dependency>
+    </dependencies>
+
+</project>

+ 114 - 0
examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/ExamCaptureQueueRepo.java

@@ -0,0 +1,114 @@
+package cn.com.qmth.examcloud.core.oe.task.dao;
+
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.task.dao.enums.ExamCaptureQueueStatus;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import javax.transaction.Transactional;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @Description 抓拍照片队列
+ * @Author lideyin
+ * @Date 2019/12/10 14:04
+ * @Version 1.0
+ */
+@Repository
+public interface ExamCaptureQueueRepo extends JpaRepository<ExamCaptureQueueEntity, Long>, JpaSpecificationExecutor<ExamCaptureQueueEntity> {
+
+    /**
+     * @param limit           一次取值数量
+     * @param processBatchNum 批次号
+     * @return List<ExamCaptureQueueEntity>
+     * @description 根据处理批次号查询没有处理过的数据或者是处理失败的数据,按优先级倒序,创建时间升序
+     * @author lideyin
+     * @date 2019/7/31 16:40
+     */
+    @Query(value = "select * from ec_oet_exam_capture_queue "
+            + " where status IN ('PENDING','PROCESS_FACE_COMPARE_FAILED')  " +
+            " and (process_batch_num is null or (process_batch_num is not null and process_batch_num!=?2))"
+            + " order by priority desc,id asc limit ?1", nativeQuery = true)
+    List<ExamCaptureQueueEntity> findNeedFaceCompareExamCaptureQueuesLimitByProcessBatchNum(Integer limit, String processBatchNum);
+
+    /**
+     * 查询人脸比对处理完成,需要百度活体检测的数据,按优先级倒序,创建时间升序
+     *
+     * @param limit           数量
+     * @param processBatchNum 批次号
+     * @return List<ExamCaptureQueueEntity>
+     */
+    @Query(value = "select * from ec_oet_exam_capture_queue "
+            + " where status IN ('PROCESS_FACE_COMPARE_COMPLETE','PROCESS_FACELIVENESS_FAILED') and process_batch_num!=?2  "
+            + " order by priority desc,id asc limit ?1", nativeQuery = true)
+    List<ExamCaptureQueueEntity> findNeedFacelivenessDetectExamCaptureQueuesLimit(Integer limit, String processBatchNum);
+
+    List<ExamCaptureQueueEntity> findByStatusOrderByCreationTimeAsc(ExamCaptureQueueStatus status);
+
+    List<ExamCaptureQueueEntity> findByExamRecordDataId(Long examRecordDataId);
+
+    /**
+     * 是否存在未处理的图片抓拍队列(处理完成的队列数据都会清理掉)
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    @Query(value = "select * from ec_oet_exam_capture_queue where exam_record_data_id=?1 limit 1", nativeQuery = true)
+    ExamCaptureQueueEntity existsUnhandledByExamRecordDataId(Long examRecordDataId);
+
+    /**
+     * 修改数据状态为处理中
+     *
+     * @param id 主键ID
+     */
+    @Transactional
+    @Modifying
+    @Query(value = "update ec_oet_exam_capture_queue set status = 'PROCESSING' where id = ?1", nativeQuery = true)
+    void updateExamCaptureQueueStatusWithProcessing(Long id);
+
+
+    /**
+     * @param priority         优先级(值越大,优先级越高)
+     * @param examRecordDataId 考试记录id
+     * @description 更新考试抓拍队列优先级
+     * @date 2019/7/31 16:46
+     * @author lideyin
+     */
+    @Transactional
+    @Modifying
+    @Query(value = "update ec_oet_exam_capture_queue set priority=?1 where exam_record_data_id=?2", nativeQuery = true)
+    void updateExamCaptureQueuePriority(int priority, Long examRecordDataId);
+
+    /**
+     * @param captureQueueId
+     * @param errorMsg
+     * @param status
+     * @param batchNumber
+     * @param updateDate
+     */
+    @Transactional
+    @Modifying
+    @Query(value = "update ec_oet_exam_capture_queue set error_msg=?2,`status`=?3,process_batch_num=?4,error_num=error_num+1,update_time=?5 where id=?1", nativeQuery = true)
+    void saveExamCaptureQueueEntityByFailed(Long captureQueueId, String errorMsg, String status, String batchNumber,
+                                            Date updateDate);
+
+    @Transactional
+    @Modifying
+    @Query(value = "update ec_oet_exam_capture_queue set is_pass=?2,is_stranger=?3,`status`=?4,face_compare_result=?5,face_compare_start_time=?6,update_time=?7 where id=?1", nativeQuery = true)
+    void saveExamCaptureQueueEntityBySuccessful(Long captureQueueId, Boolean isPass, Boolean isStranger, String status,
+                                                String faceCompareResult, Long faceCompareStartTime, Date updateDate);
+
+    /**
+     * 更新批次号
+     * @param captureQueueId 队列id
+     * @param batchNum 批次号
+     */
+    @Transactional
+    @Modifying
+    @Query(value = "update ec_oet_exam_capture_queue set process_batch_num=?2 where id=?1", nativeQuery = true)
+    void updateProcessBatchNum(Long captureQueueId, String batchNum);
+}

+ 31 - 0
examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/ExamCaptureRepo.java

@@ -0,0 +1,31 @@
+package cn.com.qmth.examcloud.core.oe.task.dao;
+
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * @Description 照片处理结果
+ * @Author lideyin
+ * @Date 2019/12/10 14:04
+ * @Version 1.0
+ */
+@Repository
+public interface ExamCaptureRepo extends JpaRepository<ExamCaptureEntity, Long>, JpaSpecificationExecutor<ExamCaptureEntity> {
+	
+	/**
+	 * propagation = Propagation.REQUIRES_NEW 
+	 * 原因:A事务运行中,B事务插入数据,A事务会出现不能读取最新数据的问题
+	 * @param examRecordDataId
+	 * @return
+	 */
+	@Transactional(propagation = Propagation.REQUIRES_NEW)
+	List<ExamCaptureEntity> findByExamRecordDataId(Long examRecordDataId);
+    
+    ExamCaptureEntity findByExamRecordDataIdAndFileName(Long examRecordDataId, String fileName);
+}

+ 17 - 0
examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/ExamSyncCaptureRepo.java

@@ -0,0 +1,17 @@
+package cn.com.qmth.examcloud.core.oe.task.dao;
+
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamSyncCaptureEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @Description 同步抓拍照片
+ * @Author lideyin
+ * @Date 2019/12/10 14:05
+ * @Version 1.0
+ */
+@Repository
+public interface ExamSyncCaptureRepo extends JpaRepository<ExamSyncCaptureEntity, Long>, JpaSpecificationExecutor<ExamSyncCaptureEntity> {
+    ExamSyncCaptureEntity findByExamRecordDataId(Long examRecordId);
+}

+ 218 - 0
examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/entity/ExamCaptureEntity.java

@@ -0,0 +1,218 @@
+package cn.com.qmth.examcloud.core.oe.task.dao.entity;
+
+import cn.com.qmth.examcloud.web.jpa.JpaEntity;
+import org.hibernate.annotations.DynamicInsert;
+
+import javax.persistence.*;
+
+/**
+ * @Description 照片处理结果队列
+ * @Author lideyin
+ * @Date 2019/12/10 14:01
+ * @Version 1.0
+ */
+@Entity
+@Table(name = "ec_oet_exam_capture", indexes = {
+        @Index(name = "IDX_E_O_E_C_001", columnList = "examRecordDataId, fileName", unique = true),
+        @Index(name = "IDX_E_O_E_C_002", columnList = "examRecordDataId")
+})
+@DynamicInsert
+public class ExamCaptureEntity extends JpaEntity {
+
+    /**
+     *
+     */
+    private static final long serialVersionUID = -6162329876793785782L;
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+    /**
+     * ec_oe_exam_record_data  ID
+     */
+    private Long examRecordDataId;
+    /**
+     * 文件URL
+     */
+    private String fileUrl;
+
+    private String fileName;
+
+    /**
+     * 比较是否通过
+     */
+    @Column(name = "is_pass")
+    private Boolean isPass;
+
+    /**
+     * 人脸比较返回信息
+     */
+    @Column(name = "face_compare_result", length = 2000)
+    private String faceCompareResult;
+
+    /**
+     * 是否有陌生人
+     * 就是摄像头拍到不止考生一人
+     */
+    @Column(name = "is_stranger")
+    private Boolean isStranger;
+    /**
+     * 百度人脸关键点坐标结果
+     */
+    @Column(name = "landmark",length = 4000)
+    private String landmark;
+    /**
+     * 百度在线活体检测结果
+     */
+    @Column(name = "faceliveness_result", length = 2000)
+    private String facelivenessResult;
+
+    /**
+     * 从进入队列到处理完成的时间
+     */
+    private Long usedTime;
+
+    /**
+     * 从开始处理到处理完成的时间
+     */
+    private Long processTime;
+
+
+    /**
+     * 是否存在虚拟摄像头
+     */
+    @Column(name = "has_virtual_camera")
+    private Boolean hasVirtualCamera;
+
+    /**
+     * 摄像头信息
+     */
+    @Column(name = "camera_infos", length = 800)
+    private String cameraInfos;
+
+    /**
+     * 其他信息
+     */
+    @Column(name = "ext_msg", length = 800)
+    private String extMsg;
+
+    public ExamCaptureEntity() {
+    }
+
+    public ExamCaptureEntity(ExamCaptureQueueEntity examCaptureQueueEntity) {
+        this.examRecordDataId = examCaptureQueueEntity.getExamRecordDataId();
+        this.fileUrl = examCaptureQueueEntity.getFileUrl();
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public String getFileUrl() {
+        return fileUrl;
+    }
+
+    public void setFileUrl(String fileUrl) {
+        this.fileUrl = fileUrl;
+    }
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    public String getFaceCompareResult() {
+        return faceCompareResult;
+    }
+
+    public void setFaceCompareResult(String faceCompareResult) {
+        this.faceCompareResult = faceCompareResult;
+    }
+
+    public Boolean getIsPass() {
+        return isPass;
+    }
+
+    public void setIsPass(Boolean isPass) {
+        this.isPass = isPass;
+    }
+
+    public Boolean getIsStranger() {
+        return isStranger;
+    }
+
+    public void setIsStranger(Boolean isStranger) {
+        this.isStranger = isStranger;
+    }
+
+    public String getLandmark() {
+        return landmark;
+    }
+
+    public void setLandmark(String landmark) {
+        this.landmark = landmark;
+    }
+
+    public Long getUsedTime() {
+        return usedTime;
+    }
+
+    public void setUsedTime(Long usedTime) {
+        this.usedTime = usedTime;
+    }
+
+    public String getFacelivenessResult() {
+        return facelivenessResult;
+    }
+
+    public void setFacelivenessResult(String facelivenessResult) {
+        this.facelivenessResult = facelivenessResult;
+    }
+
+    public Long getProcessTime() {
+        return processTime;
+    }
+
+    public void setProcessTime(Long processTime) {
+        this.processTime = processTime;
+    }
+
+    public Boolean getHasVirtualCamera() {
+        return hasVirtualCamera;
+    }
+
+    public void setHasVirtualCamera(Boolean hasVirtualCamera) {
+        this.hasVirtualCamera = hasVirtualCamera;
+    }
+
+    public String getCameraInfos() {
+        return cameraInfos;
+    }
+
+    public void setCameraInfos(String cameraInfos) {
+        this.cameraInfos = cameraInfos;
+    }
+
+    public String getExtMsg() {
+        return extMsg;
+    }
+
+    public void setExtMsg(String extMsg) {
+        this.extMsg = extMsg;
+    }
+
+}

+ 267 - 0
examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/entity/ExamCaptureQueueEntity.java

@@ -0,0 +1,267 @@
+package cn.com.qmth.examcloud.core.oe.task.dao.entity;
+
+import cn.com.qmth.examcloud.core.oe.task.dao.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.web.jpa.WithIdJpaEntity;
+
+import javax.persistence.*;
+
+/**
+ * @Description 照片抓拍队列
+ * @Author lideyin
+ * @Date 2019/12/10 14:00
+ * @Version 1.0
+ */
+@Entity
+@Table(name = "ec_oet_exam_capture_queue", indexes = {
+        @Index(name = "IDX_E_O_E_C_Q_001", columnList = "examRecordDataId,fileName", unique = true),
+        @Index(name = "IDX_E_O_E_C_Q_002", columnList = "status,errorNum")
+})
+public class ExamCaptureQueueEntity extends WithIdJpaEntity {
+    /**
+     *
+     */
+    private static final long serialVersionUID = 4094671807731989565L;
+
+    private Long studentId;
+
+    /**
+     * ec_oe_exam_record_data  ID
+     */
+    private Long examRecordDataId;
+
+
+    /**
+     * 底照Token
+     */
+    private String baseFaceToken;
+
+    /**
+     * 文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 文件名称
+     */
+    private String fileName;
+
+    /**
+     * 状态
+     */
+    @Enumerated(EnumType.STRING)
+    private ExamCaptureQueueStatus status;
+
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+
+    /**
+     * 错误次数
+     */
+    private Integer errorNum;
+
+    /**
+     * 是否存在虚拟摄像头
+     */
+    @Column(name = "has_virtual_camera")
+    private Boolean hasVirtualCamera;
+
+    /**
+     * 摄像头信息  json字符串数组
+     */
+    @Column(name = "camera_infos", length = 800)
+    private String cameraInfos;
+
+    /**
+     * 其他信息
+     * Json格式
+     * {
+     * "":""
+     * }
+     */
+    @Column(name = "ext_msg", length = 800)
+    private String extMsg;
+
+    /**
+     * 队列处理批次号(用户判断某一条数据处理状态)
+     */
+    private String processBatchNum;
+
+    /**
+     * 队列处理的优先级,默认值为0
+     */
+    private int priority = 0;
+
+    /**
+     * 是否有陌生人
+     * 就是摄像头拍到不止考生一人
+     */
+    @Column(name = "is_stranger")
+    private Boolean isStranger;
+    /**
+     * 比较是否通过
+     */
+    @Column(name = "is_pass")
+    private Boolean isPass;
+
+    /**
+     * 人脸比较返回信息
+     */
+    @Column(name = "face_compare_result", length = 2000)
+    private String faceCompareResult;
+    /**
+     * 人脸比对开始时间
+     */
+    private Long faceCompareStartTime;
+
+    /**
+     * 百度在线活体检测结果
+     */
+    @Column(name = "faceliveness_result", length = 2000)
+    private String facelivenessResult;
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public String getFileUrl() {
+        return fileUrl;
+    }
+
+    public void setFileUrl(String fileUrl) {
+        this.fileUrl = fileUrl;
+    }
+
+    public ExamCaptureQueueStatus getStatus() {
+        return status;
+    }
+
+    public void setStatus(ExamCaptureQueueStatus status) {
+        this.status = status;
+    }
+
+    public String getErrorMsg() {
+        return errorMsg;
+    }
+
+    public void setErrorMsg(String errorMsg) {
+        this.errorMsg = errorMsg;
+    }
+
+    public String getBaseFaceToken() {
+        return baseFaceToken;
+    }
+
+    public void setBaseFaceToken(String baseFaceToken) {
+        this.baseFaceToken = baseFaceToken;
+    }
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    public Integer getErrorNum() {
+        return errorNum;
+    }
+
+    public void setErrorNum(Integer errorNum) {
+        this.errorNum = errorNum;
+    }
+
+    public Long getStudentId() {
+        return studentId;
+    }
+
+    public void setStudentId(Long studentId) {
+        this.studentId = studentId;
+    }
+
+    public Boolean getHasVirtualCamera() {
+        return hasVirtualCamera;
+    }
+
+    public void setHasVirtualCamera(Boolean hasVirtualCamera) {
+        this.hasVirtualCamera = hasVirtualCamera;
+    }
+
+    public String getCameraInfos() {
+        return cameraInfos;
+    }
+
+    public void setCameraInfos(String cameraInfos) {
+        this.cameraInfos = cameraInfos;
+    }
+
+    public String getExtMsg() {
+        return extMsg;
+    }
+
+    public void setExtMsg(String extMsg) {
+        this.extMsg = extMsg;
+    }
+
+    public String getProcessBatchNum() {
+        return processBatchNum;
+    }
+
+    public void setProcessBatchNum(String processBatchNum) {
+        this.processBatchNum = processBatchNum;
+    }
+
+    public int getPriority() {
+        return priority;
+    }
+
+    public void setPriority(int priority) {
+        this.priority = priority;
+    }
+
+    public Boolean getIsStranger() {
+        return isStranger;
+    }
+
+    public void setIsStranger(Boolean stranger) {
+        isStranger = stranger;
+    }
+
+    public Boolean getIsPass() {
+        return isPass;
+    }
+
+    public void setIsPass(Boolean pass) {
+        isPass = pass;
+    }
+
+    public String getFaceCompareResult() {
+        return faceCompareResult;
+    }
+
+    public void setFaceCompareResult(String faceCompareResult) {
+        this.faceCompareResult = faceCompareResult;
+    }
+
+    public Long getFaceCompareStartTime() {
+        return faceCompareStartTime;
+    }
+
+    public void setFaceCompareStartTime(Long faceCompareStartTime) {
+        this.faceCompareStartTime = faceCompareStartTime;
+    }
+
+    public String getFacelivenessResult() {
+        return facelivenessResult;
+    }
+
+    public void setFacelivenessResult(String facelivenessResult) {
+        this.facelivenessResult = facelivenessResult;
+    }
+}

+ 218 - 0
examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/entity/ExamSyncCaptureEntity.java

@@ -0,0 +1,218 @@
+package cn.com.qmth.examcloud.core.oe.task.dao.entity;
+
+import cn.com.qmth.examcloud.web.jpa.JpaEntity;
+import org.hibernate.annotations.DynamicInsert;
+
+import javax.persistence.*;
+
+/**
+ * @Description 网考同步抓拍表
+ * @Author lideyin
+ * @Date 2019/12/6 16:07
+ * @Version 1.0
+ */
+@Entity
+@Table(name = "ec_oet_exam_sync_capture", indexes = {
+        @Index(name = "IDX_E_O_E_C_001", columnList = "examRecordDataId, fileName", unique = true),
+        @Index(name = "IDX_E_O_E_C_002", columnList = "examRecordDataId")
+})
+@DynamicInsert
+public class ExamSyncCaptureEntity extends JpaEntity {
+
+    /**
+     *
+     */
+    private static final long serialVersionUID = -6162329876793785782L;
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+    /**
+     * ec_oe_exam_record_data  ID
+     */
+    private Long examRecordDataId;
+    /**
+     * 文件URL
+     */
+    private String fileUrl;
+
+    private String fileName;
+
+    /**
+     * 比较是否通过
+     */
+    @Column(name = "is_pass")
+    private Boolean isPass;
+
+    /**
+     * 人脸比较返回信息
+     */
+    @Column(name = "face_compare_result", length = 2000)
+    private String faceCompareResult;
+
+    /**
+     * 是否有陌生人
+     * 就是摄像头拍到不止考生一人
+     */
+    @Column(name = "is_stranger")
+    private Boolean isStranger;
+    /**
+     * 百度人脸关键点坐标结果
+     */
+    @Column(name = "landmark", length = 4000)
+    private String landmark;
+    /**
+     * 百度在线活体检测结果
+     */
+    @Column(name = "faceliveness_result", length = 2000)
+    private String facelivenessResult;
+
+    /**
+     * 从进入队列到处理完成的时间
+     */
+    private Long usedTime;
+
+    /**
+     * 从开始处理到处理完成的时间
+     */
+    private Long processTime;
+
+
+    /**
+     * 是否存在虚拟摄像头
+     */
+    @Column(name = "has_virtual_camera")
+    private Boolean hasVirtualCamera;
+
+    /**
+     * 摄像头信息
+     */
+    @Column(name = "camera_infos", length = 800)
+    private String cameraInfos;
+
+    /**
+     * 其他信息
+     */
+    @Column(name = "ext_msg", length = 800)
+    private String extMsg;
+
+    public ExamSyncCaptureEntity() {
+    }
+
+    public ExamSyncCaptureEntity(ExamCaptureQueueEntity examCaptureQueueEntity) {
+        this.examRecordDataId = examCaptureQueueEntity.getExamRecordDataId();
+        this.fileUrl = examCaptureQueueEntity.getFileUrl();
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public String getFileUrl() {
+        return fileUrl;
+    }
+
+    public void setFileUrl(String fileUrl) {
+        this.fileUrl = fileUrl;
+    }
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    public String getFaceCompareResult() {
+        return faceCompareResult;
+    }
+
+    public void setFaceCompareResult(String faceCompareResult) {
+        this.faceCompareResult = faceCompareResult;
+    }
+
+    public Boolean getIsPass() {
+        return isPass;
+    }
+
+    public void setIsPass(Boolean isPass) {
+        this.isPass = isPass;
+    }
+
+    public Boolean getIsStranger() {
+        return isStranger;
+    }
+
+    public void setIsStranger(Boolean isStranger) {
+        this.isStranger = isStranger;
+    }
+
+    public String getLandmark() {
+        return landmark;
+    }
+
+    public void setLandmark(String landmark) {
+        this.landmark = landmark;
+    }
+
+    public Long getUsedTime() {
+        return usedTime;
+    }
+
+    public void setUsedTime(Long usedTime) {
+        this.usedTime = usedTime;
+    }
+
+    public String getFacelivenessResult() {
+        return facelivenessResult;
+    }
+
+    public void setFacelivenessResult(String facelivenessResult) {
+        this.facelivenessResult = facelivenessResult;
+    }
+
+    public Long getProcessTime() {
+        return processTime;
+    }
+
+    public void setProcessTime(Long processTime) {
+        this.processTime = processTime;
+    }
+
+    public Boolean getHasVirtualCamera() {
+        return hasVirtualCamera;
+    }
+
+    public void setHasVirtualCamera(Boolean hasVirtualCamera) {
+        this.hasVirtualCamera = hasVirtualCamera;
+    }
+
+    public String getCameraInfos() {
+        return cameraInfos;
+    }
+
+    public void setCameraInfos(String cameraInfos) {
+        this.cameraInfos = cameraInfos;
+    }
+
+    public String getExtMsg() {
+        return extMsg;
+    }
+
+    public void setExtMsg(String extMsg) {
+        this.extMsg = extMsg;
+    }
+
+}

+ 45 - 0
examcloud-core-oe-task-dao/src/main/java/cn/com/qmth/examcloud/core/oe/task/dao/enums/ExamCaptureQueueStatus.java

@@ -0,0 +1,45 @@
+package cn.com.qmth.examcloud.core.oe.task.dao.enums;
+
+/**
+ * @Description 抓拍照片处理状态
+ * @Author lideyin
+ * @Date 2019/12/10 13:59
+ * @Version 1.0
+ */
+public enum ExamCaptureQueueStatus {
+
+    /**
+     * 待处理
+     */
+    PENDING("待处理"),
+    /**
+     * 处理中
+     */
+    PROCESSING("处理中"),
+    /**
+     * 人脸比对处理完成
+     */
+    PROCESS_FACE_COMPARE_COMPLETE("人脸比对处理完成"),
+    /**
+     * 人脸比对处理失败
+     */
+    PROCESS_FACE_COMPARE_FAILED("人脸比对处理失败"),
+    /**
+     * 活体检测处理失败
+     */
+    PROCESS_FACELIVENESS_FAILED("活体检测处理失败");
+
+    private String desc;
+
+    private ExamCaptureQueueStatus(String desc){
+        this.desc = desc;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    public void setDesc(String desc) {
+        this.desc = desc;
+    }
+}

+ 19 - 0
examcloud-core-oe-task-service/pom.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>cn.com.qmth.examcloud</groupId>
+		<artifactId>examcloud-core-oe-task</artifactId>
+		<version>2019-SNAPSHOT</version>
+	</parent>
+	<artifactId>examcloud-core-oe-task-service</artifactId>
+
+	<dependencies>
+		<dependency>
+			<groupId>cn.com.qmth.examcloud</groupId>
+			<artifactId>examcloud-core-oe-task-dao</artifactId>
+			<version>${examcloud.version}</version>
+		</dependency>
+	</dependencies>
+
+</project>

+ 32 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamBossService.java

@@ -0,0 +1,32 @@
+package cn.com.qmth.examcloud.core.oe.task.service;
+
+import cn.com.qmth.examcloud.support.examing.ExamBoss;
+
+/**
+ * @Description 考试基础信息会话
+ * @Author lideyin
+ * @Date 2019/12/20 15:41
+ * @Version 1.0
+ */
+public interface ExamBossService {
+
+    /**
+     *
+     * @param examStudentId
+     * @param eb
+     */
+    public void saveExamBoss(Long examStudentId, ExamBoss eb);
+
+    /**
+     * 获取
+     * @param examStudentId
+     * @return
+     */
+    public ExamBoss getExamBoss(Long examStudentId);
+
+    /**
+     * 删除
+     * @param examStudentId
+     */
+    public void deleteExamBoss(Long examStudentId);
+}

+ 31 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamCaptureQueueService.java

@@ -0,0 +1,31 @@
+package cn.com.qmth.examcloud.core.oe.task.service;
+
+import cn.com.qmth.examcloud.core.oe.task.dao.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.SaveExamCaptureQueueInfo;
+
+/**
+ * @Description 图片抓拍队列
+ * @Author lideyin
+ * @Date 2019/12/11 14:40
+ * @Version 1.0
+ */
+public interface ExamCaptureQueueService {
+
+    /**
+     * 处理失败时,保存队列信息
+     * @param captureQueueId
+     * @param errorMsg
+     * @param examCaptureQueueStatus
+     */
+    boolean saveExamCaptureQueueEntityByFailed(Long captureQueueId, String errorMsg,
+                                               ExamCaptureQueueStatus examCaptureQueueStatus);
+
+    /**
+     * 保存考试抓拍照片队列
+     * @param saveExamCaptureQueueInfo
+     * @param studentId
+     * @return
+     */
+    String saveExamCaptureQueue(SaveExamCaptureQueueInfo saveExamCaptureQueueInfo, Long studentId);
+
+}

+ 50 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamCaptureService.java

@@ -0,0 +1,50 @@
+package cn.com.qmth.examcloud.core.oe.task.service;
+
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CalculateFaceCheckResultInfo;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CompareFaceSyncInfo;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.ExamCaptureQueueInfo;
+import org.json.JSONException;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * @Description 照片处理结果
+ * @Author lideyin
+ * @Date 2019/12/11 14:45
+ * @Version 1.0
+ */
+public interface ExamCaptureService {
+	void disposeBaiDuFaceLiveness(ExamCaptureQueueInfo examCaptureQueue) throws JSONException;
+
+    @Transactional
+    void saveExamCaptureAndDeleteQueue(ExamCaptureQueueInfo examCaptureQueue);
+
+	/**
+	 * 同步比较人脸:用于进入考试
+	 * @param studentId			学生ID
+	 * @param baseFaceToken		学生底照faceToken
+	 * @param fileUrl				抓拍照片Url
+	 * @return
+	 */
+	CompareFaceSyncInfo compareFaceSyncByFileUrl(Long studentId, String baseFaceToken, String fileUrl);
+
+	/**
+	 * 获取考试抓拍结果
+	 * @param examRecordDataId
+	 * @param fileName
+	 * @return
+	 */
+	ExamCaptureEntity getExamCaptureResult(Long examRecordDataId, String fileName);
+	/**
+	 * 处理单个考试抓拍照片数据
+	 * @param examCaptureQueueInfo
+	 */
+	void disposeFaceCompare(ExamCaptureQueueInfo examCaptureQueueInfo) throws JSONException;
+
+	/**
+	 * 计算人脸检测结果
+	 * @param examRecordDataId
+	 * @return
+	 */
+    CalculateFaceCheckResultInfo calculateFaceCheckResult(Long examRecordDataId);
+}

+ 55 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamRecordDataService.java

@@ -0,0 +1,55 @@
+package cn.com.qmth.examcloud.core.oe.task.service;
+
+import cn.com.qmth.examcloud.support.enums.IsSuccess;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+
+/**
+ * @author chenken
+ * @date 2018/8/15 11:16
+ * @company QMTH
+ * @description 考试记录数据服务接口
+ */
+public interface ExamRecordDataService {
+
+
+    /**
+     * 保存考试记录
+     *
+     * @param examRecordDataId
+     * @param data
+     */
+    void saveExamRecordDataCache(Long examRecordDataId, ExamRecordData data);
+
+    /**
+     * 获取
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    ExamRecordData getExamRecordDataCache(Long examRecordDataId);
+
+    /**
+     * 删除
+     *
+     * @param examRecordDataId
+     */
+    void deleteExamRecordDataCache(Long examRecordDataId);
+
+    /**
+     * 计算自动审核结果
+     *
+     * @param isNoPhotoAndIllegality
+     * @param faceVerifyResult
+     * @param isIllegality
+     * @return
+     */
+    Boolean calcAutoAuditResult(Boolean isNoPhotoAndIllegality, IsSuccess faceVerifyResult, Boolean isIllegality);
+
+    /**
+     * 交卷后续处理
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    ExamRecordData processAfterHandInExam(Long examRecordDataId);
+}

+ 13 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamSyncCaptureService.java

@@ -0,0 +1,13 @@
+package cn.com.qmth.examcloud.core.oe.task.service;
+
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * @Description 同步抓拍照片接口
+ * @Author lideyin
+ * @Date 2019/12/6 16:19
+ * @Version 1.0
+ */
+public interface ExamSyncCaptureService {
+	void saveExamCaptureSyncCompareResult(Long studentId, Long examRecordDataId);
+}

+ 30 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamingSessionService.java

@@ -0,0 +1,30 @@
+package cn.com.qmth.examcloud.core.oe.task.service;
+
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+
+/**
+ * @author chenken
+ *
+ */
+public interface ExamingSessionService {
+
+    /**
+     * 保存考试会话
+     * @param studentId
+     * @param examingSession
+     */
+    public void saveExamingSession(Long studentId, ExamingSession examingSession);
+
+    /**
+     * 获取
+     * @param studentId
+     * @return
+     */
+    public ExamingSession getExamingSession(Long studentId);
+
+    /**
+     * 删除
+     * @param studentId
+     */
+    public void deleteExamingSession(Long studentId);
+}

+ 126 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/CalculateFaceCheckResultInfo.java

@@ -0,0 +1,126 @@
+package cn.com.qmth.examcloud.core.oe.task.service.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+import javax.persistence.Column;
+
+/**
+ * 人脸检测结果实体
+ */
+public class CalculateFaceCheckResultInfo implements JsonSerializable {
+
+
+	private static final long serialVersionUID = -6342079591051146307L;
+	/**
+	 * 无照片违纪
+	 */
+	private Boolean isNoPhotoAndIllegality;
+
+	/**
+	 * 是否违纪
+	 */
+	private Boolean isIllegality;
+
+	/**
+	 * 是否异常数据
+	 */
+	private Boolean isWarn;
+
+	/**
+	 * 抓拍比对成功次数
+	 */
+	private Integer faceSuccessCount;
+	/**
+	 * 抓拍比对失败次数
+	 */
+	private Integer faceFailedCount;
+	/**
+	 * 抓拍存在陌生人的次数
+	 */
+	private Integer faceStrangerCount;
+	/**
+	 * 抓拍比对总次数
+	 */
+	private Integer faceTotalCount;
+	/**
+	 * 抓拍比对成功比率
+	 */
+	private Double faceSuccessPercent;
+
+	/**
+	 * 百度人脸活体检测通过率
+	 */
+	private Double baiduFaceLivenessSuccessPercent;
+
+	public Boolean getNoPhotoAndIllegality() {
+		return isNoPhotoAndIllegality;
+	}
+
+	public void setNoPhotoAndIllegality(Boolean noPhotoAndIllegality) {
+		isNoPhotoAndIllegality = noPhotoAndIllegality;
+	}
+
+	public Boolean getIsIllegality() {
+		return isIllegality;
+	}
+
+	public void setIsIllegality(Boolean isIllegality) {
+		this.isIllegality = isIllegality;
+	}
+
+	public Boolean getIsWarn() {
+		return isWarn;
+	}
+
+	public void setIsWarn(Boolean isWarn) {
+		this.isWarn = isWarn;
+	}
+
+	public Integer getFaceSuccessCount() {
+		return faceSuccessCount;
+	}
+
+	public void setFaceSuccessCount(Integer faceSuccessCount) {
+		this.faceSuccessCount = faceSuccessCount;
+	}
+
+	public Integer getFaceFailedCount() {
+		return faceFailedCount;
+	}
+
+	public void setFaceFailedCount(Integer faceFailedCount) {
+		this.faceFailedCount = faceFailedCount;
+	}
+
+	public Integer getFaceStrangerCount() {
+		return faceStrangerCount;
+	}
+
+	public void setFaceStrangerCount(Integer faceStrangerCount) {
+		this.faceStrangerCount = faceStrangerCount;
+	}
+
+	public Integer getFaceTotalCount() {
+		return faceTotalCount;
+	}
+
+	public void setFaceTotalCount(Integer faceTotalCount) {
+		this.faceTotalCount = faceTotalCount;
+	}
+
+	public Double getFaceSuccessPercent() {
+		return faceSuccessPercent;
+	}
+
+	public void setFaceSuccessPercent(Double faceSuccessPercent) {
+		this.faceSuccessPercent = faceSuccessPercent;
+	}
+
+	public Double getBaiduFaceLivenessSuccessPercent() {
+		return baiduFaceLivenessSuccessPercent;
+	}
+
+	public void setBaiduFaceLivenessSuccessPercent(Double baiduFaceLivenessSuccessPercent) {
+		this.baiduFaceLivenessSuccessPercent = baiduFaceLivenessSuccessPercent;
+	}
+}

+ 129 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/CompareFaceSyncInfo.java

@@ -0,0 +1,129 @@
+package cn.com.qmth.examcloud.core.oe.task.service.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class CompareFaceSyncInfo implements JsonSerializable {
+
+	/**
+	 *
+	 */
+	private static final long serialVersionUID = 802462624028077528L;
+
+
+	/**
+	 * 人脸比对是否通过
+	 */
+	private Boolean isPass;
+
+	/**
+	 * 学生ID
+	 */
+	private Long studentId;
+
+	/**
+	 * 是否有陌生人
+	 */
+	private Boolean isStranger;
+
+	/**
+	 * 是否存在系统错误
+	 */
+	private boolean existsSystemError;
+
+	/**
+	 * 错误信息
+	 */
+	private String errorMsg;
+
+	/**
+	 * 文件名称(无需返回给前台)
+	 */
+	private transient String fileName;
+
+	/**
+	 * 文件路径(无需返回给前台)
+	 */
+	private transient String fileUrl;
+
+	/**
+	 * 人脸比对结果(无需返回给前台)
+	 */
+	private transient String faceCompareResult;
+
+	/**
+	 * 人脸比对的处理时间(无需返回给前台)
+	 */
+	private transient Long processTime;
+
+	public Boolean getIsPass() {
+		return isPass;
+	}
+
+	public void setIsPass(Boolean isPass) {
+		this.isPass = isPass;
+	}
+
+	public Long getStudentId() {
+		return studentId;
+	}
+
+	public void setStudentId(Long studentId) {
+		this.studentId = studentId;
+	}
+
+	public String getErrorMsg() {
+		return errorMsg;
+	}
+
+	public void setErrorMsg(String errorMsg) {
+		this.errorMsg = errorMsg;
+	}
+
+	public Boolean getIsStranger() {
+		return isStranger;
+	}
+
+	public void setIsStranger(Boolean isStranger) {
+		this.isStranger = isStranger;
+	}
+
+	public Boolean getExistsSystemError() {
+		return existsSystemError;
+	}
+
+	public void setExistsSystemError(boolean existsSystemError) {
+		this.existsSystemError = existsSystemError;
+	}
+
+	public String getFileName() {
+		return fileName;
+	}
+
+	public void setFileName(String fileName) {
+		this.fileName = fileName;
+	}
+
+	public String getFileUrl() {
+		return fileUrl;
+	}
+
+	public void setFileUrl(String fileUrl) {
+		this.fileUrl = fileUrl;
+	}
+
+	public String getFaceCompareResult() {
+		return faceCompareResult;
+	}
+
+	public void setFaceCompareResult(String faceCompareResult) {
+		this.faceCompareResult = faceCompareResult;
+	}
+
+	public Long getProcessTime() {
+		return processTime;
+	}
+
+	public void setProcessTime(Long processTime) {
+		this.processTime = processTime;
+	}
+}

+ 296 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/ExamCaptureQueueInfo.java

@@ -0,0 +1,296 @@
+package cn.com.qmth.examcloud.core.oe.task.service.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import cn.com.qmth.examcloud.core.oe.task.dao.enums.ExamCaptureQueueStatus;
+
+import java.util.Date;
+
+/**
+ * @Description 照片抓拍队列bean
+ * @Author lideyin
+ * @Date 2019/9/26 14:24
+ * @Version 1.0
+ */
+public class ExamCaptureQueueInfo implements JsonSerializable {
+    /**
+     *
+     */
+    private static final long serialVersionUID = 4094671807731989565L;
+
+    private Long id;
+
+    private Long studentId;
+
+    /**
+     * ec_oe_exam_record_data  ID
+     */
+    private Long examRecordDataId;
+
+
+    /**
+     * 底照Token
+     */
+    private String baseFaceToken;
+
+    /**
+     * 文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 文件名称
+     */
+    private String fileName;
+
+    /**
+     * 状态
+     */
+    private ExamCaptureQueueStatus status;
+
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+
+    /**
+     * 错误次数
+     */
+    private Integer errorNum;
+
+    /**
+     * 是否存在虚拟摄像头
+     */
+    private Boolean hasVirtualCamera;
+
+    /**
+     * 摄像头信息  json字符串数组
+     */
+    private String cameraInfos;
+
+    /**
+     * 其他信息
+     * Json格式
+     * {
+     * "":""
+     * }
+     */
+    private String extMsg;
+
+    /**
+     * 队列处理批次号(用户判断某一条数据处理状态)
+     */
+    private String processBatchNum;
+
+    /**
+     * 队列处理的优先级,默认值为0
+     */
+    private int priority = 0;
+
+    /**
+     * 是否有陌生人
+     * 就是摄像头拍到不止考生一人
+     */
+    private Boolean isStranger;
+    /**
+     * 比较是否通过
+     */
+    private Boolean isPass;
+
+    /**
+     * 人脸比较返回信息
+     */
+    private String faceCompareResult;
+    /**
+     * 人脸比对开始时间
+     */
+    private Long faceCompareStartTime;
+
+    /**
+     * 百度在线活体检测结果
+     */
+    private String facelivenessResult;
+
+    /**
+     * 修改时间
+     */
+    private Date updateTime;
+
+    /**
+     * 创建时间
+     */
+    private Date creationTime;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getStudentId() {
+        return studentId;
+    }
+
+    public void setStudentId(Long studentId) {
+        this.studentId = studentId;
+    }
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public String getBaseFaceToken() {
+        return baseFaceToken;
+    }
+
+    public void setBaseFaceToken(String baseFaceToken) {
+        this.baseFaceToken = baseFaceToken;
+    }
+
+    public String getFileUrl() {
+        return fileUrl;
+    }
+
+    public void setFileUrl(String fileUrl) {
+        this.fileUrl = fileUrl;
+    }
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    public ExamCaptureQueueStatus getStatus() {
+        return status;
+    }
+
+    public void setStatus(ExamCaptureQueueStatus status) {
+        this.status = status;
+    }
+
+    public String getErrorMsg() {
+        return errorMsg;
+    }
+
+    public void setErrorMsg(String errorMsg) {
+        this.errorMsg = errorMsg;
+    }
+
+    public Integer getErrorNum() {
+        return errorNum;
+    }
+
+    public void setErrorNum(Integer errorNum) {
+        this.errorNum = errorNum;
+    }
+
+    public Boolean getHasVirtualCamera() {
+        return hasVirtualCamera;
+    }
+
+    public void setHasVirtualCamera(Boolean hasVirtualCamera) {
+        this.hasVirtualCamera = hasVirtualCamera;
+    }
+
+    public String getCameraInfos() {
+        return cameraInfos;
+    }
+
+    public void setCameraInfos(String cameraInfos) {
+        this.cameraInfos = cameraInfos;
+    }
+
+    public String getExtMsg() {
+        return extMsg;
+    }
+
+    public void setExtMsg(String extMsg) {
+        this.extMsg = extMsg;
+    }
+
+    public String getProcessBatchNum() {
+        return processBatchNum;
+    }
+
+    public void setProcessBatchNum(String processBatchNum) {
+        this.processBatchNum = processBatchNum;
+    }
+
+    public int getPriority() {
+        return priority;
+    }
+
+    public void setPriority(int priority) {
+        this.priority = priority;
+    }
+
+    public Boolean getStranger() {
+        return isStranger;
+    }
+
+    public void setStranger(Boolean stranger) {
+        isStranger = stranger;
+    }
+
+    public Boolean getPass() {
+        return isPass;
+    }
+
+    public void setPass(Boolean pass) {
+        isPass = pass;
+    }
+
+    public String getFaceCompareResult() {
+        return faceCompareResult;
+    }
+
+    public void setFaceCompareResult(String faceCompareResult) {
+        this.faceCompareResult = faceCompareResult;
+    }
+
+    public Long getFaceCompareStartTime() {
+        return faceCompareStartTime;
+    }
+
+    public void setFaceCompareStartTime(Long faceCompareStartTime) {
+        this.faceCompareStartTime = faceCompareStartTime;
+    }
+
+    public String getFacelivenessResult() {
+        return facelivenessResult;
+    }
+
+    public void setFacelivenessResult(String facelivenessResult) {
+        this.facelivenessResult = facelivenessResult;
+    }
+
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public Date getCreationTime() {
+        return creationTime;
+    }
+
+    public void setCreationTime(Date creationTime) {
+        this.creationTime = creationTime;
+    }
+
+    @Override
+    public String toString() {
+        return "ExamCaptureQueueInfo [id=" + id + ", studentId=" + studentId + ", examRecordDataId="
+                + examRecordDataId + ", fileUrl=" + fileUrl + "]";
+    }
+}

+ 83 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/SaveExamCaptureQueueInfo.java

@@ -0,0 +1,83 @@
+package cn.com.qmth.examcloud.core.oe.task.service.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+public class SaveExamCaptureQueueInfo implements JsonSerializable {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -7434669407796418900L;
+
+	private Long examRecordDataId;
+	
+	private String fileUrl;
+	/**
+     * 是否存在虚拟摄像头
+     */
+    private Boolean hasVirtualCamera;
+    /**
+     * 摄像头信息  json字符串数组
+     */
+    private String cameraInfos;
+    /**
+     * 其他信息
+     * Json格式
+     * {
+     * 	"":""
+     * }
+     */
+    private String extMsg;
+	@ApiModelProperty("又拍云签名唯一标识")
+	private String signIdentifier;
+
+	public String getSignIdentifier() {
+		return signIdentifier;
+	}
+
+	public void setSignIdentifier(String signIdentifier) {
+		this.signIdentifier = signIdentifier;
+	}
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public String getFileUrl() {
+		return fileUrl;
+	}
+
+	public void setFileUrl(String fileUrl) {
+		this.fileUrl = fileUrl;
+	}
+
+	public Boolean getHasVirtualCamera() {
+		return hasVirtualCamera;
+	}
+
+	public void setHasVirtualCamera(Boolean hasVirtualCamera) {
+		this.hasVirtualCamera = hasVirtualCamera;
+	}
+
+	public String getCameraInfos() {
+		return cameraInfos;
+	}
+
+	public void setCameraInfos(String cameraInfos) {
+		this.cameraInfos = cameraInfos;
+	}
+
+	public String getExtMsg() {
+		return extMsg;
+	}
+
+	public void setExtMsg(String extMsg) {
+		this.extMsg = extMsg;
+	}
+    
+}

+ 39 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamBossServiceImpl.java

@@ -0,0 +1,39 @@
+package cn.com.qmth.examcloud.core.oe.task.service.impl;
+
+import cn.com.qmth.examcloud.core.oe.task.service.ExamBossService;
+import cn.com.qmth.examcloud.support.examing.ExamBoss;
+import cn.com.qmth.examcloud.support.redis.RedisKeyHelper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * @Description 考试基础信息会话
+ * @Author lideyin
+ * @Date 2019/12/20 15:40
+ * @Version 1.0
+ */
+@Service("examBossService")
+public class ExamBossServiceImpl implements ExamBossService {
+
+	@Autowired
+	private RedisClient redisClient;
+
+	@Override
+	public void saveExamBoss(Long examStudentId, ExamBoss eb) {
+		String key = RedisKeyHelper.getBuilder().examBossKey(examStudentId);
+		redisClient.set(key, eb, 2592000);
+	}
+
+	@Override
+	public ExamBoss getExamBoss(Long examStudentId) {
+		String key = RedisKeyHelper.getBuilder().examBossKey(examStudentId);
+		return redisClient.get(key, ExamBoss.class);
+	}
+
+	@Override
+	public void deleteExamBoss(Long examStudentId) {
+		String key = RedisKeyHelper.getBuilder().examBossKey(examStudentId);
+		redisClient.delete(key);
+	}
+}

+ 84 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamCaptureQueueServiceImpl.java

@@ -0,0 +1,84 @@
+package cn.com.qmth.examcloud.core.oe.task.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+import cn.com.qmth.examcloud.commons.util.UrlUtil;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.task.dao.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureQueueService;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.SaveExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.StudentCacheBean;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+
+/**
+ * @author chenken
+ * @date 2018/8/17 14:48
+ * @company QMTH
+ * @description 考试抓拍处理队列服务实现
+ */
+@Service("examCaptureQueueService")
+public class ExamCaptureQueueServiceImpl implements ExamCaptureQueueService {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamCaptureQueueServiceImpl.class);
+    private final Log captureLog = LogFactory.getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+    @Autowired
+    private ExamCaptureQueueRepo examCaptureQueueRepo;
+
+    @Override
+    public boolean saveExamCaptureQueueEntityByFailed(Long captureQueueId, String errorMsg,
+                                                      ExamCaptureQueueStatus examCaptureQueueStatus) {
+        try {
+            examCaptureQueueRepo.saveExamCaptureQueueEntityByFailed(captureQueueId, errorMsg, examCaptureQueueStatus.toString(),
+                    "000000", new Date());
+            return true;
+        } catch (Exception e) {
+            captureLog.error("[UPDATE_FAILED_CAPTURE_QUEUE] 保存照片队列处理失败数据时出现异常",e);
+            return false;
+        }
+    }
+
+    @Override
+    public String saveExamCaptureQueue(SaveExamCaptureQueueInfo saveExamCaptureQueueInfo, Long studentId) {
+        //查询学生底照faceToken
+        StudentCacheBean studentCache = CacheHelper.getStudent(studentId);
+        String baseFaceToken = studentCache.getFaceToken();
+        if (StringUtils.isBlank(baseFaceToken)) {
+            throw new StatusException("300002", "学生底照的faceToken为空");
+        }
+
+        String fileUrl = UrlUtil.decode(saveExamCaptureQueueInfo.getFileUrl());
+        if (!StringUtil.isAscString(fileUrl)) {
+            log.error("The fileUrl is invalid:" + saveExamCaptureQueueInfo.getFileUrl());
+            throw new StatusException("300001", "文件路径格式不正确");
+        }
+        String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
+
+        ExamCaptureQueueEntity examCaptureQueue = new ExamCaptureQueueEntity();
+        examCaptureQueue.setStudentId(studentId);
+        examCaptureQueue.setFileUrl(fileUrl);
+        examCaptureQueue.setFileName(fileName);
+        examCaptureQueue.setBaseFaceToken(baseFaceToken);
+        examCaptureQueue.setExamRecordDataId(saveExamCaptureQueueInfo.getExamRecordDataId());
+        examCaptureQueue.setHasVirtualCamera(saveExamCaptureQueueInfo.getHasVirtualCamera());
+        examCaptureQueue.setCameraInfos(saveExamCaptureQueueInfo.getCameraInfos());
+        examCaptureQueue.setExtMsg(saveExamCaptureQueueInfo.getExtMsg());
+        examCaptureQueue.setStatus(ExamCaptureQueueStatus.PENDING);
+        examCaptureQueue.setErrorNum(0);
+        examCaptureQueue.setCreationTime(new Date());
+        examCaptureQueueRepo.save(examCaptureQueue);
+        return fileName;
+    }
+
+
+}

+ 671 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamCaptureServiceImpl.java

@@ -0,0 +1,671 @@
+package cn.com.qmth.examcloud.core.oe.task.service.impl;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.JSONException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Example;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.JsonHttpResponseHolder;
+import cn.com.qmth.examcloud.core.oe.task.base.ExamCaptureProcessStatisticController;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureRepo;
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.task.dao.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureQueueService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CalculateFaceCheckResultInfo;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CompareFaceSyncInfo;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.ExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.support.enums.ExamProperties;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import cn.com.qmth.examcloud.support.filestorage.FileStorageUtil;
+import cn.com.qmth.examcloud.support.helper.ExamCacheTransferHelper;
+import cn.com.qmth.examcloud.support.helper.FaceBiopsyHelper;
+import cn.com.qmth.examcloud.web.baidu.BaiduClient;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.facepp.FaceppClient;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+
+/**
+ * @author chenken
+ * @date 2018年9月5日 下午3:31:37
+ * @company QMTH
+ * @description 考试抓拍服务实现
+ */
+@SuppressWarnings("ALL")
+@Service("examCaptureService")
+public class ExamCaptureServiceImpl implements ExamCaptureService {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamCaptureServiceImpl.class);
+    private final Log captureLog = LogFactory.getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+    @Autowired
+    private ExamCaptureRepo examCaptureRepo;
+
+    @Autowired
+    private ExamCaptureQueueRepo examCaptureQueueRepo;
+
+    @Autowired
+    private ExamCaptureQueueService examCaptureQueueService;
+
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+
+    public static final String TEMP_FILE_EXP = "face_compare/";
+
+    @Autowired
+    private RedisClient redisClient;
+
+    /**
+     * 对图片进行人脸对比
+     *
+     * @param examCaptureQueue
+     */
+    @Override
+    @Transactional
+    public void disposeFaceCompare(ExamCaptureQueueInfo examCaptureQueue) {
+        //将队列记录修改为处理中
+        examCaptureQueueRepo.updateExamCaptureQueueStatusWithProcessing(examCaptureQueue.getId());
+        examCaptureQueue.setFaceCompareStartTime(System.currentTimeMillis());
+
+        //facepp超时最大重试次数
+        int maxRetryTimes = PropertyHolder.getInt("facepp.compare.timeOut.maxRetryTimes", 3);
+        boolean retry = false;
+        JSONObject faceCompareResult;
+        //人脸比对超时次数
+        int faceCompareTimeOutTimes = 0;
+        do {
+            retry = false;
+
+            //仅用于日志时间计算
+            long startTime = System.currentTimeMillis();
+            captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对开始...");
+
+            //调用face++API执行人脸比对,得到返回结果
+            JsonHttpResponseHolder jsonHttpResponseHolder = null;
+            try {
+                jsonHttpResponseHolder = FaceppClient.getClient().
+                        compareWithTokenAndImageUrl(examCaptureQueue.getBaseFaceToken(),
+                                FileStorageUtil.realPath(examCaptureQueue.getFileUrl()),
+                                FileStorageUtil.realPathBackup(examCaptureQueue.getFileUrl()));
+
+                faceCompareResult = jsonHttpResponseHolder.getRespBody();
+
+            } catch (StatusException e) {
+                //如果错误码是801,802,803直接结束,不重试
+                if (e.getCode().equals("801") || e.getCode().equals("802") || e.getCode().equals("803")) {
+                    examCaptureQueue.setFaceCompareResult(e.getDesc());
+                    saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对无法处理的图片地址,保存人脸检测最终结果并删除队列,errMsg=" + e.getDesc());
+                    return;
+                }
+                throw e;
+            }
+
+            if (captureLog.isDebugEnabled()) {
+                captureLog.debug("[DISPOSE_FACE_COMPARE] 调用face++API执行人脸比对,得到返回结果faceCompareResult:" + faceCompareResult);
+            }
+            examCaptureQueue.setFaceCompareResult(faceCompareResult.toString());
+
+            //人脸比对出错的处理
+            if (faceCompareResult.containsKey(Constants.ERROR_MSG)) {
+                String errMsg = faceCompareResult.getString(Constants.ERROR_MSG);
+
+                //如果API并发次数超过上限,则保存错误信息到队列,并抛出异常,用于协调满载队列线程
+                if (errMsg.contains(Constants.FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED)) {
+                    examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue.getId(),
+                            "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + faceCompareResult.toString(),
+                            ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED);
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对接口超过最大并发次数");
+
+                    throw new StatusException(Constants.FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED, "face++ API接口超过最大并发次数");
+                }
+
+                //face++无需重试的错误信息
+                SysPropertyCacheBean objNotRetryErrMsg = CacheHelper.getSysProperty("facePlusPlus.faceCompare.notRetry.errMsg");
+                if (!objNotRetryErrMsg.getHasValue()) {
+                    throw new StatusException("100001", "未找到face++人脸比对错误消息的相关配置");
+                }
+                String objNotRetryErrMsgs = objNotRetryErrMsg.getValue().toString();
+                String[] notRetryErrMsgsArr = objNotRetryErrMsgs.split(",");
+                for (int i = 0; i < notRetryErrMsgsArr.length; i++) {
+                    //如果是配置中的无法处理的图片,则保存人脸检测最终结果并删除队列
+                    if (errMsg.contains(notRetryErrMsgsArr[i])) {
+                        saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                        captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对无法处理的图片,保存人脸检测最终结果并删除队列,errMsg=" + errMsg);
+
+                        return;
+                    }
+                }
+
+                //超时错误特殊处理,重试3次后
+                if (errMsg.contains(Constants.FACE_COMPARE_IMAGE_DOWNLOAD_TIMEOUT)) {
+                    faceCompareTimeOutTimes++;
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对超时,将进行第" + faceCompareTimeOutTimes + "次重试");
+
+                    //如果没有达到最大重试次数,则继续重试
+                    if (faceCompareTimeOutTimes < maxRetryTimes) {
+                        retry = true;
+                        continue;
+                    }
+                    //超过最大重试次数,则直接保存最终结果
+                    saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对超过最大检测次数:" + maxRetryTimes + ",停止重试,直接保存最终结果");
+                    return;
+                }
+
+                captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对出现错误,即将重试,errMsg:" + errMsg);
+                // 其它错误类型,保存错误信息到队列中,待自动重新服务处理
+                examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue.getId(),
+                        "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + faceCompareResult.toString(),
+                        ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED);
+                ExamCaptureProcessStatisticController.increaseFaceCompareFailedCount();//增加错误次数
+                captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对出现错误,增加错误次数后failedCount:" +
+                        ExamCaptureProcessStatisticController.getFaceCompareFailedCount() + ",totalCount=" +
+                        ExamCaptureProcessStatisticController.getFaceCompareCount());
+            }
+            //人脸比对没有出错的情况
+            else {
+                //face++的结果检测到人脸,才执行百度活体检测
+                if (faceCompareResult.containsKey("confidence")) {
+                    examCaptureQueue.setPass(calculateFaceCompareIsPass(faceCompareResult));
+                    examCaptureQueue.setStranger(calculateFaceCompareIsStranger(examCaptureQueue.getExamRecordDataId(),
+                            examCaptureQueue.getStudentId(), faceCompareResult));
+                    //更新队列状态为face++比对完成
+                    examCaptureQueue.setStatus(ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_COMPLETE);
+                    disposeFaceCompareSuccessful(examCaptureQueue);
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对完成,即将进行百度活体检测,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+                }
+                //face++ 没有检测到人脸,直接保存人脸检测最终结果并删除队列
+                else {
+                    examCaptureQueue.setPass(false);
+                    examCaptureQueue.setStranger(false);
+                    saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                    captureLog.debug("[DISPOSE_FACE_COMPARE] face++人脸比对完成,且未检测到人脸,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+                }
+            }
+        } while (retry);
+    }
+
+    //人脸比较成功时的处理
+    public void disposeFaceCompareSuccessful(ExamCaptureQueueInfo examCaptureQueueInfo) {
+        examCaptureQueueRepo.saveExamCaptureQueueEntityBySuccessful(examCaptureQueueInfo.getId(),
+                examCaptureQueueInfo.getPass(), examCaptureQueueInfo.getStranger(),
+                examCaptureQueueInfo.getStatus().toString(), examCaptureQueueInfo.getFaceCompareResult(),
+                examCaptureQueueInfo.getFaceCompareStartTime(), new Date());
+    }
+
+    /**
+     * 对照片进行百度活体检测
+     *
+     * @param examCapture      抓拍照片最终检测最终结果实体
+     * @param examCaptureQueue 抓拍照片队列表
+     */
+    @Override
+    public void disposeBaiDuFaceLiveness(ExamCaptureQueueInfo examCaptureQueue) {
+        //活体检测超时次数
+        int facelivenessTimeOutTimes = 0;
+        //百度活检超时最大重试次数
+        int maxRetryTimes = PropertyHolder.getInt("baidu.faceliveness.timeOut.maxRetryTimes", 3);
+        boolean retry = false;
+        JSONObject faceLivenessResultJson;
+        do {
+            retry = false;
+
+            //仅用于日志时间计算
+            long startTime = System.currentTimeMillis();
+            captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测开始...");
+
+            //获取百度活检结果
+            JsonHttpResponseHolder jsonHttpResponseHolder = null;
+            try {
+                jsonHttpResponseHolder = BaiduClient.getClient().
+                        verifyFaceLiveness(FileStorageUtil.realPath(examCaptureQueue.getFileUrl()),
+                                FileStorageUtil.realPathBackup(examCaptureQueue.getFileUrl()));
+                faceLivenessResultJson = jsonHttpResponseHolder.getRespBody();
+            } catch (StatusException e) {
+                //如果错误码是901,902,903直接结束,不重试
+                if (e.getCode().equals("901") || e.getCode().equals("902") || e.getCode().equals("903")) {
+                    examCaptureQueue.setFacelivenessResult(e.getDesc());
+                    saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                    captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活检无法处理的图片地址,保存人脸检测最终结果并删除队列,errMsg=" + e.getDesc());
+                    return;
+                }
+                throw e;
+            }
+//            faceLivenessResultJson = faceLivenessService.getBaiduFaceLivenessResultJson(examCaptureQueue.getFileUrl());
+
+            //如果百度活体检测执行失败,调用队列失败处理,程序退出,失败的数据,后续会有自动服务重新处理
+            if (faceLivenessResultJson.containsKey(Constants.BAIDU_ERROR_CODE) &&
+                    !faceLivenessResultJson.getString(Constants.BAIDU_ERROR_CODE).equals(Constants.BAIDU_SUCCESS_ERROR_CODE_VALUE)) {
+                String errCode = faceLivenessResultJson.getString(Constants.BAIDU_ERROR_CODE);
+
+                //如果API并发次数超过上限,则保存错误信息到队列,并抛出异常,用于协调满载队列线程
+                if (errCode.equals(Constants.BAIDU_FACELIVENESS_QPS_LIMIT_EXCEEDED_CODE)) {
+                    examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue.getId(),
+                            "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + faceLivenessResultJson.toString(),
+                            ExamCaptureQueueStatus.PROCESS_FACELIVENESS_FAILED);
+
+                    captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度在线活体API接口超过最大并发次数");
+
+                    throw new StatusException(Constants.BAIDU_FACELIVENESS_QPS_LIMIT_EXCEEDED_CODE, "百度在线活体API接口超过最大并发次数");
+                }
+
+                //百度无需重试的错误信息
+                SysPropertyCacheBean objNotRetryErrMsg = CacheHelper.getSysProperty("baidu.faceLiveness.notRetry.errCode");
+                if (!objNotRetryErrMsg.getHasValue()) {
+                    throw new StatusException("100002", "未找到百度活体检测错误代码的相关配置");
+                }
+                String objNotRetryErrMsgs = objNotRetryErrMsg.getValue().toString();
+                String[] notRetryErrMsgsArr = objNotRetryErrMsgs.split(",");
+                for (int i = 0; i < notRetryErrMsgsArr.length; i++) {
+                    //如果是配置中的无法处理的图片,则保存人脸检测最终结果并删除队列
+                    if (errCode.equals(notRetryErrMsgsArr[i])) {
+                        captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测,无法处理的图片,将保存人脸检测最终结果并删除队列,errCode=" + errCode);
+
+                        saveExamCaptureAndDeleteQueue(examCaptureQueue);
+                        return;
+                    }
+                }
+
+                //超时错误特殊处理,重试3次后
+                if (errCode.equals(Constants.BAIDU_FACELIVENESS_CONNECTION_OR_READ_DATA_TIME_OUT_CODE)) {
+                    facelivenessTimeOutTimes++;
+
+                    captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测超时,将进行第" + facelivenessTimeOutTimes + "次重试");
+
+                    //如果没有达到最大重试次数,则继续重试
+                    if (facelivenessTimeOutTimes < maxRetryTimes) {
+                        retry = true;
+                        continue;
+                    }
+
+                    //超过最大重试次数,则直接保存最终结果
+                    saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                    captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测超过最大检测次数:" + maxRetryTimes + ",停止重试,直接保存最终结果");
+                    return;
+                }
+
+                captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测出现错误,即将重试,错误码:" + errCode);
+                // 其它错误类型,保存错误信息到队列中,待自动重新服务处理
+                examCaptureQueueService.saveExamCaptureQueueEntityByFailed(examCaptureQueue.getId(),
+                        "SatusCode:" + jsonHttpResponseHolder.getStatusCode() + " | " + faceLivenessResultJson.toString(), ExamCaptureQueueStatus.PROCESS_FACELIVENESS_FAILED);
+                ExamCaptureProcessStatisticController.increaseFaceLivenessDetectFailedCount();//增加错误次数
+                captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测出现错误,增加错误次数后failedCount:" + ExamCaptureProcessStatisticController.getFaceLivenessDetectFailedCount());
+            }
+            //百度活体检测成功,则保存最终检测结果,并删除临时的图片处理队列
+            else {
+                examCaptureQueue.setFacelivenessResult(faceLivenessResultJson.toString());
+                saveExamCaptureAndDeleteQueue(examCaptureQueue);
+
+                captureLog.debug("[DISPOSE_BAIDUFACELIVENESS] 百度活体检测完成,耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+            }
+        } while (retry);
+    }
+
+    /**
+     * 保存人脸检测最终结果并删除队列
+     *
+     * @param examCapture 抓拍照片最终检测最终结果实体
+     *                    * @param examCaptureQueue 抓拍照片队列表
+     */
+    @Override
+    @Transactional
+    public void saveExamCaptureAndDeleteQueue(ExamCaptureQueueInfo examCaptureQueue) {
+        ExamCaptureEntity examCapture = getExamCaptureFromQueue(examCaptureQueue);
+
+        //同一考试记录下如果有重复的照片,则直接跳过
+        ExamCaptureEntity query = new ExamCaptureEntity();
+        query.setExamRecordDataId(examCapture.getExamRecordDataId());
+        query.setFileName(examCapture.getFileName());
+        Example<ExamCaptureEntity> example = Example.of(query);
+        //照片处理结果中如果已存在,则以已有的数据为准
+        if (!examCaptureRepo.exists(example)) {
+            examCaptureRepo.save(examCapture);
+        }
+        //删除队列中的记录
+        examCaptureQueueRepo.deleteById(examCaptureQueue.getId());
+    }
+
+    @Override
+    public CompareFaceSyncInfo compareFaceSyncByFileUrl(Long studentId, String baseFaceToken, String fileUrl) {
+        CompareFaceSyncInfo compareFaceSyncInfo = new CompareFaceSyncInfo();
+        compareFaceSyncInfo.setStudentId(studentId);
+        JSONObject facePPResult = null;
+        JsonHttpResponseHolder jsonHttpResponseHolder = null;
+        try {
+            jsonHttpResponseHolder = FaceppClient.getClient().
+                    compareWithTokenAndImageUrl(baseFaceToken,
+                            FileStorageUtil.realPath(fileUrl),
+                            FileStorageUtil.realPathBackup(fileUrl));
+            facePPResult = jsonHttpResponseHolder.getRespBody();
+        } catch (StatusException e) {
+            //如果错误码是801,802,803直接结束,不重试
+            if (e.getCode().equals("801") || e.getCode().equals("802") || e.getCode().equals("803")) {
+                compareFaceSyncInfo.setIsPass(false);
+                compareFaceSyncInfo.setExistsSystemError(true);
+                compareFaceSyncInfo.setErrorMsg(e.getDesc());
+
+                captureLog.error("[COMPARE_FACE_SYNC] face++人脸比对无法处理的图片地址,errMsg=" + e.getDesc(), e);
+                return compareFaceSyncInfo;
+            }
+            throw e;
+        } catch (Exception e) {
+            compareFaceSyncInfo.setIsPass(false);
+            compareFaceSyncInfo.setExistsSystemError(true);
+            compareFaceSyncInfo.setErrorMsg("系统异常");
+
+            captureLog.error("[COMPARE_FACE_SYNC] 未处理的系统异常,errMsg=" + e.getMessage(), e);
+            return compareFaceSyncInfo;
+        }
+        if (facePPResult.containsKey(Constants.ERROR_MSG)) {
+            compareFaceSyncInfo.setFaceCompareResult(facePPResult.toString());
+            compareFaceSyncInfo.setIsPass(false);
+            String errMsg = facePPResult.getString(Constants.ERROR_MSG);
+            if (errMsg.contains(Constants.FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED) ||
+                    errMsg.contains(Constants.FACE_COMPARE_AUTHORIZATION_ERROR)) {
+                compareFaceSyncInfo.setExistsSystemError(true);
+            }
+            compareFaceSyncInfo.setErrorMsg("facePP called failed : " + facePPResult.toString());
+            return compareFaceSyncInfo;
+        } else {
+            compareFaceSyncInfo.setFaceCompareResult(facePPResult.toString());
+            if (facePPResult.containsKey("confidence")) {
+                double confidence = facePPResult.getDouble("confidence");
+                JSONObject thresholdsJsonObject = facePPResult.getJSONObject("thresholds");
+                double le4 = thresholdsJsonObject.getDouble("1e-4");
+                JSONArray face2Array = facePPResult.getJSONArray("faces2");
+                boolean hasStranger = face2Array.size() > 1;
+                compareFaceSyncInfo.setIsStranger(hasStranger);//是否有陌生人
+                boolean isPass = confidence >= le4;
+                compareFaceSyncInfo.setIsPass(isPass);//是否通过
+                String errorMsg = null;
+                if (hasStranger && !isPass) {
+                    errorMsg = "检测过程中相片非本人,且存在多人脸";
+                } else if (hasStranger) {
+                    errorMsg = "检测过程中多人脸失败";
+                } else if (!isPass) {
+                    errorMsg = "检测过程中相片非本人";
+                }
+                compareFaceSyncInfo.setErrorMsg(errorMsg);
+            } else {
+                compareFaceSyncInfo.setIsPass(false);
+                compareFaceSyncInfo.setErrorMsg("未检测到人脸");
+            }
+            return compareFaceSyncInfo;
+        }
+
+
+    }
+
+    @Override
+    public ExamCaptureEntity getExamCaptureResult(Long examRecordDataId, String fileName) {
+        return examCaptureRepo.findByExamRecordDataIdAndFileName(examRecordDataId, fileName);
+    }
+
+    /**
+     * 计算人脸检测结果
+     * 相片数=0,系统判断为违纪,自动审核
+     * 考试记录为异常逻辑(进入待审):
+     * 1.陌生人次数>0
+     * 2.face++阈值 = 0 && 百度真实性阈值 > 0
+     * 真实性百分比<百度真实性阈值
+     * 3.face++阈值 > 0 && 百度真实性阈值 = 0
+     * face++成功率<face++阈值
+     * 4.face++阈值 > 0 && 百度真实性阈值 > 0
+     * face++成功率<face++阈值 ||
+     * 真实性百分比<百度真实性阈值
+     *
+     * @param examRecordData
+     * @return
+     */
+    @Override
+    public CalculateFaceCheckResultInfo calculateFaceCheckResult(Long examRecordDataId) {
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+
+        Long rootOrgId = examRecordData.getRootOrgId();
+        Long examId = examRecordData.getExamId();
+        Long orgId = examRecordData.getOrgId();
+        Long studentId = examRecordData.getStudentId();
+
+        //人脸最终检测结果
+        CalculateFaceCheckResultInfo resultInfo = new CalculateFaceCheckResultInfo();
+
+        //未开启人脸检测,直接认为正常数据
+        if (!FaceBiopsyHelper.isFaceEnable(rootOrgId, examId, studentId)) {
+            resultInfo.setIsWarn(false);
+            return resultInfo;
+        }
+
+        List<ExamCaptureEntity> examCaptureList = examCaptureRepo.findByExamRecordDataId(examRecordData.getId());
+
+        //无照片违纪
+        if (examCaptureList == null || examCaptureList.size() == 0) {
+            resultInfo.setIsWarn(true);//有异常
+            resultInfo.setIsIllegality(true);//违纪
+            resultInfo.setNoPhotoAndIllegality(true);//无照片违纪
+            return resultInfo;
+        }
+
+        //根据照片结果计算
+        return calculateByCaptureResult(examCaptureList, examRecordData.getExamId(), examRecordData.getStudentId());
+    }
+
+    /**
+     * 计算人脸检测数据
+     * 陌生人记录数、成功次数、失败次数、成功率
+     *
+     * @param examCaptureEntityList
+     * @param examId
+     * @param studentId
+     * @return
+     */
+    private CalculateFaceCheckResultInfo calculateByCaptureResult(List<ExamCaptureEntity> examCaptureEntityList,
+                                                                  Long examId, Long studentId) {
+        int strangerCount = 0;    // 人脸比较陌生人记录数
+        int succCount = 0;        // 人脸比较成功次数
+        int falseCount = 0;        // 人脸比较失败次数
+        double succPercent = 0d;    // 人脸比较成功率
+        int livenessSuccessCount = 0;//百度活体检测成功次数
+        double livenessSuccessPercent = 0D;//百度活体检测成功率
+        for (ExamCaptureEntity examCaptureEntity : examCaptureEntityList) {
+            if (examCaptureEntity.getIsPass() != null && examCaptureEntity.getIsPass()) {
+                succCount++;
+            } else {
+                falseCount++;
+            }
+            if (examCaptureEntity.getIsStranger() != null && examCaptureEntity.getIsStranger()) {
+                strangerCount++;
+            }
+            livenessSuccessCount += calculateFacelivenessSuccessCount(examCaptureEntity.getFacelivenessResult());
+        }
+        int allNum = examCaptureEntityList.size();
+        BigDecimal bg = new BigDecimal(((double) succCount / allNum) * 100);
+        succPercent = bg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();// 人脸比较成功率
+
+        //活体检测最终结果
+        CalculateFaceCheckResultInfo resultInfo = new CalculateFaceCheckResultInfo();
+
+        resultInfo.setFaceTotalCount(allNum);//检测总次数
+        resultInfo.setFaceSuccessPercent(succPercent);//成功率
+        resultInfo.setFaceStrangerCount(strangerCount);//有陌生人的次数
+        resultInfo.setFaceSuccessCount(succCount);//成功次数
+        resultInfo.setFaceFailedCount(falseCount);//失败次数
+
+        //计算百度活体检测通过率
+        BigDecimal livenessSuccessBg = new BigDecimal(((double) livenessSuccessCount / allNum) * 100);
+        livenessSuccessPercent = livenessSuccessBg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();//百度活体检测成功率
+        resultInfo.setBaiduFaceLivenessSuccessPercent(livenessSuccessPercent);
+
+        //陌生人个数>0
+        if (resultInfo.getFaceStrangerCount() > 0) {
+            resultInfo.setIsWarn(true);
+            return resultInfo;
+        }
+
+        //人脸识别阀值
+        String warnThresholdStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.WARN_THRESHOLD.name()).getValue();
+        if (StringUtils.isBlank(warnThresholdStr)) {
+            throw new StatusException("400101", "人脸检测预警阈值未设置");
+        }
+
+        //人脸真实性(百度活体检测)通过阀值
+        String liveWarnThresholdStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.LIVING_WARN_THRESHOLD.name()).getValue();
+
+        if (StringUtils.isBlank(liveWarnThresholdStr)) {
+            throw new StatusException("400102", "人脸真实性阈值未设置");
+        }
+
+        double warnThreshold = Double.parseDouble(warnThresholdStr);
+        double livenessThreshold = Double.parseDouble(liveWarnThresholdStr);
+
+        if (warnThreshold == 0d && livenessThreshold > 0d) {
+            resultInfo.setIsWarn(livenessSuccessPercent < livenessThreshold);
+        } else if (warnThreshold > 0d && livenessThreshold == 0d) {
+            resultInfo.setIsWarn(succPercent < warnThreshold);
+        } else if (warnThreshold > 0d && livenessThreshold > 0d) {
+            resultInfo.setIsWarn(succPercent < warnThreshold || livenessSuccessPercent < livenessThreshold);
+        }
+
+        return resultInfo;
+    }
+
+    /**
+     * 计算百度活体检测成功数量
+     *
+     * @param facelivenessResult
+     * @return
+     */
+    private int calculateFacelivenessSuccessCount(String facelivenessResult) {
+        if (StringUtils.isNotBlank(facelivenessResult)) {
+            org.json.JSONObject jsonObject;
+            try {
+                jsonObject = new org.json.JSONObject(facelivenessResult);
+                if (jsonObject.has("error_code") && jsonObject.getInt("error_code") == 0 && jsonObject.has("result")) {
+                    org.json.JSONObject resultJson = jsonObject.getJSONObject("result");
+                    if (resultJson.has("face_liveness")) {
+                        double faceLivenessVal = resultJson.getDouble("face_liveness");
+
+                        Double baiduFacelivenessThreshold;
+                        SysPropertyCacheBean baiduFacelivenessThresholdProperty = CacheHelper.getSysProperty("$baidu.faceliveness.threshold");
+                        if (!baiduFacelivenessThresholdProperty.getHasValue()) {
+                            baiduFacelivenessThreshold = Constants.DEFAULT_BAIDU_FACELIVENESS_THRESHOLD;
+                        } else {
+                            baiduFacelivenessThreshold = Double.valueOf(baiduFacelivenessThresholdProperty.getValue().toString());
+                        }
+
+                        if (faceLivenessVal > baiduFacelivenessThreshold) {
+                            return 1;
+                        }
+                    }
+                }
+            } catch (JSONException e) {
+                e.printStackTrace();
+                return 0;
+            }
+
+        }
+        return 0;
+    }
+
+    private ExamCaptureEntity getExamCaptureFromQueue(ExamCaptureQueueInfo queue) {
+        long currentTimeMillis = System.currentTimeMillis();
+        long createTimeMillis = queue.getCreationTime().getTime();
+        long faceCompareStartTimeMillis = queue.getFaceCompareStartTime();
+
+        ExamCaptureEntity resultEntity = new ExamCaptureEntity();
+        resultEntity.setCameraInfos(queue.getCameraInfos());
+        resultEntity.setExamRecordDataId(queue.getExamRecordDataId());
+        resultEntity.setExtMsg(queue.getExtMsg());
+        resultEntity.setFaceCompareResult(queue.getFaceCompareResult());
+        resultEntity.setFacelivenessResult(queue.getFacelivenessResult());
+        resultEntity.setFileName(queue.getFileName());
+        resultEntity.setFileUrl(queue.getFileUrl());
+        resultEntity.setHasVirtualCamera(queue.getHasVirtualCamera());
+        resultEntity.setIsStranger(queue.getStranger());
+        resultEntity.setIsPass(queue.getPass());
+        resultEntity.setProcessTime(currentTimeMillis - createTimeMillis);//从进队列到处理完毕的时间
+        resultEntity.setUsedTime(currentTimeMillis - faceCompareStartTimeMillis);//从开始处理到处理完毕的时间
+        return resultEntity;
+    }
+
+
+    /**
+     * 校验是否有陌生人脸
+     *
+     * @param examCaptureQueue
+     * @param jsonObject
+     * @return
+     * @throws JSONException
+     */
+    private boolean calculateFaceCompareIsStranger(Long examRecordDataId, Long studentId, JSONObject jsonObject) {
+        JSONArray face2Array = jsonObject.getJSONArray("faces2");
+        //添加是否有陌生人开关功能
+        ExamRecordData examRecordDataCache = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+        //默认开启了陌生人检测
+        String isStrangerEnableStr = "true";
+        if (examRecordDataCache != null) {
+            isStrangerEnableStr = ExamCacheTransferHelper.getCachedExamProperty(examRecordDataCache.getExamId(),
+                    studentId,
+                    ExamProperties.IS_STRANGER_ENABLE.name()).getValue();
+        }
+        boolean isStranger;
+        // 如果开启了陌生人检测才记录陌生人数据,否则认为没有陌生人
+        if (Constants.isTrue.equals(isStrangerEnableStr)) {
+            isStranger = face2Array.size() > 1;//是否有陌生人
+        } else {
+            isStranger = false;
+        }
+        return isStranger;
+    }
+
+    /**
+     * 计算人脸比对是否通过
+     *
+     * @param jsonObject
+     * @return
+     * @throws JSONException
+     */
+    private boolean calculateFaceCompareIsPass(JSONObject jsonObject) {
+        //比对结果置信度,范围 [0,100],小数点后3位有效数字,数字越大表示两个人脸越可能是同一个人。
+        double confidence = jsonObject.getDouble("confidence");
+        //一组用于参考的置信度阈值,包含以下三个字段。每个字段的值为一个 [0,100] 的浮点数,小数点后 3 位有效数字。
+        //1e-3:误识率为千分之一的置信度阈值;
+        //1e-4:误识率为万分之一的置信度阈值;
+        //1e-5:误识率为十万分之一的置信度阈值;
+        JSONObject thresholdsJsonObject = jsonObject.getJSONObject("thresholds");
+        double le4 = thresholdsJsonObject.getDouble("1e-4");
+        //如果置信值低于“千分之一”阈值则不建议认为是同一个人;如果置信值超过“十万分之一”阈值,则是同一个人的几率非常高。
+        return confidence >= le4;
+    }
+
+}

+ 176 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamRecordDataServiceImpl.java

@@ -0,0 +1,176 @@
+package cn.com.qmth.examcloud.core.oe.task.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.student.api.ExamRecordDataCloudService;
+import cn.com.qmth.examcloud.core.oe.student.api.request.CalcExamScoreReq;
+import cn.com.qmth.examcloud.core.oe.student.api.request.CalcFaceBiopsyResultReq;
+import cn.com.qmth.examcloud.core.oe.student.api.response.CalcExamScoreResp;
+import cn.com.qmth.examcloud.core.oe.student.api.response.CalcFaceBiopsyResultResp;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CalculateFaceCheckResultInfo;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.support.enums.HandInExamType;
+import cn.com.qmth.examcloud.support.enums.IsSuccess;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import cn.com.qmth.examcloud.support.helper.FaceBiopsyHelper;
+import cn.com.qmth.examcloud.support.redis.RedisKeyHelper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * @Description 考试记录服务
+ * @Author lideyin
+ * @Date 2019/12/18 14:37
+ * @Version 1.0
+ */
+@Service("examRecordDataService")
+public class ExamRecordDataServiceImpl implements ExamRecordDataService {
+
+    @Autowired
+    private RedisClient redisClient;
+    @Autowired
+    private ExamCaptureQueueRepo examCaptureQueueRepo;
+    @Autowired
+    private ExamCaptureService examCaptureService;
+    @Autowired
+    private ExamRecordDataCloudService examRecordDataCloudService;
+
+    @Override
+    public void saveExamRecordDataCache(Long examRecordDataId, ExamRecordData data) {
+        String key = RedisKeyHelper.getBuilder().examRecordDataKey(examRecordDataId);
+        redisClient.set(key, data, 2592000);
+    }
+
+    @Override
+    public ExamRecordData getExamRecordDataCache(Long examRecordDataId) {
+        String key = RedisKeyHelper.getBuilder().examRecordDataKey(examRecordDataId);
+        return redisClient.get(key, ExamRecordData.class);
+    }
+
+    @Override
+    public void deleteExamRecordDataCache(Long examRecordDataId) {
+        String key = RedisKeyHelper.getBuilder().examRecordDataKey(examRecordDataId);
+        redisClient.delete(key);
+    }
+
+    /**
+     * 计算自动审核结果
+     *
+     * @param isNoPhotoAndIllegality
+     * @param faceVerifyResult
+     * @param isIllegality
+     * @return
+     */
+    @Override
+    public Boolean calcAutoAuditResult(Boolean isNoPhotoAndIllegality, IsSuccess faceVerifyResult, Boolean isIllegality) {
+        //无照片违纪自动审核
+        if (isNoPhotoAndIllegality != null && isNoPhotoAndIllegality) {
+            return true;
+        }
+
+        //活体检测失败违纪自动审核
+        if (null != faceVerifyResult && IsSuccess.FAILED == faceVerifyResult && null != isIllegality && isIllegality) {
+            return true;
+        }
+
+        return null;
+    }
+
+    /**
+     * 交卷后续处理
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    @Override
+    public ExamRecordData processAfterHandInExam(Long examRecordDataId) {
+        ExamRecordData examRecordData = getExamRecordDataCache(examRecordDataId);
+
+        //只有开启的人脸的才会进行如下处理
+        if (FaceBiopsyHelper.isFaceEnable(examRecordData.getRootOrgId(),
+                examRecordData.getExamId(), examRecordData.getStudentId())) {
+            // 判断是否存在未处理的图片
+            boolean existUnhandledExamCaptureQueue = (examCaptureQueueRepo
+                    .existsUnhandledByExamRecordDataId(examRecordDataId) != null);
+            if (existUnhandledExamCaptureQueue) {
+                throw new StatusException(Constants.CAPTURE_PROCESSING_STATUS_CODE, "PROCESSING");
+            }
+
+            // 计算人脸检测结果
+            CalculateFaceCheckResultInfo faceCheckResult = examCaptureService.calculateFaceCheckResult(examRecordDataId);
+            if (null != faceCheckResult.getFaceTotalCount()) {
+                examRecordData.setFaceTotalCount(faceCheckResult.getFaceTotalCount());
+            }
+            if (null != faceCheckResult.getFaceSuccessPercent()) {
+                examRecordData.setFaceSuccessPercent(faceCheckResult.getFaceSuccessPercent());
+            }
+            if (null != faceCheckResult.getFaceStrangerCount()) {
+                examRecordData.setFaceStrangerCount(faceCheckResult.getFaceStrangerCount());
+            }
+            if (null != faceCheckResult.getFaceSuccessCount()) {
+                examRecordData.setFaceSuccessCount(faceCheckResult.getFaceSuccessCount());
+            }
+            if (null != faceCheckResult.getFaceFailedCount()) {
+                examRecordData.setFaceFailedCount(faceCheckResult.getFaceFailedCount());
+            }
+            if (null != faceCheckResult.getBaiduFaceLivenessSuccessPercent()) {
+                examRecordData.setBaiduFaceLivenessSuccessPercent(faceCheckResult.getBaiduFaceLivenessSuccessPercent());
+            }
+
+            // 计算活体检测结果
+            CalcFaceBiopsyResultReq req = new CalcFaceBiopsyResultReq();
+            req.setExamRecordDataId(examRecordDataId);
+            CalcFaceBiopsyResultResp calcFaceBiopsyResultResp = examRecordDataCloudService.calcFaceBiopsyResult(req);
+            if (null != calcFaceBiopsyResultResp.getFaceVerifyResult()) {
+                examRecordData.setFaceVerifyResult(calcFaceBiopsyResultResp.getFaceVerifyResult());
+            }
+
+            //判断是否违纪
+            if (null != calcFaceBiopsyResultResp.getFaceVerifyResult() &&
+                    IsSuccess.FAILED == calcFaceBiopsyResultResp.getFaceVerifyResult()) {
+                examRecordData.setIsIllegality(true);//活检失败,直接判定为违纪
+            } else {
+                //人脸检测或活栓中有一个判断为违纪,则为违纪
+                boolean bi = (null != faceCheckResult.getIsIllegality() && faceCheckResult.getIsIllegality()) ||
+                        (null != calcFaceBiopsyResultResp.getIsIllegality() && calcFaceBiopsyResultResp.getIsIllegality());
+                examRecordData.setIsIllegality(bi);
+            }
+
+            //判断数据是否异常
+            boolean bw = (null != faceCheckResult.getIsWarn() && faceCheckResult.getIsWarn()) ||
+                    (null != calcFaceBiopsyResultResp.getIsWarn() && calcFaceBiopsyResultResp.getIsWarn());
+            examRecordData.setIsWarn(bw);//人脸检测或活检中有一个判断为异常,则为异常数据
+
+            // 违纪自动审核
+            Boolean isAudit = this.calcAutoAuditResult(faceCheckResult.getNoPhotoAndIllegality(),
+                    calcFaceBiopsyResultResp.getFaceVerifyResult(), calcFaceBiopsyResultResp.getIsIllegality());
+            if (null != isAudit) {
+                examRecordData.setIsAudit(isAudit);
+            }
+        }
+
+        //自动计算客观分
+        CalcExamScoreReq cesReq = new CalcExamScoreReq();
+        cesReq.setExamRecordDataId(examRecordDataId);
+        CalcExamScoreResp calcExamScoreResp = examRecordDataCloudService.calcExamScore(cesReq);
+        examRecordData.setObjectiveScore(calcExamScoreResp.getObjectiveScore());
+        examRecordData.setObjectiveAccuracy(calcExamScoreResp.getObjectiveAccuracy());
+        examRecordData.setSuccPercent(calcExamScoreResp.getSuccPercent());
+        examRecordData.setTotalScore(calcExamScoreResp.getTotalScore());
+
+        // 更新考试状态
+        examRecordData.setExamRecordStatus(
+                examRecordData.getHandInExamType() == HandInExamType.MANUAL
+                        ? ExamRecordStatus.EXAM_END
+                        : ExamRecordStatus.EXAM_OVERDUE);
+
+        //更新考试记录中的相关数据
+        this.saveExamRecordDataCache(examRecordDataId, examRecordData);
+
+        return examRecordData;
+    }
+}

+ 68 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamSyncCaptureServiceImpl.java

@@ -0,0 +1,68 @@
+package cn.com.qmth.examcloud.core.oe.task.service.impl;
+
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamSyncCaptureRepo;
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamSyncCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamSyncCaptureService;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CompareFaceSyncInfo;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Example;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * @Description 同步抓拍照片
+ * @Author lideyin
+ * @Date 2019/12/6 16:20
+ * @Version 1.0
+ */
+@SuppressWarnings("ALL")
+@Service("examSyncCaptureService")
+public class ExamSyncCaptureServiceImpl implements ExamSyncCaptureService {
+
+
+    @Autowired
+    private ExamSyncCaptureRepo examSyncCaptureRepo;
+
+    @Autowired
+    private RedisClient redisClient;
+
+    /**
+     * 保存同步比较的抓拍照片结果
+     *
+     * @param studentId 学生id
+     */
+    @Override
+    @Transactional
+    public void saveExamCaptureSyncCompareResult(Long studentId, Long examRecordDataId) {
+        String syncCopareResultkey = Constants.FACE_SYNC_COMPARE_RESULT_PREFIX + studentId;
+        CompareFaceSyncInfo compareFaceSyncInfo =
+                redisClient.get(syncCopareResultkey, CompareFaceSyncInfo.class);
+        if (compareFaceSyncInfo == null) {
+            return;
+        }
+
+        ExamSyncCaptureEntity examCaptureEntity = new ExamSyncCaptureEntity();
+        examCaptureEntity.setExamRecordDataId(examRecordDataId);
+        examCaptureEntity.setFaceCompareResult(compareFaceSyncInfo.getFaceCompareResult());
+        examCaptureEntity.setFileName(compareFaceSyncInfo.getFileName());
+        examCaptureEntity.setFileUrl(compareFaceSyncInfo.getFileUrl());
+        examCaptureEntity.setIsStranger(compareFaceSyncInfo.getIsStranger());
+        examCaptureEntity.setIsPass(compareFaceSyncInfo.getIsPass());
+        examCaptureEntity.setProcessTime(compareFaceSyncInfo.getProcessTime());
+
+        //照片处理结果中如果已存在,则以已有的数据为准
+        ExamSyncCaptureEntity query = new ExamSyncCaptureEntity();
+        query.setExamRecordDataId(examRecordDataId);
+        query.setFileName(compareFaceSyncInfo.getFileName());
+        Example<ExamSyncCaptureEntity> example = Example.of(query);
+        if (!examSyncCaptureRepo.exists(example)) {
+            examSyncCaptureRepo.save(examCaptureEntity);
+        }
+
+        //删除redis中的同步数据
+        redisClient.delete(syncCopareResultkey);
+    }
+
+}

+ 39 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamingSessionServiceImpl.java

@@ -0,0 +1,39 @@
+package cn.com.qmth.examcloud.core.oe.task.service.impl;
+
+import cn.com.qmth.examcloud.core.oe.task.service.ExamingSessionService;
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+import cn.com.qmth.examcloud.support.redis.RedisKeyHelper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author chenken
+ * @date 2018/8/15 9:24
+ * @company QMTH
+ * @description 考试会话服务实现
+ */
+@Service("examingSessionService")
+public class ExamingSessionServiceImpl implements ExamingSessionService {
+
+    @Autowired
+    private RedisClient redisClient;
+
+    @Override
+    public void saveExamingSession(Long studentId, ExamingSession examingSession) {
+        String key = RedisKeyHelper.getBuilder().examingSessionKey(studentId);
+        redisClient.set(key,examingSession,2592000);
+    }
+
+    @Override
+    public ExamingSession getExamingSession(Long studentId) {
+        String key = RedisKeyHelper.getBuilder().examingSessionKey(studentId);
+        return redisClient.get(key,ExamingSession.class);
+    }
+
+    @Override
+    public void deleteExamingSession(Long studentId) {
+        String key = RedisKeyHelper.getBuilder().examingSessionKey(studentId);
+        redisClient.delete(key);
+    }
+}

+ 132 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/pipeline/AfterHandInExamExecutor.java

@@ -0,0 +1,132 @@
+package cn.com.qmth.examcloud.core.oe.task.service.pipeline;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.KeyValuePair;
+import cn.com.qmth.examcloud.commons.helpers.ObjectHolder;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.NodeExecuter;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.TaskContext;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.core.oe.student.api.ExamRecordDataCloudService;
+import cn.com.qmth.examcloud.core.oe.student.api.request.CalcExamScoreReq;
+import cn.com.qmth.examcloud.core.oe.student.api.request.CalcFaceBiopsyResultReq;
+import cn.com.qmth.examcloud.core.oe.student.api.response.CalcExamScoreResp;
+import cn.com.qmth.examcloud.core.oe.student.api.response.CalcFaceBiopsyResultResp;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CalculateFaceCheckResultInfo;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.support.enums.HandInExamType;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import cn.com.qmth.examcloud.web.helpers.SequenceLockHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @Description 交卷后续处理执行器
+ * @Author lideyin
+ * @Date 2019/12/17 16:39
+ * @Version 1.0
+ */
+@Component
+public class AfterHandInExamExecutor implements NodeExecuter<Long, ExamRecordData, Long, ExamRecordData> {
+
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+    private static Long DEFAULT_MAX_PROCESS_SECONDS = 30L;
+    @Autowired
+    private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(AfterHandInExamExecutor.class);
+
+
+    /**
+     * 执行
+     *
+     * @param key
+     * @param examRecordData
+     * @param outList
+     * @param removable
+     * @param context
+     * @throws Exception
+     * @author WANGWEI
+     */
+    @Override
+    public void execute(Long key, ExamRecordData examRecordData,
+                        List<KeyValuePair<Long, ExamRecordData>> outList,
+                        ObjectHolder<Boolean> removable, TaskContext context) throws Exception {
+
+        String sequenceLockKey = Constants.EXAM_CONTROL_LOCK_PREFIX + examRecordData.getStudentId();
+
+        long st = System.currentTimeMillis();
+
+        try {
+            this.debugLog("enter executor...", examRecordData.getId());
+
+            //添加考试控制全局锁
+            SequenceLockHelper.getLockSimple(sequenceLockKey);
+
+            this.debugLog("get locker success...", examRecordData.getId());
+
+            //针对已交卷的数据进行交卷后续处理
+            if (examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_HAND_IN ||
+                    examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_AUTO_HAND_IN) {
+
+                this.debugLog("prepare to execute after hand in ...", examRecordData.getId());
+
+                SysPropertyCacheBean maxProcessSecondsProperty = CacheHelper.getSysProperty("oe.task.maxProcessSeconds");
+                //本节点最大处理时长
+                Long maxProcessSeconds;
+                if (maxProcessSecondsProperty.getHasValue()) {
+                    maxProcessSeconds = Long.valueOf(maxProcessSecondsProperty.getValue().toString());
+                } else {
+                    maxProcessSeconds = DEFAULT_MAX_PROCESS_SECONDS;
+                }
+
+                //交卷时间戳
+                Long handInTime = (examRecordData.getEndTime() == null
+                        ? examRecordData.getCleanTime()
+                        : examRecordData.getEndTime()).getTime();
+                Long times = System.currentTimeMillis() - handInTime;
+
+                //如果交卷后超过指定时长内仍未处理完成,则交给下一节点进行处理
+                if (times > maxProcessSeconds * 1000) {
+                    outList.add(new KeyValuePair<>(key, examRecordData));
+
+                    this.debugLog("not complete in time and will retry in next node...", examRecordData.getId());
+
+                    return;
+                }
+
+                examRecordData = examRecordDataService.processAfterHandInExam(examRecordData.getId());
+
+                outList.add(new KeyValuePair<>(key, examRecordData));
+
+                this.debugLog("after hand in success..", examRecordData.getId());
+
+                return;
+            }
+
+            //其它状态数据,直接交给下一步
+            outList.add(new KeyValuePair<>(key, examRecordData));
+
+            this.debugLog("current status is '" + examRecordData.getExamRecordStatus().name() +
+                    "'.do nothing and go to the next node...", examRecordData.getId());
+        } finally {
+            SequenceLockHelper.releaseLockSimple(sequenceLockKey);
+            this.debugLog("T999 HandInExamExecutor合计耗时:" +
+                    (System.currentTimeMillis() - st) + " ms", examRecordData.getId());
+        }
+    }
+
+    private void debugLog(String msg, Long examRecordDataId) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("[AFTER-HAND-IN-EXAM-EXECUTOR-" + examRecordDataId + "]:" + msg);
+        }
+    }
+}

+ 151 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/pipeline/ClearExamDataCacheExecutor.java

@@ -0,0 +1,151 @@
+package cn.com.qmth.examcloud.core.oe.task.service.pipeline;
+
+import cn.com.qmth.examcloud.commons.helpers.KeyValuePair;
+import cn.com.qmth.examcloud.commons.helpers.ObjectHolder;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.NodeExecuter;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.TaskContext;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamingSessionService;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.enums.SyncStatus;
+import cn.com.qmth.examcloud.support.examing.ExamBoss;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+import cn.com.qmth.examcloud.support.redis.RedisKeyHelper;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.helpers.SequenceLockHelper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @Description 清理考试记录缓存
+ * @Author lideyin
+ * @Date 2019/12/24 18:07
+ * @Version 1.0
+ */
+@Component
+public class ClearExamDataCacheExecutor implements NodeExecuter<Long, ExamRecordData, Long, ExamRecordData> {
+
+    @Autowired
+    private RedisClient redisClient;
+    @Autowired
+    private ExamingSessionService examingSessionService;
+    private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(ClearExamDataCacheExecutor.class);
+
+    /**
+     * 执行
+     *
+     * @param key
+     * @param examRecordData
+     * @param outList
+     * @param removable
+     * @param context
+     * @throws Exception
+     */
+    @Override
+    public void execute(Long key, ExamRecordData examRecordData,
+                        List<KeyValuePair<Long, ExamRecordData>> outList,
+                        ObjectHolder<Boolean> removable, TaskContext context) throws Exception {
+
+        String sequenceLockKey = Constants.EXAM_CONTROL_LOCK_PREFIX + examRecordData.getStudentId();
+
+        long st = System.currentTimeMillis();
+
+        try {
+            Long examRecordDataId = examRecordData.getId();
+            debugLog("enter executor...", examRecordDataId);
+
+            //添加考试控制全局锁
+            SequenceLockHelper.getLockSimple(sequenceLockKey);
+
+            debugLog("get locker success...", examRecordDataId);
+
+            //只有已同步成功的数据,才执行清理操作
+            if (SyncStatus.SYNCED == examRecordData.getSyncStatus()) {
+
+                //清除考试次数相关缓存
+                clearExamBoss(examRecordData.getExamStudentId(), examRecordDataId);
+
+                //清除学生最后活动时间缓存
+                redisClient.delete(RedisKeyHelper.getBuilder().examingActiveTimeKey(examRecordDataId));
+
+                //清除心跳缓存
+                redisClient.delete(RedisKeyHelper.getBuilder().examingHeartbeatKey(examRecordDataId));
+
+                //清除考试会话缓存
+                ExamingSession examingSession = examingSessionService.getExamingSession(examRecordData.getStudentId());
+                if (null != examingSession && examingSession.getExamRecordDataId().equals(examRecordDataId)) {
+                    String sessionKey = RedisKeyHelper.getBuilder().examingSessionKey(examRecordData.getStudentId());
+                    redisClient.delete(sessionKey);
+                    this.infoLog(String.format("清理交卷未删除的会话:%s", sessionKey), examRecordDataId);
+                }
+                redisClient.delete(RedisKeyHelper.getBuilder().examingSessionKey(examRecordData.getStudentId()));
+
+                //清除文件作答和普通作答记录缓存
+                Integer quesCount = examRecordData.getQuestionCount();
+                for (int i = 1; i <= quesCount; i++) {
+                    redisClient.delete(RedisKeyHelper.getBuilder().studentFileAnswerKey(examRecordDataId, i));
+                    redisClient.delete(RedisKeyHelper.getBuilder().studentAnswerKey(examRecordDataId, i));
+                }
+
+                //清除网考试卷结构
+                redisClient.delete(RedisKeyHelper.getBuilder().studentPaperKey(examRecordDataId));
+
+                //清除考试记录缓存(设置缓存的过期时间)
+                Long defaultExpiredSeconds = PropertyHolder.getLong("oe.student.cache.expiredSeconds", 60);//缓存默认过期时间
+//                redisClient.delete(RedisKeyHelper.getBuilder().examRecordDataKey(examRecordDataId));
+                redisClient.expire(RedisKeyHelper.getBuilder().examRecordDataKey(examRecordDataId),
+                        defaultExpiredSeconds, TimeUnit.SECONDS);
+
+                this.debugLog("all is over.", examRecordDataId);
+
+                return;
+            }
+
+            this.debugLog("current status is '" + examRecordData.getExamRecordStatus().name() +
+                    "'.may be redundant data.", examRecordDataId);
+
+        } finally {
+            SequenceLockHelper.releaseLockSimple(sequenceLockKey);
+            this.debugLog("T999 ClearExamDataCacheExecutor合计耗时:" +
+                    (System.currentTimeMillis() - st) + " ms", examRecordData.getId());
+        }
+    }
+
+    private void clearExamBoss(Long examStudentId, Long examRecordDataId) {
+        String examBossKey = RedisKeyHelper.getBuilder().examBossKey(examStudentId);
+        ExamBoss examBoss = redisClient.get(examBossKey, ExamBoss.class);
+        if (null != examBoss) {
+            //如果开考次数==考试完结次数,则删除考试基础信息缓存
+            if (examBoss.getStartCount() == examBoss.getEndCount()) {
+                redisClient.delete(examBossKey);
+            }
+            //如果examBoss未清除,则清除当前的考试记录id
+            else {
+                List<Long> examRecordDataIds = examBoss.getExamRecordDataIds();
+                if (null != examRecordDataIds && !examRecordDataIds.isEmpty()) {
+                    examRecordDataIds.removeIf(p -> examRecordDataId.equals(p));
+                    examBoss.setExamRecordDataIds(examRecordDataIds);
+                    redisClient.set(examBossKey, examBoss);
+                }
+            }
+        }
+    }
+
+    private void debugLog(String msg, Long examRecordDataId) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("[CLEAR-EXAM-DATA-CACHE-EXECUTOR-" + examRecordDataId + "]:" + msg);
+        }
+    }
+
+    private void infoLog(String msg, Long examRecordDataId) {
+        if (LOG.isInfoEnabled()) {
+            LOG.info("[CLEAR-EXAM-DATA-CACHE-EXECUTOR-" + examRecordDataId + "]:" + msg);
+        }
+    }
+}

+ 90 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/pipeline/DataGainExamExecutor.java

@@ -0,0 +1,90 @@
+package cn.com.qmth.examcloud.core.oe.task.service.pipeline;
+
+import java.util.Date;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.examcloud.commons.helpers.KeyValuePair;
+import cn.com.qmth.examcloud.commons.helpers.ObjectHolder;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.NodeExecuter;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.TaskContext;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.core.oe.student.api.ExamRecordDataCloudService;
+import cn.com.qmth.examcloud.core.oe.student.api.request.GetExamRecordDataIdsReq;
+import cn.com.qmth.examcloud.core.oe.student.api.request.UpdateExamRecordDataBatchNumReq;
+import cn.com.qmth.examcloud.core.oe.student.api.request.UpdateExamRecordStatusReq;
+import cn.com.qmth.examcloud.core.oe.student.api.response.GetExamRecordDataIdsResp;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+
+/**
+ * @Description 获取待处理数据
+ * @Author xt
+ * @Date 2019/12/17 16:39
+ * @Version 1.0
+ */
+@Component
+public class DataGainExamExecutor implements NodeExecuter<Long, ExamRecordData, Long, ExamRecordData> {
+
+    private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(DataGainExamExecutor.class);
+
+    private final static Integer batchSize = 200;
+
+    private final static Long batchNum = new Date().getTime();
+
+    @Autowired
+    private ExamRecordDataCloudService examRecordDataCloudService;
+
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+
+    @Override
+    public void execute(Long key, ExamRecordData value, List<KeyValuePair<Long, ExamRecordData>> outList,
+            ObjectHolder<Boolean> removable, TaskContext context) throws Exception {
+        // 获取考试信息id
+        Long startId = 0l;
+        GetExamRecordDataIdsReq req = new GetExamRecordDataIdsReq();
+        req.setBatchNum(batchNum);
+        req.setSize(batchSize);
+        for (;;) {
+            req.setStartId(startId);
+            GetExamRecordDataIdsResp res = examRecordDataCloudService.getExamRecordDataIds(req);
+            List<Long> ids = res.getExamRecordDataIds();
+            if (ids == null || ids.size() == 0) {
+                return;
+            }
+            for (Long id : ids) {
+                // 根据id获取考试信息缓存
+                ExamRecordData erd = examRecordDataService.getExamRecordDataCache(id);
+                if (erd == null) {
+                    LOG.error("获取Redis中考试信息为空 examRecordDataId:"+id);
+                    updateExamRecordStatusError(id);
+                }else {
+                    outList.add(new KeyValuePair<Long, ExamRecordData>(id, erd));
+                }
+            }
+
+            // 修改已获取过的考试信息batchNum
+            UpdateExamRecordDataBatchNumReq ureq = new UpdateExamRecordDataBatchNumReq();
+            ureq.setBatchNum(batchNum);
+            ureq.setIds(ids);
+            examRecordDataCloudService.updateExamRecordDataBatchNum(ureq);
+            startId = ids.get(ids.size() - 1);
+        }
+    }
+
+    private void updateExamRecordStatusError(Long id) {
+        try {
+            UpdateExamRecordStatusReq ureq = new UpdateExamRecordStatusReq();
+            ureq.setExamRecordStatus(ExamRecordStatus.EXAM_ERROR.name());
+            ureq.setId(id);
+            examRecordDataCloudService.updateExamRecordStatus(ureq);
+        } catch (Exception e) {
+            LOG.error("修改考试记录状态失败 examRecordDataId:"+id);;
+        }
+    }
+}

+ 215 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/pipeline/HandInExamExecutor.java

@@ -0,0 +1,215 @@
+package cn.com.qmth.examcloud.core.oe.task.service.pipeline;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.KeyValuePair;
+import cn.com.qmth.examcloud.commons.helpers.ObjectHolder;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.NodeExecuter;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.TaskContext;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.core.oe.student.api.ExamRecordDataCloudService;
+import cn.com.qmth.examcloud.core.oe.student.api.request.HandInExamReq;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamingSessionService;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.support.enums.HandInExamType;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import cn.com.qmth.examcloud.support.examing.ExamingActivityTime;
+import cn.com.qmth.examcloud.support.examing.ExamingHeartbeat;
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+import cn.com.qmth.examcloud.support.redis.RedisKeyHelper;
+import cn.com.qmth.examcloud.web.helpers.SequenceLockHelper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @Description 自动交卷执行器
+ * @Author lideyin
+ * @Date 2019/12/17 16:39
+ * @Version 1.0
+ */
+@Component
+public class HandInExamExecutor implements NodeExecuter<Long, ExamRecordData, Long, ExamRecordData> {
+
+    @Autowired
+    private ExamingSessionService examingSessionService;
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+    @Autowired
+    private RedisClient redisClient;
+    @Autowired
+    private ExamRecordDataCloudService examRecordDataCloudService;
+    @Autowired
+    private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(HandInExamExecutor.class);
+
+    /**
+     * 执行
+     *
+     * @param key
+     * @param uncertainExamRecordData 不确定的考试记录,数据有可能已变更
+     * @param outList
+     * @param removable
+     * @param context
+     * @throws Exception
+     * @author WANGWEI
+     */
+    @Override
+    public void execute(Long key, ExamRecordData uncertainExamRecordData,
+                        List<KeyValuePair<Long, ExamRecordData>> outList,
+                        ObjectHolder<Boolean> removable, TaskContext context) throws Exception {
+
+        String sequenceLockKey = Constants.EXAM_CONTROL_LOCK_PREFIX + uncertainExamRecordData.getStudentId();
+
+        try {
+
+            this.debugLog("enter executor...", uncertainExamRecordData.getId());
+
+            //添加考试控制全局锁
+            SequenceLockHelper.getLockSimple(sequenceLockKey);
+
+            this.debugLog("get locker success...", uncertainExamRecordData.getId());
+
+            //获取最新的考试记录状态
+            ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(uncertainExamRecordData.getId());
+
+            Long examRecordDataId = examRecordData.getId();
+
+            //处理正在进行中的考试
+            if (examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_ING) {
+
+                ExamingSession examingSession = examingSessionService.getExamingSession(examRecordData.getStudentId());
+
+                if (null == examingSession) {
+                    this.debugLog("examingSession is null...", examRecordDataId);
+                } else {
+                    this.debugLog("examingSession is not null and the json is..." + JsonUtil.toJson(examingSession),
+                            examRecordDataId);
+                }
+
+                // 如果考试会话不存在/超过断点续考时间,自动交卷
+                if (null == examingSession || isOverBreakpointTime(examingSession)) {
+
+                    try {
+
+                        this.debugLog("prepare to auto hand in...", examRecordDataId);
+
+                        //自动交卷
+                        HandInExamReq handInExamReq = new HandInExamReq();
+                        handInExamReq.setExamRecordDataId(examRecordDataId);
+                        handInExamReq.setHandInExamType(HandInExamType.AUTO);
+                        examRecordDataCloudService.handInExam(handInExamReq);
+
+                        //更改内存中的交卷状态
+                        examRecordData.setExamRecordStatus(ExamRecordStatus.EXAM_AUTO_HAND_IN);
+                        examRecordData.setCleanTime(new Date());
+
+                        //更新考试记录缓存
+                        examRecordDataService.saveExamRecordDataCache(examRecordDataId, examRecordData);
+
+                        // 删除考试会话
+                        if (null != examingSession) {
+                            examingSessionService.deleteExamingSession(examRecordData.getStudentId());
+                        }
+
+                        outList.add(new KeyValuePair<>(key, examRecordData));
+
+                        this.debugLog("auto hand in success...", examRecordDataId);
+
+                        return;
+
+                    } catch (Exception e) {
+                        //回滚自动交卷操作
+                        examRecordData.setExamRecordStatus(ExamRecordStatus.EXAM_ING);
+                        examRecordData.setCleanTime(null);
+                        examRecordDataService.saveExamRecordDataCache(examRecordDataId, examRecordData);
+
+                        outList.clear();
+                        removable.set(false);
+
+                        this.errorLog("auto hand in occurs error,to be retry...", e, examRecordDataId);
+
+                        throw new StatusException("300101", "自动交卷出现异常:" + e.getMessage());
+                    }
+                }
+
+                //如果不需要自动交卷,则需要下次轮循,继续处理
+                removable.set(false);
+                outList.clear();
+
+                this.debugLog("don't need auto hand in.to be retry...", examRecordDataId);
+
+                return;
+            }
+
+            //其它状态的数据,直接交给下一个节点处理
+            outList.add(new KeyValuePair<>(key, examRecordData));
+
+            this.debugLog("current status is '" + examRecordData.getExamRecordStatus().name() +
+                    "'.do nothing and go to the next node...", examRecordDataId);
+        } finally {
+            SequenceLockHelper.releaseLockSimple(sequenceLockKey);
+        }
+    }
+
+    /**
+     * 是否超过考试时长
+     *
+     * @param examingSession
+     * @return
+     */
+    private boolean isOverExamTime(ExamingSession examingSession) {
+        String examingHeartbeatKey = RedisKeyHelper.getBuilder()
+                .examingHeartbeatKey(examingSession.getExamRecordDataId());
+        ExamingHeartbeat examingHeartbeat = redisClient.get(examingHeartbeatKey,
+                ExamingHeartbeat.class);
+
+        //秒
+        long cost = null == examingHeartbeat ? 0 : examingHeartbeat.getCost();
+        return examingSession.getExamDuration() <= cost * 1000;
+    }
+
+    /**
+     * 是否超过断点续考时间
+     *
+     * @param examingSession
+     * @return
+     */
+    private boolean isOverBreakpointTime(ExamingSession examingSession) {
+        long now = System.currentTimeMillis();
+
+        String examingActiveTimeKey = RedisKeyHelper.getBuilder()
+                .examingActiveTimeKey(examingSession.getExamRecordDataId());
+        ExamingActivityTime examingActiveTime = redisClient.get(examingActiveTimeKey,
+                ExamingActivityTime.class);
+
+        if (null == examingActiveTime) {
+            this.debugLog("validating isOverBreakpointTime... and examingActiveTime is null...", examingSession.getExamRecordDataId());
+        } else {
+            this.debugLog("validating isOverBreakpointTime... and the json of examingActiveTime is..." + JsonUtil.toJson(examingActiveTime)
+                    , examingSession.getExamRecordDataId());
+        }
+
+        long activeTime = null == examingActiveTime
+                ? System.currentTimeMillis()
+                : examingActiveTime.getActiveTime();
+        return now - activeTime >= examingSession.getExamReconnectTime().intValue() * 60 * 1000;
+    }
+
+    private void debugLog(String msg, Long examRecordDataId) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("[HAND-IN-EXAM-EXECUTOR-" + examRecordDataId + "]:" + msg);
+        }
+    }
+
+    private void errorLog(String msg, Exception e, Long examRecordDataId) {
+        if (LOG.isErrorEnabled()) {
+            LOG.error("[HAND-IN-EXAM-EXECUTOR-" + examRecordDataId + "]:" + msg, e);
+        }
+    }
+}

+ 449 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/pipeline/SyncExamDataExecutor.java

@@ -0,0 +1,449 @@
+package cn.com.qmth.examcloud.core.oe.task.service.pipeline;
+
+import cn.com.qmth.examcloud.commons.helpers.KeyValuePair;
+import cn.com.qmth.examcloud.commons.helpers.ObjectHolder;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.NodeExecuter;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.TaskContext;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.core.oe.admin.api.SyncExamDataCloudService;
+import cn.com.qmth.examcloud.core.oe.admin.api.bean.*;
+import cn.com.qmth.examcloud.core.oe.admin.api.request.SyncExamDataReq;
+import cn.com.qmth.examcloud.core.oe.student.api.ExamRecordDataCloudService;
+import cn.com.qmth.examcloud.core.oe.student.api.bean.StuExamQuestionBean;
+import cn.com.qmth.examcloud.core.oe.student.api.request.GetExamFaceLivenessVerifiesReq;
+import cn.com.qmth.examcloud.core.oe.student.api.request.GetExamRecordPaperStructReq;
+import cn.com.qmth.examcloud.core.oe.student.api.request.GetExamRecordQuestionsReq;
+import cn.com.qmth.examcloud.core.oe.student.api.request.GetFaceBiopsyReq;
+import cn.com.qmth.examcloud.core.oe.student.api.response.GetExamFaceLivenessVerifiesResp;
+import cn.com.qmth.examcloud.core.oe.student.api.response.GetExamRecordPaperStructResp;
+import cn.com.qmth.examcloud.core.oe.student.api.response.GetExamRecordQuestionsResp;
+import cn.com.qmth.examcloud.core.oe.student.api.response.GetFaceBiopsyResp;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureRepo;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamSyncCaptureRepo;
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamSyncCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamBossService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.support.enums.SyncStatus;
+import cn.com.qmth.examcloud.support.examing.ExamBoss;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import cn.com.qmth.examcloud.support.helper.FaceBiopsyHelper;
+import cn.com.qmth.examcloud.web.helpers.SequenceLockHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @Description 同步考试数据执行器
+ * @Author lideyin
+ * @Date 2019/12/19 16:22
+ * @Version 1.0
+ */
+@Component
+public class SyncExamDataExecutor implements NodeExecuter<Long, ExamRecordData, Long, ExamRecordData> {
+
+    @Autowired
+    private SyncExamDataCloudService syncExamDataCloudService;
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+    @Autowired
+    private ExamCaptureRepo examCaptureRepo;
+    @Autowired
+    private ExamSyncCaptureRepo examSyncCaptureRepo;
+    @Autowired
+    private ExamRecordDataCloudService examRecordDataCloudService;
+    @Autowired
+    private ExamBossService examBossService;
+    private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(SyncExamDataExecutor.class);
+
+    /**
+     * 执行
+     *
+     * @param key
+     * @param examRecordData
+     * @param outList
+     * @param removable
+     * @param context
+     * @throws Exception
+     * @author WANGWEI
+     */
+    @Override
+    public void execute(Long key, ExamRecordData examRecordData,
+                        List<KeyValuePair<Long, ExamRecordData>> outList,
+                        ObjectHolder<Boolean> removable, TaskContext context) throws Exception {
+
+        Long studentId = examRecordData.getStudentId();
+        String sequenceLockKey = Constants.EXAM_CONTROL_LOCK_PREFIX + studentId;
+
+        long st = System.currentTimeMillis();
+
+        try {
+            debugLog("enter executor...", examRecordData.getId());
+
+            //添加考试控制全局锁
+            SequenceLockHelper.getLockSimple(sequenceLockKey);
+
+            debugLog("get locker success...", examRecordData.getId());
+
+            //如果已同步,直接交给下一节点
+            if (SyncStatus.SYNCED == examRecordData.getSyncStatus()) {
+
+                outList.add(new KeyValuePair<>(key, examRecordData));
+
+                this.debugLog("already synced,will go to next node...", examRecordData.getId());
+
+                return;
+            }
+
+            //处理上一节点中,指定时间内仍未处理完成的数据
+            Long examRecordDataId = examRecordData.getId();
+            if (ExamRecordStatus.EXAM_HAND_IN == examRecordData.getExamRecordStatus() ||
+                    ExamRecordStatus.EXAM_AUTO_HAND_IN == examRecordData.getExamRecordStatus()) {
+
+                this.debugLog("prepare to execute last node's hand in data...", examRecordDataId);
+                long startTime = System.currentTimeMillis();
+                examRecordData = examRecordDataService.processAfterHandInExam(examRecordDataId);
+
+                this.debugLog("T001.execute last node's hand in data success...耗时:" +
+                        (System.currentTimeMillis() - startTime) + " ms", examRecordDataId);
+            }
+
+            //如果考试记录状态为完结状态,则开始同步数据到正式库
+            if (ExamRecordStatus.EXAM_END == examRecordData.getExamRecordStatus() ||
+                    ExamRecordStatus.EXAM_OVERDUE == examRecordData.getExamRecordStatus()) {
+
+                this.debugLog("prepare to sync data...", examRecordDataId);
+
+                //同步数据
+                SyncExamDataReq syncReq = new SyncExamDataReq();
+
+                syncReq.setExamRecordData(copyExamRecordDataFrom(examRecordData));
+
+                syncReq.setExamRecordPaperStruct(getExamRecordPaperStruct(examRecordDataId));
+
+                syncReq.setExamRecordQuestions(getExamRecordQuestions(examRecordDataId));
+
+                //开启人脸检测相关数据赋值
+                Long rootOrgId = examRecordData.getRootOrgId();
+                Long examId = examRecordData.getExamId();
+                if (FaceBiopsyHelper.isFaceEnable(rootOrgId, examId, studentId)) {
+                    syncReq.setExamCaptures(getExamCaptures(examRecordDataId));
+
+                    syncReq.setExamSyncCapture(getExamSyncCapture(examRecordDataId));
+
+                    if (FaceBiopsyHelper.isFaceVerify(rootOrgId, examId, studentId)) {
+                        syncReq.setExamFaceLivenessVerifies(getExamFaceLivenessVerifies(examRecordDataId));
+
+                        syncReq.setFaceBiopsy(getFaceBiopsy(examRecordDataId));
+                    }
+                }
+                long startTime = System.currentTimeMillis();
+
+                //同步数据
+                syncExamDataCloudService.syncExamData(syncReq);
+
+                this.debugLog("T002.同步数据rpc耗时:" + (System.currentTimeMillis() - startTime) + " ms", examRecordDataId);
+
+                this.debugLog("sync data success,and to be change status...", examRecordDataId);
+
+                //考试完结次数加1
+                ExamBoss examBoss = examBossService.getExamBoss(examRecordData.getExamStudentId());
+                if (null != examBoss) {
+                    examBoss.setEndCount(examBoss.getEndCount() + 1);
+
+                    startTime = System.currentTimeMillis();
+
+                    examBossService.saveExamBoss(examRecordData.getExamStudentId(), examBoss);
+
+                    this.debugLog("T003.考试完结次数加1,并saveExamBoss耗时..." +
+                            (System.currentTimeMillis() - startTime) + " ms", examRecordDataId);
+                }
+
+                //设置并保存考试记录的同步状态
+                startTime = System.currentTimeMillis();
+
+                examRecordData.setSyncStatus(SyncStatus.SYNCED);
+                setAndSaveExamRecordDataSyncStatus(examRecordDataId);
+                this.debugLog("T004.设置并保存考试记录的同步状态,耗时..." +
+                        (System.currentTimeMillis() - startTime) + " ms", examRecordDataId);
+
+                outList.add(new KeyValuePair<>(key, examRecordData));
+
+                this.debugLog("sync data and change status success...", examRecordDataId);
+
+                return;
+            }
+
+            //其它状态的数据,直接交给下一个节点处理
+            outList.add(new KeyValuePair<>(key, examRecordData));
+
+            this.debugLog("current status is '" + examRecordData.getExamRecordStatus().name() +
+                    "'.do nothing and go to the next node...", examRecordData.getId());
+        } finally {
+            SequenceLockHelper.releaseLockSimple(sequenceLockKey);
+            this.debugLog("T999 同步数据合计耗时:" + (System.currentTimeMillis() - st) + " ms", examRecordData.getId());
+        }
+    }
+
+    private FaceBiopsyBean getFaceBiopsy(Long examRecordDataId) {
+        GetFaceBiopsyReq req = new GetFaceBiopsyReq();
+        req.setExamRecordDataId(examRecordDataId);
+
+        GetFaceBiopsyResp resp = examRecordDataCloudService.getFaceBiopsy(req);
+
+        if (null == resp.getFaceBiopsyBean()) {
+            return null;
+        }
+
+        cn.com.qmth.examcloud.core.oe.student.api.bean.FaceBiopsyBean faceBiopsyBean = resp.getFaceBiopsyBean();
+
+        FaceBiopsyBean bean = new FaceBiopsyBean();
+        bean.setErrorMsg(faceBiopsyBean.getErrorMsg());
+        bean.setExamRecordDataId(faceBiopsyBean.getExamRecordDataId());
+        bean.setResult(faceBiopsyBean.getResult());
+        bean.setRootOrgId(faceBiopsyBean.getRootOrgId());
+        bean.setVerifiedTimes(faceBiopsyBean.getVerifiedTimes());
+
+        List<FaceBiopsyItemBean> faceBiopsyItemList = new ArrayList<>();
+
+        for (cn.com.qmth.examcloud.core.oe.student.api.bean.FaceBiopsyItemBean faceBiopsyItemBean : faceBiopsyBean.getFaceBiopsyItems()) {
+            FaceBiopsyItemBean itemBean = new FaceBiopsyItemBean();
+
+            itemBean.setCompleted(faceBiopsyItemBean.getCompleted());
+            itemBean.setErrorMsg(faceBiopsyItemBean.getErrorMsg());
+            itemBean.setExamRecordDataId(faceBiopsyItemBean.getExamRecordDataId());
+            itemBean.setFaceBiopsyId(faceBiopsyItemBean.getFaceBiopsyId());
+            itemBean.setFaceBiopsyType(faceBiopsyItemBean.getFaceBiopsyType());
+            itemBean.setInFreezeTime(faceBiopsyItemBean.getInFreezeTime());
+            itemBean.setResult(faceBiopsyItemBean.getResult());
+
+            List<FaceBiopsyItemStepBean> itemStepList = new ArrayList<>();
+            for (cn.com.qmth.examcloud.core.oe.student.api.bean.FaceBiopsyItemStepBean faceBiopsyItemStepBean : faceBiopsyItemBean.getFaceBiopsyItemSteps()) {
+                FaceBiopsyItemStepBean itemStepBean = new FaceBiopsyItemStepBean();
+                itemStepBean.setAction(faceBiopsyItemStepBean.getAction());
+                itemStepBean.setActionStay(faceBiopsyItemStepBean.getActionStay());
+                itemStepBean.setErrorMsg(faceBiopsyItemStepBean.getErrorMsg());
+                itemStepBean.setExamRecordDataId(faceBiopsyItemStepBean.getExamRecordDataId());
+                itemStepBean.setFaceBiopsyItemId(faceBiopsyItemStepBean.getFaceBiopsyItemId());
+                itemStepBean.setResourceRelativePath(faceBiopsyItemStepBean.getResourceRelativePath());
+                itemStepBean.setResourceType(faceBiopsyItemStepBean.getResourceType());
+                itemStepBean.setResult(faceBiopsyItemStepBean.getResult());
+
+                itemStepList.add(itemStepBean);
+            }
+
+            itemBean.setFaceBiopsyItemSteps(itemStepList);
+
+        }
+
+        bean.setFaceBiopsyItems(faceBiopsyItemList);
+
+        return bean;
+
+    }
+
+    private ExamSyncCaptureBean getExamSyncCapture(Long examRecordDataId) {
+        ExamSyncCaptureEntity entity = examSyncCaptureRepo.findByExamRecordDataId(examRecordDataId);
+        if (null == entity) {
+            return null;
+        }
+
+        ExamSyncCaptureBean bean = new ExamSyncCaptureBean();
+        bean.setId(entity.getId());
+        bean.setExamRecordDataId(entity.getExamRecordDataId());
+        bean.setFileUrl(entity.getFileUrl());
+        bean.setFileName(entity.getFileName());
+        bean.setPass(entity.getIsPass());
+        bean.setFaceCompareResult(entity.getFaceCompareResult());
+        bean.setStranger(entity.getIsStranger());
+        bean.setLandmark(entity.getLandmark());
+        bean.setFacelivenessResult(entity.getFacelivenessResult());
+        bean.setUsedTime(entity.getUsedTime());
+        bean.setProcessTime(entity.getProcessTime());
+        bean.setHasVirtualCamera(entity.getHasVirtualCamera());
+        bean.setCameraInfos(entity.getCameraInfos());
+        bean.setExtMsg(entity.getExtMsg());
+
+        return bean;
+    }
+
+    private ExamRecordQuestionsBean getExamRecordQuestions(Long examRecordDataId) {
+        GetExamRecordQuestionsReq req = new GetExamRecordQuestionsReq();
+        req.setExamRecordDataId(examRecordDataId);
+        GetExamRecordQuestionsResp resp = examRecordDataCloudService.getExamRecordQuestions(req);
+
+        ExamRecordQuestionsBean bean = new ExamRecordQuestionsBean();
+        bean.setCreationTime(resp.getCreationTime());
+        bean.setExamRecordDataId(resp.getExamRecordDataId());
+
+        List<ExamQuestionBean> examQuestionBeanList = new ArrayList<>();
+        for (StuExamQuestionBean stuExamQuestionBean : resp.getExamQuestions()) {
+            ExamQuestionBean examQuestionBean = new ExamQuestionBean();
+            examQuestionBean.setExamRecordDataId(stuExamQuestionBean.getExamRecordDataId());
+            examQuestionBean.setMainNumber(stuExamQuestionBean.getMainNumber());
+            examQuestionBean.setQuestionId(stuExamQuestionBean.getQuestionId());
+            examQuestionBean.setOrder(stuExamQuestionBean.getOrder());
+            examQuestionBean.setQuestionScore(stuExamQuestionBean.getQuestionScore());
+            examQuestionBean.setQuestionType(stuExamQuestionBean.getQuestionType());
+            examQuestionBean.setCorrectAnswer(stuExamQuestionBean.getCorrectAnswer());
+            examQuestionBean.setStudentAnswer(stuExamQuestionBean.getStudentAnswer());
+            examQuestionBean.setStudentScore(stuExamQuestionBean.getStudentScore());
+            examQuestionBean.setAnswer(stuExamQuestionBean.getAnswer());
+            examQuestionBean.setSign(stuExamQuestionBean.getSign());
+            examQuestionBean.setOptionPermutation(stuExamQuestionBean.getOptionPermutation());
+            examQuestionBean.setAudioPlayTimes(stuExamQuestionBean.getAudioPlayTimes());
+
+            if (null != stuExamQuestionBean.getAnswerType()) {
+                examQuestionBean.setAnswerType(stuExamQuestionBean.getAnswerType().name());
+            }
+
+            examQuestionBeanList.add(examQuestionBean);
+        }
+
+        bean.setExamQuestionBeans(examQuestionBeanList);
+
+        return bean;
+    }
+
+    private ExamRecordPaperStructBean getExamRecordPaperStruct(Long examRecordDataId) {
+        GetExamRecordPaperStructReq req = new GetExamRecordPaperStructReq();
+        req.setExamRecordDataId(examRecordDataId);
+        GetExamRecordPaperStructResp resp = examRecordDataCloudService.getExamRecordPaperStruct(req);
+
+        ExamRecordPaperStructBean bean = new ExamRecordPaperStructBean();
+        bean.setId(resp.getId());
+        bean.setDefaultPaper(resp.getDefaultPaper());
+
+        return bean;
+    }
+
+    private List<ExamFaceLivenessVerifyBean> getExamFaceLivenessVerifies(Long examRecordDataId) {
+        GetExamFaceLivenessVerifiesReq req = new GetExamFaceLivenessVerifiesReq();
+        req.setExamRecordDataId(examRecordDataId);
+
+        GetExamFaceLivenessVerifiesResp resp = examRecordDataCloudService.getExamFaceLivenessVerifies(req);
+        if (null == resp.getExamFaceLivenessVerifis() || resp.getExamFaceLivenessVerifis().isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        debugLog("the result joson of method getExamFaceLivenessVerifies is :" + JsonUtil.toJson(resp), examRecordDataId);
+
+        List<ExamFaceLivenessVerifyBean> resultList = new ArrayList<>();
+        for (cn.com.qmth.examcloud.core.oe.student.api.bean.ExamFaceLivenessVerifyBean eflvb : resp.getExamFaceLivenessVerifis()) {
+            ExamFaceLivenessVerifyBean bean = new ExamFaceLivenessVerifyBean();
+            bean.setId(eflvb.getId());
+            bean.setExamRecordDataId(eflvb.getExamRecordDataId());
+            bean.setStartTime(eflvb.getStartTime());
+            bean.setUsedTime(eflvb.getUsedTime());
+            bean.setResultJson(eflvb.getResultJson());
+            bean.setVerifyResult(eflvb.getVerifyResult());
+            bean.setBizId(eflvb.getBizId());
+            bean.setIsError(eflvb.getIsError());
+            bean.setErrorMsg(eflvb.getErrorMsg());
+            bean.setOperateNum(eflvb.getOperateNum());
+
+            resultList.add(bean);
+        }
+
+        return resultList;
+    }
+
+    private List<ExamCaptureBean> getExamCaptures(Long examRecordDataId) {
+        List<ExamCaptureEntity> entityList = examCaptureRepo.findByExamRecordDataId(examRecordDataId);
+        if (null == entityList || entityList.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        List<ExamCaptureBean> resultList = new ArrayList<>();
+        for (ExamCaptureEntity entity : entityList) {
+            ExamCaptureBean bean = new ExamCaptureBean();
+
+            bean.setId(entity.getId());
+            bean.setExamRecordDataId(entity.getExamRecordDataId());
+            bean.setFileUrl(entity.getFileUrl());
+            bean.setFileName(entity.getFileName());
+            bean.setPass(entity.getIsPass());
+            bean.setFaceCompareResult(entity.getFaceCompareResult());
+            bean.setStranger(entity.getIsStranger());
+            bean.setLandmark(entity.getLandmark());
+            bean.setFacelivenessResult(entity.getFacelivenessResult());
+            bean.setUsedTime(entity.getUsedTime());
+            bean.setProcessTime(entity.getProcessTime());
+            bean.setHasVirtualCamera(entity.getHasVirtualCamera());
+            bean.setCameraInfos(entity.getCameraInfos());
+            bean.setExtMsg(entity.getExtMsg());
+
+            resultList.add(bean);
+        }
+
+        return resultList;
+    }
+
+    private ExamRecordDataBean copyExamRecordDataFrom(ExamRecordData examRecordData) {
+        ExamRecordDataBean data = new ExamRecordDataBean();
+        data.setId(examRecordData.getId());
+        data.setExamId(examRecordData.getExamId());
+        data.setExamType(examRecordData.getExamType() == null ? null : examRecordData.getExamType().toString());
+        data.setExamStudentId(examRecordData.getExamStudentId());
+        data.setStudentId(examRecordData.getStudentId());
+        data.setCourseId(examRecordData.getCourseId());
+        data.setOrgId(examRecordData.getOrgId());
+        data.setRootOrgId(examRecordData.getRootOrgId());
+        data.setBasePaperId(examRecordData.getBasePaperId());
+        data.setPaperType(examRecordData.getPaperType());
+        data.setExamRecordStatus(examRecordData.getExamRecordStatus() == null ? null : examRecordData.getExamRecordStatus().toString());
+        data.setStartTime(examRecordData.getStartTime());
+        data.setEndTime(examRecordData.getEndTime());
+        data.setCleanTime(examRecordData.getCleanTime());
+        data.setWarn(examRecordData.getIsWarn() == null ? false : examRecordData.getIsWarn());
+        data.setAudit(examRecordData.getIsAudit() == null ? false : examRecordData.getIsAudit());
+        data.setIllegality(examRecordData.getIsIllegality() == null ? false : examRecordData.getIsIllegality());
+        data.setUsedExamTime(examRecordData.getUsedExamTime());
+        data.setContinued(examRecordData.getIsContinued() == null ? false : examRecordData.getIsContinued());
+        data.setContinuedCount(examRecordData.getContinuedCount());
+        data.setExceed(examRecordData.getIsExceed() == null ? false : examRecordData.getIsExceed());
+        data.setFaceSuccessCount(examRecordData.getFaceSuccessCount());
+        data.setFaceFailedCount(examRecordData.getFaceFailedCount());
+        data.setFaceStrangerCount(examRecordData.getFaceStrangerCount());
+        data.setFaceTotalCount(examRecordData.getFaceTotalCount());
+        data.setFaceSuccessPercent(examRecordData.getFaceSuccessPercent());
+        data.setFaceVerifyResult(examRecordData.getFaceVerifyResult() == null ? null : examRecordData.getFaceVerifyResult().toString());
+        data.setBaiduFaceLivenessSuccessPercent(examRecordData.getBaiduFaceLivenessSuccessPercent());
+        data.setTotalScore(examRecordData.getTotalScore());
+        data.setObjectiveScore(examRecordData.getObjectiveScore());
+        data.setObjectiveAccuracy(examRecordData.getObjectiveAccuracy());
+        data.setSubjectiveScore(examRecordData.getSubjectiveScore());
+        data.setSuccPercent(examRecordData.getSuccPercent());
+        data.setAllObjectivePaper(examRecordData.getIsAllObjectivePaper());
+
+        return data;
+    }
+
+    /**
+     * 设置并保存考试记录的同步状态
+     *
+     * @param examRecordDataId
+     */
+    private void setAndSaveExamRecordDataSyncStatus(Long examRecordDataId) {
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+
+        if (SyncStatus.SYNCED == examRecordData.getSyncStatus()) {
+            return;
+        }
+
+        examRecordData.setSyncStatus(SyncStatus.SYNCED);
+        examRecordDataService.saveExamRecordDataCache(examRecordDataId, examRecordData);
+    }
+
+    private void debugLog(String msg, Long examRecordDataId) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("[SYNC-EXAM-DATA-EXECUTOR-" + examRecordDataId + "]:" + msg);
+        }
+    }
+}

+ 77 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/worker/BaiduFaceLivenessWorker.java

@@ -0,0 +1,77 @@
+package cn.com.qmth.examcloud.core.oe.task.service.worker;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.Worker;
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.WorkerController;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.oe.task.base.ExamCaptureProcessStatisticController;
+import cn.com.qmth.examcloud.core.oe.task.dao.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureQueueService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.ExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.logging.log4j.ThreadContext;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 百度活体检测工作线程
+ *
+ * @author lideyin 20190620
+ */
+public class BaiduFaceLivenessWorker implements Worker<ExamCaptureQueueInfo> {
+	private final Log captureLog = LogFactory.getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+	@Override
+	public void process(WorkerController controller, ExamCaptureQueueInfo element) {
+		ThreadContext.put("TRACE_ID", "Q_" + element.getId());
+		ThreadContext.put("CALLER", "BAIDU_WORKER");
+		ExamCaptureProcessStatisticController.increaseFaceLivenessDetectCount();
+		if (captureLog.isDebugEnabled()) {
+			captureLog.debug("[BAIDU_FACELIVENESS_WORKER.] 图片处理数量+1,count= "
+					+ ExamCaptureProcessStatisticController.getFaceLivenessDetectCount());
+		}
+		ExamCaptureService examCaptureService = SpringContextHolder
+				.getBean(ExamCaptureService.class);
+		ExamCaptureQueueService examCaptureQueueService = SpringContextHolder
+				.getBean(ExamCaptureQueueService.class);
+		try {
+			examCaptureService.disposeBaiDuFaceLiveness(element);
+		} catch (StatusException e) {
+			while (true){
+				// 异常处理
+				if(examCaptureQueueService.saveExamCaptureQueueEntityByFailed(element.getId(),e.getDesc(),
+						ExamCaptureQueueStatus.PROCESS_FACELIVENESS_FAILED)){
+					break;
+				};
+				Util.sleep(TimeUnit.MILLISECONDS,500);
+			}
+			if ((e.getCode().equals(Constants.BAIDU_FACELIVENESS_QPS_LIMIT_EXCEEDED_CODE))) {
+				// 如果超过并发次数,则添加异常次数
+				controller.addConcurrencyWarn();
+				captureLog.error("[BAIDU_FACELIVENESS_WORKER.] 超过并发次数 " );
+			} else {
+				ExamCaptureProcessStatisticController.increaseFaceLivenessDetectCount();
+				captureLog.error("[BAIDU_FACELIVENESS_WORKER.] 自定义异常 " + e.getDesc(), e);
+			}
+			Util.sleep(TimeUnit.MICROSECONDS, 50);
+		} catch (Exception e) {
+			ExamCaptureProcessStatisticController.increaseFaceLivenessDetectCount();
+			while (true){
+				// 异常处理
+				if(examCaptureQueueService.saveExamCaptureQueueEntityByFailed(element.getId(),"系统异常",
+						ExamCaptureQueueStatus.PROCESS_FACELIVENESS_FAILED)){
+					break;
+				};
+				Util.sleep(TimeUnit.MILLISECONDS,500);
+			}
+			captureLog.error("[BAIDU_FACELIVENESS_WORKER.] 系统异常 " + e.getMessage(), e);
+			Util.sleep(TimeUnit.MICROSECONDS, 50);
+		}finally {
+			ThreadContext.clearAll();
+		}
+	}
+}

+ 84 - 0
examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/worker/FacePPCompareWorker.java

@@ -0,0 +1,84 @@
+package cn.com.qmth.examcloud.core.oe.task.service.worker;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.Worker;
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.WorkerController;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.oe.task.base.ExamCaptureProcessStatisticController;
+import cn.com.qmth.examcloud.core.oe.task.dao.enums.ExamCaptureQueueStatus;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureQueueService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.ExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.logging.log4j.ThreadContext;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * face++人脸比对工作线程
+ *
+ * @author lideyin 20190620
+ */
+public class FacePPCompareWorker implements Worker<ExamCaptureQueueInfo> {
+    private final Log captureLog = LogFactory.getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+    @Override
+    public void process(WorkerController controller, ExamCaptureQueueInfo element) {
+
+        ThreadContext.put("TRACE_ID", "Q_" + element.getId());
+        ThreadContext.put("CALLER", "FACEPP_WORKER");
+        // 图片处理数量+1
+        ExamCaptureProcessStatisticController.increaseFaceCompareCount();
+        if (captureLog.isDebugEnabled()) {
+            captureLog.debug("[FACEPP_COMPARE_WORKER.] 图片处理数量+1,count= "
+                    + ExamCaptureProcessStatisticController.getFaceCompareCount());
+        }
+        ExamCaptureService examCaptureService = SpringContextHolder
+                .getBean(ExamCaptureService.class);
+        ExamCaptureQueueService examCaptureQueueService = SpringContextHolder
+                .getBean(ExamCaptureQueueService.class);
+        try {
+            examCaptureService.disposeFaceCompare(element);
+        } catch (StatusException e) {
+            while (true){
+                // 异常处理
+                if(examCaptureQueueService.saveExamCaptureQueueEntityByFailed(element.getId(),e.getDesc(),
+                        ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED)){
+                    break;
+                };
+                Util.sleep(TimeUnit.MILLISECONDS,500);
+            }
+
+            if ((e.getCode().equals(Constants.FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED))) {
+                // 如果超过并发次数,则添加异常次数
+                controller.addConcurrencyWarn();
+                captureLog.error("[FACEPP_COMPARE_WORKER.] 超过并发次数 ");
+            } else {
+                ExamCaptureProcessStatisticController.increaseFaceCompareFailedCount();
+                captureLog.error("[FACEPP_COMPARE_WORKER.] 自定义异常 " +
+                        ",failedCount=" + ExamCaptureProcessStatisticController.getFaceCompareFailedCount(), e);
+            }
+
+            Util.sleep(TimeUnit.MICROSECONDS, 50);
+        } catch (Exception e) {
+            ExamCaptureProcessStatisticController.increaseFaceCompareFailedCount();
+            // 异常处理
+            while (true){
+                // 异常处理
+                if(examCaptureQueueService.saveExamCaptureQueueEntityByFailed(element.getId(),"系统异常",
+                        ExamCaptureQueueStatus.PROCESS_FACE_COMPARE_FAILED)){
+                    break;
+                }
+                Util.sleep(TimeUnit.MILLISECONDS,500);
+            }
+            captureLog.error("[FACEPP_COMPARE_WORKER.] 系统异常 "  +
+                    ",failedCount=" + ExamCaptureProcessStatisticController.getFaceCompareFailedCount(), e);
+            Util.sleep(TimeUnit.MICROSECONDS, 50);
+        } finally {
+            ThreadContext.clearAll();
+        }
+    }
+}

+ 31 - 0
examcloud-core-oe-task-starter/assembly.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+    <id>distribution</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <fileSets>
+        <fileSet>
+            <directory>${project.basedir}/src/main/resources</directory>
+            <outputDirectory>/config</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>${project.basedir}/shell</directory>
+            <excludes>
+                <exclude>start.args</exclude>
+                <exclude>start.vmoptions</exclude>
+            </excludes>
+            <outputDirectory>/</outputDirectory>
+            <fileMode>0777</fileMode>
+        </fileSet>
+    </fileSets>
+    <dependencySets>
+        <dependencySet>
+            <useProjectArtifact>true</useProjectArtifact>
+            <outputDirectory>lib</outputDirectory>
+            <scope>runtime</scope>
+        </dependencySet>
+    </dependencySets>
+</assembly>

+ 66 - 0
examcloud-core-oe-task-starter/pom.xml

@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>cn.com.qmth.examcloud</groupId>
+		<artifactId>examcloud-core-oe-task</artifactId>
+		<version>2019-SNAPSHOT</version>
+	</parent>
+	<artifactId>examcloud-core-oe-task-starter</artifactId>
+
+
+	<dependencies>
+		<dependency>
+			<groupId>cn.com.qmth.examcloud</groupId>
+			<artifactId>examcloud-core-oe-task-api-provider</artifactId>
+			<version>${examcloud.version}</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<configuration>
+					<archive>
+						<manifest>
+							<mainClass>cn.com.qmth.examcloud.core.oe.task.starter.OETaskApp</mainClass>
+							<addClasspath>true</addClasspath>
+							<classpathPrefix>./</classpathPrefix>
+						</manifest>
+						<manifestEntries>
+							<Class-Path>../config/</Class-Path>
+						</manifestEntries>
+					</archive>
+					<excludes>
+						<exclude>*.properties</exclude>
+						<exclude>*.xml</exclude>
+						<exclude>classpath.location</exclude>
+					</excludes>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-assembly-plugin</artifactId>
+				<configuration>
+					<finalName>examcloud-core-oe-task</finalName>
+					<descriptors>
+						<descriptor>assembly.xml</descriptor>
+					</descriptors>
+				</configuration>
+				<executions>
+					<execution>
+						<id>make-assembly</id>
+						<phase>install</phase>
+						<goals>
+							<goal>assembly</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>

+ 1 - 0
examcloud-core-oe-task-starter/shell/start.args

@@ -0,0 +1 @@
+--spring.profiles.active=dev

+ 36 - 0
examcloud-core-oe-task-starter/shell/start.sh

@@ -0,0 +1,36 @@
+#!/bin/bash
+
+APP_MAIN_JAR="examcloud-core-oe-task-starter-2019-SNAPSHOT.jar"
+
+FILE_PATH=$(cd `dirname $0`; pwd)
+
+JAVA_OPTS=`cat $FILE_PATH/start.vmoptions`
+APP_ARGS=`cat $FILE_PATH/start.args`
+
+PID_LIST=`ps -ef|grep $APP_MAIN_JAR|grep java|awk '{print $2}'`
+
+if [ ! -z "$PID_LIST" ]; then
+    echo "[ERROR] : APP is already running!"
+    exit -1
+fi
+
+if [ "$1" ];then
+    echo "startupCode:"$1;
+else
+    echo "[ERROR] : no arguments"
+    exit -1
+fi
+
+APP_ARGS=$APP_ARGS" --examcloud.startup.startupCode="$1
+
+echo "java options:"
+echo "$JAVA_OPTS"
+echo "args:"
+echo "$APP_ARGS"
+    
+nohup java $JAVA_OPTS -jar $FILE_PATH/lib/$APP_MAIN_JAR $APP_ARGS >/dev/null 2>&1 &
+
+echo "starting......"
+
+exit 0
+

+ 1 - 0
examcloud-core-oe-task-starter/shell/start.vmoptions

@@ -0,0 +1 @@
+-server -Xms1g -Xmx1g

+ 18 - 0
examcloud-core-oe-task-starter/shell/stop.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+
+APP_MAIN_JAR="examcloud-core-oe-task-starter-2019-SNAPSHOT.jar"
+
+FILE_PATH=$(cd `dirname $0`; pwd)
+
+PID_LIST=`ps -ef|grep $APP_MAIN_JAR|grep java|awk '{print $2}'`
+
+if [ ! -z "$PID_LIST" ]; then
+    echo "Runnable jar is $APP_MAIN_JAR."
+    for PID in $PID_LIST 
+    do
+        kill -9 $PID
+    done
+    echo "stopped !"
+fi
+
+exit 0

+ 71 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/OETaskApp.java

@@ -0,0 +1,71 @@
+package cn.com.qmth.examcloud.core.oe.task.starter;
+
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+import org.springframework.web.multipart.MultipartResolver;
+import org.springframework.web.multipart.commons.CommonsMultipartResolver;
+
+import cn.com.qmth.examcloud.core.oe.task.base.UniqueRuleHolder;
+import cn.com.qmth.examcloud.support.filestorage.FileStorageUtil;
+import cn.com.qmth.examcloud.web.bootstrap.AppBootstrap;
+import cn.com.qmth.examcloud.web.jpa.DataIntegrityViolationTransverter;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+@SpringBootApplication
+@Configuration
+@EnableAutoConfiguration(exclude = {MultipartAutoConfiguration.class})
+@EnableJpaAuditing
+@EnableTransactionManagement
+@EnableEurekaClient
+@EnableDiscoveryClient
+@ComponentScan(basePackages = {"cn.com.qmth"})
+@EntityScan(basePackages = {"cn.com.qmth"})
+@EnableJpaRepositories(basePackages = {"cn.com.qmth"})
+public class OETaskApp {
+
+	static {
+		String runtimeLevel = System.getProperty("log.commonLevel");
+		if (null == runtimeLevel) {
+			System.setProperty("log.commonLevel", "DEBUG");
+		}
+		System.setProperty("hibernate.dialect.storage_engine", "innodb");
+		DataIntegrityViolationTransverter.setUniqueRules(UniqueRuleHolder.getUniqueRuleList());
+	}
+
+	/**
+	 * main
+	 * 
+	 * @param args
+	 * @throws Exception
+	 */
+	public static void main(String[] args) throws Exception {
+		AppBootstrap.run(OETaskApp.class, args);
+		FileStorageUtil.initYunClient();
+		test();
+	}
+
+	private static void test() {
+		Tester tester = SpringContextHolder.getBean(Tester.class);
+		tester.test();
+	}
+
+	@Bean(name = "multipartResolver")
+	public MultipartResolver multipartResolver() {
+		CommonsMultipartResolver resolver = new CommonsMultipartResolver();
+		resolver.setDefaultEncoding("UTF-8");
+		resolver.setResolveLazily(true);
+		resolver.setMaxInMemorySize(2);
+		resolver.setMaxUploadSize(200 * 1024 * 1024);
+		return resolver;
+	}
+}

+ 12 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/Tester.java

@@ -0,0 +1,12 @@
+package cn.com.qmth.examcloud.core.oe.task.starter;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class Tester {
+
+	public void test() {
+
+	}
+
+}

+ 129 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ExamCloudResourceManager.java

@@ -0,0 +1,129 @@
+package cn.com.qmth.examcloud.core.oe.task.starter.config;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.AccessApp;
+import cn.com.qmth.examcloud.api.commons.security.bean.Role;
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.api.commons.security.bean.UserType;
+import cn.com.qmth.examcloud.api.commons.security.enums.RoleMeta;
+import cn.com.qmth.examcloud.commons.util.PathUtil;
+import cn.com.qmth.examcloud.commons.util.PropertiesUtil;
+import cn.com.qmth.examcloud.commons.util.RegExpUtil;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.AppCacheBean;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.security.ResourceManager;
+import cn.com.qmth.examcloud.web.support.ApiInfo;
+
+/**
+ * Demo资源管理器
+ *
+ * @author WANGWEI
+ * @date 2019年2月18日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Component
+public class ExamCloudResourceManager implements ResourceManager {
+
+	@Autowired
+	RedisClient redisClient;
+
+	static {
+		PropertiesUtil.loadFromPath(PathUtil.getResoucePath("security.properties"));
+	}
+
+	@Override
+	public AccessApp getAccessApp(Long appId) {
+		AppCacheBean appCacheBean = CacheHelper.getApp(appId);
+		AccessApp app = new AccessApp();
+		app.setAppId(appCacheBean.getId());
+		app.setAppCode(appCacheBean.getCode());
+		app.setAppName(appCacheBean.getName());
+		app.setSecretKey(appCacheBean.getSecretKey());
+		app.setTimeRange(appCacheBean.getTimeRange());
+		return app;
+	}
+
+	@Override
+	public boolean isNaked(ApiInfo apiInfo, String mapping) {
+		if (null == apiInfo) {
+			return true;
+		}
+
+		if (null != apiInfo) {
+			if (apiInfo.isNaked()) {
+				return true;
+			}
+		}
+		if(mapping.matches(".*swagger.*")) {
+			return true;
+		}
+		return false;
+	}
+
+	@Override
+	public boolean hasPermission(User user, ApiInfo apiInfo, String mapping) {
+
+		// 学生鉴权
+		if (user.getUserType().equals(UserType.STUDENT)) {
+			return true;
+		}
+
+		List<Role> roleList = user.getRoleList();
+
+		if (CollectionUtils.isEmpty(roleList)) {
+			return false;
+		}
+
+		for (Role role : roleList) {
+			if (role.getRoleCode().equals(RoleMeta.SUPER_ADMIN.name())) {
+				return true;
+			}
+		}
+
+		// 权限组集合
+		String privilegeGroups = PropertiesUtil.getString(mapping);
+		if (StringUtils.isBlank(privilegeGroups)) {
+			return true;
+		}
+
+		// 用户权限集合
+		Set<String> rolePrivilegeList = Sets.newHashSet();
+		Long rootOrgId = user.getRootOrgId();
+		for (Role role : roleList) {
+			String key = "$_P_" + rootOrgId + "_" + role.getRoleId();
+			String rolePrivileges = redisClient.get(key, String.class);
+
+			List<String> rpList = RegExpUtil.findAll(rolePrivileges, "\\w+");
+			rolePrivilegeList.addAll(rpList);
+		}
+
+		List<String> privilegeGroupList = RegExpUtil.findAll(privilegeGroups, "[^\\;]+");
+
+		for (String pg : privilegeGroupList) {
+			pg = pg.trim();
+			if (StringUtils.isBlank(pg)) {
+				continue;
+			}
+
+			List<String> pList = RegExpUtil.findAll(pg, "[^\\,]+");
+			if (rolePrivilegeList.containsAll(pList)) {
+				return true;
+			} else {
+				continue;
+			}
+		}
+
+		return false;
+	}
+
+}

+ 53 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ExamCloudWebMvcConfigurer.java

@@ -0,0 +1,53 @@
+package cn.com.qmth.examcloud.core.oe.task.starter.config;
+
+import cn.com.qmth.examcloud.web.interceptor.ApiFlowLimitedInterceptor;
+import cn.com.qmth.examcloud.web.interceptor.ApiStatisticInterceptor;
+import cn.com.qmth.examcloud.web.interceptor.SeqlockInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import cn.com.qmth.examcloud.web.interceptor.FirstInterceptor;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.security.RequestPermissionInterceptor;
+import cn.com.qmth.examcloud.web.security.ResourceManager;
+import cn.com.qmth.examcloud.web.security.RpcInterceptor;
+
+/**
+ * WebMvcConfigurer
+ *
+ * @author WANGWEI
+ * @date 2019年1月30日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Configuration
+public class ExamCloudWebMvcConfigurer implements WebMvcConfigurer {
+
+	@Autowired
+	ResourceManager resourceManager;
+
+	@Autowired
+	RedisClient redisClient;
+
+	@Override
+	public void addInterceptors(InterceptorRegistry registry) {
+		registry.addInterceptor(new FirstInterceptor()).addPathPatterns("/**");
+		registry.addInterceptor(new ApiFlowLimitedInterceptor()).addPathPatterns("/**");
+		registry.addInterceptor(new RpcInterceptor(resourceManager)).addPathPatterns("/**");
+		RequestPermissionInterceptor permissionInterceptor = new RequestPermissionInterceptor(
+				resourceManager, redisClient);
+		registry.addInterceptor(permissionInterceptor).addPathPatterns("/**");
+		registry.addInterceptor(new SeqlockInterceptor(redisClient)).addPathPatterns("/**");
+		registry.addInterceptor(new ApiStatisticInterceptor()).addPathPatterns("/**");
+
+	}
+
+	@Override
+	public void addCorsMappings(CorsRegistry registry) {
+		registry.addMapping("/**").allowedOrigins("*").allowCredentials(false).allowedMethods("*")
+				.maxAge(3600);
+	}
+
+}

+ 149 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessBaiduFaceLivenessAlarmTask.java

@@ -0,0 +1,149 @@
+package cn.com.qmth.examcloud.core.oe.task.starter.config;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.oe.task.base.ExamCaptureProcessStatisticController;
+import cn.com.qmth.examcloud.exchange.inner.api.SmsCloudService;
+import cn.com.qmth.examcloud.exchange.inner.api.request.SendSmsReq;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import com.google.common.collect.Maps;
+import com.googlecode.aviator.AviatorEvaluator;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @Description 处理抓拍照片预警任务
+ * @Author lideyin
+ * @Date 2019/9/12 10:52
+ * @Version 1.0
+ */
+@Component
+@Order(203)
+public class ProcessBaiduFaceLivenessAlarmTask implements ApplicationRunner {
+
+    private static ExamCloudLog captureLog = ExamCloudLogFactory
+            .getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+    @Autowired
+    SmsCloudService smsCloudService;
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        Thread thread = new Thread(new Runnable() {
+
+            @Override
+            public void run() {
+                while (true) {
+                    try {
+                        faceLivenessDectectAlarm();
+                    } catch (Exception e) {
+                        captureLog.error("[FACE_LIVENESS_ALARM.] 活体检测预警出现异常", e);
+                    }
+
+                    // 每分钟轮循一次
+                    Util.sleep(60);
+                }
+
+            }
+        });
+        thread.setDaemon(true);
+        thread.start();
+    }
+
+    /**
+     * 活体检测,如果有必要则发短信
+     */
+    private void faceLivenessDectectAlarm() {
+        if (captureLog.isDebugEnabled()) {
+            captureLog.debug("[FACE_LIVENESS_ALARM] 进入活体检测" + System.currentTimeMillis()
+                    + "....totalCount="
+                    + ExamCaptureProcessStatisticController.getFaceLivenessDetectCount()
+                    + " ,failCount="
+                    + ExamCaptureProcessStatisticController.getFaceLivenessDetectFailedCount());
+        }
+
+        // 默认每分钟失败率超过50%则发短信报警,且总数不少于10次则短信报警
+        if (needSmsAlarm()) {
+            SysPropertyCacheBean faceLivenessSmsAssemblyCodeProperty = CacheHelper
+                    .getSysProperty("capture.faceLiveness.smsAssemblyCode");
+            if (!faceLivenessSmsAssemblyCodeProperty.getHasValue()) {
+                captureLog.error("[FACE_LIVENESS_ALARM.] 未配置人脸比对的短信模板代码,totalCount="
+                        + ExamCaptureProcessStatisticController.getFaceCompareCount()
+                        + ",errorCount="
+                        + ExamCaptureProcessStatisticController.getFaceCompareFailedCount());
+                throw new StatusException("300003", "未配置人脸活体检测的短信模板代码");
+            }
+            SysPropertyCacheBean smsPhoneProperty = CacheHelper
+                    .getSysProperty("capture.sms.phones");
+            if (!smsPhoneProperty.getHasValue()) {
+                captureLog.error("[FACE_LIVENESS_ALARM.] 未配置图片处理失败的通知手机号,totalCount="
+                        + ExamCaptureProcessStatisticController.getFaceCompareCount()
+                        + ",errorCount="
+                        + ExamCaptureProcessStatisticController.getFaceCompareFailedCount());
+                throw new StatusException("300004", "未配置图片处理失败的通知手机号");
+            }
+
+            List<String> phoneList = Arrays
+                    .asList(smsPhoneProperty.getValue().toString().split(","));
+            SendSmsReq sendSmsReq = new SendSmsReq();
+            sendSmsReq.setPhoneList(phoneList);
+            sendSmsReq
+                    .setSmsAssemblyCode(faceLivenessSmsAssemblyCodeProperty.getValue().toString());
+
+            HashMap<String, String> params = new HashMap<>();
+            params.put("totalCount", String
+                    .valueOf(ExamCaptureProcessStatisticController.getFaceLivenessDetectCount()));
+            params.put("errorCount", String.valueOf(
+                    ExamCaptureProcessStatisticController.getFaceLivenessDetectFailedCount()));
+            sendSmsReq.setParams(params);
+            try {
+                smsCloudService.sendSms(sendSmsReq);
+            } catch (Exception e) {
+                captureLog.error("[PROCESS_FACE_LIVENESS.] 发送短信出现异常", e);
+            }
+        }
+        // 每1分钟重置一次总数量与失败数量
+        ExamCaptureProcessStatisticController.resetAllFaceLivenessDetectCount();
+
+    }
+
+    /**
+     * 是否需要短信报警
+     *
+     * @return boolean
+     */
+    private boolean needSmsAlarm() {
+        if (ExamCaptureProcessStatisticController.getFaceCompareCount() == 0) {
+            return false;
+        }
+        SysPropertyCacheBean expressionProperty = CacheHelper.getSysProperty("capture.baidu.expression.alarm");
+        if (expressionProperty.getHasValue()) {
+            String expression = expressionProperty.getValue().toString();
+            Map<String, Object> env = Maps.newHashMap();
+            env.put("totalCount", ExamCaptureProcessStatisticController.getFaceLivenessDetectCount());
+            env.put("failedCount", ExamCaptureProcessStatisticController.getFaceLivenessDetectFailedCount());
+            try {
+                return (Boolean) AviatorEvaluator.execute(expression, env, true);
+            } catch (Exception e) {
+                throw new StatusException("300004", "failed to execute expression. expression=" + expression);
+            }
+
+        } else {
+            // 默认每分钟失败率超过50%则发短信报警,且失败总数不少于10次则短信报警
+            return ExamCaptureProcessStatisticController.getFaceLivenessDetectFailedCount() > 10 &&
+                    ExamCaptureProcessStatisticController.getFaceLivenessDetectFailurePercent() > 50;
+        }
+    }
+
+}

+ 143 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessBaiduFacelivenessTask.java

@@ -0,0 +1,143 @@
+package cn.com.qmth.examcloud.core.oe.task.starter.config;
+
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.ConcurrentTask;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.ExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.core.oe.task.service.worker.BaiduFaceLivenessWorker;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import org.apache.logging.log4j.ThreadContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 启动百度活体检测任务
+ */
+@Component
+@Order(201)
+public class ProcessBaiduFacelivenessTask implements ApplicationRunner {
+
+    private static ExamCloudLog captureLog = ExamCloudLogFactory
+            .getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+    @Autowired
+    ExamCaptureQueueRepo examCaptureQueueRepo;
+
+    private int process(ConcurrentTask<ExamCaptureQueueInfo> concurrentTask,
+                        String processBatchNum, Integer limit) {
+
+        // 如果队列没满,则从数据库中查数据并插入
+        List<ExamCaptureQueueEntity> examCaptureQueueList = examCaptureQueueRepo
+                .findNeedFacelivenessDetectExamCaptureQueuesLimit(limit, processBatchNum);
+
+        if (null == examCaptureQueueList || examCaptureQueueList.isEmpty()) {
+            captureLog
+                    .debug("[PROCESS_BAIDUFACELIVENESS." + processBatchNum + "] 抓拍队列中没有取到数据,2秒后重试");
+            return 0;
+        }
+
+        captureLog.debug("[PROCESS_BAIDUFACELIVENESS." + processBatchNum + "] 抓拍队列中的数据条数为:"
+                + examCaptureQueueList.size());
+
+        for (ExamCaptureQueueEntity offeredQueueEntity : examCaptureQueueList) {
+            while (true) {
+                try{
+                    ThreadContext.put("TRACE_ID", "Q_" + offeredQueueEntity.getId());
+                    ThreadContext.put("CALLER", "BAIDU_TASK");
+
+                    examCaptureQueueRepo.updateProcessBatchNum(offeredQueueEntity.getId(), processBatchNum);
+                    boolean offerSuccess = concurrentTask.offerElement(copyExamCaptureQueueInfoFrom(offeredQueueEntity));
+                    if (offerSuccess) {
+                        captureLog.debug("[PROCESS_BAIDUFACELIVENESS." + processBatchNum
+                                + "] 向工作队列中添加数据成功:fileUrl=" + offeredQueueEntity.getFileUrl());
+                        break;
+                    }
+
+                    captureLog.debug("[PROCESS_BAIDUFACELIVENESS." + processBatchNum
+                            + "] 向工作队列中添加数据失败,30秒后重试:fileUrl=" + offeredQueueEntity.getFileUrl());
+
+                    Util.sleep(PropertyHolder.getInt("$capture.queue.offer.sleepSeconds.", 30));
+                }finally {
+                    ThreadContext.clearAll();
+                }
+
+            }
+        }
+        return examCaptureQueueList.size();
+    }
+
+    @Override
+    public void run(ApplicationArguments args) {
+
+        ConcurrentTask<ExamCaptureQueueInfo> concurrentTask = new ConcurrentTask<ExamCaptureQueueInfo>("百度活体检测");
+        concurrentTask.setMaxActiveThreadSize(
+                PropertyHolder.getInt("$capture.thread.maxActiveThreadSize", 100));
+        concurrentTask.setMinThreadSize(PropertyHolder.getInt("$capture.thread.minThreadSize", 2));
+        concurrentTask.setWorker(new BaiduFaceLivenessWorker());
+        concurrentTask.setMaxActiveThreadSize(PropertyHolder.getInt("$capture.baidu.thread.maxActiveThreadSize", 30));
+        concurrentTask.start();
+        // 当前获取数据的批次号(默认用时间戳)
+        String processBatchNum = "B_" + System.currentTimeMillis();
+
+        captureLog.debug("[PROCESS_BAIDUFACELIVENESS." + processBatchNum + "] 启动百度处理服务...");
+
+        Thread thread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                while (true) {
+                    try {
+                        Integer limit = PropertyHolder.getInt("$capture.queue.limit", 100);
+                        for (int i = 0; i < 5; i++) {
+                            int realCount = process(concurrentTask, processBatchNum, limit);
+                            if (realCount < limit) {
+                                break;
+                            }
+                        }
+                        Util.sleep(PropertyHolder.getInt("$capture.queue.read.sleepSeconds.", 2));
+                    } catch (Exception e) {
+                        captureLog.error("[PROCESS_FACEPP." + processBatchNum + "] 百度活体检测出异常,3秒后重试",
+                                e);
+                        Util.sleep(3);
+                    }
+                }
+
+            }
+        });
+
+        thread.setDaemon(true);
+        thread.start();
+    }
+    private ExamCaptureQueueInfo copyExamCaptureQueueInfoFrom(ExamCaptureQueueEntity entity){
+        ExamCaptureQueueInfo info =new ExamCaptureQueueInfo();
+        info.setFaceCompareResult(entity.getFaceCompareResult());
+        info.setFacelivenessResult(entity.getFacelivenessResult());
+        info.setPass(entity.getIsPass());
+        info.setStranger(entity.getIsStranger());
+        info.setFaceCompareStartTime(entity.getFaceCompareStartTime());
+        info.setStatus(entity.getStatus());
+        info.setBaseFaceToken(entity.getBaseFaceToken());
+        info.setCameraInfos(entity.getCameraInfos());
+        info.setCreationTime(entity.getCreationTime());
+        info.setErrorMsg(entity.getErrorMsg());
+        info.setErrorNum(entity.getErrorNum());
+        info.setExamRecordDataId(entity.getExamRecordDataId());
+        info.setExtMsg(entity.getExtMsg());
+        info.setFileName(entity.getFileName());
+        info.setFileUrl(entity.getFileUrl());
+        info.setHasVirtualCamera(entity.getHasVirtualCamera());
+        info.setId(entity.getId());
+        info.setPriority(entity.getPriority());
+        info.setProcessBatchNum(entity.getProcessBatchNum());
+        info.setStudentId(entity.getStudentId());
+        info.setUpdateTime(entity.getUpdateTime());
+        return info;
+    }
+}

+ 158 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessFaceCompareAlarmTask.java

@@ -0,0 +1,158 @@
+package cn.com.qmth.examcloud.core.oe.task.starter.config;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.oe.task.base.ExamCaptureProcessStatisticController;
+import cn.com.qmth.examcloud.exchange.inner.api.SmsCloudService;
+import cn.com.qmth.examcloud.exchange.inner.api.request.SendSmsReq;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import com.google.common.collect.Maps;
+import com.googlecode.aviator.AviatorEvaluator;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @Description 人脸比对预警任务
+ * @Author lideyin
+ * @Date 2019/9/12 10:52
+ * @Version 1.0
+ */
+@Component
+@Order(202)
+public class ProcessFaceCompareAlarmTask implements ApplicationRunner {
+    @Autowired
+    SmsCloudService smsCloudService;
+
+    private final ExamCloudLog captureLog = ExamCloudLogFactory
+            .getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+
+        Thread thread = new Thread(new Runnable() {
+
+            @Override
+            public void run() {
+                while (true) {
+                    try {
+                        faceCompareAlarm();
+
+                    } catch (Exception e) {
+                        captureLog.error("[FACE_COMPARE_ALARM.] 人脸比对预警出现异常 ", e);
+                    }
+                    // 每分钟轮循一次
+                    Util.sleep(60);
+                }
+            }
+        });
+
+        thread.setDaemon(true);
+        thread.start();
+    }
+
+    /**
+     * 人脸比对,如果有必要则发短信
+     */
+    private void faceCompareAlarm() {
+        if (captureLog.isDebugEnabled()) {
+            captureLog.debug("[FACE_COMPARE_ALARM] 进入人脸" + System.currentTimeMillis()
+                    + "....totalCount="
+                    + ExamCaptureProcessStatisticController.getFaceCompareCount() + " ,failCount="
+                    + ExamCaptureProcessStatisticController.getFaceCompareFailedCount());
+        }
+
+        // 默认每分钟失败率超过50%则发短信报警,且总数不少于10次则短信报警
+        if (needSmsAlarm()) {
+            SysPropertyCacheBean faceCompareSmsAssemblyCodeProperty = CacheHelper
+                    .getSysProperty("capture.faceCompare.smsAssemblyCode");
+
+            if (!faceCompareSmsAssemblyCodeProperty.getHasValue()) {
+                captureLog.error("[FACE_COMPARE_ALARM.] 未配置人脸比对的短信模板代码,totalCount="
+                        + ExamCaptureProcessStatisticController.getFaceCompareCount()
+                        + ",errorCount="
+                        + ExamCaptureProcessStatisticController.getFaceCompareFailedCount());
+                throw new StatusException("300001", "未配置人脸比对的短信模板代码");
+            }
+            SysPropertyCacheBean smsPhoneProperty = CacheHelper
+                    .getSysProperty("capture.sms.phones");
+            if (!smsPhoneProperty.getHasValue()) {
+                if (captureLog.isErrorEnabled()) {
+                    captureLog.error("[FACE_COMPARE_ALARM.] 未配置图片处理失败的通知手机号,totalCount="
+                            + ExamCaptureProcessStatisticController.getFaceCompareCount()
+                            + ",errorCount="
+                            + ExamCaptureProcessStatisticController.getFaceCompareFailedCount());
+                }
+                throw new StatusException("300002", "未配置图片处理失败的通知手机号");
+            }
+
+            List<String> phoneList = Arrays
+                    .asList(smsPhoneProperty.getValue().toString().split(","));
+
+            // List<String> phoneList = Arrays.asList("13717595977");
+            SendSmsReq sendSmsReq = new SendSmsReq();
+            sendSmsReq.setPhoneList(phoneList);
+            sendSmsReq.setSmsAssemblyCode(faceCompareSmsAssemblyCodeProperty.getValue().toString());
+            // sendSmsReq.setSmsAssemblyCode("FACECOMPARE");
+
+            HashMap<String, String> params = new HashMap<>();
+            params.put("totalCount",
+                    String.valueOf(ExamCaptureProcessStatisticController.getFaceCompareCount()));
+            params.put("errorCount", String
+                    .valueOf(ExamCaptureProcessStatisticController.getFaceCompareFailedCount()));
+            sendSmsReq.setParams(params);
+            try {
+                if (captureLog.isDebugEnabled()) {
+                    captureLog.debug("[FACE_COMPARE_ALARM.] 开始调用发送短信接口,totalCount="
+                            + ExamCaptureProcessStatisticController.getFaceCompareCount()
+                            + ",errorCount="
+                            + ExamCaptureProcessStatisticController.getFaceCompareFailedCount());
+                }
+                smsCloudService.sendSms(sendSmsReq);
+            } catch (Exception e) {
+                captureLog.error("[FACE_COMPARE_ALARM.] 发送短信出现异常", e);
+            }
+        }
+        // 每1分钟重置一次总数量与失败数量
+        ExamCaptureProcessStatisticController.resetAllFaceCompareCount();
+    }
+
+    /**
+     * 是否需要短信报警
+     *
+     * @return boolean
+     */
+    private boolean needSmsAlarm() {
+        if (ExamCaptureProcessStatisticController.getFaceCompareCount()==0){
+            return false;
+        }
+        SysPropertyCacheBean expressionProperty = CacheHelper.getSysProperty("capture.faceCompare.expression.alarm");
+        if (expressionProperty.getHasValue()) {
+            String expression = expressionProperty.getValue().toString();
+            Map<String, Object> env = Maps.newHashMap();
+            env.put("totalCount", ExamCaptureProcessStatisticController.getFaceCompareCount());
+            env.put("failedCount", ExamCaptureProcessStatisticController.getFaceCompareFailedCount());
+            try {
+                return (Boolean) AviatorEvaluator.execute(expression, env, true);
+            } catch (Exception e) {
+                throw new StatusException("300004", "failed to execute expression. expression=" + expression);
+            }
+
+        } else {
+            // 默认每分钟失败率超过50%则发短信报警,且失败总数不少于10次则短信报警
+            return ExamCaptureProcessStatisticController.getFaceCompareFailedCount() > 10 &&
+                    ExamCaptureProcessStatisticController.getFaceLivenessDetectFailurePercent() > 50;
+        }
+    }
+
+}

+ 146 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessFaceCompareQueueTask.java

@@ -0,0 +1,146 @@
+package cn.com.qmth.examcloud.core.oe.task.starter.config;
+
+import cn.com.qmth.examcloud.commons.helpers.concurrency.simple.ConcurrentTask;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.ExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.core.oe.task.service.worker.FacePPCompareWorker;
+import cn.com.qmth.examcloud.exchange.inner.api.SmsCloudService;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import org.apache.logging.log4j.ThreadContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 启动人脸比对任务
+ */
+@Component
+@Order(200)
+public class ProcessFaceCompareQueueTask implements ApplicationRunner {
+
+    private final ExamCloudLog captureLog = ExamCloudLogFactory
+            .getLog("PROCESS_EXAM_CAPTURE_TASK_LOGGER");
+
+    @Autowired
+    ExamCaptureQueueRepo examCaptureQueueRepo;
+
+    @Autowired
+    SmsCloudService smsCloudService;
+
+    private int process(ConcurrentTask<ExamCaptureQueueInfo> concurrentTask,
+                        String processBatchNum, Integer limit) {
+        // 如果队列没满,则从数据库中查数据并插入
+        List<ExamCaptureQueueEntity> examCaptureQueueList = examCaptureQueueRepo
+                .findNeedFaceCompareExamCaptureQueuesLimitByProcessBatchNum(limit, processBatchNum);
+
+        if (null == examCaptureQueueList || examCaptureQueueList.isEmpty()) {
+            captureLog.debug("[PROCESS_FACEPP." + processBatchNum + "] 抓拍队列中没有取到数据,2秒后重试");
+            return 0;
+        }
+
+        captureLog.debug("[PROCESS_FACEPP." + processBatchNum + "] 抓拍队列中的数据条数为:"
+                + examCaptureQueueList.size());
+
+        for (ExamCaptureQueueEntity offeredQueueEntity : examCaptureQueueList) {
+            while (true) {
+                try {
+                    ThreadContext.put("TRACE_ID", "Q_" + offeredQueueEntity.getId());
+                    ThreadContext.put("CALLER", "FACEPP_TASK");
+
+                    examCaptureQueueRepo.updateProcessBatchNum(offeredQueueEntity.getId(), processBatchNum);
+                    boolean offerSuccess = concurrentTask.offerElement(copyExamCaptureQueueInfoFrom(offeredQueueEntity));
+                    // 如果向队列中添加数据成功,则更新标识
+                    if (offerSuccess) {
+                        captureLog.debug("[PROCESS_FACEPP." + processBatchNum
+                                + "] 向工作队列中添加数据成功:fileUrl=" + offeredQueueEntity.getFileUrl());
+                        break;
+                    }
+
+                    captureLog.debug("[PROCESS_FACEPP." + processBatchNum
+                            + "] 向工作队列中添加数据失败,30秒后重试:fileUrl=" + offeredQueueEntity.getFileUrl());
+
+                    Util.sleep(PropertyHolder.getInt("$capture.queue.offer.sleepSeconds.", 30));
+                } finally {
+                    ThreadContext.clearAll();
+                }
+
+            }
+        }
+        return examCaptureQueueList.size();
+    }
+
+    @Override
+    public void run(ApplicationArguments args) {
+
+        ConcurrentTask<ExamCaptureQueueInfo> concurrentTask = new ConcurrentTask<ExamCaptureQueueInfo>("Face++人脸比对");
+        concurrentTask.setMaxActiveThreadSize(
+                PropertyHolder.getInt("$capture.thread.maxActiveThreadSize", 100));
+        concurrentTask.setWorker(new FacePPCompareWorker());
+        concurrentTask.setMaxActiveThreadSize(PropertyHolder.getInt("$capture.facePP.thread.maxActiveThreadSize", 30));
+        concurrentTask.start();
+        // 当前获取数据的批次号(默认用时间戳)
+        String processBatchNum = "A_" + System.currentTimeMillis();
+
+        captureLog.debug("[PROCESS_FACEPP." + processBatchNum + "] 启动face++人脸比对服务...");
+
+        Thread thread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                while (true) {
+                    try {
+                        Integer limit = PropertyHolder.getInt("$capture.queue.limit", 100);
+                        for (int i = 0; i < 5; i++) {
+                            int realCount = process(concurrentTask, processBatchNum, limit);
+                            if (realCount < limit) {
+                                break;
+                            }
+                        }
+                        Util.sleep(PropertyHolder.getInt("$capture.queue.read.sleepSeconds.", 2));
+                    } catch (Exception e) {
+                        captureLog.error("[PROCESS_FACEPP." + processBatchNum + "] 百度活体检测出异常,3秒后重试",
+                                e);
+                        Util.sleep(3);
+                    }
+                }
+
+            }
+        });
+
+        thread.setDaemon(true);
+        thread.start();
+    }
+
+    private ExamCaptureQueueInfo copyExamCaptureQueueInfoFrom(ExamCaptureQueueEntity entity) {
+        ExamCaptureQueueInfo info = new ExamCaptureQueueInfo();
+        info.setFaceCompareResult(entity.getFaceCompareResult());
+        info.setFacelivenessResult(entity.getFacelivenessResult());
+        info.setPass(entity.getIsPass());
+        info.setStranger(entity.getIsStranger());
+        info.setFaceCompareStartTime(entity.getFaceCompareStartTime());
+        info.setStatus(entity.getStatus());
+        info.setBaseFaceToken(entity.getBaseFaceToken());
+        info.setCameraInfos(entity.getCameraInfos());
+        info.setCreationTime(entity.getCreationTime());
+        info.setErrorMsg(entity.getErrorMsg());
+        info.setErrorNum(entity.getErrorNum());
+        info.setExamRecordDataId(entity.getExamRecordDataId());
+        info.setExtMsg(entity.getExtMsg());
+        info.setFileName(entity.getFileName());
+        info.setFileUrl(entity.getFileUrl());
+        info.setHasVirtualCamera(entity.getHasVirtualCamera());
+        info.setId(entity.getId());
+        info.setPriority(entity.getPriority());
+        info.setProcessBatchNum(entity.getProcessBatchNum());
+        info.setStudentId(entity.getStudentId());
+        info.setUpdateTime(entity.getUpdateTime());
+        return info;
+    }
+}

+ 92 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/StreamTaskExecutor.java

@@ -0,0 +1,92 @@
+package cn.com.qmth.examcloud.core.oe.task.starter.config;
+
+import cn.com.qmth.examcloud.commons.helpers.pipeline.Node;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.SimpleNode;
+import cn.com.qmth.examcloud.commons.helpers.pipeline.TaskContext;
+import cn.com.qmth.examcloud.core.oe.task.service.pipeline.*;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * @Description 流式任务执行器
+ * @Author lideyin
+ * @Date 2019/12/24 15:52
+ * @Version 1.0
+ */
+@Component
+@Order(300)
+public class StreamTaskExecutor implements ApplicationRunner {
+    @Autowired
+    private DataGainExamExecutor dataGainExamExecutor;
+    @Autowired
+    private HandInExamExecutor handInExamExecutor;
+    @Autowired
+    private AfterHandInExamExecutor afterHandInExamExecutor;
+    @Autowired
+    private SyncExamDataExecutor syncExamDataExecutor;
+    @Autowired
+    private ClearExamDataCacheExecutor clearExamDataCacheExecutor;
+
+    private static Integer DEFAULT_GAIN_EXAM_DATA_EXECUTOR_SLEEP_SECONDS = 2;
+    private static Integer DEFAULT_HAND_IN_EXAM_EXECUTOR_SLEEP_SECONDS = 2;
+    private static Integer DEFAULT_AFTER_HAND_IN_EXAM_EXECUTOR_SLEEP_SECONDS = 1;
+    private static Integer DEFAULT_SYNC_EXAM_DATA_EXECUTOR_SLEEP_SECONDS = 1;
+    private static Integer DEFAULT_CLEAR_EXAM_DATA_CACHE_EXECUTOR_SLEEP_SECONDS = 1;
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        initExecutor();
+    }
+
+    private void initExecutor() {
+        TaskContext context = new TaskContext();
+
+        Node<Long, ExamRecordData, Long, ExamRecordData> node1 = new SimpleNode<>(
+                "gainExamData", dataGainExamExecutor, context);
+
+        Node<Long, ExamRecordData, Long, ExamRecordData> node2 = new SimpleNode<>(
+                "handInExam", handInExamExecutor, context);
+
+        Node<Long, ExamRecordData, Long, ExamRecordData> node3 = new SimpleNode<>(
+                "afterHandInExam", afterHandInExamExecutor, context);
+
+        Node<Long, ExamRecordData, Long, ExamRecordData> node4 = new SimpleNode<>(
+                "syncExamData", syncExamDataExecutor, context);
+
+        Node<Long, ExamRecordData, Long, ExamRecordData> node5 = new SimpleNode<>(
+                "clearExamDataCache", clearExamDataCacheExecutor, context);
+
+        node1.setFirst(true);
+        node1.setLowerNode(node2);
+        node1.setSleep(PropertyHolder.getInt("oeTask.executor.gainExamData.sleep",
+                DEFAULT_GAIN_EXAM_DATA_EXECUTOR_SLEEP_SECONDS));//单位秒
+
+        node2.setLowerNode(node3);
+        node2.setSleep(PropertyHolder.getInt("oeTask.executor.handInExam.sleep",
+                DEFAULT_HAND_IN_EXAM_EXECUTOR_SLEEP_SECONDS));//单位秒
+
+        node3.setLowerNode(node4);
+        node3.setSleep(PropertyHolder.getInt("oeTask.executor.afterHandInExam.sleep",
+                DEFAULT_AFTER_HAND_IN_EXAM_EXECUTOR_SLEEP_SECONDS));//单位秒
+
+        node4.setLowerNode(node5);
+        node4.setSleep(PropertyHolder.getInt("oeTask.executor.syncExamData.sleep",
+                DEFAULT_SYNC_EXAM_DATA_EXECUTOR_SLEEP_SECONDS));//单位秒
+
+        node5.setSleep(PropertyHolder.getInt("oeTask.executor.clearExamDataCache.sleep",
+                DEFAULT_CLEAR_EXAM_DATA_CACHE_EXECUTOR_SLEEP_SECONDS));//单位秒
+
+        node1.start();
+        node2.start();
+        node3.start();
+        node4.start();
+        node5.start();
+    }
+
+
+}

+ 44 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/SwaggerConfig.java

@@ -0,0 +1,44 @@
+/*
+ * *************************************************
+ * Copyright (c) 2018 QMTH. All Rights Reserved.
+ * Created by Deason on 2018-08-15 16:17:41.
+ * *************************************************
+ */
+
+package cn.com.qmth.examcloud.core.oe.task.starter.config;
+
+import io.swagger.annotations.ApiOperation;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+@Configuration
+@EnableSwagger2
+public class SwaggerConfig {
+
+    @Bean
+    public Docket buildDocket() {
+        return new Docket(DocumentationType.SWAGGER_2)
+                .groupName("Version 3.0")
+                .apiInfo(buildApiInfo())
+                .useDefaultResponseMessages(false)
+                .select()
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                .paths(PathSelectors.any())
+                .build();
+    }
+
+    public ApiInfo buildApiInfo() {
+        return new ApiInfoBuilder()
+                .title("网考学生端照片抓拍处理接口文档")
+                .version("3.0")
+                .build();
+    }
+
+}

+ 49 - 0
examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/SystemStartup.java

@@ -0,0 +1,49 @@
+package cn.com.qmth.examcloud.core.oe.task.starter.config;
+
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+
+/**
+ * 系统启动
+ *
+ * @author WANGWEI
+ * @date 2018年11月29日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+@Order(99)
+public class SystemStartup implements ApplicationRunner {
+
+	@Autowired
+	private DiscoveryClient discoveryClient;
+
+	public void start() {
+
+		String appName = PropertyHolder.getString("spring.application.name");
+
+		if (StringUtils.isNotBlank(appName)) {
+			List<ServiceInstance> instances = discoveryClient.getInstances(appName);
+			if (!instances.isEmpty()) {
+				throw new ExamCloudRuntimeException("multiple instances!");
+			}
+		}
+
+	}
+
+	@Override
+	public void run(ApplicationArguments args) throws Exception {
+		start();
+	}
+
+}

+ 5 - 0
examcloud-core-oe-task-starter/src/main/resources/application.properties

@@ -0,0 +1,5 @@
+spring.profiles.active=dev
+examcloud.startup.startupCode=3000
+examcloud.startup.configCenterHost=127.0.0.1
+examcloud.startup.configCenterPort=9999
+examcloud.startup.appCode=OET

+ 1 - 0
examcloud-core-oe-task-starter/src/main/resources/classpath.location

@@ -0,0 +1 @@
+classpath 定位文件

+ 21 - 0
examcloud-core-oe-task-starter/src/main/resources/limited.properties

@@ -0,0 +1,21 @@
+# mean: \u5e73\u5747\u8017\u65f6(\u6beb\u79d2)
+# meanRate: \u5e73\u5747\u901f\u7387(\u6b21/\u79d2)
+# oneMinRate: \u4e00\u5206\u949f\u5185\u901f\u7387(\u6b21/\u79d2)
+# [S] \u4e3a\u77ac\u65f6TPS
+# [E] \u76d1\u63a7\u9650\u6d41\u8868\u8fbe\u5f0f
+# [S] \u9650\u6d41\u540e\u6700\u5c0f
+
+[${app.api.oe.student.face}/examCaptureQueue][/compareFaceSync][POST][S] : 500
+[${app.api.oe.student.face}/examCaptureQueue][/compareFaceSync][POST][E] : mean>2000 && oneMinRate>10
+[${app.api.oe.student.face}/examCaptureQueue][/compareFaceSync][POST][R] : 10
+
+[${app.api.oe.student.face}/examCaptureQueue][/getExamCaptureResult][GET][S] : 300
+[${app.api.oe.student.face}/examCaptureQueue][/getExamCaptureResult][GET][E] : mean>1000 && oneMinRate>10
+[${app.api.oe.student.face}/examCaptureQueue][/getExamCaptureResult][GET][R] : 10
+
+[${app.api.oe.student.face}/examCaptureQueue][/uploadExamCapture][POST][S] : 300
+[${app.api.oe.student.face}/examCaptureQueue][/uploadExamCapture][POST][E] : mean>1000 && oneMinRate>10
+[${app.api.oe.student.face}/examCaptureQueue][/uploadExamCapture][POST][GET][R] : 10
+
+
+

+ 146 - 0
examcloud-core-oe-task-starter/src/main/resources/log4j2.xml

@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN" monitorInterval="30">
+
+	<Properties>
+		<Property name="commonLevel" value="${sys:log.commonLevel}" />
+	</Properties>
+
+	<Appenders>
+		<!-- 控制台 日志 -->
+		<Console name="Console" target="SYSTEM_OUT">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+		</Console>
+		<!-- debug 日志 -->
+		<RollingFile name="DEBUG_APPENDER" fileName="./logs/debug/debug.log" filePattern="./logs/debug/debug-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+			<DefaultRolloverStrategy max="10000">
+				<Delete basePath="./logs/debug" maxDepth="1">
+					<IfFileName glob="debug-*.log">
+						<IfAccumulatedFileSize exceeds="2 GB" />
+					</IfFileName>
+				</Delete>
+			</DefaultRolloverStrategy>
+		</RollingFile>
+		<!-- 接口日志 -->
+		<RollingFile name="INTERFACE_APPENDER" fileName="./logs/interface/interface.log" filePattern="./logs/interface/interface-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+			<DefaultRolloverStrategy max="10000">
+				<Delete basePath="./logs/interface" maxDepth="1">
+					<IfFileName glob="interface-*.log">
+						<IfAccumulatedFileSize exceeds="10 GB" />
+					</IfFileName>
+				</Delete>
+			</DefaultRolloverStrategy>
+		</RollingFile>
+		<!-- 处理照片 日志 -->
+		<RollingFile name="PROCESS_EXAM_CAPTURE_TASK_APPENDER" fileName="./logs/capture/capture.log" filePattern="./logs/capture/capture-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+			<DefaultRolloverStrategy max="10000">
+				<Delete basePath="./logs/capture" maxDepth="1">
+					<IfFileName glob="capture-*.log">
+						<IfAccumulatedFileSize exceeds="2 GB" />
+					</IfFileName>
+				</Delete>
+			</DefaultRolloverStrategy>
+		</RollingFile>
+
+		<!-- 流式自动服务综合 日志 -->
+		<RollingFile name="ROOT_STREAM_TASK_APPENDER" fileName="./logs/stream/root-stream.log" filePattern="./logs/stream/root-stream-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+			<DefaultRolloverStrategy max="10000">
+				<Delete basePath="./logs/stream" maxDepth="1">
+					<IfFileName glob="root-stream-*.log">
+						<IfAccumulatedFileSize exceeds="2 GB" />
+					</IfFileName>
+				</Delete>
+			</DefaultRolloverStrategy>
+		</RollingFile>
+
+		<!-- 流式自动服务详细 日志 -->
+		<RollingFile name="NODE_STREAM_TASK_APPENDER" fileName="./logs/stream/node-stream.log" filePattern="./logs/stream/node-stream-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+			<DefaultRolloverStrategy max="10000">
+				<Delete basePath="./logs/stream" maxDepth="1">
+					<IfFileName glob="node-stream-*.log">
+						<IfAccumulatedFileSize exceeds="2 GB" />
+					</IfFileName>
+				</Delete>
+			</DefaultRolloverStrategy>
+		</RollingFile>
+	</Appenders>
+
+	<Loggers>
+		<Logger name="cn.com.qmth" level="${commonLevel}" additivity="false">
+			<AppenderRef ref="DEBUG_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="INTERFACE_LOGGER" level="INFO" additivity="false">
+			<AppenderRef ref="INTERFACE_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="PROCESS_EXAM_CAPTURE_TASK_LOGGER" level="DEBUG" additivity="false">
+			<AppenderRef ref="PROCESS_EXAM_CAPTURE_TASK_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="cn.com.qmth.examcloud.web.facepp" level="DEBUG" additivity="false">
+			<AppenderRef ref="PROCESS_EXAM_CAPTURE_TASK_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="cn.com.qmth.examcloud.web.baidu" level="DEBUG" additivity="false">
+			<AppenderRef ref="PROCESS_EXAM_CAPTURE_TASK_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="cn.com.qmth.examcloud.commons.helpers.concurrency.simple" level="DEBUG" additivity="false">
+			<AppenderRef ref="PROCESS_EXAM_CAPTURE_TASK_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="cn.com.qmth.examcloud.commons.helpers.pipeline" level="DEBUG" additivity="false">
+			<AppenderRef ref="ROOT_STREAM_TASK_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="cn.com.qmth.examcloud.core.oe.task.service.pipeline" level="DEBUG" additivity="false">
+			<AppenderRef ref="NODE_STREAM_TASK_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="${commonLevel}" />
+		<Logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="${commonLevel}" />
+		<Logger name="org.hibernate.SQL" level="${commonLevel}" />
+		<Logger name="org.hibernate.type" level="${commonLevel}" />
+		<Logger name="org.hibernate.engine.QueryParameters" level="${commonLevel}" />
+		<Logger name="org.hibernate.engine.query.HQLQueryPlan" level="${commonLevel}" />
+
+		<Root level="INFO">
+			<AppenderRef ref="Console" />
+			<AppenderRef ref="DEBUG_APPENDER" />
+		</Root>
+	</Loggers>
+
+</Configuration>

+ 0 - 0
examcloud-core-oe-task-starter/src/main/resources/security.properties


+ 20 - 0
jenkins-dev.sh

@@ -0,0 +1,20 @@
+#!/bin/bash
+pwd
+
+rm -rf ~/project/examcloud/examcloud-core-oe-task-distribution.zip
+rm -rf ~/project/examcloud/examcloud-core-oe-task/lib/
+
+cp examcloud-core-oe-task-starter/target/examcloud-core-oe-task-distribution.zip ~/project/examcloud/
+
+cd  ~/project/examcloud/
+unzip -o examcloud-core-oe-task-distribution.zip
+
+cd examcloud-core-oe-task
+echo "--spring.profiles.active=dev --examcloud.startup.configCenterHost=localhost" > start.args
+echo "-server -Xms512m -Xmx512m  -XX:-UseGCOverheadLimit -Dfacepp.compare.url=https://api-cn.faceplusplus.com/facepp/v3/compare" > start.vmoptions
+
+bash stop.sh
+BUILD_ID=DONTKILLME
+
+sleep 30s
+bash start.sh 100

+ 4 - 0
jenkins-prod.sh

@@ -0,0 +1,4 @@
+#!/bin/bash
+pwd
+
+cp examcloud-core-oe-task-starter/target/examcloud-core-oe-task-distribution.zip ~/packages

+ 20 - 0
jenkins-test.sh

@@ -0,0 +1,20 @@
+#!/bin/bash
+pwd
+
+rm -rf ~/project/examcloud/examcloud-core-oe-task-distribution.zip
+rm -rf ~/project/examcloud/examcloud-core-oe-task/lib/
+
+cp examcloud-core-oe-task-starter/target/examcloud-core-oe-task-distribution.zip ~/project/examcloud/
+
+cd  ~/project/examcloud/
+unzip -o examcloud-core-oe-task-distribution.zip
+
+cd examcloud-core-oe-task
+echo "--spring.profiles.active=test --examcloud.startup.configCenterHost=localhost" > start.args
+echo "-server -Xms512m -Xmx512m  -XX:-UseGCOverheadLimit -Dfacepp.compare.url=https://api-cn.faceplusplus.com/facepp/v3/compare" > start.vmoptions
+
+bash stop.sh
+BUILD_ID=DONTKILLME
+
+sleep 30s
+bash start.sh 100

+ 20 - 0
pom.xml

@@ -0,0 +1,20 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>cn.com.qmth.examcloud</groupId>
+		<artifactId>examcloud-parent</artifactId>
+		<version>2019</version>
+	</parent>
+	<artifactId>examcloud-core-oe-task</artifactId>
+	<version>2019-SNAPSHOT</version>
+	<packaging>pom</packaging>
+
+
+	<modules>
+		<module>examcloud-core-oe-task-base</module>
+		<module>examcloud-core-oe-task-dao</module>
+		<module>examcloud-core-oe-task-api-provider</module>
+		<module>examcloud-core-oe-task-service</module>
+		<module>examcloud-core-oe-task-starter</module>
+	</modules>
+</project>