Browse Source

活检相关代码重构

lideyin 5 năm trước cách đây
mục cha
commit
49d80a40cb
12 tập tin đã thay đổi với 1653 bổ sung69 xóa
  1. 0 20
      examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/enums/HandInExamType.java
  2. 0 48
      examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/enums/IsSuccess.java
  3. 49 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/FaceBiopsyInfo.java
  4. 153 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/FaceBiopsyStepInfo.java
  5. 51 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/GetFaceVerifyTokenInfo.java
  6. 49 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/SaveFaceBiopsyResultReq.java
  7. 58 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/SaveFaceBiopsyResultResp.java
  8. 1 1
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamControlService.java
  9. 85 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamFaceLivenessVerifyService.java
  10. 37 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/FaceBiopsyService.java
  11. 319 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamFaceLivenessVerifyServiceImpl.java
  12. 851 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/FaceBiopsyServiceImpl.java

+ 0 - 20
examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/enums/HandInExamType.java

@@ -1,20 +0,0 @@
-package cn.com.qmth.examcloud.core.oe.student.dao.enums;
-
-
-/**
- * @Description 交卷类型
- * @Author lideyin
- * @Date 2019/8/1 18:07
- * @Version 1.0
- */
-public enum HandInExamType {
-    /**
-     * 手工交卷
-     */
-    MANUAL,
-    /**
-     * 自动交卷
-     */
-    AUTO
-
-}

+ 0 - 48
examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/enums/IsSuccess.java

@@ -1,48 +0,0 @@
-package cn.com.qmth.examcloud.core.oe.student.dao.enums;
-
-import org.apache.commons.lang3.StringUtils;
-
-/**
- * @author  	chenken
- * @date    	2018年2月26日 下午3:13:05
- * @company 	QMTH
- * @description 成功或失败
- */
-public enum IsSuccess {
-	/**
-	 * 成功
-	 */
-	SUCCESS("成功"),
-	/**
-	 * 失败
-	 */
-	FAILED("失败");
-	
-	private String desc;
-	
-	public static IsSuccess strToEnum(String name){
-		if(StringUtils.isBlank(name)){
-			return null;
-		}
-		for(IsSuccess isSuccess:IsSuccess.values()){
-			if(name.equals(isSuccess.name())){
-				return isSuccess;
-			}
-		}
-		
-		return null;
-	}
-	
-	private IsSuccess(String desc){
-		this.desc = desc;
-	}
-	
-	public String getDesc() {
-		return desc;
-	}
-
-	public void setDesc(String desc) {
-		this.desc = desc;
-	}
-}
-

+ 49 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/FaceBiopsyInfo.java

@@ -0,0 +1,49 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.List;
+
+/**
+ * @Description 人脸活体检测基本信息
+ * @Author lideyin
+ * @Date 2019/10/14 11:21
+ * @Version 1.0
+ */
+public class FaceBiopsyInfo implements JsonSerializable {
+    private static final long serialVersionUID = -8323306595159483818L;
+
+    @ApiModelProperty(value = "活体检测开始分钟数", required = true)
+    private Integer faceVerifyMinute;
+
+    @ApiModelProperty(value = "人脸活体检测明细id", required = true)
+    private Long faceBiopsyItemId;
+
+    @ApiModelProperty(value = "人脸活体检测步骤", required = true)
+    private List<FaceBiopsyStepInfo> verifySteps;
+
+    public Integer getFaceVerifyMinute() {
+        return faceVerifyMinute;
+    }
+
+    public void setFaceVerifyMinute(Integer faceVerifyMinute) {
+        this.faceVerifyMinute = faceVerifyMinute;
+    }
+
+    public Long getFaceBiopsyItemId() {
+        return faceBiopsyItemId;
+    }
+
+    public void setFaceBiopsyItemId(Long faceBiopsyItemId) {
+        this.faceBiopsyItemId = faceBiopsyItemId;
+    }
+
+    public List<FaceBiopsyStepInfo> getVerifySteps() {
+        return verifySteps;
+    }
+
+    public void setVerifySteps(List<FaceBiopsyStepInfo> verifySteps) {
+        this.verifySteps = verifySteps;
+    }
+}

+ 153 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/FaceBiopsyStepInfo.java

@@ -0,0 +1,153 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import cn.com.qmth.examcloud.core.oe.student.dao.enums.FaceBiopsyAction;
+import cn.com.qmth.examcloud.core.oe.student.dao.enums.ResourceType;
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+
+/**
+ * @Description 人脸活体检测步骤
+ * @Author lideyin
+ * @Date 2019/10/14 11:21
+ * @Version 1.0
+ */
+public class FaceBiopsyStepInfo implements JsonSerializable {
+	private static final long serialVersionUID = -2108253508943543996L;
+
+	@ApiModelProperty(value = "步骤id",required = true)
+	private Long stepId;
+
+	@ApiModelProperty(value = "具体动作",required = true)
+	@Enumerated(EnumType.STRING)
+	private FaceBiopsyAction action;
+
+	@ApiModelProperty(value = "动作时长")
+	private Integer stay;
+
+	@ApiModelProperty(value = "资源文件路径")
+	private String resourceUrl;
+
+	@ApiModelProperty(value = "资源文件类型")
+	@Enumerated(EnumType.STRING)
+	private ResourceType resourceType;
+
+	@ApiModelProperty(value = "执行结果")
+	private Boolean result;
+
+	@ApiModelProperty(value = "指令是否超时")
+	private Boolean timeout;
+
+	@ApiModelProperty(value = "是否有陌生人脸即多张人脸")
+	private Boolean stranger;
+
+	@ApiModelProperty(value = "是否晃出摄像头")
+	private Boolean waggleOutCamera;
+
+	@ApiModelProperty(value = "是否检测到人脸")
+	private Boolean hasFace;
+
+	@ApiModelProperty(value = "指令执行结果json串")
+	private String resultJson;
+
+	@ApiModelProperty(value = "错误描述")
+	private String errorMsg;
+
+	public Long getStepId() {
+		return stepId;
+	}
+
+	public void setStepId(Long stepId) {
+		this.stepId = stepId;
+	}
+
+	public FaceBiopsyAction getAction() {
+		return action;
+	}
+
+	public void setAction(FaceBiopsyAction action) {
+		this.action = action;
+	}
+
+	public Integer getStay() {
+		return stay;
+	}
+
+	public void setStay(Integer stay) {
+		this.stay = stay;
+	}
+
+	public String getResourceUrl() {
+		return resourceUrl;
+	}
+
+	public void setResourceUrl(String resourceUrl) {
+		this.resourceUrl = resourceUrl;
+	}
+
+	public ResourceType getResourceType() {
+		return resourceType;
+	}
+
+	public void setResourceType(ResourceType resourceType) {
+		this.resourceType = resourceType;
+	}
+
+	public Boolean getResult() {
+		return result;
+	}
+
+	public void setResult(Boolean result) {
+		this.result = result;
+	}
+
+	public String getResultJson() {
+		return resultJson;
+	}
+
+	public void setResultJson(String resultJson) {
+		this.resultJson = resultJson;
+	}
+
+	public String getErrorMsg() {
+		return errorMsg;
+	}
+
+	public void setErrorMsg(String errorMsg) {
+		this.errorMsg = errorMsg;
+	}
+
+	public Boolean getTimeout() {
+		return timeout;
+	}
+
+	public void setTimeout(Boolean timeout) {
+		this.timeout = timeout;
+	}
+
+	public Boolean getStranger() {
+		return stranger;
+	}
+
+	public void setStranger(Boolean stranger) {
+		this.stranger = stranger;
+	}
+
+	public Boolean getWaggleOutCamera() {
+		return waggleOutCamera;
+	}
+
+	public void setWaggleOutCamera(Boolean waggleOutCamera) {
+		this.waggleOutCamera = waggleOutCamera;
+	}
+
+	public Boolean getHasFace() {
+		return hasFace;
+	}
+
+	public void setHasFace(Boolean hasFace) {
+		this.hasFace = hasFace;
+	}
+}

+ 51 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/GetFaceVerifyTokenInfo.java

@@ -0,0 +1,51 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class GetFaceVerifyTokenInfo implements JsonSerializable {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -4053674655722904852L;
+
+	private Boolean success;
+	
+	private String faceLivenessToken;
+
+	private Long faceVerifyId;
+	
+	private String errorMsg;
+
+	public Boolean getSuccess() {
+		return success;
+	}
+
+	public void setSuccess(Boolean success) {
+		this.success = success;
+	}
+
+	public String getFaceLivenessToken() {
+		return faceLivenessToken;
+	}
+
+	public void setFaceLivenessToken(String faceLivenessToken) {
+		this.faceLivenessToken = faceLivenessToken;
+	}
+
+	public String getErrorMsg() {
+		return errorMsg;
+	}
+
+	public void setErrorMsg(String errorMsg) {
+		this.errorMsg = errorMsg;
+	}
+
+	public Long getFaceVerifyId() {
+		return faceVerifyId;
+	}
+
+	public void setFaceVerifyId(Long faceVerifyId) {
+		this.faceVerifyId = faceVerifyId;
+	}
+}

+ 49 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/SaveFaceBiopsyResultReq.java

@@ -0,0 +1,49 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.List;
+
+/**
+ * @Description 保存人脸活体检测结果请求对象
+ * @Author lideyin
+ * @Date 2019/10/14 11:21
+ * @Version 1.0
+ */
+public class SaveFaceBiopsyResultReq implements JsonSerializable {
+	private static final long serialVersionUID = 3996944262550668002L;
+
+	@ApiModelProperty(value = "考试记录id",required = true)
+	private Long examRecordDataId;
+
+	@ApiModelProperty(value = "人脸活体检测明细id",required = true)
+	private Long faceBiopsyItemId;
+
+	@ApiModelProperty(value = "实际活体检测步骤",required = true)
+	private List<FaceBiopsyStepInfo> verifySteps;
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Long getFaceBiopsyItemId() {
+		return faceBiopsyItemId;
+	}
+
+	public void setFaceBiopsyItemId(Long faceBiopsyItemId) {
+		this.faceBiopsyItemId = faceBiopsyItemId;
+	}
+
+	public List<FaceBiopsyStepInfo> getVerifySteps() {
+		return verifySteps;
+	}
+
+	public void setVerifySteps(List<FaceBiopsyStepInfo> verifySteps) {
+		this.verifySteps = verifySteps;
+	}
+}

+ 58 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/SaveFaceBiopsyResultResp.java

@@ -0,0 +1,58 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * @Description 人脸活体检测结果
+ * @Author lideyin
+ * @Date 2019/10/14 11:21
+ * @Version 1.0
+ */
+public class SaveFaceBiopsyResultResp implements JsonSerializable {
+	private static final long serialVersionUID = 3996944262550668002L;
+
+	@ApiModelProperty(value = "检测结果",required = true)
+	private Boolean verifyResult;
+
+	@ApiModelProperty(value = "错误消息",required = false)
+	private String errorMessage;
+
+	@ApiModelProperty(value = "是否结束考试",required = true)
+	private Boolean  endExam;
+
+	@ApiModelProperty(value = "是否需要下一次活检",required = true)
+	private Boolean  needNextVerify;
+
+	public Boolean getVerifyResult() {
+		return verifyResult;
+	}
+
+	public void setVerifyResult(Boolean verifyResult) {
+		this.verifyResult = verifyResult;
+	}
+
+	public String getErrorMessage() {
+		return errorMessage;
+	}
+
+	public void setErrorMessage(String errorMessage) {
+		this.errorMessage = errorMessage;
+	}
+
+	public Boolean getEndExam() {
+		return endExam;
+	}
+
+	public void setEndExam(Boolean endExam) {
+		this.endExam = endExam;
+	}
+
+	public Boolean getNeedNextVerify() {
+		return needNextVerify;
+	}
+
+	public void setNeedNextVerify(Boolean needNextVerify) {
+		this.needNextVerify = needNextVerify;
+	}
+}

+ 1 - 1
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamControlService.java

@@ -4,7 +4,7 @@ package cn.com.qmth.examcloud.core.oe.student.service;
 import cn.com.qmth.examcloud.api.commons.security.bean.User;
 import cn.com.qmth.examcloud.core.oe.student.bean.CheckExamInProgressInfo;
 import cn.com.qmth.examcloud.core.oe.student.bean.StartExamInfo;
-import cn.com.qmth.examcloud.core.oe.student.dao.enums.HandInExamType;
+import cn.com.qmth.examcloud.support.enums.HandInExamType;
 
 /**
  * @author chenken

+ 85 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamFaceLivenessVerifyService.java

@@ -0,0 +1,85 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.oe.student.bean.GetFaceVerifyTokenInfo;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamFaceLivenessVerifyEntity;
+
+import java.util.List;
+
+
+/**
+ * @Description 活体检测服务接口
+ * @Author lideyin
+ * @Date 2019/12/12 15:56
+ * @Version 1.0
+ */
+public interface ExamFaceLivenessVerifyService {
+
+    /**
+     * 保存活体检测信息
+     *
+     * @param examRecordDataId
+     */
+    ExamFaceLivenessVerifyEntity saveFaceVerify(Long examRecordDataId);
+
+    /**
+     * 使用考试记录id查询人脸活体检测信息
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    List<ExamFaceLivenessVerifyEntity> listFaceVerifyByExamRecordId(Long examRecordDataId);
+
+    /**
+     * 人脸检测完成后回调处理
+     *
+     * @param resultJson
+     * @return
+     */
+    ExamFaceLivenessVerifyEntity faceIdNotify(String resultJson);
+
+    /**
+     * 向faceId发起检测请求,返回token
+     *
+     * @return
+     */
+    GetFaceVerifyTokenInfo getFaceVerifyToken(Long studentId, String bizNo);
+
+    ExamFaceLivenessVerifyEntity saveFaceVerifyByExamRecordDataId(Long examRecordDataId);
+
+    /**
+     * 根据ID查询
+     *
+     * @param id
+     * @return
+     */
+    ExamFaceLivenessVerifyEntity findFaceVerifyById(Long id);
+
+    /**
+     * 人脸检测超时处理
+     *
+     * @param examRecordDataId
+     */
+    void faceTestTimeOut(Long examRecordDataId);
+
+    /**
+     * 人脸活体检测结束处理
+     *
+     * @param examRecordDataId
+     * @param studentId
+     * @param result
+     */
+    void faceTestEndHandle(Long examRecordDataId, Long studentId, String result);
+
+    /**
+     * 断点续考,获取活体检测开启时间
+     *
+     * @param examId
+     * @param examRecordDataId
+     * @param heartbeat
+     * @return
+     */
+    Integer getFaceLivenessVerifyMinute(Long rootOrgId, Long orgId, Long examId,
+                                        Long studentId, Long examRecordDataId, Integer heartbeat);
+
+}
+

+ 37 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/FaceBiopsyService.java

@@ -0,0 +1,37 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.oe.student.bean.FaceBiopsyInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.SaveFaceBiopsyResultReq;
+import cn.com.qmth.examcloud.core.oe.student.bean.SaveFaceBiopsyResultResp;
+import cn.com.qmth.examcloud.core.oe.student.dao.enums.FaceBiopsyType;
+
+/**
+ * @Description 人脸活体检测接口
+ * @Author lideyin
+ * @Date 2019/10/14 10:54
+ * @Version 1.0
+ */
+public interface FaceBiopsyService {
+
+    /**
+     * 获取人脸活体检测基本信息
+     *
+     * @param rootOrgId        学校id
+     * @param examRecordDataId 考试记录id
+     */
+    FaceBiopsyInfo getFaceBiopsyInfo(Long rootOrgId, Long examRecordDataId, FaceBiopsyType faceBiopsyType);
+
+    /**
+     * 保存活体检测结果
+     * @param req
+     * @return
+     */
+    SaveFaceBiopsyResultResp saveFaceBiopsyResult(SaveFaceBiopsyResultReq req, Long studentId);
+
+    /**
+     * 获取人脸活体检测开始分钟数
+     * @param examRecordDataId 考试记录id
+     * @return Integer
+     */
+    Integer calculateFaceBiopsyStartMinute(Long examRecordDataId);
+}

+ 319 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamFaceLivenessVerifyServiceImpl.java

@@ -0,0 +1,319 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.util.PathUtil;
+import cn.com.qmth.examcloud.core.oe.student.base.utils.CommonUtil;
+import cn.com.qmth.examcloud.core.oe.student.base.utils.FileDisposeUtil;
+import cn.com.qmth.examcloud.core.oe.student.base.utils.HttpPoolUtil;
+import cn.com.qmth.examcloud.core.oe.student.bean.GetFaceVerifyTokenInfo;
+import cn.com.qmth.examcloud.core.oe.student.dao.ExamFaceLivenessVerifyRepo;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamFaceLivenessVerifyEntity;
+import cn.com.qmth.examcloud.core.oe.student.dao.enums.FaceVerifyResult;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamControlService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamFaceLivenessVerifyService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamingSessionService;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.StudentCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.support.enums.ExamProperties;
+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.ExamCacheTransferHelper;
+import cn.com.qmth.examcloud.support.helper.FaceBiopsyHelper;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.config.SystemProperties;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+
+/**
+ * @author chenken
+ * @date 2018/8/15 15:57
+ * @company QMTH
+ * @description 活体检测服务实现
+ */
+@Service("examFaceLivenessVerifyService")
+public class ExamFaceLivenessVerifyServiceImpl implements ExamFaceLivenessVerifyService {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamFaceLivenessVerifyServiceImpl.class);
+
+    @Autowired
+    private ExamFaceLivenessVerifyRepo examFaceLivenessVerifyRepo;
+
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+
+    @Autowired
+    private ExamingSessionService examingSessionService;
+
+    @Autowired
+    private ExamControlService examControlService;
+    @Autowired
+    SystemProperties systemProperties;
+
+    /**
+     * 第二次人脸检测时间范围
+     */
+    private static final int secondFaceCheckMinute = 4;
+
+    @Override
+    public ExamFaceLivenessVerifyEntity saveFaceVerify(Long examRecordDataId) {
+        ExamFaceLivenessVerifyEntity faceVerify = new ExamFaceLivenessVerifyEntity();
+        faceVerify.setExamRecordDataId(examRecordDataId);
+        faceVerify.setStartTime(new Date());
+        faceVerify.setIsError(false);
+        faceVerify.setOperateNum(1);
+        return examFaceLivenessVerifyRepo.save(faceVerify);
+    }
+
+    @Override
+    public List<ExamFaceLivenessVerifyEntity> listFaceVerifyByExamRecordId(Long examRecordDataId) {
+        if (examRecordDataId == null) {
+            return new ArrayList<ExamFaceLivenessVerifyEntity>();
+        }
+        return examFaceLivenessVerifyRepo.findByExamRecordDataIdOrderById(examRecordDataId);
+    }
+
+    @Override
+    public ExamFaceLivenessVerifyEntity faceIdNotify(String faceIdResultJson) {
+        JSONObject faceIdResultJsonObject;
+        String bizNo;
+
+        ExamFaceLivenessVerifyEntity faceVerify = new ExamFaceLivenessVerifyEntity();
+        try {
+            faceIdResultJsonObject = new JSONObject(faceIdResultJson);
+            bizNo = faceIdResultJsonObject.getString("biz_no");
+            Long faceVerifyId;
+            if (bizNo.indexOf("_") == -1) {
+                faceVerifyId = Long.parseLong(bizNo + "");
+            } else {
+                faceVerifyId = Long.parseLong(bizNo.substring(0, bizNo.indexOf("_")));
+            }
+
+            faceVerify = findFaceVerifyById(faceVerifyId);
+            if (faceIdResultJsonObject.has("verify_result")
+                    && faceIdResultJsonObject.get("verify_result") != null
+                    && !"null".equals(faceIdResultJsonObject.get("verify_result") + "")) {
+                JSONObject verifyResultJson = faceIdResultJsonObject.getJSONObject("verify_result");
+                JSONObject result_ref1 = verifyResultJson.getJSONObject("result_ref1");
+                double confidence = result_ref1.getDouble("confidence");
+                //人脸相似度
+                if (confidence > 50D) {
+                    JSONObject livenessResultJson = faceIdResultJsonObject.getJSONObject("liveness_result");
+                    if (livenessResultJson.has("result") && "success".equals(livenessResultJson.getString("result"))) {
+                        faceVerify.setVerifyResult(FaceVerifyResult.VERIFY_SUCCESS);
+                    }
+                } else {
+                    faceVerify.setVerifyResult(FaceVerifyResult.NOT_ONESELF);
+                }
+            } else {
+                faceVerify.setVerifyResult(FaceVerifyResult.VERIFY_FAILED);
+            }
+            faceVerify.setResultJson(faceIdResultJsonObject.toString());
+            faceVerify.setBizId(faceIdResultJsonObject.getString("biz_id"));
+        } catch (Exception e) {
+            log.error("faceIdNotify error", e);
+            faceVerify.setVerifyResult(FaceVerifyResult.VERIFY_FAILED);
+        }
+        long usedTime = System.currentTimeMillis() - faceVerify.getStartTime().getTime();
+        faceVerify.setUsedTime(usedTime);
+        return examFaceLivenessVerifyRepo.save(faceVerify);
+    }
+
+    @Override
+    public GetFaceVerifyTokenInfo getFaceVerifyToken(Long studentId, String bizNo) {
+        GetFaceVerifyTokenInfo getFaceVerifyTokenInfo = new GetFaceVerifyTokenInfo();
+        CloseableHttpResponse httpResponse = null;
+        CloseableHttpClient httpClient = null;
+        try {
+            httpClient = HttpPoolUtil.getHttpClient();
+            HttpPost httpPost = new HttpPost(PropertyHolder.getString("app.faceid.get_token_url"));
+            File basePhotoFile = getStudentBasePhotoFile(studentId);
+            MultipartEntityBuilder multipartEntityBuilder = getMultipartEntityBuilder(bizNo, basePhotoFile);
+            HttpEntity httpEntity = multipartEntityBuilder.build();
+            httpPost.setEntity(httpEntity);
+            httpResponse = httpClient.execute(httpPost);
+            HttpEntity responseEntity = httpResponse.getEntity();
+            BufferedReader reader = new BufferedReader(new InputStreamReader(responseEntity.getContent()));
+            StringBuffer buffer = new StringBuffer();
+            String str = "";
+            while (StringUtils.isNoneBlank((str = reader.readLine()))) {
+                buffer.append(str);
+            }
+            String result = buffer.toString();
+            basePhotoFile.delete();
+            JSONObject jsonObject = new JSONObject(result);
+            if (jsonObject.has("error_message")) {
+                getFaceVerifyTokenInfo.setSuccess(false);
+                getFaceVerifyTokenInfo.setErrorMsg(jsonObject.getString("error_message"));
+                log.error("getFaceVerifyToken error", result);
+            } else if (jsonObject.has("error")) {
+                getFaceVerifyTokenInfo.setSuccess(false);
+                getFaceVerifyTokenInfo.setErrorMsg(jsonObject.getString("error"));
+                log.error("getFaceVerifyToken error", result);
+            } else if (jsonObject.has("token")) {
+                getFaceVerifyTokenInfo.setSuccess(true);
+                getFaceVerifyTokenInfo.setFaceLivenessToken(jsonObject.getString("token"));
+            }
+            httpPost.abort();
+        } catch (Exception e) {
+            log.error("getFaceVerifyToken error", e);
+            getFaceVerifyTokenInfo.setSuccess(false);
+            getFaceVerifyTokenInfo.setErrorMsg(e.getMessage());
+        } finally {
+            if (httpResponse != null) {
+                try {
+                    httpResponse.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return getFaceVerifyTokenInfo;
+    }
+
+    @Override
+    public ExamFaceLivenessVerifyEntity findFaceVerifyById(Long id) {
+        if (id == null) {
+            return null;
+        }
+        return GlobalHelper.getEntity(examFaceLivenessVerifyRepo, id, ExamFaceLivenessVerifyEntity.class);
+    }
+
+    @Override
+    public void faceTestTimeOut(Long examRecordDataId) {
+        List<ExamFaceLivenessVerifyEntity> faceVerifies = examFaceLivenessVerifyRepo.findByExamRecordDataIdOrderById(examRecordDataId);
+        ExamFaceLivenessVerifyEntity faceVerify = faceVerifies.get(faceVerifies.size() - 1);
+        if (faceVerify.getVerifyResult() == null) {
+            faceVerify.setVerifyResult(FaceVerifyResult.TIME_OUT);
+            examFaceLivenessVerifyRepo.save(faceVerify);
+        }
+    }
+
+    @Override
+    public void faceTestEndHandle(Long examRecordDataId, Long studentId, String result) {
+        //保存活体检测的结果
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+        examRecordData.setFaceVerifyResult(IsSuccess.strToEnum(result) == IsSuccess.FAILED ? IsSuccess.FAILED : IsSuccess.SUCCESS);
+        examRecordDataService.saveExamRecordDataCache(examRecordDataId, examRecordData);
+
+        //如果活体检失败,需要清除会话并自动交卷
+        if (IsSuccess.strToEnum(result) == IsSuccess.FAILED) {
+            examControlService.handInExam(examRecordDataId, HandInExamType.AUTO);
+        }
+    }
+
+    @Override
+    public ExamFaceLivenessVerifyEntity saveFaceVerifyByExamRecordDataId(Long examRecordDataId) {
+        ExamFaceLivenessVerifyEntity examFaceVerifyEntity = examFaceLivenessVerifyRepo.findErrorFaceVerifyByExamRecordDataId(examRecordDataId);
+        if (examFaceVerifyEntity != null) {
+            examFaceVerifyEntity.setStartTime(new Date());
+            examFaceVerifyEntity.setIsError(false);
+            examFaceVerifyEntity.setErrorMsg(null);
+            examFaceVerifyEntity.setOperateNum(examFaceVerifyEntity.getOperateNum().intValue() + 1);
+            return examFaceLivenessVerifyRepo.save(examFaceVerifyEntity);
+        } else {
+            examFaceVerifyEntity = saveFaceVerify(examRecordDataId);
+        }
+        return examFaceVerifyEntity;
+    }
+
+    private MultipartEntityBuilder getMultipartEntityBuilder(String bizNo, File basePhotoFile) {
+        SysPropertyCacheBean sysProperty = CacheHelper.getSysProperty("app.faceid.notify_url");
+        if (!sysProperty.getHasValue()) {
+            throw new StatusException("200001", "未找到活体检测回调地址的配置信息");
+        }
+        String faceidNotifyUrl = sysProperty.getValue().toString();
+        //详见:https://faceid.com/pages/documents/5680502
+        MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
+        multipartEntityBuilder.addTextBody("api_key", PropertyHolder.getString("$facepp.faceid.api_key"));
+
+        multipartEntityBuilder.addTextBody("api_secret", PropertyHolder.getString("$facepp.faceid.api_secret"));
+        multipartEntityBuilder.addTextBody("comparison_type", "0");
+        multipartEntityBuilder.addTextBody("return_url", faceidNotifyUrl);
+        multipartEntityBuilder.addTextBody("notify_url", faceidNotifyUrl);
+        multipartEntityBuilder.addTextBody("biz_no", bizNo);
+        multipartEntityBuilder.addTextBody("uuid", bizNo);
+
+        multipartEntityBuilder.addBinaryBody("image_ref1", basePhotoFile);
+        return multipartEntityBuilder;
+    }
+
+    /**
+     * 获取学生底照文件
+     *
+     * @param studentId
+     * @return
+     */
+    private File getStudentBasePhotoFile(Long studentId) {
+        StudentCacheBean studentBean = CacheHelper.getStudent(studentId);
+        String photoUrl = studentBean.getPhotoPath();
+        String photoName = systemProperties.getDataDir() + "/" + photoUrl.substring(photoUrl.lastIndexOf("/") + 1);
+        photoName = PathUtil.getCanonicalPath(photoName);
+        FileDisposeUtil.saveUrlAs(photoUrl, photoName);
+        return new File(photoName);
+    }
+
+    @Override
+    public Integer getFaceLivenessVerifyMinute(Long rootOrgId, Long orgId, Long examId, Long studentId,
+                                               Long examRecordDataId, Integer heartbeat) {
+        //开启了人脸检测
+        if (FaceBiopsyHelper.isFaceVerify(rootOrgId, examId, studentId)) {
+
+            List<ExamFaceLivenessVerifyEntity> faceLivenessVerifys = listFaceVerifyByExamRecordId(examRecordDataId);
+            //如果没有进行过人脸检测
+            if (faceLivenessVerifys.size() == 0) {
+
+                String faceVerifyStartMinuteStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                        studentId, ExamProperties.FACE_VERIFY_START_MINUTE.name()).getValue();
+                Integer faceVerifyStartMinute = Integer.valueOf(faceVerifyStartMinuteStr);
+
+                String faceVerifyEndMinuteStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                        studentId, ExamProperties.FACE_VERIFY_END_MINUTE.name()).getValue();
+                Integer faceVerifyEndMinute = Integer.valueOf(faceVerifyEndMinuteStr);
+                //	case1.如果考生已使用的考试时间(即心跳时间)还未达到系统设置的活体检测开始时间,则实际活体检测时间=random(配置结束时间-配置结束时间)-考试已用时间
+                if (heartbeat < faceVerifyStartMinute) {
+                    return CommonUtil.calculationRandomNumber(faceVerifyStartMinute, faceVerifyEndMinute) - heartbeat;
+                }
+                //	case2如果配置开始时间<考生已使用的考试时间<配置结束时间,则实际活体检测时间=random(配置结束时间-考试已用时间)-考试已用时间,如果结果小于1分钟则默认1分钟
+                else if (heartbeat >= faceVerifyStartMinute && heartbeat < faceVerifyEndMinute) {
+                    int verifyTime = CommonUtil.calculationRandomNumber(heartbeat, faceVerifyEndMinute) - heartbeat;
+                    return verifyTime < 1 ? 1 : verifyTime;
+                }
+                //case3如果考试已用时间>配置结束时间,则默认random(1,4)分钟后开始人脸检测
+                else if (heartbeat >= faceVerifyEndMinute) {
+                    return CommonUtil.calculationRandomNumber(1, secondFaceCheckMinute);
+                }
+            } else if (faceLivenessVerifys.size() == 1) {
+                //如果已经人脸检测过一次且未成功,再安排一次检测
+                ExamFaceLivenessVerifyEntity faceVerify = faceLivenessVerifys.get(0);
+                if (faceVerify.getVerifyResult() == null
+                        || faceVerify.getVerifyResult() != FaceVerifyResult.VERIFY_SUCCESS) {
+                    return CommonUtil.calculationRandomNumber(1, secondFaceCheckMinute);
+                }
+            }
+        }
+
+        return null;
+    }
+}

+ 851 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/FaceBiopsyServiceImpl.java

@@ -0,0 +1,851 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.core.oe.student.base.utils.CommonUtil;
+import cn.com.qmth.examcloud.core.oe.student.bean.FaceBiopsyInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.FaceBiopsyStepInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.SaveFaceBiopsyResultReq;
+import cn.com.qmth.examcloud.core.oe.student.bean.SaveFaceBiopsyResultResp;
+import cn.com.qmth.examcloud.core.oe.student.dao.FaceBiopsyItemRepo;
+import cn.com.qmth.examcloud.core.oe.student.dao.FaceBiopsyItemStepRepo;
+import cn.com.qmth.examcloud.core.oe.student.dao.FaceBiopsyRepo;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.FaceBiopsyEntity;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.FaceBiopsyItemEntity;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.FaceBiopsyItemStepEntity;
+import cn.com.qmth.examcloud.core.oe.student.dao.enums.FaceBiopsyAction;
+import cn.com.qmth.examcloud.core.oe.student.dao.enums.FaceBiopsyType;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamControlService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamingSessionService;
+import cn.com.qmth.examcloud.core.oe.student.service.FaceBiopsyService;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.enums.ExamProperties;
+import cn.com.qmth.examcloud.support.enums.IsSuccess;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+import cn.com.qmth.examcloud.support.helper.ExamCacheTransferHelper;
+import cn.com.qmth.examcloud.support.helper.FaceBiopsyHelper;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+import com.google.common.collect.Lists;
+import com.mysql.cj.util.StringUtils;
+import org.apache.commons.lang3.RandomUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @Description 人脸活体检测接口实现类
+ * @Author lideyin
+ * @Date 2019/10/14 10:56
+ * @Version 1.0
+ */
+@Service("faceBiopsyService")
+public class FaceBiopsyServiceImpl implements FaceBiopsyService {
+
+    @Autowired
+    private FaceBiopsyRepo faceBiopsyRepo;
+    @Autowired
+    private FaceBiopsyItemRepo faceBiopsyItemRepo;
+    @Autowired
+    private FaceBiopsyItemStepRepo faceBiopsyItemStepRepo;
+    @Autowired
+    private ExamControlService examControlService;
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+    @Autowired
+    ExamingSessionService examingSessionService;
+
+    @Override
+    @Transactional
+    public FaceBiopsyInfo getFaceBiopsyInfo(Long rootOrgId, Long examRecordDataId, FaceBiopsyType faceBiopsyType) {
+        //如果是第一次进行人脸活体检测,则初始化相关信息保存并返回
+        FaceBiopsyEntity faceBiopsyEntity = faceBiopsyRepo.findByExamRecordDataId(examRecordDataId);
+        if (faceBiopsyEntity == null) {
+            return addFirstFaceBiopsy(rootOrgId, examRecordDataId, faceBiopsyType);
+        }
+
+        //如果不是第一次人脸活体检测,判断是否有未检测完的记录,
+        FaceBiopsyInfo result = new FaceBiopsyInfo();
+        // 未完成的活体检测列表
+        List<FaceBiopsyItemEntity> unCompletedFaceBiopsyItemList =
+                faceBiopsyItemRepo.findByExamRecordDataIdAndCompleted(examRecordDataId, false);
+
+        //如果存在未完成的活体检测信息,则直接返回上次未完成的活检记录
+        if (unCompletedFaceBiopsyItemList != null && !unCompletedFaceBiopsyItemList.isEmpty()) {
+            FaceBiopsyItemEntity faceBiopsyItemEntity = unCompletedFaceBiopsyItemList.get(0);
+            result.setFaceBiopsyItemId(faceBiopsyItemEntity.getId());
+            result.setFaceVerifyMinute(calculateFaceBiopsyStartMinute(examRecordDataId));
+
+            List<FaceBiopsyItemStepEntity> faceBiopsyItemStepEntityList =
+                    faceBiopsyItemStepRepo.findByFaceBiopsyItemId(faceBiopsyItemEntity.getId());
+            result.setVerifySteps(copyFaceBiopsyStepDomainListFrom(faceBiopsyItemStepEntityList));
+            return result;
+        }
+
+        //不存在未完成的活检的校验逻辑
+        // 已活检次数
+        int verifiedTimes = faceBiopsyEntity.getVerifiedTimes();
+
+        if (verifiedTimes == 1) {
+            FaceBiopsyItemEntity firstFaceBiopsyItem =
+                    faceBiopsyItemRepo.findFirstByFaceBiopsyIdOrderByIdAsc(faceBiopsyEntity.getId());
+            if (null != firstFaceBiopsyItem.getResult() && firstFaceBiopsyItem.getResult()) {
+                //第一次活检成功,且未开启追加新活检,再次调用,则抛出异常
+                if (!isAddFaceVerifyOutFreezeTime(examRecordDataId)) {
+                    throw new StatusException("201006", "非法请求,无活体检测机会");
+                }
+                //第一次活检成功,且开启追加新活检,再次调用,按冻结时间外生成新的活检步骤
+                return appendFaceBiopsy(faceBiopsyEntity.getId(), verifiedTimes, examRecordDataId, faceBiopsyType, false);
+            }
+
+            //如果第一次活检失败,再次调用,按冻结时间内生成新的活检步骤
+            return appendFaceBiopsy(faceBiopsyEntity.getId(), verifiedTimes, examRecordDataId, faceBiopsyType, true);
+        }
+
+        if (verifiedTimes == 2) {
+            FaceBiopsyItemEntity secondFaceBiopsyItem =
+                    faceBiopsyItemRepo.findFirstByFaceBiopsyIdOrderByIdDesc(faceBiopsyEntity.getId());
+
+            //如果是第二次活检为冻结时间外的活检记录,再次调用,则直接抛出异常
+            if (!secondFaceBiopsyItem.getInFreezeTime()) {
+                throw new StatusException("201007", "非法请求,无活体检测机会");
+            }
+
+            //如果第二次活检也失败,再次调用,则抛出异常
+            if (!secondFaceBiopsyItem.getResult()) {
+                throw new StatusException("201008", "非法请求,无活体检测机会");
+            }
+
+            //否则追加冻结时间外的活检
+            return appendFaceBiopsy(faceBiopsyEntity.getId(), verifiedTimes, examRecordDataId, faceBiopsyType, false);
+        }
+
+        //三交活检已完成,则抛出异常
+        throw new StatusException("201009", "非法请求,无活体检测机会");
+    }
+
+    @Override
+    @Transactional
+    public SaveFaceBiopsyResultResp saveFaceBiopsyResult(SaveFaceBiopsyResultReq req, Long studentId) {
+        // 检测结果至少有一条检测结果不允许为空
+        if (!req.getVerifySteps().stream().anyMatch(p -> null != p.getResult())) {
+            throw new StatusException("201005", "检测结果不允许为空");
+        }
+
+        //构建业务实体
+        SaveFaceBiopsyResultResp resp = buildSaveFaceBiopsyResultResp(req);
+
+        //更新人脸活体检测结果至数据库
+        updateFaceBiopsyResult(req.getExamRecordDataId(), req.getFaceBiopsyItemId(), req.getVerifySteps(),
+                resp.getVerifyResult(), resp.getErrorMessage());
+
+        //同步更新考试记录表中的活体检测结果
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(req.getExamRecordDataId());
+        if (examRecordData == null) {
+            throw new StatusException("201010", "找不到相关考试记录数据");
+        }
+        examRecordData.setFaceVerifyResult(resp.getVerifyResult() ? IsSuccess.SUCCESS : IsSuccess.FAILED);
+        examRecordDataService.saveExamRecordDataCache(req.getExamRecordDataId(), examRecordData);
+
+        return resp;
+    }
+
+    /**
+     * 计算人脸活体检测开始分钟数
+     *
+     * @param examRecordDataId 考试记录id
+     * @return Integer
+     */
+    @Override
+    public Integer calculateFaceBiopsyStartMinute(Long examRecordDataId) {
+        //如果未开启人脸活体检测,则返回null
+        if (!getIsFaceVerify(examRecordDataId)) {
+            return null;
+        }
+
+        /**
+         * case0.首次调用或超过3次调用的特殊处理
+         * case0.1.第一次调用,无冻结时间内还是冻结时间外,均按照冻结时间内的算法计算下次活检时间(相当于补偿措施)
+         * case0.2.超过3次调用,
+         * case0.2.1.如果第三次活检未完成,再次调用,则按活检外计算方式,返回下次活检时间
+         * case0.2.2.如果第三次活检已完成,则直接返回null
+         */
+        FaceBiopsyEntity faceBiopsy = faceBiopsyRepo.findByExamRecordDataId(examRecordDataId);
+        if (faceBiopsy == null) {
+            return generateInFreezeTimeFaceBiopsyStartMinute(examRecordDataId);
+        }
+
+        //超过三次的特殊处理
+        if (faceBiopsy.getVerifiedTimes() >= 3) {
+            FaceBiopsyItemEntity lastFaceBiopsyItem =
+                    faceBiopsyItemRepo.findFirstByFaceBiopsyIdOrderByIdDesc(faceBiopsy.getId());
+
+            //如果第三次活检已完成,则直接返回null
+            if (lastFaceBiopsyItem.getCompleted()) {
+                return null;
+            }
+
+            //如果第三次活检未完成,再次调用,则按活检外计算方式,返回下次活检时间
+            return generateOutFreezeTimeFaceBiopsyStartMinute(examRecordDataId);
+        }
+
+        //按id升序排列的活检集合
+        List<FaceBiopsyItemEntity> sortedFaceBiopsyItems =
+                faceBiopsyItemRepo.findByFaceBiopsyIdOrderByIdAsc(faceBiopsy.getId());
+
+        //第一次活检明细
+        FaceBiopsyItemEntity firstFaceBiopsyItem = sortedFaceBiopsyItems.get(0);
+
+        //第一次活检未完成,再次调用,继续按冻结时间内方式计算活检开始时间
+        if (!firstFaceBiopsyItem.getCompleted()) {
+            return generateInFreezeTimeFaceBiopsyStartMinute(examRecordDataId);
+        }
+
+        //第一次活体检测完成且成功,再次调用时根据是否开启追加活检判断相关逻辑
+        if (firstFaceBiopsyItem.getResult()) {
+            //如果未开启冻结时间外人脸活体检测,则无需求下次活检,故直接返回null
+            if (!isAddFaceVerifyOutFreezeTime(examRecordDataId)) {
+                return null;
+            }
+
+            //如果开启冻结时间外人脸活体检测,则直接返回冻结时间外的活检开始时间
+            return generateOutFreezeTimeFaceBiopsyStartMinute(examRecordDataId);
+        }
+
+        //第一次活检完成且失败,那么第二次调用或者第N(N>=2)次调用且第二次活检未完成,采用活检内计算方式
+        if (faceBiopsy.getVerifiedTimes() == 1 ||
+                (faceBiopsy.getVerifiedTimes() == 2 && !sortedFaceBiopsyItems.get(1).getCompleted())) {
+            return generateInFreezeTimeFaceBiopsyStartMinute(examRecordDataId);
+        }
+
+        //其它情况,采用冻结时间外活检计算方式
+        return generateOutFreezeTimeFaceBiopsyStartMinute(examRecordDataId);
+    }
+
+    /**
+     * 构建保存人脸检测结果的业务实体
+     *
+     * @param req
+     * @return
+     */
+    private SaveFaceBiopsyResultResp buildSaveFaceBiopsyResultResp(SaveFaceBiopsyResultReq req) {
+        //构建业务实体
+        SaveFaceBiopsyResultResp resp = new SaveFaceBiopsyResultResp();
+
+        Boolean finalIsSuccess = calculateIsSuccess(req.getVerifySteps());
+        resp.setVerifyResult(finalIsSuccess);
+
+        Boolean isEndExam = calculateIsEndExam(req.getVerifySteps(), req.getExamRecordDataId(), finalIsSuccess);
+        resp.setEndExam(isEndExam);
+
+        resp.setNeedNextVerify(calculateNeedNextVerify(isEndExam, finalIsSuccess, req.getExamRecordDataId()));
+        resp.setErrorMessage(getErrorMsg(req.getVerifySteps()));
+        return resp;
+    }
+
+    /**
+     * 更新人脸活体检测结果至数据库
+     *
+     * @param examRecordDataId
+     * @param faceBiopsyItemId
+     * @param verifySteps
+     * @param finalIsSuccess
+     * @param errorMsg
+     */
+    public void updateFaceBiopsyResult(Long examRecordDataId, Long
+            faceBiopsyItemId, List<FaceBiopsyStepInfo> verifySteps,
+                                       boolean finalIsSuccess, String errorMsg) {
+        List<FaceBiopsyItemStepEntity> faceBiopsyItemStepEntityList =
+                faceBiopsyItemStepRepo.findByFaceBiopsyItemId(faceBiopsyItemId);
+        for (FaceBiopsyStepInfo stepInfo : verifySteps) {
+            //循环更新新活体检测每步的执行结果
+            FaceBiopsyItemStepEntity faceBiopsyItemStepEntity = faceBiopsyItemStepEntityList.stream().
+                    filter(p -> p.getId().equals(stepInfo.getStepId())).findFirst().get();
+            faceBiopsyItemStepEntity.setErrorMsg(stepInfo.getErrorMsg());
+            faceBiopsyItemStepEntity.setResourceType(stepInfo.getResourceType());
+            faceBiopsyItemStepEntity.setResourceRelativePath(getRelativePath(stepInfo.getResourceUrl()));
+            faceBiopsyItemStepEntity.setExt1(stepInfo.getResultJson());//存储动作执行结果
+            faceBiopsyItemStepEntity.setResult(stepInfo.getResult());
+
+            if (null != stepInfo.getTimeout()) {
+                faceBiopsyItemStepEntity.setExt2(stepInfo.getTimeout().toString());//指令是否超时
+            }
+
+            if (null != stepInfo.getStranger()) {
+                faceBiopsyItemStepEntity.setExt3(stepInfo.getStranger().toString());//是否有陌生人脸
+            }
+
+            if (null != stepInfo.getWaggleOutCamera()) {
+                faceBiopsyItemStepEntity.setExt3(stepInfo.getWaggleOutCamera().toString());//是否晃同摄像头
+            }
+
+            if (null != stepInfo.getHasFace()) {
+                faceBiopsyItemStepEntity.setExt4(stepInfo.getHasFace().toString());//是否有人脸
+            }
+
+        }
+        //批量更新活检步骤执行结果
+        faceBiopsyItemStepRepo.saveAll(faceBiopsyItemStepEntityList);
+        //更新人脸活体检测最终结果
+        faceBiopsyItemRepo.updateFaceBiopsyItemResult(faceBiopsyItemId, finalIsSuccess, errorMsg, true);
+        faceBiopsyRepo.updateFaceBiopsyResult(examRecordDataId, finalIsSuccess, errorMsg);
+    }
+
+
+    /**
+     * 计算活检是否成功
+     *
+     * @param verifySteps
+     * @return
+     */
+    private boolean calculateIsSuccess(List<FaceBiopsyStepInfo> verifySteps) {
+        for (FaceBiopsyStepInfo stepInfo : verifySteps) {
+            //如果当前步骤为超时,则直接返回失败或者整个检测步骤中只要有一次失败,则认为整个活检失败
+            if ((null != stepInfo.getTimeout() && stepInfo.getTimeout()) ||
+                    (null != stepInfo.getResult() && !stepInfo.getResult())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 获取错误信息
+     *
+     * @param verifySteps
+     * @return
+     */
+    private String getErrorMsg(List<FaceBiopsyStepInfo> verifySteps) {
+        for (FaceBiopsyStepInfo stepInfo : verifySteps) {
+            //如果当前步骤为超时,则直接跳出循环
+            if ((stepInfo.getTimeout() != null && stepInfo.getTimeout())) {
+                return "超时未完成";
+            }
+
+            //整个检测步骤中只要有一次失败,则认为整个活检失败
+            if (null != stepInfo.getResult() && !stepInfo.getResult()) {
+                return stepInfo.getErrorMsg();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 计算是否结束考试
+     *
+     * @param verifySteps
+     * @param examRecordDataId
+     * @param finalIsSuccess
+     * @return
+     */
+    private boolean calculateIsEndExam(List<FaceBiopsyStepInfo> verifySteps, Long examRecordDataId, Boolean finalIsSuccess) {
+        /**
+         * 指定指令失败,(多人脸,人脸晃出摄像头,无人脸)直接结束考试
+         */
+        for (FaceBiopsyStepInfo stepInfo : verifySteps) {
+            switch (stepInfo.getAction()) {
+                case FACE_COMPARE:
+                    //如果有陌生人脸或晃出摄像头或无人脸,直接结束考试
+                    if ((null != stepInfo.getStranger() && stepInfo.getStranger()) ||
+                            (null != stepInfo.getWaggleOutCamera() && stepInfo.getWaggleOutCamera()) ||
+                            (null != stepInfo.getHasFace() && !stepInfo.getHasFace())) {
+                        return true;
+                    }
+
+                    //如果第一步检测部分失败(照片非本人或检测中多人脸),需要结束考试
+                    Map faceCompareResult = JsonUtil.fromJson(stepInfo.getResultJson(), Map.class);
+                    if (null != faceCompareResult) {
+                        if (null == faceCompareResult.get("isStranger") || null == faceCompareResult.get("isPass") ||
+                                null == faceCompareResult.get("existsSystemError")) {
+                            throw new StatusException("201007", "活体检测第一步检测结果的json串格式不正确");
+                        }
+
+                        Boolean isStranger;
+                        if (null == faceCompareResult.get("isStranger")) {
+                            isStranger = false;
+                        } else {
+                            isStranger = Boolean.valueOf(faceCompareResult.get("isStranger").toString());
+                        }
+
+                        Boolean isPass = Boolean.valueOf(faceCompareResult.get("isPass").toString());
+                        Boolean existsSystemError = Boolean.valueOf(faceCompareResult.get("existsSystemError").toString());
+
+                        //如果有系统错误,也需要抛出异常
+                        if (existsSystemError) {
+                            throw new StatusException("201008", "活体检测失败,请稍候重试");
+                        }
+                        //case1.1.有陌生人(即多人脸),不管是否比对成功,直接结束考试
+                        if (isStranger) {
+                            return true;
+                        }
+                        //case1.2.无陌生人且检测失败且不是系统错误(即照片非本人或无人脸),也直接结束考试
+                        else {
+                            if (!isPass && !existsSystemError) {
+                                return true;
+                            }
+                        }
+                    }
+                    break;
+                case HAPPY:
+                case SERIOUS:
+                    //如果有陌生人脸或晃出摄像头或无人脸,直接结束考试
+                    if ((null != stepInfo.getStranger() && stepInfo.getStranger()) ||
+                            (null != stepInfo.getWaggleOutCamera() && stepInfo.getWaggleOutCamera()) ||
+                            (null != stepInfo.getHasFace() && !stepInfo.getHasFace())) {
+                        return true;
+                    }
+                    break;
+            }
+        }
+
+        /**
+         * 根据活检情况结束考试
+         * 当前为第二次活检,且失败,且第一次活检也失败,则直接结束考试
+         * 当前为第二次活检,且失败,且第一次活检成功,则直接结束考试
+         * 当前为第三次活检,且失败,且第一次活检失败,第二次活检成功,则直接结束考试
+         */
+        //如果是第二次活检,且失败,则结束考试(无论第一次是否成功)
+        int verifyTimes = getVerifyTimes(examRecordDataId);
+        if (verifyTimes == 2 && !finalIsSuccess) {
+            return true;
+        }
+
+        if (verifyTimes == 3 && !finalIsSuccess) {
+            return true;
+        }
+
+        //其它情况不结束考试
+        return false;
+    }
+
+    /**
+     * 计算是否需要下次活检
+     *
+     * @param isEndExam
+     * @param finalIsSuccess
+     * @return
+     */
+    private boolean calculateNeedNextVerify(boolean isEndExam, boolean finalIsSuccess, Long examRecordDataId) {
+        //考试已结束,不需要继续下次活检
+        if (isEndExam) {
+            return false;
+        }
+
+        int verifyTimes = getVerifyTimes(examRecordDataId);
+        //如果是第一次活检,且成功,且开启追加活检,则允许继续活检
+        //或者是第一次活检,且失败,则允许继续活检
+        if (verifyTimes == 1) {
+            if ((finalIsSuccess && isAddFaceVerifyOutFreezeTime(examRecordDataId)) ||
+                    !finalIsSuccess) {
+                return true;
+            }
+        }
+
+        //如果是第二次活检,且成功,且第一次活检失败,且开启追加活检,则允许继续活检
+        if (verifyTimes == 2 && finalIsSuccess) {
+            FaceBiopsyItemEntity firstFaceBiopsyItem =
+                    faceBiopsyItemRepo.findFirstByExamRecordDataIdOrderByIdAsc(examRecordDataId);
+            if (!firstFaceBiopsyItem.getResult() && isAddFaceVerifyOutFreezeTime(examRecordDataId)) {
+                return true;
+            }
+        }
+
+        //其它情况均不允许继续活检
+        return false;
+    }
+
+    /**
+     * 获取活体检测次数
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    private int getVerifyTimes(Long examRecordDataId) {
+        FaceBiopsyEntity faceBiopsy = faceBiopsyRepo.findByExamRecordDataId(examRecordDataId);
+        return faceBiopsy.getVerifiedTimes();
+    }
+
+    /**
+     * 根据完整路径获取相对路径
+     *
+     * @param resourceUrl
+     * @return
+     */
+    private String getRelativePath(String resourceUrl) {
+        if (!StringUtils.isNullOrEmpty(resourceUrl)) {
+            String domain = PropertyHolder.getString("$upyun.site.1.domain");
+            String backupDomain = PropertyHolder.getString("$upyun.site.1.domain.backup");
+            if (!StringUtils.isNullOrEmpty(domain)) {
+                resourceUrl = resourceUrl.replace(domain, "");
+            }
+            if (!StringUtils.isNullOrEmpty(backupDomain)) {
+                resourceUrl = resourceUrl.replace(backupDomain, "");
+            }
+        }
+        return resourceUrl;
+    }
+
+    /**
+     * 第一次添加人脸活体检测结果
+     *
+     * @param rootOrgId
+     * @param examRecordDataId
+     * @param faceBiopsyType
+     * @return
+     */
+    public FaceBiopsyInfo addFirstFaceBiopsy(Long rootOrgId, Long examRecordDataId, FaceBiopsyType faceBiopsyType) {
+        //保存人脸活体检测相关信息
+        Long faceBiopsyId = addFaceBiopsyEntity(rootOrgId, examRecordDataId);
+        Long faceBiopsyItemId = addFaceBiopsyItemEntity(examRecordDataId, faceBiopsyType, faceBiopsyId, true);
+        List<FaceBiopsyItemStepEntity> faceBiopsyItemStepEntityList =
+                addFaceBiopsyItemStepList(examRecordDataId, faceBiopsyItemId);
+
+        //构建业务实体
+        return buildFaceBiopsyInfo(examRecordDataId, faceBiopsyItemId, faceBiopsyItemStepEntityList);
+    }
+
+    /**
+     * 追加人脸活体检测结果
+     *
+     * @param faceBiopsyId
+     * @param verifiedTimes
+     * @param examRecordDataId
+     * @param faceBiopsyType
+     * @return
+     */
+    @Transactional
+    public FaceBiopsyInfo appendFaceBiopsy(Long faceBiopsyId, Integer verifiedTimes, Long examRecordDataId,
+                                           FaceBiopsyType faceBiopsyType, boolean isInFreezeTime) {
+        verifiedTimes++;
+        //更新人脸活体检测次数
+        FaceBiopsyEntity faceBiopsyEntity = GlobalHelper.getEntity(faceBiopsyRepo, faceBiopsyId, FaceBiopsyEntity.class);
+        faceBiopsyEntity.setVerifiedTimes(verifiedTimes);
+        faceBiopsyRepo.save(faceBiopsyEntity);
+        //添加人脸活体检测明细
+        Long faceBiopsyItemId = addFaceBiopsyItemEntity(examRecordDataId, faceBiopsyType, faceBiopsyId, isInFreezeTime);
+        //添加人脸活体检测步骤
+        List<FaceBiopsyItemStepEntity> faceBiopsyItemStepEntityList =
+                addFaceBiopsyItemStepList(examRecordDataId, faceBiopsyItemId);
+
+        return buildFaceBiopsyInfo(examRecordDataId, faceBiopsyItemId, faceBiopsyItemStepEntityList);
+
+    }
+
+    /**
+     * 构建人脸检测基本信息的业务实体对象
+     *
+     * @param examRecordDataId
+     * @param faceBiopsyItemId
+     * @param faceBiopsyItemStepEntityList
+     * @return
+     */
+    private FaceBiopsyInfo buildFaceBiopsyInfo(Long examRecordDataId, Long faceBiopsyItemId,
+                                               List<FaceBiopsyItemStepEntity> faceBiopsyItemStepEntityList) {
+        //构建业务实体
+        FaceBiopsyInfo faceBiopsyInfo = new FaceBiopsyInfo();
+        faceBiopsyInfo.setFaceBiopsyItemId(faceBiopsyItemId);
+        faceBiopsyInfo.setFaceVerifyMinute(calculateFaceBiopsyStartMinute(examRecordDataId));
+        faceBiopsyInfo.setVerifySteps(copyFaceBiopsyStepDomainListFrom(faceBiopsyItemStepEntityList));
+        return faceBiopsyInfo;
+    }
+
+    private List<FaceBiopsyStepInfo> copyFaceBiopsyStepDomainListFrom
+            (List<FaceBiopsyItemStepEntity> faceBiopsyItemStepEntityList) {
+        List<FaceBiopsyStepInfo> resultList = new ArrayList<>();
+        for (FaceBiopsyItemStepEntity entity : faceBiopsyItemStepEntityList) {
+            FaceBiopsyStepInfo domain = new FaceBiopsyStepInfo();
+            domain.setAction(entity.getAction());
+            domain.setStay(entity.getActionStay());
+            domain.setStepId(entity.getId());
+            resultList.add(domain);
+        }
+        return resultList;
+    }
+
+    /**
+     * 添加活体检测的具体步骤
+     *
+     * @param examRecordDataId 考试记录id
+     * @param faceBiopsyItemId 人脸活体检测明细id
+     */
+    private List<FaceBiopsyItemStepEntity> addFaceBiopsyItemStepList(Long examRecordDataId, Long faceBiopsyItemId) {
+        List<FaceBiopsyItemStepEntity> faceBiopsyItemStepEntityList = new ArrayList<FaceBiopsyItemStepEntity>();
+
+        //step1.人脸比对
+        FaceBiopsyItemStepEntity firstStep = new FaceBiopsyItemStepEntity();
+        firstStep.setExamRecordDataId(examRecordDataId);
+        firstStep.setFaceBiopsyItemId(faceBiopsyItemId);
+        firstStep.setAction(FaceBiopsyAction.FACE_COMPARE);
+        faceBiopsyItemStepEntityList.add(firstStep);
+
+        //生成乱序表情集合
+        List<FaceBiopsyAction> shuffledExpressionList = generateShuffleExpressions();
+
+        //step2随机表情
+        faceBiopsyItemStepEntityList.add(
+                randomGenerateExpressionStep(examRecordDataId, faceBiopsyItemId, shuffledExpressionList));
+
+        //step3随机表情
+        faceBiopsyItemStepEntityList.add(
+                randomGenerateExpressionStep(examRecordDataId, faceBiopsyItemId, shuffledExpressionList));
+
+        faceBiopsyItemStepRepo.saveAll(faceBiopsyItemStepEntityList);
+        return faceBiopsyItemStepEntityList;
+    }
+
+    /**
+     * 乱序生成人脸表情
+     *
+     * @return
+     */
+    private List<FaceBiopsyAction> generateShuffleExpressions() {
+        List<FaceBiopsyAction> expressionList = Lists.newArrayList();
+        expressionList.add(FaceBiopsyAction.HAPPY);
+        expressionList.add(FaceBiopsyAction.SERIOUS);
+        Collections.shuffle(expressionList);
+        return expressionList;
+    }
+
+    /**
+     * 随机生成表情相关步骤
+     *
+     * @param examRecordDataId
+     * @param faceBiopsyItemId
+     * @return
+     */
+    private FaceBiopsyItemStepEntity randomGenerateExpressionStep(Long examRecordDataId,
+                                                                  Long faceBiopsyItemId,
+                                                                  List<FaceBiopsyAction> shuffledExpressionList) {
+        FaceBiopsyItemStepEntity secondStep = new FaceBiopsyItemStepEntity();
+        secondStep.setExamRecordDataId(examRecordDataId);
+        secondStep.setFaceBiopsyItemId(faceBiopsyItemId);
+        secondStep.setAction(takeShuffledExpression(shuffledExpressionList));//获取一条随机生成表情指令
+        secondStep.setActionStay(randomGenerateActionStay());//随机生成指令时长(3至6秒)
+        return secondStep;
+    }
+
+    /**
+     * 获取一条随机生成的指令
+     *
+     * @param shuffledExpressionList
+     * @return
+     */
+    private FaceBiopsyAction takeShuffledExpression(List<FaceBiopsyAction> shuffledExpressionList) {
+        //取完第一条并删除
+        FaceBiopsyAction expression = shuffledExpressionList.get(0);
+        shuffledExpressionList.remove(0);
+        return expression;
+    }
+
+
+    /**
+     * 随机生成指令持续的时长
+     *
+     * @return
+     */
+    private Integer randomGenerateActionStay() {
+        int minStay = PropertyHolder.getInt("oe.faceBiopsy.minActionStay", 3);
+        int maxStay = PropertyHolder.getInt("oe.faceBiopsy.maxActionStay", 6);
+        return RandomUtils.nextInt(minStay, maxStay);
+    }
+
+    /**
+     * 添加人脸活体检测明细
+     *
+     * @param examRecordDataId 考试记录id
+     * @param faceBiopsyType   人脸活体检测类型
+     * @param faceBiopsyId     人脸活体检测id
+     * @return Long 人脸活体检测明细id
+     */
+    private Long addFaceBiopsyItemEntity(Long examRecordDataId, FaceBiopsyType faceBiopsyType,
+                                         Long faceBiopsyId, boolean isInFreezeTime) {
+        FaceBiopsyItemEntity faceBiopsyItemEntity = new FaceBiopsyItemEntity();
+        faceBiopsyItemEntity.setExamRecordDataId(examRecordDataId);
+        faceBiopsyItemEntity.setFaceBiopsyId(faceBiopsyId);
+        faceBiopsyItemEntity.setFaceBiopsyType(faceBiopsyType);
+        faceBiopsyItemEntity.setCompleted(false);
+        faceBiopsyItemEntity.setInFreezeTime(isInFreezeTime);
+        faceBiopsyItemRepo.save(faceBiopsyItemEntity);
+        return faceBiopsyItemEntity.getId();
+    }
+
+    /**
+     * 添加人脸活体检测结果
+     *
+     * @param rootOrgId        学校id
+     * @param examRecordDataId 考试记录id
+     * @return Long 人脸活体检测结果id
+     */
+    private Long addFaceBiopsyEntity(Long rootOrgId, Long examRecordDataId) {
+        FaceBiopsyEntity faceBiopsyEntity = new FaceBiopsyEntity();
+        faceBiopsyEntity.setRootOrgId(rootOrgId);
+        faceBiopsyEntity.setExamRecordDataId(examRecordDataId);
+        faceBiopsyEntity.setVerifiedTimes(1);
+        faceBiopsyRepo.save(faceBiopsyEntity);
+        return faceBiopsyEntity.getId();
+    }
+
+
+    /**
+     * 获取冻结时间
+     *
+     * @param studentId
+     * @return
+     */
+    private Integer getFreezeTime(Long studentId) {
+        ExamingSession examSessionInfo = examingSessionService.getExamingSession(studentId);
+        if (examSessionInfo == null) {
+            throw new StatusException("201002", "考试会话已过期");
+        }
+
+        Integer freezeTime = examSessionInfo.getFreezeTime();
+        return freezeTime;
+    }
+
+    /**
+     * 冻结时间内生成人脸活体检测的开始时间
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    private Integer generateInFreezeTimeFaceBiopsyStartMinute(Long examRecordDataId) {
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+        Long examId = examRecordData.getExamId();
+        Long studentId = examRecordData.getStudentId();
+
+        //如果一次接口都没调用过,则默认活检次数为1,否则从库中取出已活检次数
+        Integer verifyTimes = 1;
+        FaceBiopsyEntity faceBiopsyEntity = faceBiopsyRepo.findByExamRecordDataId(examRecordDataId);
+        if (faceBiopsyEntity != null) {
+            verifyTimes = faceBiopsyEntity.getVerifiedTimes();
+        }
+
+        //默认二次活检
+        int minSecondFaceCheckMinute = PropertyHolder.getInt("oe.faceBiopsy.minSecondFaceCheckMinute", 1);
+        int maxSecondFaceCheckMinute = PropertyHolder.getInt("oe.faceBiopsy.maxSecondFaceCheckMinute", 3);
+
+        //如果是第一次活体检测
+        if (verifyTimes == 1) {
+            String faceVerifyStartMinuteStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                    studentId, ExamProperties.FACE_VERIFY_START_MINUTE.name()).getValue();
+            Integer faceVerifyStartMinute = Integer.valueOf(faceVerifyStartMinuteStr);
+
+            String faceVerifyEndMinuteStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                    studentId, ExamProperties.FACE_VERIFY_END_MINUTE.name()).getValue();
+            Integer faceVerifyEndMinute = Integer.valueOf(faceVerifyEndMinuteStr);
+
+            Integer examUsedMinutes = getExamUsedMinutes(studentId);
+
+            //	case1.如果考生已使用的考试时间(即心跳时间)还未达到系统设置的活体检测开始时间,
+            //	则实际活体检测时间=random(配置结束时间,配置开始时间)-考试已用时间
+            if (examUsedMinutes < faceVerifyStartMinute) {
+                return CommonUtil.calculationRandomNumber(faceVerifyStartMinute, faceVerifyEndMinute) - examUsedMinutes;
+            }
+            //	case2如果配置开始时间<考生已使用的考试时间<配置结束时间,
+            //	则实际活体检测时间=random(配置结束时间,考试已用时间)-考试已用时间,如果结果小于1分钟则默认1分钟
+            else if (examUsedMinutes >= faceVerifyStartMinute && examUsedMinutes < faceVerifyEndMinute) {
+                int startMinute = CommonUtil.calculationRandomNumber(examUsedMinutes, faceVerifyEndMinute) - examUsedMinutes;
+                return startMinute < 1 ? 1 : startMinute;
+            }
+            //case3如果考试已用时间>配置结束时间,则默认random(1,4)分钟后开始人脸检测
+            else {
+                return CommonUtil.calculationRandomNumber(minSecondFaceCheckMinute, maxSecondFaceCheckMinute);
+            }
+        }
+        //如果是第二次人脸
+        else {
+            return CommonUtil.calculationRandomNumber(minSecondFaceCheckMinute, maxSecondFaceCheckMinute);
+        }
+    }
+
+    /**
+     * 冻结时间外生成人脸活体检测的开始时间
+     *
+     * @return
+     */
+    private Integer generateOutFreezeTimeFaceBiopsyStartMinute(Long examRecordDataId) {
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+        Long examId = examRecordData.getExamId();
+        Long orgId = examRecordData.getOrgId();
+        Long studentId = examRecordData.getStudentId();
+
+        //如果冻结时间外不添加活体检测,则直接返回null
+        if (!isAddFaceVerifyOutFreezeTime(examId, orgId, studentId)) {
+            return null;
+        }
+
+        String strOutFreezeTimeFaceVerifyStartMinute = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.OUT_FREEZE_TIME_FACE_VERIFY_START_MINUTE.name()).getValue();
+        Integer faceVerifyStartMinute = Integer.valueOf(strOutFreezeTimeFaceVerifyStartMinute);
+
+        String strOutFreezeTimeFaceVerifyEndMinute = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.OUT_FREEZE_TIME_FACE_VERIFY_END_MINUTE.name()).getValue();
+        Integer faceVerifyEndMinute = Integer.valueOf(strOutFreezeTimeFaceVerifyEndMinute);
+
+        //活检开始时间 = (冻结时间 + random(配置开始时间,配置结束时间)) -  考试已用时间
+        int result = getFreezeTime(studentId) +
+                CommonUtil.calculationRandomNumber(faceVerifyStartMinute, faceVerifyEndMinute) - getExamUsedMinutes(studentId);
+        return result < 1 ? 1 : result;
+    }
+
+    /**
+     * 是否允许冻结时间外添加人脸活体检测
+     *
+     * @param examId
+     * @param orgId
+     * @return
+     */
+    private boolean isAddFaceVerifyOutFreezeTime(Long examId, Long orgId, Long studentId) {
+        String addFaceVerifyOutFreezeTime = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.ADD_FACE_VERIFY_OUT_FREEZE_TIME.name()).getValue();
+        return Constants.isTrue.equals(addFaceVerifyOutFreezeTime);
+    }
+
+    /**
+     * 是否允许冻结时间外添加人脸活体检测
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    private boolean isAddFaceVerifyOutFreezeTime(Long examRecordDataId) {
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+        return isAddFaceVerifyOutFreezeTime(examRecordData.getExamId(),
+                examRecordData.getOrgId(), examRecordData.getStudentId());
+    }
+
+    /**
+     * 获取是否开启人脸活体检测
+     *
+     * @param examRecordDataId 考试记录id
+     * @return
+     */
+    private Boolean getIsFaceVerify(Long examRecordDataId) {
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+
+        //如果未开启人脸活体检测,则返回null
+        return FaceBiopsyHelper.isFaceVerify(examRecordData.getRootOrgId(),
+                examRecordData.getExamId(), examRecordData.getStudentId());
+    }
+
+    /**
+     * 获取心跳时间
+     *
+     * @param studentId
+     * @return
+     */
+    private Integer getExamUsedMinutes(Long studentId) {
+        ExamingSession examingSession = examingSessionService.getExamingSession(studentId);
+        if (examingSession == null) {
+            throw new StatusException("201002", "考试会话已过期");
+        }
+
+        return Math.toIntExact(examingSession.getCost() / 60);
+    }
+}