Prechádzať zdrojové kódy

图片抓拍代码迁移和重构

lideyin 5 rokov pred
rodič
commit
d961e63d1e
16 zmenil súbory, kde vykonal 2090 pridanie a 1 odobranie
  1. 152 0
      examcloud-core-oe-task-base/src/main/java/cn/com/qmth/examcloud/core/oe/task/base/ExamCaptureProcessStatisticController.java
  2. 1 1
      examcloud-core-oe-task-service/pom.xml
  3. 31 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamCaptureQueueService.java
  4. 42 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamCaptureService.java
  5. 14 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/ExamSyncCaptureService.java
  6. 129 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/CompareFaceSyncInfo.java
  7. 296 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/ExamCaptureQueueInfo.java
  8. 83 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/bean/SaveExamCaptureQueueInfo.java
  9. 84 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamCaptureQueueServiceImpl.java
  10. 501 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/impl/ExamCaptureServiceImpl.java
  11. 77 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/worker/BaiduFaceLivenessWorker.java
  12. 84 0
      examcloud-core-oe-task-service/src/main/java/cn/com/qmth/examcloud/core/oe/task/service/worker/FacePPCompareWorker.java
  13. 149 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessBaiduFaceLivenessAlarmTask.java
  14. 143 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessBaiduFacelivenessTask.java
  15. 158 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessFaceCompareAlarmTask.java
  16. 146 0
      examcloud-core-oe-task-starter/src/main/java/cn/com/qmth/examcloud/core/oe/task/starter/config/ProcessFaceCompareQueueTask.java

+ 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();
+        }
+    }
+}

+ 1 - 1
examcloud-core-oe-task-service/pom.xml

@@ -11,7 +11,7 @@
 	<dependencies>
 		<dependency>
 			<groupId>cn.com.qmth.examcloud</groupId>
-			<artifactId>examcloud-core-oe-commons-dao</artifactId>
+			<artifactId>examcloud-core-oe-task-dao</artifactId>
 			<version>${examcloud.version}</version>
 		</dependency>
 	</dependencies>

+ 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);
+
+}

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

@@ -0,0 +1,42 @@
+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.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;
+}

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

@@ -0,0 +1,14 @@
+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 {
+	@Transactional
+	void saveExamCaptureSyncCompareResult(Long studentId, Long examRecordDataId);
+}

+ 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;
+	}
+    
+}

+ 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;
+    }
+
+
+}

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

@@ -0,0 +1,501 @@
+package cn.com.qmth.examcloud.core.oe.task.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.helpers.JsonHttpResponseHolder;
+import cn.com.qmth.examcloud.support.Constants;
+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.bean.CompareFaceSyncInfo;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.ExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.ExamRecordPropertyCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.support.enums.ExamProperties;
+import cn.com.qmth.examcloud.support.helper.ExamCacheTransferHelper;
+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;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+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.data.domain.Example;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+
+/**
+ * @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;
+
+    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执行人脸比对,得到返回结果
+            String backup = PropertyHolder.getString("$upyun.site.1.domain.backup");
+            String main = PropertyHolder.getString("$upyun.site.1.domain");
+            JsonHttpResponseHolder jsonHttpResponseHolder = null;
+            try {
+                jsonHttpResponseHolder = FaceppClient.getClient().
+                        compareWithTokenAndImageUrl(examCaptureQueue.getBaseFaceToken(),
+                                examCaptureQueue.getFileUrl(), examCaptureQueue.getFileUrl().replace(main, backup));
+                /*//test code
+                jsonHttpResponseHolder =new JsonHttpResponseHolder();
+                jsonHttpResponseHolder.setStatusCode(200);
+                jsonHttpResponseHolder.setRespBody(JSONObject.parseObject("{\"error_message\":\"API_NOT_FOUND\"}"));
+*/
+                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] 百度活体检测开始...");
+
+            //获取百度活检结果
+            String backup = PropertyHolder.getString("$upyun.site.1.domain.backup");
+            String main = PropertyHolder.getString("$upyun.site.1.domain");
+            JsonHttpResponseHolder jsonHttpResponseHolder = null;
+            try {
+                jsonHttpResponseHolder = BaiduClient.getClient().verifyFaceLiveness(examCaptureQueue.getFileUrl(),
+                        examCaptureQueue.getFileUrl().replace(main, backup));
+                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());
+    }
+
+    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;
+    }
+
+
+
+    @Override
+    public CompareFaceSyncInfo compareFaceSyncByFileUrl(Long studentId, String baseFaceToken, String fileUrl) {
+        CompareFaceSyncInfo compareFaceSyncInfo = new CompareFaceSyncInfo();
+        compareFaceSyncInfo.setStudentId(studentId);
+        JSONObject facePPResult = null;
+        JsonHttpResponseHolder jsonHttpResponseHolder = null;
+        try {
+            String backup = PropertyHolder.getString("$upyun.site.1.domain.backup");
+            String main = PropertyHolder.getString("$upyun.site.1.domain");
+            jsonHttpResponseHolder = FaceppClient.getClient().
+                    compareWithTokenAndImageUrl(baseFaceToken,
+                            fileUrl, fileUrl.replace(main, backup));
+            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);
+    }
+
+
+    /**
+     * 校验是否有陌生人脸
+     *
+     * @param examCaptureQueue
+     * @param jsonObject
+     * @return
+     * @throws JSONException
+     */
+    private boolean calculateFaceCompareIsStranger(Long examRecordDataId, Long studentId, JSONObject jsonObject) {
+        JSONArray face2Array = jsonObject.getJSONArray("faces2");
+        //添加是否有陌生人开关功能
+        ExamRecordPropertyCacheBean examRecordPropertyCache = CacheHelper.getExamRecordProperty(examRecordDataId);
+        //默认开启了陌生人检测
+        String isStrangerEnableStr = "true";
+        if (examRecordPropertyCache != null) {
+            isStrangerEnableStr = ExamCacheTransferHelper.getCachedExamProperty(examRecordPropertyCache.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;
+    }
+
+}

+ 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();
+        }
+    }
+}

+ 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.common.entity.ExamCaptureQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.student.face.service.bean.ExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.core.oe.student.face.service.impl.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.student.face.service.impl.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;
+    }
+}