Kaynağa Gözat

添加考试过程记录相关功能

lideyin 4 yıl önce
ebeveyn
işleme
b350bf028c
15 değiştirilmiş dosya ile 555 ekleme ve 49 silme
  1. 9 8
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/ExamControlController.java
  2. 4 4
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/FaceBiopsyController.java
  3. 1 1
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/provider/ExamRecordDataCloudServiceProvider.java
  4. 22 0
      examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/ExamProcessRecordRepo.java
  5. 78 0
      examcloud-core-oe-student-dao/src/main/java/cn/com/qmth/examcloud/core/oe/student/dao/entity/ExamProcessRecordEntity.java
  6. 82 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/ExamProcessRecordInfo.java
  7. 68 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/report/ExamProcessRecordReport.java
  8. 64 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/report/RocketMqConsumerListener.java
  9. 6 3
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamBossService.java
  10. 7 4
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamControlService.java
  11. 34 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamProcessRecordService.java
  12. 66 28
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamControlServiceImpl.java
  13. 1 1
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamFaceLivenessVerifyServiceImpl.java
  14. 76 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamProcessRecordServiceImpl.java
  15. 37 0
      examcloud-core-oe-student-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/starter/config/ExamProcessRecordTask.java

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

@@ -36,6 +36,7 @@ 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;
@@ -70,7 +71,7 @@ public class ExamControlController extends ControllerSupport {
      */
     @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;
@@ -78,7 +79,7 @@ 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;
     }
 
@@ -103,14 +104,14 @@ public class ExamControlController extends ControllerSupport {
      */
     @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;
@@ -132,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));
     }
 
     /**
@@ -142,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();
@@ -161,7 +162,7 @@ public class ExamControlController extends ControllerSupport {
         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");
         }

+ 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();
     }

+ 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/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;
+    }
+}

+ 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;
+    }
+}

+ 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);

+ 7 - 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,7 +21,7 @@ public interface ExamControlService {
      * @param examStudentId
      * @param user
      */
-    StartExamInfo startExam(Long examStudentId, User user);
+    StartExamInfo startExam(Long examStudentId, User user, String ip);
 
     /**
      * 开始答题
@@ -33,22 +35,23 @@ public interface ExamControlService {
      *
      * @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);
 
     /**
      * 获取考试结束后的相关信息

+ 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);
+
+}

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

@@ -32,6 +32,7 @@ import cn.com.qmth.examcloud.core.oe.student.dao.ExamContinuedRecordRepo;
 import cn.com.qmth.examcloud.core.oe.student.dao.ExamRecordDataRepo;
 import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamContinuedRecordEntity;
 import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.student.report.ExamProcessRecordReport;
 import cn.com.qmth.examcloud.core.oe.student.service.*;
 import cn.com.qmth.examcloud.core.oe.task.api.ExamCaptureCloudService;
 import cn.com.qmth.examcloud.core.oe.task.api.request.SaveExamCaptureSyncCompareResultReq;
@@ -51,10 +52,7 @@ import cn.com.qmth.examcloud.reports.commons.util.ReportsUtil;
 import cn.com.qmth.examcloud.support.Constants;
 import cn.com.qmth.examcloud.support.cache.CacheHelper;
 import cn.com.qmth.examcloud.support.cache.bean.*;
-import cn.com.qmth.examcloud.support.enums.ExamProperties;
-import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
-import cn.com.qmth.examcloud.support.enums.FaceBiopsyScheme;
-import cn.com.qmth.examcloud.support.enums.HandInExamType;
+import cn.com.qmth.examcloud.support.enums.*;
 import cn.com.qmth.examcloud.support.examing.*;
 import cn.com.qmth.examcloud.support.helper.ExamCacheTransferHelper;
 import cn.com.qmth.examcloud.support.helper.FaceBiopsyHelper;
@@ -161,7 +159,7 @@ public class ExamControlServiceImpl implements ExamControlService {
 
     @Transactional
     @Override
-    public StartExamInfo startExam(Long examStudentId, User user) {
+    public StartExamInfo startExam(Long examStudentId, User user, String ip) {
         // 开考预处理
         prepare4Exam(examStudentId, user);
 
@@ -304,7 +302,7 @@ public class ExamControlServiceImpl implements ExamControlService {
         }
 
         //设置并保存上次活动时间
-        setAndSaveActiveTime(examRecordData.getId());
+        setAndSaveActiveTime(examRecordData.getId(), ip);
 
         if (log.isDebugEnabled()) {
             log.debug("10 合计 耗时:" + (System.currentTimeMillis() - st) + " ms");
@@ -312,6 +310,11 @@ public class ExamControlServiceImpl implements ExamControlService {
         // 在线考生开考打点
         ReportsUtil.report(
                 new OnlineExamStudentReport(user.getRootOrgId(), user.getUserId(), examBean.getId(), examStudentId));
+        //考试过程记录(开考)打点
+        ReportsUtil.report(
+                new ExamProcessRecordReport(examRecordData.getId(), ExamProcess.START, examRecordData.getEnterExamTime())
+        );
+
         StartExamInfo startExamInfo = buildStartExamInfo(examRecordData.getId(), examingSession, examBean, courseBean);
         return startExamInfo;
 
@@ -615,7 +618,7 @@ public class ExamControlServiceImpl implements ExamControlService {
      */
     @Transactional
     @Override
-    public void handInExam(Long examRecordDataId, HandInExamType handInExamType) {
+    public void handInExam(Long examRecordDataId, HandInExamType handInExamType, String ip) {
         // 此锁是为了避免自动交卷服务与断点续考交卷或活检失败交卷,同一时刻交卷争抢资源导致死锁
         String sequenceLockKey = Constants.HAND_IN_EXAM_LOCK_PREFIX + examRecordDataId;
         // 系统在请求结束后会,自动释放锁,无需手动解锁
@@ -649,6 +652,8 @@ public class ExamControlServiceImpl implements ExamControlService {
                     log.error("[HAND_IN_EXAM-" + examRecordDataId + "]更新照片优先级时,出现异常", e);
                 }
             }
+
+            setAndSaveActiveTime(examRecordDataId, ip);
         } else if (handInExamType == HandInExamType.AUTO) {
             examRecordData.setExamRecordStatus(ExamRecordStatus.EXAM_AUTO_HAND_IN);
             examRecordData.setCleanTime(new Date());
@@ -657,15 +662,7 @@ public class ExamControlServiceImpl implements ExamControlService {
         }
 
         //交卷时,落地最近的上次活动时间字段
-        String examingActiveTimeKey = RedisKeyHelper.getBuilder()
-                .examingActiveTimeKey(examRecordDataId);
-        ExamingActivityTime examingActiveTime = redisClient.get(examingActiveTimeKey,
-                ExamingActivityTime.class);
-
-        long activeTime = null == examingActiveTime
-                ? System.currentTimeMillis()
-                : examingActiveTime.getActiveTime();
-        examRecordData.setLastActiveTime(new Date(activeTime));
+        examRecordData.setLastActiveTime(new Date(getExamingActivityTime(examRecordDataId).getActiveTime()));
 
         //特殊处理:如果考试类型为 在线练习,则需要将部分数据提前入库,并更新相关状态
         if (ExamType.PRACTICE == examRecordData.getExamType()) {
@@ -698,6 +695,11 @@ public class ExamControlServiceImpl implements ExamControlService {
 
         // 删除redis会话
         examingSessionService.deleteExamingSession(studentId);
+
+        //考试过程记录(交卷)打点
+        ReportsUtil.report(new ExamProcessRecordReport(examRecordDataId,
+                (HandInExamType.MANUAL == handInExamType ? ExamProcess.MANUAL_HAND_IN : ExamProcess.AUTO_HAND_IN),
+                new Date()));
     }
 
     @Override
@@ -1430,7 +1432,7 @@ public class ExamControlServiceImpl implements ExamControlService {
     }
 
     @Override
-    public CheckExamInProgressInfo checkExamInProgress(Long studentId) {
+    public CheckExamInProgressInfo checkExamInProgress(Long studentId, String ip) {
         ExamingSession examSessionInfo = examingSessionService.getExamingSession(studentId);
         if (examSessionInfo == null || ExamingStatus.INFORMAL.equals(examSessionInfo.getExamingStatus())) {
             return null;
@@ -1495,6 +1497,17 @@ public class ExamControlServiceImpl implements ExamControlService {
             }
 
             checkExamInProgressInfo.setFaceVerifyMinute(faceVerifyMinute);
+
+            //考试过程记录(断点)打点
+            ExamingActivityTime lastExamingActivityTime = getExamingActivityTime(examingRecord.getId());
+            ReportsUtil.report(
+                    new ExamProcessRecordReport(examingRecord.getId(), ExamProcess.BREAK_OFF, new Date(lastExamingActivityTime.getActiveTime()))
+            );
+            //考试过程记录(断点续考)打点
+            ReportsUtil.report(new ExamProcessRecordReport(examingRecord.getId(), ExamProcess.CONTINUE, new Date()));
+
+            setAndSaveActiveTime(examingRecord.getId(), ip);
+
             return checkExamInProgressInfo;
         }
     }
@@ -1533,14 +1546,9 @@ public class ExamControlServiceImpl implements ExamControlService {
             return null;
         }
 
-        String examingActiveTimeKey = RedisKeyHelper.getBuilder()
-                .examingActiveTimeKey(examSessionInfo.getExamRecordDataId());
-        ExamingActivityTime examingActiveTime = redisClient.get(examingActiveTimeKey,
-                ExamingActivityTime.class);
+        ExamingActivityTime examingActiveTime = getExamingActivityTime(examSessionInfo.getExamRecordDataId());
 
-        long activeTime = null == examingActiveTime
-                ? System.currentTimeMillis()
-                : examingActiveTime.getActiveTime();
+        long activeTime = examingActiveTime.getActiveTime();
 
         // 如果已经过了断点续考时间,自动交卷
         long now = System.currentTimeMillis();
@@ -1559,7 +1567,7 @@ public class ExamControlServiceImpl implements ExamControlService {
      */
     private void delayHandInExamIfLocked(Long examRecordDataId) {
         try {
-            handInExam(examRecordDataId, HandInExamType.AUTO);
+            handInExam(examRecordDataId, HandInExamType.AUTO, null);
         } catch (SequenceLockException e) {
             // 如果发现自动服务正在交卷,则重试1500毫秒获取考试记录状态,判断是否已交卷
             int loopTimes = 0;
@@ -1603,7 +1611,7 @@ public class ExamControlServiceImpl implements ExamControlService {
      * @param user 学生
      */
     @Override
-    public long examHeartbeat(User user) {
+    public long examHeartbeat(User user, String ip) {
         Long studentId = user.getUserId();
         ExamingSession examSessionInfo = examingSessionService.getExamingSession(studentId);
         if (examSessionInfo == null
@@ -1662,7 +1670,7 @@ public class ExamControlServiceImpl implements ExamControlService {
         examingHeartbeat.setCost(usedExamSeconds);
         redisClient.set(examingHeartbeatKey, examingHeartbeat);//更新心跳缓存
 
-        setAndSaveActiveTime(examSessionInfo.getExamRecordDataId());
+        setAndSaveActiveTime(examSessionInfo.getExamRecordDataId(), ip);
 
         // 在线考生心跳打点
         ReportsUtil.report(new OnlineExamStudentReport(user.getRootOrgId(), user.getUserId(),
@@ -1677,7 +1685,7 @@ public class ExamControlServiceImpl implements ExamControlService {
      *
      * @param examRecrodDataId 考试记录id
      */
-    private void setAndSaveActiveTime(Long examRecrodDataId) {
+    private void setAndSaveActiveTime(Long examRecrodDataId, String ip) {
         String examingActiveTimeKey = RedisKeyHelper.getBuilder()
                 .examingActiveTimeKey(examRecrodDataId);
         ExamingActivityTime examingActiveTime = redisClient.get(examingActiveTimeKey,
@@ -1686,11 +1694,41 @@ public class ExamControlServiceImpl implements ExamControlService {
             examingActiveTime = new ExamingActivityTime();
             examingActiveTime.setExamRecordDataId(examRecrodDataId);
         }
+
+        Long now = System.currentTimeMillis();
+
+        //ip如果发生变更,则记录ip变更记录
+        String lastIp = examingActiveTime.getRealIp();
+        if (StringUtils.isNotEmpty(ip) && !ip.equals(lastIp)) {
+            ReportsUtil.report(
+                    new ExamProcessRecordReport(examRecrodDataId, ExamProcess.IP_CHANGE, new Date(now))
+            );
+        }
+
         examingActiveTime.setActiveTime(System.currentTimeMillis());
+        examingActiveTime.setRealIp(ip);
 
         redisClient.set(examingActiveTimeKey, examingActiveTime);
     }
 
+    /**
+     * 获取上次活动缓存
+     *
+     * @param examRecrodDataId
+     * @return
+     */
+    private ExamingActivityTime getExamingActivityTime(Long examRecrodDataId) {
+        String examingActiveTimeKey = RedisKeyHelper.getBuilder()
+                .examingActiveTimeKey(examRecrodDataId);
+        ExamingActivityTime examingActiveTime = redisClient.get(examingActiveTimeKey, ExamingActivityTime.class);
+        if (null == examingActiveTime) {
+            examingActiveTime = new ExamingActivityTime();
+            examingActiveTime.setExamRecordDataId(examRecrodDataId);
+        }
+
+        return examingActiveTime;
+    }
+
     /**
      * 计算考试时长 校验是否达到冻结时间
      *

+ 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;
+	}
+}

+ 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();
+    }
+}