deason 2 лет назад
Родитель
Сommit
d2b51a4db7
19 измененных файлов с 1851 добавлено и 277 удалено
  1. 26 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/DevOpsController.java
  2. 231 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/ExamCaptureController.java
  3. 1 1
      examcloud-core-oe-student-base/pom.xml
  4. 10 4
      examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/ExamRecordDataRepo.java
  5. 11 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/client/FaceLiveVerifyInfo.java
  6. 129 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/task/CalculateFaceCheckResultInfo.java
  7. 297 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/task/ExamCaptureQueueInfo.java
  8. 68 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/task/GetExamCaptureResultDomain.java
  9. 84 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/task/SaveExamCaptureQueueInfo.java
  10. 33 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamCaptureQueueService.java
  11. 53 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamCaptureService.java
  12. 16 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamSyncCaptureService.java
  13. 89 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamCaptureQueueServiceImpl.java
  14. 372 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamCaptureServiceImpl.java
  15. 135 116
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamControlServiceImpl.java
  16. 21 9
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamFaceLiveVerifyServiceImpl.java
  17. 138 100
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordDataServiceImpl.java
  18. 63 47
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordQuestionsServiceImpl.java
  19. 74 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamSyncCaptureServiceImpl.java

+ 26 - 0
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/DevOpsController.java

@@ -0,0 +1,26 @@
+package cn.com.qmth.examcloud.core.oe.student.api.controller;
+
+import cn.com.qmth.examcloud.web.support.Naked;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Api(tags = "运维接口")
+@RestController
+@RequestMapping("${app.api.oe.student}")
+public class DevOpsController {
+
+    private static final Logger log = LoggerFactory.getLogger(DevOpsController.class);
+
+    @Naked
+    @ApiOperation(value = "运维监控检测接口")
+    @GetMapping("/devops")
+    public Long devops() {
+        return System.currentTimeMillis();
+    }
+
+}

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

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

+ 1 - 1
examcloud-core-oe-student-base/pom.xml

@@ -64,7 +64,7 @@
         </dependency>
         <dependency>
             <groupId>cn.com.qmth.examcloud.rpc</groupId>
-            <artifactId>examcloud-core-oe-student-api-client</artifactId>
+            <artifactId>examcloud-core-oe-student-api</artifactId>
             <version>${project.version}</version>
         </dependency>
         <dependency>

+ 10 - 4
examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/ExamRecordDataRepo.java

@@ -1,12 +1,16 @@
 package cn.com.qmth.examcloud.core.oe.student.dao;
 
 import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.support.enums.SyncStatus;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 import org.springframework.data.jpa.repository.Modifying;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -27,13 +31,15 @@ public interface ExamRecordDataRepo extends JpaRepository<ExamRecordDataEntity,
     @Query(value = "update ec_oes_exam_record_data set batch_num=?1 where id=?2", nativeQuery = true)
     int updateBatchNumById(Long batchNum, Long id);
 
+    @Transactional
     @Modifying
-    @Query(value = "update ec_oes_exam_record_data set exam_record_status=?1 where id=?2", nativeQuery = true)
-    int updateExamRecordStatusById(String examRecordStatus, Long id);
+    @Query(value = "update ExamRecordDataEntity set examRecordStatus=?1,updateTime=?2 where id=?3")
+    int updateExamRecordStatusById(ExamRecordStatus examRecordStatus, Date updateTime, Long id);
 
+    @Transactional
     @Modifying
-    @Query(value = "update ec_oes_exam_record_data set sync_status=?1 where id=?2", nativeQuery = true)
-    int updateExamRecordSyncStatusById(String syncStatus, Long id);
+    @Query(value = "update ExamRecordDataEntity set syncStatus=?1 where id=?2")
+    int updateSyncStatusById(SyncStatus syncStatus, Long id);
 
     @Query(value = "SELECT course_id from ec_oes_exam_record_data WHERE id = ?1", nativeQuery = true)
     Long findCourseIdByExamRecordDataId(Long examRecordDataId);

+ 11 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/client/FaceLiveVerifyInfo.java

@@ -16,6 +16,9 @@ public class FaceLiveVerifyInfo implements JsonSerializable {
     @ApiModelProperty(value = "当前第几次活检")
     private Integer times;
 
+    @ApiModelProperty(value = "是否成功过")
+    private Boolean hasSuccess;
+
     public Long getFaceLiveVerifyId() {
         return faceLiveVerifyId;
     }
@@ -40,4 +43,12 @@ public class FaceLiveVerifyInfo implements JsonSerializable {
         this.times = times;
     }
 
+    public Boolean getHasSuccess() {
+        return hasSuccess;
+    }
+
+    public void setHasSuccess(Boolean hasSuccess) {
+        this.hasSuccess = hasSuccess;
+    }
+
 }

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

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

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

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

+ 68 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/task/GetExamCaptureResultDomain.java

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

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

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

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

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

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

@@ -0,0 +1,53 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.oe.student.base.bean.CompareFaceSyncInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.task.CalculateFaceCheckResultInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.task.ExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamCaptureEntity;
+
+/**
+ * @Description 照片处理结果
+ * @Author lideyin
+ * @Date 2019/12/11 14:45
+ * @Version 1.0
+ */
+public interface ExamCaptureService {
+
+    void saveExamCaptureAndDeleteQueue(ExamCaptureQueueInfo examCaptureQueue);
+
+    /**
+     * 同步比较人脸:用于进入考试
+     *
+     * @param studentId     学生ID
+     * @param baseFaceToken 学生底照faceToken
+     * @param fileUrl       抓拍照片Url
+     * @return
+     */
+    CompareFaceSyncInfo compareFaceSyncByFileUrl(Long studentId, String baseFaceToken, String fileUrl);
+
+    /**
+     * 获取考试抓拍结果
+     *
+     * @param examRecordDataId
+     * @param fileName
+     * @return
+     */
+    ExamCaptureEntity getExamCaptureResult(Long examRecordDataId, String fileName);
+
+    /**
+     * 计算人脸检测结果
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    CalculateFaceCheckResultInfo calculateFaceCheckResult(Long examRecordDataId);
+
+    /**
+     * 更新考试抓拍照片在队列优中的先级
+     *
+     * @param priority         优先级
+     * @param examRecordDataId
+     */
+    void updateExamCaptureQueuePriority(Long examRecordDataId, int priority);
+
+}

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

@@ -0,0 +1,16 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+/**
+ * @Description 同步抓拍照片接口
+ * @Author lideyin
+ * @Date 2019/12/6 16:19
+ * @Version 1.0
+ */
+public interface ExamSyncCaptureService {
+
+    /**
+     * 将同步人脸比对结果存储到抓后结果表中
+     */
+    void saveExamCaptureSyncCompareResult(Long studentId, Long examRecordDataId);
+
+}

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

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

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

@@ -0,0 +1,372 @@
+package cn.com.qmth.examcloud.core.oe.student.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.student.base.bean.CompareFaceSyncInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.task.CalculateFaceCheckResultInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.task.ExamCaptureQueueInfo;
+import cn.com.qmth.examcloud.core.oe.student.dao.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.student.dao.ExamCaptureRepo;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamCaptureEntity;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamCaptureQueueService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamCaptureService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.support.enums.ExamProperties;
+import cn.com.qmth.examcloud.support.examing.ExamRecordData;
+import cn.com.qmth.examcloud.support.filestorage.FileStorageUtil;
+import cn.com.qmth.examcloud.support.helper.ExamCacheTransferHelper;
+import cn.com.qmth.examcloud.support.helper.FaceBiopsyHelper;
+import cn.com.qmth.examcloud.web.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.json.JSONException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Example;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * @author chenken
+ * @date 2018年9月5日 下午3:31:37
+ * @company QMTH
+ * @description 考试抓拍服务实现
+ */
+@SuppressWarnings("ALL")
+@Service("examCaptureService")
+public class ExamCaptureServiceImpl implements ExamCaptureService {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamCaptureServiceImpl.class);
+
+    @Autowired
+    private ExamCaptureRepo examCaptureRepo;
+
+    @Autowired
+    private ExamCaptureQueueRepo examCaptureQueueRepo;
+
+    @Autowired
+    private ExamCaptureQueueService examCaptureQueueService;
+
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+
+    public static final String TEMP_FILE_EXP = "face_compare/";
+
+    @Autowired
+    private RedisClient redisClient;
+
+    /**
+     * 保存人脸检测最终结果并删除队列
+     *
+     * @param examCapture      抓拍照片最终检测最终结果实体
+     * @param examCaptureQueue 抓拍照片队列表
+     */
+    @Override
+    @Transactional
+    public void saveExamCaptureAndDeleteQueue(ExamCaptureQueueInfo examCaptureQueue) {
+        ExamCaptureEntity examCapture = getExamCaptureFromQueue(examCaptureQueue);
+
+        //同一考试记录下如果有重复的照片,则直接跳过
+        ExamCaptureEntity query = new ExamCaptureEntity();
+        query.setExamRecordDataId(examCapture.getExamRecordDataId());
+        query.setFileName(examCapture.getFileName());
+        Example<ExamCaptureEntity> example = Example.of(query);
+        //照片处理结果中如果已存在,则以已有的数据为准
+        if (!examCaptureRepo.exists(example)) {
+            examCaptureRepo.save(examCapture);
+        }
+        //删除队列中的记录
+        examCaptureQueueRepo.deleteById(examCaptureQueue.getId());
+    }
+
+    @Override
+    public CompareFaceSyncInfo compareFaceSyncByFileUrl(Long studentId, String baseFaceToken, String fileUrl) {
+        CompareFaceSyncInfo compareFaceSyncInfo = new CompareFaceSyncInfo();
+        compareFaceSyncInfo.setStudentId(studentId);
+        JSONObject facePPResult = null;
+        JsonHttpResponseHolder jsonHttpResponseHolder = null;
+        try {
+            jsonHttpResponseHolder = FaceppClient.getClient().
+                    compareWithTokenAndImageUrl(baseFaceToken,
+                            FileStorageUtil.realPath(fileUrl),
+                            FileStorageUtil.realPathBackup(fileUrl));
+            facePPResult = jsonHttpResponseHolder.getRespBody();
+        } catch (StatusException e) {
+            //如果错误码是801,802,803直接结束,不重试
+            if (e.getCode().equals("801") || e.getCode().equals("802") || e.getCode().equals("803")) {
+                compareFaceSyncInfo.setIsPass(false);
+                compareFaceSyncInfo.setExistsSystemError(true);
+                compareFaceSyncInfo.setErrorMsg(e.getDesc());
+
+                log.error("[COMPARE_FACE_SYNC] face++人脸比对无法处理的图片地址,errMsg=" + e.getDesc(), e);
+                return compareFaceSyncInfo;
+            }
+            throw e;
+        } catch (Exception e) {
+            compareFaceSyncInfo.setIsPass(false);
+            compareFaceSyncInfo.setExistsSystemError(true);
+            compareFaceSyncInfo.setErrorMsg("系统异常");
+
+            log.error("[COMPARE_FACE_SYNC] 未处理的系统异常,errMsg=" + e.getMessage(), e);
+            return compareFaceSyncInfo;
+        }
+        if (facePPResult.containsKey(Constants.ERROR_MSG)) {
+            compareFaceSyncInfo.setFaceCompareResult(facePPResult.toString());
+            compareFaceSyncInfo.setIsPass(false);
+            String errMsg = facePPResult.getString(Constants.ERROR_MSG);
+            if (errMsg.contains(Constants.FACE_COMPARE_AUTHORIZATION_ERROR)) {
+                compareFaceSyncInfo.setExistsSystemError(true);
+            }
+            if (errMsg.contains(Constants.FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED)) {
+                log.error("[FaceCompare] FacePlusPlus_QPS_CONCURRENCY_LIMITED");
+                compareFaceSyncInfo.setExistsSystemError(true);
+            }
+            compareFaceSyncInfo.setErrorMsg("facePP called failed : " + facePPResult.toString());
+            return compareFaceSyncInfo;
+        } else {
+            compareFaceSyncInfo.setFaceCompareResult(facePPResult.toString());
+            if (facePPResult.containsKey("confidence")) {
+                double confidence = facePPResult.getDouble("confidence");
+                JSONObject thresholdsJsonObject = facePPResult.getJSONObject("thresholds");
+                double le4 = thresholdsJsonObject.getDouble("1e-4");
+                JSONArray face2Array = facePPResult.getJSONArray("faces2");
+                boolean hasStranger = face2Array.size() > 1;
+                compareFaceSyncInfo.setIsStranger(hasStranger);//是否有陌生人
+                boolean isPass = confidence >= le4;
+                compareFaceSyncInfo.setIsPass(isPass);//是否通过
+                String errorMsg = null;
+                if (hasStranger && !isPass) {
+                    errorMsg = "检测过程中相片非本人,且存在多人脸";
+                } else if (hasStranger) {
+                    errorMsg = "检测过程中多人脸失败";
+                } else if (!isPass) {
+                    errorMsg = "检测过程中相片非本人";
+                }
+                compareFaceSyncInfo.setErrorMsg(errorMsg);
+            } else {
+                compareFaceSyncInfo.setIsPass(false);
+                compareFaceSyncInfo.setErrorMsg("未检测到人脸");
+            }
+            return compareFaceSyncInfo;
+        }
+    }
+
+    @Override
+    public ExamCaptureEntity getExamCaptureResult(Long examRecordDataId, String fileName) {
+        return examCaptureRepo.findByExamRecordDataIdAndFileName(examRecordDataId, fileName);
+    }
+
+    /**
+     * 计算人脸检测结果
+     * 相片数=0,系统判断为违纪,自动审核
+     * 考试记录为异常逻辑(进入待审):
+     * 1.陌生人次数>0
+     * 2.face++阈值 = 0 && 百度真实性阈值 > 0
+     * 真实性百分比<百度真实性阈值
+     * 3.face++阈值 > 0 && 百度真实性阈值 = 0
+     * face++成功率<face++阈值
+     * 4.face++阈值 > 0 && 百度真实性阈值 > 0
+     * face++成功率<face++阈值 ||
+     * 真实性百分比<百度真实性阈值
+     *
+     * @param examRecordData
+     * @return
+     */
+    @Override
+    public CalculateFaceCheckResultInfo calculateFaceCheckResult(Long examRecordDataId) {
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+
+        Long rootOrgId = examRecordData.getRootOrgId();
+        Long examId = examRecordData.getExamId();
+        Long orgId = examRecordData.getOrgId();
+        Long studentId = examRecordData.getStudentId();
+
+        //人脸最终检测结果
+        CalculateFaceCheckResultInfo resultInfo = new CalculateFaceCheckResultInfo();
+
+        //未开启人脸检测,直接认为正常数据
+        if (!FaceBiopsyHelper.isFaceEnable(rootOrgId, examId, studentId)) {
+            resultInfo.setIsWarn(false);
+            return resultInfo;
+        }
+
+        List<ExamCaptureEntity> examCaptureList = examCaptureRepo.findByExamRecordDataId(examRecordData.getId());
+
+        //无照片违纪
+        if (examCaptureList == null || examCaptureList.size() == 0) {
+            resultInfo.setIsWarn(true);//有异常
+            resultInfo.setIsIllegality(true);//违纪
+            resultInfo.setNoPhotoAndIllegality(true);//无照片违纪
+            return resultInfo;
+        }
+
+        //根据照片结果计算
+        return calculateByCaptureResult(examCaptureList, examRecordData.getExamId(), examRecordData.getStudentId());
+    }
+
+    @Override
+    public void updateExamCaptureQueuePriority(Long examRecordDataId, int priority) {
+        examCaptureQueueRepo.updateExamCaptureQueuePriority(priority, examRecordDataId);
+    }
+
+    /**
+     * 计算人脸检测数据
+     * 陌生人记录数、成功次数、失败次数、成功率
+     *
+     * @param examCaptureEntityList
+     * @param examId
+     * @param studentId
+     * @return
+     */
+    private CalculateFaceCheckResultInfo calculateByCaptureResult(List<ExamCaptureEntity> examCaptureEntityList,
+                                                                  Long examId, Long studentId) {
+        int strangerCount = 0;    // 人脸比较陌生人记录数
+        int succCount = 0;        // 人脸比较成功次数
+        int falseCount = 0;        // 人脸比较失败次数
+        double succPercent = 0d;    // 人脸比较成功率
+        int livenessSuccessCount = 0;//百度活体检测成功次数
+        double livenessSuccessPercent = 0D;//百度活体检测成功率
+        for (ExamCaptureEntity examCaptureEntity : examCaptureEntityList) {
+            if (examCaptureEntity.getIsPass() != null && examCaptureEntity.getIsPass()) {
+                succCount++;
+            } else {
+                falseCount++;
+            }
+            if (examCaptureEntity.getIsStranger() != null && examCaptureEntity.getIsStranger()) {
+                strangerCount++;
+            }
+            livenessSuccessCount += calculateFacelivenessSuccessCount(examCaptureEntity.getFacelivenessResult());
+        }
+        int allNum = examCaptureEntityList.size();
+        BigDecimal bg = new BigDecimal(((double) succCount / allNum) * 100);
+        succPercent = bg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();// 人脸比较成功率
+
+        //活体检测最终结果
+        CalculateFaceCheckResultInfo resultInfo = new CalculateFaceCheckResultInfo();
+
+        resultInfo.setFaceTotalCount(allNum);//检测总次数
+        resultInfo.setFaceSuccessPercent(succPercent);//成功率
+        resultInfo.setFaceStrangerCount(strangerCount);//有陌生人的次数
+        resultInfo.setFaceSuccessCount(succCount);//成功次数
+        resultInfo.setFaceFailedCount(falseCount);//失败次数
+
+        //计算百度活体检测通过率
+        BigDecimal livenessSuccessBg = new BigDecimal(((double) livenessSuccessCount / allNum) * 100);
+        livenessSuccessPercent = livenessSuccessBg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();//百度活体检测成功率
+        resultInfo.setBaiduFaceLivenessSuccessPercent(livenessSuccessPercent);
+
+        //陌生人个数>0
+        if (resultInfo.getFaceStrangerCount() > 0) {
+            resultInfo.setIsWarn(true);
+            return resultInfo;
+        }
+
+        //人脸识别阀值
+        String warnThresholdStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.WARN_THRESHOLD.name()).getValue();
+        if (StringUtils.isBlank(warnThresholdStr)) {
+            throw new StatusException("400101", "人脸检测预警阈值未设置");
+        }
+
+        //人脸真实性(百度活体检测)通过阀值
+        String liveWarnThresholdStr = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.LIVING_WARN_THRESHOLD.name()).getValue();
+
+        if (StringUtils.isBlank(liveWarnThresholdStr)) {
+            throw new StatusException("400102", "人脸真实性阈值未设置");
+        }
+
+        double warnThreshold = Double.parseDouble(warnThresholdStr);
+        double livenessThreshold = Double.parseDouble(liveWarnThresholdStr);
+
+        if (warnThreshold == 0d && livenessThreshold > 0d) {
+            resultInfo.setIsWarn(livenessSuccessPercent < livenessThreshold);
+        } else if (warnThreshold > 0d && livenessThreshold == 0d) {
+            resultInfo.setIsWarn(succPercent < warnThreshold);
+        } else if (warnThreshold > 0d && livenessThreshold > 0d) {
+            resultInfo.setIsWarn(succPercent < warnThreshold || livenessSuccessPercent < livenessThreshold);
+        }
+
+        return resultInfo;
+    }
+
+    /**
+     * 计算百度活体检测成功数量
+     */
+    private int calculateFacelivenessSuccessCount(String facelivenessResult) {
+        if (StringUtils.isNotBlank(facelivenessResult)) {
+            // 兼容C端学生端情况,人脸真实性值只存在0或1两种值
+            if ("0".equals(facelivenessResult)) {
+                return 0;
+            }
+            if ("1".equals(facelivenessResult)) {
+                return 1;
+            }
+
+            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) {
+                log.error(e.getMessage(), e);
+                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();
+
+        if (StringUtils.length(queue.getCameraInfos()) >= Constants.VM_CAMERA_SIZE_LIMIT) {
+            resultEntity.setCameraInfos(Constants.VM_CAMERA_WARN);
+            log.warn("虚拟摄像头信息超长! " + queue.getExamRecordDataId());
+        } else {
+            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;
+    }
+
+}

+ 135 - 116
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamControlServiceImpl.java

@@ -33,12 +33,10 @@ import cn.com.qmth.examcloud.core.oe.student.base.utils.CommonUtil;
 import cn.com.qmth.examcloud.core.oe.student.base.utils.QuestionTypeUtil;
 import cn.com.qmth.examcloud.core.oe.student.bean.*;
 import cn.com.qmth.examcloud.core.oe.student.dao.ExamContinuedRecordRepo;
+import cn.com.qmth.examcloud.core.oe.student.dao.ExamRecordDataRepo;
 import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamContinuedRecordEntity;
 import cn.com.qmth.examcloud.core.oe.student.report.ExamProcessRecordReport;
 import cn.com.qmth.examcloud.core.oe.student.service.*;
-import cn.com.qmth.examcloud.core.oe.task.api.ExamCaptureCloudService;
-import cn.com.qmth.examcloud.core.oe.task.api.request.SaveExamCaptureSyncCompareResultReq;
-import cn.com.qmth.examcloud.core.oe.task.api.request.UpdateExamCaptureQueuePriorityReq;
 import cn.com.qmth.examcloud.examwork.api.ExamCloudService;
 import cn.com.qmth.examcloud.examwork.api.request.GetExamPropertyReq;
 import cn.com.qmth.examcloud.examwork.api.response.GetExamPropertyResp;
@@ -120,7 +118,10 @@ public class ExamControlServiceImpl implements ExamControlService {
     private ExamRecordQuestionsService examRecordQuestionsService;
 
     @Autowired
-    private ExamCaptureCloudService examCaptureCloudService;
+    private ExamCaptureService examCaptureService;
+
+    @Autowired
+    private ExamSyncCaptureService examSyncCaptureService;
 
     @Autowired
     private ExamCloudService examCloudService;
@@ -155,6 +156,9 @@ public class ExamControlServiceImpl implements ExamControlService {
     @Autowired
     private CryptoConfigCloudService cryptoConfigCloudService;
 
+    @Autowired
+    private ExamRecordDataRepo examRecordDataRepo;
+
     @Autowired
     private CryptoFactory cryptoFactory;
 
@@ -234,10 +238,7 @@ public class ExamControlServiceImpl implements ExamControlService {
         Long rootOrgId = examRecordData.getRootOrgId();
         Long examId = examRecordData.getExamId();
         if (FaceBiopsyHelper.isFaceEnable(rootOrgId, examId, studentId)) {
-            SaveExamCaptureSyncCompareResultReq req = new SaveExamCaptureSyncCompareResultReq();
-            req.setExamRecordDataId(examRecordData.getId());
-            req.setStudentId(userId);
-            examCaptureCloudService.saveExamCaptureSyncCompareResult(req);
+            examSyncCaptureService.saveExamCaptureSyncCompareResult(userId, examRecordData.getId());
         }
 
         // 小题乱序,选项乱序
@@ -260,6 +261,9 @@ public class ExamControlServiceImpl implements ExamControlService {
         // 初始化考试会话
         initializeExamRecordSession(examingSession, examRecordData, examBean);
 
+        //设置并保存上次活动时间
+        setAndSaveActiveTime(examRecordData.getId(), ip);
+
         // 保存考试次数控制信息
         ExamBoss eb = examBossService.getExamBoss(examingSession.getExamStudentId());
         if (eb == null) {
@@ -272,9 +276,6 @@ public class ExamControlServiceImpl implements ExamControlService {
         eb.setStartCount(eb.getStartCount() + 1);
         examBossService.saveExamBoss(examStudentId, eb);
 
-        //设置并保存上次活动时间
-        setAndSaveActiveTime(examRecordData.getId(), ip);
-
         // 在线考生开考打点
         ReportsUtil.report(new OnlineExamStudentReport(rootOrgId, userId, examBean.getId(), examStudentId));
 
@@ -826,10 +827,7 @@ public class ExamControlServiceImpl implements ExamControlService {
                 //更新照片处理的优先级不影响交卷业务
                 try {
                     // 更新照片抓拍队列优先级为高优先级
-                    UpdateExamCaptureQueuePriorityReq req = new UpdateExamCaptureQueuePriorityReq();
-                    req.setExamRecordDataId(examRecordDataId);
-                    req.setPriority(Constants.PROCESS_CAPTURE_HIGH_PRIORITY);
-                    examCaptureCloudService.updateExamCaptureQueuePriority(req);
+                    examCaptureService.updateExamCaptureQueuePriority(examRecordDataId, Constants.PROCESS_CAPTURE_HIGH_PRIORITY);
                 } catch (Exception e) {
                     log.error("updateExamCaptureQueuePriority examRecordDataId:{}, error: db deadlock {}", examRecordDataId, e.getMessage());
                 }
@@ -842,8 +840,15 @@ public class ExamControlServiceImpl implements ExamControlService {
         }
 
         //交卷时,落地最近的上次活动时间字段
-        examRecordData.setLastActiveTime(new Date(getExamingActivityTime(examRecordDataId).getActiveTime()));
+        ExamingActivityTime examingActiveTime = getExamingActivityTime(examRecordDataId);
+        if (examingActiveTime.getActiveTime() != null) {
+            examRecordData.setLastActiveTime(new Date(examingActiveTime.getActiveTime()));
+        } else {
+            // activeTime is null ? todo
+            examRecordData.setLastActiveTime(new Date());
+        }
 
+        //设置并保存上次活动时间
         setAndSaveActiveTime(examRecordDataId, ip);
 
         //特殊处理:如果考试类型为 在线练习,则需要将部分数据提前入库,并更新相关状态
@@ -872,7 +877,7 @@ public class ExamControlServiceImpl implements ExamControlService {
             syncExamDataCloudService.syncExamData(syncExamDataReq);
 
             // 同步成功后,更新同步状态
-            // examRecordData.setSyncStatus(SyncStatus.SYNCED);
+            examRecordData.setSyncStatus(SyncStatus.SYNCED);
         }
 
         // 保存考试记录
@@ -881,6 +886,9 @@ public class ExamControlServiceImpl implements ExamControlService {
         // 删除redis会话
         examingSessionService.deleteExamingSession(studentId);
 
+        // 更新考试记录状态
+        examRecordDataRepo.updateExamRecordStatusById(examRecordData.getExamRecordStatus(), new Date(), examRecordDataId);
+
         log.warn("handInExam success! studentId:{}, examRecordDataId:{}, handInExamType:{}, ip:{}", studentId, examRecordDataId, handInExamType.name(), ip);
 
         //考试过程记录(交卷)打点
@@ -1660,6 +1668,7 @@ public class ExamControlServiceImpl implements ExamControlService {
             throw new StatusException("6002", ExamProperties.FREEZE_TIME.getDesc() + "未设置");
         }
         examSessionInfo.setFreezeTime(Integer.valueOf(freezeTimeStr));
+
         examSessionInfo.setExamingStatus(ExamingStatus.FORMAL);
         examingSessionService.saveExamingSession(examSessionInfo.getStudentId(), examSessionInfo);
         log.warn("initExamingSession success! studentId:{}, examRecordDataId:{}", examRecordData.getStudentId(), examRecordData.getId());
@@ -1699,85 +1708,85 @@ public class ExamControlServiceImpl implements ExamControlService {
         ExamRecordData examingRecord = checkExamSession(examSessionInfo);
         if (examingRecord == null) {
             return null;
-        } else {
-            Date now = new Date();
-            Integer maxInterruptNum = getMaxInterruptNum(examSessionInfo.getExamId(), examSessionInfo.getRootOrgId(),
-                    examSessionInfo.getOrgId());
-            CheckExamInProgressInfo checkExamInProgressInfo = new CheckExamInProgressInfo();
-
-            String examingHeartbeatKey = RedisKeyHelper.getBuilder()
-                    .examingHeartbeatKey(examSessionInfo.getExamRecordDataId());
-            ExamingHeartbeat examingHeartbeat = redisClient.get(examingHeartbeatKey,
-                    ExamingHeartbeat.class);
-            //考试已用时间(秒)
-            long usedTime = null == examingHeartbeat ? 0 : examingHeartbeat.getCost();
-
-            Long examRecordDataId = examingRecord.getId();
-            if ((examingRecord.getIsExceed() == null || !examingRecord.getIsExceed())
-                    && examingRecord.getContinuedCount().intValue() < maxInterruptNum.intValue()) {// 未达到最大断点次数,可继续断点一次
-                // 断点续考次数自增
-                int continutedCount = examingRecord.getContinuedCount() == null ? 0
-                        : examingRecord.getContinuedCount().intValue();
-                examingRecord.setContinuedCount(continutedCount + 1);
-                examingRecord.setIsContinued(true);
-                examingRecord.setIsExceed(false);
-                examingRecord.setLastActiveTime(now);
-                examingRecord.setContinuedTime(now);
-                checkExamInProgressInfo.setIsExceed(false);
-
-                //添加断点记录
-                addExamContinuedRecord(examRecordDataId, usedTime * 1000, now);
-            } else {
-                examingRecord.setIsExceed(true);
-                checkExamInProgressInfo.setIsExceed(true);
-            }
+        }
+
+        Date now = new Date();
+        Integer maxInterruptNum = getMaxInterruptNum(examSessionInfo.getExamId(), examSessionInfo.getRootOrgId(),
+                examSessionInfo.getOrgId());
+        CheckExamInProgressInfo checkExamInProgressInfo = new CheckExamInProgressInfo();
+
+        String examingHeartbeatKey = RedisKeyHelper.getBuilder()
+                .examingHeartbeatKey(examSessionInfo.getExamRecordDataId());
+        ExamingHeartbeat examingHeartbeat = redisClient.get(examingHeartbeatKey,
+                ExamingHeartbeat.class);
+        //考试已用时间(秒)
+        long usedTime = null == examingHeartbeat ? 0 : examingHeartbeat.getCost();
+
+        Long examRecordDataId = examingRecord.getId();
+        if ((examingRecord.getIsExceed() == null || !examingRecord.getIsExceed())
+                && examingRecord.getContinuedCount().intValue() < maxInterruptNum.intValue()) {// 未达到最大断点次数,可继续断点一次
+            // 断点续考次数自增
+            int continutedCount = examingRecord.getContinuedCount() == null ? 0
+                    : examingRecord.getContinuedCount().intValue();
+            examingRecord.setContinuedCount(continutedCount + 1);
+            examingRecord.setIsContinued(true);
+            examingRecord.setIsExceed(false);
             examingRecord.setLastActiveTime(now);
-            // 更新考试中的断点续考属性
-            examRecordDataService.saveExamRecordDataCache(examRecordDataId, examingRecord);
-
-
-            checkExamInProgressInfo.setExamRecordDataId(examRecordDataId);
-            checkExamInProgressInfo.setExamId(examSessionInfo.getExamId());
-            checkExamInProgressInfo.setUsedTime(usedTime);
-            checkExamInProgressInfo.setMaxInterruptNum(maxInterruptNum);
-            checkExamInProgressInfo.setInterruptNum(examingRecord.getContinuedCount());
-            checkExamInProgressInfo.setExamType(examingRecord.getExamType());
-
-            // 断点续考时重新计算活体检测的分钟数
-            Integer faceVerifyMinute;
-            FaceBiopsyScheme faceBiopsyScheme = FaceBiopsyHelper.getFaceBiopsyScheme(examSessionInfo.getRootOrgId());
-            if (FaceBiopsyScheme.FACE_CLIENT == faceBiopsyScheme) {
-                // C端活体检测方案
-                faceVerifyMinute = examFaceLiveVerifyService.calculateStartFaceVerifyMinute(examRecordDataId);
-            } else if (faceBiopsyScheme == FaceBiopsyScheme.FACE_MOTION) {
-                // Electron Client 自研活体检测方案
-                faceVerifyMinute = faceBiopsyService.calculateFaceBiopsyStartMinute(examRecordDataId);
-            } else {
-                // FaceID活体检测方案
-                faceVerifyMinute = examFaceLivenessVerifyService.getFaceLivenessVerifyMinute(
-                        examSessionInfo.getRootOrgId(), examSessionInfo.getOrgId(), examSessionInfo.getExamId(),
-                        studentId, examSessionInfo.getExamRecordDataId(), (int) usedTime / 60);
-            }
-            checkExamInProgressInfo.setFaceVerifyMinute(faceVerifyMinute);
-
-            //考试过程记录(断点)打点
-            ExamingActivityTime lastExamingActivityTime = getExamingActivityTime(examRecordDataId);
-            ReportsUtil.report(
-                    new ExamProcessRecordReport(examRecordDataId,
-                            ExamProcess.BREAK_OFF,
-                            lastExamingActivityTime.getActiveTime() == null ? new Date() : new Date(lastExamingActivityTime.getActiveTime()))
-            );
-            //考试过程记录(断点续考)打点
-            ReportsUtil.report(new ExamProcessRecordReport(examRecordDataId, ExamProcess.CONTINUE, new Date()));
-
-            setAndSaveActiveTime(examRecordDataId, ip);
-
-            checkExamInProgressInfo.setExceedMaxSwitchScreenCount(examingRecord.getExceedMaxSwitchScreenCount());
-            checkExamInProgressInfo.setSwitchScreenCount(examingRecord.getSwitchScreenCount());
-            checkExamInProgressInfo.setMaxSwitchScreenCount(examSessionInfo.getMaxSwitchScreenCount());
-            log.warn("checkExamInProgress success! studentId:{}, examRecordDataId:{}, ip:{}", studentId, examRecordDataId, ip);
-            return checkExamInProgressInfo;
+            examingRecord.setContinuedTime(now);
+            checkExamInProgressInfo.setIsExceed(false);
+
+            //添加断点记录
+            addExamContinuedRecord(examRecordDataId, usedTime * 1000, now);
+        } else {
+            examingRecord.setIsExceed(true);
+            checkExamInProgressInfo.setIsExceed(true);
+        }
+        examingRecord.setLastActiveTime(now);
+
+        // 更新考试中的断点续考属性
+        examRecordDataService.saveExamRecordDataCache(examRecordDataId, examingRecord);
+
+        checkExamInProgressInfo.setExamRecordDataId(examRecordDataId);
+        checkExamInProgressInfo.setExamId(examSessionInfo.getExamId());
+        checkExamInProgressInfo.setUsedTime(usedTime);
+        checkExamInProgressInfo.setMaxInterruptNum(maxInterruptNum);
+        checkExamInProgressInfo.setInterruptNum(examingRecord.getContinuedCount());
+        checkExamInProgressInfo.setExamType(examingRecord.getExamType());
+
+        // 断点续考时重新计算活体检测的分钟数
+        Integer faceVerifyMinute;
+        FaceBiopsyScheme faceBiopsyScheme = FaceBiopsyHelper.getFaceBiopsyScheme(examSessionInfo.getRootOrgId());
+        if (FaceBiopsyScheme.FACE_CLIENT == faceBiopsyScheme) {
+            // C端活体检测方案
+            faceVerifyMinute = examFaceLiveVerifyService.calculateStartFaceVerifyMinute(examRecordDataId);
+        } else if (faceBiopsyScheme == FaceBiopsyScheme.FACE_MOTION) {
+            // Electron Client 自研活体检测方案
+            faceVerifyMinute = faceBiopsyService.calculateFaceBiopsyStartMinute(examRecordDataId);
+        } else {
+            // FaceID活体检测方案
+            faceVerifyMinute = examFaceLivenessVerifyService.getFaceLivenessVerifyMinute(
+                    examSessionInfo.getRootOrgId(), examSessionInfo.getOrgId(), examSessionInfo.getExamId(),
+                    studentId, examSessionInfo.getExamRecordDataId(), (int) usedTime / 60);
         }
+        checkExamInProgressInfo.setFaceVerifyMinute(faceVerifyMinute);
+
+        //设置并保存上次活动时间
+        setAndSaveActiveTime(examRecordDataId, ip);
+
+        //考试过程记录(断点)打点
+        ExamingActivityTime lastExamingActivityTime = getExamingActivityTime(examRecordDataId);
+        ReportsUtil.report(
+                new ExamProcessRecordReport(examRecordDataId, ExamProcess.BREAK_OFF,
+                        lastExamingActivityTime.getActiveTime() == null ? new Date() : new Date(lastExamingActivityTime.getActiveTime()))
+        );
+        //考试过程记录(断点续考)打点
+        ReportsUtil.report(new ExamProcessRecordReport(examRecordDataId, ExamProcess.CONTINUE, new Date()));
+
+        checkExamInProgressInfo.setExceedMaxSwitchScreenCount(examingRecord.getExceedMaxSwitchScreenCount());
+        checkExamInProgressInfo.setSwitchScreenCount(examingRecord.getSwitchScreenCount());
+        checkExamInProgressInfo.setMaxSwitchScreenCount(examSessionInfo.getMaxSwitchScreenCount());
+        log.warn("checkExamInProgress success! studentId:{}, examRecordDataId:{}, ip:{}", studentId, examRecordDataId, ip);
+        return checkExamInProgressInfo;
     }
 
     /**
@@ -1938,6 +1947,7 @@ public class ExamControlServiceImpl implements ExamControlService {
         examingHeartbeat.setCost(calcUsedExamSeconds(studentId));
         redisClient.set(examingHeartbeatKey, examingHeartbeat);//更新心跳缓存
 
+        //设置并保存上次活动时间
         setAndSaveActiveTime(examSessionInfo.getExamRecordDataId(), ip);
 
         // 在线考生心跳打点
@@ -1963,21 +1973,16 @@ public class ExamControlServiceImpl implements ExamControlService {
     private void setAndSaveActiveTime(Long examRecordDataId, String ip) {
         String examingActiveTimeKey = RedisKeyHelper.getBuilder()
                 .examingActiveTimeKey(examRecordDataId);
-        ExamingActivityTime examingActiveTime = redisClient.get(examingActiveTimeKey,
-                ExamingActivityTime.class);
+        ExamingActivityTime examingActiveTime = redisClient.get(examingActiveTimeKey, ExamingActivityTime.class);
         if (null == examingActiveTime) {
             examingActiveTime = new ExamingActivityTime();
             examingActiveTime.setExamRecordDataId(examRecordDataId);
         }
 
-        Long now = System.currentTimeMillis();
-
         //ip如果发生变更,则记录ip变更记录
         String lastIp = examingActiveTime.getRealIp();
         if (StringUtils.isNotEmpty(ip) && StringUtils.isNotEmpty(lastIp) && !ip.equals(lastIp)) {
-            ReportsUtil.report(
-                    new ExamProcessRecordReport(examRecordDataId, ExamProcess.IP_CHANGE, new Date(now))
-            );
+            ReportsUtil.report(new ExamProcessRecordReport(examRecordDataId, ExamProcess.IP_CHANGE, new Date()));
         }
 
         examingActiveTime.setActiveTime(System.currentTimeMillis());
@@ -2147,34 +2152,46 @@ public class ExamControlServiceImpl implements ExamControlService {
 
     private ExamRecordDataBean copyExamRecordDataFrom(ExamRecordData examRecordData) {
         ExamRecordDataBean data = new ExamRecordDataBean();
+
         data.setId(examRecordData.getId());
-        data.setExamId(examRecordData.getExamId());
-        data.setExamType(examRecordData.getExamType() == null ? null : examRecordData.getExamType().toString());
-        data.setExamStudentId(examRecordData.getExamStudentId());
-        data.setStudentId(examRecordData.getStudentId());
-        data.setCourseId(examRecordData.getCourseId());
-        data.setOrgId(examRecordData.getOrgId());
         data.setRootOrgId(examRecordData.getRootOrgId());
+        data.setOrgId(examRecordData.getOrgId());
+        data.setCourseId(examRecordData.getCourseId());
+        data.setStudentId(examRecordData.getStudentId());
+        data.setExamStudentId(examRecordData.getExamStudentId());
+
+        data.setExamId(examRecordData.getExamId());
+        data.setExamType(examRecordData.getExamType() != null ? examRecordData.getExamType().toString() : null);
+        data.setExamStageId(examRecordData.getExamStageId());
+        data.setExamStageOrder(examRecordData.getExamStageOrder());
         data.setBasePaperId(examRecordData.getBasePaperId());
         data.setPaperType(examRecordData.getPaperType());
-        data.setExamRecordStatus(examRecordData.getExamRecordStatus() == null ? null : examRecordData.getExamRecordStatus().toString());
+
         data.setStartTime(examRecordData.getStartTime());
         data.setEndTime(examRecordData.getEndTime());
+        data.setEnterExamTime(examRecordData.getEnterExamTime());
         data.setCleanTime(examRecordData.getCleanTime());
-        data.setWarn(examRecordData.getIsWarn() == null ? false : examRecordData.getIsWarn());
-        data.setAudit(examRecordData.getIsAudit() == null ? false : examRecordData.getIsAudit());
-        data.setIllegality(examRecordData.getIsIllegality() == null ? false : examRecordData.getIsIllegality());
+        data.setLastActiveTime(examRecordData.getLastActiveTime());
         data.setUsedExamTime(examRecordData.getUsedExamTime());
-        data.setContinued(examRecordData.getIsContinued() == null ? false : examRecordData.getIsContinued());
+
         data.setContinuedCount(examRecordData.getContinuedCount());
-        data.setExceed(examRecordData.getIsExceed() == null ? false : examRecordData.getIsExceed());
+        data.setContinuedTime(examRecordData.getContinuedTime());
+        data.setContinued(examRecordData.getIsContinued() != null ? examRecordData.getIsContinued() : false);
+        data.setExceed(examRecordData.getIsExceed() != null ? examRecordData.getIsExceed() : false);
+
+        data.setExamRecordStatus(examRecordData.getExamRecordStatus() != null ? examRecordData.getExamRecordStatus().toString() : null);
+        data.setWarn(examRecordData.getIsWarn() != null ? examRecordData.getIsWarn() : false);
+        data.setAudit(examRecordData.getIsAudit() != null ? examRecordData.getIsAudit() : false);
+        data.setIllegality(examRecordData.getIsIllegality() != null ? examRecordData.getIsIllegality() : false);
+
+        data.setFaceVerifyResult(examRecordData.getFaceVerifyResult() != null ? examRecordData.getFaceVerifyResult().toString() : null);
+        data.setBaiduFaceLivenessSuccessPercent(examRecordData.getBaiduFaceLivenessSuccessPercent());
+        data.setFaceTotalCount(examRecordData.getFaceTotalCount());
         data.setFaceSuccessCount(examRecordData.getFaceSuccessCount());
         data.setFaceFailedCount(examRecordData.getFaceFailedCount());
         data.setFaceStrangerCount(examRecordData.getFaceStrangerCount());
-        data.setFaceTotalCount(examRecordData.getFaceTotalCount());
         data.setFaceSuccessPercent(examRecordData.getFaceSuccessPercent());
-        data.setFaceVerifyResult(examRecordData.getFaceVerifyResult() == null ? null : examRecordData.getFaceVerifyResult().toString());
-        data.setBaiduFaceLivenessSuccessPercent(examRecordData.getBaiduFaceLivenessSuccessPercent());
+
         data.setTotalScore(examRecordData.getTotalScore());
         data.setObjectiveScore(examRecordData.getObjectiveScore());
         data.setObjectiveAccuracy(examRecordData.getObjectiveAccuracy());
@@ -2182,6 +2199,8 @@ public class ExamControlServiceImpl implements ExamControlService {
         data.setSuccPercent(examRecordData.getSuccPercent());
         data.setAllObjectivePaper(examRecordData.getIsAllObjectivePaper());
 
+        data.setSwitchScreenCount(examRecordData.getSwitchScreenCount());
+        data.setExceedMaxSwitchScreenCount(examRecordData.getExceedMaxSwitchScreenCount());
         return data;
     }
 

+ 21 - 9
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamFaceLiveVerifyServiceImpl.java

@@ -67,11 +67,17 @@ public class ExamFaceLiveVerifyServiceImpl implements ExamFaceLiveVerifyService
             throw new StatusException("考试会话已过期");
         }
 
-        int times = 1;
+        int times = 0;
+        boolean hasSuccess = false;
         Long faceLiveVerifyId = null;
+
         List<ExamFaceLiveVerifyEntity> entities = examFaceLiveVerifyRepo.findByExamRecordDataId(examRecordData.getId());
         if (!CollectionUtils.isEmpty(entities)) {
             for (ExamFaceLiveVerifyEntity entity : entities) {
+                if (FaceLiveVerifyStatus.SUCCESS == entity.getStatus()) {
+                    hasSuccess = true;
+                }
+
                 if (entity.getFinished()) {
                     times++;
                 } else {
@@ -80,14 +86,21 @@ public class ExamFaceLiveVerifyServiceImpl implements ExamFaceLiveVerifyService
             }
         }
 
-        if (times > 2) {
-            // 返回空对象供前端判断
+        FaceLiveVerifyInfo info = new FaceLiveVerifyInfo();
+        info.setTimes(times);
+        info.setHasSuccess(hasSuccess);
+
+        if (times > 1) {
             log.warn("活检次数已超过2次!examRecordDataId = {}, studentId = {}", examRecordDataId, studentId);
-            FaceLiveVerifyInfo info = new FaceLiveVerifyInfo();
-            info.setTimes(times);
             return info;
         }
 
+        if (hasSuccess) {
+            log.warn("活检已检测成功过!examRecordDataId = {}, studentId = {}", examRecordDataId, studentId);
+            return info;
+        }
+
+        // 如果已经人脸检测过一次且未成功,再安排一次检测
         if (faceLiveVerifyId == null) {
             ExamFaceLiveVerifyEntity entity = new ExamFaceLiveVerifyEntity();
             entity.setExamRecordDataId(examRecordDataId);
@@ -95,14 +108,13 @@ public class ExamFaceLiveVerifyServiceImpl implements ExamFaceLiveVerifyService
             entity.setStatus(FaceLiveVerifyStatus.ERROR);
             examFaceLiveVerifyRepo.save(entity);
             faceLiveVerifyId = entity.getId();
+            times++;
         }
+        info.setFaceLiveVerifyId(faceLiveVerifyId);
+        info.setTimes(times);
 
         Integer startMinute = this.calculateStartFaceVerifyMinute(examRecordDataId);
-
-        FaceLiveVerifyInfo info = new FaceLiveVerifyInfo();
-        info.setFaceLiveVerifyId(faceLiveVerifyId);
         info.setStartMinute(startMinute);
-        info.setTimes(times);
         return info;
     }
 

+ 138 - 100
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordDataServiceImpl.java

@@ -55,6 +55,7 @@ import org.springframework.util.CollectionUtils;
 import java.math.BigDecimal;
 import java.text.DecimalFormat;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -142,8 +143,9 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
             }
         }
 
-        ExamRecordData bean = of(examRecordData);
+        ExamRecordData bean = ofExamRecordData(examRecordData);
         bean.setExceedMaxSwitchScreenCount(false);
+
         //存入redis
         saveExamRecordDataCache(examRecordData.getId(), bean);
 
@@ -169,50 +171,61 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
         redisClient.delete(key);
     }
 
-    private ExamRecordData of(ExamRecordDataEntity et) {
+    private ExamRecordData ofExamRecordData(ExamRecordDataEntity entity) {
         ExamRecordData bean = new ExamRecordData();
-        bean.setId(et.getId());
-        bean.setExamId(et.getExamId());
-        bean.setExamType(et.getExamType());
-
-        bean.setExamStudentId(et.getExamStudentId());
-        bean.setStudentId(et.getStudentId());
-        bean.setCourseId(et.getCourseId());
-        bean.setOrgId(et.getOrgId());
-        bean.setRootOrgId(et.getRootOrgId());
-
-        bean.setBasePaperId(et.getBasePaperId());
-
-        bean.setPaperType(et.getPaperType());
-
-        bean.setStartTime(et.getStartTime());
-        bean.setEndTime(et.getEndTime());
-        bean.setUsedExamTime(et.getUsedExamTime());
-        bean.setIsContinued(et.getIsContinued());
-        bean.setContinuedCount(et.getContinuedCount());
-        bean.setIsExceed(et.getIsExceed());
-        bean.setFaceSuccessCount(et.getFaceSuccessCount());
-        bean.setFaceFailedCount(et.getFaceFailedCount());
-        bean.setFaceStrangerCount(et.getFaceStrangerCount());
-        bean.setFaceTotalCount(et.getFaceTotalCount());
-        bean.setExamRecordStatus(et.getExamRecordStatus());
-        bean.setHandInExamType(et.getHandInExamType());
-        bean.setFaceVerifyResult(et.getFaceVerifyResult());
-        bean.setCleanTime(et.getCleanTime());
-        bean.setIsWarn(et.getIsWarn());
-        bean.setIsAudit(et.getIsAudit());
-        bean.setIsIllegality(et.getIsIllegality());
-        bean.setTotalScore(et.getTotalScore());
-        bean.setObjectiveScore(et.getObjectiveScore());
-        bean.setObjectiveAccuracy(et.getObjectiveAccuracy());
-        bean.setSubjectiveScore(et.getSubjectiveScore());
-        bean.setSuccPercent(et.getSuccPercent());
-        bean.setSyncStatus(et.getSyncStatus());
-        bean.setQuestionCount(et.getQuestionCount());
-        bean.setIsAllObjectivePaper(et.getIsAllObjectivePaper());
-        bean.setEnterExamTime(et.getEnterExamTime());
-        bean.setExamStageId(et.getExamStageId());
-        bean.setExamStageOrder(et.getExamStageOrder());
+
+        bean.setId(entity.getId());
+        bean.setRootOrgId(entity.getRootOrgId());
+        bean.setOrgId(entity.getOrgId());
+        bean.setCourseId(entity.getCourseId());
+        bean.setStudentId(entity.getStudentId());
+        bean.setExamStudentId(entity.getExamStudentId());
+
+        bean.setExamId(entity.getExamId());
+        bean.setExamType(entity.getExamType());
+        bean.setExamStageId(entity.getExamStageId());
+        bean.setExamStageOrder(entity.getExamStageOrder());
+        bean.setBasePaperId(entity.getBasePaperId());
+        bean.setPaperType(entity.getPaperType());
+
+        bean.setStartTime(entity.getStartTime());
+        bean.setEndTime(entity.getEndTime());
+        bean.setEnterExamTime(entity.getEnterExamTime());
+        bean.setCleanTime(entity.getCleanTime());
+        bean.setUsedExamTime(entity.getUsedExamTime());
+        // bean.setLastActiveTime(entity.getLastActiveTime());
+
+        bean.setIsContinued(entity.getIsContinued());
+        bean.setIsExceed(entity.getIsExceed());
+        bean.setContinuedCount(entity.getContinuedCount());
+        // bean.setContinuedTime(entity.getContinuedTime());
+
+        bean.setExamRecordStatus(entity.getExamRecordStatus());
+        bean.setIsWarn(entity.getIsWarn());
+        bean.setIsAudit(entity.getIsAudit());
+        bean.setIsIllegality(entity.getIsIllegality());
+
+        bean.setFaceVerifyResult(entity.getFaceVerifyResult());
+        bean.setFaceTotalCount(entity.getFaceTotalCount());
+        bean.setFaceSuccessCount(entity.getFaceSuccessCount());
+        bean.setFaceFailedCount(entity.getFaceFailedCount());
+        bean.setFaceStrangerCount(entity.getFaceStrangerCount());
+        // bean.setFaceSuccessPercent(entity.getFaceSuccessPercent());
+        // bean.setBaiduFaceLivenessSuccessPercent(entity.getBaiduFaceLivenessSuccessPercent());
+
+        bean.setTotalScore(entity.getTotalScore());
+        bean.setObjectiveScore(entity.getObjectiveScore());
+        bean.setObjectiveAccuracy(entity.getObjectiveAccuracy());
+        bean.setSubjectiveScore(entity.getSubjectiveScore());
+        bean.setSuccPercent(entity.getSuccPercent());
+        bean.setIsAllObjectivePaper(entity.getIsAllObjectivePaper());
+
+        bean.setSyncStatus(entity.getSyncStatus());
+        bean.setQuestionCount(entity.getQuestionCount());
+        bean.setHandInExamType(entity.getHandInExamType());
+        // bean.setSwitchScreenCount(0);
+        // bean.setExceedMaxSwitchScreenCount(false);
+
         return bean;
     }
 
@@ -339,63 +352,73 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
         }
     }
 
+    /**
+     * 计算客观题总得分和答题正确数
+     */
     @Override
     public CalcExamScoreResp calcExamScore(CalcExamScoreReq req) {
+        long startTime = System.currentTimeMillis();
+        ExamRecordQuestions examRecordQuestions = examRecordQuestionsService.getExamRecordQuestions(req.getExamRecordDataId());
 
-        Long examRecordDataId = req.getExamRecordDataId();
-        ExamRecordQuestions examRecordQuestions = examRecordQuestionsService.getExamRecordQuestions(examRecordDataId);
         // 考生作答记录明细
-        List<ExamQuestion> examQuestionList = examRecordQuestions.getExamQuestions();
-
-        double studentObjectiveScoreTotal = 0d;//客观题总分
-        int questionSuccessNum = 0;//答题正确数
-        int objectiveQuestionsNum = 0;//客观题总数
-
-        //计算客观题总得分和答题正确数
-        for (int i = 0; i < examQuestionList.size(); i++) {
-            ExamQuestion examQuestion = examQuestionList.get(i);
+        List<ExamQuestion> examQuestions = examRecordQuestions.getExamQuestions();
 
+        for (ExamQuestion examQuestion : examQuestions) {
             if (QuestionTypeUtil.isObjectiveQuestion(examQuestion.getQuestionType())) {
-                objectiveQuestionsNum++;
-
-                //如果标准答案为空,则更新标准答案
+                // 如果客观题的标准答案为空,则更新标准答案
                 if (StringUtils.isEmpty(examQuestion.getCorrectAnswer())) {
-                    updateCorrectAnswer(examRecordDataId, examQuestion);
+                    this.updateCorrectAnswer(req.getExamRecordDataId(), examQuestions, examQuestion);
                 }
+            }
+        }
 
-                //如果学生作答正确,则客观分累加,答题数累加
-                if (examQuestion.getStudentAnswer() != null && examQuestion.getCorrectAnswer() != null
-                        // && QuestionOptionHelper.isEqualAnswer(examQuestion.getCorrectAnswer(), examQuestion.getStudentAnswer())) {
-                        && examQuestion.getStudentAnswer().equals(examQuestion.getCorrectAnswer())) {
+        double objectiveScore = 0d;//客观题总分
+        int totalObjective = 0;//客观题总数
+        int totalRightObjective = 0;//答题正确数
 
-                    double questionScore = examQuestion.getQuestionScore().doubleValue();
-                    BigDecimal bigDecimalQuestionScore = new BigDecimal(Double.toString(questionScore));
-                    BigDecimal bigDecimalObjectiveScoreTotal = new BigDecimal(Double.toString(studentObjectiveScoreTotal));
+        for (int i = 0; i < examQuestions.size(); i++) {
+            ExamQuestion examQuestion = examQuestions.get(i);
+            if (!QuestionTypeUtil.isObjectiveQuestion(examQuestion.getQuestionType())) {
+                // 跳过主观题
+                continue;
+            }
 
-                    studentObjectiveScoreTotal = bigDecimalQuestionScore.add(bigDecimalObjectiveScoreTotal).doubleValue();
-                    questionSuccessNum++;
-                }
+            totalObjective++;
+
+            // 如果学生作答正确,则客观分累加,答题数累加
+            if (examQuestion.getStudentAnswer() != null && examQuestion.getCorrectAnswer() != null
+                    // && QuestionOptionHelper.isEqualAnswer(examQuestion.getCorrectAnswer(), examQuestion.getStudentAnswer())) {
+                    && examQuestion.getStudentAnswer().equals(examQuestion.getCorrectAnswer())) {
+
+                double questionScore = examQuestion.getQuestionScore();
+                BigDecimal bigObjectiveScore = new BigDecimal(Double.toString(objectiveScore));
+                BigDecimal bigQuestionScore = new BigDecimal(Double.toString(questionScore));
+
+                objectiveScore = bigQuestionScore.add(bigObjectiveScore).doubleValue();
+                totalRightObjective++;
             }
         }
 
-        //计算客观题答题正确率
+        // 计算客观题答题正确率
         double objectiveAccuracy = 0;
-        if (questionSuccessNum > 0 && objectiveQuestionsNum > 0) {
-            objectiveAccuracy = Double.valueOf(new DecimalFormat("#.00").format(questionSuccessNum * 100D / objectiveQuestionsNum));
+        if (totalRightObjective > 0 && totalObjective > 0) {
+            objectiveAccuracy = Double.parseDouble(new DecimalFormat("#.00").format(totalRightObjective * 100D / totalObjective));
         }
 
         CalcExamScoreResp resp = new CalcExamScoreResp();
-        resp.setObjectiveScore(studentObjectiveScoreTotal);
+        resp.setObjectiveScore(objectiveScore);
         resp.setObjectiveAccuracy(objectiveAccuracy);
-        resp.setTotalScore(studentObjectiveScoreTotal);//交卷时,总分=客观分得分
+        //交卷时,总分=客观分得分
+        resp.setTotalScore(objectiveScore);
 
+        long cost = System.currentTimeMillis() - startTime;
+        log.warn("calcExamScore examRecordDataId:{}, totalQuestion:{}, totalObjective:{}, cost:{}ms",
+                req.getExamRecordDataId(), examQuestions.size(), totalObjective, cost);
         return resp;
     }
 
     /**
      * 更新部分考试记录数据
-     *
-     * @param req
      */
     @Transactional
     @Override
@@ -405,13 +428,15 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
         }
 
         if (StringUtils.isNotEmpty(req.getSyncStatus())) {
+            SyncStatus status;
+
             try {
-                SyncStatus.valueOf(req.getSyncStatus());
+                status = SyncStatus.valueOf(req.getSyncStatus());
             } catch (IllegalArgumentException e) {
                 throw new StatusException("300002", "同步状态值不正确");
             }
 
-            examRecordDataRepo.updateExamRecordSyncStatusById(req.getSyncStatus(), req.getId());
+            examRecordDataRepo.updateSyncStatusById(status, req.getId());
         }
     }
 
@@ -440,33 +465,45 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
 
     /**
      * 更新客观题答案
-     *
-     * @param examRecordDataId
-     * @param examQuestion
      */
-    private void updateCorrectAnswer(Long examRecordDataId, ExamQuestion examQuestion) {
-        //更新客观题答案
+    private void updateCorrectAnswer(Long examRecordDataId, List<ExamQuestion> examQuestions, ExamQuestion examQuestion) {
+        // 按题号由小到大提取相同题ID的小题列表(注:套题的子题ID相同)
+        List<ExamQuestion> questionUnits = examQuestions.stream().
+                filter(e -> e.getQuestionId().equals(examQuestion.getQuestionId())).
+                sorted(Comparator.comparingInt(ExamQuestion::getOrder))
+                .collect(Collectors.toList());
+
+        // 获取当前题的正确答案
         QuestionAnswerCacheBean questionAnswerCache = CacheHelper.getQuestionAnswer(examQuestion.getQuestionId());
-        List<String> rightAnswerList = questionAnswerCache.getRightAnswers();
+        List<String> rightAnswers = questionAnswerCache.getRightAnswers();
+
+        if (CollectionUtils.isEmpty(rightAnswers)) {
+            log.warn("updateCorrectAnswer rightAnswers is empty, questionId = {}", examQuestion.getQuestionId());
+            return;
+        }
 
-        ExamRecordQuestions examRecordQuestions = examRecordQuestionsService.getExamRecordQuestions(examRecordDataId);
-        List<ExamQuestion> examQuestionList = examRecordQuestions.getExamQuestions();
+        if (questionUnits.size() != rightAnswers.size()) {
+            log.warn("updateCorrectAnswer rightAnswers incorrect, questionId = {}", examQuestion.getQuestionId());
+            return;
+        }
 
-        //最小维度的小题单元集合
-        List<ExamQuestion> questionUnitList = examQuestionList.stream().
-                filter(p -> p.getQuestionId().equals(examQuestion.getQuestionId())).
-                sorted((o1, o2) -> o1.getOrder().intValue() - o2.getOrder().intValue()).collect(Collectors.toList());
+        // 循环保存所有小题单元
+        for (int i = 0; i < questionUnits.size(); i++) {
+            ExamQuestion curQuestion = questionUnits.get(i);
+            if (!QuestionTypeUtil.isObjectiveQuestion(curQuestion.getQuestionType())) {
+                // 跳过套题内主观题
+                continue;
+            }
 
-        //循环保存所有小题单元
-        for (int i = 0; i < questionUnitList.size(); i++) {
-            ExamQuestion curUnitQuestion = questionUnitList.get(i);
-            curUnitQuestion.setCorrectAnswer(rightAnswerList.get(i));
-            examRecordQuestionsService.saveExamQuestion(examRecordDataId, curUnitQuestion.getOrder(), curUnitQuestion);
+            String rightAnswer = rightAnswers.get(i);
 
-            //更新当前小题的正确答案
-            if (curUnitQuestion.getOrder().equals(examQuestion.getOrder())) {
-                examQuestion.setCorrectAnswer(rightAnswerList.get(i));
+            // 更新当前小题的正确答案
+            if (curQuestion.getOrder().equals(examQuestion.getOrder())) {
+                examQuestion.setCorrectAnswer(rightAnswer);
             }
+
+            curQuestion.setCorrectAnswer(rightAnswer);
+            examRecordQuestionsService.saveExamQuestion(examRecordDataId, curQuestion.getOrder(), curQuestion);
         }
     }
 
@@ -478,10 +515,11 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
         if (id == null) {
             throw new StatusException("3001", "id 不能为空");
         }
-        if (status == null) {
+        ExamRecordStatus examRecordStatus = ExamRecordStatus.getByName(status);
+        if (status == null || examRecordStatus == null) {
             throw new StatusException("3002", "status 不能为空");
         }
-        examRecordDataRepo.updateExamRecordStatusById(status, id);
+        examRecordDataRepo.updateExamRecordStatusById(examRecordStatus, new Date(), id);
     }
 
     @Override

+ 63 - 47
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordQuestionsServiceImpl.java

@@ -36,7 +36,6 @@ import cn.com.qmth.examcloud.support.cache.bean.QuestionCacheBean;
 import cn.com.qmth.examcloud.support.examing.*;
 import cn.com.qmth.examcloud.support.handler.QuestionBodyHandler;
 import cn.com.qmth.examcloud.support.redis.RedisKeyHelper;
-import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
 import cn.com.qmth.examcloud.web.redis.RedisClient;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
@@ -176,27 +175,40 @@ public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsServic
 
     @Override
     public ExamRecordQuestions getExamRecordQuestions(Long examRecordDataId) {
-        ExamRecordData ed = examRecordDataService.getExamRecordDataCache(examRecordDataId);
-        if (ed == null) {
-            throw new StatusException("1001", "考试信息未找到");
+        ExamRecordData examRecordDataCache = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+        if (examRecordDataCache == null) {
+            throw new StatusException("1001", "考试记录缓存信息不存在!" + examRecordDataId);
         }
-        Integer quesCount = ed.getQuestionCount();
-        ExamRecordQuestions erqs = new ExamRecordQuestions();
-        List<ExamQuestion> examQuestions = new ArrayList<ExamQuestion>();
-        erqs.setExamQuestions(examQuestions);
-        erqs.setExamRecordDataId(examRecordDataId);
+
+        ExamRecordQuestions result = new ExamRecordQuestions();
+        result.setExamRecordDataId(examRecordDataId);
+        List<ExamQuestion> examQuestions = new ArrayList<>();
+        result.setExamQuestions(examQuestions);
+
+        // 试卷题目数量
+        Integer quesCount = examRecordDataCache.getQuestionCount();
         for (int i = 1; i <= quesCount; i++) {
-            ExamQuestion eq = getExamQuestion(examRecordDataId, i);
-            if (eq.getIsInMongo()) {
-                Optional<ExamQuestionTempEntity> op = examRecordQuestionTempRepo.findById(eq.getExamQuestionTempId());
+            ExamQuestion questionCache = this.getExamQuestion(examRecordDataId, i);
+            if (questionCache == null) {
+                log.warn("ExamQuestion cache not exist, examRecordDataId:{}, order:{}", examRecordDataId, i);
+                throw new StatusException("1002", "试题作答记录不存在!");
+            }
+
+            if (questionCache.getIsInMongo()) {
+                // 若记录存在mongo中,需要从mongo中获取(考生作答内容过长时)
+                Optional<ExamQuestionTempEntity> op = examRecordQuestionTempRepo.findById(questionCache.getExamQuestionTempId());
                 if (!op.isPresent()) {
-                    throw new StatusException("1002", "试题内容未找到");
+                    throw new StatusException("1002", "试题作答信息不存在!" + questionCache.getExamQuestionTempId());
                 }
-                copyFromTemp(eq, op.get());
+
+                this.copyExamQuestionInfo(questionCache, op.get());
+                log.warn("ExamQuestion data in mongodb, examQuestionTempId:{}", questionCache.getExamQuestionTempId());
             }
-            examQuestions.add(eq);
+
+            examQuestions.add(questionCache);
         }
-        return erqs;
+
+        return result;
     }
 
     @Override
@@ -309,22 +321,26 @@ public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsServic
         for (ExamStudentQuestionInfo examQuestionInfo : examQuestionInfos) {
             ExamQuestion eq = getExamQuestion(examRecordDataId, examQuestionInfo.getOrder());
             if (eq == null) {
-                throw new StatusException("2002", "试题不存在");
+                log.warn("ExamQuestion cache not exist, examRecordDataId:{}, order:{}", examRecordDataId, examQuestionInfo.getOrder());
+                throw new StatusException("2002", "试题作答记录不存在!");
             }
+
             eq.setStudentAnswer(examQuestionInfo.getStudentAnswer());
             eq.setIsSign(examQuestionInfo.getIsSign());
             eq.setIsAnswer(StringUtils.isNotBlank(eq.getStudentAnswer()));
             eq.setAudioPlayTimes(examQuestionInfo.getAudioPlayTimes());
+
             //过长存入mongo
             if (eq.getIsAnswer() && eq.getStudentAnswer().length() > ANWSER_LENGT) {
                 eq.setIsInMongo(true);
-                ExamQuestionTempEntity temp = of(eq);
+                ExamQuestionTempEntity temp = ofExamQuestionTempEntity(eq);
                 examRecordQuestionTempRepo.save(temp);
                 eq.setExamQuestionTempId(temp.getId());
                 eq.setStudentAnswer(null);
             } else {
                 eq.setIsInMongo(false);
             }
+
             saveExamQuestion(examRecordDataId, examQuestionInfo.getOrder(), eq);
         }
     }
@@ -363,9 +379,8 @@ public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsServic
     @Override
     public GetExamRecordQuestionsResp getExamRecordQuestions(GetExamRecordQuestionsReq req) {
         ExamRecordQuestions examRecordQuestions = this.getExamRecordQuestions(req.getExamRecordDataId());
-
         if (null == examRecordQuestions) {
-            throw new StatusException("200002", "找不到该考试的作答记录");
+            throw new StatusException("200002", "当前考试的作答记录不存在!");
         }
 
         GetExamRecordQuestionsResp resp = new GetExamRecordQuestionsResp();
@@ -387,17 +402,19 @@ public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsServic
             bean.setQuestionType(examQuestion.getQuestionType());
             bean.setCorrectAnswer(examQuestion.getCorrectAnswer());
 
-            //如果作答记录存在mongo中,需要从mongo中重新获取
-            if (null != examQuestion.getIsInMongo() && examQuestion.getIsInMongo()) {
-                ExamQuestionTempEntity examQuestionTempEntity =
-                        GlobalHelper.getEntity(examRecordQuestionTempRepo, examQuestion.getExamQuestionTempId(), ExamQuestionTempEntity.class);
-                if (null == examQuestionTempEntity) {
-                    throw new StatusException("200003", "找不到试题作答结果");
-                }
-
-                bean.setStudentAnswer(examQuestionTempEntity.getStudentAnswer());
-            } else {
+            if (StringUtils.isNotBlank(examQuestion.getStudentAnswer())) {
                 bean.setStudentAnswer(examQuestion.getStudentAnswer());
+            } else {
+                // 作答内容为空时,若记录存在mongo中,需要从mongo中重新获取(考生作答内容过长时)
+                if (null != examQuestion.getIsInMongo() && examQuestion.getIsInMongo()) {
+                    Optional<ExamQuestionTempEntity> op = examRecordQuestionTempRepo.findById(examQuestion.getExamQuestionTempId());
+                    if (!op.isPresent()) {
+                        throw new StatusException("200003", "试题作答结果不存在!" + examQuestion.getExamQuestionTempId());
+                    }
+
+                    bean.setStudentAnswer(op.get().getStudentAnswer());
+                    log.warn("ExamQuestion studentAnswer in mongodb, examQuestionTempId:{}", examQuestion.getExamQuestionTempId());
+                }
             }
 
             bean.setStudentScore(examQuestion.getStudentScore());
@@ -411,11 +428,10 @@ public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsServic
         }
 
         resp.setExamQuestions(beanList);
-
         return resp;
     }
 
-    private ExamQuestionTempEntity of(ExamQuestion eq) {
+    private ExamQuestionTempEntity ofExamQuestionTempEntity(ExamQuestion eq) {
         ExamQuestionTempEntity tem = new ExamQuestionTempEntity();
         tem.setExamRecordDataId(eq.getExamRecordDataId());
         tem.setMainNumber(eq.getMainNumber());
@@ -434,21 +450,21 @@ public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsServic
         return tem;
     }
 
-    private void copyFromTemp(ExamQuestion eq, ExamQuestionTempEntity temp) {
-        eq.setExamRecordDataId(temp.getExamRecordDataId());
-        eq.setMainNumber(temp.getMainNumber());
-        eq.setQuestionId(temp.getQuestionId());
-        eq.setOrder(temp.getOrder());
-        eq.setQuestionScore(temp.getQuestionScore());
-        eq.setQuestionType(temp.getQuestionType());
-        eq.setCorrectAnswer(temp.getCorrectAnswer());
-        eq.setStudentAnswer(temp.getStudentAnswer());
-        eq.setStudentScore(temp.getStudentScore());
-        eq.setIsAnswer(temp.getIsAnswer());
-        eq.setIsSign(temp.getIsSign());
-        eq.setOptionPermutation(temp.getOptionPermutation());
-        eq.setAudioPlayTimes(temp.getAudioPlayTimes());
-        eq.setAnswerType(temp.getAnswerType());
+    private void copyExamQuestionInfo(ExamQuestion question, ExamQuestionTempEntity temp) {
+        question.setExamRecordDataId(temp.getExamRecordDataId());
+        question.setMainNumber(temp.getMainNumber());
+        question.setQuestionId(temp.getQuestionId());
+        question.setOrder(temp.getOrder());
+        question.setQuestionScore(temp.getQuestionScore());
+        question.setQuestionType(temp.getQuestionType());
+        question.setCorrectAnswer(temp.getCorrectAnswer());
+        question.setStudentAnswer(temp.getStudentAnswer());
+        question.setStudentScore(temp.getStudentScore());
+        question.setIsAnswer(temp.getIsAnswer());
+        question.setIsSign(temp.getIsSign());
+        question.setOptionPermutation(temp.getOptionPermutation());
+        question.setAudioPlayTimes(temp.getAudioPlayTimes());
+        question.setAnswerType(temp.getAnswerType());
     }
 
 }

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

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