xiatian 5 лет назад
Родитель
Сommit
e7ed2d1eff
19 измененных файлов с 1556 добавлено и 6 удалено
  1. 44 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/ExamControlController.java
  2. 0 5
      examcloud-core-oe-student-base/pom.xml
  3. 203 0
      examcloud-core-oe-student-base/src/main/java/cn/com/qmth/examcloud/core/oe/student/base/bean/ExamQuestion.java
  4. 48 0
      examcloud-core-oe-student-base/src/main/java/cn/com/qmth/examcloud/core/oe/student/base/bean/ExamRecordQuestions.java
  5. 68 0
      examcloud-core-oe-student-base/src/main/java/cn/com/qmth/examcloud/core/oe/student/base/enums/ExamProperties.java
  6. 255 0
      examcloud-core-oe-student-base/src/main/java/cn/com/qmth/examcloud/core/oe/student/base/helper/ExamCacheTransferHelper.java
  7. 1 1
      examcloud-core-oe-student-service/pom.xml
  8. 81 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/StartExamInfo.java
  9. 28 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/processor/HttpMethodProcessorImpl.java
  10. 24 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamControlService.java
  11. 32 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamRecordDataService.java
  12. 29 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamRecordPaperStructService.java
  13. 32 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamRecordQuestionsService.java
  14. 29 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamingSessionService.java
  15. 462 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamControlServiceImpl.java
  16. 57 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordDataServiceImpl.java
  17. 35 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordPaperStructServiceImpl.java
  18. 82 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordQuestionsServiceImpl.java
  19. 46 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamingSessionServiceImpl.java

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

@@ -0,0 +1,44 @@
+package cn.com.qmth.examcloud.core.oe.student.api.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.core.oe.common.base.Constants;
+import cn.com.qmth.examcloud.core.oe.common.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.student.bean.StartExamInfo;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamControlService;
+import cn.com.qmth.examcloud.web.helpers.SequenceLockHelper;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+@Api(tags = "在线考试控制")
+@RestController
+@RequestMapping("${app.api.oe.student}/examControl")
+public class ExamControlController extends ControllerSupport {
+
+    @Autowired
+    private ExamControlService examControlService;
+
+    /**
+     * 开始考试
+     */
+    @ApiOperation(value = "开始考试")
+    @GetMapping("/startExam")
+    public StartExamInfo startExam(@RequestParam Long examStudentId) {
+        User user = getAccessUser();
+        String sequenceLockKey = Constants.START_EXAM_LOCK_PREFIX + user.getUserId();
+        StartExamInfo startExamInfo;
+        // 开始考试上锁,分布式锁,系统在请求结束后会,自动释放锁,无需手动解锁
+        SequenceLockHelper.getLock(sequenceLockKey);
+        Check.isNull(examStudentId, "examStudentId不能为空");
+
+        startExamInfo = examControlService.startExam(examStudentId, user);
+        return startExamInfo;
+    }
+
+}

+ 0 - 5
examcloud-core-oe-student-base/pom.xml

@@ -56,11 +56,6 @@
             <artifactId>examcloud-core-oe-admin-api-client</artifactId>
             <version>${examcloud.version}</version>
         </dependency>
-        <dependency>
-            <groupId>cn.com.qmth.examcloud.rpc</groupId>
-            <artifactId>examcloud-ws-api-client</artifactId>
-            <version>${examcloud.version}</version>
-        </dependency>
         <dependency>
             <groupId>cn.com.qmth.examcloud.rpc</groupId>
             <artifactId>examcloud-core-marking-api-client</artifactId>

+ 203 - 0
examcloud-core-oe-student-base/src/main/java/cn/com/qmth/examcloud/core/oe/student/base/bean/ExamQuestion.java

@@ -0,0 +1,203 @@
+package cn.com.qmth.examcloud.core.oe.student.base.bean;
+
+import java.io.Serializable;
+
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+
+import cn.com.qmth.examcloud.question.commons.core.question.AnswerType;
+import cn.com.qmth.examcloud.question.commons.core.question.QuestionType;
+
+/**
+ * @author chenken
+ * @date 2018/8/17 10:18
+ * @company QMTH
+ * @description 考生单题作答记录
+ */
+public class ExamQuestion implements Serializable{
+
+    /**
+	 * 
+	 */
+	private static final long serialVersionUID = -6141069483774400912L;
+	
+	/**
+	 * 考试记录Data Id
+	 */
+    private Long examRecordDataId;
+    /**
+     * 大题号
+     */
+    private Integer mainNumber;
+    /**
+     * 原题ID
+     */
+    private String questionId;
+    /**
+     * 顺序
+     */
+    private Integer order;
+    /**
+     * 小题分数
+     */
+    private Double questionScore;
+    /**
+     * 小题类型
+     */
+    private QuestionType questionType;
+    /**
+     * 标准答案
+     */
+    private String correctAnswer;
+    /**
+     * 考生作答
+     */
+    private String studentAnswer;
+    /**
+     * 学生小题得分
+     */
+    private Double studentScore;
+    /**
+     * 是否作答
+     */
+    private Boolean isAnswer;
+    /**
+     * 是否标记
+     */
+    private Boolean isSign;
+    
+    /**
+	 * 选项排序值
+	 */
+	private Integer[] optionPermutation;
+	
+	/**
+	 * 音频播放次数
+	 */
+	private String audioPlayTimes;
+	/**
+	 * 题目作答类型
+	 */
+	@Enumerated(EnumType.STRING)
+	private AnswerType answerType;	
+	
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+	public Integer getMainNumber() {
+		return mainNumber;
+	}
+	/**
+	 * 设置 大题号
+	 * @param mainNumber
+	 */
+	public void setMainNumber(Integer mainNumber) {
+		this.mainNumber = mainNumber;
+	}
+	public String getQuestionId() {
+		return questionId;
+	}
+	/**
+	 * 设置题库试题ID
+	 * @param questionId
+	 */
+	public void setQuestionId(String questionId) {
+		this.questionId = questionId;
+	}
+	public Integer getOrder() {
+		return order;
+	}
+	/**
+	 * 设置小题号
+	 * @param order
+	 */
+	public void setOrder(Integer order) {
+		this.order = order;
+	}
+	public String getStudentAnswer() {
+		return studentAnswer;
+	}
+	public void setStudentAnswer(String studentAnswer) {
+		this.studentAnswer = studentAnswer;
+	}
+	public Double getStudentScore() {
+		return studentScore;
+	}
+	/**
+	 * 设置考生得分
+	 * @param studentScore
+	 */
+	public void setStudentScore(Double studentScore) {
+		this.studentScore = studentScore;
+	}
+	public Double getQuestionScore() {
+		return questionScore;
+	}
+	/**
+	 * 设置试题分数
+	 * @param questionScore
+	 */
+	public void setQuestionScore(Double questionScore) {
+		this.questionScore = questionScore;
+	}
+	public QuestionType getQuestionType() {
+		return questionType;
+	}
+	/**
+	 * 设置题型
+	 * @param questionType
+	 */
+	public void setQuestionType(QuestionType questionType) {
+		this.questionType = questionType;
+	}
+	public Boolean getIsAnswer() {
+		return isAnswer;
+	}
+	public void setIsAnswer(Boolean isAnswer) {
+		this.isAnswer = isAnswer;
+	}
+	public Boolean getIsSign() {
+		return isSign;
+	}
+	public void setIsSign(Boolean isSign) {
+		this.isSign = isSign;
+	}
+
+	public String getCorrectAnswer() {
+		return correctAnswer;
+	}
+
+	public void setCorrectAnswer(String correctAnswer) {
+		this.correctAnswer = correctAnswer;
+	}
+
+	public Integer[] getOptionPermutation() {
+		return optionPermutation;
+	}
+
+	public void setOptionPermutation(Integer[] optionPermutation) {
+		this.optionPermutation = optionPermutation;
+	}
+
+	public String getAudioPlayTimes() {
+		return audioPlayTimes;
+	}
+
+	public void setAudioPlayTimes(String audioPlayTimes) {
+		this.audioPlayTimes = audioPlayTimes;
+	}
+
+	public AnswerType getAnswerType() {
+		return answerType;
+	}
+
+	public void setAnswerType(AnswerType answerType) {
+		this.answerType = answerType;
+	}
+
+	
+	
+}

+ 48 - 0
examcloud-core-oe-student-base/src/main/java/cn/com/qmth/examcloud/core/oe/student/base/bean/ExamRecordQuestions.java

@@ -0,0 +1,48 @@
+package cn.com.qmth.examcloud.core.oe.student.base.bean;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author chenken
+ * @date 2018年9月3日 上午10:46:06
+ * @company QMTH
+ * @description 考试作答试题集合
+ */
+public class ExamRecordQuestions implements Serializable {
+    private static final long serialVersionUID = -1688201571728312142L;
+
+    private Long examRecordDataId;
+
+    private Date creationTime;
+
+    private List<ExamQuestion> examQuestions;
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public Date getCreationTime() {
+        return creationTime;
+    }
+
+    public void setCreationTime(Date creationTime) {
+        this.creationTime = creationTime;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    
+    public List<ExamQuestion> getExamQuestions() {
+        return examQuestions;
+    }
+
+    
+    public void setExamQuestions(List<ExamQuestion> examQuestions) {
+        this.examQuestions = examQuestions;
+    }
+
+}

+ 68 - 0
examcloud-core-oe-student-base/src/main/java/cn/com/qmth/examcloud/core/oe/student/base/enums/ExamProperties.java

@@ -0,0 +1,68 @@
+package cn.com.qmth.examcloud.core.oe.student.base.enums;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年11月15日 下午5:37:35
+ * @company 	QMTH
+ * @description 考试属性
+ */
+public enum ExamProperties {
+
+	SCORE_PUBLISHING("发布成绩"),
+	IS_ENTRANCE_EXAM("是否入学考试"),
+	FREEZE_TIME("交卷冻结时间"),
+	EXAM_RECONNECT_TIME("断点续考时间"),
+	BEFORE_EXAM_REMARK("考前说明"),
+	AFTER_EXAM_REMARK("考后说明"),
+	SHOW_CHEATING_REMARK("是否展示作弊"),
+	CHEATING_REMARK("作弊说明"),
+	PRACTICE_TYPE("练习模式"),
+	SINGLE_EDIT("单选题补充说明是否可填"),
+	MUTIPLE_EDIT("多选题补充说明是否可填"),
+	BOOL_EDIT("判断题补充说明是否可填"),
+	FILL_BLANK_EDIT("填空题补充说明是否可填"),
+	SINGLE_ANSWER_REMARK("单选题补充说明"),
+	MUTIPLE_ANSWER_REMARK("多选题补充说明"),
+	BOOL_ANSWER_REMARK("判断题补充说明"),
+	FILL_BLANK_REMARK("填空题补充说明"),
+	TEXT_ANSWER_REMARK("问答题补充说明"),
+	nestedAnswerRemark("套题补充说明"),
+	IS_FACE_ENABLE("是否启用人脸识别"),
+	IS_FACE_CHECK("进入考试是否验证人脸识别"),
+	WARN_THRESHOLD("人脸检测预警阈值"),
+	MARKING_TYPE("阅卷方式"),
+	IS_FACE_VERIFY("是否开启人脸活体检测"),
+	FACE_VERIFY_START_MINUTE("活体检测开始分钟数"),
+	FACE_VERIFY_END_MINUTE("活体检测结束分钟数"),
+	ADD_FACE_VERIFY_OUT_FREEZE_TIME("冻结时间外新加人脸活体检测"),
+	OUT_FREEZE_TIME_FACE_VERIFY_START_MINUTE("冻结时间外活体检测开始分钟数"),
+	OUT_FREEZE_TIME_FACE_VERIFY_END_MINUTE("冻结时间外活体检测结束分钟数"),
+	IP_LIMIT("是否IP限制"),
+	IP_ADDRESSES("IP白名单"),
+	IS_OBJ_SCORE_VIEW("是否显示客观题成绩"),
+	CAN_UPLOAD_ATTACHMENT("是否允许上传附件(离线考试)"),
+	LIVING_WARN_THRESHOLD("人脸真实性阈值"),
+	MARKING_TASK_BUILDED("阅卷是否生成评卷任务"),
+	OFFLINE_UPLOAD_FILE_TYPE("离线考试上传文件类型"),
+	PUSH_SCORE("是否推送分数"),
+	MAX_INTERRUPT_NUM("最大断点续考次数"),
+	IS_STRANGER_ENABLE("是否启用陌生人检测"),
+	LIMITED_IF_NO_SPECIAL_SETTINGS("无特殊设置时禁止考试"),
+	WEIXIN_ANSWER_ENABLED("是否开放微信小程序作答");
+	
+	private ExamProperties(String desc){
+		this.desc = desc;
+	}
+	
+	private String desc;
+
+	public String getDesc() {
+		return desc;
+	}
+
+	public void setDesc(String desc) {
+		this.desc = desc;
+	}
+	
+}

+ 255 - 0
examcloud-core-oe-student-base/src/main/java/cn/com/qmth/examcloud/core/oe/student/base/helper/ExamCacheTransferHelper.java

@@ -0,0 +1,255 @@
+/*
+ * *************************************************
+ * Copyright (c) 2018 QMTH. All Rights Reserved.
+ * Created by Deason on 2018-08-29 10:44:05.
+ * *************************************************
+ */
+
+package cn.com.qmth.examcloud.core.oe.student.base.helper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import cn.com.qmth.examcloud.api.commons.enums.ExamSpecialSettingsType;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.oe.student.base.enums.ExamProperties;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.CourseCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExamPropertyCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExamSettingsCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.OrgPropertyCacheBean;
+import io.swagger.annotations.ApiOperation;
+import sun.reflect.generics.reflectiveObjects.NotImplementedException;
+
+/**
+ * @Description 网考缓存实体转换服务
+ * @Author lideyin
+ * @Date 2019/8/13 16:00
+ * @Version 1.0
+ */
+public class ExamCacheTransferHelper {
+
+    /**
+     * 获取缓存的考试信息
+     *
+     * @param examId    考试id
+     * @param studentId 学生id
+     * @return
+     */
+    public static ExamBean getCachedExam(Long examId, Long studentId) {
+        ExamSettingsCacheBean examCacheBean = CacheHelper.getExamSettings(examId);
+        //默认取考试中的通用设置
+        ExamBean examBean = copyExamBeanFrom(examCacheBean);
+
+        //是否开启特殊设置
+        Boolean specialSettingsEnabled = examCacheBean.getSpecialSettingsEnabled();
+        if (specialSettingsEnabled == true) {
+            ExamSpecialSettingsType specialSettingsType = examCacheBean.getSpecialSettingsType();
+
+            //开启特殊设置却未进行任何设置则直接返回通用设置
+            if (null == specialSettingsType) {
+                return examBean;
+            }
+
+            switch (specialSettingsType) {
+                case ORG_BASED:
+                    //需求调整,所有的组织机构取学生表所关联的orgId
+                    Long orgId = CacheHelper.getStudent(studentId).getOrgId();
+                    initOrgSpecialSettings(examId, orgId, examBean);
+                    break;
+                case STUDENT_BASED:
+                    //初始化学生的特殊化设置
+                    initStudentSpecialSettings(examId, studentId, examBean);
+                    break;
+                case COURSE_BASED:
+                    //暂无此需求
+                    throw new NotImplementedException();
+            }
+        }
+        return examBean;
+    }
+
+    /**
+     * 获取默认(即非个性化)的考试信息
+     * <b>注意:只有非个性化的信息才可调用此方法</b>
+     *
+     * @param examId 考试id
+     * @return
+     */
+    public static ExamBean getDefaultCachedExam(Long examId) {
+        return copyExamBeanFrom(CacheHelper.getExamSettings(examId));
+    }
+
+    /**
+     * 获取考试的属性
+     *
+     * @param examId
+     * @param studentId
+     * @param propKey
+     * @return
+     */
+    public static ExamPropertyCacheBean getCachedExamProperty(Long examId, Long studentId, String propKey) {
+        ExamSettingsCacheBean examCacheBean = CacheHelper.getExamSettings(examId);
+        //默认取考试中的通用设置
+        ExamPropertyCacheBean examPropertyCacheBean = CacheHelper.getExamProperty(examId, propKey);
+
+        //是否开启特殊设置
+        Boolean specialSettingsEnabled = examCacheBean.getSpecialSettingsEnabled();
+        if (specialSettingsEnabled == true) {
+            ExamSpecialSettingsType specialSettingsType = examCacheBean.getSpecialSettingsType();
+
+            //开启特殊设置却未进行任何设置则直接返回通用设置
+            if (null == specialSettingsType) {
+                return examPropertyCacheBean;
+            }
+
+            ExamPropertyCacheBean specialExamProperty = null;
+            switch (specialSettingsType) {
+                case ORG_BASED:
+                    //需求调整,所有的组织机构取学生表所关联的orgId
+                    Long orgId = CacheHelper.getStudent(studentId).getOrgId();
+                    specialExamProperty = CacheHelper.getExamOrgProperty(examId, orgId, propKey);
+                    break;
+                case STUDENT_BASED:
+                    specialExamProperty = CacheHelper.getExamStudentProperty(examId, studentId, propKey);
+                    break;
+                case COURSE_BASED:
+                    //暂无此需求
+                    throw new NotImplementedException();
+            }
+            if (specialExamProperty.getHasValue()) {
+                return specialExamProperty;
+            }
+        }
+
+        return examPropertyCacheBean;
+    }
+
+    /**
+     * 获取默认(即非个性化)的考试属性信息
+     *
+     * @param examId
+     * @param propKey
+     * @return
+     */
+    public static ExamPropertyCacheBean getDefaultCachedExamProperty(Long examId, String propKey) {
+        return CacheHelper.getExamProperty(examId, propKey);
+    }
+
+    /**
+     * 获取课程
+     *
+     * @param courseId 课程id
+     * @return CourseBean
+     */
+    public static CourseBean getCachedCourse(Long courseId) {
+        CourseCacheBean courseCacheBean = CacheHelper.getCourse(courseId);
+        return copyCourseBeanFrom(courseCacheBean);
+    }
+
+    @ApiOperation(value = "是否可以微信作答", notes = "")
+    @GetMapping("weixinAnswerEnabled/{examId}")
+    public static Boolean weixinAnswerEnabled(@PathVariable Long examId) {
+
+        ExamSettingsCacheBean examSettings = CacheHelper.getExamSettings(examId);
+
+        OrgPropertyCacheBean orgConf = CacheHelper.getOrgProperty(examSettings.getRootOrgId(),
+                ExamProperties.WEIXIN_ANSWER_ENABLED.name());
+        ExamPropertyCacheBean examConf = CacheHelper.getExamProperty(examId,
+                ExamProperties.WEIXIN_ANSWER_ENABLED.name());
+
+        String orgValue = orgConf.getValue();
+        String examValue = examConf.getValue();
+
+        if (!orgConf.getHasValue()) {
+            return false;
+        }
+        if (StringUtils.isBlank(orgValue)) {
+            return false;
+        }
+
+        if (StringUtils.isBlank(examValue)) {
+            return false;
+        }
+
+        if (StringUtil.isTrue(orgValue) && StringUtil.isTrue(examValue)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 初始化学生的特殊化设置
+     *
+     * @param examId
+     * @param studentId
+     * @param examBean
+     */
+    private static void initStudentSpecialSettings(Long examId, Long studentId, ExamBean examBean) {
+        ExamSettingsCacheBean examSpecialSetting =
+                CacheHelper.getExamStudentSettings(examId, studentId);
+        if (null != examSpecialSetting.getBeginTime()) {
+            examBean.setBeginTime(examSpecialSetting.getBeginTime());
+        }
+        if (null != examSpecialSetting.getEndTime()) {
+            examBean.setEndTime(examSpecialSetting.getEndTime());
+        }
+    }
+
+    /**
+     * 初始化组织机构的特殊化设置
+     *
+     * @param examId
+     * @param orgId
+     * @param examBean
+     */
+    private static void initOrgSpecialSettings(Long examId, Long orgId, ExamBean examBean) {
+        ExamSettingsCacheBean examSpecialSetting =
+                CacheHelper.getExamOrgSettings(examId, orgId);
+        if (null != examSpecialSetting.getBeginTime()) {
+            examBean.setBeginTime(examSpecialSetting.getBeginTime());
+        }
+        if (null != examSpecialSetting.getEndTime()) {
+            examBean.setEndTime(examSpecialSetting.getEndTime());
+        }
+
+        if (null != examSpecialSetting.getExamLimit()) {
+            examBean.setExamLimit(examSpecialSetting.getExamLimit());
+        }
+    }
+
+    private static ExamBean copyExamBeanFrom(ExamSettingsCacheBean examCacheBean) {
+        ExamBean resultBean = new ExamBean();
+        resultBean.setId(examCacheBean.getId());
+        resultBean.setBeginTime(examCacheBean.getBeginTime());
+        resultBean.setCode(examCacheBean.getCode());
+        resultBean.setDuration(examCacheBean.getDuration());
+        resultBean.setEnable(examCacheBean.getEnable());
+        resultBean.setEndTime(examCacheBean.getEndTime());
+        resultBean.setExamLimit(examCacheBean.getExamLimit());
+        resultBean.setExamTimes(examCacheBean.getExamTimes());
+        resultBean.setExamType(examCacheBean.getExamType());
+        resultBean.setName(examCacheBean.getName());
+        resultBean.setRemark(examCacheBean.getRemark());
+        resultBean.setRootOrgId(examCacheBean.getRootOrgId());
+        resultBean.setSpecialSettingsEnabled(examCacheBean.getSpecialSettingsEnabled());
+        resultBean.setSpecialSettingsType(examCacheBean.getSpecialSettingsType());
+        return resultBean;
+    }
+
+    private static CourseBean copyCourseBeanFrom(CourseCacheBean courseCacheBean) {
+        CourseBean resultBean = new CourseBean();
+        resultBean.setCode(courseCacheBean.getCode());
+        resultBean.setEnable(courseCacheBean.getEnable());
+        resultBean.setId(courseCacheBean.getId());
+        resultBean.setLevel(courseCacheBean.getLevel());
+        resultBean.setName(courseCacheBean.getName());
+        resultBean.setRootOrgId(courseCacheBean.getRootOrgId());
+
+        return resultBean;
+    }
+}

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

@@ -11,7 +11,7 @@
 	<dependencies>
 		<dependency>
 			<groupId>cn.com.qmth.examcloud</groupId>
-			<artifactId>examcloud-core-oe-commons-dao</artifactId>
+			<artifactId>examcloud-core-oe-student-dao</artifactId>
 			<version>${examcloud.version}</version>
 		</dependency>
 		<dependency>

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

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

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

@@ -0,0 +1,28 @@
+package cn.com.qmth.examcloud.core.oe.student.processor;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.examcloud.reports.commons.util.ReportsUtil;
+import cn.com.qmth.examcloud.web.support.HttpMethodProcessor;
+
+@Component
+public class HttpMethodProcessorImpl implements HttpMethodProcessor {
+
+	@Override
+	public void beforeMethod(HttpServletRequest request, Object[] args) {
+
+	}
+
+	@Override
+	public void onSuccess(HttpServletRequest request, Object[] args, Object ret) {
+		ReportsUtil.sendReport(false);
+	}
+
+	@Override
+	public void onException(HttpServletRequest request, Object[] args, Throwable e) {
+		ReportsUtil.sendReport(true);
+	}
+
+}

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

@@ -0,0 +1,24 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.core.oe.student.bean.StartExamInfo;
+
+/**
+ * @author chenken
+ * @date 2018年8月13日 下午2:09:38
+ * @company QMTH
+ * @description 在线考试控制服务接口
+ */
+public interface ExamControlService {
+
+    /**
+     * 开始考试
+     *
+     * @param examStudentId
+     * @param user
+     */
+    public StartExamInfo startExam(Long examStudentId, User user);
+
+
+}

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

@@ -0,0 +1,32 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+
+/**
+ * @author chenken
+ * @date 2018/8/15 11:16
+ * @company QMTH
+ * @description 考试记录数据服务接口
+ */
+public interface ExamRecordDataService {
+
+    /**
+     * 创建ExamRecordDataEntity
+     * 
+     * @param examedTimes
+     *            已考次数
+     * @param canExamTimes
+     *            可考次数
+     * @param isReExamine
+     *            本次考试是否为重考
+     * @param isFullyObjetive
+     *            是否全客观题
+     * @return
+     */
+    ExamRecordDataEntity createExamRecordData(ExamingSession examingSession, ExamBean examBean, CourseBean courseBean,
+            String basePaperId);
+
+}

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

@@ -0,0 +1,29 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+
+/**
+ * @author chenken
+ *
+ */
+public interface ExamRecordPaperStructService {
+
+    /**
+     * 保存
+     * @param timeout   秒
+     */
+    public void saveExamRecordPaperStruct(Long examRecordDataId,DefaultPaper paper);
+
+    /**
+     * 获取
+     * @param examRecordDataId
+     * @return
+     */
+    public DefaultPaper getExamRecordPaperStruct(Long examRecordDataId);
+
+    /**
+     * 删除
+     * @param examRecordDataId
+     */
+    public void deleteExamRecordPaperStruct(Long examRecordDataId);
+}

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

@@ -0,0 +1,32 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.core.oe.student.base.bean.ExamRecordQuestions;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+
+/**
+ * @author chenken
+ *
+ */
+public interface ExamRecordQuestionsService {
+
+    /**
+     * 保存
+     * @param timeout   秒
+     */
+    public void saveExamRecordQuestions(Long examRecordDataId,ExamRecordQuestions questions);
+
+    /**
+     * 获取
+     * @param examRecordDataId
+     * @return
+     */
+    public ExamRecordQuestions getExamRecordQuestions(Long examRecordDataId);
+
+    /**
+     * 删除
+     * @param examRecordDataId
+     */
+    public void deleteExamRecordQuestions(Long examRecordDataId);
+
+    public ExamRecordQuestions createExamRecordQuestions(Long examRecordDataId, DefaultPaper defaultPaper);
+}

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

@@ -0,0 +1,29 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+
+/**
+ * @author chenken
+ *
+ */
+public interface ExamingSessionService {
+
+    /**
+     * 保存
+     * @param timeout   秒
+     */
+    public void saveExamingSession(Long studentId,ExamingSession examingSession,int timeout);
+
+    /**
+     * 获取
+     * @param studentId
+     * @return
+     */
+    public ExamingSession getExamingSession(Long studentId);
+
+    /**
+     * 删除
+     * @param studentId
+     */
+    public void deleteExamingSession(Long studentId);
+}

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

@@ -0,0 +1,462 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import cn.com.qmth.examcloud.api.commons.enums.ExamSpecialSettingsType;
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.oe.student.base.enums.ExamProperties;
+import cn.com.qmth.examcloud.core.oe.student.base.helper.ExamCacheTransferHelper;
+import cn.com.qmth.examcloud.core.oe.student.base.utils.CommonUtil;
+import cn.com.qmth.examcloud.core.oe.student.base.utils.QuestionTypeUtil;
+import cn.com.qmth.examcloud.core.oe.student.bean.StartExamInfo;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamControlService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordPaperStructService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordQuestionsService;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamingSessionService;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionGroup;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionStructureWrapper;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionUnitWrapper;
+import cn.com.qmth.examcloud.question.commons.core.question.QuestionType;
+import cn.com.qmth.examcloud.reports.commons.bean.OnlineExamStudentReport;
+import cn.com.qmth.examcloud.reports.commons.util.ReportsUtil;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.ExamOrgSettingsCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExamPropertyCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExamStudentSettingsCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExtractConfigCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExtractConfigDetailCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExtractConfigPaperCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+import cn.com.qmth.examcloud.support.examing.ExamingStatus;
+
+/**
+ * @author chenken
+ * @date 2018年8月13日 下午2:09:08
+ * @company QMTH
+ * @description 在线考试控制服务实现
+ */
+@Service("examControlService")
+public class ExamControlServiceImpl implements ExamControlService {
+
+    private static final Logger log = LoggerFactory.getLogger(ExamControlServiceImpl.class);
+
+    @Autowired
+    private ExamRecordPaperStructService examRecordPaperStructService;
+
+    @Autowired
+    private ExamingSessionService examingSessionService;
+
+    @Autowired
+    private ExamRecordDataService examRecordDataService;
+
+    @Autowired
+    private ExamRecordQuestionsService examRecordQuestionsService;
+
+    @Transactional
+    @Override
+    public StartExamInfo startExam(Long examStudentId, User user) {
+        Long studentId = user.getUserId();
+        long st = System.currentTimeMillis();
+        SysPropertyCacheBean stuClientLoginLimit = CacheHelper.getSysProperty("STU_CLIENT_LOGIN_LIMIT");
+        Boolean stuClientLoginLimitBoolean = false;
+        if (stuClientLoginLimit.getHasValue()) {
+            stuClientLoginLimitBoolean = Boolean.valueOf(stuClientLoginLimit.getValue().toString());
+        }
+        if (stuClientLoginLimitBoolean) {
+            throw new StatusException("1001", "系统维护中... ...");
+        }
+
+        long startTime = System.currentTimeMillis();
+        if (log.isDebugEnabled()) {
+            log.debug("1 获取考生信息耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+        // 检查redis session
+        ExamingSession examingSession = examingSessionService.getExamingSession(studentId);
+        if (examingSession == null) {
+            throw new StatusException("1002", "未找到考试会话信息");
+        }
+        if (ExamingStatus.FORMAL.equals(examingSession.getExamingStatus())) {
+            throw new StatusException("1003", "已经有考试中的科目");
+        }
+
+        // 检查并获取考试信息
+        startTime = System.currentTimeMillis();
+        ExamBean examBean = checkExam(examingSession.getExamId(), examingSession.getStudentId());
+        if (log.isDebugEnabled()) {
+            log.debug("2 检查并获取考试信息耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 检查并获取课程信息
+        startTime = System.currentTimeMillis();
+        CourseBean courseBean = checkCourse(examingSession.getCourseId());
+        if (log.isDebugEnabled()) {
+            log.debug("3 检查并获取课程信息耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 获取题库试卷结构(由于存在随机抽卷,所以不能缓存 )
+        startTime = System.currentTimeMillis();
+
+        // 获取题库调卷规则
+        ExtractConfigCacheBean extractConfig = CacheHelper.getExtractConfig(examingSession.getExamId(),
+                courseBean.getCode());
+        // 随机生成试卷
+        Map<String, String> paperTypeMaps = getExamPaperByProbability(extractConfig.getDetails());
+        if (paperTypeMaps.isEmpty()) {
+            throw new StatusException("1006", "生成试卷失败");
+        }
+
+        String paperId = paperTypeMaps.get(examingSession.getPaperType());
+        if (StringUtils.isEmpty(paperId)) {
+            throw new StatusException("1007", "获取试卷失败");
+        }
+
+        // 生成考试记录
+        startTime = System.currentTimeMillis();
+        ExamRecordDataEntity examRecordData = examRecordDataService.createExamRecordData(examingSession, examBean,
+                courseBean, paperId);
+
+        // 如果开启人脸比对,将同步人脸比对结果存储到抓后结果表中
+        Long rootOrgId = examRecordData.getRootOrgId();
+        Long examId = examRecordData.getExamId();
+        // TODO
+        // if (FaceBiopsyHelper.isFaceEnable(rootOrgId, examId, studentId)) {
+        // SaveExamCaptureSyncCompareResultReq req = new
+        // SaveExamCaptureSyncCompareResultReq();
+        // req.setExamRecordDataId(examRecordData.getId());
+        // req.setStudentId(user.getUserId());
+        // examCaptureCloudService.saveExamCaptureSyncCompareResult(req);
+        // }
+
+        if (log.isDebugEnabled()) {
+            log.debug("8 生成考试记录耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 生成试卷结构
+        ExtractConfigPaperCacheBean extractConfigPaper = CacheHelper.getExtractConfigPaper(examingSession.getExamId(),
+                courseBean.getCode(), examingSession.getPaperType(), paperId);
+        if (log.isDebugEnabled()) {
+            log.debug("4 获取题库试卷结构耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 小题乱序,选项乱序
+        startTime = System.currentTimeMillis();
+        reorderPaperStruct(extractConfig, extractConfigPaper);
+        if (log.isDebugEnabled()) {
+            log.debug("5 小题乱序耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 保存考试试卷结构
+        startTime = System.currentTimeMillis();
+        examRecordPaperStructService.saveExamRecordPaperStruct(examRecordData.getId(),
+                extractConfigPaper.getDefaultPaper());
+        if (log.isDebugEnabled()) {
+            log.debug("6 保存考试试卷结构耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 创建考试作答记录
+        startTime = System.currentTimeMillis();
+        examRecordQuestionsService.createExamRecordQuestions(examRecordData.getId(),
+                extractConfigPaper.getDefaultPaper());
+        if (log.isDebugEnabled()) {
+            log.debug("9 创建考试作答记录耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        // 创建考试会话
+        startTime = System.currentTimeMillis();
+        // TODO
+        // initializeExamRecordSession(originalExamStudent, examRecordData,
+        // examBean);
+        if (log.isDebugEnabled()) {
+            log.debug("11 创建考试会话耗时:" + (System.currentTimeMillis() - startTime) + " ms");
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("12 合计 耗时:" + (System.currentTimeMillis() - st) + " ms");
+        }
+        // 在线考生开考打点
+        ReportsUtil.report(
+                new OnlineExamStudentReport(user.getRootOrgId(), user.getUserId(), examBean.getId(), examStudentId));
+        // TODO
+        // return buildStartExamInfo(examRecordData.getId(),
+        // originalExamStudent, examBean, courseBean);
+        return null;
+
+    }
+
+    /**
+     * 检查并返回考试 开考条件 1.enable为true 2.开始时间和结束时间判断 3.examLimit为null或false
+     * 4.剩余考试次数>0
+     *
+     * @param examStudentEntity
+     * @return
+     */
+    private ExamBean checkExam(Long examId, Long studentId) {
+
+        // 学习中心特殊考试配置(是否禁考,开考时间可以特殊设置)
+        ExamBean examBean = ExamCacheTransferHelper.getCachedExam(examId, studentId);
+        // 如果启用了了特殊设置,并且无特殊设置时结束考试 配置 设置为true..且实际未设置特殊设置则不允许考试
+        ExamPropertyCacheBean limitedIfNoSpecialSettings = ExamCacheTransferHelper.getDefaultCachedExamProperty(examId,
+                ExamProperties.LIMITED_IF_NO_SPECIAL_SETTINGS.toString());
+        if (examBean.getSpecialSettingsEnabled() && (limitedIfNoSpecialSettings.getHasValue()
+                && Boolean.valueOf(limitedIfNoSpecialSettings.getValue()))) {
+
+            // 学生特殊设置开启未配置,不允许考试
+            if (examBean.getSpecialSettingsType() == ExamSpecialSettingsType.STUDENT_BASED) {
+                ExamStudentSettingsCacheBean specialSettings = CacheHelper.getExamStudentSettings(examId, studentId);
+                if (!specialSettings.getHasValue()) {
+                    throw new StatusException("2001", "考试配置未完成,不允许考试");
+                }
+            }
+
+            // 机构特殊设置开启未配置,不允许考试
+            if (examBean.getSpecialSettingsType() == ExamSpecialSettingsType.ORG_BASED) {
+                // 需求调整,所有的组织机构取学生表所关联的orgId
+                Long orgId = CacheHelper.getStudent(studentId).getOrgId();
+                ExamOrgSettingsCacheBean specialSettings = CacheHelper.getExamOrgSettings(examId, orgId);
+                if (!specialSettings.getHasValue()) {
+                    throw new StatusException("2002", "考试配置未完成,不允许考试");
+                }
+            }
+
+        }
+
+        if (!examBean.getEnable() || (examBean.getExamLimit() != null && examBean.getExamLimit())) {
+            throw new StatusException("2003", "暂无考试资格,请与学校老师联系");
+        }
+        if (new Date().before(examBean.getBeginTime())) {
+            throw new StatusException("2004", "考试未开始");
+        }
+        if (examBean.getEndTime().before(new Date())) {
+            throw new StatusException("2005", "本次考试已结束");
+        }
+        return examBean;
+    }
+
+    private CourseBean checkCourse(Long courseId) {
+        CourseBean courseBean = ExamCacheTransferHelper.getCachedCourse(courseId);
+        if (!courseBean.getEnable()) {
+            throw new StatusException("3001", "该课程已被禁用");
+        }
+        return courseBean;
+    }
+
+    /**
+     * 每个试卷类型取出一套试卷 {A:paperId,B:paperId} A是试卷类型,paperId是A类型下选定的试卷ID
+     */
+    private Map<String, String> getExamPaperByProbability(List<ExtractConfigDetailCacheBean> examPapers) {
+        if (CollectionUtils.isEmpty(examPapers)) {
+            throw new StatusException("4001", "可供抽取的试卷集合为空!");
+        }
+
+        Map<String, List<ExtractConfigDetailCacheBean>> examPaperMaps = new HashMap<>();
+        for (ExtractConfigDetailCacheBean examPaper : examPapers) {
+            if (examPaperMaps.containsKey(examPaper.getGroupCode())) {
+                examPaperMaps.get(examPaper.getGroupCode()).add(examPaper);
+            } else {
+                List<ExtractConfigDetailCacheBean> list = new ArrayList<>();
+                list.add(examPaper);
+                examPaperMaps.put(examPaper.getGroupCode(), list);
+            }
+        }
+
+        Map<String, String> paperTypeMaps = new HashMap<>();
+        for (Map.Entry<String, List<ExtractConfigDetailCacheBean>> entry : examPaperMaps.entrySet()) {
+            List<ExtractConfigDetailCacheBean> list = examPaperMaps.get(entry.getKey());
+
+            String paperId = this.getPaperByProbability(list);
+            if (StringUtils.isEmpty(paperId)) {
+                continue;
+            }
+
+            paperTypeMaps.put(entry.getKey(), paperId);
+        }
+
+        return paperTypeMaps;
+    }
+
+    /**
+     * 根据设定几率取出一套试卷
+     */
+    private String getPaperByProbability(List<ExtractConfigDetailCacheBean> examPapers) {
+        int sum = 0;
+        for (ExtractConfigDetailCacheBean examPaper : examPapers) {
+            sum += examPaper.getWeight();
+        }
+
+        // 从1开始
+        int r = new Random().nextInt(sum) + 1;
+        for (ExtractConfigDetailCacheBean examPaper : examPapers) {
+            r -= examPaper.getWeight();
+            if (r <= 0) {
+                return examPaper.getPaperId();// 选中
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 获取试卷结构 小题乱序、选项乱序
+     *
+     * @param extractConfig
+     *            调卷规则对象
+     * @param paperStruct
+     *            试卷结构对象
+     */
+    private void reorderPaperStruct(ExtractConfigCacheBean extractConfig, ExtractConfigPaperCacheBean paperStruct) {
+        // 小题乱序
+        if (extractConfig.getSortQuestionOrder() != null && extractConfig.getSortQuestionOrder()) {
+            reorderQuestion(paperStruct.getDefaultPaper());
+        }
+        // 选项乱序
+        if (extractConfig.getSortOptionOrder() != null && extractConfig.getSortOptionOrder()) {
+            reorderOption(paperStruct.getDefaultPaper());
+        }
+    }
+
+    /**
+     * 小题乱序
+     *
+     * @param defaultPaper
+     */
+    private void reorderQuestion(DefaultPaper defaultPaper) {
+        List<DefaultQuestionGroup> defaultQuestionGroupList = defaultPaper.getQuestionGroupList();
+        for (int i = 0; i < defaultQuestionGroupList.size(); i++) {
+            DefaultQuestionGroup defaultQuestionGroup = defaultQuestionGroupList.get(i);
+            if (checkObjectiveQuestionByGroup(defaultQuestionGroup)) {
+                List<DefaultQuestionStructureWrapper> questionStructureWrapperList = defaultQuestionGroup
+                        .getQuestionWrapperList();
+                Collections.shuffle(questionStructureWrapperList);
+            }
+        }
+    }
+
+    /**
+     * 选项乱序
+     *
+     * @param defaultPaper
+     */
+    private void reorderOption(DefaultPaper defaultPaper) {
+        List<DefaultQuestionGroup> defaultQuestionGroupList = defaultPaper.getQuestionGroupList();
+        // 遍历大题
+        for (int i = 0; i < defaultQuestionGroupList.size(); i++) {
+            DefaultQuestionGroup defaultQuestionGroup = defaultQuestionGroupList.get(i);
+            List<DefaultQuestionStructureWrapper> questionStructureWrapperList = defaultQuestionGroup
+                    .getQuestionWrapperList();
+            // 遍历小题
+            for (DefaultQuestionStructureWrapper defaultQuestionStructureWrapper : questionStructureWrapperList) {
+                List<DefaultQuestionUnitWrapper> questionUnitWrapperList = defaultQuestionStructureWrapper
+                        .getQuestionUnitWrapperList();
+                // 遍历题单元
+                for (DefaultQuestionUnitWrapper defaultQuestionUnitWrapper : questionUnitWrapperList) {
+                    if (QuestionTypeUtil.isChoiceQuestion(defaultQuestionUnitWrapper.getQuestionType())) {
+                        Integer[] optionPermutation = defaultQuestionUnitWrapper.getOptionPermutation();
+                        defaultQuestionUnitWrapper.setOptionPermutation(CommonUtil.reorderArray(optionPermutation));
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 小题乱序条件:一个大题下全是单一的客观题,例如全是单选,全是多选,全是判断 判断一个大题下是否是单一的客观题
+     *
+     * @return
+     */
+    private boolean checkObjectiveQuestionByGroup(DefaultQuestionGroup defaultQuestionGroup) {
+        Set<QuestionType> questionTypes = new HashSet<QuestionType>();
+        List<DefaultQuestionStructureWrapper> questionWrapperList = defaultQuestionGroup.getQuestionWrapperList();
+        for (int i = 0; i < questionWrapperList.size(); i++) {
+            List<DefaultQuestionUnitWrapper> questionUnitWrapperList = questionWrapperList.get(i)
+                    .getQuestionUnitWrapperList();
+            if (questionUnitWrapperList.size() > 1) {
+                return false;
+            } else {
+                questionTypes.add(questionUnitWrapperList.get(0).getQuestionType());
+            }
+        }
+        if (questionTypes.size() > 1) {
+            return false;
+        }
+        QuestionType questionType = questionTypes.iterator().next();
+        if (QuestionTypeUtil.isObjectiveQuestion(questionType)) {
+            return true;
+        }
+        return false;
+    }
+    // TODO
+
+    // private StartExamInfo buildStartExamInfo(Long examRecordDataId,
+    // ExamingSession examingSession, ExamBean examBean,
+    // CourseBean courseBean) {
+    // StartExamInfo startExamInfo = new StartExamInfo();
+    // startExamInfo.setExamRecordDataId(examRecordDataId);
+    // startExamInfo.setCourseName(courseBean.getName());
+    // startExamInfo.setDuration(examBean.getDuration());
+    // startExamInfo.setFaceVerifyMinute(getFaceVerifyMinute(examingSession.getRootOrgId(),
+    // examBean.getId(),
+    // examingSession.getOrgId(), examingSession.getStudentId()));
+    // return startExamInfo;
+    // }
+
+    /**
+     * 确定活体检测开始分钟数
+     *
+     * @param examId
+     * @return
+     */
+    // TODO
+    // private Integer getFaceVerifyMinute(Long rootOrgId, Long examId, Long
+    // orgId, Long studentId) {
+    // // 如果开启了活体检测
+    // if (FaceBiopsyHelper.isFaceVerify(rootOrgId, examId, studentId)) {
+    // // 开始分钟数
+    // String startMinuteStr = ExamCacheTransferHelper
+    // .getCachedExamProperty(examId, studentId,
+    // ExamProperties.FACE_VERIFY_START_MINUTE.name()).getValue();
+    // if (CommonUtil.isBlank(startMinuteStr)) {
+    // throw new
+    // StatusException("ExamControlServiceImpl-getFaceVerifyMinute-001",
+    // ExamProperties.FACE_VERIFY_START_MINUTE.getDesc() + "未设置");
+    // }
+    // Integer faceVerifyStartMinute = Integer.valueOf(startMinuteStr);
+    //
+    // // 结束分钟数
+    // String endMinuteStr = ExamCacheTransferHelper
+    // .getCachedExamProperty(examId, studentId,
+    // ExamProperties.FACE_VERIFY_END_MINUTE.name()).getValue();
+    // if (CommonUtil.isBlank(endMinuteStr)) {
+    // throw new
+    // StatusException("ExamControlServiceImpl-getFaceVerifyMinute-002",
+    // ExamProperties.FACE_VERIFY_END_MINUTE.getDesc() + "未设置");
+    // }
+    // Integer faceVerifyEndMinute = Integer.valueOf(endMinuteStr);
+    // return CommonUtil.calculationRandomNumber(faceVerifyStartMinute,
+    // faceVerifyEndMinute);
+    // }
+    // return null;
+    // }
+}

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

@@ -0,0 +1,57 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import java.util.Date;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.oe.student.dao.ExamRecordDataRepo;
+import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamRecordDataEntity;
+import cn.com.qmth.examcloud.core.oe.student.dao.enums.ExamType;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.examwork.api.bean.ExamBean;
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+
+/**
+ * @author chenken
+ * @date 2018/8/15 11:16
+ * @company QMTH
+ * @description 考试记录数据服务实现
+ */
+@Service("examRecordDataService")
+public class ExamRecordDataServiceImpl implements ExamRecordDataService {
+
+    @Autowired
+    private ExamRecordDataRepo examRecordDataRepo;
+
+    @Transactional
+    @Override
+    public ExamRecordDataEntity createExamRecordData(ExamingSession examingSession, ExamBean examBean,
+            CourseBean courseBean, String basePaperId) {
+        ExamRecordDataEntity examRecordData = new ExamRecordDataEntity();
+        examRecordData.setExamId(examBean.getId());
+        examRecordData.setExamType(ExamType.strToEnum(examBean.getExamType()));
+
+        examRecordData.setExamStudentId(examingSession.getExamStudentId());
+        examRecordData.setStudentId(examingSession.getStudentId());
+        examRecordData.setOrgId(examingSession.getOrgId());
+        examRecordData.setRootOrgId(examingSession.getRootOrgId());
+
+        examRecordData.setCourseId(courseBean.getId());
+        examRecordData.setBasePaperId(basePaperId);
+
+        examRecordData.setPaperType(examingSession.getPaperType());
+
+        examRecordData.setStartTime(new Date());
+        examRecordData.setIsContinued(false);
+        examRecordData.setContinuedCount(0);
+        examRecordData.setFaceSuccessCount(0);
+        examRecordData.setFaceTotalCount(0);
+        examRecordData.setFaceFailedCount(0);
+        examRecordData.setFaceStrangerCount(0);
+        return examRecordDataRepo.save(examRecordData);
+    }
+
+}

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

@@ -0,0 +1,35 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordPaperStructService;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+
+@Service("examRecordPaperStructService")
+public class ExamRecordPaperStructServiceImpl implements ExamRecordPaperStructService {
+
+    @Autowired
+    private RedisClient redisClient;
+
+    @Value("${exam_paper_struct_key_prefix}")
+    private String examPaperStructKeyPrefix;
+
+    @Override
+    public void saveExamRecordPaperStruct(Long examRecordDataId, DefaultPaper paper) {
+        redisClient.set(examPaperStructKeyPrefix + examRecordDataId, paper);
+    }
+
+    @Override
+    public DefaultPaper getExamRecordPaperStruct(Long examRecordDataId) {
+        return redisClient.get(examPaperStructKeyPrefix + examRecordDataId, DefaultPaper.class);
+    }
+
+    @Override
+    public void deleteExamRecordPaperStruct(Long examRecordDataId) {
+        redisClient.delete(examPaperStructKeyPrefix + examRecordDataId);
+    }
+
+}

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

@@ -0,0 +1,82 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import cn.com.qmth.examcloud.core.oe.student.base.bean.ExamQuestion;
+import cn.com.qmth.examcloud.core.oe.student.base.bean.ExamRecordQuestions;
+import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordQuestionsService;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionGroup;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionStructureWrapper;
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionUnitWrapper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+
+@Service("examRecordQuestionsService")
+public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsService {
+
+    @Autowired
+    private RedisClient redisClient;
+
+    @Value("${exam_record_questions_key_prefix}")
+    private String examRecordQuestionsKeyPrefix;
+
+    @Override
+    public void saveExamRecordQuestions(Long examRecordDataId, ExamRecordQuestions questions) {
+        redisClient.set(examRecordQuestionsKeyPrefix + examRecordDataId, questions);
+    }
+
+    @Override
+    public ExamRecordQuestions getExamRecordQuestions(Long examRecordDataId) {
+        return redisClient.get(examRecordQuestionsKeyPrefix + examRecordDataId, ExamRecordQuestions.class);
+    }
+
+    @Override
+    public void deleteExamRecordQuestions(Long examRecordDataId) {
+        redisClient.delete(examRecordQuestionsKeyPrefix + examRecordDataId);
+    }
+
+    @Override
+    public ExamRecordQuestions createExamRecordQuestions(Long examRecordDataId, DefaultPaper defaultPaper) {
+        ExamRecordQuestions examRecordQuestions = new ExamRecordQuestions();
+
+        List<ExamQuestion> examQuestionEntityList = new ArrayList<ExamQuestion>();
+        List<DefaultQuestionGroup> defaultQuestionGroups = defaultPaper.getQuestionGroupList();
+        int order = 0;
+        for (int i = 0; i < defaultQuestionGroups.size(); i++) {
+            DefaultQuestionGroup defaultQuestionGroup = defaultQuestionGroups.get(i);
+            List<DefaultQuestionStructureWrapper> defaultQuestionStructureWrappers = defaultQuestionGroup
+                    .getQuestionWrapperList();
+            for (DefaultQuestionStructureWrapper defaultQuestionStructureWrapper : defaultQuestionStructureWrappers) {
+                List<DefaultQuestionUnitWrapper> questionUnitWrapperList = defaultQuestionStructureWrapper
+                        .getQuestionUnitWrapperList();
+                for (DefaultQuestionUnitWrapper defaultQuestionUnitWrapper : questionUnitWrapperList) {
+                    ExamQuestion examQuestionEntity = new ExamQuestion();
+                    examQuestionEntity.setExamRecordDataId(examRecordDataId);
+                    examQuestionEntity.setMainNumber(i + 1);
+                    examQuestionEntity.setOrder(++order);
+                    examQuestionEntity.setQuestionId(defaultQuestionStructureWrapper.getQuestionId());
+                    examQuestionEntity.setQuestionScore(defaultQuestionUnitWrapper.getQuestionScore());
+                    examQuestionEntity.setQuestionType(defaultQuestionUnitWrapper.getQuestionType());
+                    examQuestionEntity.setOptionPermutation(defaultQuestionUnitWrapper.getOptionPermutation());
+                    examQuestionEntity.setAudioPlayTimes(null);
+                    examQuestionEntity.setAnswerType(defaultQuestionUnitWrapper.getAnswerType());
+
+                    examQuestionEntityList.add(examQuestionEntity);
+                }
+            }
+        }
+
+        examRecordQuestions.setExamQuestions(examQuestionEntityList);
+        examRecordQuestions.setExamRecordDataId(examRecordDataId);
+        examRecordQuestions.setCreationTime(new Date());
+        saveExamRecordQuestions(examRecordDataId, examRecordQuestions);
+        return examRecordQuestions;
+    }
+
+}

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

@@ -0,0 +1,46 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.core.oe.student.service.ExamingSessionService;
+import cn.com.qmth.examcloud.support.examing.ExamingSession;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author chenken
+ * @date 2018/8/15 9:24
+ * @company QMTH
+ * @description 考试会话服务实现
+ */
+@Service("examingSessionService")
+public class ExamingSessionServiceImpl implements ExamingSessionService {
+
+    @Autowired
+    private RedisClient redisClient;
+    
+    @Value("${exam_redis_key_prefix}")
+    private String examRedisKeyPrefix;
+
+    private static final Logger log = LoggerFactory.getLogger(ExamControlServiceImpl.class);
+
+    @Override
+    public void saveExamingSession(Long studentId, ExamingSession examingSession, int timeout) {
+        log.debug("11.4.1 进入开始保存考试会话方法,redisKey="+examRedisKeyPrefix+studentId);
+        redisClient.set(examRedisKeyPrefix+studentId,examingSession,timeout);
+        ExamingSession sessionInfo =redisClient.get(examRedisKeyPrefix+studentId,ExamingSession.class);
+        log.debug("11.4.2 保存考试会话方法完成"+sessionInfo.getExamStudentId());
+    }
+
+    @Override
+    public ExamingSession getExamingSession(Long studentId) {
+        return redisClient.get(examRedisKeyPrefix+studentId,ExamingSession.class);
+    }
+
+    @Override
+    public void deleteExamingSession(Long studentId) {
+        redisClient.delete(examRedisKeyPrefix+studentId);
+    }
+}