瀏覽代碼

merge from release_v4.0.1

deason 4 年之前
父節點
當前提交
eadca394da
共有 27 個文件被更改,包括 1517 次插入458 次删除
  1. 11 1
      .gitignore
  2. 83 75
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/ExamControlController.java
  3. 4 4
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/FaceBiopsyController.java
  4. 1 1
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/provider/ExamRecordDataCloudServiceProvider.java
  5. 34 0
      examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/ExamContinuedRecordRepo.java
  6. 22 0
      examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/ExamProcessRecordRepo.java
  7. 78 0
      examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/entity/ExamContinuedRecordEntity.java
  8. 78 0
      examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/entity/ExamProcessRecordEntity.java
  9. 78 13
      examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/entity/ExamRecordDataEntity.java
  10. 82 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/ExamProcessRecordInfo.java
  11. 37 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/StartAnswerInfo.java
  12. 73 73
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/StartExamInfo.java
  13. 68 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/report/ExamProcessRecordReport.java
  14. 64 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/report/RocketMqConsumerListener.java
  15. 6 3
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamBossService.java
  16. 21 4
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamControlService.java
  17. 34 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamProcessRecordService.java
  18. 3 2
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamRecordDataService.java
  19. 497 163
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamControlServiceImpl.java
  20. 1 1
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamFaceLivenessVerifyServiceImpl.java
  21. 76 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamProcessRecordServiceImpl.java
  22. 29 2
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordDataServiceImpl.java
  23. 37 0
      examcloud-core-oe-student-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/starter/config/ExamProcessRecordTask.java
  24. 2 1
      examcloud-core-oe-student-starter/src/main/resources/application.properties
  25. 98 79
      examcloud-core-oe-student-starter/src/main/resources/log4j2.xml
  26. 0 18
      jenkins-dev.sh
  27. 0 18
      jenkins-test.sh

+ 11 - 1
.gitignore

@@ -1,9 +1,19 @@
+*.class
+
+# Proguard folder generated by ide
 .project
 .classpath
 .settings
 target/
 .idea/
 *.iml
+
+# Log Files
+*.log
+*.class
+
+
 # Package Files #
 *.jar
-logs/
+*.war
+*.ear

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

@@ -1,47 +1,10 @@
 package cn.com.qmth.examcloud.core.oe.student.api.controller;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-import javax.validation.Valid;
-
-import org.apache.commons.lang.math.RandomUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.ModelAttribute;
-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.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-import com.google.common.collect.Maps;
-import com.mysql.cj.util.StringUtils;
-
 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.Util;
 import cn.com.qmth.examcloud.core.oe.student.base.utils.Check;
-import cn.com.qmth.examcloud.core.oe.student.bean.BatchGetUpyunSignDomain;
-import cn.com.qmth.examcloud.core.oe.student.bean.BatchGetUpyunSignDomainQuery;
-import cn.com.qmth.examcloud.core.oe.student.bean.CheckExamInProgressInfo;
-import cn.com.qmth.examcloud.core.oe.student.bean.CheckQrCodeInfo;
-import cn.com.qmth.examcloud.core.oe.student.bean.EndExamInfo;
-import cn.com.qmth.examcloud.core.oe.student.bean.ExamProcessResultInfo;
-import cn.com.qmth.examcloud.core.oe.student.bean.GetAliyunSignDomain;
-import cn.com.qmth.examcloud.core.oe.student.bean.GetQrCodeReq;
-import cn.com.qmth.examcloud.core.oe.student.bean.GetUploadedFileAcknowledgeStatusReq;
-import cn.com.qmth.examcloud.core.oe.student.bean.GetUpyunSignDomain;
-import cn.com.qmth.examcloud.core.oe.student.bean.GetUpyunSignDomainQuery;
-import cn.com.qmth.examcloud.core.oe.student.bean.GetYunSignDomain;
-import cn.com.qmth.examcloud.core.oe.student.bean.GetYunSignDomainQuery;
-import cn.com.qmth.examcloud.core.oe.student.bean.GetYunSignatureReq;
-import cn.com.qmth.examcloud.core.oe.student.bean.SaveUploadedFileAcknowledgeStatusReq;
-import cn.com.qmth.examcloud.core.oe.student.bean.SaveUploadedFileReq;
-import cn.com.qmth.examcloud.core.oe.student.bean.StartExamInfo;
-import cn.com.qmth.examcloud.core.oe.student.bean.UpyunSignatureInfo;
+import cn.com.qmth.examcloud.core.oe.student.bean.*;
 import cn.com.qmth.examcloud.core.oe.student.service.ExamControlService;
 import cn.com.qmth.examcloud.core.oe.student.service.ExamFileAnswerService;
 import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordDataService;
@@ -64,8 +27,21 @@ import cn.com.qmth.examcloud.web.support.ControllerSupport;
 import cn.com.qmth.examcloud.web.support.Naked;
 import cn.com.qmth.examcloud.web.upyun.UpyunPathEnvironmentInfo;
 import cn.com.qmth.examcloud.web.upyun.UpyunService;
+import com.google.common.collect.Maps;
+import com.mysql.cj.util.StringUtils;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.apache.commons.lang.math.RandomUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 @Api(tags = "在线考试控制")
 @RestController
@@ -89,13 +65,13 @@ public class ExamControlController extends ControllerSupport {
     private RedisClient redisClient;
     @Autowired
     private ExamRecordDataService examRecordDataService;
-    
+
     /**
      * 开始考试
      */
     @ApiOperation(value = "开始考试")
     @GetMapping("/startExam")
-    public StartExamInfo startExam(@RequestParam Long examStudentId) {
+    public StartExamInfo startExam(@RequestParam Long examStudentId, HttpServletRequest request) {
         User user = getAccessUser();
         String sequenceLockKey = Constants.EXAM_CONTROL_LOCK_PREFIX + user.getUserId();
         StartExamInfo startExamInfo;
@@ -103,23 +79,39 @@ public class ExamControlController extends ControllerSupport {
         SequenceLockHelper.getLock(sequenceLockKey);
         Check.isNull(examStudentId, "examStudentId不能为空");
 
-        startExamInfo = examControlService.startExam(examStudentId, user);
+        startExamInfo = examControlService.startExam(examStudentId, user, getIp(request));
         return startExamInfo;
     }
 
+    /**
+     * 开始答题
+     */
+    @ApiOperation(value = "开始答题")
+    @PostMapping("/startAnswer")
+    public StartAnswerInfo startAnswer(@RequestParam @ApiParam(value = "考试记录id") Long examRecordDataId) {
+        User user = getAccessUser();
+        String sequenceLockKey = Constants.EXAM_CONTROL_LOCK_PREFIX + user.getUserId();
+        // 开始考试上锁,分布式锁,系统在请求结束后会,自动释放锁,无需手动解锁
+        SequenceLockHelper.getLock(sequenceLockKey);
+        Check.isNull(examRecordDataId, "examRecordDataId不能为空");
+
+        return examControlService.startAnswer(examRecordDataId);
+
+    }
+
     /**
      * 断点续考:检查正在进行中的考试
      */
     @ApiOperation(value = "断点续考:检查正在进行中的考试")
     @GetMapping("/checkExamInProgress")
-    public ExamProcessResultInfo checkExamInProgress() {
+    public ExamProcessResultInfo checkExamInProgress(HttpServletRequest request) {
         User user = getAccessUser();
         String sequenceLockKey = Constants.EXAM_CONTROL_LOCK_PREFIX + user.getUserId();
         // 系统在请求结束后会,自动释放锁,无需手动解锁
         SequenceLockHelper.getLock(sequenceLockKey);
         ExamProcessResultInfo res = new ExamProcessResultInfo();
         try {
-            CheckExamInProgressInfo info = examControlService.checkExamInProgress(user.getUserId());
+            CheckExamInProgressInfo info = examControlService.checkExamInProgress(user.getUserId(),getIp(request));
             res.setCode(Constants.COMMON_SUCCESS_CODE);
             res.setData(info);
             return res;
@@ -141,9 +133,9 @@ public class ExamControlController extends ControllerSupport {
      */
     @ApiOperation(value = "考试心跳")
     @GetMapping("/examHeartbeat")
-    public Long examHeartbeat() {
+    public Long examHeartbeat(HttpServletRequest request) {
         User user = getAccessUser();
-        return examControlService.examHeartbeat(user);
+        return examControlService.examHeartbeat(user, getIp(request));
     }
 
     /**
@@ -151,7 +143,7 @@ public class ExamControlController extends ControllerSupport {
      */
     @ApiOperation(value = "结束考试:交卷")
     @GetMapping("/endExam")
-    public void endExam() {
+    public void endExam(HttpServletRequest request) {
         User user = getAccessUser();
         Long studentId = user.getUserId();
         String sequenceLockKey = Constants.EXAM_CONTROL_LOCK_PREFIX + user.getUserId();
@@ -162,15 +154,15 @@ public class ExamControlController extends ControllerSupport {
         long startTime = System.currentTimeMillis();
 
         ExamingSession examingSession = examingSessionService.getExamingSession(studentId);
-        
-        if(examingSession==null) {
-        	throw new StatusException("8010", "无效的会话,请离开考试");
+
+        if (examingSession == null) {
+            throw new StatusException("8010", "无效的会话,请离开考试");
         }
 
         if (log.isDebugEnabled()) {
             log.debug("0 [END_EXAM] 交卷前处理耗时:" + (System.currentTimeMillis() - startTime) + " ms");
         }
-        examControlService.handInExam(examingSession.getExamRecordDataId(), HandInExamType.MANUAL);
+        examControlService.handInExam(examingSession.getExamRecordDataId(), HandInExamType.MANUAL,getIp(request));
         if (log.isDebugEnabled()) {
             log.debug("1 [END_EXAM]合计 耗时:" + (System.currentTimeMillis() - st) + " ms");
         }
@@ -196,35 +188,36 @@ public class ExamControlController extends ControllerSupport {
     public UpyunSignatureInfo getUpyunSignature(@ModelAttribute @Valid GetYunSignatureReq req) {
         return examControlService.getUpyunSignature(req);
     }
+
     /**
      * 获取云存储上传签名(微信小程序调用)
      */
     @ApiOperation(value = "获取文件上传签名(微信小程序调用)")
     @PostMapping("/yunSignature")
     public GetYunSignDomain getYunSignature(@ModelAttribute @Valid GetYunSignatureReq req) {
-        
-        if(FileStorageType.UPYUN.equals(FileStorageUtil.getFileStorageType())) {
-        	GetUpyunSignDomain result = new GetUpyunSignDomain();
-        	Map<String, String> params = Maps.newHashMap();
-        	UpyunSignatureInfo info=examControlService.getUpyunSignature(req);
-        	String signIdentifier = String.valueOf(System.currentTimeMillis());
-        	params.put("authorization", info.getSignature());
-        	params.put("policy", info.getPolicy());
-        	result.setAccessUrl(FileStorageHelper.getUrl(info.getUpyunFileDomain(), info.getFilePath()));
+
+        if (FileStorageType.UPYUN.equals(FileStorageUtil.getFileStorageType())) {
+            GetUpyunSignDomain result = new GetUpyunSignDomain();
+            Map<String, String> params = Maps.newHashMap();
+            UpyunSignatureInfo info = examControlService.getUpyunSignature(req);
+            String signIdentifier = String.valueOf(System.currentTimeMillis());
+            params.put("authorization", info.getSignature());
+            params.put("policy", info.getPolicy());
+            result.setAccessUrl(FileStorageHelper.getUrl(info.getUpyunFileDomain(), info.getFilePath()));
             result.setFormUrl(info.getUploadUrl());
             result.setFormParams(params);
             result.setSignIdentifier(signIdentifier);
-        	return result;
+            return result;
         }
-        if(FileStorageType.ALIYUN.equals(FileStorageUtil.getFileStorageType())) {
-        	String fileSuffix = req.getFileSuffix();
+        if (FileStorageType.ALIYUN.equals(FileStorageUtil.getFileStorageType())) {
+            String fileSuffix = req.getFileSuffix();
             if (StringUtils.isNullOrEmpty(fileSuffix)) {
                 throw new StatusException("5002", "文件后缀名不允许为空");
             }
-            
+
             ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(req.getExamRecordDataId());
             fileSuffix = fileSuffix.indexOf(".") == -1 ? "." + fileSuffix : fileSuffix;
-            
+
             StringBuffer filePath = new StringBuffer();
 
             filePath.append(OE_ANSWER_FILE_PATH).append(SEPARATOR)
@@ -243,7 +236,7 @@ public class ExamControlController extends ControllerSupport {
             String signIdentifier = String.valueOf(System.currentTimeMillis());
             FileStoragePathEnvInfo env = new FileStoragePathEnvInfo();
             env.setRelativePath(filePath.toString());
-            YunHttpRequest aliYunHttpRequest = FileStorageUtil.getSignature(FileStorageType.ALIYUN,Constants.MINI_PROGRAM_ANWSER_SITEID, env, req.getFileMd5());
+            YunHttpRequest aliYunHttpRequest = FileStorageUtil.getSignature(FileStorageType.ALIYUN, Constants.MINI_PROGRAM_ANWSER_SITEID, env, req.getFileMd5());
             result.setAccessUrl(aliYunHttpRequest.getAccessUrl());
             result.setFormUrl(aliYunHttpRequest.getFormUrl());
             result.setFormParams(aliYunHttpRequest.getFormParams());
@@ -284,7 +277,7 @@ public class ExamControlController extends ControllerSupport {
             String fileUrl = "";
             if (req.getFilePath().indexOf(",") > -1) {
                 for (String url : req.getFilePath().split(",")) {
-                    fileUrl += FileStorageUtil.realPath(url)+ ",";
+                    fileUrl += FileStorageUtil.realPath(url) + ",";
                 }
                 fileUrl = fileUrl.substring(0, fileUrl.length() - 1);
             } else {
@@ -336,10 +329,10 @@ public class ExamControlController extends ControllerSupport {
     public GetUpyunSignDomain getCapturePhotoUpYunSign(GetYunSignDomainQuery query) {
         return getUpYunSign(query);
     }
-    
+
     //又拍云签名
     private GetUpyunSignDomain getUpYunSign(GetYunSignDomainQuery query) {
-    	String fileSuffix = query.getFileSuffix();
+        String fileSuffix = query.getFileSuffix();
         if (StringUtils.isNullOrEmpty(fileSuffix)) {
             throw new StatusException("200001", "文件后缀名不允许为空");
         }
@@ -363,22 +356,22 @@ public class ExamControlController extends ControllerSupport {
         result.setSignIdentifier(signIdentifier);
         return result;
     }
-    
+
     @ApiOperation(value = "获取抓拍照片的云存储签名")
     @GetMapping("/getCapturePhotoYunSign")
     public GetYunSignDomain getCapturePhotoYunSign(GetYunSignDomainQuery query) {
-        if(FileStorageType.UPYUN.equals(FileStorageUtil.getFileStorageType())) {
-        	return getUpYunSign(query);
+        if (FileStorageType.UPYUN.equals(FileStorageUtil.getFileStorageType())) {
+            return getUpYunSign(query);
         }
-        if(FileStorageType.ALIYUN.equals(FileStorageUtil.getFileStorageType())) {
-        	return getAliYunSign(query);
+        if (FileStorageType.ALIYUN.equals(FileStorageUtil.getFileStorageType())) {
+            return getAliYunSign(query);
         }
         throw new StatusException("3002", "未配置正确云存储类型");
     }
 
     //阿里云签名
     private GetAliyunSignDomain getAliYunSign(GetYunSignDomainQuery query) {
-    	String fileSuffix = query.getFileSuffix();
+        String fileSuffix = query.getFileSuffix();
         if (StringUtils.isNullOrEmpty(fileSuffix)) {
             throw new StatusException("4001", "文件后缀名不允许为空");
         }
@@ -393,7 +386,7 @@ public class ExamControlController extends ControllerSupport {
         env.setRootOrgId(accessUser.getRootOrgId().toString());
         env.setUserId(accessUser.getUserId().toString());
         env.setFileSuffix(fileSuffix);
-        YunHttpRequest aliYunHttpRequest = FileStorageUtil.getSignature(FileStorageType.ALIYUN,Constants.CAPTURE_PHOTO_UPYUN_SITEID, env, query.getFileMd5());
+        YunHttpRequest aliYunHttpRequest = FileStorageUtil.getSignature(FileStorageType.ALIYUN, Constants.CAPTURE_PHOTO_UPYUN_SITEID, env, query.getFileMd5());
         redisClient.set(aliyunSignRedisKey, aliYunHttpRequest, 60);
         result.setAccessUrl(aliYunHttpRequest.getAccessUrl());
         result.setFormUrl(aliYunHttpRequest.getFormUrl());
@@ -447,4 +440,19 @@ public class ExamControlController extends ControllerSupport {
     public String getQrCode(@RequestBody GetQrCodeReq req) {
         return examControlService.getQrCode(req, getAccessUser());
     }
+
+    /**
+     * 记录切换屏幕
+     */
+    @ApiOperation(value = "记录切换屏幕次数")
+    @PostMapping("/switchScreen")
+    public void switchScreen(@RequestParam @ApiParam(value = "考试记录id") Long examRecordDataId) {
+        User user = getAccessUser();
+        String sequenceLockKey = Constants.EXAM_CONTROL_LOCK_PREFIX + user.getUserId();
+        // 开始考试上锁,分布式锁,系统在请求结束后会,自动释放锁,无需手动解锁
+        SequenceLockHelper.getLock(sequenceLockKey);
+        Check.isNull(examRecordDataId, "examRecordDataId不能为空");
+
+        examControlService.switchScreen(examRecordDataId);
+    }
 }

+ 4 - 4
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/FaceBiopsyController.java

@@ -69,7 +69,7 @@ public class FaceBiopsyController extends ControllerSupport {
 
     @Autowired
     private ExamControlService examControlService;
-    
+
     @Autowired
     private RedisClient redisClient;
 
@@ -116,10 +116,10 @@ public class FaceBiopsyController extends ControllerSupport {
         }
         // 非新活检,默认使用旧的活检计算方式
         else {
-        	
+
             String examingHeartbeatKey = RedisKeyHelper.getBuilder().examingHeartbeatKey(examSessionInfo.getExamRecordDataId());
             ExamingHeartbeat examingHeartbeat = redisClient.get(examingHeartbeatKey,ExamingHeartbeat.class);
-            
+
 			int usedMinute = null == examingHeartbeat
 					? 0
 					: examingHeartbeat.getCost().intValue() / 60;
@@ -224,7 +224,7 @@ public class FaceBiopsyController extends ControllerSupport {
 
         //如果活检满足交卷条件,则系统自动交卷,自动交卷逻辑不应该影响活检保存结果,所以不能放一个事务中
         if (resp.getEndExam()) {
-            examControlService.handInExam(req.getExamRecordDataId(), HandInExamType.AUTO);
+            examControlService.handInExam(req.getExamRecordDataId(), HandInExamType.AUTO,null);
         }
         return resp;
     }

+ 1 - 1
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/provider/ExamRecordDataCloudServiceProvider.java

@@ -257,7 +257,7 @@ public class ExamRecordDataCloudServiceProvider extends ControllerSupport implem
     @PostMapping("/handInExam")
     @Override
     public HandInExamResp handInExam(@RequestBody HandInExamReq req) {
-        examControlService.handInExam(req.getExamRecordDataId(), req.getHandInExamType());
+        examControlService.handInExam(req.getExamRecordDataId(), req.getHandInExamType(),req.getIp());
 
         return new HandInExamResp();
     }

+ 34 - 0
examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/ExamContinuedRecordRepo.java

@@ -0,0 +1,34 @@
+package cn.com.qmth.examcloud.core.oe.student.dao;
+
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamContinuedRecordEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * @Description 断点续考记录
+ * @Author lideyin
+ * @Date 2020/8/13 10:16
+ * @Version 1.0
+ */
+@Repository
+public interface ExamContinuedRecordRepo
+        extends JpaRepository<ExamContinuedRecordEntity, Long>, JpaSpecificationExecutor<ExamContinuedRecordEntity> {
+    /**
+     * 获取考试记录下的所有断点续考记录
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    List<ExamContinuedRecordEntity> findByExamRecordDataId(Long examRecordDataId);
+
+    /**
+     * 查找最新的断点续考记录
+     * @param examRecordDataId
+     * @return
+     */
+    ExamContinuedRecordEntity findTopByExamRecordDataIdOrderByIdDesc(Long examRecordDataId);
+
+}

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

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.core.oe.student.dao;
+
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamProcessRecordEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * @Description 考试过程记录
+ * @Author lideyin
+ * @Date 2020/8/20 10:28
+ * @Version 1.0
+ */
+@Repository
+public interface ExamProcessRecordRepo
+        extends JpaRepository<ExamProcessRecordEntity, Long>, JpaSpecificationExecutor<ExamProcessRecordEntity> {
+    List<ExamProcessRecordEntity> findByExamRecordDataIdOrderByRecordTime(Long examRecordDataId);
+
+    List<ExamProcessRecordEntity> findByExamRecordDataId(Long examRecordDataId);
+}

+ 78 - 0
examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/entity/ExamContinuedRecordEntity.java

@@ -0,0 +1,78 @@
+package cn.com.qmth.examcloud.core.oe.student.dao.entity;
+
+import cn.com.qmth.examcloud.web.jpa.WithIdJpaEntity;
+import org.hibernate.annotations.DynamicInsert;
+import org.hibernate.annotations.DynamicUpdate;
+
+import javax.persistence.Entity;
+import javax.persistence.Index;
+import javax.persistence.Table;
+import java.util.Date;
+
+/**
+ * @Description 断点续考记录表
+ * @Author lideyin
+ * @Date 2020/8/13 9:44
+ * @Version 1.0
+ */
+@Entity
+@Table(name = "ec_oes_exam_continued_record", indexes = {
+        @Index(name = "IDX_E_O_E_C_R_001", columnList = "examRecordDataId"),
+})
+@DynamicInsert
+@DynamicUpdate
+public class ExamContinuedRecordEntity extends WithIdJpaEntity {
+    private static final long serialVersionUID = 1559727365523696406L;
+
+    /**
+     * 考试记录ID
+     */
+    private Long examRecordDataId;
+
+    /**
+     * 断点续考时间
+     */
+    private Date continuedTime;
+
+    /**
+     * 开始答题时间
+     */
+    private Date startTime;
+
+    /**
+     * 考试已用时长(毫秒)
+     */
+    private long usedExamTime;
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public Date getContinuedTime() {
+        return continuedTime;
+    }
+
+    public void setContinuedTime(Date continuedTime) {
+        this.continuedTime = continuedTime;
+    }
+
+    public Date getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(Date startTime) {
+        this.startTime = startTime;
+    }
+
+    public long getUsedExamTime() {
+        return usedExamTime;
+    }
+
+    public void setUsedExamTime(long usedExamTime) {
+        this.usedExamTime = usedExamTime;
+    }
+}

+ 78 - 0
examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/entity/ExamProcessRecordEntity.java

@@ -0,0 +1,78 @@
+package cn.com.qmth.examcloud.core.oe.student.dao.entity;
+
+import cn.com.qmth.examcloud.web.jpa.WithIdJpaEntity;
+import org.hibernate.annotations.DynamicInsert;
+import org.hibernate.annotations.DynamicUpdate;
+
+import javax.persistence.Entity;
+import javax.persistence.Index;
+import javax.persistence.Table;
+import java.util.Date;
+
+/**
+ * @Description 考试过程记录表
+ * @Author lideyin
+ * @Date 2020/8/13 9:44
+ * @Version 1.0
+ */
+@Entity
+@Table(name = "ec_oes_exam_process_record", indexes = {
+        @Index(name = "IDX_E_O_E_P_R_001", columnList = "examRecordDataId"),
+})
+@DynamicInsert
+@DynamicUpdate
+public class ExamProcessRecordEntity extends WithIdJpaEntity {
+    private static final long serialVersionUID = 1559727365523696406L;
+
+    /**
+     * 考试记录ID
+     */
+    private Long examRecordDataId;
+
+    /**
+     * 过程名称
+     */
+    private String processName;
+
+    /**
+     * 过程记录时间
+     */
+    private Date recordTime;
+
+    /**
+     * 访问源ip
+     */
+    private String sourceIp;
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public String getProcessName() {
+        return processName;
+    }
+
+    public void setProcessName(String processName) {
+        this.processName = processName;
+    }
+
+    public Date getRecordTime() {
+        return recordTime;
+    }
+
+    public void setRecordTime(Date recordTime) {
+        this.recordTime = recordTime;
+    }
+
+    public String getSourceIp() {
+        return sourceIp;
+    }
+
+    public void setSourceIp(String sourceIp) {
+        this.sourceIp = sourceIp;
+    }
+}

+ 78 - 13
examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/entity/ExamRecordDataEntity.java

@@ -20,6 +20,7 @@ import cn.com.qmth.examcloud.api.commons.enums.ExamType;
 import cn.com.qmth.examcloud.support.enums.HandInExamType;
 import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
 import cn.com.qmth.examcloud.web.jpa.JpaEntity;
+import org.hibernate.annotations.DynamicUpdate;
 
 /**
  * @author chenken
@@ -36,6 +37,7 @@ import cn.com.qmth.examcloud.web.jpa.JpaEntity;
         @Index(name = "IDX_E_O_E_R_D_005", columnList = "batchNum"),
 })
 @DynamicInsert
+@DynamicUpdate
 public class ExamRecordDataEntity extends JpaEntity {
     /**
      *
@@ -218,17 +220,42 @@ public class ExamRecordDataEntity extends JpaEntity {
      * 批次号,自动服务遍历数据用
      */
     private Long batchNum;
-    
+
     /**
      * 试卷题目数量(校验提交答案的order)
      */
     private Integer questionCount;
-    
+
     /**
      * 是否是全客观题卷  1:是   0:否
      */
     private Boolean isAllObjectivePaper;
-    
+
+    /**
+     * 场次id
+     */
+    private Long examStageId;
+
+    /**
+     * 场次号
+     */
+    private Integer examStageOrder;
+
+    /**
+     * 学生最后活动时间
+     */
+    private Date lastActiveTime;
+
+    /**
+     * 断点续考时间
+     */
+    private Date continuedTime;
+
+    /**
+     * 进入考试时间
+     */
+    private Date enterExamTime;
+
     public Long getId() {
         return id;
     }
@@ -489,17 +516,17 @@ public class ExamRecordDataEntity extends JpaEntity {
         return syncStatus;
     }
 
-    
+
     public void setSyncStatus(SyncStatus syncStatus) {
         this.syncStatus = syncStatus;
     }
 
-    
+
     public Long getBatchNum() {
         return batchNum;
     }
 
-    
+
     public void setBatchNum(Long batchNum) {
         this.batchNum = batchNum;
     }
@@ -520,25 +547,63 @@ public class ExamRecordDataEntity extends JpaEntity {
         this.baiduFaceLivenessSuccessPercent = baiduFaceLivenessSuccessPercent;
     }
 
-    
+
     public Integer getQuestionCount() {
         return questionCount;
     }
 
-    
+
     public void setQuestionCount(Integer questionCount) {
         this.questionCount = questionCount;
     }
 
-    
+
     public Boolean getIsAllObjectivePaper() {
         return isAllObjectivePaper;
     }
 
-    
+
     public void setIsAllObjectivePaper(Boolean isAllObjectivePaper) {
         this.isAllObjectivePaper = isAllObjectivePaper;
     }
-    
-    
-}
+
+    public Long getExamStageId() {
+        return examStageId;
+    }
+
+    public void setExamStageId(Long examStageId) {
+        this.examStageId = examStageId;
+    }
+
+    public Integer getExamStageOrder() {
+        return examStageOrder;
+    }
+
+    public void setExamStageOrder(Integer examStageOrder) {
+        this.examStageOrder = examStageOrder;
+    }
+
+    public Date getLastActiveTime() {
+        return lastActiveTime;
+    }
+
+    public void setLastActiveTime(Date lastActiveTime) {
+        this.lastActiveTime = lastActiveTime;
+    }
+
+    public Date getContinuedTime() {
+        return continuedTime;
+    }
+
+    public void setContinuedTime(Date continuedTime) {
+        this.continuedTime = continuedTime;
+    }
+
+    public Date getEnterExamTime() {
+        return enterExamTime;
+    }
+
+    public void setEnterExamTime(Date enterExamTime) {
+        this.enterExamTime = enterExamTime;
+    }
+}

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

@@ -0,0 +1,82 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @Description 考试过程记录
+ * @Author lideyin
+ * @Date 2020/8/20 17:44
+ * @Version 1.0
+ */
+public class ExamProcessRecordInfo implements JsonSerializable {
+    private static final long serialVersionUID = 5667104330981650606L;
+
+    /**
+     * 主键id
+     */
+    private Long id;
+
+    /**
+     * 考试记录ID
+     */
+    private Long examRecordDataId;
+
+    /**
+     * 过程名称
+     */
+    private String processName;
+
+    /**
+     * 过程记录时间
+     */
+    private String recordTime;
+
+    /**
+     * 访问源ip
+     */
+    private String sourceIp;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public String getProcessName() {
+        return processName;
+    }
+
+    public void setProcessName(String processName) {
+        this.processName = processName;
+    }
+
+    public String getRecordTime() {
+        return recordTime;
+    }
+
+    public void setRecordTime(String recordTime) {
+        this.recordTime = recordTime;
+    }
+
+    public String getSourceIp() {
+        return sourceIp;
+    }
+
+    public void setSourceIp(String sourceIp) {
+        this.sourceIp = sourceIp;
+    }
+}

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

@@ -0,0 +1,37 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * @Description 开始答题
+ * @Author lideyin
+ * @Date 2020/8/18 10:48
+ * @Version 1.0
+ */
+public class StartAnswerInfo implements JsonSerializable {
+
+    private static final long serialVersionUID = 2633999021945517003L;
+
+    private Long examRecordDataId;
+
+    /**
+     * 考试时长
+     */
+    private Long duration;
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public Long getDuration() {
+        return duration;
+    }
+
+    public void setDuration(Long duration) {
+        this.duration = duration;
+    }
+}

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

@@ -3,79 +3,79 @@ 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
+ * @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;
-	}
-	
+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 faceVerifyMinute;
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    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;
+    }
+
 }

+ 68 - 0
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/report/ExamProcessRecordReport.java

@@ -0,0 +1,68 @@
+package cn.com.qmth.examcloud.core.oe.student.report;
+
+import cn.com.qmth.examcloud.reports.commons.bean.BaseReport;
+import cn.com.qmth.examcloud.reports.commons.enums.Tag;
+import cn.com.qmth.examcloud.reports.commons.enums.Topic;
+import cn.com.qmth.examcloud.support.enums.ExamProcess;
+
+import java.util.Date;
+
+/**
+ * @Description 考试过程记录
+ * @Author lideyin
+ * @Date 2020/8/20 14:21
+ * @Version 1.0
+ */
+public class ExamProcessRecordReport extends BaseReport {
+
+    /**
+     * 考试记录ID
+     */
+    private Long examRecordDataId;
+
+    /**
+     * 考试过程
+     */
+    private ExamProcess examProcess;
+
+    /**
+     * 过程记录时间
+     */
+    private Date recordTime;
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public ExamProcess getExamProcess() {
+        return examProcess;
+    }
+
+    public void setExamProcess(ExamProcess examProcess) {
+        this.examProcess = examProcess;
+    }
+
+    public Date getRecordTime() {
+        return recordTime;
+    }
+
+    public void setRecordTime(Date recordTime) {
+        this.recordTime = recordTime;
+    }
+
+    public ExamProcessRecordReport() {
+        super();
+    }
+
+    public ExamProcessRecordReport(Long examRecordDataId, ExamProcess examProcess, Date recordTime) {
+        this.examRecordDataId = examRecordDataId;
+        this.examProcess = examProcess;
+        this.recordTime = recordTime;
+        this.topic = Topic.REPORT_TOPIC.getCode();
+        this.tag = Tag.EXAM_PROCESS_RECORD.getCode();
+    }
+}

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

@@ -0,0 +1,64 @@
+package cn.com.qmth.examcloud.core.oe.student.report;
+
+import cn.com.qmth.examcloud.core.oe.student.service.ExamProcessRecordService;
+import cn.com.qmth.examcloud.reports.commons.enums.Tag;
+import cn.com.qmth.examcloud.reports.commons.enums.Topic;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import com.alibaba.fastjson.JSON;
+import com.aliyun.openservices.ons.api.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Properties;
+
+public class RocketMqConsumerListener {
+    private final static Logger logger = LoggerFactory.getLogger(RocketMqConsumerListener.class);
+    private static ExamProcessRecordService examProcessRecordService = SpringContextHolder.getBean(ExamProcessRecordService.class);
+    private static Properties properties = new Properties();
+
+    static {
+        properties.put(PropertyKeyConst.AccessKey, PropertyHolder.getString("$rocketmq-accesskey"));
+        // AccessKeySecret 阿里云身份验证,在阿里云服务器管理控制台创建。
+        properties.put(PropertyKeyConst.SecretKey, PropertyHolder.getString("$rocketmq-secretkey"));
+        // 设置发送超时时间,单位毫秒。
+        properties.setProperty(PropertyKeyConst.SendMsgTimeoutMillis, "3000");
+        // 设置 TCP 接入域名,进入控制台的实例详情页面的 TCP 协议客户端接入点区域查看。
+        properties.put(PropertyKeyConst.NAMESRV_ADDR, PropertyHolder.getString("$rocketmq-namesrv-addr"));
+        // 顺序消息消费失败进行重试前的等待时间,单位(毫秒),取值范围: 10 毫秒 ~ 30,000 毫秒
+        properties.put(PropertyKeyConst.SuspendTimeMillis, "100");
+        // 消息消费失败时的最大重试次数
+        properties.put(PropertyKeyConst.MaxReconsumeTimes, "10");
+    }
+
+    public static void start() {
+        onlineExamStudent();
+    }
+
+    private static void onlineExamStudent() {
+        properties.put(PropertyKeyConst.GROUP_ID, Tag.EXAM_PROCESS_RECORD.getGroup());
+        Consumer consumer = ONSFactory.createConsumer(properties);
+        consumer.subscribe(Topic.REPORT_TOPIC.getCode(), Tag.EXAM_PROCESS_RECORD.getCode(), new MessageListener() {
+            @Override
+            public Action consume(Message message, ConsumeContext context) {
+                try {
+                    String msg = new String(message.getBody(), "utf-8");
+                    onMessageExamStudent(msg);
+                    return Action.CommitMessage;
+                } catch (Exception e) {
+                    logger.error("consumer failed MsgID:" + message.getMsgID(), e);
+                    return Action.ReconsumeLater;
+                }
+            }
+
+        });
+
+        consumer.start();
+    }
+
+    private static void onMessageExamStudent(String message) {
+        ExamProcessRecordReport r = JSON.parseObject(message, ExamProcessRecordReport.class);
+        examProcessRecordService.saveExamProcessRecord(r.getExamRecordDataId(),
+                r.getExamProcess().getDesc(), r.getRecordTime(), r.getRemoteHost());
+    }
+}

+ 6 - 3
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamBossService.java

@@ -4,18 +4,20 @@ import cn.com.qmth.examcloud.support.examing.ExamBoss;
 
 /**
  * @author chenken
- *
  */
 public interface ExamBossService {
 
     /**
      * 保存
-     * @param timeout   秒
+     *
+     * @param examStudentId
+     * @param eb
      */
-    public void saveExamBoss(Long examStudentId,ExamBoss eb);
+    public void saveExamBoss(Long examStudentId, ExamBoss eb);
 
     /**
      * 获取
+     *
      * @param examStudentId
      * @return
      */
@@ -23,6 +25,7 @@ public interface ExamBossService {
 
     /**
      * 删除
+     *
      * @param examStudentId
      */
     public void deleteExamBoss(Long examStudentId);

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

@@ -5,6 +5,8 @@ import cn.com.qmth.examcloud.api.commons.security.bean.User;
 import cn.com.qmth.examcloud.core.oe.student.bean.*;
 import cn.com.qmth.examcloud.support.enums.HandInExamType;
 
+import javax.validation.constraints.Null;
+
 /**
  * @author chenken
  * @date 2018年8月13日 下午2:09:38
@@ -19,29 +21,37 @@ public interface ExamControlService {
      * @param examStudentId
      * @param user
      */
-    StartExamInfo startExam(Long examStudentId, User user);
+    StartExamInfo startExam(Long examStudentId, User user, String ip);
+
+    /**
+     * 开始答题
+     *
+     * @param examRecordDataId
+     */
+    StartAnswerInfo startAnswer(Long examRecordDataId);
 
     /**
      * 交卷
      *
      * @param examRecordDataId 考试记录id
      * @param handInExamType   交卷类型
+     * @param ip 请求ip,可以为空
      */
-    void handInExam(Long examRecordDataId, HandInExamType handInExamType);
+    void handInExam(Long examRecordDataId, HandInExamType handInExamType,@Null String ip);
 
     /**
      * 断点续考:检查正在进行中的考试
      *
      * @param studentId
      */
-    CheckExamInProgressInfo checkExamInProgress(Long studentId);
+    CheckExamInProgressInfo checkExamInProgress(Long studentId,String ip);
 
     /**
      * 考试心跳
      *
      * @param
      */
-    long examHeartbeat(User user);
+    long examHeartbeat(User user, String ip);
 
     /**
      * 获取考试结束后的相关信息
@@ -84,4 +94,11 @@ public interface ExamControlService {
      * @return
      */
     String getQrCode(GetQrCodeReq req, User user);
+
+    /**
+     * 记录切换屏幕次数
+     *
+     * @param examRecordDataId
+     */
+    void switchScreen(Long examRecordDataId);
 }

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

@@ -0,0 +1,34 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamProcessRecordInfo;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @Description 考试过程记录服务
+ * @Author lideyin
+ * @Date 2020/8/20 17:39
+ * @Version 1.0
+ */
+public interface ExamProcessRecordService {
+
+    /**
+     * 保存考试记录
+     *
+     * @param examRecordDataId
+     * @param processName
+     * @param recordTime
+     * @param sourceIp
+     */
+    void saveExamProcessRecord(Long examRecordDataId, String processName, Date recordTime, String sourceIp);
+
+    /**
+     * 获取考试过程记录
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    List<ExamProcessRecordInfo> getExamProcessRecords(Long examRecordDataId);
+
+}

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

@@ -27,8 +27,9 @@ public interface ExamRecordDataService {
      * @param isFullyObjective
      * @return
      */
-    ExamRecordData createExamRecordData(ExamingSession examingSession, ExamSettingsCacheBean examBean, CourseCacheBean courseBean,
-                                        String basePaperId,boolean isFullyObjective);
+    ExamRecordData createExamRecordData(ExamingSession examingSession, ExamSettingsCacheBean examBean,
+                                        CourseCacheBean courseBean,String basePaperId,boolean isFullyObjective,
+                                        Long examStageId,Integer examStageOrder);
 
     /**
      *

文件差異過大導致無法顯示
+ 497 - 163
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamControlServiceImpl.java


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

@@ -225,7 +225,7 @@ public class ExamFaceLivenessVerifyServiceImpl implements ExamFaceLivenessVerify
 
         //如果活体检失败,需要清除会话并自动交卷
         if (IsSuccess.strToEnum(result) == IsSuccess.FAILED) {
-            examControlService.handInExam(examRecordDataId, HandInExamType.AUTO);
+            examControlService.handInExam(examRecordDataId, HandInExamType.AUTO,null);
         }
     }
 

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

@@ -0,0 +1,76 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.commons.util.DateUtil;
+import cn.com.qmth.examcloud.core.oe.student.base.utils.DateUtils;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamProcessRecordInfo;
+import cn.com.qmth.examcloud.core.oe.student.dao.ExamProcessRecordRepo;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamProcessRecordEntity;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamBossService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamProcessRecordService;
+import cn.com.qmth.examcloud.support.examing.ExamBoss;
+import cn.com.qmth.examcloud.support.redis.RedisKeyHelper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @Description 考试过程记录
+ * @Author lideyin
+ * @Date 2020/8/20 17:49
+ * @Version 1.0
+ */
+@Service("examProcessRecordService")
+public class ExamProcessRecordServiceImpl implements ExamProcessRecordService {
+	@Autowired
+	private ExamProcessRecordRepo examProcessRecordRepo;
+
+	/**
+	 * 保存考试记录
+	 *
+	 * @param examRecordDataId
+	 * @param processName
+	 * @param recordTime
+	 * @param sourceIp
+	 */
+	@Override
+	public void saveExamProcessRecord(Long examRecordDataId, String processName, Date recordTime, String sourceIp) {
+		ExamProcessRecordEntity entity = new ExamProcessRecordEntity();
+		entity.setExamRecordDataId(examRecordDataId);
+		entity.setProcessName(processName);
+		entity.setRecordTime(recordTime);
+		entity.setSourceIp(sourceIp);
+
+		examProcessRecordRepo.save(entity);
+	}
+
+	/**
+	 * 获取考试过程记录
+	 *
+	 * @param examRecordDataId
+	 * @return
+	 */
+	@Override
+	public List<ExamProcessRecordInfo> getExamProcessRecords(Long examRecordDataId) {
+		List<ExamProcessRecordEntity> recordList =
+				examProcessRecordRepo.findByExamRecordDataIdOrderByRecordTime(examRecordDataId);
+		List<ExamProcessRecordInfo> resultList = new ArrayList<>();
+		if (null != recordList && !recordList.isEmpty()) {
+			for (ExamProcessRecordEntity record : recordList) {
+				ExamProcessRecordInfo info=new ExamProcessRecordInfo();
+				info.setId(record.getId());
+				info.setExamRecordDataId(record.getExamRecordDataId());
+				info.setProcessName(record.getProcessName());
+				info.setRecordTime(DateUtil.format(record.getRecordTime(), DateUtil.DatePatterns.CHINA_DEFAULT));
+				info.setSourceIp(record.getSourceIp());
+
+				resultList.add(info);
+			}
+		}
+
+		return resultList;
+	}
+}

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

@@ -7,6 +7,11 @@ import java.util.Date;
 import java.util.List;
 import java.util.stream.Collectors;
 
+import cn.com.qmth.examcloud.api.commons.enums.ExamSpecialSettingsType;
+import cn.com.qmth.examcloud.api.commons.enums.ExamStageStartExamStatus;
+import cn.com.qmth.examcloud.examwork.api.ExamStageCloudService;
+import cn.com.qmth.examcloud.examwork.api.request.ModifyExamStageStartExamStatusReq;
+import cn.com.qmth.examcloud.support.helper.ExamCacheTransferHelper;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -72,10 +77,14 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
     @Autowired
     private ExamRecordQuestionsService examRecordQuestionsService;
 
+    @Autowired
+    private ExamStageCloudService examStageCloudService;
+
     @Transactional
     @Override
     public ExamRecordData createExamRecordData(ExamingSession examingSession, ExamSettingsCacheBean examBean,
-                                               CourseCacheBean courseBean, String basePaperId,boolean isFullyObjective) {
+                                               CourseCacheBean courseBean, String basePaperId,boolean isFullyObjective,
+                                               Long examStageId,Integer examStageOrder) {
         ExamRecordDataEntity examRecordData = new ExamRecordDataEntity();
         examRecordData.setExamId(examBean.getId());
         examRecordData.setExamType(ExamType.valueOf(examBean.getExamType()));
@@ -90,7 +99,7 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
 
         examRecordData.setPaperType(examingSession.getPaperType());
 
-        examRecordData.setStartTime(new Date());
+        examRecordData.setEnterExamTime(new Date());
         examRecordData.setIsContinued(false);
         examRecordData.setContinuedCount(0);
         examRecordData.setFaceSuccessCount(0);
@@ -103,7 +112,22 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
         examRecordData.setIsIllegality(false);
         examRecordData.setIsWarn(false);
         examRecordData.setIsAudit(false);
+        examRecordData.setExamStageId(examStageId);
+        examRecordData.setExamStageOrder(examStageOrder);
         examRecordDataRepo.save(examRecordData);
+
+        //更新场次的开考状态
+        if (null != examStageId) {
+            if (examBean.getSpecialSettingsEnabled()
+                    && ExamSpecialSettingsType.STAGE_BASED == examBean.getSpecialSettingsType()) {
+                //更改考务中的考试状态 TODO 高并发下此处可能会影响开考的性能
+                ModifyExamStageStartExamStatusReq req = new ModifyExamStageStartExamStatusReq();
+                req.setExamStageId(examStageId);
+                req.setExamStageStartExamStatus(ExamStageStartExamStatus.STARTED.name());
+                examStageCloudService.modifyExamStageStartExamStatus(req);
+            }
+        }
+
         ExamRecordData bean = of(examRecordData);
         //存入redis
         saveExamRecordDataCache(examRecordData.getId(), bean);
@@ -169,6 +193,9 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
         bean.setSyncStatus(et.getSyncStatus());
         bean.setQuestionCount(et.getQuestionCount());
         bean.setIsAllObjectivePaper(et.getIsAllObjectivePaper());
+        bean.setEnterExamTime(et.getEnterExamTime());
+        bean.setExamStageId(et.getExamStageId());
+        bean.setExamStageOrder(et.getExamStageOrder());
         return bean;
     }
 

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

@@ -0,0 +1,37 @@
+package cn.com.qmth.examcloud.core.oe.student.starter.config;
+
+import cn.com.qmth.examcloud.core.oe.student.report.RocketMqConsumerListener;
+import cn.com.qmth.examcloud.reports.commons.enums.MqType;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+@Component
+@Order(999)
+public class ExamProcessRecordTask implements ApplicationRunner {
+    private final static Logger logger = LoggerFactory.getLogger(ExamProcessRecordTask.class);
+
+    private void startConsumerListener() {
+
+        String mqType = PropertyHolder.getString("$report.mq-type");
+
+        Boolean reportEnable = PropertyHolder.getBoolean("$report.enable", false);
+
+        if (reportEnable) {
+            if (MqType.ROCKETMQ.getCode().equals(mqType)) {
+                RocketMqConsumerListener.start();
+            } else {
+                logger.error("no $report.mq-type property config!");
+            }
+        }
+    }
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        new Thread(() -> startConsumerListener()).start();
+    }
+}

+ 2 - 1
examcloud-core-oe-student-starter/src/main/resources/application.properties

@@ -1,5 +1,6 @@
 spring.profiles.active=dev
+
 examcloud.startup.startupCode=1000
-examcloud.startup.configCenterHost=127.0.0.1
+examcloud.startup.configCenterHost=192.168.10.39
 examcloud.startup.configCenterPort=9999
 examcloud.startup.appCode=OES

+ 98 - 79
examcloud-core-oe-student-starter/src/main/resources/log4j2.xml

@@ -1,89 +1,108 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Configuration status="WARN" monitorInterval="30">
 
-	<Properties>
-		<Property name="commonLevel" value="${sys:log.commonLevel}" />
-	</Properties>
+    <Properties>
+        <Property name="commonLevel" value="${sys:log.commonLevel}"/>
+        <Property name="logPattern">
+            %d{yyyy-MM-dd HH:mm:ss.SSS} | %clr{%level} | %X{TRACE_ID} %X{CALLER} | %clr{%c{1.1}:%L}{cyan} | %m%n
+        </Property>
+    </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>
+    <Appenders>
+        <!-- 控制台 日志 -->
+        <Console name="Console" target="SYSTEM_OUT">
+            <PatternLayout pattern="${logPattern}" charset="UTF-8"/>
+        </Console>
 
-	<Loggers>
-		<Logger name="cn.com.qmth" level="${commonLevel}" additivity="false">
-			<AppenderRef ref="DEBUG_APPENDER" />
-			<AppenderRef ref="Console" />
-		</Logger>
+        <!-- debug 日志 -->
+        <RollingFile name="DEBUG_APPENDER"
+                     fileName="./logs/debug/debug.log"
+                     filePattern="./logs/debug/debug-%d{yyyy.MM.dd.HH}-%i.log">
+            <PatternLayout pattern="${logPattern}" charset="UTF-8"/>
+            <Policies>
+                <TimeBasedTriggeringPolicy interval="1" modulate="false"/>
+                <SizeBasedTriggeringPolicy size="100 MB"/>
+            </Policies>
+            <DefaultRolloverStrategy max="1000">
+                <Delete basePath="./logs/debug" maxDepth="1">
+                    <IfFileName glob="debug-*.log">
+                        <IfAccumulatedFileSize exceeds="2 GB"/>
+                    </IfFileName>
+                </Delete>
+            </DefaultRolloverStrategy>
+        </RollingFile>
 
-		<Logger name="INTERFACE_LOGGER" level="INFO" additivity="false">
-			<AppenderRef ref="INTERFACE_APPENDER" />
-			<AppenderRef ref="Console" />
-		</Logger>
+        <!-- 接口日志 -->
+        <RollingFile name="INTERFACE_APPENDER" fileName="./logs/interface/interface.log"
+                     filePattern="./logs/interface/interface-%d{yyyy.MM.dd.HH}-%i.log">
+            <PatternLayout pattern="${logPattern}" charset="UTF-8"/>
+            <Policies>
+                <TimeBasedTriggeringPolicy interval="1" modulate="false"/>
+                <SizeBasedTriggeringPolicy size="100 MB"/>
+            </Policies>
+            <DefaultRolloverStrategy max="1000">
+                <Delete basePath="./logs/interface" maxDepth="1">
+                    <IfFileName glob="interface-*.log">
+                        <IfAccumulatedFileSize exceeds="10 GB"/>
+                    </IfFileName>
+                </Delete>
+            </DefaultRolloverStrategy>
+        </RollingFile>
 
-		<Logger name="CLEAN_EXAM_RECORD_TASK_LOGGER" level="DEBUG" additivity="false">
-			<AppenderRef ref="CLEAN_EXAM_RECORD_TASK_APPENDER" />
-			<AppenderRef ref="Console" />
-		</Logger>
+        <!-- 清理考试记录自动服务的日志 -->
+        <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="${logPattern}" charset="UTF-8"/>
+            <Policies>
+                <TimeBasedTriggeringPolicy interval="1" modulate="false"/>
+                <SizeBasedTriggeringPolicy size="100MB"/>
+            </Policies>
+            <DefaultRolloverStrategy max="1000">
+                <Delete basePath="./logs/clean" maxDepth="1">
+                    <IfFileName glob="ert-*.log">
+                        <IfAccumulatedFileSize exceeds="2 GB"/>
+                    </IfFileName>
+                </Delete>
+            </DefaultRolloverStrategy>
+        </RollingFile>
+    </Appenders>
 
-		<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}" />
+    <Loggers>
+        <logger name="springfox.documentation" level="ERROR"/>
+        <logger name="org.springframework" level="ERROR"/>
+        <logger name="org.hibernate" level="ERROR"/>
+        <logger name="org.apache" level="ERROR"/>
+        <logger name="org.quartz" level="ERROR"/>
+        <logger name="org.docx4j" level="ERROR"/>
+        <logger name="cn.afterturn" level="ERROR"/>
+        <logger name="com.netflix" level="ERROR"/>
+        <logger name="com.aliyun" level="ERROR"/>
+        <logger name="io.lettuce" level="ERROR"/>
+        <logger name="io.netty" level="ERROR"/>
 
-		<Root level="INFO">
-			<AppenderRef ref="Console" />
-			<AppenderRef ref="DEBUG_APPENDER" />
-		</Root>
-	</Loggers>
+        <!--<logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG"/>-->
+        <!--<logger name="org.springframework.data.mongodb" level="DEBUG"/>-->
+        <!--<logger name="org.springframework.data.redis" level="DEBUG"/>-->
 
-</Configuration>
+        <Logger name="cn.com.qmth" level="${commonLevel}" additivity="false">
+            <AppenderRef ref="DEBUG_APPENDER"/>
+            <AppenderRef ref="Console"/>
+        </Logger>
+
+        <Logger name="INTERFACE_LOGGER" level="${commonLevel}" additivity="false">
+            <AppenderRef ref="INTERFACE_APPENDER"/>
+            <AppenderRef ref="Console"/>
+        </Logger>
+
+        <Logger name="CLEAN_EXAM_RECORD_TASK_LOGGER" level="${commonLevel}" additivity="false">
+            <AppenderRef ref="CLEAN_EXAM_RECORD_TASK_APPENDER"/>
+            <AppenderRef ref="Console"/>
+        </Logger>
+
+        <Root level="${commonLevel}">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="DEBUG_APPENDER"/>
+        </Root>
+    </Loggers>
+
+</Configuration>

+ 0 - 18
jenkins-dev.sh

@@ -1,18 +0,0 @@
-#!/bin/bash
-pwd
-
-rm -rf ~/project/examcloud/examcloud-core-oe-student-distribution.zip
-rm -rf ~/project/examcloud/examcloud-core-oe-student/lib/
-
-cp examcloud-core-oe-student-starter/target/examcloud-core-oe-student-distribution.zip ~/project/examcloud/
-
-cd  ~/project/examcloud/
-unzip -o examcloud-core-oe-student-distribution.zip
-
-cd examcloud-core-oe-student
-echo "--spring.profiles.active=dev --examcloud.startup.configCenterHost=localhost" > start.args
-echo "-server -Xms512m -Xmx512m  -XX:-UseGCOverheadLimit" > start.vmoptions
-
-bash stop.sh
-BUILD_ID=DONTKILLME
-bash start.sh 100

+ 0 - 18
jenkins-test.sh

@@ -1,18 +0,0 @@
-#!/bin/bash
-pwd
-
-rm -rf ~/project/examcloud/examcloud-core-oe-student-distribution.zip
-rm -rf ~/project/examcloud/examcloud-core-oe-student/lib/
-
-cp examcloud-core-oe-student-starter/target/examcloud-core-oe-student-distribution.zip ~/project/examcloud/
-
-cd  ~/project/examcloud/
-unzip -o examcloud-core-oe-student-distribution.zip
-
-cd examcloud-core-oe-student
-echo "--spring.profiles.active=test --examcloud.startup.configCenterHost=localhost" > start.args
-echo "-server -Xms512m -Xmx512m  -XX:-UseGCOverheadLimit" > start.vmoptions
-
-bash stop.sh
-BUILD_ID=DONTKILLME
-bash start.sh 100

部分文件因文件數量過多而無法顯示