WANG 5 jaren geleden
bovenliggende
commit
bedc3c1290
87 gewijzigde bestanden met toevoegingen van 9360 en 0 verwijderingen
  1. 367 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/ExamControlController.java
  2. 175 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/ExamFaceLivenessVerifyController.java
  3. 99 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/ExamQuestionController.java
  4. 38 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/ExamRecordPaperStructController.java
  5. 40 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/ExamScoreController.java
  6. 76 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/ExamSmsController.java
  7. 154 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/OfflineExamController.java
  8. 73 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/PracticeController.java
  9. 60 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/TestController.java
  10. 31 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/bean/ExamProcessResultDomain.java
  11. 61 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/bean/GetUpyunSignDomain.java
  12. 35 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/bean/GetUpyunSignDomainQuery.java
  13. 201 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/ExamRecordCloudServiceProvider.java
  14. 37 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/ExamRecordPaperStructProvider.java
  15. 54 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/OeExamRecordForMarkingCloudServiceProvider.java
  16. 113 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/OeExamScoreNoticeQueueCloudServiceProvider.java
  17. 84 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/OeExamScoreObtainQueueCloudServiceProvider.java
  18. 39 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/OeHandleByExamCaptureQueueFailedDisposeServiceProvider.java
  19. 40 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/CalculateFaceCheckResultInfo.java
  20. 89 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/CheckExamInProgressInfo.java
  21. 90 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/CheckQrCodeInfo.java
  22. 54 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/CleanExamRecordPriorityQueueInfo.java
  23. 60 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/EndExamInfo.java
  24. 63 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/EndExamPreInfo.java
  25. 27 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/ExamQuestionAnswerExtensionInfo.java
  26. 171 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/ExamSessionInfo.java
  27. 296 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/ExamStudentInfo.java
  28. 88 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/ExamStudentQuestionInfo.java
  29. 42 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/GetFaceVerifyTokenInfo.java
  30. 64 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/GetQrCodeReq.java
  31. 22 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/GetUploadedFileAcknowledgeStatusReq.java
  32. 24 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/GetUploadedFileAnswerListReq.java
  33. 61 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/GetUpyunSignatureReq.java
  34. 125 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/ObjectiveScoreInfo.java
  35. 202 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/OfflineExamCourseInfo.java
  36. 143 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/PaperStructInfo.java
  37. 164 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/PracticeCourseInfo.java
  38. 73 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/PracticeDetailInfo.java
  39. 135 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/PracticeRecordInfo.java
  40. 63 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/SaveUploadedFileAcknowledgeStatusReq.java
  41. 61 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/SaveUploadedFileReq.java
  42. 81 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/StartExamInfo.java
  43. 52 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/UploadedFileAnswerInfo.java
  44. 53 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/UpyunSignatureInfo.java
  45. 23 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamAuditService.java
  46. 100 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamCacheTransferHelper.java
  47. 148 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamControlService.java
  48. 72 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamFaceLivenessVerifyService.java
  49. 116 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamRecordDataService.java
  50. 29 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamRecordForMarkingService.java
  51. 22 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamRecordPaperStructService.java
  52. 52 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamRecordQuestionsService.java
  53. 28 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamRecordService.java
  54. 43 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamScoreService.java
  55. 33 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamSessionInfoService.java
  56. 38 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamStudentService.java
  57. 42 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/OfflineExamService.java
  58. 38 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/PracticeService.java
  59. 57 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/CleanExamRecordQueueHolder.java
  60. 99 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamAuditServiceImpl.java
  61. 1571 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamControlServiceImpl.java
  62. 332 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamFaceLivenessVerifyServiceImpl.java
  63. 439 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordDataServiceImpl.java
  64. 128 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordForMarkingServiceImpl.java
  65. 39 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordPaperStructServiceImpl.java
  66. 212 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordQuestionsServiceImpl.java
  67. 56 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordServiceImpl.java
  68. 259 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamScoreServiceImpl.java
  69. 46 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamSessionInfoServiceImpl.java
  70. 214 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamStudentServiceImpl.java
  71. 263 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/OfflineExamServiceImpl.java
  72. 267 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/PracticeServiceImpl.java
  73. 30 0
      examcloud-core-oe-student-starter/assembly.xml
  74. 0 0
      examcloud-core-oe-student-starter/shell/start.args
  75. 36 0
      examcloud-core-oe-student-starter/shell/start.sh
  76. 1 0
      examcloud-core-oe-student-starter/shell/start.vmoptions
  77. 18 0
      examcloud-core-oe-student-starter/shell/stop.sh
  78. 86 0
      examcloud-core-oe-student-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/starter/CoreOeApp.java
  79. 129 0
      examcloud-core-oe-student-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/starter/config/ExamCloudResourceManager.java
  80. 53 0
      examcloud-core-oe-student-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/starter/config/ExamCloudWebMvcConfigurer.java
  81. 44 0
      examcloud-core-oe-student-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/starter/config/SwaggerConfig.java
  82. 7 0
      examcloud-core-oe-student-starter/src/main/resources/application.properties
  83. 0 0
      examcloud-core-oe-student-starter/src/main/resources/limited.properties
  84. 89 0
      examcloud-core-oe-student-starter/src/main/resources/log4j2.xml
  85. 11 0
      examcloud-core-oe-student-starter/src/main/resources/security-exclusions.conf
  86. 0 0
      examcloud-core-oe-student-starter/src/main/resources/security.properties
  87. 10 0
      examcloud-core-oe-student-starter/src/main/resources/upyun.xml

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

@@ -0,0 +1,367 @@
+package cn.com.qmth.examcloud.core.oe.student.controller;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.basic.api.StudentCloudService;
+import cn.com.qmth.examcloud.core.basic.api.bean.StudentBean;
+import cn.com.qmth.examcloud.core.basic.api.request.GetStudentReq;
+import cn.com.qmth.examcloud.core.basic.api.response.GetStudentResp;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamFileAnswerTempEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamProperties;
+import cn.com.qmth.examcloud.core.oe.common.enums.HandInExamType;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamCaptureQueueRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordDataRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordRepo;
+import cn.com.qmth.examcloud.core.oe.common.service.ExamScoreNoticeQueueService;
+import cn.com.qmth.examcloud.core.oe.common.service.GainBaseDataService;
+import cn.com.qmth.examcloud.core.oe.student.api.request.GetStudentOnlineExamInfoReq;
+import cn.com.qmth.examcloud.core.oe.student.api.response.GetStudentOnlineExamInfoResp;
+import cn.com.qmth.examcloud.core.oe.student.bean.*;
+import cn.com.qmth.examcloud.core.oe.student.controller.bean.ExamProcessResultDomain;
+import cn.com.qmth.examcloud.core.oe.student.controller.bean.GetUpyunSignDomain;
+import cn.com.qmth.examcloud.core.oe.student.controller.bean.GetUpyunSignDomainQuery;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamControlService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamStudentService;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+import cn.com.qmth.examcloud.web.helpers.SequenceLockHelper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import cn.com.qmth.examcloud.web.support.Naked;
+import cn.com.qmth.examcloud.web.upyun.UpYunHttpRequest;
+import cn.com.qmth.examcloud.web.upyun.UpyunPathEnvironmentInfo;
+import cn.com.qmth.examcloud.web.upyun.UpyunService;
+import com.mysql.cj.util.StringUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author chenken
+ * @date 2018年8月13日 下午2:28:08
+ * @company QMTH
+ * @description 在线考试控制Controller
+ */
+@Api(tags = "在线考试控制")
+@RestController
+@RequestMapping("${app.api.oe.student}/examControl")
+public class ExamControlController extends ControllerSupport {
+    //抓拍照片的又拍云id
+    private static final String CAPTURE_PHOTO_UPYUN_SITEID = "capturePhoto";
+    @Autowired
+    StudentCloudService studentCloudService;
+    @Autowired
+    ExamRecordDataRepo examRecordDataRepo;
+    @Autowired
+    private ExamStudentService examStudentService;
+    @Autowired
+    private ExamControlService examControlService;
+    @Autowired
+    private ExamScoreNoticeQueueService examScoreNoticeQueueService;
+    @Autowired
+    private GainBaseDataService gainBaseDataService;
+    @Autowired
+    UpyunService upyunService;
+    @Value("${$upyun.site.1.domain}")
+    private String upyunFileUrl;
+    @Autowired
+    RedisClient redisClient;
+    @Autowired
+    private ExamCaptureQueueRepo examCaptureQueueRepo;
+    @Autowired
+    private ExamRecordRepo examRecordRepo;
+    @Autowired
+    ExamRecordDataService examRecordDataService;
+
+    /**
+     * 获取当前服务器时间
+     *
+     * @return
+     */
+    @ApiOperation(value = "获取当前服务器时间")
+    @GetMapping("/currentTime")
+    public Long getCurrentTime() {
+        return new Date().getTime();
+    }
+
+    /**
+     * 获取在线考试待考列表
+     *
+     * @return
+     */
+    @ApiOperation(value = "获取在线考试待考列表")
+    @GetMapping("/queryExamList")
+    public List<ExamStudentInfo> queryExamList() {
+        User user = getAccessUser();
+        return examStudentService.queryOnlineExamList(user.getUserId());
+    }
+
+    /**
+     * 开始考试
+     */
+    @ApiOperation(value = "开始考试")
+    @GetMapping("/startExam")
+    public StartExamInfo startExam(@RequestParam Long examStudentId) {
+        User user = getAccessUser();
+        String sequenceLockKey = Constants.START_EXAM_LOCK_PREFIX + user.getUserId();
+        StartExamInfo startExamInfo;
+        //开始考试上锁,分布式锁,系统在请求结束后会,自动释放锁,无需手动解锁
+        SequenceLockHelper.getLock(sequenceLockKey);
+        Check.isNull(examStudentId, "examStudentId不能为空");
+
+        startExamInfo = examControlService.startExam(examStudentId, user);
+        return startExamInfo;
+    }
+
+    /**
+     * 结束考试:交卷..
+     */
+    @ApiOperation(value = "结束考试:交卷")
+    @GetMapping("/endExam")
+    public void endExam() {
+        User user = getAccessUser();
+        Long studentId = user.getUserId();
+        String sequenceLockKey = Constants.END_EXAM_LOCK_PREFIX + studentId;
+        //系统在请求结束后会,自动释放锁,无需手动解锁
+        SequenceLockHelper.getLock(sequenceLockKey);
+
+        long st = System.currentTimeMillis();
+        long startTime = System.currentTimeMillis();
+        EndExamPreInfo endExamPreInfo = examControlService.endExamPre(studentId);
+        if (endExamPreInfo == null) {
+            throw new StatusException("oestudent-100100", "初始化交卷信息失败");
+        }
+        if (log.isDebugEnabled()) {
+            log.debug("0 [END_EXAM] 交卷前处理耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+        examControlService.handInExam(endExamPreInfo.getExamRecordData(), HandInExamType.MANUAL);
+        if (log.isDebugEnabled()) {
+            log.debug("1 [END_EXAM]合计 耗时:" + (System.currentTimeMillis() - st) + " ms");
+        }
+    }
+
+    /**
+     * 交卷后续处理
+     */
+    @ApiOperation(value = "交卷后续处理")
+    @GetMapping("/processAfterEndExam")
+    public ExamProcessResultDomain processAfterEndExam(@RequestParam Long examRecordDataId) {
+        User user = getAccessUser();
+        ExamProcessResultDomain res = new ExamProcessResultDomain();
+        ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, examRecordDataId,
+                ExamRecordDataEntity.class);
+        ExamRecordEntity examRecord = GlobalHelper.getEntity(examRecordRepo, examRecordData.getExamRecordId(), ExamRecordEntity.class);
+        try {
+            examControlService.processAfterHandInExam(examRecordDataId, examRecord.getStudentId(), HandInExamType.MANUAL);
+        } catch (StatusException e) {
+            if (e.getCode().equals(Constants.CAPTURE_PROCESSING_STATUS_CODE)) {
+                res.setCode(Constants.PROCESSING_EXAM_RECORD_CODE);
+                return res;
+            }
+            throw e;
+        } catch (Exception e) {
+            throw e;
+        }
+
+
+        //根据相关条件添加推分队列
+        Long examId = examRecord.getExamId();
+        String isPushScore = CacheHelper.getExamOrgProperty(examId, examRecord.getOrgId(),
+                ExamProperties.PUSH_SCORE.name()).getValue();
+        if (isPushScore != null && Constants.isTrue.equals(isPushScore)) {
+            //保存分数通知队列(只有考试设置中,配置为允许推分的才发送通知)
+            boolean isExistNoticeQueue = examScoreNoticeQueueService.isExistExamScoreNoticeQueue(user.getRootOrgId());
+            //如果当前组织机构的通知队列数据不存在则保存至队列,否则不作任何处理
+            if (!isExistNoticeQueue) {
+                examScoreNoticeQueueService.addExamScoreNoticeQueue(user.getRootOrgId());
+            }
+        }
+        res.setCode(Constants.COMMON_SUCCESS_CODE);
+        return res;
+    }
+
+    @ApiOperation(value = "获取考试记录信息")
+    @GetMapping("/getEndExamInfo")
+    public EndExamInfo getEndExamInfo(@RequestParam Long examRecordDataId) {
+        return examControlService.getEndExamInfo(examRecordDataId);
+    }
+
+    /**
+     * 考试心跳
+     *
+     * @return 剩余时间
+     */
+    @ApiOperation(value = "考试心跳")
+    @GetMapping("/examHeartbeat")
+    public Long examHeartbeat() {
+        User user = getAccessUser();
+        return examControlService.examHeartbeat(user.getUserId());
+    }
+
+    /**
+     * 断点续考:检查正在进行中的考试
+     */
+    @ApiOperation(value = "断点续考:检查正在进行中的考试")
+    @GetMapping("/checkExamInProgress")
+    public ExamProcessResultDomain checkExamInProgress() {
+        User user = getAccessUser();
+        String sequenceLockKey = Constants.START_EXAM_LOCK_PREFIX + user.getUserId();
+        //系统在请求结束后会,自动释放锁,无需手动解锁
+        SequenceLockHelper.getLock(sequenceLockKey);
+        ExamProcessResultDomain res = new ExamProcessResultDomain();
+        try {
+            CheckExamInProgressInfo info = examControlService.checkExamInProgress(user.getUserId());
+            res.setCode(Constants.COMMON_SUCCESS_CODE);
+            res.setData(info);
+            return res;
+        } catch (StatusException e) {
+            if (e.getCode().equals(Constants.EXAM_RECORD_NOT_END_STATUS_CODE)) {
+                res.setCode(Constants.PROCESSING_EXAM_RECORD_CODE);
+                return res;
+            }
+            throw e;
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+    /**
+     * 获取又拍云文件上传签名(微信小程序调用)
+     */
+    @ApiOperation(value = "获取又拍云文件上传签名(微信小程序调用)")
+    @PostMapping("/upyunSignature")
+    public UpyunSignatureInfo getUpyunSignature(@ModelAttribute @Valid GetUpyunSignatureReq req) {
+        return examControlService.getUpyunSignature(req);
+    }
+
+    /**
+     * 校验二维码(微信小程序调用)
+     */
+    @Naked
+    @ApiOperation(value = "校验二维码(微信小程序调用)")
+    @PostMapping("/checkQrCode")
+    public CheckQrCodeInfo checkQrCode(@RequestParam(required = true) String qrCode) {
+        return examControlService.checkQrCode(qrCode);
+    }
+
+    /**
+     * 保存上传的文件(微信小程序调用)
+     */
+    @ApiOperation(value = "保存上传的文件(微信小程序调用)")
+    @PostMapping("/saveUploadedFile")
+    public Long saveUploadedFile(@ModelAttribute @Valid SaveUploadedFileReq req) {
+        User user = getAccessUser();
+        ExamFileAnswerTempEntity examFileAnswerTempEntity = examControlService.saveUploadedFile(req, user);
+        try {
+            String fileUrl = "";
+            if (req.getFilePath().indexOf(",") > -1) {
+                for (String url : req.getFilePath().split(",")) {
+                    fileUrl += upyunFileUrl + url + ",";
+                }
+                fileUrl = fileUrl.substring(0, fileUrl.length() - 1);
+            } else {
+                fileUrl = upyunFileUrl + req.getFilePath();
+            }
+            examControlService.sendFileAnswerToWebSocket(req.getExamRecordDataId(), req.getOrder(),
+                    fileUrl, req.getTransferFileType(), user.getUserId());
+        } catch (Exception e) {
+            examControlService.deleteExamFileAnswerTemp(req);
+            throw new StatusException("100009", "消息通知失败", e);
+        }
+        return examFileAnswerTempEntity.getId();
+    }
+
+    /**
+     * 查询客户端对上传的文件的响应状态(微信小程序调用)
+     */
+    @ApiOperation(value = "查询客户端对上传的文件的响应状态(微信小程序调用)")
+    @PostMapping("/getUploadedFileAcknowledgeStatus")
+    public String getUploadedFileAcknowledgeStatus(@ModelAttribute @Valid GetUploadedFileAcknowledgeStatusReq req) {
+        return examControlService.getUploadedFileAcknowledgeStatus(req);
+    }
+
+    /**
+     * 修改上传音频结果推送状态
+     */
+    @ApiOperation(value = "修改上传音频结果推送状态")
+    @PostMapping("/saveUploadedFileAcknowledgeStatus")
+    public void saveUploadedFileAcknowledgeStatus(@RequestBody @Valid SaveUploadedFileAcknowledgeStatusReq req) {
+        examControlService.saveUploadedFileAcknowledgeStatus(req);
+    }
+
+    @Naked
+    @ApiOperation(value = "获取学生是否有正在进行的网考信息")
+    @PostMapping("/getStudentOnLineExamInfo")
+    public GetStudentOnlineExamInfoResp getStudentOnLineExamInfo(@RequestBody GetStudentOnlineExamInfoReq req) {
+        if (null == req.getRootOrgId()) {
+            throw new StatusException("101001", "组织机构id参数不允许为空");
+        }
+        if (StringUtils.isNullOrEmpty(req.getStudentCode()) && StringUtils.isNullOrEmpty(req.getIdentityNumber())
+                && StringUtils.isNullOrEmpty(req.getPhone())) {
+            throw new StatusException("101002", "学生学号、身份证号或手机号参数至少有一个不为空");
+        }
+        GetStudentReq getStudentReq = new GetStudentReq();
+        getStudentReq.setRootOrgId(req.getRootOrgId());
+        if (!StringUtils.isNullOrEmpty(req.getStudentCode())) {
+            getStudentReq.setStudentCode(req.getStudentCode());
+        }
+        if (!StringUtils.isNullOrEmpty(req.getIdentityNumber())) {
+            getStudentReq.setIdentityNumber(req.getIdentityNumber());
+        }
+        if (!StringUtils.isNullOrEmpty(req.getPhone())) {
+            getStudentReq.setSecurityPhone(req.getPhone());
+        }
+        //此方法内部已作空处理,不需要再判断非空
+        GetStudentResp studentResp = studentCloudService.getStudent(getStudentReq);
+        StudentBean studentInfo = studentResp.getStudentInfo();
+        Long studentId = studentInfo.getId();
+        ExamRecordDataEntity examRecordData = examRecordDataRepo.findOnlineExamingRecordByStudentId(studentId);
+        GetStudentOnlineExamInfoResp resp = new GetStudentOnlineExamInfoResp();
+        if (null != examRecordData && null != examRecordData.getId()) {
+            resp.setExistExamingRecord(true);
+        } else {
+            resp.setExistExamingRecord(false);
+        }
+        return resp;
+    }
+
+    @ApiOperation(value = "获取抓拍照片的又拍云签名")
+    @GetMapping("/getCapturePhotoUpYunSign")
+    public GetUpyunSignDomain getCapturePhotoUpYunSign(GetUpyunSignDomainQuery query) {
+        String fileSuffix = query.getFileSuffix();
+        if (StringUtils.isNullOrEmpty(fileSuffix)) {
+            throw new StatusException("", "文件后缀名不允许为空");
+        }
+        fileSuffix = fileSuffix.indexOf(".") == -1 ? "." + fileSuffix : fileSuffix;
+
+        GetUpyunSignDomain result = new GetUpyunSignDomain();
+        User accessUser = this.getAccessUser();
+        String signIdentifier = String.valueOf(System.currentTimeMillis());
+        String upyunSignRedisKey = Constants.EXAM_CAPTURE_PHOTO_UPYUN_SIGN_PREFIX
+                + accessUser.getUserId() + "_" + signIdentifier;
+
+        UpyunPathEnvironmentInfo env = new UpyunPathEnvironmentInfo();
+        env.setRootOrgId(accessUser.getRootOrgId().toString());
+        env.setUserId(accessUser.getUserId().toString());
+        env.setFileSuffix(fileSuffix);
+        UpYunHttpRequest upYunHttpRequest = upyunService.buildUpYunHttpRequest(CAPTURE_PHOTO_UPYUN_SITEID, env, query.getFileMd5());
+        redisClient.set(upyunSignRedisKey, upYunHttpRequest, 60);
+        result.setAccessUrl(upYunHttpRequest.getAccessUrl());
+        result.setFormUrl(upYunHttpRequest.getFormUrl());
+        result.setFormParams(upYunHttpRequest.getFormParams());
+        result.setSignIdentifier(signIdentifier);
+        return result;
+    }
+
+}

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

@@ -0,0 +1,175 @@
+package cn.com.qmth.examcloud.core.oe.student.controller;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamFaceLivenessVerifyEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamFaceLivenessVerifyRepo;
+import cn.com.qmth.examcloud.core.oe.student.bean.GetFaceVerifyTokenInfo;
+import cn.com.qmth.examcloud.core.oe.student.controller.bean.ExamProcessResultDomain;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamFaceLivenessVerifyService;
+import cn.com.qmth.examcloud.core.oe.websocket.api.WebsocketCloudService;
+import cn.com.qmth.examcloud.core.oe.websocket.api.request.SendMessageReq;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import cn.com.qmth.examcloud.web.support.Naked;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月5日 下午6:28:34
+ * @company 	QMTH
+ * @description 活体检测controller
+ */
+@Api(tags = "活体检测")
+@RestController
+@RequestMapping("${app.api.oe.student}/examFaceLivenessVerify")
+public class ExamFaceLivenessVerifyController extends ControllerSupport{
+
+	@Autowired
+	private ExamFaceLivenessVerifyService examFaceLivenessVerifyService;
+	
+    @Autowired
+    private WebsocketCloudService websocketCloudService;
+	
+	@Autowired
+	private ExamFaceLivenessVerifyRepo examFaceLivenessVerifyRepo;
+	
+	
+	@ApiOperation(value = "检测学生底照是否能获取到faceId验证的token")
+	@GetMapping("/checkFaceLiveness")
+	public GetFaceVerifyTokenInfo checkFaceLiveness(){
+		User user = getAccessUser();
+		String bizNo = user.getUserId().toString();
+		return examFaceLivenessVerifyService.getFaceVerifyToken(user.getUserId(),bizNo);
+	} 
+	
+	/**
+	 * 获得一个faceid用于网页端活体检测的token
+	 * @param request
+	 * @param examRecordId
+	 * @return
+	 */
+	@ApiOperation(value = "获得一个faceid用于网页端活体检测的token")
+	@GetMapping("/getFaceLivenessVerifyToken/{examRecordDataId}")
+	public GetFaceVerifyTokenInfo getFaceVerifyToken(@PathVariable Long examRecordDataId){
+		Check.isNull(examRecordDataId, "examRecordDataId不能为空");
+		User user = getAccessUser();
+        ExamFaceLivenessVerifyEntity faceVerify = examFaceLivenessVerifyService.saveFaceVerifyByExamRecordDataId(examRecordDataId);
+        GetFaceVerifyTokenInfo getFaceVerifyTokenInfo = examFaceLivenessVerifyService.getFaceVerifyToken(user.getUserId(),faceVerify.getId().toString());
+        if(!getFaceVerifyTokenInfo.getSuccess()){
+        	faceVerify.setIsError(true);
+        	faceVerify.setErrorMsg(getFaceVerifyTokenInfo.getErrorMsg());
+			examFaceLivenessVerifyRepo.save(faceVerify);
+        }
+		return getFaceVerifyTokenInfo;
+	}
+	
+	@ApiOperation(value = "更新活体检测结果")
+	@GetMapping("/updateFaceLivenessVerify/{examRecordDataId}")
+	public void updateFaceVerify(@PathVariable Long examRecordDataId,@RequestParam String errorMsg){
+		Check.isNull(examRecordDataId, "examRecordDataId不能为空");
+		List<ExamFaceLivenessVerifyEntity> examFaceLivenessVerifyEntities = examFaceLivenessVerifyService.listFaceVerifyByExamRecordId(examRecordDataId);
+		if(examFaceLivenessVerifyEntities!=null && examFaceLivenessVerifyEntities.size()>0){
+			ExamFaceLivenessVerifyEntity examFaceLivenessVerifyEntity = examFaceLivenessVerifyEntities.get(0);
+			examFaceLivenessVerifyEntity.setIsError(true);
+			examFaceLivenessVerifyEntity.setErrorMsg(errorMsg);
+			examFaceLivenessVerifyRepo.save(examFaceLivenessVerifyEntity);
+		}
+	}
+	
+	/**
+	 * 人脸验证完成后的回调,由faceId调用
+	 * @param data
+	 * @throws Exception 
+	 */
+	@Naked
+	@ApiOperation(value = "人脸验证完成后的回调,由faceId调用")
+	@PostMapping("/faceLivenessVerifyCallback")
+	public void faceLivenessVerifyCallback(@RequestParam String data) throws Exception{
+		log.info("faceId回调,data="+data);
+		
+		JSONObject returnJsonObject = new JSONObject(data);
+		Long faceVerifyId = Long.parseLong(returnJsonObject.get("biz_no")+"");
+		ExamFaceLivenessVerifyEntity currentFaceVerify = examFaceLivenessVerifyService.findFaceVerifyById(faceVerifyId);
+		/**
+		 * 如果该检测记录结果已经非空了,直接返回,
+		 * 有可能超时程序已经将结果填成TIME_OUT了
+		 */
+		if(currentFaceVerify.getVerifyResult()!=null){
+			return;
+		}
+		
+		ExamFaceLivenessVerifyEntity faceVerify = examFaceLivenessVerifyService.faceIdNotify(data);
+		List<ExamFaceLivenessVerifyEntity> faceVerifies = examFaceLivenessVerifyService.listFaceVerifyByExamRecordId(faceVerify.getExamRecordDataId());
+		JSONObject jsonObject = new JSONObject();
+		//验证次数
+		jsonObject.put("verifyCount", faceVerifies.size());
+		//取最后一次验证结果
+		jsonObject.put("verifyResult", faceVerifies.get(faceVerifies.size()-1).getVerifyResult().name());
+		jsonObject.put("examRecordDataId",faceVerify.getExamRecordDataId());
+		
+		SendMessageReq sendMessageReq = new SendMessageReq();
+		sendMessageReq.setExamRecordDataId(faceVerify.getExamRecordDataId());
+		sendMessageReq.setReturnMsgJson(jsonObject.toString());
+		websocketCloudService.sendMessage(sendMessageReq);
+	}
+	
+	/**
+	 * 人脸检测超时处理
+	 * @param examRecordId
+	 * @return
+	 */
+	@ApiOperation(value = "人脸检测超时处理")
+	@GetMapping("/faceLivenessVerifyTimeOut/{examRecordDataId}")
+	public String faceTestTimeOut(@PathVariable Long examRecordDataId){
+		JSONObject jsonObject = new JSONObject();
+		examFaceLivenessVerifyService.faceTestTimeOut(examRecordDataId);
+		List<ExamFaceLivenessVerifyEntity> faceVerifies = examFaceLivenessVerifyService.listFaceVerifyByExamRecordId(examRecordDataId);
+		//验证次数
+		try {
+			jsonObject.put("verifyCount", faceVerifies.size());
+			//取最后一次验证结果
+			jsonObject.put("verifyResult", faceVerifies.get(faceVerifies.size()-1).getVerifyResult().name());
+			jsonObject.put("examRecordDataId",examRecordDataId);
+		} catch (JSONException e) {
+			e.printStackTrace();
+		}
+		
+		return jsonObject.toString();
+	}
+	
+	 /**
+     * 人脸检测结束处理
+     * @param examRecordId
+     * @throws Exception 
+     */
+	@ApiOperation(value = "人脸检测结束处理")
+	@GetMapping(value = "faceLivenessVerifyEnd/{examRecordDataId}")
+    public ExamProcessResultDomain faceTestEndHandle(@PathVariable Long examRecordDataId, @RequestParam String result) throws Exception{
+		ExamProcessResultDomain res = new ExamProcessResultDomain();
+		try {
+			examFaceLivenessVerifyService.faceTestEndHandle(examRecordDataId,result);
+			res.setCode(Constants.COMMON_SUCCESS_CODE);
+			return res;
+		} catch (StatusException e) {
+			if (e.getCode().equals(Constants.EXAM_RECORD_NOT_END_STATUS_CODE)) {
+				res.setCode(Constants.PROCESSING_EXAM_RECORD_CODE);
+				return res;
+			}
+			throw e;
+		} catch (Exception e) {
+			throw e;
+		}
+    }
+	
+}

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

@@ -0,0 +1,99 @@
+package cn.com.qmth.examcloud.core.oe.student.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.JsonUtil;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamQuestionEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordQuestionsEntity;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamSessionInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamStudentQuestionInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordQuestionsService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamSessionInfoService;
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionStructure;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author  	chenken
+ * @date    	2018年9月5日 下午4:57:05
+ * @company 	QMTH
+ * @description 考试作答记录controller
+ */
+@Api(tags = "考试过程中-试题相关接口")
+@RestController
+@RequestMapping("${app.api.oe.student}/examQuestion")
+public class ExamQuestionController extends ControllerSupport {
+	
+	@Autowired
+	private ExamRecordQuestionsService examRecordQuestionsService;
+	
+	@Autowired
+    private ExamSessionInfoService examSessionInfoService;
+
+	/**
+	 * 将mongodb中的答过的题和redis中的题目列表合并返回给前端
+	 * 返回给前端时注意将正确答案和得分置成null
+	 * @param examRecordDataId
+	 * @return
+	 */
+	@ApiOperation(value = "考试过程中-获取试题列表")
+	@GetMapping("/findExamQuestionList")
+	public List<ExamQuestionEntity> findExamQuestionList(){
+		User user = getAccessUser();
+		ExamSessionInfo examSessionInfo = examSessionInfoService.getExamSessionInfo(user.getUserId());
+		if (null== examSessionInfo){
+			throw new StatusException("500001","考试会话已过期,请重新开考");
+		}
+		ExamRecordQuestionsEntity examRecordQuestionsEntity = examRecordQuestionsService.
+				getExamRecordQuestionsAndFixExamRecordDataIfNecessary(examSessionInfo.getExamRecordDataId());
+
+		List<ExamQuestionEntity> examQuestionList = examRecordQuestionsEntity.getExamQuestionEntities();
+		for(ExamQuestionEntity examQuestion:examQuestionList){
+			examQuestion.setCorrectAnswer(null);
+			examQuestion.setStudentScore(null);
+		}
+		return examQuestionList;
+	}
+
+	/**
+	 * 获取试题内容
+	 * @param examStudentId
+	 * @param questionId
+	 * @return
+	 */
+	@ApiOperation(value = "考试过程中-获取试题内容")
+	@GetMapping("/getQuestionContent")
+	public DefaultQuestionStructure getQuestionContent(@RequestParam String questionId){
+		User user = getAccessUser();
+		Check.isBlank(questionId, "questionId不能为空");
+		return examRecordQuestionsService.getQuestionContent(user.getUserId(),questionId);
+	}
+	
+	/**
+	 * 考生作答
+	 * @param examQuestions
+	 */
+	@ApiOperation(value = "考试过程中-考生作答:更新试题作答信息(包括提交试题答案,更新是否标记)")
+	@PostMapping("/submitQuestionAnswer")
+	public void submitQuestionAnswer(@RequestBody List<ExamStudentQuestionInfo> examQuestionInfos){
+		if(log.isDebugEnabled()) {
+			String strJosn=JsonUtil.toJson(examQuestionInfos);
+			log.debug("ExamQuestionController--submitQuestionAnswer参数信息:"+strJosn);
+		}
+		User user = getAccessUser();
+		if(examQuestionInfos!=null && examQuestionInfos.size()>0){
+			for(ExamStudentQuestionInfo examStudentQuestionInfo:examQuestionInfos){
+				if(examStudentQuestionInfo.getOrder() == null){
+					throw new StatusException("examQuestionController-submitQuestionAnswer", "illegal params");
+				}
+			}
+			examRecordQuestionsService.submitQuestionAnswer(user.getUserId(),examQuestionInfos);
+		}
+	}
+}

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

@@ -0,0 +1,38 @@
+package cn.com.qmth.examcloud.core.oe.student.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordPaperStructEntity;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordPaperStructService;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月5日 下午4:02:15
+ * @company 	QMTH
+ * @description 考试记录-试卷结构controller
+ */
+@Api(tags = "考试记录-试卷结构")
+@RestController
+@RequestMapping("${app.api.oe.student}/examRecordPaperStruct")
+public class ExamRecordPaperStructController extends ControllerSupport{
+
+	@Autowired
+	private ExamRecordPaperStructService examRecordPaperStructService;
+	
+	@ApiOperation(value = "获取考试记录试卷结构")
+	@GetMapping("/getExamRecordPaperStruct")
+	public ExamRecordPaperStructEntity getExamRecordPaperStruct(@RequestParam Long examRecordDataId){
+		Check.isNull(examRecordDataId, "examRecordDataId不能为空");
+		return examRecordPaperStructService.getExamRecordPaperStruct(examRecordDataId);
+	}
+	
+}

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

@@ -0,0 +1,40 @@
+package cn.com.qmth.examcloud.core.oe.student.controller;
+
+import java.util.List;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.student.bean.ObjectiveScoreInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamScoreService;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年10月20日 下午3:33:26
+ * @company 	QMTH
+ * @description ExamScoreController.java
+ */
+@Api(tags = "分数相关接口")
+@RestController
+@RequestMapping("${app.api.oe.student}/examScore")
+public class ExamScoreController {
+	
+	@Autowired
+	private ExamScoreService examScoreService;
+	
+	@ApiOperation(value = "根据examStudentId获取客观分信息")
+	@GetMapping("/queryObjectiveScoreList")
+	public List<ObjectiveScoreInfo> queryObjectiveScoreList(@RequestParam Long examStudentId){
+		Check.isNull(examStudentId, "examStudentId 不能为空");
+		return examScoreService.queryObjectiveScoreList(examStudentId);
+	}
+	
+}

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

@@ -0,0 +1,76 @@
+package cn.com.qmth.examcloud.core.oe.student.controller;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.basic.api.StudentCloudService;
+import cn.com.qmth.examcloud.core.basic.api.request.GetStudentReq;
+import cn.com.qmth.examcloud.core.basic.api.response.GetStudentResp;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.CommonUtil;
+import cn.com.qmth.examcloud.exchange.inner.api.SmsCloudService;
+import cn.com.qmth.examcloud.exchange.inner.api.request.CheckSmsCodeReq;
+import cn.com.qmth.examcloud.exchange.inner.api.request.SendSmsCodeReq;
+import cn.com.qmth.examcloud.exchange.inner.api.response.CheckSmsCodeResp;
+import cn.com.qmth.examcloud.exchange.inner.api.response.SendSmsCodeResp;
+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.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月5日 下午3:33:26
+ * @company 	QMTH
+ * @description ExamSmsController.java
+ */
+@RestController
+@Api(tags = "考试短信接口")
+@RequestMapping("${app.api.oe.student}/sms")
+public class ExamSmsController extends ControllerSupport{
+
+	@Autowired
+	private StudentCloudService studentCloudService;
+	
+	@Autowired
+//	private SendSmsCloudService sendSmsCloudService;
+	SmsCloudService smsCloudService;
+
+	@ApiOperation(value = "发送短信验证码")
+	@PostMapping("/sendSmsCodeToStudent")
+    public SendSmsCodeResp sendSmsCodeToStudent(){
+    	User user = getAccessUser();
+    	GetStudentReq getStudentReq = new GetStudentReq();
+    	getStudentReq.setStudentId(user.getUserId());
+    	GetStudentResp getStudentResp = studentCloudService.getStudent(getStudentReq);
+
+		SendSmsCodeResp sendSmsResp = new SendSmsCodeResp();
+    	if(StringUtils.isBlank(getStudentResp.getStudentInfo().getPhoneNumber())){
+    		throw new StatusException("100001","系统中手机号码为空,请联系管理员");
+    	}
+    	SendSmsCodeReq sendSmsReq = new SendSmsCodeReq();
+    	sendSmsReq.setPhone(getStudentResp.getStudentInfo().getPhoneNumber());
+    	sendSmsReq.setCode(CommonUtil.makeRandomNum(6));
+
+    	return smsCloudService.sendSmsCode(sendSmsReq);
+    }
+	
+	@ApiOperation(value = "检查验证码是否正确")
+	@PostMapping(value = "/checkSmsCode")
+    public CheckSmsCodeResp checkSmsCode(@RequestParam String phoneNumber, @RequestParam String code){
+		Check.isBlank(phoneNumber, "phoneNumber不能为空");
+		Check.isBlank(code, "code不能为空");
+    	CheckSmsCodeReq checkSmsCodeReq = new CheckSmsCodeReq();
+    	checkSmsCodeReq.setPhone(phoneNumber);
+    	checkSmsCodeReq.setCode(code);
+//    	return sendSmsCloudService.checkSmsCode(checkSmsCodeReq);
+    	return smsCloudService.checkSmsCode(checkSmsCodeReq);
+    }
+	
+}

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

@@ -0,0 +1,154 @@
+package cn.com.qmth.examcloud.core.oe.student.controller;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordRepo;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamProperties;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamType;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordDataRepo;
+import cn.com.qmth.examcloud.core.oe.common.service.GainBaseDataService;
+import cn.com.qmth.examcloud.core.oe.student.bean.OfflineExamCourseInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.OfflineExamService;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月5日 下午3:33:26
+ * @company 	QMTH
+ * @description 离线考试Controller
+ */
+@Api(tags = "离线考试控制")
+@RestController
+@RequestMapping("${app.api.oe.student}/offlineExam")
+public class OfflineExamController extends ControllerSupport{
+
+	@Autowired
+	private OfflineExamService offlineExamService;
+	
+	@Autowired
+	private GainBaseDataService gainBaseDataService;
+	
+	@Autowired
+	private ExamRecordDataRepo examRecordDataRepo;
+	@Autowired
+	private ExamRecordRepo examRecordRepo;
+	/**
+	 * 答案文件最大限制
+	 * 单位:M
+	 */
+	private static int answerMaxsize = 30;
+	
+	
+	public static final String TEMP_FILE_EXP = "offlineExam/";
+	
+	
+	/**
+	 * 获取离线考试列表
+	 * @return
+	 */
+	@ApiOperation(value = "获取离线考试列表")
+	@GetMapping("/getOfflineCourse")
+	public List<OfflineExamCourseInfo> getOfflineCourse(){
+		User user = getAccessUser();
+		List<OfflineExamCourseInfo> offlineExamCourseInfos = offlineExamService.getOfflineCourse(user.getUserId());
+		return offlineExamCourseInfos;
+	}
+	
+	/**
+	 * 开始考试
+	 * @param examStudentId
+	 */
+	@ApiOperation(value = "离线考试:开始考试")
+	@GetMapping("/startOfflineExam")
+	public void startOfflineExam(@RequestParam long examStudentId){
+		Check.isNull(examStudentId, "examStudentId不能为空");
+		offlineExamService.startOfflineExam(examStudentId);
+	}
+	
+	/**
+	 * 交卷
+	 */
+	@ApiOperation(value = "离线考试:交卷")
+	@PostMapping("/submitPaper")
+	public void submitPaper(@RequestParam(value = "file") MultipartFile file,
+							@RequestParam long examRecordDataId)  throws Exception{
+		Check.isNull(file, "file不能为空");
+		Check.isNull(examRecordDataId, "examRecordDataId不能为空");
+		String fileName = file.getOriginalFilename();
+		int index = fileName.lastIndexOf(".");
+		String fileSuffix = fileName.substring(index+1, fileName.length()).toUpperCase();
+		if(!"PDF".equals(fileSuffix) && !"ZIP".equals(fileSuffix)){
+			throw new StatusException("OfflineExamController-submitPaper-001","文件格式不正确"); 
+		}
+		
+		ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo,examRecordDataId,ExamRecordDataEntity.class);
+
+		ExamRecordEntity examRecord =GlobalHelper.getEntity(
+				examRecordRepo,examRecordData.getExamRecordId(),ExamRecordEntity.class);
+		if(examRecord.getExamType() != ExamType.OFFLINE){
+			throw new StatusException("OfflineExamController-submitPaper-002","非离线考试"); 
+		}
+		String offlineUploadFileType = CacheHelper.getExamOrgProperty(examRecord.getExamId(),
+				examRecord.getOrgId(),ExamProperties.OFFLINE_UPLOAD_FILE_TYPE.name()).getValue();
+		if(StringUtils.isBlank(offlineUploadFileType) || "[]".equals(offlineUploadFileType)){
+			throw new StatusException("OfflineExamController-submitPaper-003","当前考试设置不允许上传附件"); 
+		}
+		if(offlineUploadFileType.indexOf(fileSuffix)<0){
+			throw new StatusException("OfflineExamController-submitPaper-004","当前考试允许上传文件格式为:" + offlineUploadFileType); 
+		}
+		//判断文件大小
+		long fileSize = file.getSize();
+		if(fileSize > answerMaxsize * 1048576){
+			throw new StatusException("OfflineExamController-submitPaper-005","文件大小不能超过"+answerMaxsize+"M"); 
+		}
+		offlineExamService.submitPaper(examRecordDataId,getUploadFile(file));
+	}
+	
+	private File getUploadFile(MultipartFile file){
+        //建临时文件夹
+		File dirFile = new File(TEMP_FILE_EXP);
+		if(!dirFile.exists()){
+			dirFile.mkdirs();
+		}
+        String fileName = file.getOriginalFilename();
+        File tempFile = new File(TEMP_FILE_EXP + fileName);
+        OutputStream os = null;
+        try {
+			os = new FileOutputStream(tempFile);
+			IOUtils.copyLarge(file.getInputStream(), os);
+		} catch (FileNotFoundException e) {
+			e.printStackTrace();
+		} catch (IOException e) {
+			e.printStackTrace();
+		}finally{
+			IOUtils.closeQuietly(os);
+		}
+        return tempFile;
+    }
+}

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

@@ -0,0 +1,73 @@
+package cn.com.qmth.examcloud.core.oe.student.controller;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.student.bean.PracticeCourseInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.PracticeDetailInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.PracticeRecordInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.PracticeService;
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月7日 上午11:12:28
+ * @company 	QMTH
+ * @description PracticeController.java
+ */
+@Api(tags = "练习相关接口")
+@RestController
+@RequestMapping("${app.api.oe.student}/practice")
+public class PracticeController extends ControllerSupport{
+
+	@Autowired
+	private PracticeService practiceService;
+	
+	/**
+	 * 练习课程列表
+	 * @param examId
+	 * @param studentId
+	 * @return
+	 */
+	@ApiOperation(value = "练习课程列表")
+	@GetMapping("/queryPracticeCourseList")
+	public List<PracticeCourseInfo> queryPracticeCourseList(@RequestParam Long examId){
+		Check.isNull(examId, "examId不能为空");
+		User user = getAccessUser();
+		return practiceService.queryPracticeCourseList(examId, user.getUserId());
+	}
+	
+	/**
+	 * 课程练习记录详情
+	 * @param examStudentId
+	 * @return
+	 */
+	@ApiOperation(value = "课程练习记录详情")
+	@GetMapping("/queryPracticeRecordList")
+	public List<PracticeRecordInfo> queryPracticeRecordList(@RequestParam Long examStudentId){
+		Check.isNull(examStudentId, "examStudentId不能为空");
+		return practiceService.queryPracticeRecordList(examStudentId);
+	}
+	
+	/**
+	 * 单次练习答题情况统计
+	 * @return
+	 */
+	@ApiOperation(value = "单次练习答题情况统计")
+	@GetMapping("/getPracticeDetailInfo")
+	public PracticeDetailInfo getPracticeDetailInfo(@RequestParam Long examRecordDataId){
+		Check.isNull(examRecordDataId, "examRecordDataId不能为空");
+		return practiceService.getPracticeDetailInfo(examRecordDataId);
+	}
+	
+}

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

@@ -0,0 +1,60 @@
+package cn.com.qmth.examcloud.core.oe.student.controller;
+
+import java.util.Collections;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.commons.util.DateUtil;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+
+/**
+ * 测试状态.勿修改或删除
+ *
+ * @author WANGWEI
+ * @date 2018年8月23日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@RestController
+@RequestMapping("${app.api.oe.student}")
+public class TestController extends ControllerSupport {
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.GET)
+	public String get() {
+		return DateUtil.chinaNow();
+	}
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.HEAD)
+	public ResponseEntity<?> head() {
+		return new ResponseEntity<Object>(HttpStatus.NO_CONTENT);
+	}
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.OPTIONS)
+	public HttpEntity<?> options() {
+		HttpHeaders headers = new HttpHeaders();
+		headers.setAllow(Collections.singleton(HttpMethod.GET));
+		return new ResponseEntity<Object>(headers, HttpStatus.OK);
+	}
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.POST)
+	public String post() {
+		return DateUtil.chinaNow();
+	}
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.PUT)
+	public String put() {
+		return DateUtil.chinaNow();
+	}
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.DELETE)
+	public String delete() {
+		return DateUtil.chinaNow();
+	}
+
+}

+ 31 - 0
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/bean/ExamProcessResultDomain.java

@@ -0,0 +1,31 @@
+package cn.com.qmth.examcloud.core.oe.student.controller.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * @Description 考试处理结果
+ * @Author lideyin
+ * @Date 2019/8/9 19:49
+ * @Version 1.0
+ */
+public class ExamProcessResultDomain implements JsonSerializable {
+    private String code;
+
+    private Object data;
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public Object getData() {
+        return data;
+    }
+
+    public void setData(Object data) {
+        this.data = data;
+    }
+}

+ 61 - 0
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/bean/GetUpyunSignDomain.java

@@ -0,0 +1,61 @@
+package cn.com.qmth.examcloud.core.oe.student.controller.bean;
+
+import java.util.Map;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * @Description 获取又拍云签名实体
+ * @Author lideyin
+ * @Date 2019/7/29 13:51
+ * @Version 1.0
+ */
+public class GetUpyunSignDomain implements JsonSerializable {
+	private static final long serialVersionUID = -7428336128716146142L;
+
+	@ApiModelProperty("又拍云读取文件地址")
+	private String accessUrl;
+
+	@ApiModelProperty("又拍云签名唯一标识")
+	private String signIdentifier;
+
+	@ApiModelProperty("又拍云上传form请求地址 POST")
+	private String formUrl;
+
+	@ApiModelProperty("form表单参数(上传文件的参数名为'file')")
+	private Map<String, String> formParams;
+
+	public String getAccessUrl() {
+		return accessUrl;
+	}
+
+	public void setAccessUrl(String accessUrl) {
+		this.accessUrl = accessUrl;
+	}
+
+	public String getSignIdentifier() {
+		return signIdentifier;
+	}
+
+	public void setSignIdentifier(String signIdentifier) {
+		this.signIdentifier = signIdentifier;
+	}
+
+	public String getFormUrl() {
+		return formUrl;
+	}
+
+	public void setFormUrl(String formUrl) {
+		this.formUrl = formUrl;
+	}
+
+	public Map<String, String> getFormParams() {
+		return formParams;
+	}
+
+	public void setFormParams(Map<String, String> formParams) {
+		this.formParams = formParams;
+	}
+
+}

+ 35 - 0
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/controller/bean/GetUpyunSignDomainQuery.java

@@ -0,0 +1,35 @@
+package cn.com.qmth.examcloud.core.oe.student.controller.bean;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import java.io.Serializable;
+
+/**
+ * @Description 获取又拍云签名查询实体
+ * @Author lideyin
+ * @Date 2019/7/29 13:52
+ * @Version 1.0
+ */
+public class GetUpyunSignDomainQuery implements Serializable {
+    private static final long serialVersionUID = 6594663402697883755L;
+    @ApiModelProperty("文件后缀名,示例:jpg")
+    private String fileSuffix;
+    @ApiModelProperty("文件md5加密值")
+    private String fileMd5;
+
+    public String getFileSuffix() {
+        return fileSuffix;
+    }
+
+    public void setFileSuffix(String fileSuffix) {
+        this.fileSuffix = fileSuffix;
+    }
+
+    public String getFileMd5() {
+        return fileMd5;
+    }
+
+    public void setFileMd5(String fileMd5) {
+        this.fileMd5 = fileMd5;
+    }
+}

+ 201 - 0
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/ExamRecordCloudServiceProvider.java

@@ -0,0 +1,201 @@
+package cn.com.qmth.examcloud.core.oe.student.provider;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.basic.api.StudentCloudService;
+import cn.com.qmth.examcloud.core.basic.api.bean.StudentBean;
+import cn.com.qmth.examcloud.core.basic.api.request.GetStudentReq;
+import cn.com.qmth.examcloud.core.basic.api.response.GetStudentResp;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamingRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.HandInExamRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordDataRepo;
+import cn.com.qmth.examcloud.core.oe.student.api.OeStudentExamRecordCloudService;
+import cn.com.qmth.examcloud.core.oe.student.api.bean.ExamingRecordBean;
+import cn.com.qmth.examcloud.core.oe.student.api.bean.HandInExamRecordBean;
+import cn.com.qmth.examcloud.core.oe.student.api.request.*;
+import cn.com.qmth.examcloud.core.oe.student.api.response.*;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamSessionInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamControlService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamSessionInfoService;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import cn.com.qmth.examcloud.web.support.Naked;
+import com.mysql.cj.util.StringUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Api(tags = "清理考试记录")
+@RestController
+@RequestMapping("${$rmp.cloud.oe.student}" + "examRecord")
+public class ExamRecordCloudServiceProvider extends ControllerSupport implements OeStudentExamRecordCloudService {
+
+    /**
+     *
+     */
+    private static final long serialVersionUID = 2873433904730318971L;
+    @Autowired
+    ExamSessionInfoService examSessionInfoService;
+    @Autowired
+    StudentCloudService studentCloudService;
+    @Autowired
+    ExamRecordDataRepo examRecordDataRepo;
+    @Autowired
+    ExamRecordDataService examRecordDataService;
+    @Autowired
+    private ExamControlService examControlService;
+
+    @Naked
+    @PostMapping("/cleanExamingRecord")
+    @Override
+    public void cleanExamingRecord(@RequestBody CleanExamingRecordReq req) {
+        ExamingRecordEntity examingRecordEntity = new ExamingRecordEntity();
+        examingRecordEntity.setExamRecordDataId(req.getExamRecordDataId());
+        examingRecordEntity.setId(req.getId());
+        examingRecordEntity.setStudentId(req.getStudentId());
+        examControlService.cleanExamingRecord(examingRecordEntity);
+    }
+
+    @Naked
+    @PostMapping("/cleanHandInExamRecord")
+    @Override
+    public void cleanHandInExamRecord(@RequestBody CleanHandInExamRecordReq req) {
+        HandInExamRecordEntity handInExamRecordEntity = new HandInExamRecordEntity();
+        handInExamRecordEntity.setExamRecordDataId(req.getExamRecordDataId());
+        handInExamRecordEntity.setId(req.getId());
+        handInExamRecordEntity.setStudentId(req.getStudentId());
+        examControlService.cleanHandInExamRecord(handInExamRecordEntity);
+    }
+
+    @Naked
+    @ApiOperation(value = "获取二维码")
+    @Override
+    @PostMapping("/getQrCode")
+    public GetQrCodeResp getQrCode(@RequestBody GetQrCodeReq req) {
+        log.info("开始调用获取二维码方法---getQrCode");
+        cn.com.qmth.examcloud.core.oe.student.bean.GetQrCodeReq beanReq = new cn.com.qmth.examcloud.core.oe.student.bean.GetQrCodeReq();
+        beanReq.setExamRecordDataId(req.getExamRecordDataId());
+        beanReq.setExamStudentId(req.getExamStudentId());
+        beanReq.setOrder(req.getOrder());
+        beanReq.setTransferFileType(req.getTransferFileType());
+        beanReq.setTestEnv(req.isTestEnv());
+        log.info("参数准备完毕---getQrCode,req.getExamRecordDataId()=" + req.getExamRecordDataId());
+        log.info("参数准备完毕---getQrCode,req.getExamStudentId()=" + req.getExamStudentId());
+        log.info("参数准备完毕---getQrCode,req.getOrder()=" + req.getOrder());
+        log.info("参数准备完毕---getQrCode,req.getTokenKey()=" + req.getTokenKey());
+        String qrCode = examControlService.getQrCode(beanReq, req.getTokenKey());
+        GetQrCodeResp resp = new GetQrCodeResp();
+        resp.setQrCode(qrCode);
+        return resp;
+    }
+
+    @Naked
+    @ApiOperation(value = "获取考试会话")
+    @Override
+    @PostMapping("/getExamSessionInfo")
+    public GetExamSessionInfoResp getExamSessionInfo(@RequestBody GetExamSessionInfoReq req) {
+
+        ExamSessionInfo examSessionInfo = examSessionInfoService.getExamSessionInfo(req.getStudentId());
+        if (null == examSessionInfo) {
+            throw new StatusException("100001", "考试已结束");
+        }
+        GetExamSessionInfoResp resp = new GetExamSessionInfoResp();
+        resp.setExamStudentId(examSessionInfo.getExamStudentId());
+        return resp;
+    }
+
+    @Naked
+    @ApiOperation(value = "获取学生是否有正在进行的网考信息")
+    @Override
+    @PostMapping("/getStudentOnLineExamInfo")
+    public GetStudentOnlineExamInfoResp getStudentOnLineExamInfo(@RequestBody GetStudentOnlineExamInfoReq req) {
+        if (null == req.getRootOrgId()) {
+            throw new StatusException("101001", "组织机构id参数不允许为空");
+        }
+        if (StringUtils.isNullOrEmpty(req.getStudentCode()) && StringUtils.isNullOrEmpty(req.getIdentityNumber())
+                && StringUtils.isNullOrEmpty(req.getPhone())) {
+            throw new StatusException("101002", "学生学号、身份证号或手机号参数至少有一个不为空");
+        }
+        GetStudentReq getStudentReq = new GetStudentReq();
+        getStudentReq.setRootOrgId(req.getRootOrgId());
+        if (!StringUtils.isNullOrEmpty(req.getStudentCode())) {
+            getStudentReq.setStudentCode(req.getStudentCode());
+        }
+        if (!StringUtils.isNullOrEmpty(req.getIdentityNumber())) {
+            getStudentReq.setIdentityNumber(req.getIdentityNumber());
+        }
+        if (!StringUtils.isNullOrEmpty(req.getPhone())) {
+            getStudentReq.setSecurityPhone(req.getPhone());
+        }
+        //此方法内部已作空处理,不需要再判断非空
+        GetStudentResp studentResp = studentCloudService.getStudent(getStudentReq);
+        StudentBean studentInfo = studentResp.getStudentInfo();
+        Long studentId = studentInfo.getId();
+        ExamRecordDataEntity examRecordData = examRecordDataRepo.findOnlineExamingRecordByStudentId(studentId);
+        GetStudentOnlineExamInfoResp resp = new GetStudentOnlineExamInfoResp();
+        if (null != examRecordData && null != examRecordData.getId()) {
+            resp.setExistExamingRecord(true);
+        } else {
+            resp.setExistExamingRecord(false);
+        }
+        return resp;
+    }
+
+    @Naked
+    @PostMapping("/getExamingRecords")
+    @Override
+    public GetExamingRecordResp getExamingRecords(@RequestBody GetExamingRecordReq req) {
+        List<ExamingRecordEntity> examingRecords = examRecordDataService.getLimitExamingRecords(req.getStartId(), req.getLimit());
+        if (null == examingRecords || examingRecords.isEmpty()) {
+            return new GetExamingRecordResp(req.getStartId(), null);
+        }
+
+        List<ExamingRecordBean> examingRecordBeanList = new ArrayList<>();
+        for (ExamingRecordEntity entity : examingRecords) {
+            ExamingRecordBean bean = new ExamingRecordBean();
+            bean.setExamRecordDataId(entity.getExamRecordDataId());
+            bean.setId(entity.getId());
+            bean.setStudentId(entity.getStudentId());
+            examingRecordBeanList.add(bean);
+        }
+        Long nextId = examingRecords.get(examingRecords.size() - 1).getId() + 1;
+        return new GetExamingRecordResp(nextId, examingRecordBeanList);
+    }
+
+
+    @Naked
+    @PostMapping("/getHandInExamRecords")
+    @Override
+    public GetHandInExamRecordResp getHandInExamRecords(@RequestBody GetHandInExamRecordReq req) {
+        List<HandInExamRecordEntity> handInExamRecords = examRecordDataService.getLimitHandInExamRecords(req.getStartId(), req.getLimit());
+        if (null == handInExamRecords || handInExamRecords.isEmpty()) {
+            return new GetHandInExamRecordResp(req.getStartId(), null);
+        }
+
+        List<HandInExamRecordBean> handInExamRecordBeanList = new ArrayList<>();
+        for (HandInExamRecordEntity entity : handInExamRecords) {
+            HandInExamRecordBean bean = new HandInExamRecordBean();
+            bean.setExamRecordDataId(entity.getExamRecordDataId());
+            bean.setId(entity.getId());
+            bean.setStudentId(entity.getStudentId());
+            handInExamRecordBeanList.add(bean);
+        }
+        Long nextId = handInExamRecords.get(handInExamRecords.size() - 1).getId() + 1;
+        return new GetHandInExamRecordResp(nextId, handInExamRecordBeanList);
+    }
+
+    @Naked
+    @ApiOperation(value = "清理考试已结束的文件作答记录")
+    @Override
+    @PostMapping("/cleanTempFileAnswers")
+    public void cleanTempFileAnswers() {
+        examControlService.cleanTempFileAnswers();
+    }
+}

+ 37 - 0
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/ExamRecordPaperStructProvider.java

@@ -0,0 +1,37 @@
+package cn.com.qmth.examcloud.core.oe.student.provider;
+
+import cn.com.qmth.examcloud.core.oe.admin.api.OeExamStudentCloudService;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordPaperStructEntity;
+import cn.com.qmth.examcloud.core.oe.student.api.OeExamPaperStructCloudService;
+import cn.com.qmth.examcloud.core.oe.student.api.request.GetExamRecordPaperStructReq;
+import cn.com.qmth.examcloud.core.oe.student.api.response.GetExamRecordPaperStructResp;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordPaperStructService;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@Api(tags = "考试记录-试卷结构")
+@RestController
+@RequestMapping("${$rmp.cloud.oe.student}" + "examRecordPaperStruct")
+public class ExamRecordPaperStructProvider extends ControllerSupport implements OeExamPaperStructCloudService {
+
+	@Autowired
+	private ExamRecordPaperStructService examRecordPaperStructService;
+	
+	@ApiOperation(value = "获取考试记录试卷结构")
+	@PostMapping("/getExamRecordPaperStruct")
+	@Override
+	public GetExamRecordPaperStructResp getExamRecordPaperStruct(@RequestBody GetExamRecordPaperStructReq req) {
+		Check.isNull(req.getExamRecordDataId(), "考试记录id不能为空");
+		ExamRecordPaperStructEntity examRecordPaperStructEntity = examRecordPaperStructService.getExamRecordPaperStruct(req.getExamRecordDataId());
+		GetExamRecordPaperStructResp resp = new GetExamRecordPaperStructResp();
+		if (null == examRecordPaperStructEntity){
+			return null;
+		}
+		resp.setDefaultPaper(examRecordPaperStructEntity.getDefaultPaper());
+		return resp;
+	}
+}

+ 54 - 0
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/OeExamRecordForMarkingCloudServiceProvider.java

@@ -0,0 +1,54 @@
+package cn.com.qmth.examcloud.core.oe.student.provider;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordForMarkingEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordForMarkingRepo;
+import cn.com.qmth.examcloud.core.oe.student.api.OeExamRecordForMarkingCloudService;
+import cn.com.qmth.examcloud.core.oe.student.api.request.SaveExamRecordForMarkingReq;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordQuestionsService;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+
+
+@Api(tags = "保存阅卷数据接口")
+@RestController
+@RequestMapping("${$rmp.cloud.oe.student}" + "examRecordForMarking")
+public class OeExamRecordForMarkingCloudServiceProvider extends ControllerSupport implements OeExamRecordForMarkingCloudService{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 5991001721278000904L;
+
+	@Autowired
+	private ExamRecordQuestionsService examRecordQuestionsService;
+	
+	@Autowired
+	private ExamRecordForMarkingRepo examRecordForMarkingRepo;
+	
+	@Override
+	@PostMapping("/saveExamRecordForMarking")
+	public void saveExamRecordForMarking(@RequestBody SaveExamRecordForMarkingReq req) {
+		ExamRecordForMarkingEntity examRecordForMarkingExists = examRecordForMarkingRepo.findByExamRecordDataId(req.getExamRecordDataId());
+		if(examRecordForMarkingExists != null){
+			return;
+		}
+		ExamRecordForMarkingEntity examRecordForMarking = new ExamRecordForMarkingEntity();
+        examRecordForMarking.setExamId(req.getExamId());
+        examRecordForMarking.setExamRecordDataId(req.getExamRecordDataId());
+        examRecordForMarking.setExamStudentId(req.getExamStudentId());
+        examRecordForMarking.setBasePaperId(req.getBasePaperId());
+        examRecordForMarking.setPaperType(req.getPaperType());
+        examRecordForMarking.setCourseId(req.getCourseId());
+        examRecordForMarking.setObjectiveScore(req.getObjectiveScore());
+        int subjectiveAnswerLength = examRecordQuestionsService.calculationSubjectiveAnswerLength(req.getExamRecordDataId());
+        examRecordForMarking.setSubjectiveAnswerLength(subjectiveAnswerLength);
+        examRecordForMarkingRepo.save(examRecordForMarking);
+	}
+
+}

+ 113 - 0
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/OeExamScoreNoticeQueueCloudServiceProvider.java

@@ -0,0 +1,113 @@
+package cn.com.qmth.examcloud.core.oe.student.provider;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.commons.util.OKHttpUtil;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamScoreNoticeQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.OrgScoreHandleEntity;
+import cn.com.qmth.examcloud.core.oe.common.info.NotifyUrlInfo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamScoreNoticeQueueRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.OrgScoreHandleRepo;
+import cn.com.qmth.examcloud.core.oe.common.service.ExamScoreNoticeQueueService;
+import cn.com.qmth.examcloud.core.oe.common.service.ExamScoreObtainQueueService;
+import cn.com.qmth.examcloud.core.oe.student.api.OeExamScoreNoticeQueueCloudService;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+@Api(tags = "定时发送获取分数通知接口")
+@RestController
+@RequestMapping("${$rmp.cloud.oe.student}" + "examScoreNoticeQueue")
+public class OeExamScoreNoticeQueueCloudServiceProvider extends ControllerSupport
+		implements OeExamScoreNoticeQueueCloudService {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -2622841462905285211L;
+
+//	@Autowired
+//	private OrgScoreHandleRepo orgScoreHandleRepo;
+	@Autowired
+	private ExamScoreNoticeQueueRepo examScoreNoticeQueueRepo;
+	@Autowired
+	ExamScoreObtainQueueService examScoreObtainQueueService;
+
+	/**
+	 * 定时发送分数获取通知 定时任务每5分钟发送一次
+	 */
+	@ApiOperation(value = "定时发送获取分数通知接口")
+	@PostMapping("/sendObtainScoreNotice")
+	@Override
+	public void sendObtainScoreNotice() {
+		System.out.println("1开始执行sendObtainScoreNotice");
+		log.info("开始执行sendObtainScoreNotice");
+		// 获取所有的通知队列
+		List<ExamScoreNoticeQueueEntity> examScoreNoticeQueueList = examScoreNoticeQueueRepo.findAll();
+		if (examScoreNoticeQueueList == null || examScoreNoticeQueueList.size() == 0) {
+			return;
+		}
+		System.out
+				.println("2执行sendObtainScoreNotice,examScoreNoticeQueueList.size()=" + examScoreNoticeQueueList.size());
+		// 获取所有配置通知路径的组织机构集合
+//		List<OrgScoreHandleEntity> orgScoreHandleList = orgScoreHandleRepo.findEnableNotifyItems();
+//		
+//		if(orgScoreHandleList == null || orgScoreHandleList.size() == 0){
+//			return;
+//		}
+//		System.out.println("3执行sendObtainScoreNotice,orgScoreHandleList.size()="+orgScoreHandleList.size());
+		for (ExamScoreNoticeQueueEntity noticeEntity : examScoreNoticeQueueList) {
+			// 获取当前组织机构的通知对象
+			NotifyUrlInfo notifyUrlInfo = examScoreObtainQueueService.getNotifyUrlInfo(noticeEntity.getRootOrgId());
+//			找到当前组织机构对应的通知队列 ldy20190516代码重构,修改获取通知方式
+//			Optional<OrgScoreHandleEntity> optionalOrgScoreHandle = orgScoreHandleList.stream().filter(org->org.getRootOrgId().equals(noticeEntity.getRootOrgId())).findFirst();
+//			if(optionalOrgScoreHandle.isPresent()) {
+//				OrgScoreHandleEntity orgScoreHandle = optionalOrgScoreHandle.get();
+			// 只有配置了通知接口的才发送通知
+			if (StringUtils.isNotBlank(notifyUrlInfo.getNotifyUrl())) {
+				try {
+					OKHttpUtil.call(notifyUrlInfo.getHttpMethod(), notifyUrlInfo.getNotifyUrl());
+//						this.post(notifyUrlInfo.getNotifyUrl(), null, 2000,15000);
+					// 发送通知没有问题,则清除通知队列中数据
+					examScoreNoticeQueueRepo.deleteById(noticeEntity.getRootOrgId());
+				} catch (Exception e) {
+					if (e instanceof UnknownHostException || e instanceof SocketException) {
+						try {
+							// 如果是由于连接超时,或者读取数据超时导致异常,需要对发送通知失败次数进行累加
+							long failTimes = noticeEntity.getFailTimes() == null ? 0
+									: noticeEntity.getFailTimes().longValue();
+							noticeEntity.setFailTimes(failTimes + 1);
+							noticeEntity.setUpdateTime(new Date());
+							examScoreNoticeQueueRepo.save(noticeEntity);
+						} catch (Exception e1) {
+							log.error("examScoreNoticeQueueRepo.save exception:" + e1.getMessage(), e1);
+						}
+					}
+					log.error("OeExamScoreNoticeQueueCloudServiceProvider-sendObtainScoreNotice:" + e.getMessage(), e);
+				}
+			}
+//			}
+
+		}
+		log.info("结束执行sendObtainScoreNotice");
+		System.out.println("结束执行sendObtainScoreNotice");
+	}
+
+}

+ 84 - 0
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/OeExamScoreObtainQueueCloudServiceProvider.java

@@ -0,0 +1,84 @@
+package cn.com.qmth.examcloud.core.oe.student.provider;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import cn.com.qmth.examcloud.core.oe.common.base.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamScoreNoticeQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamScoreObtainQueueEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamScoreNoticeQueueRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamScoreObtainQueueRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.OrgScoreHandleRepo;
+import cn.com.qmth.examcloud.core.oe.student.api.OeExamScoreObtainQueueCloudService;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+@Api(tags = "获取分数队列")
+@RestController
+@RequestMapping("${$rmp.cloud.oe.student}" + "examScoreObtainQueue")
+public class OeExamScoreObtainQueueCloudServiceProvider extends ControllerSupport implements OeExamScoreObtainQueueCloudService{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -2622841462905285211L;
+	
+	@Autowired
+	private OrgScoreHandleRepo orgScoreHandleRepo;
+	@Autowired
+	ExamScoreObtainQueueRepo examScoreObtainQueueRepo;
+	@Autowired
+	ExamScoreNoticeQueueRepo examScoreNoticeQueueRepo;
+	
+	/**
+	 * 定时更新获取分数通知队列
+	 * 
+	 */
+	@ApiOperation(value = "定时更新获取分数通知队列")
+	@PostMapping("/updateObtainScoreNodifyQueue")
+	@Override
+	public void updateObtainScoreNodifyQueue() {
+		Date now = new Date();
+		Date twoHoursBefore= DateUtils.addHours(now, -2);
+		//1.查找超过创建超过2小时,且通知队列中不存在的待获取的数据
+		List<ExamScoreObtainQueueEntity> toObtainQueueList = examScoreObtainQueueRepo.findByCreationTimeLessThanEquals(twoHoursBefore);
+		//1.1如果不存在,不作任何处理
+		if(toObtainQueueList==null || toObtainQueueList.isEmpty()) {
+			return;
+		}
+		//1.2如果存在,则创建相应组织机构的通知队列
+		List<Long> rootOrgList = toObtainQueueList.stream().map(ExamScoreObtainQueueEntity::getRootOrgId).distinct().collect(Collectors.<Long>toList());
+		List<ExamScoreNoticeQueueEntity> noticeQueueEntityList= new ArrayList<ExamScoreNoticeQueueEntity>(); 
+		rootOrgList.forEach(ro->{
+			ExamScoreNoticeQueueEntity noticeQueueEntity = new ExamScoreNoticeQueueEntity();
+			noticeQueueEntity.setRootOrgId(ro);
+			noticeQueueEntity.setCreationTime(now);
+			noticeQueueEntityList.add(noticeQueueEntity);
+		});
+		//批量插入通知队列
+		examScoreNoticeQueueRepo.saveAll(noticeQueueEntityList);
+//		List<OrgScoreHandleEntity> orgScoreHandleList = orgScoreHandleRepo.findEnableNotifyItems();
+//		if(orgScoreHandleList == null || orgScoreHandleList.size() == 0){
+//			return;
+//		}
+//		for(OrgScoreHandleEntity orgScoreHandle:orgScoreHandleList){
+//			if(StringUtils.isNotBlank(orgScoreHandle.getNotifyUrl())){
+//				try{
+//					HttpClientUtil.post(orgScoreHandle.getNotifyUrl(), null, 2000);
+//				}catch(Exception e){
+//					log.error("ExamScoreObtainQueueServiceImpl-sendNotifyError:"+e.getMessage(),e);
+//				}
+//			}
+//		}
+	}
+
+}

+ 39 - 0
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/provider/OeHandleByExamCaptureQueueFailedDisposeServiceProvider.java

@@ -0,0 +1,39 @@
+package cn.com.qmth.examcloud.core.oe.student.provider;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.student.api.OeHandleByExamCaptureQueueFailedDisposeService;
+import cn.com.qmth.examcloud.core.oe.student.api.request.HandleByExamCaptureQueueFailedDisposeReq;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamControlService;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+
+@Api(tags = "抓拍照片失败处理")
+@RestController
+@RequestMapping("${$rmp.cloud.oe.student}" + "handleByExamCaptureQueueFailedDispose")
+public class OeHandleByExamCaptureQueueFailedDisposeServiceProvider extends ControllerSupport implements OeHandleByExamCaptureQueueFailedDisposeService{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 4864914290302594216L;
+
+	@Autowired
+	private ExamControlService examControlService;
+	
+	@ApiOperation(value = "抓拍照片失败处理")
+	@Override
+	@PostMapping
+	public void handleByExamCaptureQueueFailedDispose(@RequestBody HandleByExamCaptureQueueFailedDisposeReq req) {
+		Check.isNull(req.getExamRecordDataId(), "examRecordDataId 不能为空");
+		examControlService.handleByExamCaptureQueueFailedDispose(req.getExamRecordDataId());
+	}
+
+}

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

@@ -0,0 +1,40 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class CalculateFaceCheckResultInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 5045871814686279423L;
+
+	private ExamRecordDataEntity examRecordData;
+	
+	/**
+	 * 无照片违纪
+	 */
+	private Boolean isNoPhotoAndIllegality;
+
+	public ExamRecordDataEntity getExamRecordData() {
+		return examRecordData;
+	}
+
+	public void setExamRecordData(ExamRecordDataEntity examRecordData) {
+		this.examRecordData = examRecordData;
+	}
+
+	public Boolean getIsNoPhotoAndIllegality() {
+		return isNoPhotoAndIllegality;
+	}
+	
+	/**
+	 * 设置无照片违纪
+	 * @param isNoPhotoAndIllegality
+	 */
+	public void setIsNoPhotoAndIllegality(Boolean isNoPhotoAndIllegality) {
+		this.isNoPhotoAndIllegality = isNoPhotoAndIllegality;
+	}
+
+}

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

@@ -0,0 +1,89 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class CheckExamInProgressInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 5411680698118472710L;
+
+	//已断点次数
+	private Integer interruptNum;
+	
+	//最大断点次数限制
+	private Integer maxInterruptNum;
+	
+	//是否达到最大断点限制
+	private Boolean isExceed;
+	
+	private Long examRecordDataId;
+	
+	private Long examId;
+	/**
+	 * 使用时间
+	 */
+	private Integer usedTime;
+	/**
+	 * 活体检测启动分钟数
+	 */
+	private Integer faceVerifyMinute;
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public Integer getUsedTime() {
+		return usedTime;
+	}
+
+	public void setUsedTime(Integer usedTime) {
+		this.usedTime = usedTime;
+	}
+
+	public Integer getFaceVerifyMinute() {
+		return faceVerifyMinute;
+	}
+
+	public void setFaceVerifyMinute(Integer faceVerifyMinute) {
+		this.faceVerifyMinute = faceVerifyMinute;
+	}
+
+	public Integer getInterruptNum() {
+		return interruptNum;
+	}
+
+	public void setInterruptNum(Integer interruptNum) {
+		this.interruptNum = interruptNum;
+	}
+
+	public Integer getMaxInterruptNum() {
+		return maxInterruptNum;
+	}
+
+	public void setMaxInterruptNum(Integer maxInterruptNum) {
+		this.maxInterruptNum = maxInterruptNum;
+	}
+
+	public Boolean getIsExceed() {
+		return isExceed;
+	}
+
+	public void setIsExceed(Boolean isExceed) {
+		this.isExceed = isExceed;
+	}
+
+}

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

@@ -0,0 +1,90 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+public class CheckQrCodeInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 7830558100815856011L;
+	@ApiModelProperty(value = "登录信息key")
+	private String key;
+	@ApiModelProperty(value = "登录信息token")
+	private String token;
+	@ApiModelProperty(value = "考生ID")
+	private Long examStudentId;
+	@ApiModelProperty(value = "考试记录DataID")
+	private Long examRecordDataId;
+	@ApiModelProperty(value = "考试试题题序")
+	private Integer questionOrder;
+	@ApiModelProperty(value = "考试试题大题号")
+    private Integer questionMainNumber;
+	@ApiModelProperty(value = "课程ID")
+	private Long courseId;
+	@ApiModelProperty(value = "课程名称")
+	private String courseName;
+	@ApiModelProperty(value = "小题显示序号")
+    private Integer subNumber;
+	public String getKey() {
+		return key;
+	}
+	public void setKey(String key) {
+		this.key = key;
+	}
+	public String getToken() {
+		return token;
+	}
+	public void setToken(String token) {
+		this.token = token;
+	}
+
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Long getCourseId() {
+		return courseId;
+	}
+	public void setCourseId(Long courseId) {
+		this.courseId = courseId;
+	}
+	public String getCourseName() {
+		return courseName;
+	}
+	public void setCourseName(String courseName) {
+		this.courseName = courseName;
+	}
+	public Integer getQuestionOrder() {
+		return questionOrder;
+	}
+	public void setQuestionOrder(Integer questionOrder) {
+		this.questionOrder = questionOrder;
+	}
+	public Integer getQuestionMainNumber() {
+		return questionMainNumber;
+	}
+	public void setQuestionMainNumber(Integer questionMainNumber) {
+		this.questionMainNumber = questionMainNumber;
+	}
+	public Integer getSubNumber() {
+		return subNumber;
+	}
+	public void setSubNumber(Integer subNumber) {
+		this.subNumber = subNumber;
+	}
+	
+}

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

@@ -0,0 +1,54 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamingRecordEntity;
+
+/**
+ * @Description 清理考试记录优先级队列实体
+ * @Author lideyin
+ * @Date 2019/8/11 13:51
+ * @Version 1.0
+ */
+public class CleanExamRecordPriorityQueueInfo implements Comparable<CleanExamRecordPriorityQueueInfo> {
+
+    private int priority;
+    private ExamingRecordEntity examingRecord;
+
+    public CleanExamRecordPriorityQueueInfo() {
+    }
+    public CleanExamRecordPriorityQueueInfo(int priority, ExamingRecordEntity examingRecord) {
+        this.priority = priority;
+        this.examingRecord = examingRecord;
+    }
+
+    public int getPriority() {
+        return priority;
+    }
+
+    public void setPriority(int priority) {
+        this.priority = priority;
+    }
+
+    public ExamingRecordEntity getExamingRecord() {
+        return examingRecord;
+    }
+
+    public void setExamingRecord(ExamingRecordEntity examingRecord) {
+        this.examingRecord = examingRecord;
+    }
+
+    //升序
+    @Override
+    public int compareTo(CleanExamRecordPriorityQueueInfo data) {
+        return this.priority > data.priority ? -1 : this.priority == data.priority ? 0 : 1;
+    }
+
+    //考试记录id 相同则认为是同一个对象
+    @Override
+    public boolean equals(Object o) {
+        if (this.examingRecord==null){
+            return false;
+        }
+        return this.examingRecord.getId()
+                .equals(((CleanExamRecordPriorityQueueInfo) o).getExamingRecord().getId());
+    }
+}

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

@@ -0,0 +1,60 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月27日 下午4:44:09
+ * @company 	QMTH
+ * @description EndExamInfo.java
+ */
+public class EndExamInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 3567311334163339241L;
+
+	private Long examRecordDataId;
+	
+	private Boolean isWarn;
+	
+	private Double objectiveScore;
+	
+	private Double objectiveAccuracy;
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Boolean getIsWarn() {
+		return isWarn;
+	}
+
+	public void setIsWarn(Boolean isWarn) {
+		this.isWarn = isWarn;
+	}
+
+	public Double getObjectiveScore() {
+		return objectiveScore;
+	}
+
+	public void setObjectiveScore(Double objectiveScore) {
+		this.objectiveScore = objectiveScore;
+	}
+
+	public Double getObjectiveAccuracy() {
+		return objectiveAccuracy;
+	}
+
+	public void setObjectiveAccuracy(Double objectiveAccuracy) {
+		this.objectiveAccuracy = objectiveAccuracy;
+	}
+	
+	
+}

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

@@ -0,0 +1,63 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年11月9日 下午4:52:40
+ * @company 	QMTH
+ * @description 交卷前置处理info
+ */
+public class EndExamPreInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -8434235190113801338L;
+
+	private ExamRecordDataEntity examRecordData;
+	
+	private Long studentId;
+	
+	private Long usedExamTime;
+	
+	/**
+	 * 无照片违纪
+	 */
+	private Boolean isNoPhotoAndIllegality;
+
+	public ExamRecordDataEntity getExamRecordData() {
+		return examRecordData;
+	}
+
+	public void setExamRecordData(ExamRecordDataEntity examRecordData) {
+		this.examRecordData = examRecordData;
+	}
+
+	public Long getUsedExamTime() {
+		return usedExamTime;
+	}
+
+	public void setUsedExamTime(Long usedExamTime) {
+		this.usedExamTime = usedExamTime;
+	}
+
+	public Long getStudentId() {
+		return studentId;
+	}
+
+	public void setStudentId(Long studentId) {
+		this.studentId = studentId;
+	}
+
+	public Boolean getIsNoPhotoAndIllegality() {
+		return isNoPhotoAndIllegality;
+	}
+
+	public void setIsNoPhotoAndIllegality(Boolean isNoPhotoAndIllegality) {
+		this.isNoPhotoAndIllegality = isNoPhotoAndIllegality;
+	}
+	
+}

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

@@ -0,0 +1,27 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import java.io.Serializable;
+
+import io.swagger.annotations.ApiModelProperty;
+/**
+ * 考生作答记录扩展类
+ * @author lideyin
+ * @date 2019年5月14日 下午3:29:59
+ * @description
+ */
+public class ExamQuestionAnswerExtensionInfo implements Serializable {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 6459207019695307248L;
+	@ApiModelProperty(value = "小题显示序号")
+    private Integer subNumber;
+	public Integer getSubNumber() {
+		return subNumber;
+	}
+	public void setSubNumber(Integer subNumber) {
+		this.subNumber = subNumber;
+	}
+	
+}

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

@@ -0,0 +1,171 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * @author chenken
+ * @date 2018/8/14 16:27
+ * @company QMTH
+ * @description 考试会话信息
+ */
+public class ExamSessionInfo implements JsonSerializable {
+
+    /**
+	 * 
+	 */
+	private static final long serialVersionUID = 7271713913526550662L;
+	private Long examId;
+	/**
+     * 考试记录ID
+     */
+    private Long examRecordId;
+    /**
+     * 考试记录DataID
+     */
+    private Long examRecordDataId;
+    /**
+     * 考生ID
+     */
+    private Long examStudentId;
+    /**
+     * 考试开始时间
+     */
+    private Long startTime;
+    /**
+     * 考试时长:毫秒
+     */
+    private Long examDuration;
+    /**
+     * 冻结时间:分钟
+     */
+    private Integer freezeTime;
+    /**
+     * 心跳次数
+     */
+    private Integer heartbeat;
+    /**
+     * 最后一次心跳时间
+     */
+    private Long lastHeartbeat;
+    /**
+     * 考试类型
+     */
+    private String examType;
+    /**
+     * 断点续考时间:分钟
+     */
+    private Integer examReconnectTime;
+    /**
+     * 课程代码
+     */
+    private String courseCode;
+    /**
+     * 试卷类型
+     */
+    private String paperType;
+
+    public Long getExamRecordId() {
+        return examRecordId;
+    }
+
+    public void setExamRecordId(Long examRecordId) {
+        this.examRecordId = examRecordId;
+    }
+
+    public Long getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(Long startTime) {
+        this.startTime = startTime;
+    }
+
+    public Long getExamDuration() {
+        return examDuration;
+    }
+
+    public void setExamDuration(Long examDuration) {
+        this.examDuration = examDuration;
+    }
+
+    public Integer getHeartbeat() {
+        return heartbeat;
+    }
+
+    public void setHeartbeat(Integer heartbeat) {
+        this.heartbeat = heartbeat;
+    }
+
+    public String getExamType() {
+        return examType;
+    }
+
+    public void setExamType(String examType) {
+        this.examType = examType;
+    }
+
+    public Integer getExamReconnectTime() {
+        return examReconnectTime;
+    }
+
+    public void setExamReconnectTime(Integer examReconnectTime) {
+        this.examReconnectTime = examReconnectTime;
+    }
+
+    public Long getLastHeartbeat() {
+        return lastHeartbeat;
+    }
+
+    public void setLastHeartbeat(Long lastHeartbeat) {
+        this.lastHeartbeat = lastHeartbeat;
+    }
+
+    public Integer getFreezeTime() {
+        return freezeTime;
+    }
+
+    public void setFreezeTime(Integer freezeTime) {
+        this.freezeTime = freezeTime;
+    }
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+    public String getCourseCode() {
+        return courseCode;
+    }
+
+    public void setCourseCode(String courseCode) {
+        this.courseCode = courseCode;
+    }
+
+    public String getPaperType() {
+        return paperType;
+    }
+
+    public void setPaperType(String paperType) {
+        this.paperType = paperType;
+    }
+}

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

@@ -0,0 +1,296 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import java.util.Date;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * @author  	chenken
+ * @date    	2018年8月13日 下午3:18:45
+ * @company 	QMTH
+ * @description ExamStudentInfo.java
+ */
+public class ExamStudentInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 6364550318267288565L;
+
+	private Long examStudentId;//考试报考ID
+	
+	private Long examId;//网考批次ID
+	
+	private String examName;//批次名称
+	
+	private String studentName;//考生名称
+	
+	private String studentCode;//考生学号
+	
+	private String identityNumber;//身份证号码
+	
+	private String specialtyName;//专业名称
+	
+	private String specialtyLevel;//层次
+	
+	private Long courseId;//课程ID
+	
+	private String courseName;//课程名称
+	
+	private String courseCode;//课程代码
+	
+	private String courseLevel;//课程层次
+	
+	private Long orgId;//学校中心ID
+	
+	private String orgName;//学习中心名称
+	
+	private String isPhotoUpload;//是否上传照片
+	
+	private int paperMins;//考试时长
+	
+	private int allowExamCount;//允许考试次数
+	
+	private Date startTime;//考试时间范围开始时间
+	
+	private Date endTime; //考试时间范围结束时间
+	
+	private Long rootOrgId;//机构ID
+	
+	private String isFinished;//是否缺考
+	
+	/**
+	 * 考试状态
+	 */
+	private String examStatus;
+
+	/**
+	 * 是否有效
+	 */
+	//private Boolean isvalid;
+	/**
+	 * 是否启用人脸识别
+	 */
+	private Boolean faceEnable;
+	/**
+	 * 进入考试是否验证人脸识别(强制、非强制)
+	 */
+	private Boolean faceCheck;
+	
+	/**
+	 * 是否显示客观分
+	 */
+	private Boolean isObjScoreView;
+
+
+	public String getStudentName() {
+		return studentName;
+	}
+
+	public void setStudentName(String studentName) {
+		this.studentName = studentName;
+	}
+	
+	public String getStudentCode() {
+		return studentCode;
+	}
+
+	public void setStudentCode(String studentCode) {
+		this.studentCode = studentCode;
+	}
+
+	public String getSpecialtyName() {
+		return specialtyName;
+	}
+
+	public void setSpecialtyName(String specialtyName) {
+		this.specialtyName = specialtyName;
+	}
+
+	public String getSpecialtyLevel() {
+		return specialtyLevel;
+	}
+
+	public void setSpecialtyLevel(String specialtyLevel) {
+		this.specialtyLevel = specialtyLevel;
+	}
+
+	public Long getCourseId() {
+		return courseId;
+	}
+
+	public void setCourseId(Long courseId) {
+		this.courseId = courseId;
+	}
+
+	public String getCourseName() {
+		return courseName;
+	}
+
+	public void setCourseName(String courseName) {
+		this.courseName = courseName;
+	}
+
+	public String getCourseCode() {
+		return courseCode;
+	}
+
+	public void setCourseCode(String courseCode) {
+		this.courseCode = courseCode;
+	}
+
+
+	public String getIsPhotoUpload() {
+		return isPhotoUpload;
+	}
+
+	public void setIsPhotoUpload(String isPhotoUpload) {
+		this.isPhotoUpload = isPhotoUpload;
+	}
+
+	public int getPaperMins() {
+		return paperMins;
+	}
+
+	public void setPaperMins(int paperMins) {
+		this.paperMins = paperMins;
+	}
+
+	public int getAllowExamCount() {
+		return allowExamCount;
+	}
+
+	public void setAllowExamCount(int allowExamCount) {
+		this.allowExamCount = allowExamCount;
+	}
+
+	public Date getStartTime() {
+		return startTime;
+	}
+
+	public void setStartTime(Date startTime) {
+		this.startTime = startTime;
+	}
+
+	public Date getEndTime() {
+		return endTime;
+	}
+
+	public void setEndTime(Date endTime) {
+		this.endTime = endTime;
+	}
+
+	public String getIsFinished() {
+		return isFinished;
+	}
+
+	public void setIsFinished(String isFinished) {
+		this.isFinished = isFinished;
+	}
+
+	public String getExamStatus() {
+		return examStatus;
+	}
+
+	public void setExamStatus(String examStatus) {
+		this.examStatus = examStatus;
+	}
+
+	public String getIdentityNumber() {
+		return identityNumber;
+	}
+
+	public void setIdentityNumber(String identityNumber) {
+		this.identityNumber = identityNumber;
+	}
+
+	public String getCourseLevel() {
+		return courseLevel;
+	}
+
+	public void setCourseLevel(String courseLevel) {
+		this.courseLevel = courseLevel;
+	}
+
+	/*public Boolean getIsvalid() {
+		return isvalid;
+	}
+
+	public void setIsvalid(Boolean isvalid) {
+		this.isvalid = isvalid;
+	}*/
+
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public String getExamName() {
+		return examName;
+	}
+
+	public void setExamName(String examName) {
+		this.examName = examName;
+	}
+
+	public Long getOrgId() {
+		return orgId;
+	}
+
+	public void setOrgId(Long orgId) {
+		this.orgId = orgId;
+	}
+
+	public String getOrgName() {
+		return orgName;
+	}
+
+	public void setOrgName(String orgName) {
+		this.orgName = orgName;
+	}
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public Boolean getFaceEnable() {
+		return faceEnable;
+	}
+
+	public void setFaceEnable(Boolean faceEnable) {
+		this.faceEnable = faceEnable;
+	}
+
+	public Boolean getFaceCheck() {
+		return faceCheck;
+	}
+
+	public void setFaceCheck(Boolean faceCheck) {
+		this.faceCheck = faceCheck;
+	}
+
+	public Boolean getIsObjScoreView() {
+		return isObjScoreView;
+	}
+
+	public void setIsObjScoreView(Boolean isObjScoreView) {
+		this.isObjScoreView = isObjScoreView;
+	}
+	
+}
+

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

@@ -0,0 +1,88 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import cn.com.qmth.examcloud.question.commons.core.question.AnswerType;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月25日 上午9:43:19
+ * @company 	QMTH
+ * @description 考生作答信息 
+ */
+public class ExamStudentQuestionInfo implements JsonSerializable{
+	
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 8080615797817377990L;
+
+	/**
+	 * 题目序号
+	 */
+	private Integer order;
+	
+	/**
+	 * 是否标记
+	 */
+	private Boolean isSign;
+	
+	/**
+	 * 考生作答
+	 */
+	private String studentAnswer;
+	
+	/**
+	 * 音频播放次数
+	 */
+	private String audioPlayTimes;
+	/**
+	 * 题目作答类型
+	 */
+	@Enumerated(EnumType.STRING)
+	private AnswerType answerType;
+
+	public Integer getOrder() {
+		return order;
+	}
+
+	public void setOrder(Integer order) {
+		this.order = order;
+	}
+
+	public String getStudentAnswer() {
+		return studentAnswer;
+	}
+
+	public void setStudentAnswer(String studentAnswer) {
+		this.studentAnswer = studentAnswer;
+	}
+
+	public Boolean getIsSign() {
+		return isSign;
+	}
+
+	public void setIsSign(Boolean isSign) {
+		this.isSign = isSign;
+	}
+
+	public String getAudioPlayTimes() {
+		return audioPlayTimes;
+	}
+
+	public void setAudioPlayTimes(String audioPlayTimes) {
+		this.audioPlayTimes = audioPlayTimes;
+	}
+
+	public AnswerType getAnswerType() {
+		return answerType;
+	}
+
+	public void setAnswerType(AnswerType answerType) {
+		this.answerType = answerType;
+	}
+	
+}

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

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

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

@@ -0,0 +1,64 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import javax.validation.constraints.NotNull;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+public class GetQrCodeReq implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -8684452576786540515L;
+	@NotNull(message = "考生ID不能为空")
+	@ApiModelProperty(required = true,value = "考生ID")
+	private Long examStudentId;
+	@NotNull(message = "考试记录DataID不能为空")
+	@ApiModelProperty(required = true,value = "考试记录DataID")
+	private Long examRecordDataId;
+	@NotNull(message = "题号不能为空")
+	@ApiModelProperty(required = true,value = "考试试题号")
+	private Integer order;
+	@ApiModelProperty(required = true,value = "传输文件类型")
+	private String transferFileType;
+
+	@ApiModelProperty(required = true,value = "是否用来测试环境")
+	private boolean testEnv;
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Integer getOrder() {
+		return order;
+	}
+	public void setOrder(Integer order) {
+		this.order = order;
+	}
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+
+	public String getTransferFileType() {
+		return transferFileType;
+	}
+
+	public void setTransferFileType(String transferFileType) {
+		this.transferFileType = transferFileType;
+	}
+
+	public boolean isTestEnv() {
+		return testEnv;
+	}
+
+	public void setTestEnv(boolean testEnv) {
+		this.testEnv = testEnv;
+	}
+}

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

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.validation.constraints.NotNull;
+
+public class GetUploadedFileAcknowledgeStatusReq implements JsonSerializable {
+
+    private static final long serialVersionUID = 2015767610417418141L;
+    @NotNull(message = "响应id不允许为空")
+    @ApiModelProperty(required = true, value = "响应id不允许为空")
+    private Long acknowledgeId;
+
+    public Long getAcknowledgeId() {
+        return acknowledgeId;
+    }
+
+    public void setAcknowledgeId(Long acknowledgeId) {
+        this.acknowledgeId = acknowledgeId;
+    }
+}

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

@@ -0,0 +1,24 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import javax.validation.constraints.NotNull;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+public class GetUploadedFileAnswerListReq implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 2015767610417418141L;
+	@NotNull(message = "考试记录DataID不能为空")
+	@ApiModelProperty(required = true,value = "考试记录DataID")
+	private Long examRecordDataId;
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+	
+}

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

@@ -0,0 +1,61 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import javax.validation.constraints.NotNull;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+public class GetUpyunSignatureReq implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -3761016079101373608L;
+	@NotNull(message = "考试记录DataID不能为空")
+	@ApiModelProperty(required = true,value = "考试记录DataID")
+	private Integer examRecordDataId;
+	@NotNull(message = "题号不能为空")
+	@ApiModelProperty(required = true,value = "考试试题号")
+	private Integer order;
+	@NotNull(message = "文件MD5不能为空")
+	@ApiModelProperty(required = true,value = "文件MD5")
+	private String fileMd5;
+	@NotNull(message = "文件后缀不能为空")
+	@ApiModelProperty(required = true,value = "文件后缀")
+	private String fileSuffix;
+	@ApiModelProperty(value = "文件名自定义参数")
+	private String ext;
+
+	public Integer getExamRecordDataId() {
+		return examRecordDataId;
+	}
+	public void setExamRecordDataId(Integer examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Integer getOrder() {
+		return order;
+	}
+	public void setOrder(Integer order) {
+		this.order = order;
+	}
+	public String getFileMd5() {
+		return fileMd5;
+	}
+	public void setFileMd5(String fileMd5) {
+		this.fileMd5 = fileMd5;
+	}
+	public String getFileSuffix() {
+		return fileSuffix;
+	}
+	public void setFileSuffix(String fileSuffix) {
+		this.fileSuffix = fileSuffix;
+	}
+	public String getExt() {
+		return ext;
+	}
+	public void setExt(String ext) {
+		this.ext = ext;
+	}
+	
+}

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

@@ -0,0 +1,125 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+
+import java.util.Date;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月27日 下午3:45:58
+ * @company 	QMTH
+ * @description 客观分信息
+ */
+public class ObjectiveScoreInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 3722050952090373601L;
+
+	/**
+	 * 考试记录ID
+	 */
+	private Long examRecordDataId;
+	
+	/**
+	 * 客观分总分
+	 */
+	private Double objectiveScore;
+	
+	/**
+	 * 第几次考
+	 */
+	private Integer examOrder;
+	
+	/**
+	 * 开始时间
+	 */
+	private Date startTime;
+	
+	/**
+	 * 交卷时间
+	 */
+	private Date endTime;
+	
+	/**
+	 * 是否待审核
+	 */
+	private Boolean isAuditing;
+	
+	/**
+	 * 是否违纪
+	 */
+	private Boolean isIllegality;
+
+	/**
+	 * 考试是否已结束
+	 */
+	private Boolean isExamEnded;
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Double getObjectiveScore() {
+		return objectiveScore;
+	}
+
+	public void setObjectiveScore(Double objectiveScore) {
+		this.objectiveScore = objectiveScore;
+	}
+
+	public Integer getExamOrder() {
+		return examOrder;
+	}
+
+	public void setExamOrder(Integer examOrder) {
+		this.examOrder = examOrder;
+	}
+
+	public Date getStartTime() {
+		return startTime;
+	}
+
+	public void setStartTime(Date startTime) {
+		this.startTime = startTime;
+	}
+
+	public Date getEndTime() {
+		return endTime;
+	}
+
+	public void setEndTime(Date endTime) {
+		this.endTime = endTime;
+	}
+
+	public Boolean getIsAuditing() {
+		return isAuditing;
+	}
+
+	public void setIsAuditing(Boolean isAuditing) {
+		this.isAuditing = isAuditing;
+	}
+
+	public Boolean getIsIllegality() {
+		return isIllegality;
+	}
+
+	public void setIsIllegality(Boolean isIllegality) {
+		this.isIllegality = isIllegality;
+	}
+
+	public Boolean getIsExamEnded() {
+		return isExamEnded;
+	}
+
+	public void setIsExamEnded(Boolean examEnded) {
+		isExamEnded = examEnded;
+	}
+}

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

@@ -0,0 +1,202 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamRecordStatus;
+
+
+
+/**
+ * 离线考试课程
+ * @author ting.yin
+ *
+ */
+public class OfflineExamCourseInfo implements Serializable {
+
+	private static final long serialVersionUID = 4102608764528925537L;
+
+	private Long examId;
+
+	private String examName;
+	
+	private String orgName;//学校名称
+
+	private Long examStudentId;
+
+	private String courseName;
+
+	private String courseCode;
+
+	private String courseLevel;
+
+	private String studentCode;
+
+	private String studentName;
+
+	private Date startTime;// 考试时间范围开始时间
+
+	private Date endTime; // 考试时间范围结束时间
+
+	private String specialtyName;
+
+	private Long examRecordDataId;
+	
+	private ExamRecordStatus status;//用于判断是否上传
+	
+	private String paperId;
+	
+	/**
+	 * 是否有效
+	 */
+	private Boolean isvalid;
+	
+	private String offlineFileUrl;//答卷地址
+	
+	private String fileName;//答卷文件名
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+
+	public String getCourseName() {
+		return courseName;
+	}
+
+	public void setCourseName(String courseName) {
+		this.courseName = courseName;
+	}
+
+	public String getCourseCode() {
+		return courseCode;
+	}
+
+	public void setCourseCode(String courseCode) {
+		this.courseCode = courseCode;
+	}
+
+	public String getStudentCode() {
+		return studentCode;
+	}
+
+	public void setStudentCode(String studentCode) {
+		this.studentCode = studentCode;
+	}
+
+	public String getStudentName() {
+		return studentName;
+	}
+
+	public void setStudentName(String studentName) {
+		this.studentName = studentName;
+	}
+
+	public Date getStartTime() {
+		return startTime;
+	}
+
+	public void setStartTime(Date startTime) {
+		this.startTime = startTime;
+	}
+
+	public Date getEndTime() {
+		return endTime;
+	}
+
+	public void setEndTime(Date endTime) {
+		this.endTime = endTime;
+	}
+
+	public String getExamName() {
+		return examName;
+	}
+
+	public void setExamName(String examName) {
+		this.examName = examName;
+	}
+
+	public String getCourseLevel() {
+		return courseLevel;
+	}
+
+	public void setCourseLevel(String courseLevel) {
+		this.courseLevel = courseLevel;
+	}
+
+	public String getSpecialtyName() {
+		return specialtyName;
+	}
+
+	public void setSpecialtyName(String specialtyName) {
+		this.specialtyName = specialtyName;
+	}
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Boolean getIsvalid() {
+		return isvalid;
+	}
+
+	public void setIsvalid(Boolean isvalid) {
+		this.isvalid = isvalid;
+	}
+
+	public String getPaperId() {
+		return paperId;
+	}
+
+	public void setPaperId(String paperId) {
+		this.paperId = paperId;
+	}
+
+	public ExamRecordStatus getStatus() {
+		return status;
+	}
+
+	public void setStatus(ExamRecordStatus status) {
+		this.status = status;
+	}
+
+	public String getOrgName() {
+		return orgName;
+	}
+
+	public void setOrgName(String orgName) {
+		this.orgName = orgName;
+	}
+
+	public String getOfflineFileUrl() {
+		return offlineFileUrl;
+	}
+
+	public void setOfflineFileUrl(String offlineFileUrl) {
+		this.offlineFileUrl = offlineFileUrl;
+	}
+
+	public String getFileName() {
+		return fileName;
+	}
+
+	public void setFileName(String fileName) {
+		this.fileName = fileName;
+	}
+
+}

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

@@ -0,0 +1,143 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.core.oe.common.enums.QuesStructType;
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+
+/**
+ * 试卷结构。一套试卷结构包含多条数据,即多个大题
+ * Created by zhengmin on 2016/8/18.
+ */
+public class PaperStructInfo implements JsonSerializable{
+
+    private static final long serialVersionUID = 3386140600282845440L;
+
+    /**
+     * 序号
+     */
+    private int index;
+
+    /**
+     * 试题类型
+     */
+    private QuesStructType questionType;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 总分
+     */
+    private double totalScore;
+
+    /**
+     * 题量,套题则算1题
+     */
+    private int count;
+
+    /**
+     * 试卷结构下的题目总量,套题和套题下的子题都算
+     */
+    private int questionCount;
+
+    /**
+     * 每小题分
+     */
+    private String score;
+    
+	/**
+	 * 答对数量
+	 */
+	private int succQuestionNum;
+	/**
+	 * 答错数量
+	 */
+	private int failQuestionNum;
+	/**
+	 * 未作答
+	 */
+	private Integer notAnsweredCount;
+
+    public int getIndex() {
+        return index;
+    }
+
+    public void setIndex(int index) {
+        this.index = index;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public double getTotalScore() {
+        return totalScore;
+    }
+
+    public void setTotalScore(double totalScore) {
+        this.totalScore = totalScore;
+    }
+
+    public int getCount() {
+        return count;
+    }
+
+    public void setCount(int count) {
+        this.count = count;
+    }
+
+    public String getScore() {
+        return score;
+    }
+
+    public void setScore(String score) {
+        this.score = score;
+    }
+
+    public QuesStructType getQuestionType() {
+        return questionType;
+    }
+
+    public void setQuestionType(QuesStructType questionType) {
+        this.questionType = questionType;
+    }
+
+    public int getQuestionCount() {
+        return questionCount;
+    }
+
+    public void setQuestionCount(int questionCount) {
+        this.questionCount = questionCount;
+    }
+
+	public int getSuccQuestionNum() {
+		return succQuestionNum;
+	}
+
+	public void setSuccQuestionNum(int succQuestionNum) {
+		this.succQuestionNum = succQuestionNum;
+	}
+
+	public int getFailQuestionNum() {
+		return failQuestionNum;
+	}
+
+	public void setFailQuestionNum(int failQuestionNum) {
+		this.failQuestionNum = failQuestionNum;
+	}
+
+	public Integer getNotAnsweredCount() {
+		return notAnsweredCount;
+	}
+
+	public void setNotAnsweredCount(Integer notAnsweredCount) {
+		this.notAnsweredCount = notAnsweredCount;
+	}
+    
+}

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

@@ -0,0 +1,164 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import java.util.Date;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class PracticeCourseInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 3648502707781974505L;
+
+	private Long examId;
+	    
+    private String examName;
+    
+    private String examType;
+
+    private Long examStudentId;
+
+    private String courseName;
+
+    private String courseCode;
+
+    private String studentCode;
+
+    private String studentName;
+
+    /**
+     * 练习次数
+     */
+    private long practiceCount;
+    /**
+     * 最高正确率
+     */
+    private double maxObjectiveAccuracy;
+    /**
+     * 平均正确率
+     */
+    private double aveObjectiveAccuracy;
+
+    /**
+     * 最近正确率
+     */
+    private double recentObjectiveAccuracy;
+
+    private Date startTime;//考试时间范围开始时间
+
+    private Date endTime; //考试时间范围结束时间
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public String getExamName() {
+		return examName;
+	}
+
+	public void setExamName(String examName) {
+		this.examName = examName;
+	}
+
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+
+	public String getCourseName() {
+		return courseName;
+	}
+
+	public void setCourseName(String courseName) {
+		this.courseName = courseName;
+	}
+
+	public String getCourseCode() {
+		return courseCode;
+	}
+
+	public void setCourseCode(String courseCode) {
+		this.courseCode = courseCode;
+	}
+
+	public String getStudentCode() {
+		return studentCode;
+	}
+
+	public void setStudentCode(String studentCode) {
+		this.studentCode = studentCode;
+	}
+
+	public String getStudentName() {
+		return studentName;
+	}
+
+	public void setStudentName(String studentName) {
+		this.studentName = studentName;
+	}
+
+	public long getPracticeCount() {
+		return practiceCount;
+	}
+
+	public void setPracticeCount(long practiceCount) {
+		this.practiceCount = practiceCount;
+	}
+
+	public double getMaxObjectiveAccuracy() {
+		return maxObjectiveAccuracy;
+	}
+
+	public void setMaxObjectiveAccuracy(double maxObjectiveAccuracy) {
+		this.maxObjectiveAccuracy = maxObjectiveAccuracy;
+	}
+
+	public double getAveObjectiveAccuracy() {
+		return aveObjectiveAccuracy;
+	}
+
+	public void setAveObjectiveAccuracy(double aveObjectiveAccuracy) {
+		this.aveObjectiveAccuracy = aveObjectiveAccuracy;
+	}
+
+	public double getRecentObjectiveAccuracy() {
+		return recentObjectiveAccuracy;
+	}
+
+	public void setRecentObjectiveAccuracy(double recentObjectiveAccuracy) {
+		this.recentObjectiveAccuracy = recentObjectiveAccuracy;
+	}
+
+	public Date getStartTime() {
+		return startTime;
+	}
+
+	public void setStartTime(Date startTime) {
+		this.startTime = startTime;
+	}
+
+	public Date getEndTime() {
+		return endTime;
+	}
+
+	public void setEndTime(Date endTime) {
+		this.endTime = endTime;
+	}
+
+	public String getExamType() {
+		return examType;
+	}
+
+	public void setExamType(String examType) {
+		this.examType = examType;
+	}
+
+}

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

@@ -0,0 +1,73 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import java.util.List;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月7日 上午10:23:19
+ * @company 	QMTH
+ * @description 答题情况统计
+ */
+public class PracticeDetailInfo implements JsonSerializable {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1712307616138306152L;
+
+	private Long id;
+
+	private String courseName;
+
+	private String courseCode;
+	/**
+	 * 客观题正确率
+	 */
+	private double objectiveAccuracy;
+	
+	private List<PaperStructInfo> paperStructInfos;
+	
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCourseName() {
+		return courseName;
+	}
+
+	public void setCourseName(String courseName) {
+		this.courseName = courseName;
+	}
+
+	public String getCourseCode() {
+		return courseCode;
+	}
+
+	public void setCourseCode(String courseCode) {
+		this.courseCode = courseCode;
+	}
+
+	public double getObjectiveAccuracy() {
+		return objectiveAccuracy;
+	}
+
+	public void setObjectiveAccuracy(double objectiveAccuracy) {
+		this.objectiveAccuracy = objectiveAccuracy;
+	}
+
+	public List<PaperStructInfo> getPaperStructInfos() {
+		return paperStructInfos;
+	}
+
+	public void setPaperStructInfos(List<PaperStructInfo> paperStructInfos) {
+		this.paperStructInfos = paperStructInfos;
+	}
+
+}

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

@@ -0,0 +1,135 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import java.util.Date;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+public class PracticeRecordInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -5420413503946583744L;
+
+	private Long id;
+
+	private String examName;
+
+	private String courseName;
+
+	private String courseCode;
+
+	private Date startTime;
+
+	private Date endTime;
+
+	/**
+	 * 考生考试使用的考试时长
+	 */
+	private Long usedExamTime;
+
+	/**
+	 * 题目数量
+	 */
+	private Integer totalQuestionCount;
+	/**
+	 * 答对数量
+	 */
+	private Integer succQuestionNum;
+	/**
+	 * 答错数量
+	 */
+	private Integer failQuestionNum;
+	/**
+	 * 未作答
+	 */
+	private Integer notAnsweredCount;
+	/**
+	 * 客观题正确率
+	 */
+	private Double objectiveAccuracy;
+	/**
+	 * 客观题数量
+	 */
+	private Integer objectiveNum;
+	
+	public Long getId() {
+		return id;
+	}
+	public void setId(Long id) {
+		this.id = id;
+	}
+	public String getExamName() {
+		return examName;
+	}
+	public void setExamName(String examName) {
+		this.examName = examName;
+	}
+	public String getCourseName() {
+		return courseName;
+	}
+	public void setCourseName(String courseName) {
+		this.courseName = courseName;
+	}
+	public String getCourseCode() {
+		return courseCode;
+	}
+	public void setCourseCode(String courseCode) {
+		this.courseCode = courseCode;
+	}
+	public Date getStartTime() {
+		return startTime;
+	}
+	public void setStartTime(Date startTime) {
+		this.startTime = startTime;
+	}
+	public Date getEndTime() {
+		return endTime;
+	}
+	public void setEndTime(Date endTime) {
+		this.endTime = endTime;
+	}
+	public Long getUsedExamTime() {
+		return usedExamTime;
+	}
+	public void setUsedExamTime(Long usedExamTime) {
+		this.usedExamTime = usedExamTime;
+	}
+	public Integer getTotalQuestionCount() {
+		return totalQuestionCount;
+	}
+	public void setTotalQuestionCount(Integer totalQuestionCount) {
+		this.totalQuestionCount = totalQuestionCount;
+	}
+	public Integer getSuccQuestionNum() {
+		return succQuestionNum;
+	}
+	public void setSuccQuestionNum(Integer succQuestionNum) {
+		this.succQuestionNum = succQuestionNum;
+	}
+	public Integer getFailQuestionNum() {
+		return failQuestionNum;
+	}
+	public void setFailQuestionNum(Integer failQuestionNum) {
+		this.failQuestionNum = failQuestionNum;
+	}
+	public Integer getNotAnsweredCount() {
+		return notAnsweredCount;
+	}
+	public void setNotAnsweredCount(Integer notAnsweredCount) {
+		this.notAnsweredCount = notAnsweredCount;
+	}
+	public Double getObjectiveAccuracy() {
+		return objectiveAccuracy;
+	}
+	public void setObjectiveAccuracy(Double objectiveAccuracy) {
+		this.objectiveAccuracy = objectiveAccuracy;
+	}
+	public Integer getObjectiveNum() {
+		return objectiveNum;
+	}
+	public void setObjectiveNum(Integer objectiveNum) {
+		this.objectiveNum = objectiveNum;
+	}
+	
+}

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

@@ -0,0 +1,63 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.validation.constraints.NotNull;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import cn.com.qmth.examcloud.core.oe.common.enums.FileAnswerAcknowledgeStatus;
+import io.swagger.annotations.ApiModelProperty;
+
+public class SaveUploadedFileAcknowledgeStatusReq implements JsonSerializable {
+
+    /**
+     *
+     */
+    private static final long serialVersionUID = 2015767610417418141L;
+    @NotNull(message = "考试记录DataID不能为空")
+    @ApiModelProperty(required = true, value = "考试记录DataID")
+    private Long examRecordDataId;
+    @NotNull(message = "题号不能为空")
+    @ApiModelProperty(required = true, value = "考试试题号")
+    private Integer order;
+    @NotNull(message = "文件路径(含文件名)不能为空")
+    @ApiModelProperty(required = true, value = "文件路径(含文件名)")
+    private String filePath;
+
+    @NotNull(message = "答复状态不请允许为空")
+    @ApiModelProperty(required = true,value = "答复状态(CONFIRMED:已确认,DISCARDED:已弃用)")
+    private String acknowledgeStatus;
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+
+    public Integer getOrder() {
+        return order;
+    }
+
+    public void setOrder(Integer order) {
+        this.order = order;
+    }
+
+    public String getFilePath() {
+        return filePath;
+    }
+
+    public void setFilePath(String filePath) {
+        this.filePath = filePath;
+    }
+
+    public String getAcknowledgeStatus() {
+        return acknowledgeStatus;
+    }
+
+    public void setAcknowledgeStatus(String acknowledgeStatus) {
+        this.acknowledgeStatus = acknowledgeStatus;
+    }
+}

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

@@ -0,0 +1,61 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import javax.validation.constraints.NotNull;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+public class SaveUploadedFileReq implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 5983488494596822561L;
+	@NotNull(message = "考生ID不能为空")
+	@ApiModelProperty(required = true,value = "考生ID")
+	private Long examStudentId;
+	@NotNull(message = "考试记录DataID不能为空")
+	@ApiModelProperty(required = true,value = "考试记录DataID")
+	private Long examRecordDataId;
+	@NotNull(message = "题号不能为空")
+	@ApiModelProperty(required = true,value = "考试试题号")
+	private Integer order;
+	@NotNull(message = "文件路径(含文件名)不能为空")
+	@ApiModelProperty(required = true,value = "文件路径(含文件名)")
+	private String filePath;
+	@ApiModelProperty(required = true,value = "传输文件类型(用于标识,通过二维码传输文件的类型)")
+	private String transferFileType;
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Integer getOrder() {
+		return order;
+	}
+	public void setOrder(Integer order) {
+		this.order = order;
+	}
+	public String getFilePath() {
+		return filePath;
+	}
+	public void setFilePath(String filePath) {
+		this.filePath = filePath;
+	}
+
+	public String getTransferFileType() {
+		return transferFileType;
+	}
+
+	public void setTransferFileType(String transferFileType) {
+		this.transferFileType = transferFileType;
+	}
+}

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

@@ -0,0 +1,81 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月27日 下午4:44:15
+ * @company 	QMTH
+ * @description StartExamInfo.java
+ */
+public class StartExamInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 7964522965649314640L;
+	
+	private Long examRecordDataId;
+	
+	private String courseCode;
+	
+	private String courseName;
+	
+	private String studentCode;
+	
+	private String studentName;
+	
+	/**
+	 * 考试时长
+	 */
+	private Integer duration;
+	/**
+	 * 活体检测开始分钟数
+	 */
+	private Integer faceVerifyMinute;
+	
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+	public Integer getDuration() {
+		return duration;
+	}
+	public void setDuration(Integer duration) {
+		this.duration = duration;
+	}
+	public Integer getFaceVerifyMinute() {
+		return faceVerifyMinute;
+	}
+	public void setFaceVerifyMinute(Integer faceVerifyMinute) {
+		this.faceVerifyMinute = faceVerifyMinute;
+	}
+	public String getCourseCode() {
+		return courseCode;
+	}
+	public void setCourseCode(String courseCode) {
+		this.courseCode = courseCode;
+	}
+	public String getCourseName() {
+		return courseName;
+	}
+	public void setCourseName(String courseName) {
+		this.courseName = courseName;
+	}
+	public String getStudentCode() {
+		return studentCode;
+	}
+	public void setStudentCode(String studentCode) {
+		this.studentCode = studentCode;
+	}
+	public String getStudentName() {
+		return studentName;
+	}
+	public void setStudentName(String studentName) {
+		this.studentName = studentName;
+	}
+	
+}

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

@@ -0,0 +1,52 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class UploadedFileAnswerInfo extends ExamQuestionAnswerExtensionInfo{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -8684452576786540515L;
+	@ApiModelProperty(value = "考试记录DataID")
+	private Long examRecordDataId;
+//	@ApiModelProperty(value = "考试试题ID")
+//	private String questionId;
+	@ApiModelProperty(value = "考生答案")
+	private String studentAnswer;
+	@ApiModelProperty(value = "大题号")
+	private Integer mainNumber;
+	@ApiModelProperty(value = "小题号")
+    private Integer order;
+	
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+//	public String getQuestionId() {
+//		return questionId;
+//	}
+//	public void setQuestionId(String questionId) {
+//		this.questionId = questionId;
+//	}
+	public String getStudentAnswer() {
+		return studentAnswer;
+	}
+	public void setStudentAnswer(String studentAnswer) {
+		this.studentAnswer = studentAnswer;
+	}
+	public Integer getMainNumber() {
+		return mainNumber;
+	}
+	public void setMainNumber(Integer mainNumber) {
+		this.mainNumber = mainNumber;
+	}
+	public Integer getOrder() {
+		return order;
+	}
+	public void setOrder(Integer order) {
+		this.order = order;
+	}
+}

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

@@ -0,0 +1,53 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+public class UpyunSignatureInfo implements JsonSerializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -2401837848131443152L;
+	@ApiModelProperty(value = "上传到又拍云的url")
+	private String uploadUrl;
+	@ApiModelProperty(value = "又拍云签名")
+	private String signature;
+	@ApiModelProperty(value = "又拍云policy")
+	private String policy;
+	@ApiModelProperty(value = "上传到又拍云的文件路径(含文件名)")
+	private String filePath;
+	@ApiModelProperty(value = "取又拍云文件时的域名")
+	private String upyunFileDomain;
+	public String getSignature() {
+		return signature;
+	}
+	public void setSignature(String signature) {
+		this.signature = signature;
+	}
+	public String getPolicy() {
+		return policy;
+	}
+	public void setPolicy(String policy) {
+		this.policy = policy;
+	}
+	public String getFilePath() {
+		return filePath;
+	}
+	public void setFilePath(String filePath) {
+		this.filePath = filePath;
+	}
+	public String getUploadUrl() {
+		return uploadUrl;
+	}
+	public void setUploadUrl(String uploadUrl) {
+		this.uploadUrl = uploadUrl;
+	}
+	public String getUpyunFileDomain() {
+		return upyunFileDomain;
+	}
+	public void setUpyunFileDomain(String upyunFileDomain) {
+		this.upyunFileDomain = upyunFileDomain;
+	}
+	
+}

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

@@ -0,0 +1,23 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月5日 下午3:25:40
+ * @company 	QMTH
+ * @description 学生端-考试记录审核接口
+ */
+public interface ExamAuditService {
+
+	/**
+	 * 活体检测失败,系统审核
+	 * @param examRecordDataEntity
+	 */
+	public void saveExamAuditByFaceVerifyFailed(Long examRecordDataId);
+	
+	/**
+	 * 抓拍照片为0,系统审核
+	 * @param examRecordDataEntity
+	 */
+	public void saveExamAuditByNoPhoto(Long examRecordDataId);
+}

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

@@ -0,0 +1,100 @@
+/*
+ * *************************************************
+ * Copyright (c) 2018 QMTH. All Rights Reserved.
+ * Created by Deason on 2018-08-29 10:44:05.
+ * *************************************************
+ */
+
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.CourseCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExamOrgSettingsCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExamSettingsCacheBean;
+
+/**
+ * @Description 网考缓存实体转换服务
+ * @Author lideyin
+ * @Date 2019/8/13 16:00
+ * @Version 1.0
+ */
+public class ExamCacheTransferHelper {
+
+    /**
+     * 获取缓存的考试信息
+     * @param examId 考试id
+     * @param orgId 组织机构id
+     * @return ExamBean
+     */
+    public static ExamBean getCachedExam(Long examId,Long orgId){
+        ExamOrgSettingsCacheBean examCacheBean = CacheHelper.getExamOrgSettings(examId,orgId);
+        return copyExamBeanFrom(examCacheBean);
+    }
+    /**
+     * 获取缓存的考试信息
+     * @param examId 考试id
+     * @return ExamBean
+     */
+    public static ExamBean getCachedExam(Long examId){
+        ExamSettingsCacheBean examCacheBean = CacheHelper.getExamSettings(examId);
+        return copyExamBeanFrom(examCacheBean);
+    }
+
+    /**
+     * 获取能在的课程
+     * @param courseId 课程id
+     * @return CourseBean
+     */
+    public static CourseBean getCachedCourse(Long courseId){
+        CourseCacheBean courseCacheBean = CacheHelper.getCourse(courseId);
+        return copyCourseBeanFrom(courseCacheBean);
+    }
+
+    private static ExamBean copyExamBeanFrom(ExamOrgSettingsCacheBean examCacheBean) {
+        ExamBean resultBean = new ExamBean();
+        resultBean.setId(examCacheBean.getId());
+        resultBean.setBeginTime(examCacheBean.getBeginTime());
+        resultBean.setCode(examCacheBean.getCode());
+        resultBean.setDuration(examCacheBean.getDuration());
+        resultBean.setEnable(examCacheBean.getEnable());
+        resultBean.setEndTime(examCacheBean.getEndTime());
+        resultBean.setExamLimit(examCacheBean.getExamLimit());
+        resultBean.setExamTimes(examCacheBean.getExamTimes());
+        resultBean.setExamType(examCacheBean.getExamType());
+        resultBean.setName(examCacheBean.getName());
+        resultBean.setRemark(examCacheBean.getRemark());
+        resultBean.setRootOrgId(examCacheBean.getRootOrgId());
+        return resultBean;
+    }
+
+    private static ExamBean copyExamBeanFrom(ExamSettingsCacheBean examCacheBean) {
+        ExamBean resultBean = new ExamBean();
+        resultBean.setId(examCacheBean.getId());
+        resultBean.setBeginTime(examCacheBean.getBeginTime());
+        resultBean.setCode(examCacheBean.getCode());
+        resultBean.setDuration(examCacheBean.getDuration());
+        resultBean.setEnable(examCacheBean.getEnable());
+        resultBean.setEndTime(examCacheBean.getEndTime());
+        resultBean.setExamLimit(examCacheBean.getExamLimit());
+        resultBean.setExamTimes(examCacheBean.getExamTimes());
+        resultBean.setExamType(examCacheBean.getExamType());
+        resultBean.setName(examCacheBean.getName());
+        resultBean.setRemark(examCacheBean.getRemark());
+        resultBean.setRootOrgId(examCacheBean.getRootOrgId());
+        return resultBean;
+    }
+
+    private static CourseBean copyCourseBeanFrom(CourseCacheBean courseCacheBean) {
+        CourseBean resultBean = new CourseBean();
+        resultBean.setCode(courseCacheBean.getCode());
+        resultBean.setEnable(courseCacheBean.getEnable());
+        resultBean.setId(courseCacheBean.getId());
+        resultBean.setLevel(courseCacheBean.getLevel());
+        resultBean.setName(courseCacheBean.getName());
+        resultBean.setRootOrgId(courseCacheBean.getRootOrgId());
+
+        return resultBean;
+    }
+}

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

@@ -0,0 +1,148 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamFileAnswerTempEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamingRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.HandInExamRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.HandInExamType;
+import cn.com.qmth.examcloud.core.oe.student.bean.*;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年8月13日 下午2:09:38
+ * @company 	QMTH
+ * @description 在线考试控制服务接口
+ */
+public interface ExamControlService {
+
+    /**
+     * 开始考试
+     * @param examStudentId
+     * @param user
+     */
+    public StartExamInfo startExam(Long examStudentId, User user);
+    
+    /**
+     * 结束考试前置处理
+     * @param studentId
+     * @return
+     */
+    public EndExamPreInfo endExamPre(Long studentId);
+
+    /**
+     * 考试心跳
+     * @param studentId
+     */
+    public long examHeartbeat(Long studentId);
+
+    /**
+     * 断点续考:检查正在进行中的考试
+     * @param studentId
+     */
+    public CheckExamInProgressInfo checkExamInProgress(Long studentId);
+	/**
+     * 获取考试记录信息
+     * @param examRecordDataId
+     */
+	public EndExamInfo getEndExamInfo(Long examRecordDataId);
+	
+	/**
+	 * 照片失败扫描处理
+	 */
+	public void handleByExamCaptureQueueFailedDispose(Long examRecordDataId);
+
+	/**获取上传考试音频所需的又拍云签名
+	 * @param req
+	 * @return
+	 */
+	UpyunSignatureInfo getUpyunSignature(GetUpyunSignatureReq req);
+
+	/**
+	 *  发送消息到websocket
+	 * @param examRecordDataId 考试记录id
+	 * @param order 题序号
+	 * @param fileUrl 文件路径
+	 * @param transferFileType 传输文件类型
+	 * @throws Exception
+	 */
+	public void sendFileAnswerToWebSocket(Long examRecordDataId, Integer order,
+										  String fileUrl, String transferFileType,Long userId) throws Exception;
+	public String getQrCode(GetQrCodeReq req,String key);
+
+	/**校验二维码
+	 * @param param
+	 * @return
+	 */
+	public CheckQrCodeInfo checkQrCode(String param);
+
+	/**成功上传音频结果推送(微信小程序调用)
+	 * @param req
+	 */
+	public ExamFileAnswerTempEntity saveUploadedFile(SaveUploadedFileReq req, User user);
+
+	/**查询上传音频结果推送状态(微信小程序调用)
+	 * @param req
+	 */
+	public String getUploadedFileAcknowledgeStatus(GetUploadedFileAcknowledgeStatusReq req);
+
+	public void saveUploadedFileAcknowledgeStatus(SaveUploadedFileAcknowledgeStatusReq req);
+
+	/**
+	 * 通过websocket发送二维码扫描信息
+	 * @param examRecordDataId
+	 * @param clientId
+	 * @throws Exception
+	 */
+	public void sendScanQrCodeToWebSocket(String clientId,Long examRecordDataId, Integer order) throws Exception;
+
+	/**
+	 * 获取已上传的文件答案列表(微信小程序调用)
+	 * @param req
+	 * @return
+	 */
+	public List<UploadedFileAnswerInfo> getUploadedFileAnswerList(@Valid GetUploadedFileAnswerListReq req);
+
+	String getQrCode(GetQrCodeReq req, User user);
+
+	void deleteExamFileAnswerTemp(SaveUploadedFileReq req);
+
+	/**
+	 * 交卷
+	 * @param examRecordData
+	 * @param handInExamType 交卷类型
+	 */
+	void handInExam(ExamRecordDataEntity examRecordData, HandInExamType handInExamType);
+
+	/**
+	 * 交卷的后续处理
+	 * @param examRecordDataId 考试记录id
+	 * @param handInExamType 交卷类型
+	 * @param studentId 学生id
+	 * @return boolean
+	 */
+    boolean processAfterHandInExam(Long examRecordDataId,Long studentId, HandInExamType handInExamType);
+
+	/**
+	 * 清理考试已结束的文件作答记录
+	 */
+	void cleanTempFileAnswers();
+
+	/**
+	 * 清理考试中的考试记录
+	 * @param examingRecordEntity
+	 */
+    void cleanExamingRecord(ExamingRecordEntity examingRecordEntity);
+
+	/**
+	 * 清理已交卷的考试记录
+	 * @param handInExamRecordEntity
+	 */
+	void cleanHandInExamRecord(HandInExamRecordEntity handInExamRecordEntity);
+
+}

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

@@ -0,0 +1,72 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamFaceLivenessVerifyEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.student.bean.GetFaceVerifyTokenInfo;
+
+import java.util.List;
+
+
+/**
+ * @author  	chenken
+ * @date    	2018年2月5日 上午8:44:19
+ * @company 	QMTH
+ * @description 活体检测服务接口
+ */
+public interface ExamFaceLivenessVerifyService {
+	
+	/**
+	 * 保存活体检测信息
+	 * @param examRecordDataId
+	 */
+	public ExamFaceLivenessVerifyEntity saveFaceVerify(Long examRecordDataId);
+	/**
+	 * 使用考试记录id查询人脸活体检测信息
+	 * @param examRecordDataId
+	 * @return
+	 */
+	public List<ExamFaceLivenessVerifyEntity> listFaceVerifyByExamRecordId(Long examRecordDataId);
+	/**
+	 * 人脸检测完成后回调处理
+	 * @param resultJson
+	 * @return
+	 */
+	public ExamFaceLivenessVerifyEntity faceIdNotify(String resultJson);
+	/**
+	 * 向faceId发起检测请求,返回token
+	 * @param examRecordDataId
+	 * @return
+	 */
+	public GetFaceVerifyTokenInfo getFaceVerifyToken(Long studentId,String bizNo);
+	
+	public ExamFaceLivenessVerifyEntity saveFaceVerifyByExamRecordDataId(Long examRecordDataId);
+	
+	/**
+	 * 根据ID查询
+	 * @param id
+	 * @return
+	 */
+	public ExamFaceLivenessVerifyEntity findFaceVerifyById(Long id);
+	
+	/**
+	 * 人脸检测超时处理
+	 * @param examRecordDataId
+	 */
+	public void faceTestTimeOut(Long examRecordDataId);
+	/**
+	 * 人脸活体检测结束处理
+	 * @param examRecordDataId
+	 * @param result
+	 */
+	public void faceTestEndHandle(Long examRecordDataId, String result);
+	/**
+	 * 断点续考,获取活体检测开启时间
+	 * @param examId
+	 * @param examRecordDataId
+	 * @param heartbeat
+	 * @return
+	 */
+	public Integer getFaceLivenessVerifyMinute(Long orgId,Long examId,Long examRecordDataId, Integer heartbeat);
+	
+}
+

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

@@ -0,0 +1,116 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import java.util.List;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.*;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamType;
+import cn.com.qmth.examcloud.core.oe.student.bean.CalculateFaceCheckResultInfo;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+
+/**
+ * @author chenken
+ * @date 2018/8/15 11:16
+ * @company QMTH
+ * @description 考试记录数据服务接口
+ */
+public interface ExamRecordDataService {
+	
+	/**
+	 * 创建ExamRecordDataEntity
+	 * @param examRecordEntity
+	 * @return
+	 */
+	ExamRecordDataEntity createExamRecordData(ExamRecordEntity examRecordEntity, ExamStudentEntity examStudentEntity, ExamBean examBean, boolean isFullyObjetive);
+
+    List<ExamRecordDataEntity> findByStatusAndExamTypeIn(ExamRecordStatus examRecordStatus, List<String> examTypeList);
+    
+    /**
+     * 根据考生ID查询所有有效考试记录
+     * @param examStudentId
+     * @return
+     */
+	List<ExamRecordDataEntity> findValidExamRecordDataByExamStudentId(Long examStudentId);
+    
+    /**
+     * 计算人脸检测结果
+     * @param examRecordDataEntity
+     * @return
+     */
+	CalculateFaceCheckResultInfo calculateFaceCheckResult(ExamRecordDataEntity examRecordDataEntity);
+
+	/**
+	 * 创建进行中的考试记录
+	 * @param examRecordId
+	 * @param examRecordDataId
+	 * @param studentId
+	 */
+    void createExamingRecord(Long examRecordId, Long examRecordDataId, Long studentId, ExamType examType);
+
+	/**
+	 * 创建已交卷的考试记录
+	 * @param examRecordId
+	 * @param examRecordDataId
+	 * @param studentId
+	 */
+	void createHandInExamRecord(Long examRecordId, Long examRecordDataId, Long studentId);
+
+	/**
+	 * 删除进行中考试记录id
+	 * @param examRecordId
+	 */
+	void deleteExamingRecord(Long examRecordId);
+
+    void deleteHandInExamRecord(Long examRecordId);
+
+	/**
+	 * 获取指定数量的考试中的数据
+	 * @param startId
+	 * @param limit
+	 * @return
+	 */
+	List<ExamingRecordEntity> getLimitExamingRecords(Long startId, int limit);
+
+	/**
+	 * 获取指定数量的已交卷的考试数据
+	 * @param startId
+	 * @param limit
+	 * @return
+	 */
+	List<HandInExamRecordEntity> getLimitHandInExamRecords(Long startId, int limit);
+
+    /**
+     * 计算活体检测结果
+     * @param examRecordDataEntity
+     */
+	ExamRecordDataEntity calculateFaceLivenessVerifyResult(ExamRecordDataEntity examRecordDataEntity);
+    
+    /**
+     * 创建离线考试记录
+     * @param examRecord
+     * @param fullyObjective
+     * @return
+     */
+	ExamRecordDataEntity createOfflineExamRecordData(ExamRecordEntity examRecord, Boolean fullyObjective);
+	
+	/**
+	 * 违纪自动审核
+	 * @return
+	 */
+	ExamRecordDataEntity examRecordAutoAudit(Boolean isNoPhotoAndIllegality, ExamRecordDataEntity examRecordData);
+
+	/**
+	 * 考试记录是否已结束
+	 * @param examRecordData 考试记录
+	 * @return boolean
+	 */
+	boolean isExamRecordEnded(ExamRecordDataEntity examRecordData);
+
+	/**
+	 * 考试记录是否已结束
+	 * @param examRecordDataId 考试记录id
+	 * @return boolean
+	 */
+    boolean isExamRecordEnded(Long examRecordDataId);
+
+}

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

@@ -0,0 +1,29 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+
+
+public interface ExamRecordForMarkingService {
+
+	/**
+	 * 保存阅卷相关数据-对外
+	 * @param examRecordData
+	 * @param objectiveQuestionTotalScore
+	 */
+	public void saveExamRecordForMarking(Long examRecordDataId);
+	
+	/**
+	 * 在线考试-保存阅卷相关数据-对内
+	 * @param examRecordData
+	 * @param objectiveQuestionTotalScore
+	 */
+	public void saveExamRecordForMarking(ExamRecordDataEntity examRecordData,double objectiveQuestionTotalScore);
+	
+	/**
+	 * 离线考试-保存阅卷相关数据-对内
+	 * @param examRecordData
+	 * @param fileName
+	 * @param fileUrl
+	 */
+	public void saveOffLineExamRecordForMarking(ExamRecordDataEntity examRecordData,String fileName,String fileUrl);
+}

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

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordPaperStructEntity;
+
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月1日 上午10:48:31
+ * @company 	QMTH
+ * @description 考试记录-试卷结构服务接口
+ */
+public interface ExamRecordPaperStructService {
+	
+	/**
+	 * 根据examRecordDataId查询考试记录试卷结构
+	 * @param examRecordDataId
+	 * @return
+	 */
+	public ExamRecordPaperStructEntity getExamRecordPaperStruct(Long examRecordDataId);
+	
+}

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

@@ -0,0 +1,52 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordQuestionsEntity;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamStudentQuestionInfo;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionStructure;
+
+import java.util.List;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月3日 上午10:56:56
+ * @company 	QMTH
+ * @description 考试记录-作答归档服务接口
+ */
+public interface ExamRecordQuestionsService {
+
+	/**
+	 * 计算主观题作答总长度
+	 * @param examRecordDataId
+	 * @return
+	 */
+	Integer calculationSubjectiveAnswerLength(Long examRecordDataId);
+
+	/**
+	 * 初始化ExamQuestion
+	 * @param examRecordDataId
+	 * @param paperStructByReorder
+	 */
+	ExamRecordQuestionsEntity createExamRecordQuestions(Long examRecordDataId, DefaultPaper paperStructByReorder);
+
+	/**
+	 * 获取考试作答记录并修复考试记录表的数据(如有必要)
+	 * @param examRecordData 考试记录
+	 * @return
+	 */
+    ExamRecordQuestionsEntity getExamRecordQuestionsAndFixExamRecordDataIfNecessary(ExamRecordDataEntity examRecordData);
+
+	/**
+	 *  获取考试作答记录并修复考试记录表的数据(如有必要)
+	 * @param examRecordDataId 考试记录id
+	 * @return
+	 */
+	ExamRecordQuestionsEntity getExamRecordQuestionsAndFixExamRecordDataIfNecessary(Long examRecordDataId);
+
+	void submitQuestionAnswer(Long studentId, List<ExamStudentQuestionInfo> examQuestionInfos);
+
+	DefaultQuestionStructure getQuestionContent(Long studentId, String questionId);
+}

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

@@ -0,0 +1,28 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamStudentEntity;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月1日 上午10:28:52
+ * @company 	QMTH
+ * @description 考试记录服务接口
+ */
+public interface ExamRecordService {
+
+	/**
+	 * 开始考试,创建考试记录
+	 * @param examStudentEntity	考生
+	 * @param examBean			考试
+	 * @param courseBean		课程
+	 * @param basePaperId		基础试卷ID-题库
+	 * @param paperStructId		新试卷结构ID-网考
+	 * @return
+	 */
+	public ExamRecordEntity createExamRecord(ExamStudentEntity examStudent,ExamBean examBean,CourseBean courseBean,String basePaperId,String paperStructId);
+	
+}

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

@@ -0,0 +1,43 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import java.util.List;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamScoreEntity;
+import cn.com.qmth.examcloud.core.oe.student.bean.ObjectiveScoreInfo;
+
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月3日 上午9:54:31
+ * @company 	QMTH
+ * @description 考试分数接口
+ */
+public interface ExamScoreService {
+
+	
+	public ExamScoreEntity saveExamScore(ExamRecordEntity examRecord, ExamRecordDataEntity examRecordData);
+	
+	/**
+	 * 离线考试初始化得分
+	 * @param examRecordDataId
+	 */
+	public void createExamScoreWithOffline(Long examRecordDataId);
+	
+	/**
+	 * 使用考生ID查询有效考试记录的成绩
+	 * @return
+	 */
+	public List<ExamScoreEntity> queryExamScoreListByExamStudentId(Long examStudentId);
+	
+	/**
+	 * 使用考生ID查询客观分信息 
+	 * 客观分是指考生完成过一次考试后,有效的客观分,
+	 * 如此条考试记录需要监考审核则显示为"待审",客观分显示时,展示次数(第几次考试)、开始时间、交卷时间,客观分;
+	 * @return
+	 */
+	public List<ObjectiveScoreInfo> queryObjectiveScoreList(Long examStudentId);
+	
+}

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

@@ -0,0 +1,33 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamSessionInfo;
+
+/**
+ * @author chenken
+ * @date 2018/8/15 9:24
+ * @company QMTH
+ * @description 考试会话接口
+ */
+public interface ExamSessionInfoService {
+
+    /**
+     * 保存
+     * @param key
+     * @param examSessionInfo
+     * @param timeout   秒
+     */
+    public void saveExamSessionInfo(Long studentId,ExamSessionInfo examSessionInfo,int timeout);
+
+    /**
+     * 获取
+     * @param key
+     * @return
+     */
+    public ExamSessionInfo getExamSessionInfo(Long studentId);
+
+    /**
+     * 删除
+     * @param key
+     */
+    public void deleteExamSessionInfo(Long studentId);
+}

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

@@ -0,0 +1,38 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import java.util.List;
+
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamStudentInfo;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamStudentEntity;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年8月13日 下午2:45:21
+ * @company 	QMTH
+ * @description 考生服务接口
+ */
+public interface ExamStudentService {
+
+	/**
+	 * 获取在线考试待考列表
+	 * @param studentId
+	 * @return
+	 */
+	public List<ExamStudentInfo> queryOnlineExamList(Long studentId);
+	
+	/**
+	 * 统计剩余考试次数
+	 * @param examStudentInfo
+	 * @return
+	 */
+	public Integer countExamTimes(ExamStudentEntity examStudentInfo,ExamBean examBean);
+	
+	/**
+	 * 开始考试时,更新考生信息
+	 * @param examStudentInfo
+	 * @param examBean
+	 */
+	public ExamStudentEntity updateExamStudentByStartExam(ExamStudentEntity examStudentEntity,ExamBean examBean);
+}

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

@@ -0,0 +1,42 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import java.io.File;
+import java.util.List;
+
+import cn.com.qmth.examcloud.core.oe.student.bean.OfflineExamCourseInfo;
+
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月5日 上午10:35:18
+ * @company 	QMTH
+ * @description 离线考试服务接口
+ */
+public interface OfflineExamService {
+	
+	/**
+	 * 获取待考课程列表
+	 * @param studentId
+	 * @return
+	 */
+	public List<OfflineExamCourseInfo> getOfflineCourse(Long studentId);
+
+	/**
+	 * 开始离线考试
+	 * @param token
+	 * @param examStudentId
+	 * @param studentId
+	 * @return
+	 */
+	public void startOfflineExam(Long examStudentId);
+	
+	/**
+	 * 上传作答
+	 * @param examRecordDataId
+	 * @param tempFile
+	 * @param fileType
+	 */
+	public void submitPaper(Long examRecordDataId,File tempFile)  throws Exception;
+	
+}

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

@@ -0,0 +1,38 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import java.util.List;
+
+import cn.com.qmth.examcloud.core.oe.student.bean.PracticeCourseInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.PracticeDetailInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.PracticeRecordInfo;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月7日 上午11:04:59
+ * @company 	QMTH
+ * @description PracticeService.java
+ */
+public interface PracticeService {
+
+	/**
+	 * 练习课程列表
+	 * @param examId
+	 * @param studentId
+	 * @return
+	 */
+	public List<PracticeCourseInfo> queryPracticeCourseList(Long examId,Long studentId);
+	
+	/**
+	 * 课程练习记录详情
+	 * @param examStudentId
+	 * @return
+	 */
+	public List<PracticeRecordInfo> queryPracticeRecordList(Long examStudentId);
+	
+	/**
+	 * 单次练习答题情况统计
+	 * @return
+	 */
+	public PracticeDetailInfo getPracticeDetailInfo(Long examRecordDataId);
+}

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

@@ -0,0 +1,57 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamingRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamingRecordRepo;
+import cn.com.qmth.examcloud.core.oe.student.bean.CleanExamRecordPriorityQueueInfo;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+public class CleanExamRecordQueueHolder {
+    public static final int HIGH_PRIORITY=1;
+    public static final int LOW_PRIORITY=0;
+    private static final BlockingQueue<CleanExamRecordPriorityQueueInfo> priorityQueue = new PriorityBlockingQueue<>();
+
+    /**
+     * 如果队列中数据不存在,则向队列添加数据,默认超时时间10秒
+     *
+     * @param element 数据对象
+     * @return boolean
+     */
+    public static boolean offer(CleanExamRecordPriorityQueueInfo element) throws InterruptedException {
+        return priorityQueue.offer(element, 10, TimeUnit.SECONDS);
+    }
+
+//    /**
+//     * 从队列中获取指定数量的考试记录id
+//     * @param limitNum 数量
+//     * @return List<ExamRecordDataEntity>
+//     */
+//    public static List<ExamingRecordEntity> getLimtExamRecordDataListFromQueue(int limitNum) throws InterruptedException {
+//        //如果除外中不足 limitNum条数据,则从数据库中查询limitNum条数据至队列中
+//        if (priorityQueue.size()<limitNum){
+//            ExamingRecordRepo examingRecordRepo = SpringContextHolder.getBean(ExamingRecordRepo.class);
+//            List<ExamingRecordEntity> examingRecordList = examingRecordRepo.findLimitExamingRecordList(limitNum);
+//            for (ExamingRecordEntity record:examingRecordList){
+//                CleanExamRecordPriorityQueueInfo info = new CleanExamRecordPriorityQueueInfo();
+//                //数据库中取出来的数据,默认低优先级处理
+//                info.setPriority(LOW_PRIORITY);
+//                info.setExamingRecord(record);
+//                if (!offer(info)){
+//                    throw new StatusException("100001","向清理考试队列中添加数据失败");
+//                }
+//            }
+//        }
+//        List<CleanExamRecordPriorityQueueInfo> priorityQueueInfoList = new ArrayList<>();
+//        priorityQueue.drainTo(priorityQueueInfoList,limitNum);
+//        return priorityQueueInfoList.stream()
+//                .map(CleanExamRecordPriorityQueueInfo::getExamingRecord)
+//                .collect(Collectors.toList());
+//    }
+}

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

@@ -0,0 +1,99 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import cn.com.qmth.examcloud.core.oe.common.base.utils.CommonUtil;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamAuditEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamFaceLivenessVerifyEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.AuditStatus;
+import cn.com.qmth.examcloud.core.oe.common.enums.DisciplineType;
+import cn.com.qmth.examcloud.core.oe.common.enums.FaceVerifyResult;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamAuditRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamFaceLivenessVerifyRepo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamAuditService;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月5日 下午3:25:06
+ * @company 	QMTH
+ * @description 学生端-考试记录审核服务实现
+ */
+@Service("examAuditService")
+public class ExamAuditServiceImpl implements ExamAuditService {
+
+	//private static final Logger log = LoggerFactory.getLogger(ExamAuditServiceImpl.class);
+	
+	/**
+	 * 活体检测失败自动审核
+	 */
+	private static final String FACE_LIVENESS_VERIFY_AUTO_AUDIT = "FACE_LIVENESS_VERIFY_AUTO_AUDIT";
+	
+	/**
+	 * 无照片自动审核
+	 */
+	private static final String NOPHOTO_VERIFY_AUTO_AUDIT = "NOPHOTO_VERIFY_AUTO_AUDIT";
+	
+	/**
+	 * 系统审核
+	 */
+	private static final String AUDIT_USER_NAME = "SYSTEM";
+	
+	@Autowired
+	private ExamFaceLivenessVerifyRepo examFaceLivenessVerifyRepo;
+	
+	@Autowired
+	private ExamAuditRepo examAuditRepo;
+	
+	@Override
+	public void saveExamAuditByFaceVerifyFailed(Long examRecordDataId) {
+		ExamAuditEntity examAudit = examAuditRepo.findByExamRecordDataId(examRecordDataId);
+		if(examAudit != null){
+			return;
+		}
+		ExamAuditEntity examAuditEntity = new ExamAuditEntity();
+		examAuditEntity.setExamRecordDataId(examRecordDataId);
+		examAuditEntity.setDisciplineType(DisciplineType.ACTION_FAILURE);
+		examAuditEntity.setUserId(FACE_LIVENESS_VERIFY_AUTO_AUDIT);//活体检测自动审核
+		examAuditEntity.setAuditUserName(AUDIT_USER_NAME);
+		
+		List<ExamFaceLivenessVerifyEntity> faceVerifies = examFaceLivenessVerifyRepo.findByExamRecordDataIdOrderById(examRecordDataId);
+		if(faceVerifies == null || faceVerifies.size() == 0){
+			examAuditEntity.setDisciplineDetail("未进行人脸动作检测");
+		}else{
+			StringBuffer sbBuffer = new StringBuffer("");
+			for(ExamFaceLivenessVerifyEntity faceVerify:faceVerifies){
+				String startTime = CommonUtil.getDateStrWithSecond(faceVerify.getStartTime());
+				sbBuffer.append(startTime+":"+FaceVerifyResult.getDescByName(faceVerify.getVerifyResult()));
+				sbBuffer.append("&&");
+			}
+			String disciplineDetail = sbBuffer.toString();
+			if(disciplineDetail.length()>500){
+				disciplineDetail = disciplineDetail.substring(0,500);
+			}
+			examAuditEntity.setDisciplineDetail(disciplineDetail);
+		}
+		examAuditEntity.setStatus(AuditStatus.UN_PASS);
+		examAuditRepo.save(examAuditEntity);
+	}
+
+	@Override
+	public void saveExamAuditByNoPhoto(Long examRecordDataId) {
+		ExamAuditEntity examAudit = examAuditRepo.findByExamRecordDataId(examRecordDataId);
+		if(examAudit != null){
+			return;
+		}
+		ExamAuditEntity examAuditEntity = new ExamAuditEntity();
+		examAuditEntity.setExamRecordDataId(examRecordDataId);
+		examAuditEntity.setDisciplineType(DisciplineType.OTHER);
+		examAuditEntity.setDisciplineDetail("抓拍照片数量为0");
+		examAuditEntity.setUserId(NOPHOTO_VERIFY_AUTO_AUDIT);//抓拍照片为0自动审核
+		examAuditEntity.setAuditUserName(AUDIT_USER_NAME);
+		examAuditEntity.setStatus(AuditStatus.UN_PASS);
+		examAuditRepo.save(examAuditEntity);
+	}
+	
+}

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

@@ -0,0 +1,1571 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+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.ByteUtil;
+import cn.com.qmth.examcloud.commons.util.SHA256;
+import cn.com.qmth.examcloud.commons.util.UrlUtil;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.CommonUtil;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.QuestionTypeUtil;
+import cn.com.qmth.examcloud.core.oe.common.entity.*;
+import cn.com.qmth.examcloud.core.oe.common.enums.*;
+import cn.com.qmth.examcloud.core.oe.common.repository.*;
+import cn.com.qmth.examcloud.core.oe.common.service.ExamScoreObtainQueueService;
+import cn.com.qmth.examcloud.core.oe.common.service.ExamScorePushQueueService;
+import cn.com.qmth.examcloud.core.oe.common.service.GainBaseDataService;
+import cn.com.qmth.examcloud.core.oe.student.bean.*;
+import cn.com.qmth.examcloud.core.oe.student.face.api.ExamCaptureQueueCloudService;
+import cn.com.qmth.examcloud.core.oe.student.service.*;
+import cn.com.qmth.examcloud.core.oe.websocket.api.FileAnswerWebsocketCloudService;
+import cn.com.qmth.examcloud.core.oe.websocket.api.enums.WebSocketEventType;
+import cn.com.qmth.examcloud.core.oe.websocket.api.request.SendFileAnswerMessageReq;
+import cn.com.qmth.examcloud.core.oe.websocket.api.request.SendScanQrCodeMessageReq;
+import cn.com.qmth.examcloud.core.questions.api.ExtractConfigCloudService;
+import cn.com.qmth.examcloud.examwork.api.ExamCloudService;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+import cn.com.qmth.examcloud.examwork.api.request.GetExamPropertyReq;
+import cn.com.qmth.examcloud.examwork.api.response.GetExamPropertyResp;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionGroup;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionStructureWrapper;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionUnitWrapper;
+import cn.com.qmth.examcloud.question.commons.core.question.QuestionType;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.ExtractConfigCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExtractConfigDetailCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExtractConfigPaperCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import com.google.common.base.Splitter;
+import main.java.com.upyun.Base64Coder;
+import main.java.com.upyun.UpException;
+import main.java.com.upyun.UpYunUtils;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.time.DateUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.validation.Valid;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * @author chenken
+ * @date 2018年8月13日 下午2:09:08
+ * @company QMTH
+ * @description 在线考试控制服务实现
+ */
+@Service("examControlService")
+public class ExamControlServiceImpl implements ExamControlService {
+    private static final String SESSION_TIMEOUT = "$core.basic.sessionTimeout";
+    // 又拍云签名有效时间(秒)
+    private static final Integer SIGN_TIMEOUT = 60;
+    private static final String SEPARATOR = "/";
+    private static final String UNDERLINE = "_";
+    // 又拍云音频答案上传目录
+    private static final String OE_ANSWER_FILE_PATH = "oe-answer-file";
+    private static final Logger log = LoggerFactory.getLogger(ExamControlServiceImpl.class);
+    private static final Log cleanExamRecordTaskLog = LogFactory.getLog("CLEAN_EXAM_RECORD_TASK_LOGGER");
+    // 发送题目答案序号集合
+//    private static ConcurrentHashMap<String,Integer> sendQuestionAnswerSequenceMap=new ConcurrentHashMap<String,Integer>();
+    @Autowired
+    private ExamStudentService examStudentService;
+    @Autowired
+    private ExamCloudService examCloudService;
+    @Autowired
+    private ExamSessionInfoService examSessionInfoService;
+
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+
+    @Autowired
+    private ExamRecordQuestionsService examRecordQuestionsService;
+
+    @Autowired
+    private ExamRecordService examRecordService;
+
+    @Autowired
+    private ExamScoreService examScoreService;
+
+    @Autowired
+    private ExamCaptureQueueCloudService examCaptureQueueCloudService;
+
+    @Autowired
+    private ExamFaceLivenessVerifyService examFaceLivenessVerifyService;
+
+    @Autowired
+    private ExamRecordForMarkingService examRecordForMarkingService;
+
+    @Autowired
+    private GainBaseDataService gainBaseDataService;
+
+    @Autowired
+    private ExamStudentRepo examStudentRepo;
+
+    @Autowired
+    private ExamScoreRepo examScoreRepo;
+
+    @Autowired
+    private ExamRecordDataRepo examRecordDataRepo;
+    @Autowired
+    private ExamingRecordRepo examingRecordRepo;
+
+    @Autowired
+    private ExamRecordPaperStructRepo examRecordPaperStructRepo;
+
+    @Autowired
+    private ExamFaceLivenessVerifyRepo examFaceLivenessVerifyRepo;
+
+    @Autowired
+    private RedisClient redisClient;
+
+    @Autowired
+    private ExamScorePushQueueService examScorePushQueueService;
+
+    @Autowired
+    private ExamScoreObtainQueueService examScoreObtainQueueService;
+    @Autowired
+    private ExamFileAnswerTempRepo examFileAnswerTempRepo;
+
+    @Autowired
+    private FileAnswerWebsocketCloudService fileAnswerWebsocketCloudService;
+    @Autowired
+    private ExamCaptureQueueRepo examCaptureQueueRepo;
+    @Autowired
+    private ExamRecordRepo examRecordRepo;
+
+    @Autowired
+    private ExtractConfigCloudService extractConfigCloudService;
+    @Value("${audio.app.url}")
+    private String audioAppUrl;
+    @Value("${$upyun.site.1.bucketName}")
+    private String bucketName;
+
+    @Value("${$upyun.site.1.userName}")
+    private String userName;
+
+    @Value("${$upyun.site.1.password}")
+    private String password;
+    @Value("https://v0.api.upyun.com")
+    private String bucketUrl;
+    @Value("${$upyun.site.1.domain}")
+    private String upyunFileUrl;
+
+    private Object lock = new Object();
+
+    @Transactional
+    @Override
+    public StartExamInfo startExam(Long examStudentId, User user) {
+        long st = System.currentTimeMillis();
+        SysPropertyCacheBean stuClientLoginLimit = CacheHelper.getSysProperty("STU_CLIENT_LOGIN_LIMIT");
+        Boolean stuClientLoginLimitBoolean = false;
+        if (stuClientLoginLimit.getHasValue()) {
+            stuClientLoginLimitBoolean = Boolean.valueOf(stuClientLoginLimit.getValue().toString());
+        }
+        if (stuClientLoginLimitBoolean) {
+            throw new StatusException("OE-001505", "系统维护中... ...");
+        }
+
+//        try {
+        long startTime = System.currentTimeMillis();
+        // 获取考生信息
+        ExamStudentEntity examStudentEntity = examStudentRepo.findByExamStudentId(examStudentId);
+        if (log.isDebugEnabled()) {
+            log.debug("1 获取考生信息耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+        if (examStudentEntity == null) {
+            throw new StatusException("ExamControlServiceImpl-startExam-exception", "考生不存在");
+        }
+        if (examStudentEntity.getStudentId().longValue() != user.getUserId().longValue()) {
+            throw new StatusException("ExamControlServiceImpl-startExam-exception", "考生与当前用户不吻合");
+        }
+        // 检查redis session
+        ExamSessionInfo examSessionInfo = examSessionInfoService
+                .getExamSessionInfo(examStudentEntity.getStudentId());
+        if (examSessionInfo != null) {
+            throw new StatusException("ExamControlServiceImpl-startExam-exception", "已经有考试中的科目");
+        }
+        // 检查并获取考试信息
+        startTime = System.currentTimeMillis();
+        ExamBean examBean = checkExam(examStudentEntity);
+        if (log.isDebugEnabled()) {
+            log.debug("2 检查并获取考试信息耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 检查并获取课程信息
+        startTime = System.currentTimeMillis();
+        CourseBean courseBean = checkCourse(examStudentEntity);
+        if (log.isDebugEnabled()) {
+            log.debug("3 检查并获取课程信息耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 获取题库试卷结构(由于存在随机抽卷,所以不能缓存 )
+        startTime = System.currentTimeMillis();
+
+        //获取题库调卷规则
+        ExtractConfigCacheBean extractConfig = CacheHelper.getExtractConfig(
+                examStudentEntity.getExamId(), courseBean.getCode());
+        //随机生成试卷
+        Map<String, String> paperTypeMaps = getExamPaperByProbability(extractConfig.getDetails());
+        if (paperTypeMaps.isEmpty()) {
+            throw new StatusException("100001", "生成试卷失败");
+        }
+
+        String paperId = paperTypeMaps.get(examStudentEntity.getPaperType());
+        if (StringUtils.isEmpty(paperId)) {
+            throw new StatusException("100002", "获取试卷失败");
+        }
+        //生成试卷结构
+        ExtractConfigPaperCacheBean extractConfigPaper = CacheHelper.getExtractConfigPaper(examStudentEntity.getExamId(),
+                courseBean.getCode(), examStudentEntity.getPaperType(), paperId);
+        if (log.isDebugEnabled()) {
+            log.debug("4 获取题库试卷结构耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 小题乱序,选项乱序
+        startTime = System.currentTimeMillis();
+        reorderPaperStruct(extractConfig, extractConfigPaper);
+        if (log.isDebugEnabled()) {
+            log.debug("5 小题乱序耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 保存考试试卷结构
+        startTime = System.currentTimeMillis();
+        ExamRecordPaperStructEntity examRecordPaperStruct = new ExamRecordPaperStructEntity();
+        examRecordPaperStruct.setDefaultPaper(extractConfigPaper.getDefaultPaper());
+        examRecordPaperStruct = examRecordPaperStructRepo.save(examRecordPaperStruct);
+        if (log.isDebugEnabled()) {
+            log.debug("6 保存考试试卷结构耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 更新考生信息
+        startTime = System.currentTimeMillis();
+        examStudentEntity=examStudentService.updateExamStudentByStartExam(examStudentEntity, examBean);
+        if (log.isDebugEnabled()) {
+            log.debug("7 更新考生信息耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 生成考试记录
+        startTime = System.currentTimeMillis();
+        ExamRecordEntity examRecord = examRecordService.createExamRecord(examStudentEntity, examBean, courseBean,
+                paperId, examRecordPaperStruct.getId());
+        ExamRecordDataEntity examRecordData = examRecordDataService.createExamRecordData(examRecord,
+                examStudentEntity, examBean, extractConfigPaper.getDefaultPaper().getFullyObjective());
+        //生成进行中的考试记录
+        examRecordDataService.createExamingRecord(examRecord.getId(), examRecordData.getId(),
+                examRecord.getStudentId(), examRecord.getExamType());
+
+        if (log.isDebugEnabled()) {
+            log.debug("8 生成考试记录耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 创建考试作答记录
+        startTime = System.currentTimeMillis();
+        ExamRecordQuestionsEntity examRecordQuestions = examRecordQuestionsService.createExamRecordQuestions(
+                examRecordData.getId(), extractConfigPaper.getDefaultPaper());
+        if (log.isDebugEnabled()) {
+            log.debug("9 创建考试作答记录耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        //更新考试记录(冗余考试记录id)
+        startTime = System.currentTimeMillis();
+        examRecordData.setExamRecordQuestionsId(examRecordQuestions.getId());
+        examRecordDataRepo.updateExamRecordDataQuestionIdById(examRecordQuestions.getId(), examRecordData.getId());
+        if (log.isDebugEnabled()) {
+            log.debug("10 更新考试记录耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 创建考试会话
+        startTime = System.currentTimeMillis();
+
+        initializeExamRecordSession(examStudentEntity, examRecordData, examBean);
+        if (log.isDebugEnabled()) {
+            log.debug("11 创建考试会话耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("12 合计 耗时:" + (System.currentTimeMillis() - st) + " ms");
+        }
+        return buildStartExamInfo(examRecordData.getId(), examStudentEntity, examBean, courseBean);
+//        } finally {
+//
+//        }
+
+    }
+
+    /**
+     * 每个试卷类型取出一套试卷
+     * {A:paperId,B:paperId} A是试卷类型,paperId是A类型下选定的试卷ID
+     */
+    private Map<String, String> getExamPaperByProbability(List<ExtractConfigDetailCacheBean> examPapers) {
+        if (CollectionUtils.isEmpty(examPapers)) {
+            throw new StatusException("500", "可供抽取的试卷集合为空!");
+        }
+
+        Map<String, List<ExtractConfigDetailCacheBean>> examPaperMaps = new HashMap<>();
+        for (ExtractConfigDetailCacheBean examPaper : examPapers) {
+            if (examPaperMaps.containsKey(examPaper.getGroupCode())) {
+                examPaperMaps.get(examPaper.getGroupCode()).add(examPaper);
+            } else {
+                List<ExtractConfigDetailCacheBean> list = new ArrayList<>();
+                list.add(examPaper);
+                examPaperMaps.put(examPaper.getGroupCode(), list);
+            }
+        }
+
+        Map<String, String> paperTypeMaps = new HashMap<>();
+        for (Map.Entry<String, List<ExtractConfigDetailCacheBean>> entry : examPaperMaps.entrySet()) {
+            List<ExtractConfigDetailCacheBean> list = examPaperMaps.get(entry.getKey());
+
+            String paperId = this.getPaperByProbability(list);
+            if (StringUtils.isEmpty(paperId)) {
+                continue;
+            }
+
+            paperTypeMaps.put(entry.getKey(), paperId);
+        }
+
+        return paperTypeMaps;
+    }
+
+    /**
+     * 根据设定几率取出一套试卷
+     */
+    private String getPaperByProbability(List<ExtractConfigDetailCacheBean> examPapers) {
+        int sum = 0;
+        for (ExtractConfigDetailCacheBean examPaper : examPapers) {
+            sum += examPaper.getWeight();
+        }
+
+        // 从1开始
+        int r = new Random().nextInt(sum) + 1;
+        for (ExtractConfigDetailCacheBean examPaper : examPapers) {
+            r -= examPaper.getWeight();
+            if (r <= 0) {
+                return examPaper.getPaperId();// 选中
+            }
+        }
+
+        return null;
+    }
+
+    private StartExamInfo buildStartExamInfo(Long examRecordDataId, ExamStudentEntity examStudentEntity,
+                                             ExamBean examBean, CourseBean courseBean) {
+        StartExamInfo startExamInfo = new StartExamInfo();
+        startExamInfo.setExamRecordDataId(examRecordDataId);
+        startExamInfo.setStudentCode(examStudentEntity.getStudentCode());
+        startExamInfo.setStudentName(examStudentEntity.getStudentName());
+        startExamInfo.setCourseCode(examStudentEntity.getCourseCode());
+        startExamInfo.setCourseName(courseBean.getName());
+        startExamInfo.setDuration(examBean.getDuration());
+        startExamInfo.setFaceVerifyMinute(getFaceVerifyMinute(examBean.getId(), examStudentEntity.getOrgId()));
+        return startExamInfo;
+    }
+
+    /**
+     * 确定活体检测开始分钟数
+     *
+     * @param examId
+     * @return
+     */
+    private Integer getFaceVerifyMinute(Long examId, Long orgId) {
+        String isFaceVerifyStr = CacheHelper.getExamOrgProperty(
+                examId, orgId, ExamProperties.IS_FACE_VERIFY.name()).getValue();
+        // 如果开启了活体检测
+        if (Constants.isTrue.equals(isFaceVerifyStr)) {
+            // 开始分钟数
+            String startMinuteStr = CacheHelper.getExamOrgProperty(
+                    examId, orgId, ExamProperties.FACE_VERIFY_START_MINUTE.name()).getValue();
+            if (CommonUtil.isBlank(startMinuteStr)) {
+                throw new StatusException("ExamControlServiceImpl-getFaceVerifyMinute-001",
+                        ExamProperties.FACE_VERIFY_START_MINUTE.getDesc() + "未设置");
+            }
+            Integer faceVerifyStartMinute = Integer.valueOf(startMinuteStr);
+
+            // 结束分钟数
+            String endMinuteStr = CacheHelper.getExamOrgProperty(
+                    examId, orgId, ExamProperties.FACE_VERIFY_END_MINUTE.name()).getValue();
+            if (CommonUtil.isBlank(endMinuteStr)) {
+                throw new StatusException("ExamControlServiceImpl-getFaceVerifyMinute-002",
+                        ExamProperties.FACE_VERIFY_END_MINUTE.getDesc() + "未设置");
+            }
+            Integer faceVerifyEndMinute = Integer.valueOf(endMinuteStr);
+            return CommonUtil.calculationRandomNumber(faceVerifyStartMinute, faceVerifyEndMinute);
+        }
+        return null;
+    }
+
+    private CourseBean checkCourse(ExamStudentEntity examStudentEntity) {
+        CourseBean courseBean = ExamCacheTransferHelper.getCachedCourse(examStudentEntity.getCourseId());
+        if (!courseBean.getEnable()) {
+            throw new StatusException("ExamControlServiceImpl-checkCourse-exception", "该课程已被禁用");
+        }
+        return courseBean;
+    }
+
+    /**
+     * 检查并返回考试 开考条件 1.enable为true 2.开始时间和结束时间判断 3.examLimit为null或false 4.剩余考试次数>0
+     *
+     * @param examStudentEntity
+     * @return
+     */
+    private ExamBean checkExam(ExamStudentEntity examStudentEntity) {
+
+//                examStudentEntity.getOrgId());
+        ExamBean examBean = ExamCacheTransferHelper.getCachedExam(examStudentEntity.getExamId(),
+                examStudentEntity.getOrgId());
+        if (!examBean.getEnable() || (examBean.getExamLimit() != null && examBean.getExamLimit())) {
+            throw new StatusException("ExamControlServiceImpl-checkExam-exception-01", "暂无考试资格,请与学校老师联系");
+        }
+        if (new Date().before(examBean.getBeginTime())) {
+            throw new StatusException("ExamControlServiceImpl-checkExam-exception-02", "考试未开始");
+        }
+        if (examBean.getEndTime().before(new Date())) {
+            throw new StatusException("ExamControlServiceImpl-checkExam-exception-03", "本次考试已结束");
+        }
+        if (ExamType.ONLINE.name().equals(examBean.getExamType())
+                || ExamType.PRACTICE.name().equals(examBean.getExamType())) {
+            if (examBean.getExamTimes() == null
+                    || examStudentService.countExamTimes(examStudentEntity, examBean) <= 0) {
+                throw new StatusException("ExamControlServiceImpl-checkExam-exception-04", "无剩余考试次数可用");
+            }
+        }
+        return examBean;
+    }
+
+
+    /**
+     * 创建考试会话
+     *
+     * @param examRecordData
+     * @param examBean
+     */
+    public void initializeExamRecordSession(final ExamStudentEntity examStudent,
+                                            final ExamRecordDataEntity examRecordData, final ExamBean examBean) {
+        ExamSessionInfo examSessionInfo = new ExamSessionInfo();
+        examSessionInfo.setExamRecordId(examRecordData.getExamRecordId());
+        examSessionInfo.setExamRecordDataId(examRecordData.getId());
+        examSessionInfo.setExamStudentId(examStudent.getExamStudentId());
+        examSessionInfo.setStartTime(examRecordData.getStartTime().getTime());
+        long examDuration = examBean.getDuration() * 60 * 1000;
+        examSessionInfo.setExamDuration(examDuration);
+        examSessionInfo.setHeartbeat(-1);
+        examSessionInfo.setExamType(examBean.getExamType());
+        examSessionInfo.setExamId(examBean.getId());
+        examSessionInfo.setCourseCode(examStudent.getCourseCode());
+        examSessionInfo.setPaperType(examStudent.getPaperType());
+        // EXAM_RECONNECT_TIME:断点续考时间
+        String examReconnectTimeStr = CacheHelper.getExamOrgProperty(examBean.getId(), examStudent.getOrgId(),
+                ExamProperties.EXAM_RECONNECT_TIME.name()).getValue();
+        log.debug("11.2 断点时间:" + examReconnectTimeStr);
+        if (CommonUtil.isBlank(examReconnectTimeStr)) {
+            throw new StatusException("ExamControlServiceImpl-initializeExamRecordSession-001",
+                    ExamProperties.EXAM_RECONNECT_TIME.getDesc() + "未设置");
+        }
+        examSessionInfo.setExamReconnectTime(Integer.valueOf(examReconnectTimeStr));
+        // FREEZE_TIME:冻结时间
+        String freezeTimeStr = CacheHelper.getExamOrgProperty(examBean.getId(), examStudent.getOrgId(),
+                ExamProperties.FREEZE_TIME.name()).getValue();
+        log.debug("11.3 冻结时间:" + freezeTimeStr);
+        if (CommonUtil.isBlank(freezeTimeStr)) {
+            throw new StatusException("ExamControlServiceImpl-initializeExamRecordSession-002",
+                    ExamProperties.FREEZE_TIME.getDesc() + "未设置");
+        }
+        examSessionInfo.setFreezeTime(Integer.valueOf(freezeTimeStr));
+
+        log.debug("11.4 开始保存考试会话...studentId=" + examStudent.getStudentId() + ",timeOut=" + examSessionInfo.getExamReconnectTime() * 60);
+        // 过期时间=断点续考时间
+        examSessionInfoService.saveExamSessionInfo(examStudent.getStudentId(), examSessionInfo,
+                examSessionInfo.getExamReconnectTime() * 60);
+        log.debug("11.5 保存考试会话结束 ");
+    }
+
+    /**
+     * 获取试卷结构 小题乱序、选项乱序
+     *
+     * @param extractConfig 调卷规则对象
+     * @param paperStruct   试卷结构对象
+     */
+    private void reorderPaperStruct(ExtractConfigCacheBean extractConfig, ExtractConfigPaperCacheBean paperStruct) {
+        // 小题乱序
+        if (extractConfig.getSortQuestionOrder() != null && extractConfig.getSortQuestionOrder()) {
+            reorderQuestion(paperStruct.getDefaultPaper());
+        }
+        // 选项乱序
+        if (extractConfig.getSortOptionOrder() != null && extractConfig.getSortOptionOrder()) {
+            reorderOption(paperStruct.getDefaultPaper());
+        }
+    }
+
+    /**
+     * 小题乱序
+     *
+     * @param defaultPaper
+     */
+    private void reorderQuestion(DefaultPaper defaultPaper) {
+        List<DefaultQuestionGroup> defaultQuestionGroupList = defaultPaper.getQuestionGroupList();
+        for (int i = 0; i < defaultQuestionGroupList.size(); i++) {
+            DefaultQuestionGroup defaultQuestionGroup = defaultQuestionGroupList.get(i);
+            if (checkObjectiveQuestionByGroup(defaultQuestionGroup)) {
+                List<DefaultQuestionStructureWrapper> questionStructureWrapperList = defaultQuestionGroup
+                        .getQuestionWrapperList();
+                Collections.shuffle(questionStructureWrapperList);
+            }
+        }
+    }
+
+    /**
+     * 选项乱序
+     *
+     * @param defaultPaper
+     */
+    private void reorderOption(DefaultPaper defaultPaper) {
+        List<DefaultQuestionGroup> defaultQuestionGroupList = defaultPaper.getQuestionGroupList();
+        // 遍历大题
+        for (int i = 0; i < defaultQuestionGroupList.size(); i++) {
+            DefaultQuestionGroup defaultQuestionGroup = defaultQuestionGroupList.get(i);
+            List<DefaultQuestionStructureWrapper> questionStructureWrapperList = defaultQuestionGroup
+                    .getQuestionWrapperList();
+            // 遍历小题
+            for (DefaultQuestionStructureWrapper defaultQuestionStructureWrapper : questionStructureWrapperList) {
+                List<DefaultQuestionUnitWrapper> questionUnitWrapperList = defaultQuestionStructureWrapper
+                        .getQuestionUnitWrapperList();
+                // 遍历题单元
+                for (DefaultQuestionUnitWrapper defaultQuestionUnitWrapper : questionUnitWrapperList) {
+                    if (QuestionTypeUtil.isChoiceQuestion(defaultQuestionUnitWrapper.getQuestionType())) {
+                        Integer[] optionPermutation = defaultQuestionUnitWrapper.getOptionPermutation();
+                        defaultQuestionUnitWrapper.setOptionPermutation(CommonUtil.reorderArray(optionPermutation));
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 小题乱序条件:一个大题下全是单一的客观题,例如全是单选,全是多选,全是判断 判断一个大题下是否是单一的客观题
+     *
+     * @return
+     */
+    private boolean checkObjectiveQuestionByGroup(DefaultQuestionGroup defaultQuestionGroup) {
+        Set<QuestionType> questionTypes = new HashSet<QuestionType>();
+        List<DefaultQuestionStructureWrapper> questionWrapperList = defaultQuestionGroup.getQuestionWrapperList();
+        for (int i = 0; i < questionWrapperList.size(); i++) {
+            List<DefaultQuestionUnitWrapper> questionUnitWrapperList = questionWrapperList.get(i)
+                    .getQuestionUnitWrapperList();
+            if (questionUnitWrapperList.size() > 1) {
+                return false;
+            } else {
+                questionTypes.add(questionUnitWrapperList.get(0).getQuestionType());
+            }
+        }
+        if (questionTypes.size() > 1) {
+            return false;
+        }
+        QuestionType questionType = questionTypes.iterator().next();
+        if (QuestionTypeUtil.isObjectiveQuestion(questionType)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 计算考试时长 校验是否达到冻结时间
+     *
+     * @param examSessionInfo
+     * @return
+     * @Param studentId
+     */
+    private Long checkAndComputeExamDuration(ExamSessionInfo examSessionInfo, Long studentId) {
+        long st = System.currentTimeMillis();
+        long startTime = System.currentTimeMillis();
+
+        long now = System.currentTimeMillis();
+        Long lastHeartbeat = examSessionInfo.getLastHeartbeat();
+        long usedTime = examSessionInfo.getHeartbeat() * 60 * 1000;// 考试时长=心跳次数*60*1000
+        if (lastHeartbeat == null) {
+            lastHeartbeat = now;
+            usedTime = 1 * 60 * 1000;// 算作1分钟
+        }
+
+        if (now - lastHeartbeat < 60 * 1000) {
+            // 如果最后一次心跳和当前时间小于1分钟,则把零碎的时间也加进去
+            usedTime += now - lastHeartbeat;
+        }
+        // 如果没有超过冻结时间,抛出异常
+        if (examSessionInfo.getExamType().equals(ExamType.ONLINE.name())) {
+            startTime = System.currentTimeMillis();
+            ExamingRecordEntity rec = GlobalHelper.getEntity(examingRecordRepo, examSessionInfo.getExamRecordId(), ExamingRecordEntity.class);
+            if (log.isDebugEnabled()) {
+                log.debug("1.1 [CHECK_AND_COMPUTE_EXAMD_URATION]获取状态为examing的进行中的考试记录 耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+            }
+
+            if (rec != null && rec.getIsExceed() != null && rec.getIsExceed()) {// 超过断点最大次数的不校验冻结时间
+                return usedTime;
+            }
+            long freezeTime = examSessionInfo.getFreezeTime() * 60 * 1000;
+            if (usedTime < freezeTime) {
+                throw new StatusException("ExamControlServiceImpl-checkAndComputeExamDuration-exception",
+                        "开考" + examSessionInfo.getFreezeTime() + "分钟后才能交卷");
+            }
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("1.2 [CHECK_AND_COMPUTE_EXAMD_URATION]合计 耗时:" + (System.currentTimeMillis() - st) + " ms");
+        }
+        return usedTime;
+    }
+
+    @Override
+    public EndExamPreInfo endExamPre(Long studentId) {
+        long st = System.currentTimeMillis();
+        long startTime = System.currentTimeMillis();
+        // 获取考试会话,判断考生是否已结束考试
+        ExamSessionInfo examSessionInfo = examSessionInfoService.getExamSessionInfo(studentId);
+        if (examSessionInfo == null) {
+            return null;
+        }
+        if (log.isDebugEnabled()) {
+            log.debug("1 [END_EXAM_PRE]获取考试会话 耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        long examRecordDataId = examSessionInfo.getExamRecordDataId();
+
+        startTime = System.currentTimeMillis();
+        // 得到考试时长,校验是否达到冻结时间
+        long usedExamTime = checkAndComputeExamDuration(examSessionInfo, studentId);
+        if (log.isDebugEnabled()) {
+            log.debug("2 [END_EXAM_PRE]校验是否达到冻结时间 耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        startTime = System.currentTimeMillis();
+        // 查询考试记录
+        ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, examRecordDataId,
+                ExamRecordDataEntity.class);
+        examRecordData.setUsedExamTime(usedExamTime);
+        EndExamPreInfo endExamPreInfo = new EndExamPreInfo();
+        endExamPreInfo.setStudentId(studentId);
+        endExamPreInfo.setExamRecordData(examRecordData);
+        if (log.isDebugEnabled()) {
+            log.debug("3 [END_EXAM_PRE]查询考试记录 耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("4 [END_EXAM_PRE]合计 耗时:" + (System.currentTimeMillis() - st) + " ms");
+        }
+        return endExamPreInfo;
+    }
+
+    /**
+     * @param examRecordData 考试记录对象
+     * @param handInExamType 交卷类型
+     */
+    @Override
+    @Transactional
+    public void handInExam(ExamRecordDataEntity examRecordData, HandInExamType handInExamType) {
+        //如果当前考试记录状态不为考试中,则直接返回
+        if (examRecordData.getExamRecordStatus() != ExamRecordStatus.EXAM_ING) {
+            return;
+        }
+        long st = System.currentTimeMillis();
+        long startTime = System.currentTimeMillis();
+
+        ExamRecordEntity examRecord = GlobalHelper.getEntity(examRecordRepo, examRecordData.getExamRecordId(), ExamRecordEntity.class);
+        if (log.isDebugEnabled()) {
+            log.debug("1  [HAND_IN_EXAM]获取考试记录耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        if (handInExamType == HandInExamType.MANUAL) {
+            startTime = System.currentTimeMillis();
+            examRecordData.setExamRecordStatus(ExamRecordStatus.EXAM_HAND_IN);
+            examRecordData.setEndTime(new Date());
+
+            //手工手卷时,如果开启人脸检测,则更新抓拍队列优先级
+            String isFaceEnable = CacheHelper.getExamOrgProperty(examRecord.getExamId(), examRecord.getOrgId(),
+                    ExamProperties.IS_FACE_ENABLE.name()).getValue();
+            if (isFaceEnable != null && Constants.isTrue.equals(isFaceEnable)) {
+                //更新照片抓拍队列优先级为高优先级
+                examCaptureQueueRepo.updateExamCaptureQueuePriority(Constants.PROCESS_CAPTURE_HIGH_PRIORITY, examRecordData.getId());
+                if (log.isDebugEnabled()) {
+                    log.debug("2.1  [HAND_IN_EXAM]更新照片抓拍队列优先级为高优先级耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+                }
+            }
+        } else if (handInExamType == HandInExamType.AUTO) {
+            examRecordData.setExamRecordStatus(ExamRecordStatus.EXAM_AUTO_HAND_IN);
+            examRecordData.setCleanTime(new Date());
+        } else {
+            throw new StatusException("201101", "暂不支持的交卷类型");
+        }
+
+        startTime = System.currentTimeMillis();
+        //将考试中的断点相关信息更新到考试记录表中
+        ExamingRecordEntity examingRecord = GlobalHelper.getEntity(examingRecordRepo, examRecordData.getExamRecordId(), ExamingRecordEntity.class);
+        examRecordData.setIsContinued(examingRecord.getIsContinued());
+        examRecordData.setContinuedCount(examingRecord.getContinuedCount());
+        examRecordData.setIsExceed(examingRecord.getIsExceed());
+
+        // 保存考试记录
+        examRecordData = examRecordDataRepo.save(examRecordData);
+        if (log.isDebugEnabled()) {
+            log.debug("2.2 [HAND_IN_EXAM]保存考试记录 耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        startTime = System.currentTimeMillis();
+        // 计算保存考试分数
+        examScoreService.saveExamScore(examRecord,examRecordData);
+        if (log.isDebugEnabled()) {
+            log.debug("3 [HAND_IN_EXAM]计算保存考试分数耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+
+        startTime = System.currentTimeMillis();
+        //把进行中的考试记录放入交卷队列中
+        examRecordDataService.createHandInExamRecord(examRecord.getId(), examRecordData.getId(), examRecord.getStudentId());
+        if (log.isDebugEnabled()) {
+            log.debug("4 [HAND_IN_EXAM]把进行中的考试记录放入交卷队列中耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        startTime = System.currentTimeMillis();
+        //删除进行中的考试记录数据
+        examRecordDataService.deleteExamingRecord(examRecordData.getExamRecordId());
+        if (log.isDebugEnabled()) {
+            log.debug("5 [HAND_IN_EXAM]删除进行中的考试记录数据耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        startTime = System.currentTimeMillis();
+        // 删除redis会话
+        examSessionInfoService.deleteExamSessionInfo(examRecord.getStudentId());
+        if (log.isDebugEnabled()) {
+            log.debug("6 [HAND_IN_EXAM]删除redis会话:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+        if (log.isDebugEnabled()) {
+            log.debug("7 [HAND_IN_EXAM]合计 耗时:" + (System.currentTimeMillis() - st) + " ms");
+        }
+    }
+
+    @Override
+    @Transactional
+    public boolean processAfterHandInExam(Long examRecordDataId, Long studentId, HandInExamType handInExamType) {
+        ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, examRecordDataId, ExamRecordDataEntity.class);
+
+        //如果考试记录状态为已处理,则直接返回true.
+        if (examRecordDataService.isExamRecordEnded(examRecordData)) {
+            return true;
+        }
+
+        //判断是否存在未处理的图片
+        boolean existUnhandledExamCaptureQueue =
+                (examCaptureQueueRepo.existsUnhandledByExamRecordDataId(examRecordDataId) != null);
+        if (existUnhandledExamCaptureQueue) {
+            throw new StatusException(Constants.CAPTURE_PROCESSING_STATUS_CODE, "PROCESSING");
+        }
+
+        // 处理照片上锁,处理抓拍照片已经完成,此时不允许再次上传抓拍照片,锁默认存在2分钟
+        redisClient.set(Constants.EXAM_CAPTURE_PHOTO_LOCK_PREFIX + examRecordDataId, 1, 120);
+
+        // 计算人脸检测结果(根据已检测出照片数据更新考试记录中的相关人脸检测的统计字段)
+        CalculateFaceCheckResultInfo calculateFaceCheckResultInfo = examRecordDataService
+                .calculateFaceCheckResult(examRecordData);
+        examRecordData = calculateFaceCheckResultInfo.getExamRecordData();
+        // 计算活体检测结果(根据已检测出照片数据更新考试记录中的相关人脸检测的统计字段)
+        examRecordData = examRecordDataService.calculateFaceLivenessVerifyResult(examRecordData);
+        // 违纪自动审核
+        examRecordData = examRecordDataService.examRecordAutoAudit(calculateFaceCheckResultInfo.getIsNoPhotoAndIllegality(),
+                examRecordData);
+
+        final ExamScoreEntity examScore = examScoreRepo.findByExamRecordDataId(examRecordData.getId());
+        // 保存阅卷相关数据
+        examRecordForMarkingService.saveExamRecordForMarking(examRecordData, examScore.getObjectiveScore());
+
+        //更新考试记录相关数据
+        if (handInExamType == HandInExamType.MANUAL) {
+            examRecordData.setExamRecordStatus(ExamRecordStatus.EXAM_END);
+            //根产品确认,自动服务清理时,不再更新考试结束时间,以交卷时的时间为准。
+            //            examRecordData.setEndTime(new Date());
+        } else if (handInExamType == HandInExamType.AUTO) {
+            examRecordData.setExamRecordStatus(ExamRecordStatus.EXAM_OVERDUE);
+            examRecordData.setCleanTime(new Date());
+        } else {
+            throw new StatusException("201101", "暂不支持的交卷类型");
+        }
+        examRecordDataRepo.save(examRecordData);
+        // 保存考试分数数据到推分队列
+        examScorePushQueueService.saveScoreDataInfoToQueue(examRecordData, examScore.getId());
+        // 保存考试分数数据到分数获取队列
+        examScoreObtainQueueService.saveExamScoreObtainQueue(examRecordData);
+
+        //删除已交卷的考试记录数据
+        examRecordDataService.deleteHandInExamRecord(examRecordData.getExamRecordId());
+
+        return true;
+    }
+
+    @Override
+    public void cleanTempFileAnswers() {
+        //默认删除7天以前的数据
+        int days = PropertyHolder.getInt("oe.cleanTempFileAnswer.thresholdDays", 7);
+        Date dateBeforeDays = DateUtils.addDays(new Date(), -1 * days);
+        examFileAnswerTempRepo.deleteByCreationTimeBefore(dateBeforeDays);
+    }
+
+    /**
+     * 考试心跳每分钟调用一次
+     *
+     * @param studentId 学生id
+     */
+    @Override
+    public long examHeartbeat(Long studentId) {
+        ExamSessionInfo examSessionInfo = examSessionInfoService.getExamSessionInfo(studentId);
+        if (examSessionInfo == null) {
+            throw new StatusException("ExamControlServiceImpl-examHeartbeat-exception", "会话已过期,请离开考试!");
+        }
+        long now = System.currentTimeMillis();
+        if (examSessionInfo.getLastHeartbeat() != null) {
+            if (now - examSessionInfo.getLastHeartbeat() >= examSessionInfo.getExamReconnectTime() * 60 * 1000) {
+                // 如果当前时间和上一次心跳间隔超过了断点续考时间,则删除考试会话,并停止心跳
+                examSessionInfoService.deleteExamSessionInfo(studentId);
+                return 0L;
+            }
+        }
+        // 记录最后心跳时间
+        examSessionInfo.setLastHeartbeat(now);
+        // 更新心跳次数
+        examSessionInfo.setHeartbeat(examSessionInfo.getHeartbeat() + 1);
+        // 如果进行过人脸活体检测,过期时间加1分钟
+        Long faceVerifyTimes = examFaceLivenessVerifyRepo
+                .countByExamRecordDataId(examSessionInfo.getExamRecordDataId());
+        int addTimes = faceVerifyTimes == null ? 0 : 1;
+        // 会话过期时间:秒
+        int expireTime = (examSessionInfo.getExamReconnectTime() + addTimes + 1) * 60;
+        // 更新考试会话过期时间
+        examSessionInfoService.saveExamSessionInfo(studentId, examSessionInfo, expireTime);
+        // 返回考试剩余时间 :考试时长-心跳次数*60000
+        return examSessionInfo.getExamDuration() - (examSessionInfo.getHeartbeat() * 60 * 1000);
+    }
+
+    @Override
+    public CheckExamInProgressInfo checkExamInProgress(Long studentId) {
+        ExamSessionInfo examSessionInfo = examSessionInfoService.getExamSessionInfo(studentId);
+        // 检查考试会话是否存在,或者是否失效,如果没有失效,则返回考试中的考试记录实体,否则直接返回null
+        ExamingRecordEntity examingRecord = checkExamSession(examSessionInfo, studentId);
+        if (examingRecord == null) {
+            return null;
+        } else {
+            ExamStudentEntity examStudentEntity = examStudentRepo.findByExamStudentId(examSessionInfo.getExamStudentId());
+            Integer maxInterruptNum = getMaxInterruptNum(examStudentEntity.getExamId(), examStudentEntity.getRootOrgId(), examStudentEntity.getOrgId());
+            CheckExamInProgressInfo checkExamInProgressInfo = new CheckExamInProgressInfo();
+
+            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);
+                checkExamInProgressInfo.setIsExceed(false);
+            } else {
+                examingRecord.setIsExceed(true);
+                checkExamInProgressInfo.setIsExceed(true);
+            }
+            //更新考试中的断点续考属性
+            examingRecordRepo.save(examingRecord);
+
+            checkExamInProgressInfo.setExamRecordDataId(examingRecord.getExamRecordDataId());
+            checkExamInProgressInfo.setExamId(examSessionInfo.getExamId());
+            checkExamInProgressInfo.setUsedTime(examSessionInfo.getHeartbeat() * 60 * 1000);
+            checkExamInProgressInfo.setMaxInterruptNum(maxInterruptNum);
+            checkExamInProgressInfo.setInterruptNum(examingRecord.getContinuedCount());
+
+            // 断点续考时重新计算活体检测的分钟数
+            checkExamInProgressInfo.setFaceVerifyMinute(examFaceLivenessVerifyService.getFaceLivenessVerifyMinute(
+                    examStudentEntity.getOrgId(), examStudentEntity.getExamId(),
+                    examingRecord.getExamRecordDataId(), examSessionInfo.getHeartbeat()));
+            return checkExamInProgressInfo;
+        }
+    }
+
+    private ExamingRecordEntity checkExamSession(ExamSessionInfo examSessionInfo, Long studentId) {
+        ExamingRecordEntity examingRecord = examingRecordRepo.findOnlineExamingRecord(studentId);
+        // 如果考试记录不存在,清除会话
+        if (examingRecord == null) {
+            examSessionInfoService.deleteExamSessionInfo(studentId);
+            return null;
+        }
+
+        // 如果会话不存在,自动交卷
+        if (examSessionInfo == null) {
+            // 查询考试记录
+            ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, examingRecord.getExamRecordDataId(),
+                    ExamRecordDataEntity.class);
+            handInExam(examRecordData, HandInExamType.AUTO);
+            return null;
+        }
+        // 如果已经过了断点续考时间,清除考试记录,清除会话
+        Long lastHeartbeat = examSessionInfo.getLastHeartbeat();
+        if (lastHeartbeat == null) {
+            lastHeartbeat = examSessionInfo.getStartTime();
+        }
+        long now = System.currentTimeMillis();
+        if (now - lastHeartbeat >= examSessionInfo.getExamReconnectTime().intValue() * 60 * 1000) {
+            examSessionInfoService.deleteExamSessionInfo(studentId);
+            ExamRecordDataEntity examRecordDataEntity = GlobalHelper.getEntity(examRecordDataRepo,
+                    examingRecord.getExamRecordDataId(), ExamRecordDataEntity.class);
+            handInExam(examRecordDataEntity, HandInExamType.AUTO);
+            return null;
+        }
+        return examingRecord;
+    }
+
+    @Override
+    public void cleanExamingRecord(ExamingRecordEntity examingRecord) {
+
+        //只有考试会话结束的考试才执行清理动作
+        ExamSessionInfo examSessionInfo = examSessionInfoService.getExamSessionInfo(examingRecord.getStudentId());
+        if (examSessionInfo != null) {
+            cleanExamRecordTaskLog.debug("[CLEAN_EXAMING_RECORD_" + examingRecord.getExamRecordDataId() + "]考试未结束,不执行清理.");
+            return;
+        }
+
+        ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, examingRecord.getExamRecordDataId(), ExamRecordDataEntity.class);
+
+        //只有考试状态为考试中,系统才需要执行交卷动作,因为有可能已经手动交卷,所以这里再做一次判断
+        if (examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_ING) {
+            cleanExamRecordTaskLog.debug("[CLEAN_EXAMING_RECORD_" + examRecordData.getId() + "]状态为进行中考试,执行交卷[handInExam],.");
+            handInExam(examRecordData, HandInExamType.AUTO);
+        }
+
+        cleanExamRecordTaskLog.debug("[CLEAN_EXAMING_RECORD_" + examRecordData.getId() + "]考试记录交卷完成.");
+    }
+
+    @Override
+    public void cleanHandInExamRecord(HandInExamRecordEntity handInExamRecord) {
+        ExamRecordEntity examRecord = GlobalHelper.getEntity(examRecordRepo, handInExamRecord.getId(), ExamRecordEntity.class);
+        ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, handInExamRecord.getExamRecordDataId(), ExamRecordDataEntity.class);
+
+        //只清理状态为已交卷的数据,才能执行交卷后续动作
+        if (examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_AUTO_HAND_IN ||
+                examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_HAND_IN) {
+            //默认自动交卷
+            HandInExamType handInExamType = HandInExamType.AUTO;
+            if (examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_AUTO_HAND_IN) {
+                handInExamType = HandInExamType.AUTO;
+            } else if (examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_HAND_IN) {
+                handInExamType = HandInExamType.MANUAL;
+            }
+            cleanExamRecordTaskLog.debug("[CLEAN_HAND_IN_EXAM_RECORD_" + examRecordData.getId() + "]开始执行交卷后续动作,.");
+            try {
+                processAfterHandInExam(handInExamRecord.getExamRecordDataId(), examRecord.getStudentId(), handInExamType);
+            } catch (StatusException e) {
+                if (e.getCode().equals(Constants.CAPTURE_PROCESSING_STATUS_CODE)) {
+                    cleanExamRecordTaskLog.debug("[CLEAN_HAND_IN_EXAM_RECORD_" + examRecordData.getId() + "]有未处理完成的图片,稍侯重试...");
+                    return;
+                }
+                throw e;
+            } catch (Exception e) {
+                throw e;
+            }
+        }
+
+        cleanExamRecordTaskLog.debug("[CLEAN_HAND_IN_EXAM_RECORD_" + examRecordData.getId() + "]考试记录交卷后续动作完成.");
+    }
+
+
+    @Override
+    public EndExamInfo getEndExamInfo(Long examRecordDataId) {
+        ExamScoreEntity examScore = examScoreRepo.findByExamRecordDataId(examRecordDataId);
+        if (examScore == null) {
+            return null;
+        }
+        ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, examRecordDataId,
+                ExamRecordDataEntity.class);
+        //如果考试没有最终结束,则返回空值
+        if (examRecordData.getExamRecordStatus() != ExamRecordStatus.EXAM_END &&
+                examRecordData.getExamRecordStatus() != ExamRecordStatus.EXAM_OVERDUE) {
+            return null;
+        }
+        EndExamInfo endExamInfo = new EndExamInfo();
+        endExamInfo.setExamRecordDataId(examRecordDataId);
+        endExamInfo.setIsWarn(examRecordData.getIsWarn());// 是否异常数据
+        endExamInfo.setObjectiveScore(examScore.getObjectiveScore());// 客观题总分
+        endExamInfo.setObjectiveAccuracy(examScore.getObjectiveAccuracy());// 客观点答对比率
+        return endExamInfo;
+    }
+
+    @Override
+    public void handleByExamCaptureQueueFailedDispose(Long examRecordDataId) {
+        // 查询考试记录
+        ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, examRecordDataId,
+                ExamRecordDataEntity.class);
+        // 计算人脸检测结果
+        CalculateFaceCheckResultInfo calculateFaceCheckResultInfo = examRecordDataService
+                .calculateFaceCheckResult(examRecordData);
+        examRecordData = calculateFaceCheckResultInfo.getExamRecordData();
+        examRecordDataRepo.save(examRecordData);
+        // 保存阅卷相关数据
+        ExamScoreEntity examScore = examScoreRepo.findByExamRecordDataId(examRecordDataId);
+        examRecordForMarkingService.saveExamRecordForMarking(examRecordData, examScore.getObjectiveScore());
+        // 保存考试分数数据到推分队列
+        examScorePushQueueService.saveScoreDataInfoToQueue(examRecordData, examScore.getId());
+        // 保存考试分数数据到分数获取队列
+        examScoreObtainQueueService.saveExamScoreObtainQueue(examRecordData);
+        // 发送获取分数通知
+
+        ExamRecordEntity examRecord = GlobalHelper.getEntity(
+                examRecordRepo, examRecordData.getExamRecordId(), ExamRecordEntity.class);
+        examScoreObtainQueueService.sendObtainScoreNotify(examRecord.getRootOrgId());
+    }
+
+    @Override
+    public UpyunSignatureInfo getUpyunSignature(GetUpyunSignatureReq req) {
+        UpyunSignatureInfo u = new UpyunSignatureInfo();
+        try {
+            ExamRecordDataEntity reco = GlobalHelper.getEntity(examRecordDataRepo, Long.valueOf(req.getExamRecordDataId()), ExamRecordDataEntity.class);
+            String md5 = req.getFileMd5();
+            Date signDate = null;
+            Date now = new Date();
+            Date expirationDate = DateUtils.addSeconds(now, SIGN_TIMEOUT);
+
+            StringBuffer filePath = new StringBuffer();
+
+            ExamRecordEntity examRecord = GlobalHelper.getEntity(
+                    examRecordRepo, reco.getExamRecordId(), ExamRecordEntity.class);
+            synchronized (lock) {
+                filePath.append(SEPARATOR).append(OE_ANSWER_FILE_PATH).append(SEPARATOR).append(examRecord.getExamStudentId())
+                        .append(SEPARATOR).append(req.getExamRecordDataId()).append(SEPARATOR).append(req.getOrder())
+                        .append(SEPARATOR).append(examRecord.getExamStudentId()).append(UNDERLINE)
+                        .append(req.getExamRecordDataId()).append(UNDERLINE).append(req.getOrder()).append(UNDERLINE)
+                        .append(System.currentTimeMillis());
+                Util.sleep(TimeUnit.MILLISECONDS, 1);
+            }
+
+            if (StringUtils.isNotEmpty(req.getExt())) {
+                filePath.append(UNDERLINE).append(req.getExt());
+            }
+            filePath.append(".").append(req.getFileSuffix());
+
+            long expiration = expirationDate.getTime() / 1000;
+            String policy = policy(bucketName, expiration, filePath.toString(), signDate, md5);
+            String sign = sign("POST", getGMTDate(signDate), bucketName, policy, userName, UpYunUtils.md5(password),
+                    md5);
+            u.setPolicy(policy);
+            u.setSignature(sign);
+            u.setFilePath(filePath.toString());
+            u.setUploadUrl(UrlUtil.joinUrl(bucketUrl, bucketName));
+            u.setUpyunFileDomain(upyunFileUrl);
+        } catch (UpException e) {
+            throw new StatusException("100003", "获取又拍云签名失败");
+        }
+        return u;
+    }
+
+    private String getGMTDate(Date d) {
+        if (d == null) {
+            return null;
+        }
+        SimpleDateFormat formater = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
+        formater.setTimeZone(TimeZone.getTimeZone("GMT+8"));
+        return formater.format(d);
+    }
+
+    /**
+     * @param bucketName //不能为空
+     * @param expiration //不能为空
+     * @param filePath   //不能为空
+     * @param date       为空时,以又怕云时间和expiration比较,不为空时以此date和expiration比较
+     * @param md5        //可以为空
+     * @return
+     */
+    private String policy(String bucketName, Long expiration, String filePath, Date date, String md5) {
+        Map<String, Object> paramMap = new HashMap<String, Object>();
+        // 不能为空
+        paramMap.put("bucket", bucketName);
+        Date expirationDate = DateUtils.addSeconds(new Date(), SIGN_TIMEOUT);
+        // 不能为空
+        paramMap.put("expiration", expirationDate.getTime() / 1000);
+        paramMap.put("save-key", filePath);
+        // 为空时,以又怕云时间和expiration比较,不为空时以此date和expiration比较
+        paramMap.put("date", date);
+        // 可以为空
+        paramMap.put("content-md5", md5);
+        String policy = UpYunUtils.getPolicy(paramMap);
+        return policy;
+    }
+
+    /**
+     * 必须和policy中date一致,可以都为空.GMTDate
+     *
+     * @param method
+     * @param date
+     * @param bucketName
+     * @param policy
+     * @param userName
+     * @param password
+     * @param md5
+     * @return
+     * @throws UpException
+     */
+    private String sign(String method, String date, String bucketName, String policy, String userName, String password,
+                        String md5) throws UpException {
+
+        StringBuilder sb = new StringBuilder();
+        String sp = "&";
+        sb.append(method);
+        sb.append(sp);
+        sb.append(SEPARATOR + bucketName);
+        if (date != null) {
+            sb.append(sp);
+            sb.append(date);
+        }
+        sb.append(sp);
+        sb.append(policy);
+        if (md5 != null && md5.length() > 0) {
+            sb.append(sp);
+            sb.append(md5);
+        }
+        String raw = sb.toString().trim();
+        byte[] hmac = null;
+        try {
+            hmac = UpYunUtils.calculateRFC2104HMACRaw(password, raw);
+        } catch (Exception e) {
+            throw new UpException("calculate SHA1 wrong.");
+        }
+
+        if (hmac != null) {
+            return "UPYUN " + userName + ":" + Base64Coder.encodeLines(hmac).trim();
+        }
+
+        return null;
+    }
+
+    @Override
+    public String getQrCode(@Valid GetQrCodeReq req, String key) {
+        int sessionTimeout = PropertyHolder.getInt(SESSION_TIMEOUT, 3600);
+        User user = redisClient.get(key, User.class, sessionTimeout);
+        if (null == user) {
+            throw new StatusException("100007", "登录信息已失效");
+        }
+        return getQrCode(req, user);
+    }
+
+    @Override
+    public String getQrCode(GetQrCodeReq req, User user) {
+        if (user == null) {
+            throw new StatusException("100002", "登录信息错误");
+        }
+
+        //如果是调用环境监测的接口,不用做如下校验
+        if (!req.isTestEnv()) {
+            ExamSessionInfo examSessionInfo = examSessionInfoService
+                    .getExamSessionInfo(user.getUserId());
+
+            ExamStudentEntity examStudentEntity = examStudentRepo.findByExamStudentId(req.getExamStudentId());
+            if (examStudentEntity == null) {
+                throw new StatusException("100012", "考生不存在");
+            }
+            if (!user.getUserId().equals(examStudentEntity.getStudentId())) {
+                throw new StatusException("100013", "无效的请求");
+            }
+            if (examSessionInfo == null) {
+                throw new StatusException("100006", "考试已结束");
+            }
+            if (examSessionInfo.getExamRecordDataId().longValue() != req.getExamRecordDataId().longValue()
+                    || examSessionInfo.getExamStudentId().longValue() != req.getExamStudentId().longValue()) {
+                throw new StatusException("100008", "无效的请求");
+            }
+        } else {
+            //环境检测时,需要重新给考生id赋值
+            ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, req.getExamRecordDataId(),
+                    ExamRecordDataEntity.class);
+            ExamRecordEntity examingRecord = GlobalHelper.getEntity(examRecordRepo, examRecordData.getExamRecordId(),
+                    ExamRecordEntity.class);
+            req.setExamStudentId(examingRecord.getExamStudentId());
+        }
+        String key = user.getKey();
+        StringBuffer param = new StringBuffer();
+        String transferFileType = StringUtils.isBlank(req.getTransferFileType()) ? "" : req.getTransferFileType();
+        param.append("examStudentId=").append(req.getExamStudentId()).append("&examRecordDataId=")
+                .append(req.getExamRecordDataId()).append("&order=").append(req.getOrder())
+                .append("&transferFileType=").append(transferFileType).append("&key=").append(key);
+        // 需要签名的参数
+        StringBuffer sourStr = new StringBuffer();
+        sourStr.append(req.getOrder()).append(UNDERLINE).append(req.getExamRecordDataId()).append(UNDERLINE)
+                .append(key).append(UNDERLINE).append(req.getExamStudentId());
+        // 签名
+        byte[] bytes = SHA256.encode(sourStr.toString());
+        String hexAscii = ByteUtil.toHexAscii(bytes);
+        param.append("&token=").append(hexAscii);
+        String qrStr;
+        try {
+            qrStr = audioAppUrl + SEPARATOR + URLEncoder.encode(param.toString(), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new StatusException("100001", "参数编码异常");
+        }
+        return qrStr;
+    }
+
+    /**
+     * 通过websocket发送消息
+     *
+     * @param examRecordDataId 考试记录id
+     * @param order            题序号
+     * @param fileUrl          文件路径
+     * @param transferFileType 传输文件类型
+     * @param userId
+     * @throws Exception
+     */
+    @Override
+    public void sendFileAnswerToWebSocket(Long examRecordDataId, Integer order,
+                                          String fileUrl, String transferFileType, Long userId) throws Exception {
+        Map<String, Object> data = new HashMap<String, Object>();
+        data.put("examRecordDataId", examRecordDataId);
+        data.put("order", order);
+        data.put("fileUrl", fileUrl);
+        data.put("transferFileType", transferFileType);
+
+        SendFileAnswerMessageReq sendMessageReq = new SendFileAnswerMessageReq();
+        Long clientId;
+        //如果是环境检测,则使用用户id(即学生id)作为clientId
+        if (isTestDev(examRecordDataId)) {
+            clientId = userId;
+        }
+        //不是环境检测,仍然使用考试记录id作为clientId
+        else {
+            clientId = examRecordDataId;
+        }
+        sendMessageReq.setClientId(clientId);
+        sendMessageReq.setEventType(WebSocketEventType.GET_FILE_ANSWER.toString());
+        sendMessageReq.setIsSuccess(true);
+        sendMessageReq.setData(data);
+        fileAnswerWebsocketCloudService.sendFileAnswerMessage(sendMessageReq);
+    }
+
+    /**
+     * 通过websocket发送二维码扫描信息
+     *
+     * @param clientId
+     * @param examRecordDataId
+     * @param order
+     * @throws Exception
+     */
+    @Override
+    public void sendScanQrCodeToWebSocket(String clientId, Long examRecordDataId, Integer order) throws Exception {
+        Map<String, Object> data = new HashMap<String, Object>();
+        data.put("examRecordDataId", examRecordDataId);
+        data.put("order", order);
+        data.put("scanStatus", "SCANNED");
+        SendScanQrCodeMessageReq sendScanQrCodeMessageReq = new SendScanQrCodeMessageReq();
+        sendScanQrCodeMessageReq.setExamRecordDataId(examRecordDataId);
+        sendScanQrCodeMessageReq.setEventType(WebSocketEventType.SCAN_QR_CODE.toString());
+        sendScanQrCodeMessageReq.setIsSuccess(true);
+        sendScanQrCodeMessageReq.setClientId(clientId);
+        sendScanQrCodeMessageReq.setData(data);
+        fileAnswerWebsocketCloudService.sendScanQrCodeMessage(sendScanQrCodeMessageReq);
+    }
+
+    @Override
+    public CheckQrCodeInfo checkQrCode(String param) {
+        String str;
+        str = UrlUtil.decode(param);
+        Map<String, String> map = Splitter.on("&").withKeyValueSeparator("=").split(str);
+        String examStudentId = map.get("examStudentId");
+        String examRecordDataId = map.get("examRecordDataId");
+        String order = map.get("order");
+        String key = map.get("key");
+        String token = map.get("token");
+        // 需要签名的参数
+        StringBuffer sourStr = new StringBuffer();
+        sourStr.append(order).append(UNDERLINE).append(examRecordDataId).append(UNDERLINE).append(key)
+                .append(UNDERLINE).append(examStudentId);
+        // 签名
+        byte[] bytes = SHA256.encode(sourStr.toString());
+        String hexAscii = ByteUtil.toHexAscii(bytes);
+        if (!hexAscii.equals(token)) {
+            throw new StatusException("100005", "无效的二维码");
+        }
+
+        //判断考生是否存在
+        ExamStudentEntity examStudentEntity = examStudentRepo.findByExamStudentId(Long.valueOf(examStudentId));
+        if (examStudentEntity == null) {
+            throw new StatusException("100012", "考生不存在");
+        }
+
+        String clientId;
+        //未开启环境检测,才进行如下校验
+        if (!isTestDev(Long.valueOf(examRecordDataId))) {
+            //非环境检测,clientId即examRecordDataId
+            clientId = examRecordDataId;
+            // 判断考试是否结束
+            ExamSessionInfo examSessionInfo = examSessionInfoService
+                    .getExamSessionInfo(examStudentEntity.getStudentId());
+            if (examSessionInfo == null) {
+                throw new StatusException("100006", "考试已结束");
+            }
+            if (examSessionInfo.getExamRecordDataId().longValue() != Long.valueOf(examRecordDataId).longValue()
+                    || examSessionInfo.getExamStudentId().longValue() != Long.valueOf(examStudentId).longValue()) {
+                throw new StatusException("100008", "无效的二维码");
+            }
+        } else {
+            //环境检测时,clientId即用户id
+            clientId = key.substring(key.lastIndexOf("_") + 1);
+        }
+
+        // 校验通过
+        CheckQrCodeInfo res = new CheckQrCodeInfo();
+        res.setExamRecordDataId(Long.valueOf(examRecordDataId));
+        res.setExamStudentId(Long.valueOf(examStudentId));
+        res.setKey(key);
+        int sessionTimeout = PropertyHolder.getInt(SESSION_TIMEOUT, 3600);
+        User user = redisClient.get(key, User.class, sessionTimeout);
+        if (null == user) {
+            throw new StatusException("100007", "登录信息已失效");
+        }
+        res.setToken(user.getToken());
+        CourseBean courseBean = ExamCacheTransferHelper.getCachedCourse(examStudentEntity.getCourseId());
+
+        res.setCourseId(courseBean.getId());
+        res.setCourseName(courseBean.getName());
+        ExamRecordQuestionsEntity examRecordQuestionsEntity = examRecordQuestionsService.
+                getExamRecordQuestionsAndFixExamRecordDataIfNecessary(Long.valueOf(examRecordDataId));
+        List<ExamQuestionEntity> examQuestionList = examRecordQuestionsEntity.getExamQuestionEntities();
+
+        if (examRecordQuestionsEntity == null || examQuestionList == null || examQuestionList.isEmpty()) {
+            throw new StatusException("100008", "无效的二维码");
+        }
+        List<ExamQuestionEntity> filterList = examQuestionList.stream().filter(p ->
+                p.getOrder().equals(Integer.valueOf(order))).collect(Collectors.toList());
+        if (filterList == null || filterList.isEmpty()) {
+            throw new StatusException("100008", "无效的二维码");
+        }
+        ExamQuestionEntity eqe = filterList.get(0);
+
+        res.setQuestionOrder(eqe.getOrder());
+        res.setQuestionMainNumber(eqe.getMainNumber());
+        res.setSubNumber(getSubNumber(examRecordQuestionsEntity,Integer.valueOf(order)));
+        try {
+            this.sendScanQrCodeToWebSocket(clientId, Long.valueOf(examRecordDataId), Integer.valueOf(order));
+        } catch (Exception e) {
+            throw new StatusException("100011", "消息通知失败");
+        }
+        return res;
+    }
+
+    private Integer getSubNumber(ExamRecordQuestionsEntity examRecordQuestionsEntity,Integer order) {
+        List<UploadedFileAnswerInfo> list = getReSortedQuestionList(examRecordQuestionsEntity);
+        for (UploadedFileAnswerInfo info : list) {
+            if (order.intValue() == info.getOrder().intValue()) {
+                return info.getSubNumber();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    @Transactional
+    public ExamFileAnswerTempEntity saveUploadedFile(SaveUploadedFileReq req, User user) {
+        // 判断考试是否结束
+        ExamStudentEntity examStudentEntity = examStudentRepo.findByExamStudentId(req.getExamStudentId());
+        if (examStudentEntity == null) {
+            throw new StatusException("100012", "考生不存在");
+        }
+
+        //开启了环境检测,则不需要进行如下的校验
+        if (!isTestDev(req.getExamRecordDataId())) {
+            if (!user.getUserId().equals(examStudentEntity.getStudentId())) {
+                throw new StatusException("100013", "无效的请求");
+            }
+            ExamSessionInfo examSessionInfo = examSessionInfoService
+                    .getExamSessionInfo(examStudentEntity.getStudentId());
+            if (examSessionInfo == null) {
+                throw new StatusException("100006", "考试已结束");
+            }
+            if (examSessionInfo.getExamRecordDataId().longValue() != req.getExamRecordDataId().longValue()
+                    || examSessionInfo.getExamStudentId().longValue() != req.getExamStudentId().longValue()) {
+                throw new StatusException("100008", "考试已结束");
+            }
+        }
+
+        ExamFileAnswerTempEntity et = new ExamFileAnswerTempEntity();
+        et.setExamRecordDataId(req.getExamRecordDataId());
+        et.setExamStudentId(req.getExamStudentId());
+        et.setQuestionOrder(req.getOrder());
+        et.setFilePath(req.getFilePath());
+        et.setStatus(FileAnswerAcknowledgeStatus.UNCONFIRMED);
+        et.setTransferFileType(req.getTransferFileType());
+        return examFileAnswerTempRepo.save(et);
+
+    }
+
+    @Override
+    public String getUploadedFileAcknowledgeStatus(GetUploadedFileAcknowledgeStatusReq req) {
+        ExamFileAnswerTempEntity et = GlobalHelper.getEntity(examFileAnswerTempRepo,
+                req.getAcknowledgeId(), ExamFileAnswerTempEntity.class);
+        if (et != null) {
+            return et.getStatus().toString();
+        }
+        return FileAnswerAcknowledgeStatus.UNCONFIRMED.toString();
+    }
+
+    @Override
+    @Transactional
+    public void saveUploadedFileAcknowledgeStatus(SaveUploadedFileAcknowledgeStatusReq req) {
+        ExamFileAnswerTempEntity et = examFileAnswerTempRepo
+                .findByExamRecordDataIdAndQuestionOrderAndFilePath(req.getExamRecordDataId(), req.getOrder(), req.getFilePath().replace(upyunFileUrl, ""));
+        if (et == null) {
+            throw new StatusException("100010", "无效的数据");
+        }
+        et.setStatus(FileAnswerAcknowledgeStatus.valueOf(req.getAcknowledgeStatus()));
+        examFileAnswerTempRepo.save(et);
+    }
+
+    @Override
+    @Transactional
+    public void deleteExamFileAnswerTemp(SaveUploadedFileReq req) {
+        examFileAnswerTempRepo.delete(req.getExamRecordDataId(), req.getExamStudentId(), req.getOrder(), req.getFilePath());
+    }
+
+    @Override
+    public List<UploadedFileAnswerInfo> getUploadedFileAnswerList(@Valid GetUploadedFileAnswerListReq req) {
+        List<UploadedFileAnswerInfo> resultList = new ArrayList<UploadedFileAnswerInfo>();
+        /*//未分组集合
+        List<ExamFileAnswerTempEntity> uploadedFileAnswerList = examFileAnswerTempRepo
+                .findByExamRecordDataIdAndStatus(req.getExamRecordDataId(), FileAnswerAcknowledgeStatus.CONFIRMED);
+        if (null == uploadedFileAnswerList || uploadedFileAnswerList.isEmpty()) {
+            return resultList;
+        } else {
+            //新集合,只取同一考试记录id和题号下的最新记录
+            List<ExamFileAnswerTempEntity> newestUploadedFileAnswerList = new ArrayList<>();
+            for (ExamFileAnswerTempEntity temp : uploadedFileAnswerList) {
+                boolean existData = newestUploadedFileAnswerList.stream().anyMatch(p ->
+                        p.getQuestionOrder().equals(temp.getQuestionOrder())
+                                && p.getExamRecordDataId().equals(temp.getExamRecordDataId()));
+                if (!existData) {
+                    newestUploadedFileAnswerList.add(temp);
+                } else {
+                    ExamFileAnswerTempEntity currentTemp = newestUploadedFileAnswerList.stream().filter(p -> p.getQuestionOrder().equals(temp.getQuestionOrder())
+                            && p.getExamRecordDataId().equals(temp.getExamRecordDataId())).collect(Collectors.toList()).get(0);
+                    //集合中取id最大的值
+                    if (currentTemp.getId() < temp.getId()) {
+                        newestUploadedFileAnswerList.remove(currentTemp);
+                        newestUploadedFileAnswerList.add(temp);
+                    }
+                }
+            }
+
+            //按大题号给所有的小题定义序号
+            List<UploadedFileAnswerInfo> allQuestionAnswerList = getReSortedQuestionList(req.getExamRecordDataId());
+            //当前考生已作答的题目集合
+            List<ExamQuestionEntity> examQuestionsInMongo = examQuestionRepo.findByExamRecordDataId(req.getExamRecordDataId());
+            newestUploadedFileAnswerList.forEach(p -> {
+                //过滤出所有的音频题
+                List<UploadedFileAnswerInfo> uploadedFileAnswerInfoList = allQuestionAnswerList.stream().filter(pa -> pa.getOrder().equals(p.getQuestionOrder())).collect(Collectors.toList());
+                if (null != uploadedFileAnswerInfoList && !uploadedFileAnswerInfoList.isEmpty()) {
+                    UploadedFileAnswerInfo info = uploadedFileAnswerInfoList.get(0);
+//					//过滤掉重置过答案的数据 case 1  采用case1还是case2需要找张营确认一下
+//					if(!examQuestionsInMongo.stream().anyMatch(pm->pm.getOrder().equals(info.getOrder()) 
+//							&& StringUtils.isEmpty(pm.getStudentAnswer()))) {
+//						info.setExamRecordDataId(p.getExamRecordDataId());
+//						//由于有可能学生答案在PC端展示时还未提交到服务器,所以必须用临时表中的数据
+//						info.setStudentAnswer(upyunFileUrl+ p.getFilePath());
+//						resultList.add(info);
+//					}
+                    //必须是考生作答过,且答案不为空,才能展示 case 2
+                    if (examQuestionsInMongo.stream().anyMatch(pm -> pm.getOrder().equals(info.getOrder())
+                            && !StringUtils.isEmpty(pm.getStudentAnswer()))) {
+                        info.setExamRecordDataId(p.getExamRecordDataId());
+                        //由于有可能学生答案在PC端展示时还未提交到服务器,所以必须用临时表中的数据
+                        info.setStudentAnswer(upyunFileUrl + p.getFilePath());
+                        resultList.add(info);
+                    }
+                }
+            });
+        }
+        //最后按序号从小到大再排一次
+        resultList.sort(Comparator.comparing(UploadedFileAnswerInfo::getOrder));*/
+        return resultList;
+    }
+
+    /**
+     * 根据考试记录id获取所有已经重排序号的考试列表
+     * @param examRecordQuestionsEntity
+     * @return
+     */
+    private List<UploadedFileAnswerInfo> getReSortedQuestionList(ExamRecordQuestionsEntity examRecordQuestionsEntity) {
+        //所有试题集合,包括非音频题
+        List<UploadedFileAnswerInfo> resultList = new ArrayList<UploadedFileAnswerInfo>();
+
+        if (null == examRecordQuestionsEntity || null == examRecordQuestionsEntity.getExamQuestionEntities() ||
+                examRecordQuestionsEntity.getExamQuestionEntities().isEmpty()) {
+            return resultList;
+        }
+
+        List<ExamQuestionEntity> examQuestionAnswerList = examRecordQuestionsEntity.getExamQuestionEntities();
+        //按大题号分组并排序
+        Map<Integer, List<ExamQuestionEntity>> sortedMainNumberList = examQuestionAnswerList.stream().
+                sorted(Comparator.comparing(ExamQuestionEntity::getMainNumber)).
+                collect(Collectors.groupingBy(ExamQuestionEntity::getMainNumber, Collectors.toList()));
+        for (Integer mainNum : sortedMainNumberList.keySet()) {
+            //当前大题下的小题集合
+            List<ExamQuestionEntity> subEmptyExamQuestionAnswerList = sortedMainNumberList.get(mainNum);
+            //按order对小题进行重新排序
+            subEmptyExamQuestionAnswerList.sort(Comparator.comparing(ExamQuestionEntity::getOrder));
+            for (int i = 0; i < subEmptyExamQuestionAnswerList.size(); i++) {
+                ExamQuestionEntity curExamQuestion = subEmptyExamQuestionAnswerList.get(i);
+                UploadedFileAnswerInfo info = new UploadedFileAnswerInfo();
+                info.setExamRecordDataId(curExamQuestion.getExamRecordDataId());
+                info.setMainNumber(curExamQuestion.getMainNumber());
+                info.setOrder(curExamQuestion.getOrder());
+//				info.setQuestionId(curExamQuestion.getQuestionId());
+                info.setSubNumber(i + 1);//每个大题下,给小题序号重新赋值
+                resultList.add(info);
+            }
+        }
+        return resultList;
+    }
+
+    private Integer getMaxInterruptNum(Long examId, Long rootOrgId, Long orgId) {
+        GetExamPropertyReq req = new GetExamPropertyReq();
+        req.setExamId(examId);
+        req.setRootOrgId(rootOrgId);
+        req.setOrgId(orgId);
+        req.setKey(ExamProperties.MAX_INTERRUPT_NUM.name());
+        GetExamPropertyResp res = examCloudService.getExamProperty(req);
+        Integer ret = 100;
+        if (StringUtils.isNoneBlank(res.getValue())) {
+            try {
+                ret = Integer.valueOf(res.getValue());
+            } catch (NumberFormatException e) {
+                log.error("MaxInterruptNum is not a number,return default value:100", e);
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * 根据考试记录id判断是否开启环境检测
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    private boolean isTestDev(Long examRecordDataId) {
+        //用于环境检测的考试记录id
+        SysPropertyCacheBean examRecordDataIdObject = CacheHelper.getSysProperty("oe.testDev.examRecordDataId");
+        //是否开启了环境检测(请求的考试记录id等于用于环境检测的考试记录时,则认为开启了环境检测)
+        return examRecordDataIdObject.getHasValue() && examRecordDataId.equals(Long.valueOf(examRecordDataIdObject.getValue().toString()));
+    }
+}
+

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

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

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

@@ -0,0 +1,439 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.base.jpa.Searcher;
+import cn.com.qmth.examcloud.core.oe.common.base.jpa.SpecUtils;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.CommonUtil;
+import cn.com.qmth.examcloud.core.oe.common.entity.*;
+import cn.com.qmth.examcloud.core.oe.common.enums.*;
+import cn.com.qmth.examcloud.core.oe.common.repository.*;
+import cn.com.qmth.examcloud.core.oe.common.service.ExamScoreObtainQueueService;
+import cn.com.qmth.examcloud.core.oe.common.service.ExamScorePushQueueService;
+import cn.com.qmth.examcloud.core.oe.common.service.GainBaseDataService;
+import cn.com.qmth.examcloud.core.oe.student.bean.CalculateFaceCheckResultInfo;
+import cn.com.qmth.examcloud.core.oe.student.face.api.ExamCaptureQueueCloudService;
+import cn.com.qmth.examcloud.core.oe.student.service.*;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author chenken
+ * @date 2018/8/15 11:16
+ * @company QMTH
+ * @description 考试记录数据服务实现
+ */
+@Service("examRecordDataService")
+public class ExamRecordDataServiceImpl implements ExamRecordDataService {
+
+    @Autowired
+    private ExamSessionInfoService examSessionInfoService;
+
+    @Autowired
+    private ExamAuditService examAuditService;
+
+    @Autowired
+    private ExamScoreService examScoreService;
+
+    @Autowired
+    private ExamRecordQuestionsService examRecordQuestionsService;
+
+    @Autowired
+    private ExamRecordForMarkingService examRecordForMarkingService;
+
+    @Autowired
+    private ExamCaptureQueueCloudService examCaptureQueueCloudService;
+
+    @Autowired
+    private GainBaseDataService gainBaseDataService;
+
+    @Autowired
+    private ExamScorePushQueueService examScorePushQueueService;
+
+    @Autowired
+    private ExamScoreObtainQueueService examScoreObtainQueueService;
+
+    @Autowired
+    private ExamFaceLivenessVerifyRepo examFaceLivenessVerifyRepo;
+
+    @Autowired
+    private ExamRecordDataRepo examRecordDataRepo;
+
+    @Autowired
+    private ExamCaptureRepo examCaptureRepo;
+
+    @Autowired
+    private ExamRecordRepo examRecordRepo;
+
+    @Autowired
+    private ExamingRecordRepo examingRecordRepo;
+    @Autowired
+    private HandInExamRecordRepo handInExamRecordRepo;
+
+    private static final Logger log = LoggerFactory.getLogger(ExamRecordDataServiceImpl.class);
+
+    @Override
+    public ExamRecordDataEntity createExamRecordData(ExamRecordEntity examRecord, ExamStudentEntity examStudent,
+                                                     ExamBean examBean, boolean isFullyObjetive) {
+        ExamRecordDataEntity examRecordData = new ExamRecordDataEntity();
+        examRecordData.setExamRecordId(examRecord.getId());
+        examRecordData.setStartTime(new Date());
+        examRecordData.setExamRecordStatus(ExamRecordStatus.EXAM_ING);
+        //已考的考试次数
+        int examTimes = examStudent.getNormalExamTimes();
+        //考试机会
+        Long canExamTimes = examBean.getExamTimes() == null ? 0 : examBean.getExamTimes();
+        Boolean isReExamine =examStudent.getIsReExamine()==null?false:examStudent.getIsReExamine();
+        //如果是重考,则考试次数加1
+        if (examTimes >= canExamTimes && isReExamine) {
+            examTimes = canExamTimes.intValue() + 1;
+        }
+        examRecordData.setExamOrder(examTimes);
+        examRecordData.setIsAllObjectivePaper(isFullyObjetive);
+        examRecordData.setIsWarn(false);
+        examRecordData.setIsAudit(false);
+        examRecordData.setIsIllegality(false);
+        examRecordData.setIsContinued(false);
+        examRecordData.setIsReexamine(false);
+        examRecordData.setContinuedCount(0);
+        examRecordData.setFaceSuccessCount(0);
+        examRecordData.setFaceTotalCount(0);
+        examRecordData.setFaceFailedCount(0);
+        examRecordData.setFaceStrangerCount(0);
+        if (examStudent.getIsReExamine() != null
+                && examStudent.getIsReExamine()
+                && examBean.getExamTimes() <= examStudent.getNormalExamTimes()) {
+            examRecordData.setIsReexamine(true);
+        }
+        return examRecordDataRepo.save(examRecordData);
+    }
+
+    @Override
+    public void createExamingRecord(Long examRecordId, Long examRecordDataId, Long studentId, ExamType examType) {
+        ExamingRecordEntity examingRecord = new ExamingRecordEntity();
+        examingRecord.setStudentId(studentId);
+        examingRecord.setId(examRecordId);
+        examingRecord.setExamRecordDataId(examRecordDataId);
+        examingRecord.setExamType(examType);
+        examingRecord.setIsContinued(false);
+        examingRecord.setContinuedCount(0);
+        examingRecordRepo.save(examingRecord);
+    }
+
+    @Override
+    public void createHandInExamRecord(Long examRecordId, Long examRecordDataId, Long studentId) {
+        HandInExamRecordEntity handInExamRecord = new HandInExamRecordEntity();
+        handInExamRecord.setStudentId(studentId);
+        handInExamRecord.setId(examRecordId);
+        handInExamRecord.setExamRecordDataId(examRecordDataId);
+        handInExamRecordRepo.save(handInExamRecord);
+    }
+
+    @Override
+    public void deleteExamingRecord(Long examRecordId) {
+        examingRecordRepo.deleteById(examRecordId);
+    }
+
+    @Override
+    public void deleteHandInExamRecord(Long examRecordId) {
+        handInExamRecordRepo.deleteById(examRecordId);
+    }
+
+    @Override
+    public List<ExamingRecordEntity> getLimitExamingRecords(Long startId, int limit) {
+        return examingRecordRepo.getLimitExamingRecords(startId, limit);
+    }
+
+    @Override
+    public List<HandInExamRecordEntity> getLimitHandInExamRecords(Long startId, int limit) {
+        return handInExamRecordRepo.getLimitHandInExamRecords(startId, limit);
+    }
+
+    /**
+     * 计算活体检测结果
+     *
+     * @return
+     */
+    @Override
+    public ExamRecordDataEntity calculateFaceLivenessVerifyResult(ExamRecordDataEntity examRecordDataEntity) {
+        if (examRecordDataEntity.getFaceVerifyResult() != null) {
+            if (examRecordDataEntity.getFaceVerifyResult() == IsSuccess.FAILED) {
+                List<ExamFaceLivenessVerifyEntity> faceVerifies = examFaceLivenessVerifyRepo.findByExamRecordDataIdOrderById(examRecordDataEntity.getId());
+                setExamRecordWithFaceLivenessVerifyFailed(examRecordDataEntity, faceVerifies);
+            }
+        } else {
+            ExamRecordEntity examRecord = GlobalHelper.getEntity(
+                    examRecordRepo, examRecordDataEntity.getExamRecordId(), ExamRecordEntity.class);
+            Long examId = examRecord.getExamId();
+            String isFaceVerify = CacheHelper.getExamOrgProperty(examId, examRecord.getOrgId(),
+                    ExamProperties.IS_FACE_VERIFY.name()).getValue();
+
+            //是否进行活体检测
+            if (isFaceVerify != null && Constants.isTrue.equals(isFaceVerify)) {
+                List<ExamFaceLivenessVerifyEntity> faceVerifies = examFaceLivenessVerifyRepo.findByExamRecordDataIdOrderById(examRecordDataEntity.getId());
+                if (faceVerifies != null && faceVerifies.size() > 0) {
+                    ExamFaceLivenessVerifyEntity latestFaceVerify = faceVerifies.get(faceVerifies.size() - 1);
+                    if (latestFaceVerify != null && latestFaceVerify.getVerifyResult() != null && latestFaceVerify.getVerifyResult() == FaceVerifyResult.VERIFY_SUCCESS) {
+                        examRecordDataEntity.setFaceVerifyResult(IsSuccess.SUCCESS);
+                    } else {
+                        setExamRecordWithFaceLivenessVerifyFailed(examRecordDataEntity, faceVerifies);
+                    }
+                } else {
+                    setExamRecordWithFaceLivenessVerifyFailed(examRecordDataEntity, faceVerifies);
+                }
+            }
+        }
+        return examRecordDataEntity;
+    }
+
+    /**
+     * 活体检测失败,设置考试记录属性
+     *
+     * @param examRecordDataEntity
+     * @param faceVerifies
+     */
+    private void setExamRecordWithFaceLivenessVerifyFailed(ExamRecordDataEntity examRecordDataEntity, List<ExamFaceLivenessVerifyEntity> faceVerifies) {
+        examRecordDataEntity.setIsIllegality(true);//判定为违纪
+        examRecordDataEntity.setIsWarn(true);    //有警告
+        examRecordDataEntity.setFaceVerifyResult(IsSuccess.FAILED);//活体检测失败
+    }
+
+    /**
+     * 计算人脸检测结果
+     * 相片数=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(ExamRecordDataEntity examRecordData) {
+        CalculateFaceCheckResultInfo calculateFaceCheckResultInfo = new CalculateFaceCheckResultInfo();
+        ExamRecordEntity examRecord = GlobalHelper.getEntity(
+                examRecordRepo, examRecordData.getExamRecordId(), ExamRecordEntity.class);
+        Long examId = examRecord.getExamId();
+        String isFaceEnable = CacheHelper.getExamOrgProperty(examId, examRecord.getOrgId(),
+                ExamProperties.IS_FACE_ENABLE.name()).getValue();
+        //未开启人脸检测
+        if (!Constants.isTrue.equals(isFaceEnable)) {
+            examRecordData.setIsWarn(false);
+            calculateFaceCheckResultInfo.setExamRecordData(examRecordData);
+            return calculateFaceCheckResultInfo;
+        }
+        List<ExamCaptureEntity> examCaptureList = examCaptureRepo.findByExamRecordDataId(examRecordData.getId());
+        if (examCaptureList == null || examCaptureList.size() == 0) {
+            examRecordData.setIsWarn(true);//有异常
+            examRecordData.setIsIllegality(true);//违纪
+            calculateFaceCheckResultInfo.setExamRecordData(examRecordData);
+            calculateFaceCheckResultInfo.setIsNoPhotoAndIllegality(true);
+            return calculateFaceCheckResultInfo;
+        }
+        //计算
+        examRecordData = computeFaceCheckData(examRecordData, examCaptureList);
+        //陌生人个数>0
+        if (examRecordData.getFaceStrangerCount() > 0) {
+            examRecordData.setIsWarn(true);
+            calculateFaceCheckResultInfo.setExamRecordData(examRecordData);
+            return calculateFaceCheckResultInfo;
+        }
+        //人脸识别阀值
+        String warnThresholdStr = CacheHelper.getExamOrgProperty(examId, examRecord.getOrgId(),
+                ExamProperties.WARN_THRESHOLD.name()).getValue();
+        if (CommonUtil.isBlank(warnThresholdStr)) {
+            throw new StatusException("ExamRecordDataServiceImpl-calculateFaceCheckResult-001", ExamProperties.WARN_THRESHOLD.getDesc() + "未设置");
+        }
+        double warnThreshold = Double.parseDouble(warnThresholdStr);
+        //人脸真实性(百度活体检测)通过阀值
+        String liveWarnThresholdStr = CacheHelper.getExamOrgProperty(examId, examRecord.getOrgId(),
+                ExamProperties.LIVING_WARN_THRESHOLD.name()).getValue();
+        if (CommonUtil.isBlank(liveWarnThresholdStr)) {
+            throw new StatusException("ExamRecordDataServiceImpl-calculateFaceCheckResult-002", ExamProperties.LIVING_WARN_THRESHOLD.getDesc() + "未设置");
+        }
+        double livenessThreshold = Double.parseDouble(liveWarnThresholdStr);
+
+        if (warnThreshold == 0d && livenessThreshold > 0d) {
+            examRecordData.setIsWarn(examRecordData.getBaiduFaceLivenessSuccessPercent() < livenessThreshold);
+        } else if (warnThreshold > 0d && livenessThreshold == 0d) {
+            examRecordData.setIsWarn(examRecordData.getFaceSuccessPercent() < warnThreshold);
+        } else if (warnThreshold > 0d && livenessThreshold > 0d) {
+            boolean isWarn = examRecordData.getFaceSuccessPercent() < warnThreshold ||
+                    examRecordData.getBaiduFaceLivenessSuccessPercent() < livenessThreshold;
+            examRecordData.setIsWarn(isWarn);
+        }
+        calculateFaceCheckResultInfo.setExamRecordData(examRecordData);
+        return calculateFaceCheckResultInfo;
+    }
+
+    /**
+     * 计算人脸检测数据
+     * 陌生人记录数、成功次数、失败次数、成功率
+     *
+     * @param examRecordData
+     * @param examCaptureEntityList
+     * @return
+     */
+    private ExamRecordDataEntity computeFaceCheckData(ExamRecordDataEntity examRecordData, List<ExamCaptureEntity> examCaptureEntityList) {
+        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 += isFacelivenessSuccess(examCaptureEntity);
+        }
+        int allNum = examCaptureEntityList.size();
+        BigDecimal bg = new BigDecimal(((double) succCount / allNum) * 100);
+        succPercent = bg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();// 人脸比较成功率
+        examRecordData.setFaceTotalCount(allNum);//检测总次数
+        examRecordData.setFaceSuccessPercent(succPercent);//成功率
+        examRecordData.setFaceStrangerCount(strangerCount);//有陌生人的次数
+        examRecordData.setFaceSuccessCount(succCount);//成功次数
+        examRecordData.setFaceFailedCount(falseCount);//失败次数
+        //人脸关键点坐标相似比率-废弃
+        //examRecordDataEntity.setFaceLandmarkVal(analyseLankmark(examCaptureEntityList));
+        BigDecimal livenessSuccessBg = new BigDecimal(((double) livenessSuccessCount / allNum) * 100);
+        livenessSuccessPercent = livenessSuccessBg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();//百度活体检测成功率
+        examRecordData.setBaiduFaceLivenessSuccessPercent(livenessSuccessPercent);
+        return examRecordData;
+    }
+
+    private int isFacelivenessSuccess(ExamCaptureEntity examCapture) {
+        if (StringUtils.isNotBlank(examCapture.getFacelivenessResult())) {
+            String livenessJson = examCapture.getFacelivenessResult();
+            JSONObject jsonObject;
+            try {
+                jsonObject = new JSONObject(livenessJson);
+                if (jsonObject.has("error_code") && jsonObject.getInt("error_code") == 0 && jsonObject.has("result")) {
+                    JSONObject resultJson = jsonObject.getJSONObject("result");
+                    if (resultJson.has("face_liveness")) {
+                        double faceLivenessVal = resultJson.getDouble("face_liveness");
+                        double baiduFacelivenessThreshold = Double.parseDouble(PropertyHolder.getString("$baidu.faceliveness.threshold"));
+                        if (faceLivenessVal > baiduFacelivenessThreshold) {
+                            return 1;
+                        }
+                    }
+                }
+            } catch (JSONException e) {
+                e.printStackTrace();
+                return 0;
+            }
+
+        }
+        return 0;
+    }
+
+    @Override
+    public List<ExamRecordDataEntity> findByStatusAndExamTypeIn(ExamRecordStatus examRecordStatus, List<String> examTypeList) {
+        Searcher searchers = new Searcher().in("examRecord.examType", examTypeList)
+                .eq("examRecordStatus", examRecordStatus.name());
+        Specification<ExamRecordDataEntity> spec = SpecUtils.buildSearchers(ExamRecordDataEntity.class, searchers.build());
+        return examRecordDataRepo.findAll(spec);
+    }
+
+    @Override
+    public List<ExamRecordDataEntity> findValidExamRecordDataByExamStudentId(Long examStudentId) {
+        Searcher searchers = new Searcher().eq("examRecord.examStudentId", examStudentId);
+        Specification<ExamRecordDataEntity> spec = SpecUtils.buildSearchers(ExamRecordDataEntity.class, searchers.build());
+        List<ExamRecordDataEntity> examRecordDataList = examRecordDataRepo.findAll(spec);
+        //第一次过滤:正常结束或者被系统处理的
+        List<ExamRecordDataEntity> firstFilteredexamRecordDataList = examRecordDataList.stream().filter(examRecordData -> {
+            return examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_END
+                    || examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_OVERDUE;
+        }).collect(Collectors.toList());
+        /**
+         * 第二次过滤:
+         * 1.没有违纪的
+         * 2.无异常或有异常已审核通过
+         */
+        if (firstFilteredexamRecordDataList != null && firstFilteredexamRecordDataList.size() > 0) {
+            return firstFilteredexamRecordDataList.stream().filter(examRecordData -> {
+                return !examRecordData.getIsIllegality() &&
+                        (!examRecordData.getIsWarn() || (examRecordData.getIsWarn() && examRecordData.getIsAudit()));
+            }).collect(Collectors.toList());
+        }
+
+        return null;
+    }
+
+    @Override
+    public ExamRecordDataEntity createOfflineExamRecordData(ExamRecordEntity examRecord, Boolean fullyObjective) {
+        ExamRecordDataEntity examRecordDataEntity = new ExamRecordDataEntity();
+        examRecordDataEntity.setExamRecordId(examRecord.getId());
+        examRecordDataEntity.setStartTime(new Date());
+        examRecordDataEntity.setExamRecordStatus(ExamRecordStatus.EXAM_ING);
+        examRecordDataEntity.setExamOrder(1);
+        examRecordDataEntity.setIsAllObjectivePaper(fullyObjective);
+        return examRecordDataRepo.save(examRecordDataEntity);
+    }
+
+    @Override
+    public ExamRecordDataEntity examRecordAutoAudit(Boolean isNoPhotoAndIllegality, ExamRecordDataEntity examRecordData) {
+        //无照片违纪自动审核
+        if (isNoPhotoAndIllegality != null && isNoPhotoAndIllegality) {
+            examAuditService.saveExamAuditByNoPhoto(examRecordData.getId());
+            examRecordData.setIsAudit(true);
+        } else {
+            //活体检测失败违纪自动审核
+            if (examRecordData.getFaceVerifyResult() != null
+                    && examRecordData.getFaceVerifyResult() == IsSuccess.FAILED
+                    && examRecordData.getIsIllegality()) {
+                examAuditService.saveExamAuditByFaceVerifyFailed(examRecordData.getId());
+                examRecordData.setIsAudit(true);
+            }
+        }
+        return examRecordData;
+    }
+
+    @Override
+    public boolean isExamRecordEnded(ExamRecordDataEntity examRecordData) {
+        //如果考试记录状态为已处理,则直接返回true.
+        if (examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_END ||
+                examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_OVERDUE ||
+                examRecordData.getExamRecordStatus() == ExamRecordStatus.EXAM_INVALID) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isExamRecordEnded(Long examRecordDataId) {
+        log.debug("进入isExamRecordEnded方法,examRecordDataId=" + examRecordDataId);
+        ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, examRecordDataId, ExamRecordDataEntity.class);
+        if (examRecordData == null) {
+            throw new StatusException("", "找不到id为:" + examRecordDataId + "的考试记录");
+        }
+        return isExamRecordEnded(examRecordData);
+    }
+}

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

@@ -0,0 +1,128 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import java.util.Date;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordRepo;
+import main.java.com.UpYun;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordForMarkingEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamScoreEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordDataRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordForMarkingRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamScoreRepo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordForMarkingService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordQuestionsService;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+
+@Service("examRecordForMarkingService")
+public class ExamRecordForMarkingServiceImpl implements ExamRecordForMarkingService {
+
+	@Autowired
+	private ExamRecordQuestionsService examRecordQuestionsService;
+	
+	@Autowired
+	private ExamRecordForMarkingRepo examRecordForMarkingRepo;
+	
+	@Autowired
+	private ExamRecordDataRepo examRecordDataRepo;
+	
+	@Autowired
+	private ExamScoreRepo examScoreRepo;
+
+	@Autowired
+	private ExamRecordRepo examRecordRepo;
+	
+	@Value("${$upyun.site.1.bucketName}")
+    private String bucketName;
+	
+	@Value("${$upyun.site.1.userName}")
+	private String userName;
+	
+	@Value("${$upyun.site.1.password}")
+	private String password;
+
+	@Value("${$upyun.site.1.domain}")
+	private String upyunFileUrl;
+
+	@Override
+	public void saveExamRecordForMarking(Long examRecordDataId) {
+		ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo, examRecordDataId, ExamRecordDataEntity.class);
+		ExamScoreEntity examScore = examScoreRepo.findByExamRecordDataId(examRecordDataId);
+		saveExamRecordForMarking(examRecordData, examScore.getObjectiveScore());
+	}
+
+	@Override
+	public void saveExamRecordForMarking(ExamRecordDataEntity examRecordData,double objectiveQuestionTotalScore) {
+		//全客观题卷
+    	if(examRecordData.getIsAllObjectivePaper()){
+    		return;
+    	}
+    	//违纪
+    	if(examRecordData.getIsIllegality()){
+    		return;
+    	}
+    	//有警告未审核
+    	if(examRecordData.getIsWarn()&&!examRecordData.getIsAudit()){
+    		return;
+    	}
+    	//已经存在
+    	ExamRecordForMarkingEntity examRecordForMarkingExists = examRecordForMarkingRepo.findByExamRecordDataId(examRecordData.getId());
+    	if(examRecordForMarkingExists != null){
+    		return;
+    	}
+
+
+		ExamRecordEntity examRecord =GlobalHelper.getEntity(
+				examRecordRepo,examRecordData.getExamRecordId(),ExamRecordEntity.class);
+		ExamRecordForMarkingEntity examRecordForMarking = new ExamRecordForMarkingEntity();
+        examRecordForMarking.setExamId(examRecord.getExamId());
+        examRecordForMarking.setExamRecordDataId(examRecordData.getId());
+        examRecordForMarking.setExamStudentId(examRecord.getExamStudentId());
+        examRecordForMarking.setBasePaperId(examRecord.getBasePaperId());
+        examRecordForMarking.setPaperType(examRecord.getPaperType());
+        examRecordForMarking.setCourseId(examRecord.getCourseId());
+        examRecordForMarking.setObjectiveScore(objectiveQuestionTotalScore);
+
+        int subjectiveAnswerLength = examRecordQuestionsService.calculationSubjectiveAnswerLength(examRecordData.getId());
+        examRecordForMarking.setSubjectiveAnswerLength(subjectiveAnswerLength);
+
+        examRecordForMarkingRepo.save(examRecordForMarking);
+	}
+
+	@Override
+	public void saveOffLineExamRecordForMarking(ExamRecordDataEntity examRecordData,String offlineFileName,String fileUrl) {
+		ExamRecordForMarkingEntity examRecordForMarking = examRecordForMarkingRepo.findByExamRecordDataId(examRecordData.getId());
+		if(examRecordForMarking == null){
+			examRecordForMarking = new ExamRecordForMarkingEntity();
+			examRecordForMarking.setCreationTime(new Date());
+		}else{
+			//将原文件删掉
+			UpYun upyun = new UpYun(bucketName,userName,password);
+			String offlineFileUrl = examRecordForMarking.getOfflineFileUrl();
+			offlineFileUrl = offlineFileUrl.replace(upyunFileUrl,"");
+			upyun.deleteFile(offlineFileUrl);
+		}
+
+		ExamRecordEntity examRecord =GlobalHelper.getEntity(
+				examRecordRepo,examRecordData.getExamRecordId(),ExamRecordEntity.class);
+        examRecordForMarking.setExamId(examRecord.getExamId());
+        examRecordForMarking.setExamRecordDataId(examRecordData.getId());
+        examRecordForMarking.setExamStudentId(examRecord.getExamStudentId());
+        examRecordForMarking.setCourseId(examRecord.getCourseId());
+        examRecordForMarking.setOfflineFileUrl(fileUrl);
+        examRecordForMarking.setOfflineFileName(offlineFileName);
+        examRecordForMarking.setObjectiveScore(0D);
+        examRecordForMarking.setSubjectiveAnswerLength(0);
+        examRecordForMarking.setBasePaperId(examRecord.getBasePaperId());
+        examRecordForMarking.setPaperType(examRecord.getPaperType());
+        examRecordForMarking.setUpdateTime(new Date());
+        examRecordForMarkingRepo.save(examRecordForMarking);
+	}
+	
+}

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

@@ -0,0 +1,39 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordRepo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordPaperStructEntity;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordDataRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordPaperStructRepo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordPaperStructService;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月1日 下午3:01:30
+ * @company 	QMTH
+ * @description 考试记录-试卷结构服务实现
+ */
+@Service("examRecordPaperStructService")
+public class ExamRecordPaperStructServiceImpl implements ExamRecordPaperStructService{
+
+	@Autowired
+	private ExamRecordDataRepo examRecordDataRepo;
+	
+	@Autowired
+	private ExamRecordPaperStructRepo examRecordPaperStructRepo;
+	@Autowired
+	private ExamRecordRepo examRecordRepo;
+	@Override
+	public ExamRecordPaperStructEntity getExamRecordPaperStruct(Long examRecordDataId) {
+		ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo,examRecordDataId,ExamRecordDataEntity.class);
+		ExamRecordEntity examRecord =GlobalHelper.getEntity(examRecordRepo,examRecordData.getExamRecordId(),ExamRecordEntity.class);
+		return GlobalHelper.getEntity(examRecordPaperStructRepo,examRecord.getPaperStructId(),ExamRecordPaperStructEntity.class);
+	}
+
+}

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

@@ -0,0 +1,212 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.QuestionTypeUtil;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamQuestionEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordQuestionsEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamStudentEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamType;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordDataRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordQuestionsRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamScoreRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamStudentRepo;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamSessionInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamStudentQuestionInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordQuestionsService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamSessionInfoService;
+import cn.com.qmth.examcloud.core.questions.api.ExtractConfigCloudService;
+import cn.com.qmth.examcloud.core.questions.api.request.GetQuestionReq;
+import cn.com.qmth.examcloud.core.questions.api.response.GetQuestionResp;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionGroup;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionStructureWrapper;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionUnitWrapper;
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionStructure;
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionUnit;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.QuestionCacheBean;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author chenken
+ * @date 2018年9月5日 下午3:23:26
+ * @company QMTH
+ * @description 考试作答归档服务实现
+ */
+@Service("examRecordQuestionsService")
+public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsService {
+
+    @Autowired
+    private ExamRecordQuestionsRepo examRecordQuestionsRepo;
+
+    @Autowired
+    private ExamScoreRepo examScoreRepo;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private ExamRecordDataRepo examRecordDataRepo;
+
+    @Autowired
+    private ExamSessionInfoService examSessionInfoService;
+
+    @Autowired
+    private ExtractConfigCloudService extractConfigCloudService;
+
+    @Autowired
+    private ExamStudentRepo examStudentRepo;
+
+//    @Value("${exam_question_key_prefix}")
+//    private String examQuestionKeyPrefix;
+
+    @Override
+    public Integer calculationSubjectiveAnswerLength(Long examRecordDataId) {
+        ExamRecordQuestionsEntity examRecordQuesitonsEntity = getExamRecordQuestionsAndFixExamRecordDataIfNecessary(examRecordDataId);
+        if (examRecordQuesitonsEntity == null || examRecordQuesitonsEntity.getExamQuestionEntities() == null ||
+                examRecordQuesitonsEntity.getExamQuestionEntities().isEmpty()) {
+            return 0;
+        }
+        List<ExamQuestionEntity> examQuestionList = examRecordQuesitonsEntity.getExamQuestionEntities();
+
+		/*ExamRecordQuestionsEntity examRecordQuestions = examRecordQuestionsRepo.findByExamRecordDataId(examRecordDataId);
+		List<ExamQuestionEntity> examQuestionEntities = examRecordQuestions.getExamQuestionEntities();*/
+        int answerLength = 0;
+        for (ExamQuestionEntity examQuestionEntity : examQuestionList) {
+            if (!QuestionTypeUtil.isObjectiveQuestion(examQuestionEntity.getQuestionType())
+                    && examQuestionEntity.getStudentAnswer() != null) {
+                answerLength += examQuestionEntity.getStudentAnswer().length();
+            }
+        }
+        return answerLength;
+    }
+
+    @Override
+    public ExamRecordQuestionsEntity createExamRecordQuestions(Long examRecordDataId, DefaultPaper defaultPaper) {
+        ExamRecordQuestionsEntity examRecordQuestionsEntity = new ExamRecordQuestionsEntity();
+
+        List<ExamQuestionEntity> examQuestionEntityList = new ArrayList<ExamQuestionEntity>();
+        List<DefaultQuestionGroup> defaultQuestionGroups = defaultPaper.getQuestionGroupList();
+        int order = 0;
+        for (int i = 0; i < defaultQuestionGroups.size(); i++) {
+            DefaultQuestionGroup defaultQuestionGroup = defaultQuestionGroups.get(i);
+            List<DefaultQuestionStructureWrapper> defaultQuestionStructureWrappers = defaultQuestionGroup.getQuestionWrapperList();
+            for (DefaultQuestionStructureWrapper defaultQuestionStructureWrapper : defaultQuestionStructureWrappers) {
+                List<DefaultQuestionUnitWrapper> questionUnitWrapperList = defaultQuestionStructureWrapper.getQuestionUnitWrapperList();
+                for (DefaultQuestionUnitWrapper defaultQuestionUnitWrapper : questionUnitWrapperList) {
+                    ExamQuestionEntity examQuestionEntity = new ExamQuestionEntity();
+                    examQuestionEntity.setExamRecordDataId(examRecordDataId);
+                    examQuestionEntity.setMainNumber(i + 1);
+                    examQuestionEntity.setOrder(++order);
+                    examQuestionEntity.setQuestionId(defaultQuestionStructureWrapper.getQuestionId());
+                    examQuestionEntity.setQuestionScore(defaultQuestionUnitWrapper.getQuestionScore());
+                    examQuestionEntity.setQuestionType(defaultQuestionUnitWrapper.getQuestionType());
+                    examQuestionEntity.setOptionPermutation(defaultQuestionUnitWrapper.getOptionPermutation());
+                    examQuestionEntity.setAudioPlayTimes(null);
+                    examQuestionEntity.setAnswerType(defaultQuestionUnitWrapper.getAnswerType());
+
+                    examQuestionEntityList.add(examQuestionEntity);
+                }
+            }
+        }
+
+        examRecordQuestionsEntity.setExamQuestionEntities(examQuestionEntityList);
+        examRecordQuestionsEntity.setExamRecordDataId(examRecordDataId);
+        examRecordQuestionsEntity.setCreationTime(new Date());
+        ExamRecordQuestionsEntity saveResult = examRecordQuestionsRepo.save(examRecordQuestionsEntity);
+//        redisTemplate.opsForList().leftPushAll(examQuestionKeyPrefix+examRecordDataId,examQuestionEntityList);
+        return saveResult;
+    }
+
+    /**
+     * 获取考试作答记录并修复考试记录数据(如有必要)
+     *
+     * @param examRecordData 考试记录
+     * @return
+     */
+    @Override
+    public ExamRecordQuestionsEntity getExamRecordQuestionsAndFixExamRecordDataIfNecessary(ExamRecordDataEntity examRecordData) {
+        if (examRecordData == null) {
+            throw new StatusException("201101", "examRecordData参数不允许为空");
+        }
+        Long examRecordDataId = examRecordData.getId();
+        String examRecordQuestionId = examRecordData.getExamRecordQuestionsId();
+
+        //如果考试作答记录id不为空,则根据id查询考试作答记录
+        if (StringUtils.isNotEmpty(examRecordQuestionId)) {
+            return GlobalHelper.getEntity(examRecordQuestionsRepo, examRecordQuestionId,
+                    ExamRecordQuestionsEntity.class);
+        }
+        //如果考试作答记录id为空,则根据考试记录id获取考试作答记录,并将考试作答记录id保存至examRecordData表中
+        else {
+            ExamRecordQuestionsEntity examRecordQuestionsEntity = examRecordQuestionsRepo.findByExamRecordDataId(examRecordDataId);
+
+            //将考试作答记录id保存至examRecordData表中,目的:纠正历史数据
+            examRecordDataRepo.updateExamRecordDataQuestionIdById(examRecordQuestionsEntity.getId(), examRecordData.getId());
+            return examRecordQuestionsEntity;
+        }
+    }
+
+    @Override
+    public ExamRecordQuestionsEntity getExamRecordQuestionsAndFixExamRecordDataIfNecessary(Long examRecordDataId) {
+        ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo,
+                examRecordDataId, ExamRecordDataEntity.class);
+        return getExamRecordQuestionsAndFixExamRecordDataIfNecessary(examRecordData);
+    }
+
+    @Override
+    public void submitQuestionAnswer(Long studentId, List<ExamStudentQuestionInfo> examQuestionInfos) {
+        ExamSessionInfo examSessionInfo = examSessionInfoService.getExamSessionInfo(studentId);
+        long examRecordDataId = examSessionInfo.getExamRecordDataId();
+
+        ExamRecordQuestionsEntity examRecordQuesitonsEntity = getExamRecordQuestionsAndFixExamRecordDataIfNecessary(examRecordDataId);
+        List<ExamQuestionEntity> examQuestionEntities = examRecordQuesitonsEntity.getExamQuestionEntities();
+
+        for (ExamStudentQuestionInfo examQuestionInfo : examQuestionInfos) {
+            for (ExamQuestionEntity examQuestion : examQuestionEntities) {
+                if (examQuestion.getOrder().equals(examQuestionInfo.getOrder())) {
+                    examQuestion.setStudentAnswer(examQuestionInfo.getStudentAnswer());
+                    examQuestion.setIsSign(examQuestionInfo.getIsSign());
+                    examQuestion.setIsAnswer(StringUtils.isNotBlank(examQuestion.getStudentAnswer()));
+                    examQuestion.setAudioPlayTimes(examQuestionInfo.getAudioPlayTimes());
+                    break;
+                }
+            }
+        }
+        examRecordQuestionsRepo.save(examRecordQuesitonsEntity);
+    }
+
+    @Override
+    public DefaultQuestionStructure getQuestionContent(Long studentId, String questionId) {
+        ExamSessionInfo examSessionInfo = examSessionInfoService.getExamSessionInfo(studentId);
+        if (examSessionInfo == null) {
+            throw new StatusException("100002", "考试已结束,不允许获取试题内容");
+        }
+
+        QuestionCacheBean getQuestionResp = CacheHelper.getQuestion(examSessionInfo.getExamId(),
+                examSessionInfo.getCourseCode(), examSessionInfo.getPaperType(), questionId);
+
+        DefaultQuestionStructure questionStructure = getQuestionResp.getDefaultQuestion().getMasterVersion();
+        List<DefaultQuestionUnit> questionUnits = questionStructure.getQuestionUnitList();
+
+        //在线考试,清除答案
+        if (examSessionInfo.getExamType().equals(ExamType.ONLINE.name())) {
+            for (DefaultQuestionUnit questionUnit : questionUnits) {
+                questionUnit.setRightAnswer(null);
+            }
+        }
+        return questionStructure;
+    }
+}

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

@@ -0,0 +1,56 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamStudentEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamType;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordRepo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordService;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月1日 上午10:25:41
+ * @company 	QMTH
+ * @description 考试记录服务实现
+ */
+@Service("examRecordService")
+public class ExamRecordServiceImpl implements ExamRecordService{
+	
+	@Autowired
+	private ExamRecordRepo examRecordRepo;
+
+	@Override
+	public ExamRecordEntity createExamRecord(ExamStudentEntity examStudent, 
+											 ExamBean examBean,
+											 CourseBean courseBean, 
+											 String basePaperId,
+											 String paperStructId) {
+		ExamRecordEntity examRecordEntity = new ExamRecordEntity();
+        examRecordEntity.setExamId(examBean.getId());
+        examRecordEntity.setExamType(ExamType.strToEnum(examBean.getExamType()));
+        
+        examRecordEntity.setExamStudentId(examStudent.getExamStudentId());
+        examRecordEntity.setStudentId(examStudent.getStudentId());
+        examRecordEntity.setStudentCode(examStudent.getStudentCode());
+        examRecordEntity.setStudentName(examStudent.getStudentName());
+        examRecordEntity.setIdentityNumber(examStudent.getIdentityNumber());
+        examRecordEntity.setOrgId(examStudent.getOrgId());
+        examRecordEntity.setRootOrgId(examStudent.getRootOrgId());
+        
+        examRecordEntity.setCourseId(courseBean.getId());
+        examRecordEntity.setCourseLevel(examStudent.getCourseLevel());
+        examRecordEntity.setBasePaperId(basePaperId);
+        
+        examRecordEntity.setPaperType(examStudent.getPaperType());
+        examRecordEntity.setPaperStructId(paperStructId);
+        
+        examRecordEntity.setInfoCollector(examStudent.getInfoCollector());
+        return examRecordRepo.save(examRecordEntity);
+	}
+
+}

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

@@ -0,0 +1,259 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.core.oe.common.base.utils.QuestionTypeUtil;
+import cn.com.qmth.examcloud.core.oe.common.entity.*;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordDataRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordQuestionsRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamScoreRepo;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamSessionInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.ObjectiveScoreInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordQuestionsService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamScoreService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamSessionInfoService;
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionStructure;
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionUnit;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.CourseCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.QuestionAnswerCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.QuestionCacheBean;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import org.apache.commons.lang3.StringUtils;
+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.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+/**
+ * @author chenken
+ * @date 2018年9月3日 上午10:02:38
+ * @company QMTH
+ * @description 考试记录得分(总分)服务实现
+ */
+@Service("examScoreService")
+public class ExamScoreServiceImpl implements ExamScoreService {
+
+    @Autowired
+    private ExamScoreRepo examScoreRepo;
+
+    @Autowired
+    private ExamRecordQuestionsService examRecordQuestionsService;
+
+    @Autowired
+    private ExamRecordDataRepo examRecordDataRepo;
+
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+
+    @Autowired
+    private RedisTemplate redisTemplate;
+    @Autowired
+    RedisClient redisClient;
+    @Autowired
+    private ExamSessionInfoService examSessionInfoService;
+    @Autowired
+    private ExamRecordQuestionsRepo examRecordQuestionsRepo;
+
+    private static final Logger log = LoggerFactory.getLogger(ExamScoreServiceImpl.class);
+
+    /**
+     * 保存考试总成绩
+     *
+     * @param examRecordData
+     * @return
+     */
+    @Override
+    public ExamScoreEntity saveExamScore(final ExamRecordEntity examRecord, ExamRecordDataEntity examRecordData) {
+        long st = System.currentTimeMillis();
+        long startTime = System.currentTimeMillis();
+        //获取考试作答记录并修复考试记录数据(如有必要)
+        ExamRecordQuestionsEntity examRecordQuestionsEntity = examRecordQuestionsService.
+                getExamRecordQuestionsAndFixExamRecordDataIfNecessary(examRecordData);
+        if (log.isDebugEnabled()) {
+            log.debug("1 [SAVE_EXAM_SCORE]获取考试作答记录并修复考试记录数据耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 考生作答记录明细
+        List<ExamQuestionEntity> examQuestionList = examRecordQuestionsEntity.getExamQuestionEntities();
+
+        double studentObjectiveScoreTotal = 0d;//客观题总分
+        int succQuestionNum = 0;//答题正确数
+        int objectiveQuestionsNum = 0;//客观题总数
+        //是否需要更新客观题答案
+        Boolean needUpdateObjectiveAnswer=false;
+        //获取考试会话
+        ExamSessionInfo examSessionInfo = examSessionInfoService.getExamSessionInfo(examRecord.getStudentId());
+        for (int i = 0; i < examQuestionList.size(); i++) {
+            ExamQuestionEntity examQuestionEntity = examQuestionList.get(i);
+            String courseCode;
+            //自动服务交卷时,会话可能已经过期,需要重新查询课程
+            if (examSessionInfo == null) {
+                CourseCacheBean courseCache = CacheHelper.getCourse(examRecord.getCourseId());
+                courseCode = courseCache.getCode();
+            }
+            //会话未过期,可以直接从会话中获取
+            else {
+                courseCode = examSessionInfo.getCourseCode();
+            }
+
+            //只计算客观点题分数
+            if (QuestionTypeUtil.isObjectiveQuestion(examQuestionEntity.getQuestionType())) {
+                objectiveQuestionsNum++;
+
+                //如果标准答案为空,则更新标准答案
+                if (StringUtils.isEmpty(examQuestionEntity.getCorrectAnswer())) {
+                    needUpdateObjectiveAnswer=true;
+                    updateCorrectAnswer(examRecord.getExamId(),courseCode,examRecord.getPaperType(),
+                            examQuestionEntity.getQuestionId(),examRecordQuestionsEntity);
+                }
+
+                if (examQuestionEntity.getStudentAnswer() != null
+                        && examQuestionEntity.getCorrectAnswer() != null
+                        && examQuestionEntity.getStudentAnswer().equals(examQuestionEntity.getCorrectAnswer())) {
+                    //小题分数
+                    double questionScore = examQuestionEntity.getQuestionScore().doubleValue();
+                    BigDecimal bigDecimalQuestionScore = new BigDecimal(Double.toString(questionScore));
+                    BigDecimal bigDecimalObjectiveScoreTotal = new BigDecimal(Double.toString(studentObjectiveScoreTotal));
+                    studentObjectiveScoreTotal = bigDecimalQuestionScore.add(bigDecimalObjectiveScoreTotal).doubleValue();
+                    succQuestionNum++;
+                }
+            }
+        }
+        if (needUpdateObjectiveAnswer){
+            examRecordQuestionsRepo.save(examRecordQuestionsEntity);
+        }
+
+        double objectiveAccuracy = 0;//客观题答题正确率
+        if (succQuestionNum > 0 && objectiveQuestionsNum > 0) {
+            objectiveAccuracy = Double.valueOf(new DecimalFormat("#.00").format(succQuestionNum * 100D / objectiveQuestionsNum));
+        }
+
+        Long examRecordDataId = examRecordData.getId();
+
+        startTime = System.currentTimeMillis();
+        //如果考试成绩不存在,则新增,否则更新数据
+        ExamScoreEntity examScoreEntity = examScoreRepo.findByExamRecordDataId(examRecordDataId);
+        if (log.isDebugEnabled()) {
+            log.debug("2 [SAVE_EXAM_SCORE]根据考试记录id获取考试分数数据耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        if (examScoreEntity == null) {
+            examScoreEntity = new ExamScoreEntity();
+            examScoreEntity.setExamRecordDataId(examRecordDataId);
+        }
+        examScoreEntity.setObjectiveScore(studentObjectiveScoreTotal);
+        examScoreEntity.setObjectiveAccuracy(objectiveAccuracy);
+        examScoreEntity.setTotalScore(studentObjectiveScoreTotal);//交卷时,总分=客观分得分
+
+        startTime = System.currentTimeMillis();
+        //保存考试成绩
+        ExamScoreEntity resultEntity = examScoreRepo.save(examScoreEntity);
+        if (log.isDebugEnabled()) {
+            log.debug("3 [SAVE_EXAM_SCORE]保存考试成绩耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        log.debug("4 [SAVE_EXAM_SCORE]合计 耗时:" + (System.currentTimeMillis() - st) + " ms");
+
+        return resultEntity;
+    }
+
+    /**
+     * 更新客观题答案
+     * @param examId
+     * @param courseCode
+     * @param paperType
+     * @param questionId
+     * @param examRecordQuestionsEntity
+     * @return
+     */
+    private void updateCorrectAnswer(Long examId, String courseCode, String paperType,
+                                    String questionId,ExamRecordQuestionsEntity examRecordQuestionsEntity) {
+        //更新客观题答案
+        QuestionAnswerCacheBean questionAnswerCache = CacheHelper.getQuestionAnswer(questionId);
+        List<String> rightAnswerList = questionAnswerCache.getRightAnswers();
+        //最小维度的小题单元集合
+        List<ExamQuestionEntity> questionUnitList = examRecordQuestionsEntity.getExamQuestionEntities().stream().
+                filter(p -> p.getQuestionId().equals(questionId)).
+                sorted((o1, o2) -> o1.getOrder().intValue() - o2.getOrder().intValue()).collect(Collectors.toList());
+
+        for (int i = 0; i < questionUnitList.size(); i++) {
+            ExamQuestionEntity examQuestionEntity = questionUnitList.get(i);
+            examQuestionEntity.setCorrectAnswer(rightAnswerList.get(i));
+        }
+    }
+
+    @Override
+    public void createExamScoreWithOffline(Long examRecordDataId) {
+        ExamScoreEntity examScoreEntity = new ExamScoreEntity();
+        examScoreEntity.setExamRecordDataId(examRecordDataId);
+        examScoreEntity.setObjectiveScore(0D);
+        examScoreEntity.setSubjectiveScore(0D);
+        examScoreEntity.setTotalScore(0D);
+        examScoreRepo.save(examScoreEntity);
+    }
+
+    @Override
+    public List<ExamScoreEntity> queryExamScoreListByExamStudentId(Long examStudentId) {
+        List<ExamRecordDataEntity> examRecordDataEntities = examRecordDataService.findValidExamRecordDataByExamStudentId(examStudentId);
+        if (examRecordDataEntities == null || examRecordDataEntities.size() == 0) {
+            return null;
+        }
+        List<Long> examRecordDataIds = examRecordDataEntities.stream().map(ExamRecordDataEntity::getId).collect(Collectors.toList());
+        return examScoreRepo.findByExamRecordDataIdIn(examRecordDataIds);
+    }
+
+    @Override
+    public List<ObjectiveScoreInfo> queryObjectiveScoreList(Long examStudentId) {
+        List<ExamRecordDataEntity> examRecordDataList = examRecordDataRepo.findByExamStudentId(examStudentId);
+        //过滤已完成的考试记录(包括违纪的)
+        examRecordDataList = examRecordDataList.stream().filter((o -> {
+            return o.getExamRecordStatus() == ExamRecordStatus.EXAM_END ||
+                    o.getExamRecordStatus() == ExamRecordStatus.EXAM_OVERDUE ||
+                    o.getExamRecordStatus() == ExamRecordStatus.EXAM_HAND_IN ||
+                    o.getExamRecordStatus() == ExamRecordStatus.EXAM_AUTO_HAND_IN;
+        })).collect(Collectors.toList());
+
+        List<ObjectiveScoreInfo> objectiveScoreInfoList = new ArrayList<ObjectiveScoreInfo>();
+        for (ExamRecordDataEntity examRecordDataEntity : examRecordDataList) {
+            ObjectiveScoreInfo objectiveScoreInfo = new ObjectiveScoreInfo();
+            objectiveScoreInfo.setExamRecordDataId(examRecordDataEntity.getId());
+            objectiveScoreInfo.setExamOrder(examRecordDataEntity.getExamOrder());
+            objectiveScoreInfo.setStartTime(examRecordDataEntity.getStartTime());
+
+            //如果考试没有结束,则只能返回部分数据
+            if (!examRecordDataService.isExamRecordEnded(examRecordDataEntity)) {
+                objectiveScoreInfo.setIsExamEnded(false);
+                objectiveScoreInfoList.add(objectiveScoreInfo);
+                continue;
+            } else {
+                objectiveScoreInfo.setIsExamEnded(true);
+            }
+
+            objectiveScoreInfo.setEndTime(examRecordDataEntity.getEndTime());
+            if (!examRecordDataEntity.getIsIllegality()) {
+                if (examRecordDataEntity.getIsWarn() && !examRecordDataEntity.getIsAudit()) {
+                    objectiveScoreInfo.setIsAuditing(true);
+                } else if (!examRecordDataEntity.getIsWarn() || (examRecordDataEntity.getIsWarn() && examRecordDataEntity.getIsAudit())) {
+                    ExamScoreEntity examScore = examScoreRepo.findByExamRecordDataId(examRecordDataEntity.getId());
+                    objectiveScoreInfo.setIsAuditing(false);
+                    objectiveScoreInfo.setObjectiveScore(examScore.getObjectiveScore());
+                }
+                objectiveScoreInfo.setIsIllegality(false);
+            } else {
+                objectiveScoreInfo.setIsIllegality(true);
+            }
+            objectiveScoreInfoList.add(objectiveScoreInfo);
+        }
+        return objectiveScoreInfoList;
+    }
+
+}

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

@@ -0,0 +1,46 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamSessionInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamSessionInfoService;
+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.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author chenken
+ * @date 2018/8/15 9:24
+ * @company QMTH
+ * @description 考试会话服务实现
+ */
+@Service("examSessionInfoService")
+public class ExamSessionInfoServiceImpl implements ExamSessionInfoService {
+
+    @Autowired
+    private RedisClient redisClient;
+    
+    @Value("${exam_redis_key_prefix}")
+    private String examRedisKeyPrefix;
+
+    private static final Logger log = LoggerFactory.getLogger(ExamSessionInfoServiceImpl.class);
+
+    @Override
+    public void saveExamSessionInfo(Long studentId, ExamSessionInfo examSessionInfo, int timeout) {
+        log.debug("11.4.1 进入开始保存考试会话方法,redisKey="+examRedisKeyPrefix+studentId);
+        redisClient.set(examRedisKeyPrefix+studentId,examSessionInfo,timeout);
+        ExamSessionInfo sessionInfo =redisClient.get(examRedisKeyPrefix+studentId,ExamSessionInfo.class);
+        log.debug("11.4.2 保存考试会话方法完成,尝试取一下值 ="+sessionInfo.getExamRecordId());
+    }
+
+    @Override
+    public ExamSessionInfo getExamSessionInfo(Long studentId) {
+        return redisClient.get(examRedisKeyPrefix+studentId,ExamSessionInfo.class);
+    }
+
+    @Override
+    public void deleteExamSessionInfo(Long studentId) {
+        redisClient.delete(examRedisKeyPrefix+studentId);
+    }
+}

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

@@ -0,0 +1,214 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.basic.api.bean.OrgBean;
+import cn.com.qmth.examcloud.core.basic.api.bean.StudentBean;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.CommonUtil;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamStudentEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.CourseLevel;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamProperties;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamType;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamStudentRepo;
+import cn.com.qmth.examcloud.core.oe.common.service.GainBaseDataService;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamStudentInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamCacheTransferHelper;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamStudentService;
+import cn.com.qmth.examcloud.examwork.api.ExamCloudService;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamSpecialSettingsBean;
+import cn.com.qmth.examcloud.examwork.api.request.GetOngoingExamListReq;
+import cn.com.qmth.examcloud.examwork.api.response.GetOngoingExamListResp;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.OrgCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.StudentCacheBean;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author chenken
+ * @date 2018年8月13日 下午2:46:10
+ * @company QMTH
+ * @description 考生服务实现
+ */
+@Service("examStudentService")
+public class ExamStudentServiceImpl implements ExamStudentService {
+
+    @Autowired
+    private ExamStudentRepo examStudentRepo;
+
+    @Autowired
+    private ExamCloudService examCloudService;
+
+    @Autowired
+    private GainBaseDataService gainBaseDataService;
+
+    /**
+     * 根据学生id获取考试列表
+     *
+     * @param studentId 学生id
+     */
+    @Override
+    public List<ExamStudentInfo> queryOnlineExamList(Long studentId) {
+        StudentCacheBean studentBean = CacheHelper.getStudent(studentId);
+
+        //获取可以考的和即将考的考试Id
+        GetOngoingExamListReq getOngoingExamListReq = new GetOngoingExamListReq();
+        getOngoingExamListReq.setExamType(ExamType.ONLINE.name());
+        getOngoingExamListReq.setRootOrgId(studentBean.getRootOrgId());
+        getOngoingExamListReq.setOrgId(studentBean.getOrgId());
+        GetOngoingExamListResp getOngoingExamListResp = examCloudService.getOngoingExamList(getOngoingExamListReq);
+
+        //获取学生所在组织机构的所有考试列表集合(虽然名字起的特殊考试设置,事实上取的就是实际的可考的考试列表,可能是考试中心特殊设置的考试时间,也可能不是)
+        List<ExamSpecialSettingsBean> examSpecialSettingsBeanList = getOngoingExamListResp.getExamSpecialSettingsList();
+        if (examSpecialSettingsBeanList == null || examSpecialSettingsBeanList.size() == 0) {
+            return null;
+        }
+        List<Long> examIds = examSpecialSettingsBeanList.stream().map(ExamSpecialSettingsBean::getExamId).collect(Collectors.toList());
+
+        //根据studentId和考试Id查询考生()
+//		List<ExamStudentEntity> examStudents = examStudentRepo.findByStudentIdAndExamIdIn(studentId, examIds);
+        //只查没有禁用的考生
+        List<ExamStudentEntity> examStudents = examStudentRepo.findByStudentIdAndEnableAndExamIdIn(studentId, true, examIds);
+        List<ExamStudentInfo> examStudentDtoList = new ArrayList<ExamStudentInfo>();
+        for (ExamStudentEntity examStudent : examStudents) {
+            Stream<ExamSpecialSettingsBean> examSpecialSettingsBeanStream = examSpecialSettingsBeanList.stream().filter(examSpecialSettingsBean -> {
+                return examSpecialSettingsBean.getExamId().longValue() == examStudent.getExamId().longValue();
+            });
+            if (examSpecialSettingsBeanStream != null) {
+                ExamSpecialSettingsBean examSpecialSettingsBean = examSpecialSettingsBeanStream.findFirst().get();
+                examStudentDtoList.add(assemblingExamStudentDto(examStudent, examSpecialSettingsBean));
+            }
+        }
+        return examStudentDtoList;
+    }
+
+    private ExamStudentInfo assemblingExamStudentDto(ExamStudentEntity examStudent, ExamSpecialSettingsBean examSpecialSettingsBean) {
+        ExamStudentInfo examStudentInfo = new ExamStudentInfo();
+        examStudentInfo.setExamStudentId(examStudent.getExamStudentId());
+        examStudentInfo.setStudentCode(examStudent.getStudentCode());
+        examStudentInfo.setStudentName(examStudent.getStudentName());
+        examStudentInfo.setRootOrgId(examStudent.getRootOrgId());
+        examStudentInfo.setIdentityNumber(examStudent.getIdentityNumber());
+
+        CourseBean courseBean = ExamCacheTransferHelper.getCachedCourse(examStudent.getCourseId());
+        examStudentInfo.setCourseName(courseBean.getName());
+        examStudentInfo.setCourseCode(courseBean.getCode());
+        examStudentInfo.setCourseLevel(CourseLevel.getCourseLevel(courseBean.getLevel()).getTitle());
+        examStudentInfo.setCourseId(examStudent.getCourseId());
+        examStudentInfo.setSpecialtyName(examStudent.getSpecialtyName());
+        examStudentInfo.setOrgId(examStudent.getOrgId());
+
+        OrgCacheBean orgBean = gainBaseDataService.getOrgBean(examStudent.getOrgId());
+        ExamBean examBean = ExamCacheTransferHelper.getCachedExam(examStudent.getExamId());
+
+        examStudentInfo.setOrgName(orgBean.getName());
+        examStudentInfo.setExamId(examBean.getId());
+        examStudentInfo.setExamName(examBean.getName());
+        examStudentInfo.setStartTime(examSpecialSettingsBean.getBeginTime());//考试开始时间设置
+        examStudentInfo.setEndTime(examSpecialSettingsBean.getEndTime());//考试结束时间设置
+        examStudentInfo.setAllowExamCount(countExamTimes(examStudent, examBean));
+        examStudentInfo.setPaperMins(examBean.getDuration());
+
+        //是否启用人脸识别
+        String isFaceEnable = CacheHelper.getExamOrgProperty(examBean.getId(), examStudent.getOrgId(),
+                ExamProperties.IS_FACE_ENABLE.name()).getValue();
+        if (StringUtils.isBlank(isFaceEnable)) {
+            examStudentInfo.setFaceEnable(false);
+        } else {
+            examStudentInfo.setFaceEnable(Boolean.valueOf(isFaceEnable));
+        }
+
+        //进入考试是否验证人脸识别(强制、非强制)
+        String isFaceCheck = CacheHelper.getExamOrgProperty(examBean.getId(), examStudent.getOrgId(),
+                ExamProperties.IS_FACE_CHECK.name()).getValue();
+        if (StringUtils.isBlank(isFaceCheck)) {
+            examStudentInfo.setFaceCheck(false);
+        } else {
+            examStudentInfo.setFaceCheck(Boolean.valueOf(isFaceCheck));
+        }
+        //是否显示客观分
+        String isObjScoreView = CacheHelper.getExamOrgProperty(examBean.getId(), examStudent.getOrgId(),
+                ExamProperties.IS_OBJ_SCORE_VIEW.name()).getValue();
+        if (StringUtils.isBlank(isObjScoreView)) {
+            examStudentInfo.setIsObjScoreView(false);
+        } else {
+            examStudentInfo.setIsObjScoreView(Boolean.valueOf(isObjScoreView));
+        }
+        return examStudentInfo;
+    }
+
+    @Override
+    public Integer countExamTimes(ExamStudentEntity examStudentInfo, ExamBean examBean) {
+        if (ExamType.OFFLINE.name().equals(examBean.getExamType())) {
+            return 1;
+        }
+        //考试批次中设置的考试次数
+        int examTimes = examBean.getExamTimes().intValue();
+        //正常考试的次数
+        int normalExamTimes = examStudentInfo.getNormalExamTimes();
+        /**
+         * 如果设置了重考,重考未完成;
+         * 且正常考试的次数大于等于设定的次数
+         * 将考试剩余次数设置为1
+         */
+        if (reexamineNotCompleted(examStudentInfo) && normalExamTimes >= examTimes) {
+            return 1;
+        }
+        int allowExamCount = examTimes - normalExamTimes;
+        return allowExamCount < 0 ? 0 : allowExamCount;
+    }
+
+    /**
+     * 设置了重考,且重考未完成
+     *
+     * @param examStudentInfo
+     * @return
+     */
+    private boolean reexamineNotCompleted(ExamStudentEntity examStudentInfo) {
+        if (examStudentInfo.getIsReExamine() != null && examStudentInfo.getIsReExamine()
+                && (examStudentInfo.getReExamineCompleted() == null || !examStudentInfo.getReExamineCompleted())) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public ExamStudentEntity updateExamStudentByStartExam(ExamStudentEntity examStudent, ExamBean examBean) {
+        Boolean isReExamine = examStudent.getIsReExamine();//是否有重考
+        Boolean reExamineCompleted = examStudent.getReExamineCompleted();//重考是否已完成
+        //考生的已考次数(不包括重考)
+        int normalExamTimes = examStudent.getNormalExamTimes() == null ? 0 : examStudent.getNormalExamTimes();
+
+        //考试机会,即可考次数
+        long canExamTimes = examBean.getExamTimes();
+        //有可能中途改变考试次数
+        if (canExamTimes > normalExamTimes) {
+            normalExamTimes = normalExamTimes + 1;
+            isReExamine = null;
+            reExamineCompleted = null;
+        }
+        // 考生开始重考时,将重考已完成设置为true
+        if (CommonUtil.isTrue(examStudent.getIsReExamine())) {
+            reExamineCompleted = true;
+        }
+        Date now = new Date();
+        //更新相关属性
+        examStudentRepo.updateExamStudentStartExamStatusInfo(examStudent.getId(), true,
+                normalExamTimes, isReExamine, reExamineCompleted, now);
+        examStudent.setFinished(true);
+        examStudent.setNormalExamTimes(normalExamTimes);
+        examStudent.setIsReExamine(isReExamine);
+        examStudent.setReExamineCompleted(isReExamine);
+        examStudent.setUpdateTime(now);
+        return examStudent;
+    }
+
+
+}

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

@@ -0,0 +1,263 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.basic.api.bean.OrgBean;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamRecordForMarkingEntity;
+import cn.com.qmth.examcloud.core.oe.common.entity.ExamStudentEntity;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamType;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordDataRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordForMarkingRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamRecordRepo;
+import cn.com.qmth.examcloud.core.oe.common.repository.ExamStudentRepo;
+import cn.com.qmth.examcloud.core.oe.common.service.GainBaseDataService;
+import cn.com.qmth.examcloud.core.oe.student.bean.OfflineExamCourseInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.*;
+import cn.com.qmth.examcloud.core.questions.api.ExtractConfigCloudService;
+import cn.com.qmth.examcloud.core.questions.api.request.GetPaperReq;
+import cn.com.qmth.examcloud.core.questions.api.response.GetPaperResp;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.OrgCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+import main.java.com.UpYun;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.transaction.Transactional;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月5日 下午3:24:44
+ * @company 	QMTH
+ * @description 离线考试服务实现
+ */
+@Service("offlineExamService")
+public class OfflineExamServiceImpl implements OfflineExamService {
+	
+	@Autowired
+	private ExamStudentRepo examStudentRepo;
+	
+	@Autowired
+	private ExamRecordDataRepo examRecordDataRepo;
+	
+	@Autowired
+	public ExamRecordRepo examRecordRepo;
+	
+    @Autowired
+    private GainBaseDataService gainBaseDataService;
+	
+	@Autowired
+    private ExtractConfigCloudService extractConfigCloudService;
+	
+	@Autowired
+    private ExamRecordService examRecordService;
+	
+	@Autowired
+    private ExamRecordDataService examRecordDataService;
+	
+	@Autowired
+	private ExamScoreService examScoreService;
+	
+	@Autowired
+	private ExamRecordForMarkingService examRecordForMarkingService;
+	
+	@Autowired
+	private ExamRecordForMarkingRepo examRecordForMarkingRepo;
+
+	@Value("${$upyun.site.1.bucketName}")
+    private String bucketName;
+	
+	@Value("${$upyun.site.1.userName}")
+	private String userName;
+	
+	@Value("${$upyun.site.1.password}")
+	private String password;
+	
+	@Value("${app.upyun.uploadUrl}")
+	private String upyunUploadUrl;
+	
+	@Value("${$upyun.site.1.domain}")
+	private String upyunFileUrl;
+	
+	@Override
+	public List<OfflineExamCourseInfo> getOfflineCourse(Long studentId) {
+		List<ExamStudentEntity> examStudents = examStudentRepo.findByStudentId(studentId);
+		List<OfflineExamCourseInfo> offlineExamCourseInfoList = new ArrayList<OfflineExamCourseInfo>();
+		for(ExamStudentEntity examStudent:examStudents){
+			ExamBean examBean = ExamCacheTransferHelper.getCachedExam(examStudent.getExamId(),
+					examStudent.getOrgId());
+			if(!ExamType.OFFLINE.name().equals(examBean.getExamType())){
+				continue;
+			}
+			if(!examBean.getEnable() || (examBean.getExamLimit() !=null && examBean.getExamLimit())){
+				continue;
+	        }
+	        if(new Date().before(examBean.getBeginTime()) || examBean.getEndTime().before(new Date())){
+	        	continue;
+	        }
+	        offlineExamCourseInfoList.add(toOfflineExamCourse(examStudent,examBean));
+		}
+		return offlineExamCourseInfoList;
+	}
+	private OfflineExamCourseInfo toOfflineExamCourse(ExamStudentEntity examStudent,ExamBean examBean) {
+		OfflineExamCourseInfo offlineExamCourseInfo = new OfflineExamCourseInfo();
+		
+		CourseBean courseBean = ExamCacheTransferHelper.getCachedCourse(examStudent.getCourseId());
+		offlineExamCourseInfo.setCourseCode(courseBean.getCode());
+		offlineExamCourseInfo.setCourseLevel(courseBean.getLevel());
+		offlineExamCourseInfo.setCourseName(courseBean.getName());
+		offlineExamCourseInfo.setExamId(examBean.getId());
+		offlineExamCourseInfo.setExamName(examBean.getName());
+		offlineExamCourseInfo.setSpecialtyName(examStudent.getSpecialtyName());
+		offlineExamCourseInfo.setExamStudentId(examStudent.getExamStudentId());
+		offlineExamCourseInfo.setStudentCode(examStudent.getStudentCode());
+		offlineExamCourseInfo.setStudentName(examStudent.getStudentName());
+		offlineExamCourseInfo.setStartTime(examBean.getBeginTime());
+		offlineExamCourseInfo.setEndTime(examBean.getEndTime());
+		Date nowDate = new Date();
+        if(nowDate.getTime()>offlineExamCourseInfo.getStartTime().getTime()
+        				&&nowDate.getTime()<offlineExamCourseInfo.getEndTime().getTime()){
+        	offlineExamCourseInfo.setIsvalid(true);
+        }else{
+        	offlineExamCourseInfo.setIsvalid(false);
+        }
+
+		OrgCacheBean orgBean = gainBaseDataService.getOrgBean(examStudent.getOrgId());
+		offlineExamCourseInfo.setOrgName(orgBean.getName());
+        
+		List<ExamRecordEntity> examRecords = examRecordRepo.findByExamStudentId(examStudent.getExamStudentId());
+		if(examRecords.size()>0){
+			ExamRecordEntity examRecord = examRecords.get(0);
+			ExamRecordDataEntity examRecordDataEntity = examRecordDataRepo.findByExamRecordId(examRecord.getId());
+			offlineExamCourseInfo.setExamRecordDataId(examRecordDataEntity.getId());
+			offlineExamCourseInfo.setStatus(examRecordDataEntity.getExamRecordStatus());
+			offlineExamCourseInfo.setPaperId(examRecord.getBasePaperId());
+			if(examRecordDataEntity.getExamRecordStatus() == ExamRecordStatus.EXAM_END){
+				ExamRecordForMarkingEntity examRecordForMarkingEntity = examRecordForMarkingRepo.findByExamRecordDataId(examRecordDataEntity.getId());
+				offlineExamCourseInfo.setOfflineFileUrl(examRecordForMarkingEntity.getOfflineFileUrl());
+				offlineExamCourseInfo.setFileName(examRecordForMarkingEntity.getOfflineFileName());
+			}
+		}
+		return offlineExamCourseInfo;
+	}
+
+	@Override
+	public void startOfflineExam(Long examStudentId) {
+		SysPropertyCacheBean stuClientLoginLimit = CacheHelper.getSysProperty("STU_CLIENT_LOGIN_LIMIT");
+		Boolean stuClientLoginLimitBoolean=false;
+		if (stuClientLoginLimit.getHasValue()){
+			stuClientLoginLimitBoolean= Boolean.valueOf(stuClientLoginLimit.getValue().toString());
+		}
+    	if (stuClientLoginLimitBoolean) {
+			throw new StatusException("OE-001505", "系统维护中... ...");
+		}
+		List<ExamRecordEntity> examRecordList = examRecordRepo.findByExamStudentId(examStudentId);
+        if(examRecordList!=null && examRecordList.size()>0){
+        	throw new StatusException("OfflineExamServiceImpl-startOfflineExam-exception","已经存在examStudentId="+examStudentId+"的离线考试记录");
+        }
+		//获取考生信息
+		ExamStudentEntity examStudentEntity = examStudentRepo.findByExamStudentId(examStudentId);
+		//检查并获取课程信息
+        CourseBean courseBean = checkCourse(examStudentEntity);
+        //检查并获取考试信息
+        ExamBean examBean = checkExam(examStudentEntity);
+        //获取题库试卷结构(由于存在随机抽卷,所以不能缓存 )
+        GetPaperReq getPaperReq = new GetPaperReq();
+        getPaperReq.setExamId(examStudentEntity.getExamId());
+        getPaperReq.setCourseCode(courseBean.getCode());
+        getPaperReq.setGroupCode(examStudentEntity.getPaperType());
+        GetPaperResp getPaperResp = extractConfigCloudService.getPaper(getPaperReq);
+
+        //生成考试记录
+        ExamRecordEntity examRecord = examRecordService.createExamRecord(examStudentEntity,examBean,courseBean,getPaperResp.getPaperId(),null);
+        ExamRecordDataEntity examRecordData = examRecordDataService.createOfflineExamRecordData(examRecord,getPaperResp.getDefaultPaper().getFullyObjective());
+        //生成分数
+        examScoreService.createExamScoreWithOffline(examRecordData.getId());
+        //更新考生
+        examStudentRepo.updateExamStudentFinished(examStudentId);
+	}
+	
+	private CourseBean checkCourse(ExamStudentEntity examStudentEntity) {
+        CourseBean courseBean = ExamCacheTransferHelper.getCachedCourse(examStudentEntity.getCourseId());
+        if(!courseBean.getEnable()){
+            throw new StatusException("OfflineExamServiceImpl-checkCourse-exception","该课程已被禁用");
+        }
+        return courseBean;
+	}
+	
+	private ExamBean checkExam(ExamStudentEntity examStudentEntity) {
+		ExamBean examBean = ExamCacheTransferHelper.getCachedExam(examStudentEntity.getExamId(),
+				examStudentEntity.getOrgId());
+
+		if(!examBean.getEnable() || (examBean.getExamLimit() !=null && examBean.getExamLimit())){
+        	throw new StatusException("ExamControlServiceImpl-checkExam-exception-01","暂无考试资格,请与学校老师联系");
+        }
+        if(new Date().before(examBean.getBeginTime())){
+        	throw new StatusException("ExamControlServiceImpl-checkExam-exception-02","考试未开始");
+        }
+        if(examBean.getEndTime().before(new Date())){
+        	throw new StatusException("ExamControlServiceImpl-checkExam-exception-03","本次考试已结束");
+        }
+        return examBean;
+	}
+
+	@Override
+	@Transactional
+	public void submitPaper(Long examRecordDataId, File tempFile) throws Exception {
+		SysPropertyCacheBean stuClientLoginLimit = CacheHelper.getSysProperty("STU_CLIENT_LOGIN_LIMIT");
+		Boolean stuClientLoginLimitBoolean=false;
+		if (stuClientLoginLimit.getHasValue()){
+			stuClientLoginLimitBoolean= Boolean.valueOf(stuClientLoginLimit.getValue().toString());
+		}
+    	if (stuClientLoginLimitBoolean) {
+			throw new StatusException("OE-001505", "系统维护中... ...");
+		}
+		ExamRecordDataEntity examRecordDataEntity = GlobalHelper.getEntity(examRecordDataRepo,examRecordDataId,ExamRecordDataEntity.class);
+		if(examRecordDataEntity == null){
+			return;
+		}
+		String fileName = tempFile.getName();
+		String fileSuffix = fileName.substring(fileName.lastIndexOf(".")+1, fileName.length()).toLowerCase();
+		//上传文件至又拍云
+		String fileNewName = createOfflineFileName(examRecordDataEntity)+"."+fileSuffix;
+
+		ExamRecordEntity examRecord =GlobalHelper.getEntity(examRecordRepo,examRecordDataEntity.getExamRecordId(),ExamRecordEntity.class);
+		String upyunFilePath = upyunUploadUrl+examRecord.getExamId()+"/"+fileNewName;
+		UpYun upyun = new UpYun(bucketName,userName,password);
+		upyun.writeFile(upyunFilePath,tempFile,true);
+		tempFile.delete();
+		//保存 文件信息
+		String fileUrl = upyunFileUrl+upyunFilePath;
+		examRecordForMarkingService.saveOffLineExamRecordForMarking(examRecordDataEntity, fileNewName, fileUrl);
+		//更新考试记录状态
+		examRecordDataEntity.setExamRecordStatus(ExamRecordStatus.EXAM_END);
+		examRecordDataEntity.setEndTime(new Date());//交卷(上传)时间
+		examRecordDataRepo.save(examRecordDataEntity);
+	}
+
+	private String createOfflineFileName(ExamRecordDataEntity examRecordDataEntity){
+		long currentTime = System.currentTimeMillis();
+
+		ExamRecordEntity examRecord =GlobalHelper.getEntity(
+				examRecordRepo,examRecordDataEntity.getExamRecordId(),ExamRecordEntity.class);
+		long orgId = examRecord.getOrgId();
+		OrgCacheBean orgBean = gainBaseDataService.getOrgBean(orgId);
+		long courseId = examRecord.getCourseId();
+		CourseBean courseBean = ExamCacheTransferHelper.getCachedCourse(courseId);
+		return orgBean.getCode()+"_"+
+				examRecord.getStudentCode()+"_"+
+				examRecord.getStudentName()+"_"+
+				courseBean.getCode()+"_"+currentTime;
+	}
+}

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

@@ -0,0 +1,267 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.QuestionTypeUtil;
+import cn.com.qmth.examcloud.core.oe.common.entity.*;
+import cn.com.qmth.examcloud.core.oe.common.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.core.oe.common.repository.*;
+import cn.com.qmth.examcloud.core.oe.common.service.GainBaseDataService;
+import cn.com.qmth.examcloud.core.oe.student.bean.PaperStructInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.PracticeCourseInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.PracticeDetailInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.PracticeRecordInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamCacheTransferHelper;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordPaperStructService;
+import cn.com.qmth.examcloud.core.oe.student.service.PracticeService;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionGroup;
+import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年9月7日 上午11:05:18
+ * @company 	QMTH
+ * @description PracticeServiceImpl.java
+ */
+@Service("practiceService")
+public class PracticeServiceImpl implements PracticeService {
+
+	@Autowired
+	private ExamStudentRepo examStudentRepo;
+	
+	@Autowired
+	private ExamRecordRepo examRecordRepo;
+	
+	@Autowired
+	private ExamRecordDataRepo examRecordDataRepo;
+	
+	@Autowired
+	private ExamScoreRepo examScoreRepo;
+	
+	@Autowired
+	private ExamRecordQuestionsRepo examRecordQuestionsRepo;
+	
+	@Autowired
+	private ExamRecordPaperStructService examRecordPaperStructService;
+	
+    @Autowired
+    private GainBaseDataService gainBaseDataService;
+	
+	@Override
+	public List<PracticeCourseInfo> queryPracticeCourseList(Long examId,Long studentId) {
+		if(examId == null || studentId == null){
+			return null;
+		}
+		List<ExamStudentEntity> examStudentList = examStudentRepo.findByExamIdAndStudentId(examId, studentId);
+		if(examStudentList == null || examStudentList.size() == 0){
+			return null;
+		}
+		List<PracticeCourseInfo> practiceCourseInfos = new ArrayList<PracticeCourseInfo>();
+		ExamBean examBean = ExamCacheTransferHelper.getCachedExam(examId,examStudentList.get(0).getOrgId());
+		for(ExamStudentEntity examStudent:examStudentList){
+			practiceCourseInfos.add(buildPracticeCourseInfo(examStudent,examBean));
+		}
+		return practiceCourseInfos;
+	}
+
+
+	private PracticeCourseInfo buildPracticeCourseInfo(ExamStudentEntity examStudent,ExamBean examBean) {
+		PracticeCourseInfo practiceCourseInfo = new PracticeCourseInfo();
+		practiceCourseInfo.setExamStudentId(examStudent.getExamStudentId());
+		practiceCourseInfo.setCourseCode(examStudent.getCourseCode());
+		
+		CourseBean courseBean = ExamCacheTransferHelper.getCachedCourse(examStudent.getCourseId());
+		practiceCourseInfo.setCourseName(courseBean.getName());
+		practiceCourseInfo.setStudentCode(examStudent.getStudentCode());
+		practiceCourseInfo.setStudentName(examStudent.getStudentName());
+		
+		practiceCourseInfo.setExamId(examBean.getId());
+		practiceCourseInfo.setExamName(examBean.getName());
+		practiceCourseInfo.setExamType(examBean.getExamType());
+		practiceCourseInfo.setStartTime(examBean.getBeginTime());
+		practiceCourseInfo.setEndTime(examBean.getEndTime());
+		
+		List<ExamRecordEntity> examRecordEntities = examRecordRepo.findByExamStudentId(examStudent.getExamStudentId());
+		
+		int allExamRecordNums = 0;
+		List<ExamScoreEntity> examScoreEntities = new ArrayList<ExamScoreEntity>();
+		for(ExamRecordEntity examRecord:examRecordEntities){
+			ExamRecordDataEntity examRecordData = examRecordDataRepo.findByExamRecordId(examRecord.getId());
+			if(examRecordData.getExamRecordStatus() != ExamRecordStatus.EXAM_ING){
+				allExamRecordNums++;
+				ExamScoreEntity examScore = examScoreRepo.findByExamRecordDataId(examRecordData.getId());
+				examScoreEntities.add(examScore);
+			}
+		}
+		double allObjectiveAccuracy = 0;//总正确率
+        double maxObjectiveAccuracy = 0;//最高客观题正确率
+        long recentObjectiveAccuracyId = 0;//最近ID
+        double recentObjectiveAccuracy = 0;//最近客观题正确率
+        
+        for(ExamScoreEntity examScore:examScoreEntities){
+        	if(examScore != null){
+        		if (examScore.getObjectiveAccuracy() > maxObjectiveAccuracy) {
+                    maxObjectiveAccuracy = examScore.getObjectiveAccuracy();
+                }
+            	if (examScore.getId() > recentObjectiveAccuracyId) {//判断是否为最近的
+                    recentObjectiveAccuracyId = examScore.getId();
+                    recentObjectiveAccuracy = examScore.getObjectiveAccuracy();
+                }
+            	allObjectiveAccuracy = allObjectiveAccuracy + examScore.getObjectiveAccuracy();
+        	}
+        }
+        //平均正确率
+        double aveObjectiveAccuracy = 0D;
+        if(allExamRecordNums>0){
+        	 aveObjectiveAccuracy = Double.valueOf(new DecimalFormat("#.00").format(allObjectiveAccuracy / allExamRecordNums));
+        }
+        practiceCourseInfo.setAveObjectiveAccuracy(aveObjectiveAccuracy);
+        practiceCourseInfo.setMaxObjectiveAccuracy(maxObjectiveAccuracy);
+        practiceCourseInfo.setRecentObjectiveAccuracy(recentObjectiveAccuracy);
+        practiceCourseInfo.setPracticeCount(allExamRecordNums);//练习总次数
+		return practiceCourseInfo;
+	}
+
+	@Override
+	public List<PracticeRecordInfo> queryPracticeRecordList(Long examStudentId) {
+		if(examStudentId == null){
+			return null;
+		}
+		List<ExamRecordEntity> examRecordList = examRecordRepo.findByExamStudentId(examStudentId);
+		List<PracticeRecordInfo> practiceRecords = new ArrayList<PracticeRecordInfo>();
+		ExamStudentEntity examStudent = examStudentRepo.findByExamStudentId(examStudentId);
+		CourseBean courseBean = ExamCacheTransferHelper.getCachedCourse(examStudent.getCourseId());
+		for(ExamRecordEntity examRecord:examRecordList){
+			ExamRecordDataEntity examRecordData = examRecordDataRepo.findByExamRecordId(examRecord.getId());
+			if(examRecordData.getExamRecordStatus() != ExamRecordStatus.EXAM_ING){
+				practiceRecords.add(buildPracticeRecord(examRecordData,courseBean));
+			}
+		}
+		return practiceRecords;
+	}
+
+	private PracticeRecordInfo buildPracticeRecord(ExamRecordDataEntity examRecordData,CourseBean courseBean) {
+		PracticeRecordInfo practiceRecordInfo = new PracticeRecordInfo();
+		
+		practiceRecordInfo.setId(examRecordData.getId());
+		practiceRecordInfo.setCourseCode(courseBean.getCode());
+		practiceRecordInfo.setCourseName(courseBean.getName());
+		practiceRecordInfo.setStartTime(examRecordData.getStartTime());
+		practiceRecordInfo.setEndTime(examRecordData.getEndTime());
+		practiceRecordInfo.setUsedExamTime(examRecordData.getUsedExamTime());
+		
+		ExamScoreEntity examScoreEntity = examScoreRepo.findByExamRecordDataId(examRecordData.getId());
+		practiceRecordInfo.setObjectiveAccuracy(examScoreEntity.getObjectiveAccuracy());//客观题答对的比率
+		ExamRecordQuestionsEntity examRecordQuestions = examRecordQuestionsRepo.findByExamRecordDataId(examRecordData.getId());
+		List<ExamQuestionEntity> examQuestionEntities = examRecordQuestions.getExamQuestionEntities();
+		practiceRecordInfo = calculationExamQuestionSituationInfo(practiceRecordInfo,examQuestionEntities);
+		
+		return practiceRecordInfo;
+	}
+
+	/**
+	 * 计算答题情况
+	 * @param practiceRecordInfo
+	 * @param examQuestionEntities
+	 * @return
+	 */
+	private PracticeRecordInfo calculationExamQuestionSituationInfo(PracticeRecordInfo practiceRecordInfo,List<ExamQuestionEntity> examQuestionEntities){
+		if(practiceRecordInfo == null){
+			practiceRecordInfo = new PracticeRecordInfo();
+		}
+		int succQuestionNum = 0;//正确答题数
+		int failQuestionNum = 0;//错误答题数
+		int notAnsweredCount = 0;//未作答题数
+		for(ExamQuestionEntity examQuestionEntity:examQuestionEntities){
+			if(examQuestionEntity.getIsAnswer() == null || !examQuestionEntity.getIsAnswer()){
+				notAnsweredCount++;
+			}else{
+				//客观题判断正确错误
+				if(QuestionTypeUtil.isObjectiveQuestion(examQuestionEntity.getQuestionType())){
+					if(examQuestionEntity.getCorrectAnswer() !=null 
+							&& examQuestionEntity.getStudentAnswer() != null 
+								&& examQuestionEntity.getCorrectAnswer().equals(examQuestionEntity.getStudentAnswer())){
+						succQuestionNum++;
+					}else{
+						failQuestionNum++;
+					}
+				}
+			}
+		}
+		int objectiveQuestionsNum = 0;//客观题总数
+		for(ExamQuestionEntity examQuestionEntity:examQuestionEntities){
+			if(QuestionTypeUtil.isObjectiveQuestion(examQuestionEntity.getQuestionType())){
+				objectiveQuestionsNum++;
+			}
+		}
+		practiceRecordInfo.setObjectiveNum(objectiveQuestionsNum);
+		practiceRecordInfo.setSuccQuestionNum(succQuestionNum);
+		practiceRecordInfo.setFailQuestionNum(failQuestionNum);
+		practiceRecordInfo.setNotAnsweredCount(notAnsweredCount);
+		practiceRecordInfo.setTotalQuestionCount(examQuestionEntities.size());
+		return practiceRecordInfo;
+	}
+
+	@Override
+	public PracticeDetailInfo getPracticeDetailInfo(Long examRecordDataId) {
+		if(examRecordDataId == null){
+			return null;
+		}
+		PracticeDetailInfo practiceDetailInfo = new PracticeDetailInfo();
+		//取出试卷结构
+		ExamRecordPaperStructEntity examRecordPaperStruct = examRecordPaperStructService.getExamRecordPaperStruct(examRecordDataId);
+		DefaultPaper defaultPaper = examRecordPaperStruct.getDefaultPaper();
+		List<PaperStructInfo> paperStructInfos = new  ArrayList<PaperStructInfo>();
+		List<DefaultQuestionGroup> questionGroupList = defaultPaper.getQuestionGroupList();
+		//取出作答记录
+		ExamRecordQuestionsEntity examRecordQuestions = examRecordQuestionsRepo.findByExamRecordDataId(examRecordDataId);
+		List<ExamQuestionEntity> examQuestionEntities = examRecordQuestions.getExamQuestionEntities();
+		//遍历大题
+		int objectiveNum = 0;//各个大题中的客观题总数
+		int objectSuccessNum = 0;//各个大题中的客观题答对的总数量
+		for(int i = 0;i<questionGroupList.size();i++){
+			DefaultQuestionGroup defaultQuestionGroup = questionGroupList.get(i);
+			PaperStructInfo paperStructInfo = new PaperStructInfo();
+			paperStructInfo.setTitle(defaultQuestionGroup.getGroupName());//大题名称
+			paperStructInfo.setIndex(i+1);//大题号
+			//使用大题号过滤
+			List<ExamQuestionEntity> examQuestionEntitiesOfMainQuestion = examQuestionEntities.stream().filter(o1->o1.getMainNumber().intValue() == paperStructInfo.getIndex()).collect(Collectors.toList());
+			//计算出作答情况
+			PracticeRecordInfo practiceRecordInfo = calculationExamQuestionSituationInfo(null,examQuestionEntitiesOfMainQuestion);
+			
+			objectiveNum += practiceRecordInfo.getObjectiveNum();
+			objectSuccessNum += practiceRecordInfo.getSuccQuestionNum();
+			
+			paperStructInfo.setQuestionCount(practiceRecordInfo.getTotalQuestionCount());
+			paperStructInfo.setSuccQuestionNum(practiceRecordInfo.getSuccQuestionNum());
+			paperStructInfo.setFailQuestionNum(practiceRecordInfo.getFailQuestionNum());
+			paperStructInfo.setNotAnsweredCount(practiceRecordInfo.getNotAnsweredCount());
+			paperStructInfos.add(paperStructInfo);
+		}
+		practiceDetailInfo.setPaperStructInfos(paperStructInfos);
+		ExamRecordDataEntity examRecordData = GlobalHelper.getEntity(examRecordDataRepo,examRecordDataId,ExamRecordDataEntity.class);
+		ExamRecordEntity examRecord =GlobalHelper.getEntity(examRecordRepo,examRecordData.getExamRecordId(),ExamRecordEntity.class);
+		CourseBean courseBean = ExamCacheTransferHelper.getCachedCourse(examRecord.getCourseId());
+		practiceDetailInfo.setCourseCode(courseBean.getCode());
+		practiceDetailInfo.setCourseName(courseBean.getName());
+		practiceDetailInfo.setId(examRecordDataId);
+		
+		double objectiveAccuracy = 0;//客观题答题正确率
+		if (objectiveNum > 0) {
+            objectiveAccuracy = Double.valueOf(new DecimalFormat("#.00").format(objectSuccessNum * 100D / objectiveNum));
+        }
+		practiceDetailInfo.setObjectiveAccuracy(objectiveAccuracy);
+		return practiceDetailInfo;
+	}
+	
+}

+ 30 - 0
examcloud-core-oe-student-starter/assembly.xml

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

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


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

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

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

@@ -0,0 +1 @@
+-server -Xms2g -Xmx2g

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

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

+ 86 - 0
examcloud-core-oe-student-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/starter/CoreOeApp.java

@@ -0,0 +1,86 @@
+package cn.com.qmth.examcloud.core.oe.student.starter;
+
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.oe.student.service.impl.ExamControlServiceImpl;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import cn.com.qmth.examcloud.web.upyun.UpyunSiteManager;
+import cn.com.qmth.examcloud.web.upyun.UpyunTest;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+import org.springframework.web.multipart.MultipartResolver;
+import org.springframework.web.multipart.commons.CommonsMultipartResolver;
+
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.web.bootstrap.AppBootstrap;
+
+@SuppressWarnings("SpringBootApplicationSetup")
+@SpringBootApplication
+@Configuration
+@EnableAutoConfiguration(exclude = {MultipartAutoConfiguration.class})
+@EnableJpaAuditing
+@EnableTransactionManagement
+@EnableEurekaClient
+@EnableDiscoveryClient
+@ComponentScan(basePackages = { "cn.com.qmth" })
+@EntityScan(basePackages = { "cn.com.qmth" })
+@EnableJpaRepositories(basePackages = { "cn.com.qmth" })
+@EnableMongoRepositories(basePackages= { "cn.com.qmth" })
+public class CoreOeApp {
+	static {
+		String runtimeLevel = System.getProperty("log.commonLevel");
+		if (null == runtimeLevel) {
+			System.setProperty("log.commonLevel", "DEBUG");
+		}
+		System.setProperty("hibernate.dialect.storage_engine", "innodb");
+	}
+
+	public static void main(String[] args) {
+		AppBootstrap.run(CoreOeApp.class, args);
+		UpyunSiteManager.init();
+		test();
+	/*for(int i=0;i<10000;i++) {
+		UpyunTest.testFormApi();
+			try {
+//				CacheHelper.getSysProperty("oe.examScoreNotify.url.7");
+				CacheHelper.getExamRecordProperty(80505L);
+				CacheHelper.getExamRecordProperty(9999999999L);
+			} catch (Exception e) {
+				e.printStackTrace();
+			}
+			CacheHelper.getExamRecordProperty(80505L);
+		}*/
+	}
+
+	private static void test() {
+		/*for(int i=0;i<99999;i++){
+			ExamControlServiceImpl bean = SpringContextHolder.getBean(ExamControlServiceImpl.class);
+			try {
+				bean.cleanExamRecords();
+			} catch (InterruptedException e) {
+				e.printStackTrace();
+			}
+			Util.sleep(5);
+		}*/
+	}
+
+	@Bean(name = "multipartResolver")
+	public MultipartResolver multipartResolver() {
+		CommonsMultipartResolver resolver = new CommonsMultipartResolver();
+		resolver.setDefaultEncoding("UTF-8");
+		resolver.setResolveLazily(true);
+		resolver.setMaxInMemorySize(2);
+		resolver.setMaxUploadSize(200 * 1024 * 1024);
+		return resolver;
+	}
+}

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

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

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

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

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

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

+ 7 - 0
examcloud-core-oe-student-starter/src/main/resources/application.properties

@@ -0,0 +1,7 @@
+spring.profiles.active=dev
+examcloud.startup.startupCode=1000
+examcloud.startup.configCenterHost=localhost
+#examcloud.startup.configCenterHost=192.168.10.201
+#examcloud.startup.configCenterHost=192.168.10.10
+examcloud.startup.configCenterPort=9999
+examcloud.startup.appSimpleName=oe-student

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


+ 89 - 0
examcloud-core-oe-student-starter/src/main/resources/log4j2.xml

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN" monitorInterval="30">
+
+	<Properties>
+		<Property name="commonLevel" value="${sys:log.commonLevel}" />
+	</Properties>
+
+	<Appenders>
+		<!-- 控制台 日志 -->
+		<Console name="Console" target="SYSTEM_OUT">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+		</Console>
+		<!-- debug 日志 -->
+		<RollingFile name="DEBUG_APPENDER" fileName="./logs/debug/debug.log" filePattern="./logs/debug/debug-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+			<DefaultRolloverStrategy max="10000">
+				<Delete basePath="./logs/debug" maxDepth="1">
+					<IfFileName glob="debug-*.log">
+						<IfAccumulatedFileSize exceeds="2 GB" />
+					</IfFileName>
+				</Delete>
+			</DefaultRolloverStrategy>
+		</RollingFile>
+		<!-- 接口日志 -->
+		<RollingFile name="INTERFACE_APPENDER" fileName="./logs/interface/interface.log" filePattern="./logs/interface/interface-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+			<DefaultRolloverStrategy max="10000">
+				<Delete basePath="./logs/interface" maxDepth="1">
+					<IfFileName glob="interface-*.log">
+						<IfAccumulatedFileSize exceeds="10 GB" />
+					</IfFileName>
+				</Delete>
+			</DefaultRolloverStrategy>
+		</RollingFile>
+		<!-- 清理考试记录自动服务的 日志 -->
+		<RollingFile name="CLEAN_EXAM_RECORD_TASK_APPENDER" fileName="./logs/clean/ert.log" filePattern="./logs/clean/ert-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100MB" />
+			</Policies>
+			<DefaultRolloverStrategy max="10000">
+				<Delete basePath="./logs/clean" maxDepth="1">
+					<IfFileName glob="ert-*.log">
+						<IfAccumulatedFileSize exceeds="2 GB" />
+					</IfFileName>
+				</Delete>
+			</DefaultRolloverStrategy>
+		</RollingFile>
+	</Appenders>
+
+	<Loggers>
+		<Logger name="cn.com.qmth" level="${commonLevel}" additivity="false">
+			<AppenderRef ref="DEBUG_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="INTERFACE_LOGGER" level="INFO" additivity="false">
+			<AppenderRef ref="INTERFACE_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="CLEAN_EXAM_RECORD_TASK_LOGGER" level="DEBUG" additivity="false">
+			<AppenderRef ref="CLEAN_EXAM_RECORD_TASK_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="${commonLevel}" />
+		<Logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="${commonLevel}" />
+		<Logger name="org.hibernate.SQL" level="${commonLevel}" />
+		<Logger name="org.hibernate.type" level="${commonLevel}" />
+		<Logger name="org.hibernate.engine.QueryParameters" level="${commonLevel}" />
+		<Logger name="org.hibernate.engine.query.HQLQueryPlan" level="${commonLevel}" />
+
+		<Root level="INFO">
+			<AppenderRef ref="Console" />
+			<AppenderRef ref="DEBUG_APPENDER" />
+		</Root>
+	</Loggers>
+
+</Configuration>

+ 11 - 0
examcloud-core-oe-student-starter/src/main/resources/security-exclusions.conf

@@ -0,0 +1,11 @@
+[][/][GET]
+[][/swagger/ui/index][GET]
+[/swagger-resources][][GET]
+[/swagger-resources][/configuration/ui][GET]
+[/swagger-resources][/configuration/security][GET]
+[][${springfox.documentation.swagger.v2.path:/v2/api-docs}][GET]
+[][/doc.html][GET]
+[][/swagger-ui.html][GET]
+[][/docs.html][GET]
+[${app.api.oe.student}/examFaceLivenessVerify][/faceLivenessVerifyCallback][POST]
+[${$rmp.cloud.oe.student}examRecord][/cleanExamRecords][POST]

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


+ 10 - 0
examcloud-core-oe-student-starter/src/main/resources/upyun.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<sites>
+	<site>
+		<id>capturePhoto</id>
+		<name>网考学生端抓拍照片</name>
+		<upyunId>1</upyunId>
+		<maxSize>1M</maxSize>
+		<path>/capture_photo/${rootOrgId}/${userId}/${timeMillis}${fileSuffix}</path>
+	</site>
+</sites>