Browse Source

交卷后续处理,代码重构及bug fix

lideyin 5 năm trước cách đây
mục cha
commit
53c201e5c7

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

@@ -7,12 +7,15 @@ import cn.com.qmth.examcloud.core.oe.task.controller.bean.GetExamCaptureResultDo
 import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureEntity;
 import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureQueueService;
 import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
 import cn.com.qmth.examcloud.core.oe.task.service.ExamingSessionService;
 import cn.com.qmth.examcloud.core.oe.task.service.bean.CompareFaceSyncInfo;
 import cn.com.qmth.examcloud.core.oe.task.service.bean.SaveExamCaptureQueueInfo;
 import cn.com.qmth.examcloud.support.Constants;
 import cn.com.qmth.examcloud.support.cache.CacheHelper;
 import cn.com.qmth.examcloud.support.cache.bean.StudentCacheBean;
+import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
 import cn.com.qmth.examcloud.support.examing.ExamingSession;
 import cn.com.qmth.examcloud.support.examing.ExamingStatus;
 import cn.com.qmth.examcloud.web.redis.RedisClient;
@@ -46,9 +49,9 @@ public class ExamCaptureController extends ControllerSupport {
     @Autowired
     private ExamCaptureQueueService examCaptureQueueService;
     @Autowired
-    private RedisTemplate<String,Object> redisTemplate;
+    private RedisClient redisClient;
     @Autowired
-    RedisClient redisClient;
+    private ExamRecordDataService examRecordDataService;
 
 //	private static AES aes = new AES();
 
@@ -134,20 +137,34 @@ public class ExamCaptureController extends ControllerSupport {
         User user = getAccessUser();
 
         ExamingSession examingSession = examingSessionService.getExamingSession(user.getUserId());
-        //不存在考试会话,直接返回
+        //不存在考试会话,或者会话状态不正确,不允许上传图片直接返回
         if (null == examingSession || examingSession.getExamingStatus() == ExamingStatus.INFORMAL) {
             return null;
         }
+
         //参数校验
         if (saveExamCaptureQueueInfo == null) {
-            throw new StatusException("ExamCaptureQueueController-uploadExamCapture-001", "对象不能为空");
+            throw new StatusException("301001", "对象不能为空");
+        }
+
+        Long examRecordDataId = saveExamCaptureQueueInfo.getExamRecordDataId();
+        if (null == examRecordDataId) {
+            throw new StatusException("301002", "examRecordDataId不能为空");
+        }
+
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+        if (null == examRecordData) {
+            throw new StatusException("301004", "无效的考试记录id");
         }
-        if (saveExamCaptureQueueInfo.getExamRecordDataId() == null) {
-            throw new StatusException("ExamCaptureQueueController-uploadExamCapture-002", "examRecordDataId不能为空");
+        //非考试中不允许上传图片
+        if (ExamRecordStatus.EXAM_ING != examRecordData.getExamRecordStatus()) {
+            return null;
         }
+
         if (StringUtils.isBlank(saveExamCaptureQueueInfo.getFileUrl())) {
-            throw new StatusException("ExamCaptureQueueController-uploadExamCapture-003", "fileUrl不能为空");
+            throw new StatusException("301005", "fileUrl不能为空");
         }
+
         //校验虚拟摄像头格式,必须 是Json数组,如果格式不正确,则置为null
         if (StringUtils.isNoneBlank(saveExamCaptureQueueInfo.getCameraInfos())) {
             try {
@@ -162,11 +179,6 @@ public class ExamCaptureController extends ControllerSupport {
         }
         validateUpyunSign(saveExamCaptureQueueInfo.getSignIdentifier(), saveExamCaptureQueueInfo.getFileUrl(), user.getUserId());
 
-        //查看redis中是否存在锁(交卷时如果照片已经处理完后,就会上锁,此时不允许再向库中存照片)
-        if (redisTemplate.hasKey(Constants.EXAM_CAPTURE_PHOTO_LOCK_PREFIX + saveExamCaptureQueueInfo.getExamRecordDataId())) {
-            return null;
-        }
-
         return examCaptureQueueService.saveExamCaptureQueue(saveExamCaptureQueueInfo, user.getUserId());
     }
 }

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

@@ -1,6 +1,7 @@
 package cn.com.qmth.examcloud.core.oe.task.service;
 
 import cn.com.qmth.examcloud.core.oe.task.dao.entity.ExamCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CalculateFaceCheckResultInfo;
 import cn.com.qmth.examcloud.core.oe.task.service.bean.CompareFaceSyncInfo;
 import cn.com.qmth.examcloud.core.oe.task.service.bean.ExamCaptureQueueInfo;
 import org.json.JSONException;
@@ -39,4 +40,11 @@ public interface ExamCaptureService {
 	 * @param examCaptureQueueInfo
 	 */
 	void disposeFaceCompare(ExamCaptureQueueInfo examCaptureQueueInfo) throws JSONException;
+
+	/**
+	 * 计算人脸检测结果
+	 * @param examRecordDataId
+	 * @return
+	 */
+    CalculateFaceCheckResultInfo calculateFaceCheckResult(Long examRecordDataId);
 }

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

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

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

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

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

@@ -2,6 +2,8 @@ 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.core.oe.task.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CalculateFaceCheckResultInfo;
 import cn.com.qmth.examcloud.support.Constants;
 import cn.com.qmth.examcloud.core.oe.task.base.ExamCaptureProcessStatisticController;
 import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureQueueRepo;
@@ -16,15 +18,19 @@ 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.examing.ExamRecordData;
 import cn.com.qmth.examcloud.support.helper.ExamCacheTransferHelper;
+import cn.com.qmth.examcloud.support.helper.FaceBiopsyHelper;
 import cn.com.qmth.examcloud.web.baidu.BaiduClient;
 import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
 import cn.com.qmth.examcloud.web.facepp.FaceppClient;
 import cn.com.qmth.examcloud.web.redis.RedisClient;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.json.JSONException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -32,7 +38,9 @@ import org.springframework.data.domain.Example;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.math.BigDecimal;
 import java.util.Date;
+import java.util.List;
 
 /**
  * @author chenken
@@ -56,6 +64,9 @@ public class ExamCaptureServiceImpl implements ExamCaptureService {
     @Autowired
     private ExamCaptureQueueService examCaptureQueueService;
 
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+
     public static final String TEMP_FILE_EXP = "face_compare/";
 
     @Autowired
@@ -347,29 +358,6 @@ public class ExamCaptureServiceImpl implements ExamCaptureService {
         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();
@@ -442,12 +430,199 @@ public class ExamCaptureServiceImpl implements ExamCaptureService {
 
     }
 
-
     @Override
     public ExamCaptureEntity getExamCaptureResult(Long examRecordDataId, String fileName) {
         return examCaptureRepo.findByExamRecordDataIdAndFileName(examRecordDataId, fileName);
     }
 
+    /**
+     * 计算人脸检测结果
+     * 相片数=0,系统判断为违纪,自动审核
+     * 考试记录为异常逻辑(进入待审):
+     * 1.陌生人次数>0
+     * 2.face++阈值 = 0 && 百度真实性阈值 > 0
+     * 真实性百分比<百度真实性阈值
+     * 3.face++阈值 > 0 && 百度真实性阈值 = 0
+     * face++成功率<face++阈值
+     * 4.face++阈值 > 0 && 百度真实性阈值 > 0
+     * face++成功率<face++阈值 ||
+     * 真实性百分比<百度真实性阈值
+     *
+     * @param examRecordData
+     * @return
+     */
+    @Override
+    public CalculateFaceCheckResultInfo calculateFaceCheckResult(Long examRecordDataId) {
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+
+        Long rootOrgId = examRecordData.getRootOrgId();
+        Long examId = examRecordData.getExamId();
+        Long orgId = examRecordData.getOrgId();
+        Long studentId = examRecordData.getStudentId();
+
+        //人脸最终检测结果
+        CalculateFaceCheckResultInfo resultInfo = new CalculateFaceCheckResultInfo();
+
+        //未开启人脸检测,直接认为异常数据
+        if (!FaceBiopsyHelper.isFaceEnable(rootOrgId, examId, studentId)) {
+            resultInfo.setWarn(false);
+            return resultInfo;
+        }
+
+        List<ExamCaptureEntity> examCaptureList = examCaptureRepo.findByExamRecordDataId(examRecordData.getId());
+
+        //无照片违纪
+        if (examCaptureList == null || examCaptureList.size() == 0) {
+            resultInfo.setWarn(true);//有异常
+            resultInfo.setIllegality(true);//违纪
+            resultInfo.setNoPhotoAndIllegality(true);//无照片违纪
+            return resultInfo;
+        }
+
+        //根据照片结果计算
+        return calculateByCaptureResult(examCaptureList, examRecordData.getExamId(), examRecordData.getStudentId());
+    }
+
+    /**
+     * 计算人脸检测数据
+     * 陌生人记录数、成功次数、失败次数、成功率
+     *
+     * @param examCaptureEntityList
+     * @param examId
+     * @param studentId
+     * @return
+     */
+    private CalculateFaceCheckResultInfo calculateByCaptureResult(List<ExamCaptureEntity> examCaptureEntityList,
+                                                                  Long examId, Long studentId) {
+        int strangerCount = 0;    // 人脸比较陌生人记录数
+        int succCount = 0;        // 人脸比较成功次数
+        int falseCount = 0;        // 人脸比较失败次数
+        double succPercent = 0d;    // 人脸比较成功率
+        int livenessSuccessCount = 0;//百度活体检测成功次数
+        double livenessSuccessPercent = 0D;//百度活体检测成功率
+        for (ExamCaptureEntity examCaptureEntity : examCaptureEntityList) {
+            if (examCaptureEntity.getIsPass() != null && examCaptureEntity.getIsPass()) {
+                succCount++;
+            } else {
+                falseCount++;
+            }
+            if (examCaptureEntity.getIsStranger() != null && examCaptureEntity.getIsStranger()) {
+                strangerCount++;
+            }
+            livenessSuccessCount += calculateFacelivenessSuccessCount(examCaptureEntity.getFacelivenessResult());
+        }
+        int allNum = examCaptureEntityList.size();
+        BigDecimal bg = new BigDecimal(((double) succCount / allNum) * 100);
+        succPercent = bg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();// 人脸比较成功率
+
+        //活体检测最终结果
+        CalculateFaceCheckResultInfo resultInfo = new CalculateFaceCheckResultInfo();
+
+        resultInfo.setFaceTotalCount(allNum);//检测总次数
+        resultInfo.setFaceSuccessPercent(succPercent);//成功率
+        resultInfo.setFaceStrangerCount(strangerCount);//有陌生人的次数
+        resultInfo.setFaceSuccessCount(succCount);//成功次数
+        resultInfo.setFaceFailedCount(falseCount);//失败次数
+
+        //计算百度活体检测通过率
+        BigDecimal livenessSuccessBg = new BigDecimal(((double) livenessSuccessCount / allNum) * 100);
+        livenessSuccessPercent = livenessSuccessBg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();//百度活体检测成功率
+        resultInfo.setBaiduFaceLivenessSuccessPercent(livenessSuccessPercent);
+
+        //陌生人个数>0
+        if (resultInfo.getFaceStrangerCount() > 0) {
+            resultInfo.setWarn(true);
+            return resultInfo;
+        }
+
+        //人脸识别阀值
+        String warnThresholdStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.WARN_THRESHOLD.name()).getValue();
+        if (StringUtils.isBlank(warnThresholdStr)) {
+            throw new StatusException("400101", "人脸检测预警阈值未设置");
+        }
+
+        //人脸真实性(百度活体检测)通过阀值
+        String liveWarnThresholdStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.LIVING_WARN_THRESHOLD.name()).getValue();
+
+        if (StringUtils.isBlank(liveWarnThresholdStr)) {
+            throw new StatusException("400102", "人脸真实性阈值未设置");
+        }
+
+        double warnThreshold = Double.parseDouble(warnThresholdStr);
+        double livenessThreshold = Double.parseDouble(liveWarnThresholdStr);
+
+        if (warnThreshold == 0d && livenessThreshold > 0d) {
+            resultInfo.setWarn(livenessSuccessPercent < livenessThreshold);
+        } else if (warnThreshold > 0d && livenessThreshold == 0d) {
+            resultInfo.setWarn(succPercent < warnThreshold);
+        } else if (warnThreshold > 0d && livenessThreshold > 0d) {
+            resultInfo.setWarn(succPercent < warnThreshold || livenessSuccessPercent < livenessThreshold);
+        }
+
+        return resultInfo;
+    }
+
+    /**
+     * 计算百度活体检测成功数量
+     *
+     * @param facelivenessResult
+     * @return
+     */
+    private int calculateFacelivenessSuccessCount(String facelivenessResult) {
+        if (StringUtils.isNotBlank(facelivenessResult)) {
+            org.json.JSONObject jsonObject;
+            try {
+                jsonObject = new org.json.JSONObject(facelivenessResult);
+                if (jsonObject.has("error_code") && jsonObject.getInt("error_code") == 0 && jsonObject.has("result")) {
+                    org.json.JSONObject resultJson = jsonObject.getJSONObject("result");
+                    if (resultJson.has("face_liveness")) {
+                        double faceLivenessVal = resultJson.getDouble("face_liveness");
+
+                        Double baiduFacelivenessThreshold;
+                        SysPropertyCacheBean baiduFacelivenessThresholdProperty = CacheHelper.getSysProperty("$baidu.faceliveness.threshold");
+                        if (!baiduFacelivenessThresholdProperty.getHasValue()) {
+                            baiduFacelivenessThreshold = Constants.DEFAULT_BAIDU_FACELIVENESS_THRESHOLD;
+                        } else {
+                            baiduFacelivenessThreshold = Double.valueOf(baiduFacelivenessThresholdProperty.getValue().toString());
+                        }
+
+                        if (faceLivenessVal > baiduFacelivenessThreshold) {
+                            return 1;
+                        }
+                    }
+                }
+            } catch (JSONException e) {
+                e.printStackTrace();
+                return 0;
+            }
+
+        }
+        return 0;
+    }
+
+    private ExamCaptureEntity getExamCaptureFromQueue(ExamCaptureQueueInfo queue) {
+        long currentTimeMillis = System.currentTimeMillis();
+        long createTimeMillis = queue.getCreationTime().getTime();
+        long faceCompareStartTimeMillis = queue.getFaceCompareStartTime();
+
+        ExamCaptureEntity resultEntity = new ExamCaptureEntity();
+        resultEntity.setCameraInfos(queue.getCameraInfos());
+        resultEntity.setExamRecordDataId(queue.getExamRecordDataId());
+        resultEntity.setExtMsg(queue.getExtMsg());
+        resultEntity.setFaceCompareResult(queue.getFaceCompareResult());
+        resultEntity.setFacelivenessResult(queue.getFacelivenessResult());
+        resultEntity.setFileName(queue.getFileName());
+        resultEntity.setFileUrl(queue.getFileUrl());
+        resultEntity.setHasVirtualCamera(queue.getHasVirtualCamera());
+        resultEntity.setIsStranger(queue.getStranger());
+        resultEntity.setIsPass(queue.getPass());
+        resultEntity.setProcessTime(currentTimeMillis - createTimeMillis);//从进队列到处理完毕的时间
+        resultEntity.setUsedTime(currentTimeMillis - faceCompareStartTimeMillis);//从开始处理到处理完毕的时间
+        return resultEntity;
+    }
+
 
     /**
      * 校验是否有陌生人脸

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

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

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

@@ -28,12 +28,12 @@ public class ExamingSessionServiceImpl implements ExamingSessionService {
     @Override
     public ExamingSession getExamingSession(Long studentId) {
         String key = RedisKeyHelper.getBuilder().examingSessionKey(studentId);
-        return redisClient.get(key+studentId,ExamingSession.class);
+        return redisClient.get(key,ExamingSession.class);
     }
 
     @Override
     public void deleteExamingSession(Long studentId) {
         String key = RedisKeyHelper.getBuilder().examingSessionKey(studentId);
-        redisClient.delete(key+studentId);
+        redisClient.delete(key);
     }
 }

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

@@ -1,15 +1,29 @@
 package cn.com.qmth.examcloud.core.oe.task.service.pipeline;
 
+import cn.com.qmth.examcloud.commons.exception.StatusException;
 import cn.com.qmth.examcloud.commons.helpers.KeyValuePair;
 import cn.com.qmth.examcloud.commons.helpers.pipeline.NodeExecuter;
-import cn.com.qmth.examcloud.commons.helpers.pipeline.Storer;
 import cn.com.qmth.examcloud.commons.helpers.pipeline.TaskContext;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang3.RandomUtils;
+import cn.com.qmth.examcloud.core.oe.student.api.ExamRecordDataCloudService;
+import cn.com.qmth.examcloud.core.oe.student.api.request.CalcExamScoreReq;
+import cn.com.qmth.examcloud.core.oe.student.api.request.CalcFaceBiopsyResultReq;
+import cn.com.qmth.examcloud.core.oe.student.api.response.CalcExamScoreResp;
+import cn.com.qmth.examcloud.core.oe.student.api.response.CalcFaceBiopsyResultResp;
+import cn.com.qmth.examcloud.core.oe.task.dao.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.task.service.bean.CalculateFaceCheckResultInfo;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.support.enums.HandInExamType;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
 
-import java.io.File;
+import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 
 /**
  * @Description 交卷后续处理执行器
@@ -17,10 +31,47 @@ import java.util.Objects;
  * @Date 2019/12/17 16:39
  * @Version 1.0
  */
-public class AfterHandInExamExecutor implements NodeExecuter<String, String, String, String> {
+@Component
+public class AfterHandInExamExecutor implements NodeExecuter<Long, ExamRecordData, Long, ExamRecordData> {
+
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
 
     @Override
-    public List<KeyValuePair<String, String>> execute(String key, String value, TaskContext context) throws Exception {
-        return null;
+    public List<KeyValuePair<Long, ExamRecordData>> execute(Long key, ExamRecordData examRecordData, TaskContext context) throws Exception {
+
+        List<KeyValuePair<Long, ExamRecordData>> resultList = new ArrayList<>();
+        KeyValuePair<Long, ExamRecordData> keyValuePair = new KeyValuePair<>(key, examRecordData);
+
+        //针对已交卷的数据进行交卷后续处理
+        if (examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_HAND_IN ||
+                examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_AUTO_HAND_IN) {
+
+            SysPropertyCacheBean maxProcessSecondsProperty = CacheHelper.getSysProperty("oe.task.maxProcessSeconds");
+            if (maxProcessSecondsProperty.getHasValue()) {
+                //本节点最大处理时长
+                Long maxProcessSeconds = Long.valueOf(maxProcessSecondsProperty.getValue().toString());
+                //交卷时间戳
+                Long handInTime = (examRecordData.getEndTime() == null ? examRecordData.getCleanTime() : examRecordData.getEndTime()).getTime();
+                Long times = System.currentTimeMillis() - handInTime;
+
+                //如果交卷后超过指定时长内仍未处理完成,则交给下一节点进行处理
+                if (times > maxProcessSeconds * 1000) {
+                    resultList.add(keyValuePair);
+                    return resultList;
+                }
+            }
+
+            examRecordData=examRecordDataService.processAfterHandInExam(examRecordData.getId());
+
+            keyValuePair.setValue(examRecordData);
+            resultList.add(keyValuePair);
+
+            return resultList;
+        }
+
+        resultList.add(keyValuePair);
+        return resultList;
     }
+
 }

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

@@ -4,6 +4,7 @@ import cn.com.qmth.examcloud.commons.helpers.KeyValuePair;
 import cn.com.qmth.examcloud.commons.helpers.pipeline.NodeExecuter;
 import cn.com.qmth.examcloud.commons.helpers.pipeline.Storer;
 import cn.com.qmth.examcloud.commons.helpers.pipeline.TaskContext;
+import cn.com.qmth.examcloud.core.oe.task.service.ExamRecordDataService;
 import cn.com.qmth.examcloud.core.oe.task.service.ExamingSessionService;
 import cn.com.qmth.examcloud.examwork.api.ExamStudentCloudService;
 import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
@@ -33,6 +34,8 @@ public class HandInExamExecutor implements NodeExecuter<Long, ExamRecordData, Lo
 
     @Autowired
     private ExamingSessionService examingSessionService;
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
 
     @Override
     public List<KeyValuePair<Long, ExamRecordData>> execute(Long key, ExamRecordData examRecordData, TaskContext context) throws Exception {
@@ -46,8 +49,14 @@ public class HandInExamExecutor implements NodeExecuter<Long, ExamRecordData, Lo
 
             // 如果考试会话不存在/超过考试时间/超过断点续考时间,自动交卷
             if (null == examingSession || isOverExamTime(examingSession) || isOverBreakpointTime(examingSession)) {
+                Date now = new Date();
+
+                //更改内存中的交卷状态
                 examRecordData.setExamRecordStatus(ExamRecordStatus.EXAM_AUTO_HAND_IN);
-                examRecordData.setCleanTime(new Date());
+                examRecordData.setCleanTime(now);
+
+                examRecordDataService.saveExamRecordDataCache(examRecordData.getId(), examRecordData);
+
                 keyValuePair.setValue(examRecordData);
                 resultList.add(keyValuePair);
 
@@ -62,23 +71,23 @@ public class HandInExamExecutor implements NodeExecuter<Long, ExamRecordData, Lo
     }
 
     /**
-	 * 是否超过考试时长
-	 *
-	 * @param examingSession
-	 * @return
-	 */
-	private boolean isOverExamTime(ExamingSession examingSession) {
-		return examingSession.getExamDuration() <= examingSession.getCost() * 1000;
-	}
+     * 是否超过考试时长
+     *
+     * @param examingSession
+     * @return
+     */
+    private boolean isOverExamTime(ExamingSession examingSession) {
+        return examingSession.getExamDuration() <= examingSession.getCost() * 1000;
+    }
 
-	/**
-	 * 是否超过断点续考时间
-	 *
-	 * @param examingSession
-	 * @return
-	 */
-	private boolean isOverBreakpointTime(ExamingSession examingSession) {
-		long now = System.currentTimeMillis();
-		return now - examingSession.getActiveTime() >= examingSession.getExamReconnectTime().intValue() * 60 * 1000;
-	}
+    /**
+     * 是否超过断点续考时间
+     *
+     * @param examingSession
+     * @return
+     */
+    private boolean isOverBreakpointTime(ExamingSession examingSession) {
+        long now = System.currentTimeMillis();
+        return now - examingSession.getActiveTime() >= examingSession.getExamReconnectTime().intValue() * 60 * 1000;
+    }
 }