فهرست منبع

考生端websocket

wangliang 4 سال پیش
والد
کامیت
0725fd2471

+ 8 - 0
themis-backend/src/main/resources/application.properties

@@ -202,6 +202,14 @@ mq.config.taskConsumerRoomCodeExportGroup=${mq.config.taskConsumerGroup}-${mq.co
 mq.config.taskTopicExamPaperImportTag=examPaperImport
 mq.config.taskConsumerExamPaperImportGroup=${mq.config.taskConsumerGroup}-${mq.config.taskTopicExamPaperImportTag}
 
+#websocket\u8D85\u65F6\u9000\u51FA\u76D1\u542C
+mq.config.websocketUnNormalTopic=${mq.config.server}-topic-websocketUnNormal
+mq.config.websocketUnNormalConsumerGroup=${mq.config.server}-group-websocketUnNormal
+
+#oe\u8003\u751F\u7AEF
+mq.config.websocketUnNormalTopicOeTag=oe
+mq.config.websocketUnNormalConsumerOeGroup=${mq.config.websocketUnNormalConsumerGroup}-${mq.config.websocketUnNormalTopicOeTag}
+
 #dlq\u6B7B\u4FE1\u961F\u5217
 #mq.config.sessionConsumerGroupDlq=${mq.config.sessionConsumerGroup}-dlq
 #mq.config.sessionTopicDlq=%DLQ%${mq.config.sessionConsumerGroup}

+ 7 - 0
themis-business/src/main/java/com/qmth/themis/business/dao/TEExamMapper.java

@@ -41,4 +41,11 @@ public interface TEExamMapper extends BaseMapper<TEExam> {
      * @return
      */
     public List<Map> getWaitingExam(@Param("studentId") Long studentId, @Param("examId") Long examId, @Param("orgId") Long orgId);
+
+    /**
+     * 获取考试待考列表
+     *
+     * @return
+     */
+    public List<Map> getWaitingExamForJob();
 }

+ 37 - 10
themis-business/src/main/java/com/qmth/themis/business/domain/QuartzConfigDomain.java

@@ -11,22 +11,49 @@ import java.io.Serializable;
  */
 public class QuartzConfigDomain implements Serializable {
 
-    private String backendJobName;
-    private String backendJobGroupName;
+    private String mqJobName;
+    private String mqJobGroupName;
+    private String examJobName;
+    private String examJobGroupName;
+    private String examActivityJobGroupName;
 
-    public String getBackendJobName() {
-        return backendJobName;
+    public String getMqJobName() {
+        return mqJobName;
     }
 
-    public void setBackendJobName(String backendJobName) {
-        this.backendJobName = backendJobName;
+    public void setMqJobName(String mqJobName) {
+        this.mqJobName = mqJobName;
     }
 
-    public String getBackendJobGroupName() {
-        return backendJobGroupName;
+    public String getMqJobGroupName() {
+        return mqJobGroupName;
     }
 
-    public void setBackendJobGroupName(String backendJobGroupName) {
-        this.backendJobGroupName = backendJobGroupName;
+    public void setMqJobGroupName(String mqJobGroupName) {
+        this.mqJobGroupName = mqJobGroupName;
+    }
+
+    public String getExamJobName() {
+        return examJobName;
+    }
+
+    public void setExamJobName(String examJobName) {
+        this.examJobName = examJobName;
+    }
+
+    public String getExamJobGroupName() {
+        return examJobGroupName;
+    }
+
+    public void setExamJobGroupName(String examJobGroupName) {
+        this.examJobGroupName = examJobGroupName;
+    }
+
+    public String getExamActivityJobGroupName() {
+        return examActivityJobGroupName;
+    }
+
+    public void setExamActivityJobGroupName(String examActivityJobGroupName) {
+        this.examActivityJobGroupName = examActivityJobGroupName;
     }
 }

+ 16 - 0
themis-business/src/main/java/com/qmth/themis/business/enums/FinishExamTypeEnum.java

@@ -0,0 +1,16 @@
+package com.qmth.themis.business.enums;
+
+/**
+* @Description: 结束考试 enum
+* @Param:
+* @return:
+* @Author: wangliang
+* @Date: 2020/7/27
+*/
+public enum FinishExamTypeEnum {
+
+    /**
+     * 强制交卷
+     */
+    must_finish;
+}

+ 20 - 11
themis-business/src/main/java/com/qmth/themis/business/service/TEExamService.java

@@ -1,14 +1,14 @@
 package com.qmth.themis.business.service;
 
-import java.util.List;
-import java.util.Map;
-
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.qmth.themis.business.bean.exam.ExamPrepareBean;
 import com.qmth.themis.business.bean.exam.ExamStartBean;
 import com.qmth.themis.business.entity.TEExam;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * @Description: 考试批次 服务类
  * @Param:
@@ -35,18 +35,27 @@ public interface TEExamService extends IService<TEExam> {
      * 获取考试待考列表
      *
      * @param studentId
-	 * @param examId
-	 * @param orgId
+     * @param examId
+     * @param orgId
      * @return
      */
     public List<Map> getWaitingExam(Long studentId, Long examId, Long orgId);
 
-	/** 开始候考
-	 * @param studentId
-	 * @param activityId
-	 * @return
-	 */
-	public ExamPrepareBean prepare(Long studentId, Long examStudentId);
+    /**
+     * 获取考试待考列表
+     *
+     * @return
+     */
+    public List<Map> getWaitingExamForJob();
+
+    /**
+     * 开始候考
+     *
+     * @param studentId
+     * @param activityId
+     * @return
+     */
+    public ExamPrepareBean prepare(Long studentId, Long examStudentId);
 	
 	/**开始考试
 	 * @param id

+ 259 - 249
themis-business/src/main/java/com/qmth/themis/business/service/impl/TEExamServiceImpl.java

@@ -48,255 +48,265 @@ import com.qmth.themis.common.util.FileUtil;
 @Service
 public class TEExamServiceImpl extends ServiceImpl<TEExamMapper, TEExam> implements TEExamService {
     private SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
-	@Resource
-	TEExamMapper teExamMapper;
-
-	@Resource
-	TEExamActivityService teExamActivityService;
-
-	@Resource
-	TEExamStudentService teExamStudentService;
-
-	@Resource
-	TEExamCourseService teExamCourseService;
-
-	@Resource
-	TEExamPaperService teExamPaperService;
-
-	@Resource
-	TOeExamRecordService toeExamRecordService;
-
-	@Resource
-	RedisUtil redisUtil;
-
-	@Resource
-	SystemConfig systemConfig;
-
-	/**
-	 * 查询考试批次
-	 *
-	 * @param iPage
-	 * @param id
-	 * @param code
-	 * @param name
-	 * @param mode
-	 * @param enable
-	 * @return
-	 */
-	@Override
-	public IPage<TEExam> examQuery(IPage<Map> iPage, Long id, String code, String name, Integer mode, Integer enable) {
-		return teExamMapper.examQuery(iPage, id, code, name, mode, enable);
-	}
-
-	/**
-	 * 获取考试待考列表
-	 *
-	 * @param studentId
-	 * @param examId
-	 * @param orgId
-	 * @return
-	 */
-	@Override
-	public List<Map> getWaitingExam(Long studentId, Long examId, Long orgId) {
-		List<Map> list = teExamMapper.getWaitingExam(studentId, examId, orgId);
-		if (Objects.nonNull(list) && list.size() > 0) {
-			for (int i = 0; i < list.size(); i++) {
-				Map m = list.get(i);
-				List<Map> teExamActivityList = teExamActivityService.getWaitingExam(studentId,
-						Long.parseLong(String.valueOf(m.get("id"))),
-						Long.parseLong(String.valueOf(m.get("examActivityId"))));
-				m.put("activities", teExamActivityList);
-			}
-		}
-		return list;
-	}
-
-	@Transactional
-	@Override
-	public ExamPrepareBean prepare(Long studentId, Long examStudentId) {
-		ExamPrepareBean ret = null;
-		ExamStudentCacheBean es = null;
-		es = (ExamStudentCacheBean) redisUtil.get(RedisKeyHelper.examStudentCacheKey(examStudentId));
-		if (es == null) {
-			es = teExamStudentService.getExamStudnetCacheBean(examStudentId);
-		}
-		if (es == null) {
-			throw new BusinessException("未找到考试");
-		}
-
-		if (studentId.equals(es.getStudentId())) {
-			throw new BusinessException("考生Id和当前登录用户不一致");
-		}
-		if (es.getLeftExamCount() == 0) {
-			throw new BusinessException("没有剩余考试次数");
-		}
-		Long activityId = es.getExamActivityId();
-		ExamActivityCacheBean ac = teExamActivityService.getExamActivityCacheBean(activityId);
-		if (ac == null) {
-			throw new BusinessException("未找到场次");
-		}
-		Date now = new Date();
-		Long start = ac.getStartTime().getTime() - (ac.getPrepareSeconds() * 1000);
-		Long end = ac.getStartTime().getTime() + (ac.getOpeningSeconds() * 1000);
-		if (now.getTime() < start) {
-			throw new BusinessException("没有到允许开考的时间");
-		}
-		if (now.getTime() > end) {
-			throw new BusinessException("允许开考的时间已结束");
-		}
-		ExamCourseCacheBean ec = teExamCourseService.getExamCourseCacheBean(es.getExamId(), es.getCourseCode());
-		if (ec == null) {
-			throw new BusinessException("未找到考试科目");
-		}
-		if (ec.getPaperIds() == null) {
-			throw new BusinessException("考试科目未绑定试卷");
-		}
-		if (ec.getHasAnswer() == null || ec.getHasAnswer().intValue() == 0) {
-			throw new BusinessException("考试科目答案未补全");
-		}
-		// 根据权重选中试卷
-		Long paperId = ec.getPaperIds().get(getPaperByWeight(ec.getPaperWeight()));
-		ExamPaperCacheBean ep = teExamPaperService.getExamPaperCacheBean(paperId);
-		if (ep == null) {
-			throw new BusinessException("未找到试卷:" + paperId);
-		}
-		if (StringUtils.isBlank(ep.getAnswerPath())) {
-			throw new BusinessException("试卷答案未上传:" + paperId);
-		}
-
-		// 写入次数
-		Integer serialNumber = es.getCurrentSerialNumber();
-		if (serialNumber == null) {
-			serialNumber = 0;
-		}
-		es.setCurrentSerialNumber(serialNumber + 1);
-
-		Long recordId = toeExamRecordService.saveByPrepare(es.getExamId(), es.getExamActivityId(), examStudentId,
-				paperId, es.getCurrentSerialNumber());
-
-		Integer leftExamCount = es.getLeftExamCount();
-		es.setLeftExamCount(leftExamCount - 1);
-
-		es.setCurrentRecordId(recordId);
-
-		ExamPrepareBean prepare = new ExamPrepareBean();
-		prepare.setRecordId(recordId);
-		prepare.setAudioPlayTime(ep.getAudioPlayTime());
-		prepare.setHasAudio((ep.getHasAudio() == null || ep.getHasAudio().intValue() == 0 ? false : true));
-		prepare.setObjectiveShuffle(
-				(ec.getObjectiveShuffle() == null || ec.getObjectiveShuffle().intValue() == 0 ? false : true));
-		prepare.setOptionShuffle(
-				(ec.getOptionShuffle() == null || ec.getOptionShuffle().intValue() == 0 ? false : true));
-		String paperurl = OssUtil.getUrlForPrivateBucket(systemConfig.getOssEnv(3), ep.getPaperPath());
-		String structurl = OssUtil.getUrlForPrivateBucket(systemConfig.getOssEnv(3), ep.getStructPath());
-		prepare.setPaperUrl(paperurl);
-		prepare.setStructUrl(structurl);
-
-		// 更新考生缓存
-		redisUtil.set(RedisKeyHelper.examStudentCacheKey(examStudentId), es);
-
-		return ret;
-	}
-
-	/**
-	 * 根据设定几率取出一套试卷
-	 */
-	private int getPaperByWeight(List<Double> paperWeight) {
-		// 从1开始
-		double r = Math.random();
-		for (int i = 0; i < paperWeight.size(); i++) {
-			r -= paperWeight.get(i);
-			if (r <= 0.0d) {
-				return i;// 选中
-			}
-		}
-
-		return -1;
-	}
-
-	@Transactional
-	@Override
-	public ExamStartBean start(Long studentId, Long recordId) {
-		ExamStartBean ret = null;
-		ExamRecordCacheBean er = (ExamRecordCacheBean) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId));
-		if (er == null) {
-			throw new BusinessException("未找到考试记录");
-		}
-		ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
-				.get(RedisKeyHelper.examStudentCacheKey(er.getExamStudentId()));
-		if (es == null) {
-			throw new BusinessException("未找到考生");
-		}
-		if (studentId.equals(es.getStudentId())) {
-			throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
-		}
-		Long activityId = es.getExamActivityId();
-		ExamActivityCacheBean ac = teExamActivityService.getExamActivityCacheBean(activityId);
-		if (ac == null) {
-			throw new BusinessException("未找到场次");
-		}
-		Date now = new Date();
-		Long start = ac.getStartTime().getTime();
-		Long end = ac.getStartTime().getTime() + (ac.getOpeningSeconds() * 1000);
-		if (now.getTime() < start) {
-			throw new BusinessException("没有到允许开考的时间");
-		}
-		if (now.getTime() > end) {
-			throw new BusinessException("允许开考的时间已结束");
-		}
-
-		// 修改考试记录数据
-		er.setFirstStartTime(new Date());
-		er.setStatus(2);
-
-		ExamPaperCacheBean ep = teExamPaperService.getExamPaperCacheBean(er.getPaperId());
-		if (ep == null) {
-			throw new BusinessException("未找到试卷");
-		}
-
-		ret = new ExamStartBean();
-		ret.setPaperDecryptSecret(ep.getDecryptSecret());
-		ret.setPaperDecryptVector(ep.getDecryptVector());
-
-		// 更新考试记录缓存
-		redisUtil.set(RedisKeyHelper.examRecordCacheKey(recordId), er);
-
-		return ret;
-	}
-
-	
-	@Transactional
-	@Override
-	public Long studentPaperStruct(Long studentId, Long recordId,String content) {
-		ExamRecordCacheBean er = (ExamRecordCacheBean) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId));
-		if (er == null) {
-			throw new BusinessException("未找到考试记录");
-		}
-		ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
-				.get(RedisKeyHelper.examStudentCacheKey(er.getExamStudentId()));
-		if (es == null) {
-			throw new BusinessException("未找到考生");
-		}
-		if (studentId.equals(es.getStudentId())) {
-			throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
-		}
-
-		String tempDir = systemConfig.getProperty("sys.config.tempDataDir");
-		File file=new File(tempDir+"/"+ uuid() + ".json");
-		try {
-			FileUtil.saveAsFile(file.getAbsolutePath(), content);
-			String filePath = "upload/" + sdf.format(new Date()) + "/" + uuid() + ".json";
-			er.setPaperStruct(filePath);
-			er.setPaperStructUpload(1);
-			OssUtil.ossUpload(systemConfig.getOssEnv(3), filePath, file);
-			// 更新考试记录缓存
-			redisUtil.set(RedisKeyHelper.examRecordCacheKey(recordId), er);
-			return new Date().getTime();
-		} finally {
-			FileUtil.deleteFile(file);
-		}
-	}
-	
+    @Resource
+    TEExamMapper teExamMapper;
+
+    @Resource
+    TEExamActivityService teExamActivityService;
+
+    @Resource
+    TEExamStudentService teExamStudentService;
+
+    @Resource
+    TEExamCourseService teExamCourseService;
+
+    @Resource
+    TEExamPaperService teExamPaperService;
+
+    @Resource
+    TOeExamRecordService toeExamRecordService;
+
+    @Resource
+    RedisUtil redisUtil;
+
+    @Resource
+    SystemConfig systemConfig;
+
+    /**
+     * 查询考试批次
+     *
+     * @param iPage
+     * @param id
+     * @param code
+     * @param name
+     * @param mode
+     * @param enable
+     * @return
+     */
+    @Override
+    public IPage<TEExam> examQuery(IPage<Map> iPage, Long id, String code, String name, Integer mode, Integer enable) {
+        return teExamMapper.examQuery(iPage, id, code, name, mode, enable);
+    }
+
+    /**
+     * 获取考试待考列表
+     *
+     * @param studentId
+     * @param examId
+     * @param orgId
+     * @return
+     */
+    @Override
+    public List<Map> getWaitingExam(Long studentId, Long examId, Long orgId) {
+        List<Map> list = teExamMapper.getWaitingExam(studentId, examId, orgId);
+        if (Objects.nonNull(list) && list.size() > 0) {
+            for (int i = 0; i < list.size(); i++) {
+                Map m = list.get(i);
+                List<Map> teExamActivityList = teExamActivityService.getWaitingExam(studentId,
+                        Long.parseLong(String.valueOf(m.get("id"))),
+                        Long.parseLong(String.valueOf(m.get("examActivityId"))));
+                m.put("activities", teExamActivityList);
+            }
+        }
+        return list;
+    }
+
+    /**
+     * 获取考试待考列表
+     *
+     * @return
+     */
+    @Override
+    public List<Map> getWaitingExamForJob() {
+        return teExamMapper.getWaitingExamForJob();
+    }
+
+    @Transactional
+    @Override
+    public ExamPrepareBean prepare(Long studentId, Long examStudentId) {
+        ExamPrepareBean ret = null;
+        ExamStudentCacheBean es = null;
+        es = (ExamStudentCacheBean) redisUtil.get(RedisKeyHelper.examStudentCacheKey(examStudentId));
+        if (es == null) {
+            es = teExamStudentService.getExamStudnetCacheBean(examStudentId);
+        }
+        if (es == null) {
+            throw new BusinessException("未找到考试");
+        }
+
+        if (studentId.equals(es.getStudentId())) {
+            throw new BusinessException("考生Id和当前登录用户不一致");
+        }
+        if (es.getLeftExamCount() == 0) {
+            throw new BusinessException("没有剩余考试次数");
+        }
+        Long activityId = es.getExamActivityId();
+        ExamActivityCacheBean ac = teExamActivityService.getExamActivityCacheBean(activityId);
+        if (ac == null) {
+            throw new BusinessException("未找到场次");
+        }
+        Date now = new Date();
+        Long start = ac.getStartTime().getTime() - (ac.getPrepareSeconds() * 1000);
+        Long end = ac.getStartTime().getTime() + (ac.getOpeningSeconds() * 1000);
+        if (now.getTime() < start) {
+            throw new BusinessException("没有到允许开考的时间");
+        }
+        if (now.getTime() > end) {
+            throw new BusinessException("允许开考的时间已结束");
+        }
+        ExamCourseCacheBean ec = teExamCourseService.getExamCourseCacheBean(es.getExamId(), es.getCourseCode());
+        if (ec == null) {
+            throw new BusinessException("未找到考试科目");
+        }
+        if (ec.getPaperIds() == null) {
+            throw new BusinessException("考试科目未绑定试卷");
+        }
+        if (ec.getHasAnswer() == null || ec.getHasAnswer().intValue() == 0) {
+            throw new BusinessException("考试科目答案未补全");
+        }
+        // 根据权重选中试卷
+        Long paperId = ec.getPaperIds().get(getPaperByWeight(ec.getPaperWeight()));
+        ExamPaperCacheBean ep = teExamPaperService.getExamPaperCacheBean(paperId);
+        if (ep == null) {
+            throw new BusinessException("未找到试卷:" + paperId);
+        }
+        if (StringUtils.isBlank(ep.getAnswerPath())) {
+            throw new BusinessException("试卷答案未上传:" + paperId);
+        }
+
+        // 写入次数
+        Integer serialNumber = es.getCurrentSerialNumber();
+        if (serialNumber == null) {
+            serialNumber = 0;
+        }
+        es.setCurrentSerialNumber(serialNumber + 1);
+
+        Long recordId = toeExamRecordService.saveByPrepare(es.getExamId(), es.getExamActivityId(), examStudentId,
+                paperId, es.getCurrentSerialNumber());
+
+        Integer leftExamCount = es.getLeftExamCount();
+        es.setLeftExamCount(leftExamCount - 1);
+
+        es.setCurrentRecordId(recordId);
+
+        ExamPrepareBean prepare = new ExamPrepareBean();
+        prepare.setRecordId(recordId);
+        prepare.setAudioPlayTime(ep.getAudioPlayTime());
+        prepare.setHasAudio((ep.getHasAudio() == null || ep.getHasAudio().intValue() == 0 ? false : true));
+        prepare.setObjectiveShuffle(
+                (ec.getObjectiveShuffle() == null || ec.getObjectiveShuffle().intValue() == 0 ? false : true));
+        prepare.setOptionShuffle(
+                (ec.getOptionShuffle() == null || ec.getOptionShuffle().intValue() == 0 ? false : true));
+        String paperurl = OssUtil.getUrlForPrivateBucket(systemConfig.getOssEnv(3), ep.getPaperPath());
+        String structurl = OssUtil.getUrlForPrivateBucket(systemConfig.getOssEnv(3), ep.getStructPath());
+        prepare.setPaperUrl(paperurl);
+        prepare.setStructUrl(structurl);
+
+        // 更新考生缓存
+        redisUtil.set(RedisKeyHelper.examStudentCacheKey(examStudentId), es);
+
+        return ret;
+    }
+
+    /**
+     * 根据设定几率取出一套试卷
+     */
+    private int getPaperByWeight(List<Double> paperWeight) {
+        // 从1开始
+        double r = Math.random();
+        for (int i = 0; i < paperWeight.size(); i++) {
+            r -= paperWeight.get(i);
+            if (r <= 0.0d) {
+                return i;// 选中
+            }
+        }
+
+        return -1;
+    }
+
+    @Transactional
+    @Override
+    public ExamStartBean start(Long studentId, Long recordId) {
+        ExamStartBean ret = null;
+        ExamRecordCacheBean er = (ExamRecordCacheBean) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId));
+        if (er == null) {
+            throw new BusinessException("未找到考试记录");
+        }
+        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
+                .get(RedisKeyHelper.examStudentCacheKey(er.getExamStudentId()));
+        if (es == null) {
+            throw new BusinessException("未找到考生");
+        }
+        if (studentId.equals(es.getStudentId())) {
+            throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
+        }
+        Long activityId = es.getExamActivityId();
+        ExamActivityCacheBean ac = teExamActivityService.getExamActivityCacheBean(activityId);
+        if (ac == null) {
+            throw new BusinessException("未找到场次");
+        }
+        Date now = new Date();
+        Long start = ac.getStartTime().getTime();
+        Long end = ac.getStartTime().getTime() + (ac.getOpeningSeconds() * 1000);
+        if (now.getTime() < start) {
+            throw new BusinessException("没有到允许开考的时间");
+        }
+        if (now.getTime() > end) {
+            throw new BusinessException("允许开考的时间已结束");
+        }
+
+        // 修改考试记录数据
+        er.setFirstStartTime(new Date());
+        er.setStatus(2);
+
+        ExamPaperCacheBean ep = teExamPaperService.getExamPaperCacheBean(er.getPaperId());
+        if (ep == null) {
+            throw new BusinessException("未找到试卷");
+        }
+
+        ret = new ExamStartBean();
+        ret.setPaperDecryptSecret(ep.getDecryptSecret());
+        ret.setPaperDecryptVector(ep.getDecryptVector());
+
+        // 更新考试记录缓存
+        redisUtil.set(RedisKeyHelper.examRecordCacheKey(recordId), er);
+
+        return ret;
+    }
+
+
+    @Transactional
+    @Override
+    public Long studentPaperStruct(Long studentId, Long recordId, String content) {
+        ExamRecordCacheBean er = (ExamRecordCacheBean) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId));
+        if (er == null) {
+            throw new BusinessException("未找到考试记录");
+        }
+        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
+                .get(RedisKeyHelper.examStudentCacheKey(er.getExamStudentId()));
+        if (es == null) {
+            throw new BusinessException("未找到考生");
+        }
+        if (studentId.equals(es.getStudentId())) {
+            throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
+        }
+
+        String tempDir = systemConfig.getProperty("sys.config.tempDataDir");
+        File file = new File(tempDir + "/" + uuid() + ".json");
+        try {
+            FileUtil.saveAsFile(file.getAbsolutePath(), content);
+            String filePath = "upload/" + sdf.format(new Date()) + "/" + uuid() + ".json";
+            er.setPaperStruct(filePath);
+            er.setPaperStructUpload(1);
+            OssUtil.ossUpload(systemConfig.getOssEnv(3), filePath, file);
+            // 更新考试记录缓存
+            redisUtil.set(RedisKeyHelper.examRecordCacheKey(recordId), er);
+            return new Date().getTime();
+        } finally {
+            FileUtil.deleteFile(file);
+        }
+    }
+
     private String uuid() {
         return UUID.randomUUID().toString().replaceAll("-", "");
     }

+ 2 - 1
themis-business/src/main/resources/mapper/TEExamActivityMapper.xml

@@ -81,6 +81,7 @@
     <select id="getWaitingExam" resultType="java.util.Map">
          select
             teea.id,
+            teea.code,
             tees.id as examStudentId,
             tees.course_code as courseCode,
             teec.course_name as courseName,
@@ -92,7 +93,7 @@
             tee.max_duration_seconds as maxDurationSeconds,
             teea.max_duration_seconds as activityMaxDurationSeconds,
             tee.exam_count as examCount,
-            tee.force_finish as fouceFinish,
+            tee.force_finish as forceFinish,
             tee.entry_authentication_policy as entryAuthenticationPolicy,
             tee.in_process_face_verify as inProcessFaceVerify,
             tee.in_process_face_stranger_ignore as inProcessFaceStrangerIgnore,

+ 55 - 0
themis-business/src/main/resources/mapper/TEExamMapper.xml

@@ -60,4 +60,59 @@
             or t.reallyTime = 0
         </where>
     </select>
+
+    <select id="getWaitingExamForJob" resultType="java.util.Map">
+        select
+            teea.id,
+            teea.code,
+            tee.opening_seconds as openingSeconds,
+            tee.prepare_seconds as prepareSeconds,
+            tee.min_duration_seconds as minDurationSeconds,
+            tee.max_duration_seconds as maxDurationSeconds,
+            tee.exam_count as examCount,
+            tee.force_finish as forceFinish,
+            tee.entry_authentication_policy as entryAuthenticationPolicy,
+            tee.in_process_face_verify as inProcessFaceVerify,
+            tee.in_process_face_stranger_ignore as inProcessFaceStrangerIgnore,
+            tee.in_process_liveness_verify as inProcessLivenessVerify,
+            tee.in_process_liveness_interval_seconds as inProcessLivenessIntervalSeconds,
+            tee.in_process_liveness_judge_policy as inProcessLivenessJudgePolicy,
+            tee.in_process_liveness_retry_count as inProcessLivenessRetryCount,
+            tee.client_video_push as clientVideoPush,
+            tee.wxapp_video_push as wxappVideoPush,
+            tee.camera_photo_upload as cameraPhotoUpload,
+            tee.wxapp_photo_upload as wxappPhotoUpload,
+            tee.wxapp_video_record as wxappVideoRecord,
+            teea.id as activityId,
+            teea.code as activityCode,
+            teea.opening_seconds as activityOpeningSeconds,
+            teea.prepare_seconds as activityPrepareSeconds,
+            teea.max_duration_seconds as activityMaxDurationSeconds,
+            teea.start_time as startTime,
+            teea.finish_time as finishTime
+        from
+            t_e_exam_activity teea
+        left join (
+            select
+                distinct tee.id, teea.id as examActivityId, tee.name, tee.mode, tee.pre_notice as preNotice, tee.pre_notice_stay_seconds as preNoticeStaySeconds, tee.post_notice as postNotice, datediff(now(), teea.start_time) as reallyTime
+            from
+                t_e_exam_student tees
+            left join t_e_exam_course teec on
+                teec.course_code = tees.course_code
+            left join t_e_exam tee on
+                tee.id = tees.exam_id
+            left join t_e_exam_activity teea on
+                teea.id = tees.exam_activity_id
+            WHERE
+                tee.enable = 1
+                and teea.enable = 1
+                and tees.enable = 1
+                and teea.start_time >= date_add(now(), interval IFNULL(teea.opening_seconds, tee.opening_seconds) second) ) t on
+            t.examActivityId = teea.id
+        left join t_e_exam tee on
+            tee.id = t.id
+        WHERE
+            t.reallyTime = -1
+            or t.reallyTime = 0;
+    </select>
 </mapper>

+ 86 - 0
themis-task/src/main/java/com/qmth/themis/task/quartz/ExamActivityJob.java

@@ -0,0 +1,86 @@
+package com.qmth.themis.task.quartz;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.qmth.themis.business.entity.TEExamActivity;
+import com.qmth.themis.business.entity.TEExamStudent;
+import com.qmth.themis.business.entity.TOeExamRecord;
+import com.qmth.themis.business.enums.ExamRecordStatusEnum;
+import com.qmth.themis.business.enums.FinishExamTypeEnum;
+import com.qmth.themis.business.service.TEExamActivityService;
+import com.qmth.themis.business.service.TEExamStudentService;
+import com.qmth.themis.business.service.TOeExamRecordService;
+import com.qmth.themis.business.util.JacksonUtil;
+import org.quartz.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.quartz.QuartzJobBean;
+import org.springframework.transaction.annotation.Transactional;
+import sun.net.idn.StringPrep;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @Description: examActivity job,一次性延时任务
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2020/7/27
+ */
+public class ExamActivityJob extends QuartzJobBean {
+    private final static Logger log = LoggerFactory.getLogger(ExamActivityJob.class);
+
+    @Resource
+    TEExamActivityService teExamActivityService;
+
+    @Resource
+    TEExamStudentService teExamStudentService;
+
+    @Resource
+    TOeExamRecordService tOeExamRecordService;
+
+    @Override
+    @Transactional
+    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
+        log.info("exam_acvitity_job进来了,context:{}", context);
+        JobDetail jobDetail = context.getJobDetail();
+        String key = String.valueOf(jobDetail.getKey());
+        if (Objects.nonNull(key)) {
+            String[] strings = key.split("\\.");
+            QueryWrapper<TEExamActivity> teExamActivityQueryWrapper = new QueryWrapper<>();
+            teExamActivityQueryWrapper.lambda().eq(TEExamActivity::getCode, strings[1]);
+            TEExamActivity teExamActivity = teExamActivityService.getOne(teExamActivityQueryWrapper);
+            log.info("key:{}", key);
+
+            //获取该考试批次下所有未交卷的考生的考试记录
+            QueryWrapper<TOeExamRecord> tOeExamRecordQueryWrapper = new QueryWrapper<>();
+            tOeExamRecordQueryWrapper.lambda().eq(TOeExamRecord::getExamActivityId, teExamActivity.getId()).eq(TOeExamRecord::getStatus, ExamRecordStatusEnum.finished.ordinal());
+            List<TOeExamRecord> tOeExamRecordList = tOeExamRecordService.list(tOeExamRecordQueryWrapper);
+            List<Long> examStudentIdList = new ArrayList<>();
+            tOeExamRecordList.forEach(s -> {
+                s.setStatus(ExamRecordStatusEnum.finished.ordinal());
+                s.setFinishTime(new Date());
+                s.setFinishType(FinishExamTypeEnum.must_finish.ordinal());
+                examStudentIdList.add(s.getExamStudentId());
+            });
+            tOeExamRecordService.updateBatchById(tOeExamRecordList);
+
+            if (examStudentIdList.size() > 0) {
+                //获取该考试批次下所有考生,考试次数减1
+                QueryWrapper<TEExamStudent> teExamStudentQueryWrapper = new QueryWrapper<>();
+                teExamStudentQueryWrapper.lambda().in(TEExamStudent::getId, examStudentIdList);
+                List<TEExamStudent> teExamStudentList = teExamStudentService.list(teExamStudentQueryWrapper);
+                teExamStudentList.forEach(s -> {
+                    int count = s.getLeftExamCount();
+                    count--;
+                    s.setLeftExamCount(count < 0 ? 0 : count);
+                });
+                teExamStudentService.updateBatchById(teExamStudentList);
+            }
+            //todo 未完待续,需要加入交卷逻辑
+        }
+    }
+}

+ 77 - 0
themis-task/src/main/java/com/qmth/themis/task/quartz/ExamJob.java

@@ -0,0 +1,77 @@
+package com.qmth.themis.task.quartz;
+
+import com.qmth.themis.business.service.TEExamService;
+import com.qmth.themis.business.util.JacksonUtil;
+import com.qmth.themis.task.config.DictionaryConfig;
+import com.qmth.themis.task.service.QuartzService;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.quartz.QuartzJobBean;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+/**
+ * @Description: 考试job
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2020/7/27
+ */
+public class ExamJob extends QuartzJobBean {
+    private final static Logger log = LoggerFactory.getLogger(ExamJob.class);
+
+    @Resource
+    TEExamService teExamService;
+
+    @Resource
+    QuartzService quartzService;
+
+    @Resource
+    DictionaryConfig dictionaryConfig;
+
+    @Override
+    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
+        log.info("exam_job进来了,context:{}", context);
+        List<Map> teExamList = teExamService.getWaitingExamForJob();
+        if (Objects.nonNull(teExamList) && teExamList.size() > 0) {
+            for (int i = 0; i < teExamList.size(); i++) {
+                Map map = teExamList.get(i);
+                log.info("map:{}", JacksonUtil.parseJson(map));
+                Integer forceFinish = Integer.parseInt(String.valueOf(map.get("forceFinish")));
+                Date startTime = (Date) map.get("startTime");
+                Date finishTime = (Date) map.get("finishTime");
+                if (Objects.nonNull(forceFinish) && forceFinish.intValue() == 1) {//强制收卷
+                    Long activityMaxDurationSeconds = Objects.nonNull(map.get("activityMaxDurationSeconds")) ? Long.parseLong(String.valueOf(map.get("activityMaxDurationSeconds"))) : null;
+                    Long maxDurationSeconds = Objects.nonNull(map.get("maxDurationSeconds")) ? Long.parseLong(String.valueOf(map.get("maxDurationSeconds"))) : null;
+                    Long finalMaxDurationSeconds = Objects.nonNull(activityMaxDurationSeconds) ? activityMaxDurationSeconds : maxDurationSeconds;
+                    Calendar calendar = Calendar.getInstance();
+                    if (Objects.nonNull(finalMaxDurationSeconds)) {
+                        calendar.setTime(startTime);
+                        calendar.add(Calendar.SECOND, activityMaxDurationSeconds.intValue());
+                    } else {
+                        calendar.setTime(finishTime);
+                    }
+//                    calendar.setTime(new Date());
+                    int year = calendar.get(Calendar.YEAR);//获取年份
+                    int month = calendar.get(Calendar.MONTH) + 1;//获取月份
+                    int day = calendar.get(Calendar.DATE);//获取日
+                    int hour = calendar.get(Calendar.HOUR_OF_DAY);//小时
+                    int minute = calendar.get(Calendar.MINUTE);//分
+                    int second = calendar.get(Calendar.SECOND);//秒
+//                    String cron = (second + 30) + " " + (minute) + " " + hour + " " + day + " " + month + " ? " + year;
+                    String cron = second + " " + (minute + 1) + " " + hour + " " + day + " " + month + " ? " + year;
+                    log.info("cron:{}", cron);
+                    String activityCode = String.valueOf(map.get("activityCode"));
+                    //执行一次性延时任务
+                    Map mapJob = new HashMap();
+                    mapJob.put("name", activityCode);
+                    quartzService.deleteJob(activityCode, dictionaryConfig.quartzConfigDomain().getExamActivityJobGroupName());
+                    quartzService.addJob(ExamActivityJob.class, activityCode, dictionaryConfig.quartzConfigDomain().getExamActivityJobGroupName(), cron, mapJob);
+                }
+            }
+        }
+    }
+}

+ 3 - 5
themis-task/src/main/java/com/qmth/themis/task/quartz/MqJob.java

@@ -2,6 +2,7 @@ package com.qmth.themis.task.quartz;
 
 import com.qmth.themis.business.constant.SystemConstant;
 import com.qmth.themis.business.threadPool.MyThreadPool;
+import com.qmth.themis.business.util.JacksonUtil;
 import com.qmth.themis.business.util.RedisUtil;
 import com.qmth.themis.mq.dto.MqDto;
 import com.qmth.themis.mq.service.ProducerServer;
@@ -37,17 +38,14 @@ public class MqJob extends QuartzJobBean {
     @Override
     protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
         myThreadPool.arbitratePoolTaskExecutor.execute(() -> {
-            log.info("session_topic_job进来了");
+            log.info("mq_job进来了,context:{}", context);
             this.assembleJob(SystemConstant.SESSION_TOPIC_BUFFER_LIST);
-            log.info("userLog_topic_job进来了");
             this.assembleJob(SystemConstant.USERLOG_TOPIC_BUFFER_LIST);
-            log.info("studentLog_topic_job进来了");
             this.assembleJob(SystemConstant.STUDENTLOG_TOPIC_BUFFER_LIST);
-            log.info("task_topic_job进来了");
             this.assembleJob(SystemConstant.TASKLOG_TOPIC_BUFFER_LIST);
         });
         myThreadPool.arbitratePoolTaskExecutor.execute(() -> {
-            log.info("websocket_un_normal_topic_job进来了");
+            log.info("mq_delay_job进来了,context:{}", context);
             this.assembleDelayJob(SystemConstant.WEBSOCKET_UN_NORMAL_LOG_TOPIC_BUFFER_LIST);
         });
     }

+ 13 - 5
themis-task/src/main/java/com/qmth/themis/task/start/StartRunning.java

@@ -2,6 +2,7 @@ package com.qmth.themis.task.start;
 
 import com.qmth.themis.business.util.JacksonUtil;
 import com.qmth.themis.task.config.DictionaryConfig;
+import com.qmth.themis.task.quartz.ExamJob;
 import com.qmth.themis.task.quartz.MqJob;
 import com.qmth.themis.task.service.QuartzService;
 import org.slf4j.Logger;
@@ -35,12 +36,19 @@ public class StartRunning implements CommandLineRunner {
     public void run(String... args) throws Exception {
         log.info("服务器启动时执行 start");
         log.info("增加mqjob start");
-        //如果已经创建了任务,下次并不需要删除和新增
-        Map map = new HashMap();
-        map.put("name", MqJob.class.getName());
-        quartzService.deleteJob(dictionaryConfig.quartzConfigDomain().getBackendJobName(), dictionaryConfig.quartzConfigDomain().getBackendJobGroupName());
-        quartzService.addJob(MqJob.class, dictionaryConfig.quartzConfigDomain().getBackendJobName(), dictionaryConfig.quartzConfigDomain().getBackendJobGroupName(), "0 0/1 * * * ?", map);
+        Map mqMap = new HashMap();
+        mqMap.put("name", MqJob.class.getName());
+        quartzService.deleteJob(dictionaryConfig.quartzConfigDomain().getMqJobName(), dictionaryConfig.quartzConfigDomain().getMqJobGroupName());
+        quartzService.addJob(MqJob.class, dictionaryConfig.quartzConfigDomain().getMqJobName(), dictionaryConfig.quartzConfigDomain().getMqJobGroupName(), "0 0/1 * * * ?", mqMap);
         log.info("增加mqjob end");
+
+        log.info("增加examjob start");
+        Map examMap = new HashMap();
+        examMap.put("name", ExamJob.class.getName());
+        quartzService.deleteJob(dictionaryConfig.quartzConfigDomain().getExamJobName(), dictionaryConfig.quartzConfigDomain().getExamJobGroupName());
+        quartzService.addJob(ExamJob.class, dictionaryConfig.quartzConfigDomain().getExamJobName(), dictionaryConfig.quartzConfigDomain().getExamJobGroupName(), "0 0 0 * * ?", examMap);
+//        quartzService.addJob(ExamJob.class, dictionaryConfig.quartzConfigDomain().getExamJobName(), dictionaryConfig.quartzConfigDomain().getExamJobGroupName(), "0 26 14 27 7 ? 2020", examMap);
+        log.info("增加examjob end");
         log.info("服务器启动时执行 end");
     }
 }

+ 5 - 2
themis-task/src/main/resources/application.properties

@@ -143,8 +143,11 @@ spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThre
 spring.quartz.properties.org.quartz.threadPool.threadCount=10
 # \u8BBE\u7F6E\u7EBF\u7A0B\u7684\u4F18\u5148\u7EA7(\u53EF\u4EE5\u662FThread.MIN_PRIORITY\uFF08\u53731\uFF09\u548CThread.MAX_PRIORITY\uFF08\u8FD9\u662F10\uFF09\u4E4B\u95F4\u7684\u4EFB\u4F55int \u3002\u9ED8\u8BA4\u503C\u4E3AThread.NORM_PRIORITY\uFF085\uFF09\u3002)
 spring.quartz.properties.org.quartz.threadPool.threadPriority=5
-quartz.config.backendJobName=backendJob
-quartz.config.backendJobGroupName=backendGroupName
+quartz.config.mqJobName=mqJob
+quartz.config.mqJobGroupName=mqGroupName
+quartz.config.examJobName=examJob
+quartz.config.examJobGroupName=examGroupName
+quartz.config.examActivityJobGroupName=examActivityGroupName
 
 #============================================================================
 # \u914D\u7F6Erocketmq