Переглянути джерело

Merge branch 'branch_other' 6

deason 5 роки тому
батько
коміт
06413790f1
71 змінених файлів з 6108 додано та 0 видалено
  1. 218 0
      src/main/java/cn/com/qmth/examcloud/support/Constants.java
  2. 369 0
      src/main/java/cn/com/qmth/examcloud/support/cache/CacheHelper.java
  3. 67 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/AppCacheBean.java
  4. 27 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/BasePaperCacheBean.java
  5. 73 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/CourseCacheBean.java
  6. 24 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamOrgPropertyCacheBean.java
  7. 14 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamOrgSettingsCacheBean.java
  8. 46 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamPropertyCacheBean.java
  9. 39 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamRecordPropertyCacheBean.java
  10. 198 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamSettingsCacheBean.java
  11. 132 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStudentCacheBean.java
  12. 24 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStudentPropertyCacheBean.java
  13. 14 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStudentSettingsCacheBean.java
  14. 93 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExtractConfigCacheBean.java
  15. 63 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExtractConfigDetailCacheBean.java
  16. 27 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExtractConfigPaperCacheBean.java
  17. 84 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/OrgCacheBean.java
  18. 39 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/OrgPropertyCacheBean.java
  19. 41 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/PrivilegeRolesCacheBean.java
  20. 41 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/QuestionAnswerCacheBean.java
  21. 27 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/QuestionCacheBean.java
  22. 84 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/RootOrgCacheBean.java
  23. 41 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/RootOrgPrivilegesCacheBean.java
  24. 206 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/SmsAssemblyCacheBean.java
  25. 143 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/StudentCacheBean.java
  26. 30 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/SysPropertyCacheBean.java
  27. 68 0
      src/main/java/cn/com/qmth/examcloud/support/cache/bean/ThirdPartyAccessCacheBean.java
  28. 6 0
      src/main/java/cn/com/qmth/examcloud/support/enums/BlockType.java
  29. 21 0
      src/main/java/cn/com/qmth/examcloud/support/enums/DataCategory.java
  30. 19 0
      src/main/java/cn/com/qmth/examcloud/support/enums/DbType.java
  31. 69 0
      src/main/java/cn/com/qmth/examcloud/support/enums/ExamProperties.java
  32. 51 0
      src/main/java/cn/com/qmth/examcloud/support/enums/ExamRecordStatus.java
  33. 41 0
      src/main/java/cn/com/qmth/examcloud/support/enums/FaceBiopsyScheme.java
  34. 22 0
      src/main/java/cn/com/qmth/examcloud/support/enums/FileAnswerAcknowledgeStatus.java
  35. 20 0
      src/main/java/cn/com/qmth/examcloud/support/enums/HandInExamType.java
  36. 48 0
      src/main/java/cn/com/qmth/examcloud/support/enums/IsSuccess.java
  37. 21 0
      src/main/java/cn/com/qmth/examcloud/support/enums/SyncStatus.java
  38. 63 0
      src/main/java/cn/com/qmth/examcloud/support/examing/ExamBoss.java
  39. 100 0
      src/main/java/cn/com/qmth/examcloud/support/examing/ExamFileAnswer.java
  40. 229 0
      src/main/java/cn/com/qmth/examcloud/support/examing/ExamQuestion.java
  41. 544 0
      src/main/java/cn/com/qmth/examcloud/support/examing/ExamRecordData.java
  42. 41 0
      src/main/java/cn/com/qmth/examcloud/support/examing/ExamRecordPaperStruct.java
  43. 48 0
      src/main/java/cn/com/qmth/examcloud/support/examing/ExamRecordQuestions.java
  44. 42 0
      src/main/java/cn/com/qmth/examcloud/support/examing/ExamingActivityTime.java
  45. 54 0
      src/main/java/cn/com/qmth/examcloud/support/examing/ExamingHeartbeat.java
  46. 251 0
      src/main/java/cn/com/qmth/examcloud/support/examing/ExamingSession.java
  47. 22 0
      src/main/java/cn/com/qmth/examcloud/support/examing/ExamingStatus.java
  48. 405 0
      src/main/java/cn/com/qmth/examcloud/support/filestorage/FileStorageUtil.java
  49. 65 0
      src/main/java/cn/com/qmth/examcloud/support/handler/richText/AudioTextHandler.java
  50. 145 0
      src/main/java/cn/com/qmth/examcloud/support/handler/richText/ComplexTextHandler.java
  51. 193 0
      src/main/java/cn/com/qmth/examcloud/support/handler/richText/HtmlTextHandler.java
  52. 172 0
      src/main/java/cn/com/qmth/examcloud/support/handler/richText/ImageTextHandler.java
  53. 13 0
      src/main/java/cn/com/qmth/examcloud/support/handler/richText/RichTextHandler.java
  54. 41 0
      src/main/java/cn/com/qmth/examcloud/support/handler/richText/RichTextHandlerFactory.java
  55. 70 0
      src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/BlockBean.java
  56. 31 0
      src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/SectionBean.java
  57. 25 0
      src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/SectionCollectionBean.java
  58. 220 0
      src/main/java/cn/com/qmth/examcloud/support/helper/ExamCacheTransferHelper.java
  59. 160 0
      src/main/java/cn/com/qmth/examcloud/support/helper/FaceBiopsyHelper.java
  60. 80 0
      src/main/java/cn/com/qmth/examcloud/support/helper/IdentityNumberHelper.java
  61. 86 0
      src/main/java/cn/com/qmth/examcloud/support/privilege/PrivilegeDefine.java
  62. 69 0
      src/main/java/cn/com/qmth/examcloud/support/privilege/PrivilegeManager.java
  63. 36 0
      src/main/java/cn/com/qmth/examcloud/support/redis/RedisKeyBuilder.java
  64. 24 0
      src/main/java/cn/com/qmth/examcloud/support/redis/RedisKeyDefine.java
  65. 59 0
      src/main/java/cn/com/qmth/examcloud/support/redis/RedisKeyHelper.java
  66. 49 0
      src/test/java/cn/com/qmth/examcloud/support/test/Block.java
  67. 6 0
      src/test/java/cn/com/qmth/examcloud/support/test/BlockType.java
  68. 162 0
      src/test/java/cn/com/qmth/examcloud/support/test/Context.java
  69. 13 0
      src/test/java/cn/com/qmth/examcloud/support/test/RedisKeyBuilderTest.java
  70. 21 0
      src/test/java/cn/com/qmth/examcloud/support/test/Section.java
  71. 19 0
      src/test/java/cn/com/qmth/examcloud/support/test/Test.java

+ 218 - 0
src/main/java/cn/com/qmth/examcloud/support/Constants.java

@@ -0,0 +1,218 @@
+/*
+ * *************************************************
+ * Copyright (c) 2018 QMTH. All Rights Reserved.
+ * Created by Deason on 2018-08-24 11:34:17.
+ * *************************************************
+ */
+
+package cn.com.qmth.examcloud.support;
+
+/**
+ * @Description 系统常量
+ * @Author lideyin
+ * @Date 2019/12/11 15:52
+ * @Version 1.0
+ */
+public interface Constants {
+
+	/**
+	 * 系统错误
+	 */
+	String OE_CODE_500 = "OE-000500";
+
+	/**
+	 * 参数错误
+	 */
+	String OE_CODE_400 = "OE-000400";
+
+	/**
+	 * 权限错误
+	 */
+	String OE_CODE_403 = "OE-000403";
+
+	/**
+	 * 考试控制锁
+	 */
+	String EXAM_CONTROL_LOCK_PREFIX = "oe_student:exam_control_lock_studentid_";
+
+	/**
+	 * 考试同步控制锁
+	 */
+	String EXAM_SYNC_CONTROL_LOCK_PREFIX = "oe_student:exam_sync_control_lock_studentid_";
+
+	/**
+	 * 考试交卷锁前缀
+	 */
+	String HAND_IN_EXAM_LOCK_PREFIX = "oe_student:hand_in_exam_lock_";
+
+	/**
+	 * 获取人脸活体检测基本信息前缀
+	 */
+	String GET_FACE_BIOPSY_INFO_PREFIX = "oe_student:get_face_biopsy_info_lock_";
+
+	//
+	String ERROR_MSG = "error_message";
+
+	/**
+	 * 学生考试session前缀
+	 */
+	String OE_STUDENT_EXAM_SESSION_PREFIX = "oe_student:exam_session_";
+
+	String EXAM_CAPTURE_QUEUE_LOCK_PREFIX = "oe_student:exam_capture_queue_lock_";
+
+	/**
+	 * 原始数据库表
+	 */
+	String OE_EXAM_RECORD = "oe_exam_record";
+
+	String ECS_EXAM_STUDENT = "ecs_exam_student";
+
+	String OE_EXAM_QUESTION = "oe_exam_question";
+
+	String OE_EXAM_CAPTURE = "oe_exam_capture";
+
+	String OE_EXAM_SCORE = "oe_exam_score";
+
+	String OE_EXAM_AUDIT = "oe_exam_audit";
+
+	String OE_FACE_VERIFY = "oe_face_verify";
+
+	/**
+	 * 新数据库表
+	 */
+	String EC_OE_EXAM_RECORD_DATA = "ec_oe_exam_record_data";
+
+	String EC_OE_EXAM_STUDENT = "ec_oe_exam_student";
+
+	String EXAM_RECORD_QUESTIONS = "examRecordQuestions";
+
+	String EC_OE_EXAM_CAPTURE = "ec_oe_exam_capture";
+
+	String EC_OE_EXAM_SCORE = "ec_oe_exam_score";
+
+	String EC_OE_EXAM_AUDIT = "ec_oe_exam_audit";
+
+	String EC_OE_EXAM_FACE_VERIFY = "ec_oe_exam_face_liveness_verify";
+
+	/**
+	 * 课程信息表
+	 */
+	String ECS_CORE_COURSE = "ecs_core_course";
+
+	// 缓存
+	String CACHE_EXAM_STUDENT_PREFIX = "cache_examStudent_";
+
+	String CACHE_PHONE_PREFIX = "cache_phone_";
+
+	String CACHE_STUDENT_PREFIX = "cache_student_";
+
+	String CACHE_ORG_PREFIX = "cache_org_";
+
+	String CACHE_COURSE_PREFIX = "cache_course_";
+
+	String isTrue = "true";
+
+	String isFalse = "false";
+
+	/**
+	 * 考试成绩通知路径前缀
+	 */
+	String OE_EXAM_SCORE_NOTIFY_URL_PREFIX = "oe.examScoreNotify.url.";
+
+	/**
+	 * 考试成绩通知路径传输方法前缀
+	 */
+	String OE_EXAM_SCORE_NOTIFY_URL_HTTP_METHOD_PREFIX = "oe.examScoreNotify.url.httpMethod.";
+
+	// face++ 人脸比对相关错误详情
+	/**
+	 * face++ 人脸比对API并发次数超过上限
+	 */
+	String FACE_COMPARE_CONCURRENCY_LIMIT_EXCEEDED = "CONCURRENCY_LIMIT_EXCEEDED";
+
+	/**
+	 * face++
+	 * api_key没有调用本API的权限,具体原因为:用户自己禁止该api_key调用、管理员禁止该api_key调用、由于账户余额不足禁止调用。
+	 */
+	String FACE_COMPARE_AUTHORIZATION_ERROR = "AUTHORIZATION_ERROR";
+
+	/**
+	 * face++ 下载图片超时
+	 */
+	String FACE_COMPARE_IMAGE_DOWNLOAD_TIMEOUT = "IMAGE_DOWNLOAD_TIMEOUT";
+
+	// 百度活检错误码 http://ai.baidu.com/docs#/Face-Java-SDK/514d7ea4
+	String BAIDU_ERROR_CODE = "error_code";
+
+	String BAIDU_ERROR_MSG = "error_msg";
+
+	String BAIDU_SUCCESS_ERROR_CODE_VALUE = "0";
+
+	/**
+	 * 连接超时或读取数据超时
+	 */
+	String BAIDU_FACELIVENESS_CONNECTION_OR_READ_DATA_TIME_OUT_CODE = "SDK108";
+
+	/**
+	 * 百度在线活体检测QPS超过上限的错误码
+	 */
+	String BAIDU_FACELIVENESS_QPS_LIMIT_EXCEEDED_CODE = "18";
+
+	/**
+	 * 抓拍照片又拍云签名前缀
+	 */
+	String EXAM_CAPTURE_PHOTO_UPYUN_SIGN_PREFIX = "OE_EXAM_CAPTURE_PHOTO_UPYUN_SIGN_";
+
+	// 抓拍照片的又拍云id
+	String CAPTURE_PHOTO_UPYUN_SITEID = "capturePhoto";
+
+	/**
+	 * 处理照片高优先级
+	 */
+	int PROCESS_CAPTURE_HIGH_PRIORITY = 1;
+
+	/**
+	 * 照片处理中状态码
+	 */
+	String CAPTURE_PROCESSING_STATUS_CODE = "101222";
+
+	/**
+	 * 考试未结束状态码
+	 */
+	String EXAM_RECORD_NOT_END_STATUS_CODE = "101333";
+
+	/**
+	 * 交卷处理中
+	 */
+	String PROCESSING_EXAM_RECORD_CODE = "S-101000";
+
+	/**
+	 * 通用成功编码
+	 */
+	String COMMON_SUCCESS_CODE = "000000";
+
+	/**
+	 * 同步人脸比对结果前缀
+	 */
+	String FACE_SYNC_COMPARE_RESULT_PREFIX = "FACE_SYNC_COMPARE_RESULT_";
+
+	/**
+	 * 活体检测方案key
+	 */
+	String IDENTIFICATION_OF_LIVING_BODY_SCHEME_KEY = "IDENTIFICATION_OF_LIVING_BODY_SCHEME";
+
+	/**
+	 * 默认的百度活检阈值
+	 */
+	Double DEFAULT_BAIDU_FACELIVENESS_THRESHOLD=0.39;
+	
+	/**
+	 * 小程序作答文件上传id
+	 */
+	String MINI_PROGRAM_ANWSER_SITEID = "miniProgramAnwser";
+
+	/**
+	 * 证件隐私模式key
+	 */
+	String ID_NUMBER_PRIVATE_MODE_KEY = "ID_NUMBER_PRIVATE_MODE";
+}

+ 369 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/CacheHelper.java

@@ -0,0 +1,369 @@
+package cn.com.qmth.examcloud.support.cache;
+
+import cn.com.qmth.examcloud.support.cache.bean.AppCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.BasePaperCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.CourseCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExamOrgPropertyCacheBean;
+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.ExamRecordPropertyCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExamSettingsCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExamStudentCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ExamStudentPropertyCacheBean;
+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.ExtractConfigPaperCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.OrgCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.OrgPropertyCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.PrivilegeRolesCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.QuestionAnswerCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.QuestionCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.RootOrgCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.RootOrgPrivilegesCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.SmsAssemblyCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.StudentCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.ThirdPartyAccessCacheBean;
+import cn.com.qmth.examcloud.web.cache.HashRedisCacheProcessor;
+import cn.com.qmth.examcloud.web.cache.ObjectRedisCacheProcessor;
+
+/**
+ * 所有缓存对象从此类获取
+ *
+ * @author WANGWEI
+ * @date 2019年5月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class CacheHelper {
+
+	/**
+	 * 获取APP
+	 *
+	 * @param appId
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static AppCacheBean getApp(Long appId) {
+		return ObjectRedisCacheProcessor.get("$_APP:", new Object[]{appId}, AppCacheBean.class);
+	}
+
+	/**
+	 * 获取系统属性<br>
+	 *
+	 * @param propKey
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static SysPropertyCacheBean getSysProperty(String propKey) {
+		return ObjectRedisCacheProcessor.get("$_SYS_PROP:", new Object[]{propKey},
+				SysPropertyCacheBean.class, "EC-CORE-BASIC",
+				"cn.com.qmth.examcloud.core.basic.service.cache.SystemPropertyCache");
+	}
+
+	/**
+	 * 根据考试记录id获取考试信息<br>
+	 *
+	 * @param propKey
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static ExamRecordPropertyCacheBean getExamRecordProperty(Long examRecordDataId) {
+		return ObjectRedisCacheProcessor.get("OE_EXAM_RECORD_PROP:", new Object[]{examRecordDataId},
+				ExamRecordPropertyCacheBean.class, "EC-CORE-OE-STUDENT",
+				"cn.com.qmth.examcloud.core.oe.common.cache.ExamRecordPropertyCache");
+	}
+
+	/**
+	 * 根据短信配置代码获取短信配置详情
+	 *
+	 * @param smsAssemblyCode
+	 *            短信配置代码
+	 * @return
+	 */
+	public static SmsAssemblyCacheBean getSmsAssembly(String smsAssemblyCode) {
+		return ObjectRedisCacheProcessor.get("B_SMS_ASSEMBLY_PROP:", new Object[]{smsAssemblyCode},
+				SmsAssemblyCacheBean.class, "EC-CORE-BASIC",
+				"cn.com.qmth.examcloud.core.basic.service.cache.SmsAssemblyCache");
+	}
+
+	/**
+	 * 获取权限关联的角色集合
+	 *
+	 * @param rootOrgId
+	 * @param privilegeCode
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static PrivilegeRolesCacheBean getPrivilegeRoles(Long rootOrgId, String privilegeCode) {
+		return ObjectRedisCacheProcessor.get("$_PRIV_ROLES:",
+				new Object[]{rootOrgId, privilegeCode}, PrivilegeRolesCacheBean.class,
+				"EC-CORE-BASIC",
+				"cn.com.qmth.examcloud.core.basic.service.cache.PrrivilegeRolesCache");
+	}
+
+	/**
+	 * 获取顶级机构关联的角色集合
+	 *
+	 * @param rootOrgId
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static RootOrgPrivilegesCacheBean getRootOrgPrivileges(Long rootOrgId) {
+		return ObjectRedisCacheProcessor.get("B_ROOT_ORG_PRIVS:", new Object[]{rootOrgId},
+				PrivilegeRolesCacheBean.class, "EC-CORE-BASIC",
+				"cn.com.qmth.examcloud.core.basic.service.cache.RootOrgPrrivilegesCache");
+	}
+
+	/**
+	 * 获取调卷规则信息(题库)
+	 *
+	 * @param examId
+	 * @param courseCode
+	 * @return
+	 */
+	public static ExtractConfigCacheBean getExtractConfig(Long examId, String courseCode) {
+		return ObjectRedisCacheProcessor.get("Q_EXTRACT_CONFIG:", new Object[]{examId, courseCode},
+				ExtractConfigCacheBean.class, "EC-CORE-QUESTION",
+				"cn.com.qmth.examcloud.core.questions.service.cache.ExtractConfigCache");
+	}
+
+	/**
+	 * 获取调卷规则调取的试卷结构(题库)
+	 *
+	 * @param examId
+	 * @param courseCode
+	 * @param groupCode
+	 * @param paperId
+	 * @return
+	 */
+	public static ExtractConfigPaperCacheBean getExtractConfigPaper(Long examId, String courseCode,
+																	String groupCode, String paperId) {
+		return HashRedisCacheProcessor.get("Q_PAPER:EXTRACT_CONFIG_PAPER_",
+				new Object[]{paperId},new Object[]{examId, courseCode, groupCode},
+				ExtractConfigPaperCacheBean.class, "EC-CORE-QUESTION",
+				"cn.com.qmth.examcloud.core.questions.service.cache.ExtractConfigPaperCache");
+	}
+
+	/**
+	 * 获取试卷结构(题库)
+	 *
+	 * @param paperId
+	 * @return
+	 */
+	public static BasePaperCacheBean getBasePaper(String paperId) {
+		return ObjectRedisCacheProcessor.get("Q_PAPER:BASE_", new Object[]{paperId},
+				BasePaperCacheBean.class, "EC-CORE-QUESTION",
+				"cn.com.qmth.examcloud.core.questions.service.cache.BasePaperCache");
+	}
+
+	/**
+	 * 获取试题(题库)
+	 *
+	 * @param examId
+	 * @param courseCode
+	 * @param groupCode
+	 * @param questionId
+	 * @return
+	 */
+	public static QuestionCacheBean getQuestion(Long examId, String courseCode, String groupCode,
+												String questionId) {
+		return HashRedisCacheProcessor.get("Q_QUESTION:",
+				new Object[]{questionId},new Object[]{examId, courseCode, groupCode}, QuestionCacheBean.class,
+				"EC-CORE-QUESTION",
+				"cn.com.qmth.examcloud.core.questions.service.cache.QuestionCache");
+	}
+
+	/**
+	 * 获取试题答案(题库)
+	 *
+	 * @param questionId
+	 * @return
+	 */
+	public static QuestionAnswerCacheBean getQuestionAnswer(String questionId) {
+		return ObjectRedisCacheProcessor.get("Q_QUESTION:ANSWER_", new Object[]{questionId},
+				QuestionAnswerCacheBean.class, "EC-CORE-QUESTION",
+				"cn.com.qmth.examcloud.core.questions.service.cache.QuestionAnswerCache");
+	}
+
+	/**
+	 * 查询课程
+	 *
+	 * @param courseId
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static CourseCacheBean getCourse(Long courseId) {
+		return ObjectRedisCacheProcessor.get("B_COURSE:", new Object[]{courseId},
+				CourseCacheBean.class, "EC-CORE-BASIC",
+				"cn.com.qmth.examcloud.core.basic.service.cache.CourseCache");
+	}
+
+	/**
+	 * 查询学生
+	 *
+	 * @param courseId
+	 * @param rootOrgId
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static StudentCacheBean getStudent(Long studentId) {
+		return ObjectRedisCacheProcessor.get("B_STUDENT:", new Object[]{studentId},
+				StudentCacheBean.class, "EC-CORE-BASIC",
+				"cn.com.qmth.examcloud.core.basic.service.cache.StudentCache");
+	}
+
+	/**
+	 * 查询考试学生配置
+	 *
+	 * @param examId
+	 * @param studentId
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static ExamStudentSettingsCacheBean getExamStudentSettings(Long examId, Long studentId) {
+		return ObjectRedisCacheProcessor.get("E_EXAM_STUDENT_SETTINGS:",
+				new Object[]{examId, studentId}, ExamStudentSettingsCacheBean.class,
+				"EC-CORE-EXAMWORK",
+				"cn.com.qmth.examcloud.core.examwork.service.cache.ExamStudentSettingsCache");
+	}
+
+	/**
+	 * 查询考试学生属性
+	 *
+	 * @author WANGWEI
+	 * @param examId
+	 * @param studentId
+	 * @param key
+	 * @return
+	 */
+	public static ExamStudentPropertyCacheBean getExamStudentProperty(Long examId, Long studentId,
+																	  String key) {
+		return ObjectRedisCacheProcessor.get("E_EXAM_STUDENT_PROP:",
+				new Object[]{examId, studentId, key}, ExamStudentPropertyCacheBean.class,
+				"EC-CORE-EXAMWORK",
+				"cn.com.qmth.examcloud.core.examwork.service.cache.ExamStudentPropertyCache");
+	}
+
+	/**
+	 * 查询考试机构配置
+	 *
+	 * @param examId
+	 * @param orgId
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static ExamOrgSettingsCacheBean getExamOrgSettings(Long examId, Long orgId) {
+		return ObjectRedisCacheProcessor.get("E_EXAM_ORG_SETTINGS:", new Object[]{examId, orgId},
+				ExamOrgSettingsCacheBean.class, "EC-CORE-EXAMWORK",
+				"cn.com.qmth.examcloud.core.examwork.service.cache.ExamOrgSettingsCache");
+	}
+
+	/**
+	 * 查询考试机构属性
+	 *
+	 * @author WANGWEI
+	 * @param examId
+	 * @param orgId
+	 * @param key
+	 * @return
+	 */
+	public static ExamOrgPropertyCacheBean getExamOrgProperty(Long examId, Long orgId, String key) {
+		return ObjectRedisCacheProcessor.get("E_EXAM_ORG_PROP:", new Object[]{examId, orgId, key},
+				ExamOrgPropertyCacheBean.class, "EC-CORE-EXAMWORK",
+				"cn.com.qmth.examcloud.core.examwork.service.cache.ExamOrgPropertyCache");
+	}
+
+	/**
+	 * 查询考试属性
+	 *
+	 * @author WANGWEI
+	 * @param examId
+	 * @param key
+	 * @return
+	 */
+	public static ExamPropertyCacheBean getExamProperty(Long examId, String key) {
+		return ObjectRedisCacheProcessor.get("E_EXAM_PROP:", new Object[]{examId, key},
+				ExamPropertyCacheBean.class, "EC-CORE-EXAMWORK",
+				"cn.com.qmth.examcloud.core.examwork.service.cache.ExamPropertyCache");
+	}
+
+	/**
+	 * 查询考试
+	 *
+	 * @param examId
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static ExamSettingsCacheBean getExamSettings(Long examId) {
+		return ObjectRedisCacheProcessor.get("E_EXAM:", new Object[]{examId},
+				ExamSettingsCacheBean.class, "EC-CORE-EXAMWORK",
+				"cn.com.qmth.examcloud.core.examwork.service.cache.ExamSettingsCache");
+	}
+
+	/**
+	 * 查询机构
+	 *
+	 * @param orgId
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static OrgCacheBean getOrg(Long orgId) {
+		return ObjectRedisCacheProcessor.get("B_ORG:", new Object[]{orgId}, OrgCacheBean.class,
+				"EC-CORE-BASIC", "cn.com.qmth.examcloud.core.basic.service.cache.OrgCache");
+	}
+
+	/**
+	 * 通过域名查询顶级机构
+	 *
+	 * @param domain
+	 * @return
+	 * @author WANGWEI
+	 */
+	public static RootOrgCacheBean getRootOrg(String domain) {
+		return ObjectRedisCacheProcessor.get("B_ROOT_ORG:", new Object[]{domain},
+				RootOrgCacheBean.class, "EC-CORE-BASIC",
+				"cn.com.qmth.examcloud.core.basic.service.cache.RootOrgCache");
+	}
+
+	/**
+	 * 获取机构属性
+	 *
+	 * @author WANGWEI
+	 * @param orgId
+	 * @param key
+	 * @return
+	 */
+	public static OrgPropertyCacheBean getOrgProperty(Long orgId, String key) {
+		return ObjectRedisCacheProcessor.get("B_ORG_PROP:", new Object[]{orgId, key},
+				OrgPropertyCacheBean.class, "EC-CORE-BASIC",
+				"cn.com.qmth.examcloud.core.basic.service.cache.OrgPropertyCache");
+	}
+
+	/**
+	 * 第三方接入信息
+	 *
+	 * @author WANGWEI
+	 * @param rootOrgId
+	 * @param appId
+	 * @return
+	 */
+	public static ThirdPartyAccessCacheBean getThirdPartyAccess(Long rootOrgId, String appId) {
+		return ObjectRedisCacheProcessor.get("B_THIRD_PARTY_ACCESS:",
+				new Object[]{rootOrgId, appId}, ThirdPartyAccessCacheBean.class, "EC-CORE-BASIC",
+				"cn.com.qmth.examcloud.core.basic.service.cache.ThirdPartyAccessCache");
+	}
+
+	/**
+	 * 考生信息
+	 *
+	 * @author WANGWEI
+	 * @param examStudentId
+	 * @return
+	 */
+	public static ExamStudentCacheBean getExamStudent(Long examStudentId) {
+		return ObjectRedisCacheProcessor.get("OE_ES:", new Object[]{examStudentId},
+				ExamStudentCacheBean.class, "EC-CORE-OE-ADMIN",
+				"cn.com.qmth.examcloud.core.oe.admin.service.cache.ExamStudentCache");
+	}
+
+}

+ 67 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/AppCacheBean.java

@@ -0,0 +1,67 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+public class AppCacheBean {
+
+	private Long id;
+
+	/**
+	 * APP服务名(大写字母加"-"组成)
+	 */
+	private String name;
+
+	/**
+	 * APP编码(通常为1到3位大写字母和数字组合)
+	 */
+	private String code;
+
+	/**
+	 * 密钥
+	 */
+	private String secretKey;
+
+	/**
+	 * 接口鉴权时差范围
+	 */
+	private Long timeRange;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public String getSecretKey() {
+		return secretKey;
+	}
+
+	public void setSecretKey(String secretKey) {
+		this.secretKey = secretKey;
+	}
+
+	public Long getTimeRange() {
+		return timeRange;
+	}
+
+	public void setTimeRange(Long timeRange) {
+		this.timeRange = timeRange;
+	}
+
+}

+ 27 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/BasePaperCacheBean.java

@@ -0,0 +1,27 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * @Description 试卷结构缓存实体
+ * @Author lideyin
+ * @Date 2019/7/30 14:36
+ */
+public class BasePaperCacheBean extends RandomCacheBean {
+    private static final long serialVersionUID = 6309635978462557320L;
+
+    /**
+     * 试卷结构
+     */
+    private DefaultPaper defaultPaper;
+
+    public DefaultPaper getDefaultPaper() {
+        return defaultPaper;
+    }
+
+    public void setDefaultPaper(DefaultPaper defaultPaper) {
+        this.defaultPaper = defaultPaper;
+    }
+
+}

+ 73 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/CourseCacheBean.java

@@ -0,0 +1,73 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * 课程缓存实体
+ * @author lideyin
+ * @date 2019年5月21日 下午4:26:19
+ * @description 可根据需求扩展属性
+ */
+public class CourseCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = 3910143938927203635L;
+	private Long id;
+
+	private Long rootOrgId;
+
+	private String code;
+
+	private String name;
+
+	private String level;
+
+	private Boolean enable;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getLevel() {
+		return level;
+	}
+
+	public void setLevel(String level) {
+		this.level = level;
+	}
+
+	public Boolean getEnable() {
+		return enable;
+	}
+
+	public void setEnable(Boolean enable) {
+		this.enable = enable;
+	}
+}

+ 24 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamOrgPropertyCacheBean.java

@@ -0,0 +1,24 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+/**
+ * 机构考试属性
+ *
+ * @author WANGWEI
+ * @date 2019年8月8日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamOrgPropertyCacheBean extends ExamPropertyCacheBean {
+
+	private static final long serialVersionUID = -8521363493722068369L;
+
+	private Long orgId;
+
+	public Long getOrgId() {
+		return orgId;
+	}
+
+	public void setOrgId(Long orgId) {
+		this.orgId = orgId;
+	}
+
+}

+ 14 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamOrgSettingsCacheBean.java

@@ -0,0 +1,14 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+/**
+ * 机构考试配置
+ *
+ * @author WANGWEI
+ * @date 2019年8月8日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamOrgSettingsCacheBean extends ExamSettingsCacheBean {
+
+	private static final long serialVersionUID = -8007446755455123835L;
+
+}

+ 46 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamPropertyCacheBean.java

@@ -0,0 +1,46 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * 机构属性
+ *
+ * @author WANGWEI
+ * @date 2019年8月8日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamPropertyCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = -8521363493722068369L;
+
+	private Long examId;
+
+	private String key;
+
+	private String value;
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public String getKey() {
+		return key;
+	}
+
+	public void setKey(String key) {
+		this.key = key;
+	}
+
+	public String getValue() {
+		return value;
+	}
+
+	public void setValue(String value) {
+		this.value = value;
+	}
+
+}

+ 39 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamRecordPropertyCacheBean.java

@@ -0,0 +1,39 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * 考试记录相关常用属性缓存
+ * 注意:此缓存中只能添加不会发生改变的属性,否则需要埋点同步刷新缓存
+ * @author lideyin
+ * @date 2019年5月21日 下午4:26:19
+ * @description 可根据需求扩展属性
+ */
+public class ExamRecordPropertyCacheBean extends RandomCacheBean {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 4325869001570331457L;
+
+	//考试id
+	private Long examId;
+	//组织机构id
+	private Long orgId;
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public Long getOrgId() {
+		return orgId;
+	}
+
+	public void setOrgId(Long orgId) {
+		this.orgId = orgId;
+	}
+}

+ 198 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamSettingsCacheBean.java

@@ -0,0 +1,198 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import java.util.Date;
+
+import cn.com.qmth.examcloud.api.commons.enums.ExamSpecialSettingsType;
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * 机构考试配置
+ *
+ * @author WANGWEI
+ * @date 2019年8月8日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamSettingsCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = -8007446755455123835L;
+
+	private Long id;
+
+	/**
+	 * 考试编码
+	 */
+	private String code;
+
+	/**
+	 * 顶级机构Id
+	 */
+	private Long rootOrgId;
+
+	/**
+	 * 考试批次开始时间
+	 */
+	private Date beginTime;
+
+	/**
+	 * 考试批次结束时间
+	 */
+	private Date endTime;
+
+	/**
+	 * 考试名称
+	 */
+	private String name;
+
+	/**
+	 * 考试类型
+	 */
+	private String examType;
+
+	/**
+	 * 考试时长
+	 */
+	private Integer duration;
+
+	/**
+	 * 是否可用
+	 */
+	private Boolean enable;
+
+	/**
+	 * 考试备注
+	 */
+	private String remark;
+
+	/**
+	 * 考试次数
+	 */
+	private Long examTimes;
+
+	/**
+	 * 是否禁止考试
+	 */
+	private Boolean examLimit;
+
+	/**
+	 * 开启特殊设置
+	 */
+	private Boolean specialSettingsEnabled;
+
+	/**
+	 * 特殊设置类型
+	 */
+	private ExamSpecialSettingsType specialSettingsType;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public Date getBeginTime() {
+		return beginTime;
+	}
+
+	public void setBeginTime(Date beginTime) {
+		this.beginTime = beginTime;
+	}
+
+	public Date getEndTime() {
+		return endTime;
+	}
+
+	public void setEndTime(Date endTime) {
+		this.endTime = endTime;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getExamType() {
+		return examType;
+	}
+
+	public void setExamType(String examType) {
+		this.examType = examType;
+	}
+
+	public Integer getDuration() {
+		return duration;
+	}
+
+	public void setDuration(Integer duration) {
+		this.duration = duration;
+	}
+
+	public Boolean getEnable() {
+		return enable;
+	}
+
+	public void setEnable(Boolean enable) {
+		this.enable = enable;
+	}
+
+	public String getRemark() {
+		return remark;
+	}
+
+	public void setRemark(String remark) {
+		this.remark = remark;
+	}
+
+	public Long getExamTimes() {
+		return examTimes;
+	}
+
+	public void setExamTimes(Long examTimes) {
+		this.examTimes = examTimes;
+	}
+
+	public Boolean getExamLimit() {
+		return examLimit;
+	}
+
+	public void setExamLimit(Boolean examLimit) {
+		this.examLimit = examLimit;
+	}
+
+	public Boolean getSpecialSettingsEnabled() {
+		return specialSettingsEnabled;
+	}
+
+	public void setSpecialSettingsEnabled(Boolean specialSettingsEnabled) {
+		this.specialSettingsEnabled = specialSettingsEnabled;
+	}
+
+	public ExamSpecialSettingsType getSpecialSettingsType() {
+		return specialSettingsType;
+	}
+
+	public void setSpecialSettingsType(ExamSpecialSettingsType specialSettingsType) {
+		this.specialSettingsType = specialSettingsType;
+	}
+
+}

+ 132 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStudentCacheBean.java

@@ -0,0 +1,132 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * exam student
+ *
+ * @author WANGWEI
+ * @date 2019年12月10日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamStudentCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = -645838123919802377L;
+
+	/**
+	 * 考生ID
+	 */
+	private Long examStudentId;
+
+	/**
+	 * 考试ID
+	 */
+	private Long examId;
+
+	/**
+	 * 学生ID
+	 */
+	private Long studentId;
+
+	/**
+	 * 课程ID
+	 */
+	private Long courseId;
+
+	/**
+	 * 已经考试的次数
+	 */
+	private Integer usedNum;
+
+	/**
+	 * 补加的考试次数
+	 */
+	private Integer extraNum;
+
+	/**
+	 * 是否可用
+	 */
+	private Boolean enable;
+
+	/**
+	 * 试卷类型
+	 */
+	private String paperType;
+
+	/**
+	 * 年级
+	 */
+	private String grade;
+
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public Long getStudentId() {
+		return studentId;
+	}
+
+	public void setStudentId(Long studentId) {
+		this.studentId = studentId;
+	}
+
+	public Long getCourseId() {
+		return courseId;
+	}
+
+	public void setCourseId(Long courseId) {
+		this.courseId = courseId;
+	}
+
+	public Integer getUsedNum() {
+		return usedNum;
+	}
+
+	public void setUsedNum(Integer usedNum) {
+		this.usedNum = usedNum;
+	}
+
+	public Integer getExtraNum() {
+		return extraNum;
+	}
+
+	public void setExtraNum(Integer extraNum) {
+		this.extraNum = extraNum;
+	}
+
+	public Boolean getEnable() {
+		return enable;
+	}
+
+	public void setEnable(Boolean enable) {
+		this.enable = enable;
+	}
+
+	public String getPaperType() {
+		return paperType;
+	}
+
+	public void setPaperType(String paperType) {
+		this.paperType = paperType;
+	}
+
+	public String getGrade() {
+		return grade;
+	}
+
+	public void setGrade(String grade) {
+		this.grade = grade;
+	}
+}

+ 24 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStudentPropertyCacheBean.java

@@ -0,0 +1,24 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+/**
+ * 机构考试属性
+ *
+ * @author WANGWEI
+ * @date 2019年8月8日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamStudentPropertyCacheBean extends ExamPropertyCacheBean {
+
+	private static final long serialVersionUID = -8521363493722068369L;
+
+	private Long studentId;
+
+	public Long getStudentId() {
+		return studentId;
+	}
+
+	public void setStudentId(Long studentId) {
+		this.studentId = studentId;
+	}
+
+}

+ 14 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExamStudentSettingsCacheBean.java

@@ -0,0 +1,14 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+/**
+ * 机构考试配置
+ *
+ * @author WANGWEI
+ * @date 2019年8月8日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamStudentSettingsCacheBean extends ExamSettingsCacheBean {
+
+	private static final long serialVersionUID = -8007446755455123835L;
+
+}

+ 93 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExtractConfigCacheBean.java

@@ -0,0 +1,93 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+import java.util.List;
+
+/**
+ * @Description 调卷规则信息
+ * @Author lideyin
+ * @Date 2019/7/30 14:36
+ */
+public class ExtractConfigCacheBean extends RandomCacheBean {
+    private static final long serialVersionUID = 6309635978462557320L;
+
+    /**
+     * ID
+     */
+    protected String id;
+
+    /**
+     * 考试ID
+     */
+    private Long examId;
+
+    /**
+     * 课程代码
+     */
+    private String courseCode;
+
+    /**
+     * 抽取试卷对象集合
+     */
+    private List<ExtractConfigDetailCacheBean> details;
+
+    /**
+     * 是否小题乱序
+     */
+    Boolean sortQuestionOrder;
+
+    /**
+     * 是否选项乱序
+     */
+    Boolean sortOptionOrder;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public Long getExamId() {
+        return examId;
+    }
+
+    public void setExamId(Long examId) {
+        this.examId = examId;
+    }
+
+    public String getCourseCode() {
+        return courseCode;
+    }
+
+    public void setCourseCode(String courseCode) {
+        this.courseCode = courseCode;
+    }
+
+    public List<ExtractConfigDetailCacheBean> getDetails() {
+        return details;
+    }
+
+    public void setDetails(List<ExtractConfigDetailCacheBean> details) {
+        this.details = details;
+    }
+
+    public Boolean getSortQuestionOrder() {
+        return sortQuestionOrder;
+    }
+
+    public void setSortQuestionOrder(Boolean sortQuestionOrder) {
+        this.sortQuestionOrder = sortQuestionOrder;
+    }
+
+    public Boolean getSortOptionOrder() {
+        return sortOptionOrder;
+    }
+
+    public void setSortOptionOrder(Boolean sortOptionOrder) {
+        this.sortOptionOrder = sortOptionOrder;
+    }
+
+}

+ 63 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExtractConfigDetailCacheBean.java

@@ -0,0 +1,63 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * @Description 调卷规则的详细抽取试卷规则
+ * @Author lideyin
+ * @Date 2019/7/30 14:36
+ */
+public class ExtractConfigDetailCacheBean extends RandomCacheBean {
+    private static final long serialVersionUID = 6309635978462557320L;
+
+    /**
+     * 试卷类型
+     */
+    private String groupCode;
+
+    /**
+     * 关联试卷
+     */
+    private String paperId;
+
+    /**
+     * 抽取权重比例
+     * 整数类型 例如:70%  数据库里存70
+     */
+    private Integer weight;
+
+    public ExtractConfigDetailCacheBean() {
+
+    }
+
+    public ExtractConfigDetailCacheBean(String groupCode, String paperId, Integer weight) {
+        this.groupCode = groupCode;
+        this.paperId = paperId;
+        this.weight = weight;
+    }
+
+    public String getGroupCode() {
+        return groupCode;
+    }
+
+    public void setGroupCode(String groupCode) {
+        this.groupCode = groupCode;
+    }
+
+    public String getPaperId() {
+        return paperId;
+    }
+
+    public void setPaperId(String paperId) {
+        this.paperId = paperId;
+    }
+
+    public Integer getWeight() {
+        return weight;
+    }
+
+    public void setWeight(Integer weight) {
+        this.weight = weight;
+    }
+
+}

+ 27 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ExtractConfigPaperCacheBean.java

@@ -0,0 +1,27 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * @Description 调卷规则的试卷结构缓存实体
+ * @Author lideyin
+ * @Date 2019/7/30 14:36
+ */
+public class ExtractConfigPaperCacheBean extends RandomCacheBean {
+    private static final long serialVersionUID = 6309635978462557320L;
+
+    /**
+     * 试卷结构
+     */
+    private DefaultPaper defaultPaper;
+
+    public DefaultPaper getDefaultPaper() {
+        return defaultPaper;
+    }
+
+    public void setDefaultPaper(DefaultPaper defaultPaper) {
+        this.defaultPaper = defaultPaper;
+    }
+
+}

+ 84 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/OrgCacheBean.java

@@ -0,0 +1,84 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * 
+ * @author WANG
+ *
+ */
+public class OrgCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = -4045967342902486442L;
+
+	private Long id;
+
+	private Long rootId;
+
+	private Long parentId;
+
+	private String name;
+
+	private Boolean enable;
+
+	private String code;
+
+	private String domainName;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public Long getRootId() {
+		return rootId;
+	}
+
+	public void setRootId(Long rootId) {
+		this.rootId = rootId;
+	}
+
+	public Long getParentId() {
+		return parentId;
+	}
+
+	public void setParentId(Long parentId) {
+		this.parentId = parentId;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public Boolean getEnable() {
+		return enable;
+	}
+
+	public void setEnable(Boolean enable) {
+		this.enable = enable;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public String getDomainName() {
+		return domainName;
+	}
+
+	public void setDomainName(String domainName) {
+		this.domainName = domainName;
+	}
+
+}

+ 39 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/OrgPropertyCacheBean.java

@@ -0,0 +1,39 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+public class OrgPropertyCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = -8320620858676945769L;
+
+	private Long orgId;
+
+	private String key;
+
+	private String value;
+
+	public Long getOrgId() {
+		return orgId;
+	}
+
+	public void setOrgId(Long orgId) {
+		this.orgId = orgId;
+	}
+
+	public String getKey() {
+		return key;
+	}
+
+	public void setKey(String key) {
+		this.key = key;
+	}
+
+	public String getValue() {
+		return value;
+	}
+
+	public void setValue(String value) {
+		this.value = value;
+	}
+
+}

+ 41 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/PrivilegeRolesCacheBean.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import java.util.Set;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+public class PrivilegeRolesCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = 7517123394296666833L;
+
+	private Long rootOrgId;
+
+	private Set<Long> roleIdList;
+
+	private Set<String> roleCodeList;
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public Set<Long> getRoleIdList() {
+		return roleIdList;
+	}
+
+	public void setRoleIdList(Set<Long> roleIdList) {
+		this.roleIdList = roleIdList;
+	}
+
+	public Set<String> getRoleCodeList() {
+		return roleCodeList;
+	}
+
+	public void setRoleCodeList(Set<String> roleCodeList) {
+		this.roleCodeList = roleCodeList;
+	}
+
+}

+ 41 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/QuestionAnswerCacheBean.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+import java.util.List;
+
+/**
+ * @Description 试题答案缓存实体
+ * @Author lideyin
+ * @Date 2019/7/30 14:36
+ */
+public class QuestionAnswerCacheBean extends RandomCacheBean {
+    private static final long serialVersionUID = 6309635978462557320L;
+
+    /**
+     * 试题ID
+     */
+    private String questionId;
+
+    /**
+     * 正确答案
+     */
+    private List<String> rightAnswers;
+
+    public String getQuestionId() {
+        return questionId;
+    }
+
+    public void setQuestionId(String questionId) {
+        this.questionId = questionId;
+    }
+
+    public List<String> getRightAnswers() {
+        return rightAnswers;
+    }
+
+    public void setRightAnswers(List<String> rightAnswers) {
+        this.rightAnswers = rightAnswers;
+    }
+
+}

+ 27 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/QuestionCacheBean.java

@@ -0,0 +1,27 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestion;
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * @Description 试题缓存实体
+ * @Author lideyin
+ * @Date 2019/7/30 14:36
+ */
+public class QuestionCacheBean extends RandomCacheBean {
+    private static final long serialVersionUID = 6309635978462557320L;
+
+    /**
+     * 试题
+     */
+    private DefaultQuestion defaultQuestion;
+
+    public DefaultQuestion getDefaultQuestion() {
+        return defaultQuestion;
+    }
+
+    public void setDefaultQuestion(DefaultQuestion defaultQuestion) {
+        this.defaultQuestion = defaultQuestion;
+    }
+
+}

+ 84 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/RootOrgCacheBean.java

@@ -0,0 +1,84 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * 
+ * @author WANG
+ *
+ */
+public class RootOrgCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = -4045967342902486442L;
+
+	private Long id;
+
+	private Long rootId;
+
+	private Long parentId;
+
+	private String name;
+
+	private Boolean enable;
+
+	private String code;
+
+	private String domainName;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public Long getRootId() {
+		return rootId;
+	}
+
+	public void setRootId(Long rootId) {
+		this.rootId = rootId;
+	}
+
+	public Long getParentId() {
+		return parentId;
+	}
+
+	public void setParentId(Long parentId) {
+		this.parentId = parentId;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public Boolean getEnable() {
+		return enable;
+	}
+
+	public void setEnable(Boolean enable) {
+		this.enable = enable;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public String getDomainName() {
+		return domainName;
+	}
+
+	public void setDomainName(String domainName) {
+		this.domainName = domainName;
+	}
+
+}

+ 41 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/RootOrgPrivilegesCacheBean.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import java.util.Set;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+public class RootOrgPrivilegesCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = 7517123394296666833L;
+
+	private Long rootOrgId;
+
+	private Set<Long> privilegeIdList;
+
+	private Set<String> privilegeCodeList;
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public Set<Long> getPrivilegeIdList() {
+		return privilegeIdList;
+	}
+
+	public void setPrivilegeIdList(Set<Long> privilegeIdList) {
+		this.privilegeIdList = privilegeIdList;
+	}
+
+	public Set<String> getPrivilegeCodeList() {
+		return privilegeCodeList;
+	}
+
+	public void setPrivilegeCodeList(Set<String> privilegeCodeList) {
+		this.privilegeCodeList = privilegeCodeList;
+	}
+
+}

+ 206 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/SmsAssemblyCacheBean.java

@@ -0,0 +1,206 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * 短信发送相关缓存实体
+ */
+public class SmsAssemblyCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = 6227721820083892418L;
+
+	/**
+	 * 短信装配ID
+	 */
+	private Long id;
+
+	/**
+	 * 短信装配编码
+	 */
+	private String code;
+
+	/**
+	 * 短信装配名称
+	 */
+	private String name;
+
+	/**
+	 * 短信样例
+	 */
+	private String example;
+
+	/**
+	 * 短信模板
+	 */
+	private String template;
+
+	/**
+	 * 扩展属性1
+	 */
+	private String ext1;
+
+	/**
+	 * 扩展属性2
+	 */
+	private String ext2;
+
+	/**
+	 * 扩展属性3
+	 */
+	private String ext3;
+
+	/**
+	 * 扩展属性4
+	 */
+	private String ext4;
+
+	/**
+	 * 扩展属性5
+	 */
+	private String ext5;
+
+	/**
+	 * 扩展属性6
+	 */
+	private String ext6;
+
+	/**
+	 * 扩展属性7
+	 */
+	private String ext7;
+
+	/**
+	 * 扩展属性8
+	 */
+	private String ext8;
+
+	/**
+	 * 扩展属性9
+	 */
+	private String ext9;
+
+	/**
+	 * 扩展属性10
+	 */
+	private String ext10;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getExample() {
+		return example;
+	}
+
+	public void setExample(String example) {
+		this.example = example;
+	}
+
+	public String getTemplate() {
+		return template;
+	}
+
+	public void setTemplate(String template) {
+		this.template = template;
+	}
+
+	public String getExt1() {
+		return ext1;
+	}
+
+	public void setExt1(String ext1) {
+		this.ext1 = ext1;
+	}
+
+	public String getExt2() {
+		return ext2;
+	}
+
+	public void setExt2(String ext2) {
+		this.ext2 = ext2;
+	}
+
+	public String getExt3() {
+		return ext3;
+	}
+
+	public void setExt3(String ext3) {
+		this.ext3 = ext3;
+	}
+
+	public String getExt4() {
+		return ext4;
+	}
+
+	public void setExt4(String ext4) {
+		this.ext4 = ext4;
+	}
+
+	public String getExt5() {
+		return ext5;
+	}
+
+	public void setExt5(String ext5) {
+		this.ext5 = ext5;
+	}
+
+	public String getExt6() {
+		return ext6;
+	}
+
+	public void setExt6(String ext6) {
+		this.ext6 = ext6;
+	}
+
+	public String getExt7() {
+		return ext7;
+	}
+
+	public void setExt7(String ext7) {
+		this.ext7 = ext7;
+	}
+
+	public String getExt8() {
+		return ext8;
+	}
+
+	public void setExt8(String ext8) {
+		this.ext8 = ext8;
+	}
+
+	public String getExt9() {
+		return ext9;
+	}
+
+	public void setExt9(String ext9) {
+		this.ext9 = ext9;
+	}
+
+	public String getExt10() {
+		return ext10;
+	}
+
+	public void setExt10(String ext10) {
+		this.ext10 = ext10;
+	}
+}

+ 143 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/StudentCacheBean.java

@@ -0,0 +1,143 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import java.util.List;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+public class StudentCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = 2870680150437521145L;
+
+	private Long id;
+
+	private String name;
+
+	private String identityNumber;
+
+	private String photoPath;
+
+	private String remark;
+
+	private Boolean enable;
+
+	private Long orgId;
+
+	private Long rootOrgId;
+
+	/**
+	 * 手机号码
+	 */
+	private String phoneNumber;
+
+	/**
+	 * 安全手机号码(用于登录)
+	 */
+	private String securityPhone;
+
+	/**
+	 * 学号集合
+	 */
+	private List<String> studentCodeList;
+
+	/**
+	 * face++参数
+	 */
+	private String faceToken;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getIdentityNumber() {
+		return identityNumber;
+	}
+
+	public void setIdentityNumber(String identityNumber) {
+		this.identityNumber = identityNumber;
+	}
+
+	public String getPhotoPath() {
+		return photoPath;
+	}
+
+	public void setPhotoPath(String photoPath) {
+		this.photoPath = photoPath;
+	}
+
+	public String getRemark() {
+		return remark;
+	}
+
+	public void setRemark(String remark) {
+		this.remark = remark;
+	}
+
+	public Boolean getEnable() {
+		return enable;
+	}
+
+	public void setEnable(Boolean enable) {
+		this.enable = enable;
+	}
+
+	public Long getOrgId() {
+		return orgId;
+	}
+
+	public void setOrgId(Long orgId) {
+		this.orgId = orgId;
+	}
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public String getPhoneNumber() {
+		return phoneNumber;
+	}
+
+	public void setPhoneNumber(String phoneNumber) {
+		this.phoneNumber = phoneNumber;
+	}
+
+	public String getSecurityPhone() {
+		return securityPhone;
+	}
+
+	public void setSecurityPhone(String securityPhone) {
+		this.securityPhone = securityPhone;
+	}
+
+	public List<String> getStudentCodeList() {
+		return studentCodeList;
+	}
+
+	public void setStudentCodeList(List<String> studentCodeList) {
+		this.studentCodeList = studentCodeList;
+	}
+
+	public String getFaceToken() {
+		return faceToken;
+	}
+
+	public void setFaceToken(String faceToken) {
+		this.faceToken = faceToken;
+	}
+
+}

+ 30 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/SysPropertyCacheBean.java

@@ -0,0 +1,30 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.api.commons.enums.BasicDataType;
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+public class SysPropertyCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = -8320620858676945769L;
+
+	private BasicDataType valueType;
+
+	private Object value;
+
+	public BasicDataType getValueType() {
+		return valueType;
+	}
+
+	public void setValueType(BasicDataType valueType) {
+		this.valueType = valueType;
+	}
+
+	public Object getValue() {
+		return value;
+	}
+
+	public void setValue(Object value) {
+		this.value = value;
+	}
+
+}

+ 68 - 0
src/main/java/cn/com/qmth/examcloud/support/cache/bean/ThirdPartyAccessCacheBean.java

@@ -0,0 +1,68 @@
+package cn.com.qmth.examcloud.support.cache.bean;
+
+import cn.com.qmth.examcloud.web.cache.RandomCacheBean;
+
+/**
+ * 第三方接入信息
+ *
+ * @author WANGWEI
+ * @date 2018年7月5日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ThirdPartyAccessCacheBean extends RandomCacheBean {
+
+	private static final long serialVersionUID = 9212710623859419481L;
+
+	/**
+	 * 顶级机构ID
+	 */
+	private Long rootOrgId;
+
+	/**
+	 * 应用ID
+	 */
+	private String appId;
+
+	/**
+	 * 密钥
+	 */
+	private String secretKey;
+
+	/**
+	 * 时间差范围
+	 */
+	private Long timeRange;
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public String getAppId() {
+		return appId;
+	}
+
+	public void setAppId(String appId) {
+		this.appId = appId;
+	}
+
+	public String getSecretKey() {
+		return secretKey;
+	}
+
+	public void setSecretKey(String secretKey) {
+		this.secretKey = secretKey;
+	}
+
+	public Long getTimeRange() {
+		return timeRange;
+	}
+
+	public void setTimeRange(Long timeRange) {
+		this.timeRange = timeRange;
+	}
+
+}

+ 6 - 0
src/main/java/cn/com/qmth/examcloud/support/enums/BlockType.java

@@ -0,0 +1,6 @@
+package cn.com.qmth.examcloud.support.enums;
+
+public enum BlockType {
+
+    text, image, audio;
+}

+ 21 - 0
src/main/java/cn/com/qmth/examcloud/support/enums/DataCategory.java

@@ -0,0 +1,21 @@
+package cn.com.qmth.examcloud.support.enums;
+
+
+/**
+ * @Description 数据分类
+ * @Author lideyin
+ * @Date 2020/3/12 16:43
+ * @Version 1.0
+ */
+public enum DataCategory {
+    /**
+     * 系统数据
+     */
+    SYSTEM,
+
+    /**
+     * 自定义数据
+     */
+    CUSTOM
+
+}

+ 19 - 0
src/main/java/cn/com/qmth/examcloud/support/enums/DbType.java

@@ -0,0 +1,19 @@
+package cn.com.qmth.examcloud.support.enums;
+
+/**
+ * 数据类型
+ */
+public enum DbType {
+    /**
+     * 缓存
+     */
+    CACHE,
+    /**
+     * 表格
+     */
+    TABLE,
+    /**
+     * 文档集合
+     */
+    COLLECTION
+}

+ 69 - 0
src/main/java/cn/com/qmth/examcloud/support/enums/ExamProperties.java

@@ -0,0 +1,69 @@
+package cn.com.qmth.examcloud.support.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("是否开放微信小程序作答"),
+	APP_EXAM_ENABLED("是否开放app考试");
+
+	private ExamProperties(String desc){
+		this.desc = desc;
+	}
+
+	private String desc;
+
+	public String getDesc() {
+		return desc;
+	}
+
+	public void setDesc(String desc) {
+		this.desc = desc;
+	}
+
+}

+ 51 - 0
src/main/java/cn/com/qmth/examcloud/support/enums/ExamRecordStatus.java

@@ -0,0 +1,51 @@
+package cn.com.qmth.examcloud.support.enums;
+
+/**
+ * 考试记录状态
+ *
+ * @author xudengqi
+ * 2016年8月23日
+ */
+public enum ExamRecordStatus {
+    /**
+     * 考试中
+     */
+    EXAM_ING,
+    /**
+     * 手工交卷处理中
+     */
+    EXAM_HAND_IN,
+    /**
+     * 系统交卷处理中
+     */
+    EXAM_AUTO_HAND_IN,
+    /**
+     * 考试结束
+     */
+    EXAM_END,
+
+    /**
+     * 考试过期
+     */
+    EXAM_OVERDUE,
+
+    /**
+     * 考试无效/作废
+     */
+    EXAM_INVALID,
+    
+    /**
+     * 考试异常数据
+     */
+    EXAM_ERROR;
+
+    public static ExamRecordStatus getByName(String name) {
+        for (ExamRecordStatus status : values()) {
+            if (status.name().equals(name)) {
+                return status;
+            }
+        }
+        return null;
+    }
+
+}

+ 41 - 0
src/main/java/cn/com/qmth/examcloud/support/enums/FaceBiopsyScheme.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.support.enums;
+
+/**
+ * @Description 活体检测方案枚举
+ * @Author lideyin
+ * @Date 2019/11/6 15:03
+ * @Version 1.0
+ */
+public enum FaceBiopsyScheme {
+    /**
+     * FaceID活体检测方案(即旧活体检测方案)
+     */
+    FACE_ID("S1", "FaceID活体检测方案"),
+    /**
+     * 新活体检测方案(暂时无法给出具体命名,以后有需要再改动)
+     */
+    NEW("S2", "新活体检测方案");
+    private String code;
+    private String desc;
+
+    FaceBiopsyScheme(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    /**
+     * 获取活检方案代码
+     * @return
+     */
+    public String getCode(){
+        return this.code;
+    }
+
+    /**
+     * 获取活检方案描述
+     * @return
+     */
+    public String getDesc(){
+        return this.desc;
+    }
+}

+ 22 - 0
src/main/java/cn/com/qmth/examcloud/support/enums/FileAnswerAcknowledgeStatus.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.support.enums;
+
+/**
+ * 文件作答的响应状态
+ */
+public enum FileAnswerAcknowledgeStatus {
+    UNCONFIRMED("UNCONFIRMED", "未确认"),
+    CONFIRMED("CONFIRMED", "已确认"),
+    DISCARDED("DISCARDED", "已弃用");
+
+    private String code;
+
+    private String desc;
+
+    /**
+     * 构造函数
+     */
+    FileAnswerAcknowledgeStatus(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+}

+ 20 - 0
src/main/java/cn/com/qmth/examcloud/support/enums/HandInExamType.java

@@ -0,0 +1,20 @@
+package cn.com.qmth.examcloud.support.enums;
+
+
+/**
+ * @Description 交卷类型
+ * @Author lideyin
+ * @Date 2019/8/1 18:07
+ * @Version 1.0
+ */
+public enum HandInExamType {
+    /**
+     * 手工交卷
+     */
+    MANUAL,
+    /**
+     * 自动交卷
+     */
+    AUTO
+
+}

+ 48 - 0
src/main/java/cn/com/qmth/examcloud/support/enums/IsSuccess.java

@@ -0,0 +1,48 @@
+package cn.com.qmth.examcloud.support.enums;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author  	chenken
+ * @date    	2018年2月26日 下午3:13:05
+ * @company 	QMTH
+ * @description 成功或失败
+ */
+public enum IsSuccess {
+	/**
+	 * 成功
+	 */
+	SUCCESS("成功"),
+	/**
+	 * 失败
+	 */
+	FAILED("失败");
+	
+	private String desc;
+	
+	public static IsSuccess strToEnum(String name){
+		if(StringUtils.isBlank(name)){
+			return null;
+		}
+		for(IsSuccess isSuccess:IsSuccess.values()){
+			if(name.equals(isSuccess.name())){
+				return isSuccess;
+			}
+		}
+		
+		return null;
+	}
+	
+	private IsSuccess(String desc){
+		this.desc = desc;
+	}
+	
+	public String getDesc() {
+		return desc;
+	}
+
+	public void setDesc(String desc) {
+		this.desc = desc;
+	}
+}
+

+ 21 - 0
src/main/java/cn/com/qmth/examcloud/support/enums/SyncStatus.java

@@ -0,0 +1,21 @@
+package cn.com.qmth.examcloud.support.enums;
+
+
+/**
+ * @Description 同步状态
+ * @Author lideyin
+ * @Date 2019/12/18 11:24
+ * @Version 1.0
+ */
+public enum SyncStatus {
+    /**
+     * 未同步
+     */
+    UNSYNC,
+
+    /**
+     * 已同步
+     */
+    SYNCED
+
+}

+ 63 - 0
src/main/java/cn/com/qmth/examcloud/support/examing/ExamBoss.java

@@ -0,0 +1,63 @@
+package cn.com.qmth.examcloud.support.examing;
+
+import java.util.List;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 考试控制属性<br>
+ * ExamBoss存储于redis,无过期时间<br>
+ * 开考时,startCount++<br>
+ * 数据从student同步至admin后,endCount++<br>
+ * startCount== endCount 时,清理 {@link ExamBoss}
+ *
+ * @author WANGWEI
+ * @date 2019年12月16日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamBoss implements JsonSerializable {
+
+	private static final long serialVersionUID = -3499412275759440367L;
+
+	/**
+	 * 开考次数
+	 */
+	private int startCount;
+
+	/**
+	 * 完结次数
+	 */
+	private int endCount;
+	
+	/**
+	 * 未同步的考试记录id
+	 */
+	private List<Long> examRecordDataIds;
+
+	public int getStartCount() {
+		return startCount;
+	}
+
+	public void setStartCount(int startCount) {
+		this.startCount = startCount;
+	}
+
+	public int getEndCount() {
+		return endCount;
+	}
+
+	public void setEndCount(int endCount) {
+		this.endCount = endCount;
+	}
+
+    
+    public List<Long> getExamRecordDataIds() {
+        return examRecordDataIds;
+    }
+
+    
+    public void setExamRecordDataIds(List<Long> examRecordDataIds) {
+        this.examRecordDataIds = examRecordDataIds;
+    }
+
+}

+ 100 - 0
src/main/java/cn/com/qmth/examcloud/support/examing/ExamFileAnswer.java

@@ -0,0 +1,100 @@
+package cn.com.qmth.examcloud.support.examing;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import cn.com.qmth.examcloud.support.enums.FileAnswerAcknowledgeStatus;
+
+/**
+ * @Description 文件作答记录(考完则清理)
+ * @Author lideyin
+ * @Date 2019/12/13 16:22
+ * @Version 1.0
+ */
+public class ExamFileAnswer implements JsonSerializable {
+
+	private static final long serialVersionUID = -7441792839367736076L;
+	/**
+	 * 主键id
+	 */
+	private Long id;
+	/**
+	 * 考试记录id
+	 */
+	private Long examRecordDataId;
+	/**
+	 * 考生id
+	 */
+	private Long examStudentId;
+	/**
+	 * 小题序号
+	 */
+	private Integer questionOrder;
+	/**
+	 * 文件路径
+	 */
+	private String filePath;
+	/**
+	 * 文件作答的响应状态
+	 */
+	private FileAnswerAcknowledgeStatus status;
+	/**
+	 * 传输文件的类型(PIC | AUDIO)
+	 */
+	private String transferFileType;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+
+
+	public Integer getQuestionOrder() {
+		return questionOrder;
+	}
+
+	public void setQuestionOrder(Integer questionOrder) {
+		this.questionOrder = questionOrder;
+	}
+
+	public String getFilePath() {
+		return filePath;
+	}
+
+	public void setFilePath(String filePath) {
+		this.filePath = filePath;
+	}
+
+	public FileAnswerAcknowledgeStatus getStatus() {
+		return status;
+	}
+
+	public void setStatus(FileAnswerAcknowledgeStatus status) {
+		this.status = status;
+	}
+
+	public String getTransferFileType() {
+		return transferFileType;
+	}
+
+	public void setTransferFileType(String transferFileType) {
+		this.transferFileType = transferFileType;
+	}
+}

+ 229 - 0
src/main/java/cn/com/qmth/examcloud/support/examing/ExamQuestion.java

@@ -0,0 +1,229 @@
+package cn.com.qmth.examcloud.support.examing;
+
+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;
+	
+	/**
+     * 作答记录是否在mongo
+     */
+    private Boolean isInMongo;
+    
+    /**
+     * 作答记录在mongo中的id
+     */
+    private String examQuestionTempId;
+	
+	/**
+	 * 考试记录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;
+	}
+    
+    public String getExamQuestionTempId() {
+        return examQuestionTempId;
+    }
+    
+    public void setExamQuestionTempId(String examQuestionTempId) {
+        this.examQuestionTempId = examQuestionTempId;
+    }
+    
+    public Boolean getIsInMongo() {
+        return isInMongo;
+    }
+    
+    public void setIsInMongo(Boolean isInMongo) {
+        this.isInMongo = isInMongo;
+    }
+
+	
+	
+}

+ 544 - 0
src/main/java/cn/com/qmth/examcloud/support/examing/ExamRecordData.java

@@ -0,0 +1,544 @@
+package cn.com.qmth.examcloud.support.examing;
+
+import java.util.Date;
+
+import cn.com.qmth.examcloud.api.commons.enums.ExamType;
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import cn.com.qmth.examcloud.support.enums.ExamRecordStatus;
+import cn.com.qmth.examcloud.support.enums.HandInExamType;
+import cn.com.qmth.examcloud.support.enums.IsSuccess;
+import cn.com.qmth.examcloud.support.enums.SyncStatus;
+
+public class ExamRecordData implements JsonSerializable {
+
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 3881189287358373638L;
+
+    /**
+     * 主键
+     */
+    private Long id;
+
+    /**
+     * 考试ID
+     */
+    private Long examId;
+
+    /**
+     * 考试类型
+     */
+    private ExamType examType;
+
+    /**
+     * 考生ID
+     */
+    private Long examStudentId;
+
+    /**
+     * 学生ID
+     */
+    private Long studentId;
+
+    /**
+     * 课程ID
+     */
+    private Long courseId;
+
+    /**
+     * 学习中心ID
+     */
+    private Long orgId;
+
+    /**
+     * 顶级机构ID
+     */
+    private Long rootOrgId;
+
+    /**
+     * 基础试卷ID
+     */
+    private String basePaperId;
+
+    /**
+     * 试卷类型
+     */
+    private String paperType;
+
+    /**
+     * 考试开始时间
+     */
+    private Date startTime;
+
+    /**
+     * 考试结束时间
+     */
+    private Date endTime;
+
+    /**
+     * 考试时长
+     */
+    private Long usedExamTime;
+
+    /**
+     * 是否断点续考
+     */
+    private Boolean isContinued;
+
+    /**
+     * 断点续考次数
+     */
+    private Integer continuedCount;
+
+    /**
+     * 是否达到最大断点限制
+     */
+    private Boolean isExceed;
+
+    /**
+     * 抓拍比对成功次数
+     */
+    private Integer faceSuccessCount;
+
+    /**
+     * 抓拍比对失败次数
+     */
+    private Integer faceFailedCount;
+
+    /**
+     * 抓拍存在陌生人的次数
+     */
+    private Integer faceStrangerCount;
+
+    /**
+     * 抓拍比对总次数
+     */
+    private Integer faceTotalCount;
+
+    // 考试记录状态
+    private ExamRecordStatus examRecordStatus;
+
+    /**
+     * 交卷类型
+     */
+    private HandInExamType handInExamType;
+
+    /**
+     *
+     * 活体检测结果
+     */
+    private IsSuccess faceVerifyResult;
+
+    /**
+     * 考试被清理时间
+     */
+    private Date cleanTime;
+
+    /**
+     * 是否异常数据
+     */
+    private Boolean isWarn;
+    /**
+     * 是否被审核过
+     */
+    private Boolean isAudit;
+    /**
+     * 是否违纪
+     */
+    private Boolean isIllegality;
+
+    /**
+     * 总分
+     */
+    private Double totalScore;
+
+    /**
+     * 客观题得分总分
+     */
+    private Double objectiveScore;
+
+    /**
+     * 客观题答对的比率
+     * (客观题答对的题数/客观题总题数)*100  取2位小数
+     */
+    private Double objectiveAccuracy;
+
+    /**
+     * 主观题得分总分
+     */
+    private Double subjectiveScore;
+
+    /**
+     * 答题正确率
+     */
+    private Double succPercent;
+
+    /**
+     * 抓拍比对通过率
+     */
+    private Double faceSuccessPercent;
+
+    /**
+     * 百度人脸活体检测通过率
+     */
+    private Double baiduFaceLivenessSuccessPercent;
+
+    /**
+     * 数据同步状态
+     */
+    private SyncStatus syncStatus;
+    
+    /**
+     * 试卷题目数量(校验提交答案的order)
+     */
+    private Integer questionCount;
+    
+    /**
+     * 是否是全客观题卷  1:是   0:否
+     */
+    private Boolean isAllObjectivePaper;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getExamId() {
+        return examId;
+    }
+
+    public void setExamId(Long examId) {
+        this.examId = examId;
+    }
+
+    public ExamType getExamType() {
+        return examType;
+    }
+
+    public void setExamType(ExamType examType) {
+        this.examType = examType;
+    }
+
+    public Long getExamStudentId() {
+        return examStudentId;
+    }
+
+    public void setExamStudentId(Long examStudentId) {
+        this.examStudentId = examStudentId;
+    }
+
+    public Long getStudentId() {
+        return studentId;
+    }
+
+    public void setStudentId(Long studentId) {
+        this.studentId = studentId;
+    }
+
+    public Long getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Long courseId) {
+        this.courseId = courseId;
+    }
+
+    public Long getOrgId() {
+        return orgId;
+    }
+
+    public void setOrgId(Long orgId) {
+        this.orgId = orgId;
+    }
+
+    public Long getRootOrgId() {
+        return rootOrgId;
+    }
+
+    public void setRootOrgId(Long rootOrgId) {
+        this.rootOrgId = rootOrgId;
+    }
+
+    public String getBasePaperId() {
+        return basePaperId;
+    }
+
+    public void setBasePaperId(String basePaperId) {
+        this.basePaperId = basePaperId;
+    }
+
+    public String getPaperType() {
+        return paperType;
+    }
+
+    public void setPaperType(String paperType) {
+        this.paperType = paperType;
+    }
+
+    public Date getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(Date startTime) {
+        this.startTime = startTime;
+    }
+
+    public Date getEndTime() {
+        return endTime;
+    }
+
+    public void setEndTime(Date endTime) {
+        this.endTime = endTime;
+    }
+
+    public Long getUsedExamTime() {
+        return usedExamTime;
+    }
+
+    public void setUsedExamTime(Long usedExamTime) {
+        this.usedExamTime = usedExamTime;
+    }
+
+    public Boolean getIsContinued() {
+        return isContinued;
+    }
+
+    public void setIsContinued(Boolean isContinued) {
+        this.isContinued = isContinued;
+    }
+
+    public Integer getContinuedCount() {
+        return continuedCount;
+    }
+
+    public void setContinuedCount(Integer continuedCount) {
+        this.continuedCount = continuedCount;
+    }
+
+    public Integer getFaceSuccessCount() {
+        return faceSuccessCount;
+    }
+
+    public void setFaceSuccessCount(Integer faceSuccessCount) {
+        this.faceSuccessCount = faceSuccessCount;
+    }
+
+    public Integer getFaceFailedCount() {
+        return faceFailedCount;
+    }
+
+    public void setFaceFailedCount(Integer faceFailedCount) {
+        this.faceFailedCount = faceFailedCount;
+    }
+
+    public Integer getFaceStrangerCount() {
+        return faceStrangerCount;
+    }
+
+    public void setFaceStrangerCount(Integer faceStrangerCount) {
+        this.faceStrangerCount = faceStrangerCount;
+    }
+
+    public Integer getFaceTotalCount() {
+        return faceTotalCount;
+    }
+
+    public void setFaceTotalCount(Integer faceTotalCount) {
+        this.faceTotalCount = faceTotalCount;
+    }
+
+    public Boolean getIsExceed() {
+        return isExceed;
+    }
+
+    public void setIsExceed(Boolean isExceed) {
+        this.isExceed = isExceed;
+    }
+
+    public ExamRecordStatus getExamRecordStatus() {
+        return examRecordStatus;
+    }
+
+    public void setExamRecordStatus(ExamRecordStatus examRecordStatus) {
+        this.examRecordStatus = examRecordStatus;
+    }
+
+    public HandInExamType getHandInExamType() {
+        return handInExamType;
+    }
+
+    public void setHandInExamType(HandInExamType handInExamType) {
+        this.handInExamType = handInExamType;
+    }
+
+    public IsSuccess getFaceVerifyResult() {
+        return faceVerifyResult;
+    }
+
+    public void setFaceVerifyResult(IsSuccess faceVerifyResult) {
+        this.faceVerifyResult = faceVerifyResult;
+    }
+
+    public Date getCleanTime() {
+        return cleanTime;
+    }
+
+    public void setCleanTime(Date cleanTime) {
+        this.cleanTime = cleanTime;
+    }
+
+    public Double getTotalScore() {
+        return totalScore;
+    }
+
+    public void setTotalScore(Double totalScore) {
+        this.totalScore = totalScore;
+    }
+
+    public Double getObjectiveScore() {
+        return objectiveScore;
+    }
+
+    public void setObjectiveScore(Double objectiveScore) {
+        this.objectiveScore = objectiveScore;
+    }
+
+    public Double getObjectiveAccuracy() {
+        return objectiveAccuracy;
+    }
+
+    public void setObjectiveAccuracy(Double objectiveAccuracy) {
+        this.objectiveAccuracy = objectiveAccuracy;
+    }
+
+    public Double getSubjectiveScore() {
+        return subjectiveScore;
+    }
+
+    public void setSubjectiveScore(Double subjectiveScore) {
+        this.subjectiveScore = subjectiveScore;
+    }
+
+    public Double getSuccPercent() {
+        return succPercent;
+    }
+
+    public void setSuccPercent(Double succPercent) {
+        this.succPercent = succPercent;
+    }
+
+    public Boolean getIsWarn() {
+        return isWarn;
+    }
+
+    public void setIsWarn(Boolean warn) {
+        isWarn = warn;
+    }
+
+    public Boolean getIsAudit() {
+        return isAudit;
+    }
+
+    public void setIsAudit(Boolean audit) {
+        isAudit = audit;
+    }
+
+    public Boolean getIsIllegality() {
+        return isIllegality;
+    }
+
+    public void setIsIllegality(Boolean illegality) {
+        isIllegality = illegality;
+    }
+
+    public SyncStatus getSyncStatus() {
+        return syncStatus;
+    }
+
+    public void setSyncStatus(SyncStatus syncStatus) {
+        this.syncStatus = syncStatus;
+    }
+
+    public Double getFaceSuccessPercent() {
+        return faceSuccessPercent;
+    }
+
+    public void setFaceSuccessPercent(Double faceSuccessPercent) {
+        this.faceSuccessPercent = faceSuccessPercent;
+    }
+
+    public Double getBaiduFaceLivenessSuccessPercent() {
+        return baiduFaceLivenessSuccessPercent;
+    }
+
+    public void setBaiduFaceLivenessSuccessPercent(Double baiduFaceLivenessSuccessPercent) {
+        this.baiduFaceLivenessSuccessPercent = baiduFaceLivenessSuccessPercent;
+    }
+
+    
+    public Integer getQuestionCount() {
+        return questionCount;
+    }
+
+    
+    public void setQuestionCount(Integer questionCount) {
+        this.questionCount = questionCount;
+    }
+
+    
+    public Boolean getIsAllObjectivePaper() {
+        return isAllObjectivePaper;
+    }
+
+    
+    public void setIsAllObjectivePaper(Boolean isAllObjectivePaper) {
+        this.isAllObjectivePaper = isAllObjectivePaper;
+    }
+
+    @Override
+    public String toString() {
+        return "ExamRecordData{" +
+                "id=" + id +
+                ", examId=" + examId +
+                ", examType=" + examType +
+                ", examStudentId=" + examStudentId +
+                ", studentId=" + studentId +
+                ", courseId=" + courseId +
+                ", orgId=" + orgId +
+                ", rootOrgId=" + rootOrgId +
+                ", basePaperId='" + basePaperId + '\'' +
+                ", paperType='" + paperType + '\'' +
+                ", startTime=" + startTime +
+                ", endTime=" + endTime +
+                ", usedExamTime=" + usedExamTime +
+                ", isContinued=" + isContinued +
+                ", continuedCount=" + continuedCount +
+                ", isExceed=" + isExceed +
+                ", faceSuccessCount=" + faceSuccessCount +
+                ", faceFailedCount=" + faceFailedCount +
+                ", faceStrangerCount=" + faceStrangerCount +
+                ", faceTotalCount=" + faceTotalCount +
+                ", examRecordStatus=" + examRecordStatus +
+                ", handInExamType=" + handInExamType +
+                ", faceVerifyResult=" + faceVerifyResult +
+                ", cleanTime=" + cleanTime +
+                ", isWarn=" + isWarn +
+                ", isAudit=" + isAudit +
+                ", isIllegality=" + isIllegality +
+                ", totalScore=" + totalScore +
+                ", objectiveScore=" + objectiveScore +
+                ", objectiveAccuracy=" + objectiveAccuracy +
+                ", subjectiveScore=" + subjectiveScore +
+                ", succPercent=" + succPercent +
+                ", faceSuccessPercent=" + faceSuccessPercent +
+                ", baiduFaceLivenessSuccessPercent=" + baiduFaceLivenessSuccessPercent +
+                ", syncStatus=" + syncStatus +
+                ", questionCount=" + questionCount +
+                ", isAllObjectivePaper=" + isAllObjectivePaper +
+                '}';
+    }
+}

+ 41 - 0
src/main/java/cn/com/qmth/examcloud/support/examing/ExamRecordPaperStruct.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.support.examing;
+
+import java.io.Serializable;
+
+import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
+
+/**
+ * 
+ * @author  	chenken
+ * @date    	2018年8月27日 下午3:26:27
+ * @company 	QMTH
+ * @description ExamRecordPaper.java
+ */
+public class ExamRecordPaperStruct implements Serializable{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -4672132535133881321L;
+
+	private String id;
+	
+	private DefaultPaper defaultPaper;
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public DefaultPaper getDefaultPaper() {
+		return defaultPaper;
+	}
+
+	public void setDefaultPaper(DefaultPaper defaultPaper) {
+		this.defaultPaper = defaultPaper;
+	}
+	
+}

+ 48 - 0
src/main/java/cn/com/qmth/examcloud/support/examing/ExamRecordQuestions.java

@@ -0,0 +1,48 @@
+package cn.com.qmth.examcloud.support.examing;
+
+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;
+    }
+
+}

+ 42 - 0
src/main/java/cn/com/qmth/examcloud/support/examing/ExamingActivityTime.java

@@ -0,0 +1,42 @@
+package cn.com.qmth.examcloud.support.examing;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 考试活动时间
+ *
+ * @author WANGWEI
+ * @date 2019年12月27日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamingActivityTime implements JsonSerializable {
+
+	private static final long serialVersionUID = 380814978042469776L;
+
+	/**
+	 * 考试记录ID
+	 */
+	private Long examRecordDataId;
+
+	/**
+	 * 学生最后活动时间
+	 */
+	private Long activeTime;
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Long getActiveTime() {
+		return activeTime;
+	}
+
+	public void setActiveTime(Long activeTime) {
+		this.activeTime = activeTime;
+	}
+
+}

+ 54 - 0
src/main/java/cn/com/qmth/examcloud/support/examing/ExamingHeartbeat.java

@@ -0,0 +1,54 @@
+package cn.com.qmth.examcloud.support.examing;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 考试心跳
+ *
+ * @author WANGWEI
+ * @date 2019年12月27日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamingHeartbeat implements JsonSerializable {
+	private static final long serialVersionUID = 2981954395650966836L;
+
+	/**
+	 * 考试记录ID
+	 */
+	private Long examRecordDataId;
+
+	/**
+	 * 心跳次数
+	 */
+	private Long times;
+
+	/**
+	 * 耗时(单位:秒)
+	 */
+	private Long cost;
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Long getTimes() {
+		return times;
+	}
+
+	public void setTimes(Long times) {
+		this.times = times;
+	}
+
+	public Long getCost() {
+		return cost;
+	}
+
+	public void setCost(Long cost) {
+		this.cost = cost;
+	}
+
+}

+ 251 - 0
src/main/java/cn/com/qmth/examcloud/support/examing/ExamingSession.java

@@ -0,0 +1,251 @@
+package cn.com.qmth.examcloud.support.examing;
+
+import java.util.Date;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 考试会话
+ *
+ * @author WANGWEI
+ * @date 2019年12月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class ExamingSession implements JsonSerializable {
+
+	private static final long serialVersionUID = -7960639278784327099L;
+
+	/**
+	 * 全局唯一考试标识符
+	 */
+	private String key;
+
+	/**
+	 * 顶级机构ID
+	 */
+	private Long rootOrgId;
+
+	/**
+	 * 学习中心ID
+	 */
+	private Long orgId;
+
+	/**
+	 * 考试状态
+	 */
+	private ExamingStatus examingStatus;
+
+	/**
+	 * 学生ID
+	 */
+	private Long studentId;
+
+	/**
+	 * 考试ID
+	 */
+	private Long examId;
+
+	/**
+	 * 课程ID
+	 */
+	private Long courseId;
+
+	/**
+	 * 课程CODE
+	 */
+	private String courseCode;
+
+	/**
+	 * 考生ID
+	 */
+	private Long examStudentId;
+
+	/**
+	 * 创建时间
+	 */
+	private Date creationTime;
+
+	/**
+	 * 试卷类型
+	 */
+	private String paperType;
+
+	/**
+	 * 考试记录DataID
+	 */
+	private Long examRecordDataId;
+
+	/**
+	 * 考试开始时间
+	 */
+	private Long startTime;
+
+	/**
+	 * 冻结时间:分钟
+	 */
+	private Integer freezeTime;
+
+	/**
+	 * 考试类型
+	 */
+	private String examType;
+
+	/**
+	 * 断点续考时间:分钟
+	 */
+	private Integer examReconnectTime;
+
+	/**
+	 * 考试时长:毫秒
+	 */
+	private Long examDuration;
+
+	/**
+	 * 构建key
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public String buildKey() {
+		this.key = new StringBuilder().append("EXAMING:").append(rootOrgId).append(":")
+				.append(studentId).toString();
+		return this.key;
+	}
+
+	public String getKey() {
+		return key;
+	}
+
+	public void setKey(String key) {
+		this.key = key;
+	}
+
+	public Long getRootOrgId() {
+		return rootOrgId;
+	}
+
+	public void setRootOrgId(Long rootOrgId) {
+		this.rootOrgId = rootOrgId;
+	}
+
+	public ExamingStatus getExamingStatus() {
+		return examingStatus;
+	}
+
+	public void setExamingStatus(ExamingStatus examingStatus) {
+		this.examingStatus = examingStatus;
+	}
+
+	public Long getStudentId() {
+		return studentId;
+	}
+
+	public void setStudentId(Long studentId) {
+		this.studentId = studentId;
+	}
+
+	public Long getExamId() {
+		return examId;
+	}
+
+	public void setExamId(Long examId) {
+		this.examId = examId;
+	}
+
+	public Long getCourseId() {
+		return courseId;
+	}
+
+	public void setCourseId(Long courseId) {
+		this.courseId = courseId;
+	}
+
+	public Long getExamStudentId() {
+		return examStudentId;
+	}
+
+	public void setExamStudentId(Long examStudentId) {
+		this.examStudentId = examStudentId;
+	}
+
+	public Date getCreationTime() {
+		return creationTime;
+	}
+
+	public void setCreationTime(Date creationTime) {
+		this.creationTime = creationTime;
+	}
+
+	public String getPaperType() {
+		return paperType;
+	}
+
+	public void setPaperType(String paperType) {
+		this.paperType = paperType;
+	}
+
+	public Long getOrgId() {
+		return orgId;
+	}
+
+	public void setOrgId(Long orgId) {
+		this.orgId = orgId;
+	}
+
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+
+	public Long getStartTime() {
+		return startTime;
+	}
+
+	public void setStartTime(Long startTime) {
+		this.startTime = startTime;
+	}
+
+	public Integer getFreezeTime() {
+		return freezeTime;
+	}
+
+	public void setFreezeTime(Integer freezeTime) {
+		this.freezeTime = freezeTime;
+	}
+
+	public String getExamType() {
+		return examType;
+	}
+
+	public void setExamType(String examType) {
+		this.examType = examType;
+	}
+
+	public Integer getExamReconnectTime() {
+		return examReconnectTime;
+	}
+
+	public void setExamReconnectTime(Integer examReconnectTime) {
+		this.examReconnectTime = examReconnectTime;
+	}
+
+	public Long getExamDuration() {
+		return examDuration;
+	}
+
+	public void setExamDuration(Long examDuration) {
+		this.examDuration = examDuration;
+	}
+
+	public String getCourseCode() {
+		return courseCode;
+	}
+
+	public void setCourseCode(String courseCode) {
+		this.courseCode = courseCode;
+	}
+
+}

+ 22 - 0
src/main/java/cn/com/qmth/examcloud/support/examing/ExamingStatus.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.support.examing;
+
+/**
+ * 考试状态
+ *
+ * @author WANGWEI
+ * @date 2019年12月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public enum ExamingStatus {
+
+	/**
+	 * 非正式状态
+	 */
+	INFORMAL,
+
+	/**
+	 * 正式状态
+	 */
+	FORMAL
+
+}

+ 405 - 0
src/main/java/cn/com/qmth/examcloud/support/filestorage/FileStorageUtil.java

@@ -0,0 +1,405 @@
+package cn.com.qmth.examcloud.support.filestorage;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import org.apache.commons.lang3.StringUtils;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.util.MD5;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
+import cn.com.qmth.examcloud.web.aliyun.AliyunSiteManager;
+import cn.com.qmth.examcloud.web.filestorage.FileStorage;
+import cn.com.qmth.examcloud.web.filestorage.FileStorageHelper;
+import cn.com.qmth.examcloud.web.filestorage.FileStoragePathEnvInfo;
+import cn.com.qmth.examcloud.web.filestorage.FileStorageType;
+import cn.com.qmth.examcloud.web.filestorage.YunHttpRequest;
+import cn.com.qmth.examcloud.web.filestorage.YunPathInfo;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+import cn.com.qmth.examcloud.web.upyun.UpyunSiteManager;
+
+/**
+ * 文件存储服务接口工具类
+ * 
+ * @author 86182
+ *
+ */
+public class FileStorageUtil {
+
+//	private static String fileStorageType = PropertyHolder.getString("$filestorage-type");
+
+//	private static String tempDir = PropertyHolder.getString("$filestorage-trans-tempdir");
+
+	private static String beanSuff = "FileStorage";
+
+	/**
+	 * 根据当前配置存储类型保存文件到存储服务器
+	 * 
+	 * @param siteId
+	 * @param env
+	 * @param in     文件流
+	 * @param md5    文件MD5 可为空
+	 * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
+	 */
+	public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in, String md5) {
+		FileStorageType fsType = getFileStorageType();
+		return saveFile(siteId, env, in, fsType, md5);
+	}
+
+	/**
+	 * 根据当前配置存储类型保存文件到存储服务器
+	 * 
+	 * @param siteId
+	 * @param env
+	 * @param file    文件
+	 * @param withMd5 是否校验MD5
+	 * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
+	 */
+	public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, boolean withMd5) {
+		FileStorageType fsType = getFileStorageType();
+		String md5 = null;
+		if (withMd5) {
+			md5 = MD5.md5Hex(file);
+		}
+		return saveFile(siteId, env, file, fsType, md5);
+	}
+
+	/**
+	 * 根据当前配置存储类型保存文件到存储服务器
+	 * 
+	 * @param siteId
+	 * @param env
+	 * @param file   文件
+	 * @param md5    文件MD5 可为空
+	 * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
+	 */
+	public static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, String md5) {
+		FileStorageType fsType = getFileStorageType();
+		return saveFile(siteId, env, file, fsType, md5);
+	}
+
+	/**
+	 * 根据存储类型保存文件到存储服务器
+	 * 
+	 * @param siteId
+	 * @param env
+	 * @param in     文件流
+	 * @param fsType 存储类型
+	 * @param md5    文件MD5 可为空
+	 * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
+	 */
+	private static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, InputStream in,
+			FileStorageType fsType, String md5) {
+
+		if (siteId == null) {
+			throw new StatusException("1000", "siteId是空");
+		}
+		if (in == null) {
+			throw new StatusException("1001", "文件流是空");
+		}
+		if (env == null) {
+			throw new StatusException("1002", "文件上传路径信息是空");
+		}
+		env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
+		FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
+		return fs.saveFile(siteId, env, in, md5);
+	}
+
+	/**
+	 * 根据存储类型保存文件到存储服务器
+	 * 
+	 * @param siteId
+	 * @param env
+	 * @param file   文件
+	 * @param fsType 存储类型
+	 * @param md5    文件MD5 可为空
+	 * @return 返回包含协议名的地址,数据库直接存储用 如:upyun-1://student_photo/001.jpg
+	 */
+	private static YunPathInfo saveFile(String siteId, FileStoragePathEnvInfo env, File file, FileStorageType fsType,
+			String md5) {
+		if (siteId == null) {
+			throw new StatusException("2000", "siteId是空");
+		}
+		if (file == null) {
+			throw new StatusException("2001", "文件是空");
+		}
+		if (env == null) {
+			throw new StatusException("2002", "文件上传路径信息是空");
+		}
+		env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
+		FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
+		return fs.saveFile(siteId, env, file, md5);
+	}
+
+	/**
+	 * 获取文件访问路径
+	 * 
+	 * @param path 数据库保存的路径 如:upyun-1://student_photo/001.jpg,兼容老数据,路径必须是包含根路径的
+	 * @return 可直接访问的文件地址
+	 */
+	public static String realPath(String path) {
+
+		if (StringUtils.isBlank(path)) {
+			return null;
+		}
+		// 兼容处理老数据文件路径
+		path = diposeOldPath(path);
+		// 如果是全路径直接返回
+		if (path.startsWith("http") || path.startsWith("https")) {
+			return path;
+		}
+		// 根据路径头获取对应的处理类
+		FileStorage fs = SpringContextHolder.getBean(FileStorageHelper.getHead(path).toLowerCase() + beanSuff,
+				FileStorage.class);
+		// 返回处理类处理结果
+		return fs.realPath(path);
+
+	}
+
+	/**
+	 * 将文件转换到指定存储服务上
+	 * 
+	 * @param path        数据库保存的路径
+	 *                    如:upyun-1://student_photo/001.jpg,兼容老数据,路径必须是包含根路径的
+	 * @param transFsType 要转换的类型
+	 * @return 转换之后的路径
+	 */
+//	public static String transPath(String path, FileStorageType transFsType) {
+//		
+//		File file = null;
+//		if (StringUtils.isBlank(path)) {
+//			throw new StatusException("4001", "文件路径是空");
+//		}
+//		if (transFsType == null) {
+//			throw new StatusException("4002", "转换类型是空");
+//		}
+//		// 兼容处理老数据文件路径
+//		path = diposeOldPath(path);
+//		// 如果是全路径直接返回
+//		if (path.startsWith("http") || path.startsWith("https")) {
+//			return path;
+//		}
+//
+//		FileStorageType sourseType = FileStorageType.valueOf(FileStorageHelper.getHead(path));
+//		// 相同的类型直接返回
+//		if (sourseType.equals(transFsType)) {
+//			return path;
+//		}
+//		// 获取对应的处理类
+//		FileStorage fs = SpringContextHolder.getBean(transFsType.name().toLowerCase() + beanSuff, FileStorage.class);
+//		try {
+//			// 下载文件到本地
+//			String httppath = fs.realPath(FileStorageHelper.getPath(path));
+//			file = downFile(httppath);
+//			// 保存文件到指定存储服务器,并返回路径
+//			YunPathInfo ret = fs.saveFile(file, "from" + sourseType.name().toLowerCase() + "/" + FileStorageHelper.getPath(path));
+//			return ret.getRelativePath();
+//		} catch (IOException e) {
+//			throw new StatusException("4003", "下载文件出错 " + e.getMessage());
+//		} finally {
+//			if (file != null) {
+//				file.delete();
+//			}
+//		}
+//
+//	}
+
+//	private static File downFile(String url) throws IOException {
+//		String name = url.substring(url.lastIndexOf("/") + 1);
+//		File file = new File(tempDir + "/" + name);
+//		if (file.exists()) {
+//			file.delete();
+//		}
+//		file.createNewFile();
+//		saveUrlAs(url, file.getAbsolutePath());
+//		return file;
+//	}
+
+	/**
+	 * @param path 数据库存储的路径。路径必须是包含根路径的
+	 * @return 处理后的路径。补全路径头。
+	 */
+	private static String diposeOldPath(String path) {
+		if (StringUtils.isBlank(path)) {
+			return null;
+		}
+		// 如果是全路径直接返回
+		if (path.startsWith("http") || path.startsWith("https")) {
+			return path;
+		}
+		// 如果是半路径,转换成又拍云类型
+		if (path.indexOf(":") == -1) {
+			if (path.startsWith("/")) {
+				path = path.substring(1);
+			}
+			return FileStorageHelper.getUpyunSiteOne(path);
+		}
+		// 无需处理
+		return path;
+	}
+
+	/**
+	 * 将网络文件保存到本地
+	 *
+	 * @param fileUrl       网络文件URL
+	 * @param localFilePath 例如D:/123.txt
+	 * @throws IOException
+	 */
+	private static void saveUrlAs(String fileUrl, String localFilePath) throws IOException {
+		URL url = new URL(fileUrl);
+
+		HttpURLConnection connection;
+		connection = (HttpURLConnection) url.openConnection();
+
+		try (DataInputStream dataInputStream = new DataInputStream(connection.getInputStream());
+				FileOutputStream fileOutputStream = new FileOutputStream(localFilePath);
+				DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);) {
+
+			byte[] buffer = new byte[4096];
+			int count;
+			while ((count = dataInputStream.read(buffer)) > 0) {
+				dataOutputStream.write(buffer, 0, count);
+			}
+			fileOutputStream.flush();
+			dataOutputStream.flush();
+		} finally {
+			if (connection != null) {
+				connection.disconnect();
+			}
+		}
+	}
+
+	/**
+	 * 将网络文件保存到本地
+	 * 
+	 * @param fileUrl   网络文件URL
+	 * @param localFile 本地文件对象
+	 * @throws IOException
+	 */
+	public static void saveUrlAs(String fileUrl, File localFile) {
+		try {
+			saveUrlAs(fileUrl, localFile.getAbsolutePath());
+		} catch (IOException e) {
+			throw new StatusException("5001", "下载文件出错", e);
+		}
+	}
+
+	/**
+	 * 获取指定云存储的签名
+	 * 
+	 * @param siteId
+	 * @param env
+	 * @param md5
+	 * @return
+	 */
+	public static YunHttpRequest getSignature(FileStorageType fsType, String siteId, FileStoragePathEnvInfo env,
+			String md5) {
+		env.setTimeMillis(String.valueOf(System.currentTimeMillis()));
+		// 获取对应的处理类
+		FileStorage fs = SpringContextHolder.getBean(fsType.name().toLowerCase() + beanSuff, FileStorage.class);
+		return fs.getSignature(siteId, env, md5);
+	}
+
+	/**
+	 * 获取完整的不带域名的路径
+	 * 
+	 * @param prefixPath 路径前缀
+	 * @param sourcePath 数据库存储的路径
+	 * @return 处理之后的路径
+	 */
+	public static String getIntactPath(String prefixPath, String sourcePath) {
+		if (StringUtils.isBlank(sourcePath)) {
+			return null;
+		}
+		// 如果是全路径直接返回
+		if (sourcePath.startsWith("http") || sourcePath.startsWith("https")) {
+			return sourcePath;
+		}
+		// 如果是半路径则拼接
+		if (sourcePath.indexOf(":") == -1) {
+			if (sourcePath.startsWith("/")) {
+				sourcePath = sourcePath.substring(1);
+			}
+			if (prefixPath.endsWith("/")) {
+				prefixPath = prefixPath.substring(0, prefixPath.length());
+			}
+			return prefixPath + "/" + sourcePath;
+		}
+		// 新协议无需处理
+		return sourcePath;
+	}
+
+	/**
+	 * 删除云存储文件
+	 * 
+	 * @param path 全路径,包含根目录,含协议名
+	 */
+	public static void deleteFile(String path) {
+		if (StringUtils.isBlank(path)) {
+			return;
+		}
+		path = diposeOldPath(path);
+		// 如果是全路径直接返回
+		if (path.startsWith("http") || path.startsWith("https")) {
+			return;
+		}
+		FileStorage fs = SpringContextHolder.getBean(FileStorageHelper.getHead(path).toLowerCase() + beanSuff,
+				FileStorage.class);
+		fs.deleteFile(path);
+	}
+
+	/**
+	 * 获取文件访问路径(备用域名地址)
+	 * 
+	 * @param path 数据库保存的路径 如:upyun-1://student_photo/001.jpg,兼容老数据,路径必须是包含根路径的
+	 * @return 可直接访问的文件地址,如果没有配置备用地址,则返回主域名地址
+	 */
+	public static String realPathBackup(String path) {
+		if (StringUtils.isBlank(path)) {
+			return null;
+		}
+		// 兼容处理老数据文件路径
+		path = diposeOldPath(path);
+		// 如果是全路径直接返回
+		if (path.startsWith("http") || path.startsWith("https")) {
+			return path;
+		}
+		// 根据路径头获取对应的处理类
+		FileStorage fs = SpringContextHolder.getBean(FileStorageHelper.getHead(path).toLowerCase() + beanSuff,
+				FileStorage.class);
+		// 返回处理类处理结果
+		return fs.realPathBackup(path);
+
+	}
+
+	public static void initYunClient() {
+		UpyunSiteManager.initClient();
+		AliyunSiteManager.initClient();
+	}
+
+	public static void initYunSite() {
+		UpyunSiteManager.initSite();
+		AliyunSiteManager.initSite();
+	}
+
+	public static FileStorageType getFileStorageType() {
+		SysPropertyCacheBean spc = CacheHelper.getSysProperty("filestorage.type");
+		if (spc == null || spc.getValue() == null) {
+			throw new StatusException("10001", "未配置文件存储服务类型");
+		}
+		String type = (String) spc.getValue();
+		try {
+			return FileStorageType.valueOf(type);
+		} catch (Exception e) {
+			throw new StatusException("10002", "配置文件存储服务类型错误");
+		}
+	}
+
+}

+ 65 - 0
src/main/java/cn/com/qmth/examcloud/support/handler/richText/AudioTextHandler.java

@@ -0,0 +1,65 @@
+package cn.com.qmth.examcloud.support.handler.richText;
+
+import cn.com.qmth.examcloud.support.enums.BlockType;
+import cn.com.qmth.examcloud.support.handler.richText.bean.BlockBean;
+import cn.com.qmth.examcloud.support.handler.richText.bean.SectionBean;
+import cn.com.qmth.examcloud.support.handler.richText.bean.SectionCollectionBean;
+import com.mysql.cj.util.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @Description 音频处理器类
+ * @Author lideyin
+ * @Date 2020/5/15 16:29
+ * @Version 1.0
+ */
+public class AudioTextHandler implements RichTextHandler{
+    private static Log logger = LogFactory.getLog(HtmlTextHandler.class);
+
+    @Override
+    public SectionCollectionBean handle(String richText) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("[HTML-TEXT-HANDLER]--richText: " + richText);
+        }
+
+        if (StringUtils.isNullOrEmpty(richText)) {
+            return new SectionCollectionBean();
+        }
+
+        SectionCollectionBean sectionCollection = new SectionCollectionBean();
+        List<SectionBean> sectionList = new ArrayList<>();
+        SectionBean section = new SectionBean();
+        List<BlockBean> blockList = new ArrayList<>();
+
+        if (validateAudioAnswer(richText)) {
+            BlockBean blockBean = new BlockBean(BlockType.audio.name());
+            blockBean.setValue(richText);
+            blockList.add(blockBean);
+        }
+
+        section.setBlocks(blockList);
+        sectionList.add(section);
+        sectionCollection.setSections(sectionList);
+
+        return sectionCollection;
+    }
+
+    /**
+     * 校验音频答案格式是否正确
+     *
+     * @param studentAnswer
+     * @return
+     */
+    private boolean validateAudioAnswer(String studentAnswer) {
+        if (StringUtils.isNullOrEmpty(studentAnswer)) {
+            return false;
+        }
+
+        String regExp = "^(ftp|https?)\\:\\/\\/([\\w\\_\\-]+)\\.([\\w\\-]+[\\.]?)*[\\w]+\\.[a-zA-Z]{2,10}(.*)\\.(mp3)";
+        return studentAnswer.matches(regExp);
+    }
+}

+ 145 - 0
src/main/java/cn/com/qmth/examcloud/support/handler/richText/ComplexTextHandler.java

@@ -0,0 +1,145 @@
+package cn.com.qmth.examcloud.support.handler.richText;
+
+import cn.com.qmth.examcloud.support.enums.BlockType;
+import cn.com.qmth.examcloud.support.handler.richText.bean.BlockBean;
+import cn.com.qmth.examcloud.support.handler.richText.bean.SectionBean;
+import cn.com.qmth.examcloud.support.handler.richText.bean.SectionCollectionBean;
+import com.mysql.cj.util.StringUtils;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @Description 复合文本处理器类
+ * @Author lideyin
+ * @Date 2020/5/15 16:29
+ * @Version 1.0
+ */
+public class ComplexTextHandler implements RichTextHandler {
+    private static Log logger = LogFactory.getLog(HtmlTextHandler.class);
+
+    @Override
+    public SectionCollectionBean handle(String richText) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("[HTML-TEXT-HANDLER]--richText: " + richText);
+        }
+
+        if (StringUtils.isNullOrEmpty(richText)) {
+            return new SectionCollectionBean();
+        }
+
+        SectionCollectionBean body = new SectionCollectionBean();
+        List<SectionBean> sections = new ArrayList<>();
+        // 得到小题题干或者答案行数
+        if (org.apache.commons.lang3.StringUtils.isBlank(richText)) {
+            return body;
+        }
+
+        String[] questionRowStrings = richText.split("</p>");
+        for (int i = 0; i < questionRowStrings.length; i++) {
+            List<BlockBean> blocks = disposeQuestionBodyOrOption(questionRowStrings[i]);
+            if (blocks != null && blocks.size() > 0) {
+                SectionBean section = new SectionBean();
+                // 将小题题干拆分为Block集合
+                section.setBlocks(blocks);
+                sections.add(section);
+            }
+        }
+        body.setSections(sections);
+        return body;
+    }
+
+
+    private List<BlockBean> disposeQuestionBodyOrOption(String questionRow) {
+        List<BlockBean> blocks = new ArrayList<>();
+        // 去掉每行里面的<p>,<span>,</span>标签
+        questionRow = questionRow.replaceAll("<p>", "").replaceAll("</p>", "").replaceAll("<span>", "")
+                .replaceAll("</span>", "").replaceAll("</a>", "");
+        String[] questionRowStrings = questionRow.split("<|/>|>");
+        for (int i = 0; i < questionRowStrings.length; i++) {
+            BlockBean block = new BlockBean(BlockType.text.name());
+            String rowStr = questionRowStrings[i];
+            // 判断是否有图片
+            if (rowStr.startsWith("img")) {
+                rowStr = "<" + rowStr + ">";
+                Map<String, Object> param = new HashMap<>();
+                // 需要继续做截取,取到Parma
+                block.setType("image");
+                // 获取图片的路径
+                List<String> strSrcList = getImg(rowStr, "src");
+                if (strSrcList.size() > 0) {
+                    String strSrc = strSrcList.get(0).replaceAll("src=\"", "").replaceAll("\"", "");
+                    block.setValue(strSrc);
+                }
+                // 获取图片的高度
+                List<String> strHeightList = getImg(rowStr, "height");
+                if (strHeightList.size() > 0) {
+                    String strHeight = strHeightList.get(0).replaceAll("height=\"", "").replaceAll("\"", "");
+                    param.put("height", strHeight);
+                }
+                // 获取图片的宽度
+                List<String> strWidthList = getImg(rowStr, "width");
+                if (strHeightList.size() > 0) {
+                    String strWidth = strWidthList.get(0).replaceAll("width=\"", "").replaceAll("\"", "");
+                    param.put("width", strWidth);
+                }
+                block.setParam(param);
+                blocks.add(block);
+            } else if (rowStr.startsWith("a") && rowStr.contains("id") && rowStr.contains("name")) { // 处理音频
+                rowStr = "<" + rowStr + ">";
+                block.setPlayTime(1);
+                block.setType("audio");
+                block.setValue(this.getAttrValue(rowStr, "url"));
+                blocks.add(block);
+            } else {
+                block.setType("text");
+                if (org.apache.commons.lang3.StringUtils.isNotBlank(rowStr)) {
+                    block.setValue(StringEscapeUtils.unescapeHtml(rowStr));
+                    blocks.add(block);
+                }
+            }
+        }
+
+        return blocks;
+    }
+
+    /**
+     * 获取图片里面的路径,长度,宽度
+     */
+    private List<String> getImg(String s, String str) {
+        String regex;
+        List<String> list = new ArrayList<>();
+        regex = str + "=\"(.*?)\"";
+        Pattern pa = Pattern.compile(regex, Pattern.DOTALL);
+        Matcher ma = pa.matcher(s);
+        while (ma.find()) {
+            list.add(ma.group());
+        }
+        return list;
+    }
+
+    private String getAttrValue(String questionStr, String attrName) {
+        Pattern aPattern = Pattern.compile("a.*");
+        Matcher aMatcher = aPattern.matcher(questionStr);
+
+        if (aMatcher.find()) {
+            String idRegex = attrName + "=\".*?\"";
+            Pattern idPattern = Pattern.compile(idRegex);
+            Matcher idMatcher = idPattern.matcher(aMatcher.group());
+            if (idMatcher.find()) {
+                return idMatcher.group()
+                        .replaceAll(attrName + "=\"", "")
+                        .replaceAll("\"", "");
+            }
+        }
+
+        return "";
+    }
+}

+ 193 - 0
src/main/java/cn/com/qmth/examcloud/support/handler/richText/HtmlTextHandler.java

@@ -0,0 +1,193 @@
+package cn.com.qmth.examcloud.support.handler.richText;
+
+import cn.com.qmth.examcloud.commons.util.RegExpUtil;
+import cn.com.qmth.examcloud.support.enums.BlockType;
+import cn.com.qmth.examcloud.support.handler.richText.bean.BlockBean;
+import cn.com.qmth.examcloud.support.handler.richText.bean.SectionBean;
+import cn.com.qmth.examcloud.support.handler.richText.bean.SectionCollectionBean;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+import com.mysql.cj.util.StringUtils;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jsoup.Jsoup;
+import org.jsoup.safety.Whitelist;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @Description html类型的文本处理器
+ * @Author lideyin
+ * @Date 2020/5/15 16:28
+ * @Version 1.0
+ */
+public class HtmlTextHandler implements RichTextHandler {
+    private static Log logger = LogFactory.getLog(HtmlTextHandler.class);
+
+    private static Map<String, String> tagMap;
+    private static String DEFAULT_RETAIN_TAG = "b,u,i,sup,sub";
+
+    static {
+        tagMap = new HashMap<>();
+        tagMap.put("b", "bold");
+        tagMap.put("u", "underline");
+        tagMap.put("i", "italic");
+        tagMap.put("sup", "sup");
+        tagMap.put("sub", "sub");
+    }
+
+    @Override
+    public SectionCollectionBean handle(String richText) {
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("[HTML-TEXT-HANDLER]--richText: " + richText);
+        }
+
+        if (StringUtils.isNullOrEmpty(richText)) {
+            return new SectionCollectionBean();
+        }
+
+        /*过滤不需要的html标签*/
+        String retainTags = PropertyHolder.getString("cloudMarking.interface.retainTags", DEFAULT_RETAIN_TAG);
+
+//        String[] specialRetainTagArr = (retainTags + ",br").split(",");
+//        richText = Jsoup.clean(richText, new Whitelist().addTags(specialRetainTagArr));
+
+        richText = richText.replace("<div>", "\n")
+                .replace("</div>", "")
+                .replace("<br>", "");
+        richText = richText.replace("<br>", "\n");
+
+        SectionCollectionBean sectionCollection = new SectionCollectionBean();
+        List<SectionBean> sectionList = new ArrayList<>();
+        String[] retainTagArr = retainTags.split(",");
+        //有多少个换行,则生成多少个section
+        String[] paragraphArray = richText.split("\n");
+        for (int i = 0; i < paragraphArray.length; i++) {
+            String p = paragraphArray[i];
+            SectionBean section = new SectionBean();
+
+            List<BlockBean> blockList = new ArrayList<>();
+            blockList = getSplitList(p, blockList, retainTagArr);
+
+            section.setBlocks(blockList);
+            sectionList.add(section);
+        }
+
+        sectionCollection.setSections(sectionList);
+
+        //html实体符号编码解析
+
+        return sectionCollection;
+    }
+
+    private static List<BlockBean> getSplitList(String str, List<BlockBean> resultList, final String[] retainTagArr) {
+        if (StringUtils.isNullOrEmpty(str)) {
+            return null;
+        }
+
+        String regPattern = "";
+        for (int i = 0; i < retainTagArr.length; i++) {
+            String tag = retainTagArr[i];
+            if (i != 0) {
+                regPattern += "|";
+            }
+            regPattern += String.format("<%s>\\S*<\\/%s>", tag, tag);
+        }
+
+
+        //找到第一个匹配项
+        String firstMatch = RegExpUtil.find(str, regPattern);
+
+        //如果未找到任何匹配项,则直接返回原字符串
+        if (StringUtils.isNullOrEmpty(firstMatch)) {
+            resultList.add(buildNormalTextAnswer(str));
+            return resultList;
+        }
+
+        //查找到的结果的索引
+        int matchIndex = str.indexOf(firstMatch);
+
+        //如果字符串以特殊标签开头的场景特殊处理
+        if (matchIndex == 0) {
+            //如果字符串中只有特殊标签,则直接返回处理过的数据
+            if (firstMatch.length() == str.length()) {
+                resultList.add(buildSpecialTextAnswer(Jsoup.clean(firstMatch, Whitelist.none()), getTag(firstMatch)));
+                return resultList;
+            }
+
+            //如果字符串以特殊标签开头,且还有其它内容
+            //首先,将特殊标签加入返回集合
+            resultList.add(buildSpecialTextAnswer(Jsoup.clean(firstMatch, Whitelist.none()), getTag(firstMatch)));
+            //其次,将剩下的字符串递归继续处理
+            str = str.substring(firstMatch.length());
+            getSplitList(str, resultList, retainTagArr);
+
+            return resultList;
+        }
+
+        //如果字符串不以特殊标签开头,且还有其它内容
+        String value = str.substring(0, matchIndex);
+        //首先,将前面的普通文本添加到结果集
+        resultList.add(buildNormalTextAnswer(value));
+        //其次,将特殊结果添加到结果集
+        resultList.add(buildSpecialTextAnswer(Jsoup.clean(firstMatch, Whitelist.none()), getTag(firstMatch)));
+        //剩下部分的字符串,继续递归处理
+        str = str.substring(matchIndex + firstMatch.length());
+        getSplitList(str, resultList, retainTagArr);
+
+        return resultList;
+    }
+
+    //构建普通的文本作答
+    private static BlockBean buildNormalTextAnswer(String value) {
+        BlockBean obb = new BlockBean(BlockType.text.name());
+        obb.setValue(StringEscapeUtils.unescapeHtml(value));
+
+        return obb;
+    }
+
+    //构建带特殊标签的文本作答
+    private static BlockBean buildSpecialTextAnswer(String value, String tag) {
+        BlockBean obb = new BlockBean(BlockType.text.name());
+        obb.setType("text");
+        obb.setValue(StringEscapeUtils.unescapeHtml(value));
+
+        Map<String, Object> paramMap = new HashMap<>();
+        paramMap.put(tag, true);
+        obb.setParam(paramMap);
+
+        return obb;
+    }
+
+    private static String getTag(String target) {
+        if (StringUtils.isNullOrEmpty(target)) {
+            return null;
+        }
+
+        for (String s : tagMap.keySet()) {
+            if (target.indexOf(String.format("<%s>", s)) != -1) {
+                return tagMap.get(s);
+            }
+        }
+
+        return null;
+    }
+
+//    public static void main(String[] args) {
+////        String testStr = "1111111<div>222&lt;div&gt;&lt;/div&gt;21000<sup>2</sup></div><div>3333399<sub>3</sub>5555555</div><div><br></div><div>444444</div>";
+////        String testStr="{\"mainNumber\":2,\"order\":23,\"questionId\":\"5ebdf1cbcad4db1aee85e907\",\"studentAnswer\":\"发发呆<div>ななぅぅぉてふっふてふ</div><div class='photo-answers-block'><a href='https://ecs-test-static.qmth.com.cn/oe-answer-file/21083/19288/23/21083_19288_23_15895106360336244.jpeg' target='_blank' ><img class='photo-answer' src='https://ecs-test-static.qmth.com.cn/oe-answer-file/21083/19288/23/21083_19288_23_15895106360336244.jpeg?x-oss-process=image/resize,m_lfit,h_200,w_200' /></a></div>\",\"answerType\":null,\"answer\":\"<p><span> えいが</span></p>\",\"body\":\"<p>映画</p>\",\"parentBody\":null,\"questionType\":\"ESSAY\"}";
+//            String testStr="{\"mainNumber\":5,\"order\":19,\"questionId\":\"5e942585cad4db278def3df5\",\"studentAnswer\":\"范德萨范德萨飞<sup>电风扇的</sup><div>范德萨范德萨</div><div>范德萨范德萨范<sub>德萨发的</sub></div><div class='photo-answers-block'><a href='https://ecs-test-static.qmth.com.cn/oe-answer-file/20820/19336/19/20820_19336_19_15899464519267961.jpeg' target='_blank' ><img class='photo-answer' src='https://ecs-test-static.qmth.com.cn/oe-answer-file/20820/19336/19/20820_19336_19_15899464519267961.jpeg?x-oss-process=image/resize,m_lfit,h_200,w_200' /></a><a href='https://ecs-test-static.qmth.com.cn/oe-answer-file/20820/19336/19/20820_19336_19_15899464518985666.jpeg' target='_blank' ><img class='photo-answer' src='https://ecs-test-static.qmth.com.cn/oe-answer-file/20820/19336/19/20820_19336_19_15899464518985666.jpeg?x-oss-process=image/resize,m_lfit,h_200,w_200' /></a><a href='https://ecs-test-static.qmth.com.cn/oe-answer-file/20820/19336/19/20820_19336_19_15899464519221951.jpeg' target='_blank' ><img class='photo-answer' src='https://ecs-test-static.qmth.com.cn/oe-answer-file/20820/19336/19/20820_19336_19_15899464519221951.jpeg?x-oss-process=image/resize,m_lfit,h_200,w_200' /></a></div>\",\"answerType\":\"DIVERSIFIED_TEXT\",\"answer\":\"<p>1).杆件变形的基本形式有轴向拉伸和压缩,剪切,扭转和弯曲。</p>\\n\\n<p>2).强度&mdash;&mdash;构建抵抗破坏的能力。刚度&mdash;&mdash;构件抵抗变形的能力。</p>\\n\\n<p>3).稳定性&mdash;&mdash;构件保持原型状的能力。</p>\",\"body\":\"<p>杆件变形的<em><strong><u>基本形式</u></strong></em>有哪几种?何谓构件的强度,刚度和稳定性?</p><p><a id=\\\"5eba5fadcad4db05908c8d13\\\" name=\\\"19_1_2.mp3\\\" question-audio url=\\\"https://ecs-test-static.qmth.com.cn/comm-ques-bank/prod/audio/5e942585cad4db278def3df5_514_19_1_2.mp3\\\"></a></p>\",\"parentBody\":null,\"questionType\":\"ESSAY\"}";
+//
+//        HtmlTextHandler o = new HtmlTextHandler();
+//
+//
+//        SectionCollectionBean sfds = o.handle(testStr);
+//
+//        System.out.println(JsonUtil.toJson(sfds));
+//    }
+
+}

+ 172 - 0
src/main/java/cn/com/qmth/examcloud/support/handler/richText/ImageTextHandler.java

@@ -0,0 +1,172 @@
+package cn.com.qmth.examcloud.support.handler.richText;
+
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.util.RegExpUtil;
+import cn.com.qmth.examcloud.support.enums.BlockType;
+import cn.com.qmth.examcloud.support.handler.richText.bean.BlockBean;
+import cn.com.qmth.examcloud.support.handler.richText.bean.SectionBean;
+import cn.com.qmth.examcloud.support.handler.richText.bean.SectionCollectionBean;
+import com.mysql.cj.util.StringUtils;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.safety.Whitelist;
+import org.jsoup.select.Elements;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @Description 图片类型的文本处理器
+ * @Author lideyin
+ * @Date 2020/5/15 16:28
+ * @Version 1.0
+ */
+public class ImageTextHandler implements RichTextHandler {
+    private static Log logger = LogFactory.getLog(HtmlTextHandler.class);
+
+    @Override
+    public SectionCollectionBean handle(String richText) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("[HTML-TEXT-HANDLER]--richText: " + richText);
+        }
+
+        if (StringUtils.isNullOrEmpty(richText)) {
+            return new SectionCollectionBean();
+        }
+
+        SectionCollectionBean sectionCollection = new SectionCollectionBean();
+        List<SectionBean> sectionList = new ArrayList<>();
+        SectionBean section = new SectionBean();
+        List<BlockBean> blockList = new ArrayList<>();
+
+        //图片文本部分
+        String txtStr = getImgTxt(richText);
+        if (!StringUtils.isNullOrEmpty(txtStr)) {
+            BlockBean blockBean = new BlockBean(BlockType.text.name());
+            blockBean.setValue(StringEscapeUtils.unescapeHtml(txtStr));
+            blockList.add(blockBean);
+        }
+
+        //图片部分
+        if (validateImgAnswer(richText)) {
+            String imgSrc = getImgSrc(richText);
+            String[] imgAnswers = imgSrc.split("\\|");
+
+            for (int i = 0; i < imgAnswers.length; i++) {
+                BlockBean blockBean = new BlockBean(BlockType.image.name());
+                blockBean.setValue(getPureUrl(imgAnswers[i]));
+
+                //又拍云图片的宽*高
+                String width=RegExpUtil.find(imgAnswers[i], "w_(\\d+)");
+                String height=RegExpUtil.find(imgAnswers[i], "h_(\\d+)");
+                Map<String, Object> paramMap = new HashMap<>();
+                if (!StringUtils.isNullOrEmpty(width) && !StringUtils.isNullOrEmpty(height)) {
+                    paramMap.put("width", Integer.valueOf(width.replaceAll("w_","")));
+                    paramMap.put("height", Integer.valueOf(height.replaceAll("h_","")));
+                    blockBean.setParam(paramMap);
+                }
+
+                blockList.add(blockBean);
+            }
+        }
+
+        section.setBlocks(blockList);
+        sectionList.add(section);
+        sectionCollection.setSections(sectionList);
+
+        return sectionCollection;
+
+    }
+
+    /**
+     * 获取图片作答的文本部分
+     *
+     * @param studentAnswer
+     * @return
+     */
+    private String getImgTxt(String studentAnswer) {
+        if (!StringUtils.isNullOrEmpty(studentAnswer)) {
+            return Jsoup.clean(studentAnswer, Whitelist.none());
+        }
+
+        return null;
+    }
+
+    /**
+     * 校验图片答案格式是否正确
+     *
+     * @param studentAnswer
+     * @return
+     */
+    private boolean validateImgAnswer(String studentAnswer) {
+        if (StringUtils.isNullOrEmpty(studentAnswer)) {
+            return false;
+        }
+
+        String regExp = "[\\s\\S]*(ftp|https?)\\:\\/\\/([\\w\\_\\-]+)\\.([\\w\\-]+[\\.]?)*[\\w]+\\.[a-zA-Z]{2,10}(.*)\\.(png|jpg|gif|jpeg).*[\\s\\S]*";
+        return studentAnswer.matches(regExp);
+    }
+
+    /**
+     * 获取图片作答图片路径
+     *
+     * @param studentAnswer
+     * @return
+     */
+    private String getImgSrc(String studentAnswer) {
+        if (StringUtils.isNullOrEmpty(studentAnswer)) {
+            return studentAnswer;
+        }
+
+        //图片题特殊处理(因为图片作答题中有html标签,只需要取url即可)
+        Document doc = Jsoup.parse(studentAnswer);
+        Elements imgElements = doc.select("img[src]");
+
+        String imgSrc = "";
+
+        for (Element el : imgElements) {
+            String src = el.attr("src");
+            if (!StringUtils.isNullOrEmpty(src)) {
+                imgSrc += src + "|";
+            }
+        }
+        if (!StringUtils.isNullOrEmpty(imgSrc)) {
+            return imgSrc.substring(0, imgSrc.length() - 1);
+        }
+
+        return imgSrc;
+    }
+
+    /**
+     * 获取图片url中不带参数部分,去除地址中的多余后缀
+     *
+     * @param imgUrl 图片地址
+     * @return
+     */
+    private String getPureUrl(String imgUrl) {
+        if (StringUtils.isNullOrEmpty(imgUrl)) {
+            return "";
+        }
+
+        String regExp = "(ftp|https?)\\:\\/\\/([\\w\\_\\-]+)\\.([\\w\\-]+[\\.]?)*[\\w]+\\.[a-zA-Z]{2,10}(.*)\\.(png|jpg|gif|jpeg)";
+        return RegExpUtil.find(imgUrl, regExp);
+    }
+
+//    public static void main(String[] args) {
+////        String testStr = "1111111<div>222&lt;div&gt;&lt;/div&gt;21000<sup>2</sup></div><div>3333399<sub>3</sub>5555555</div><div><br></div><div>444444</div>";
+//        String testStr = "{\"mainNumber\":2,\"order\":23,\"questionId\":\"5ebdf1cbcad4db1aee85e907\",\"studentAnswer\":\"发发呆<div>ななぅぅぉてふっふてふ</div><div class='photo-answers-block'><a href='https://ecs-test-static.qmth.com.cn/oe-answer-file/21083/19288/23/21083_19288_23_15895106360336244.jpeg' target='_blank' ><img class='photo-answer' src='https://ecs-test-static.qmth.com.cn/oe-answer-file/21083/19288/23/21083_19288_23_15895106360336244.jpeg?x-oss-process=image/resize,m_lfit,h_200,w_200' /></a></div>\",\"answerType\":null,\"answer\":\"<p><span> えいが</span></p>\",\"body\":\"<p>映画</p>\",\"parentBody\":null,\"questionType\":\"ESSAY\"}";
+//
+//        ImageTextHandler o = new ImageTextHandler();
+//
+//
+//        SectionCollectionBean sfds = o.handle(testStr);
+//
+//        System.out.println(JsonUtil.toJson(sfds));
+//    }
+}

+ 13 - 0
src/main/java/cn/com/qmth/examcloud/support/handler/richText/RichTextHandler.java

@@ -0,0 +1,13 @@
+package cn.com.qmth.examcloud.support.handler.richText;
+
+import cn.com.qmth.examcloud.support.handler.richText.bean.SectionCollectionBean;
+
+/**
+ * @Description 富文本处理器接口
+ * @Author lideyin
+ * @Date 2020/5/15 17:00
+ * @Version 1.0
+ */
+public interface RichTextHandler {
+    SectionCollectionBean handle(String richText);
+}

+ 41 - 0
src/main/java/cn/com/qmth/examcloud/support/handler/richText/RichTextHandlerFactory.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.support.handler.richText;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import com.mysql.cj.util.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @Description 富文本处理器工厂类
+ * @Author lideyin
+ * @Date 2020/5/15 16:33
+ * @Version 1.0
+ */
+public class RichTextHandlerFactory {
+    private static final Map<String,RichTextHandler> handlerMap=new HashMap<>();
+
+    static {
+        handlerMap.put("text",new HtmlTextHandler());
+        handlerMap.put("image",new ImageTextHandler());
+        handlerMap.put("audio",new AudioTextHandler());
+        handlerMap.put("complex",new ComplexTextHandler());
+    }
+
+    /**
+     * 根据类型获取实例
+     * @param type 文本类型
+     * @return
+     */
+    public static RichTextHandler getHandler(String type) {
+        if (StringUtils.isNullOrEmpty(type)) {
+            throw new StatusException("900101","参数type不允许为空");
+        }
+
+        if (!handlerMap.containsKey(type)) {
+            throw new StatusException("900102","不支持的文本类型");
+        }
+
+        return handlerMap.get(type);
+    }
+}

+ 70 - 0
src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/BlockBean.java

@@ -0,0 +1,70 @@
+package cn.com.qmth.examcloud.support.handler.richText.bean;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @Description 块
+ * @Author lideyin
+ * @Date 2020/3/2 18:35
+ * @Version 1.0
+ */
+public class BlockBean {
+    public BlockBean(String type) {
+        this.type = type;
+        this.param = new HashMap<>();
+        this.value = "";
+    }
+
+    /**
+     * text:文字
+     * image:图片
+     * audio:音频
+     * video:视频
+     */
+    private String type;
+    /**
+     * 资源相对路径
+     */
+    private String value;
+    /**
+     * 播放次数
+     */
+    private Integer playTime;
+
+    private Map<String, Object> param;
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public Map<String, Object> getParam() {
+        return param;
+    }
+
+    public void setParam(Map<String, Object> param) {
+        this.param = param;
+    }
+
+    public Integer getPlayTime() {
+        return playTime;
+    }
+
+    public void setPlayTime(Integer playTime) {
+        this.playTime = playTime;
+    }
+
+}
+

+ 31 - 0
src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/SectionBean.java

@@ -0,0 +1,31 @@
+package cn.com.qmth.examcloud.support.handler.richText.bean;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @Description 节点
+ * @Author lideyin
+ * @Date 2020/3/30 17:00
+ * @Version 1.0
+ */
+public class SectionBean {
+
+    public SectionBean() {
+        this.blocks=new ArrayList<>();
+    }
+
+    private List<BlockBean> blocks;
+
+    public List<BlockBean> getBlocks() {
+        return blocks;
+    }
+
+    public void setBlocks(List<BlockBean> blocks) {
+        this.blocks = blocks;
+    }
+
+
+
+}
+

+ 25 - 0
src/main/java/cn/com/qmth/examcloud/support/handler/richText/bean/SectionCollectionBean.java

@@ -0,0 +1,25 @@
+package cn.com.qmth.examcloud.support.handler.richText.bean;
+
+import java.util.List;
+
+/**
+ * @Description 节点集合
+ * @Author lideyin
+ * @Date 2020/3/30 16:59
+ * @Version 1.0
+ */
+public class SectionCollectionBean {
+
+    private List<SectionBean> sections;
+
+    public List<SectionBean> getSections() {
+        return sections;
+    }
+
+    public void setSections(List<SectionBean> sections) {
+        this.sections = sections;
+    }
+
+
+}
+

+ 220 - 0
src/main/java/cn/com/qmth/examcloud/support/helper/ExamCacheTransferHelper.java

@@ -0,0 +1,220 @@
+/*
+ * *************************************************
+ * Copyright (c) 2018 QMTH. All Rights Reserved.
+ * Created by Deason on 2018-08-29 10:44:05.
+ * *************************************************
+ */
+
+package cn.com.qmth.examcloud.support.helper;
+
+import cn.com.qmth.examcloud.api.commons.enums.ExamSpecialSettingsType;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+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 cn.com.qmth.examcloud.support.enums.ExamProperties;
+import io.swagger.annotations.ApiOperation;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+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 ExamSettingsCacheBean getCachedExam(Long examId, Long studentId) {
+        //默认取考试中的通用设置
+        ExamSettingsCacheBean examBean = CacheHelper.getExamSettings(examId);
+
+        //是否开启特殊设置
+        Boolean specialSettingsEnabled = examBean.getSpecialSettingsEnabled();
+        if (specialSettingsEnabled == true) {
+            ExamSpecialSettingsType specialSettingsType = examBean.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 ExamSettingsCacheBean getDefaultCachedExam(Long examId) {
+        return 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 CourseCacheBean getCachedCourse(Long courseId) {
+        return CacheHelper.getCourse(courseId);
+    }
+
+    @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, ExamSettingsCacheBean 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, ExamSettingsCacheBean 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());
+        }
+    }
+
+}

+ 160 - 0
src/main/java/cn/com/qmth/examcloud/support/helper/FaceBiopsyHelper.java

@@ -0,0 +1,160 @@
+package cn.com.qmth.examcloud.support.helper;
+
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+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 cn.com.qmth.examcloud.support.enums.ExamProperties;
+import cn.com.qmth.examcloud.support.enums.FaceBiopsyScheme;
+import cn.com.qmth.examcloud.support.privilege.PrivilegeDefine;
+import cn.com.qmth.examcloud.support.privilege.PrivilegeManager;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @Description 活体检测帮助类
+ * @Author lideyin
+ * @Date 2019/11/6 15:01
+ * @Version 1.0
+ */
+public class FaceBiopsyHelper {
+    /**
+     * 获取活体检测方案
+     *
+     * @param rootOrgId
+     * @return
+     */
+    public static FaceBiopsyScheme getFaceBiopsyScheme(Long rootOrgId) {
+        OrgPropertyCacheBean orgProperty = CacheHelper.getOrgProperty(rootOrgId,
+                Constants.IDENTIFICATION_OF_LIVING_BODY_SCHEME_KEY);
+        if (orgProperty.getHasValue()) {
+            if (FaceBiopsyScheme.FACE_ID.getCode().equals(orgProperty.getValue())) {
+                return FaceBiopsyScheme.FACE_ID;
+            }
+            return FaceBiopsyScheme.NEW;
+        }
+        //默认使用旧活体检测方案(即faceId方案)
+        return FaceBiopsyScheme.FACE_ID;
+    }
+
+    /**
+     * 是否开启人脸
+     *
+     * @param rootOrgId
+     * @param examId
+     * @param studentId
+     * @return
+     */
+    public static Boolean isFaceEnable(Long rootOrgId, Long examId, Long studentId) {
+        //判断是否有人脸识别的权限
+        String faceCheck = PrivilegeDefine.RootOrgFunctions.OnlineExamFunctions.FaceCheck.CODE;
+        Boolean hasFaceCheckFunction = PrivilegeManager.judge(rootOrgId, faceCheck);
+
+        //如果有权限,具体的人脸识别的配置值
+        ExamPropertyCacheBean examConf = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.IS_FACE_ENABLE.name());
+        String examValue = examConf.getValue();
+
+        if (!hasFaceCheckFunction) {
+            return false;
+        }
+
+        if (StringUtils.isBlank(examValue)) {
+            return false;
+        }
+
+        if (StringUtil.isTrue(examValue)) {
+            return true;
+        }
+
+        return false;
+
+    }
+
+    /**
+     * 是否开启人脸活体检测
+     *
+     * @param rootOrgId
+     * @param examId
+     * @param studentId
+     * @return
+     */
+    public static Boolean isFaceVerify(Long rootOrgId, Long examId, Long studentId) {
+        ExamSettingsCacheBean examSettings = CacheHelper.getExamSettings(examId);
+
+        //如果未开启人脸识别,则直接返回false
+        Boolean faceEnable = isFaceEnable(rootOrgId, examId, studentId);
+        if (!faceEnable) {
+            return false;
+        }
+
+        //人脸活体检测权限
+        String IdentificationOfLivingBody = PrivilegeDefine.RootOrgFunctions.OnlineExamFunctions.IdentificationOfLivingBody.CODE;
+        Boolean hasIdentificationOfLivingBodyFunction = PrivilegeManager
+                .judge(examSettings.getRootOrgId(), IdentificationOfLivingBody);
+
+        //人脸活体检测配置值
+        ExamPropertyCacheBean examConf = CacheHelper.getExamProperty(examId, ExamProperties.IS_FACE_VERIFY.name());
+        String examValue = examConf.getValue();
+
+        if (!hasIdentificationOfLivingBodyFunction) {
+            return false;
+        }
+
+        if (StringUtils.isBlank(examValue)) {
+            return false;
+        }
+
+        if (StringUtil.isTrue(examValue)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 进入考试是否验证人脸识别(强制、非强制)
+     *
+     * @return
+     */
+    public static Boolean isFaceCheck(Long examId, Long studentId) {
+        String isFaceCheck = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.IS_FACE_CHECK.name()).getValue();
+        if (StringUtils.isBlank(isFaceCheck)) {
+            return false;
+        }
+        return Boolean.valueOf(isFaceCheck);
+    }
+
+    /**
+     * 判断是否有人脸识别权限
+     *
+     * @param rootOrgId
+     * @param examId
+     * @return
+     * @author WANGWEI
+     */
+    private Boolean faceCheckEnabled(Long rootOrgId, Long examId, Long studentId) {
+        String faceCheck = PrivilegeDefine.RootOrgFunctions.OnlineExamFunctions.FaceCheck.CODE;
+        Boolean hasFaceCheckFunction = PrivilegeManager.judge(rootOrgId, faceCheck);
+
+        ExamPropertyCacheBean examConf = ExamCacheTransferHelper.getCachedExamProperty(examId,
+                studentId, ExamProperties.IS_FACE_ENABLE.name());
+        String examValue = examConf.getValue();
+
+        if (!hasFaceCheckFunction) {
+            return false;
+        }
+
+        if (StringUtils.isBlank(examValue)) {
+            return false;
+        }
+
+        if (StringUtil.isTrue(examValue)) {
+            return true;
+        }
+
+        return false;
+    }
+}

+ 80 - 0
src/main/java/cn/com/qmth/examcloud/support/helper/IdentityNumberHelper.java

@@ -0,0 +1,80 @@
+package cn.com.qmth.examcloud.support.helper;
+
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.support.Constants;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.OrgPropertyCacheBean;
+import org.apache.commons.lang3.StringUtils;
+
+public class IdentityNumberHelper {
+
+    /**
+     * 身份证从右向左第3位开始的6位字符为*,不足6位按实际位数来替换
+     *
+     * @param rootOrgId
+     * @param identityNumber
+     * @return
+     */
+    public static String conceal(Long rootOrgId, String identityNumber) {
+        if (rootOrgId == null) {
+            throw new StatusException("1001", "rootOrgId不能为空");
+        }
+        if (!identityNumbeConceal(rootOrgId)) {
+            return identityNumber;
+        }
+        if (StringUtils.isBlank(identityNumber)) {
+            return identityNumber;
+        }
+        if (identityNumber.length() <= 2) {
+            return identityNumber;
+        }
+
+        int startIndex = (identityNumber.length() - 8) < 0 ? 0 : (identityNumber.length() - 8);
+
+        return replaceStrToSymbol(identityNumber, startIndex, identityNumber.length() - 2, "*");
+    }
+
+    /**
+     * 将字符串内指定字符替换为特殊符号(包头不包尾)
+     *
+     * @param str   源字符串
+     * @param start 起始索引
+     * @param end   结束索引
+     * @return
+     */
+    private static String replaceStrToSymbol(String str, int start, int end, String symbol) {
+        StringBuilder sb = new StringBuilder();
+        String preStr = "";
+        String sufStr = "";
+        if (start > 0) {
+            preStr = str.substring(0, start);
+        }
+        sb.append(preStr);
+
+        for (int i = start; i < end; i++) {
+            sb.append(symbol);
+        }
+
+        if (str.length() > end + 1) {
+            sufStr = str.substring(end);
+        }
+
+        sb.append(sufStr);
+
+        return sb.toString();
+    }
+
+    private static boolean identityNumbeConceal(Long rootOrgId) {
+        OrgPropertyCacheBean orgProperty = CacheHelper.getOrgProperty(rootOrgId, Constants.ID_NUMBER_PRIVATE_MODE_KEY);
+
+        if (orgProperty == null) {
+            return false;
+        }
+
+        if (orgProperty.getHasValue()) {
+            return "true".equals(orgProperty.getValue());
+        } else {
+            return false;
+        }
+    }
+}

+ 86 - 0
src/main/java/cn/com/qmth/examcloud/support/privilege/PrivilegeDefine.java

@@ -0,0 +1,86 @@
+package cn.com.qmth.examcloud.support.privilege;
+
+/**
+ * 权限定义<br>
+ * 一个接口代表一个权限组或权限<br>
+ * CODE为权限组编码或权限编码 <br>
+ * <{@link PrivilegeDefine}内部第一级接口必须为权限组<br>
+ * 权限组下以树形结构定义权限<br>
+ *
+ * @author WANGWEI
+ * @date 2019年7月10日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public interface PrivilegeDefine {
+
+	// 数据访问权限组
+	public interface DataAccess {
+
+		String CODE = "DATA_ACCESS";
+
+		// 网考数据访问权限
+		public interface OnlineExamData {
+
+			String CODE = "ONLINE_EXAM_DATA";
+
+			// 违纪名单数据权限
+			public interface DataViolatingDiscipline {
+				String CODE = "DATA_VIOLATING_DISCIPLINE";
+
+				// 未通过审核的学生照片权限
+				public interface UnapprovedStudentPhoto {
+					String CODE = "UNAPPROVED_STUDENT_PHOTO";
+				}
+
+				// 所有学生照片权限
+				public interface AllStudentPhoto {
+					String CODE = "ALL_STUDENT_PHOTO";
+				}
+			}
+		}
+
+		// 考务数据访问权限
+		public interface ExamWorkData {
+
+			String CODE = "EXAM_WORK_DATA";
+
+			// 学生数据
+			public interface StudentInfoData {
+				String CODE = "STUDENT_INFO_DATA";
+
+				// 学习中心学生数据
+				public interface OrgStudentData {
+					String CODE = "ORG_STUDENT_DATA";
+				}
+
+				// 所有学生数据
+				public interface AllStudentData {
+					String CODE = "ALL_STUDENT_DATA";
+				}
+			}
+		}
+
+	}
+
+	// 顶级机构功能权限组
+	public interface RootOrgFunctions {
+		String CODE = "ROOT_ORG_FUNCTIONS";
+
+		// 网考功能
+		public interface OnlineExamFunctions {
+			String CODE = "ONLINE_EXAM_FUNCTIONS";
+
+			// 人脸识别
+			public interface FaceCheck {
+				String CODE = "FACE_CHECK";
+			}
+
+			// 活体检测
+			public interface IdentificationOfLivingBody {
+				String CODE = "IDENTIFICATION_OF_LIVING_BODY";
+			}
+		}
+
+	}
+
+}

+ 69 - 0
src/main/java/cn/com/qmth/examcloud/support/privilege/PrivilegeManager.java

@@ -0,0 +1,69 @@
+package cn.com.qmth.examcloud.support.privilege;
+
+import java.util.List;
+import java.util.Set;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.Role;
+import cn.com.qmth.examcloud.support.cache.CacheHelper;
+import cn.com.qmth.examcloud.support.cache.bean.PrivilegeRolesCacheBean;
+import cn.com.qmth.examcloud.support.cache.bean.RootOrgPrivilegesCacheBean;
+
+/**
+ * 权限管理器
+ *
+ * @author WANGWEI
+ * @date 2019年7月10日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class PrivilegeManager {
+
+	/**
+	 * 判断角色是否有权限
+	 *
+	 * @author WANGWEI
+	 * @param rootOrgId
+	 * @param roleList
+	 * @param privilegeCode
+	 * @return
+	 */
+	public static Boolean judge(Long rootOrgId, List<Role> roleList, String privilegeCode) {
+
+		PrivilegeRolesCacheBean cacheBean = CacheHelper.getPrivilegeRoles(rootOrgId, privilegeCode);
+		Set<Long> roleIdList = cacheBean.getRoleIdList();
+		Set<String> roleCodeList = cacheBean.getRoleCodeList();
+
+		for (Role cur : roleList) {
+			Long roleId = cur.getRoleId();
+			String roleCode = cur.getRoleCode();
+
+			if (null != roleId && roleIdList.contains(roleId)) {
+				return true;
+			}
+			if (null != roleCode && roleCodeList.contains(roleCode)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * 判断顶级机构是否有权限
+	 *
+	 * @author WANGWEI
+	 * @param rootOrgId
+	 * @param privilegeCode
+	 * @return
+	 */
+	public static Boolean judge(Long rootOrgId, String privilegeCode) {
+
+		RootOrgPrivilegesCacheBean cacheBean = CacheHelper.getRootOrgPrivileges(rootOrgId);
+
+		Set<String> privilegeCodeList = cacheBean.getPrivilegeCodeList();
+		if (privilegeCodeList.contains(privilegeCode)) {
+			return true;
+		}
+
+		return false;
+	}
+
+}

+ 36 - 0
src/main/java/cn/com/qmth/examcloud/support/redis/RedisKeyBuilder.java

@@ -0,0 +1,36 @@
+package cn.com.qmth.examcloud.support.redis;
+
+/**
+ * Redis key构建器接口
+ *
+ * @author WANGWEI
+ * @date 2019年8月16日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public interface RedisKeyBuilder {
+
+	@RedisKeyDefine(value = "网考考试会话", Prefix = "OE_SESSION")
+	String examingSessionKey(Long studentId);
+
+	@RedisKeyDefine(value = "考试活动时间", Prefix = "OE_ACTIVE")
+	String examingActiveTimeKey(Long examRecordDataId);
+
+	@RedisKeyDefine(value = "考试心跳", Prefix = "OE_HEARTBEAT")
+	String examingHeartbeatKey(Long examRecordDataId);
+
+	@RedisKeyDefine(value = "网考考试记录", Prefix = "OE_ERD")
+	String examRecordDataKey(Long examRecordDataId);
+
+	@RedisKeyDefine(value = "网考试卷结构", Prefix = "OE_PAPER")
+	String studentPaperKey(Long examRecordDataId);
+
+	@RedisKeyDefine(value = "网考作答", Prefix = "OE_ANSWER")
+	String studentAnswerKey(Long examRecordDataId, Integer order);
+
+	@RedisKeyDefine(value = "文件作答", Prefix = "OE_File_ANSWER")
+	String studentFileAnswerKey(Long examRecordDataId, Integer order);
+
+	@RedisKeyDefine(value = "考试控制属性", Prefix = "OE_BOSS")
+	String examBossKey(Long examStudentId);
+
+}

+ 24 - 0
src/main/java/cn/com/qmth/examcloud/support/redis/RedisKeyDefine.java

@@ -0,0 +1,24 @@
+package cn.com.qmth.examcloud.support.redis;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * redis key定义
+ *
+ * @author WANGWEI
+ * @date 2019年12月12日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RedisKeyDefine {
+
+	String value();
+
+	String Prefix();
+}

+ 59 - 0
src/main/java/cn/com/qmth/examcloud/support/redis/RedisKeyHelper.java

@@ -0,0 +1,59 @@
+package cn.com.qmth.examcloud.support.redis;
+
+import java.lang.reflect.Method;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.cglib.proxy.Enhancer;
+import org.springframework.cglib.proxy.MethodInterceptor;
+import org.springframework.cglib.proxy.MethodProxy;
+
+/**
+ * redis key helper
+ *
+ * @author WANGWEI
+ * @date 2019年12月12日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class RedisKeyHelper {
+
+	private static RedisKeyBuilder redisKeyBuilder;
+
+	/**
+	 * 创建 {@link RedisKeyBuilder} 实例
+	 *
+	 * @author WANGWEI
+	 */
+	private static void newInstance() {
+		Enhancer enhancer = new Enhancer();
+		enhancer.setSuperclass(RedisKeyBuilder.class);
+		enhancer.setCallback(new MethodInterceptor() {
+			@Override
+			public Object intercept(Object obj, Method method, Object[] objects,
+					MethodProxy methodProxy) throws Throwable {
+				RedisKeyDefine define = method.getAnnotation(RedisKeyDefine.class);
+				String prefix = define.Prefix();
+				String key = prefix + ":" + StringUtils.join(objects, '_');
+				return key;
+			}
+		});
+		redisKeyBuilder = (RedisKeyBuilder) enhancer.create();
+	}
+
+	/**
+	 * 获取 {@link RedisKeyBuilder} 实例
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static RedisKeyBuilder getBuilder() {
+		if (null == redisKeyBuilder) {
+			synchronized (RedisKeyHelper.class) {
+				if (null == redisKeyBuilder) {
+					newInstance();
+				}
+			}
+		}
+		return redisKeyBuilder;
+	}
+
+}

+ 49 - 0
src/test/java/cn/com/qmth/examcloud/support/test/Block.java

@@ -0,0 +1,49 @@
+package cn.com.qmth.examcloud.support.test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Block {
+
+    private String type;
+
+    private String value;
+
+    /**
+     * 播放次数
+     */
+    private Integer playTime;
+
+    public Integer getPlayTime() {
+        return playTime;
+    }
+
+    public void setPlayTime(Integer playTime) {
+        this.playTime = playTime;
+    }
+
+    private Map<String, Object> param;
+
+    public Block(BlockType type) {
+        this.type = type.name().toLowerCase();
+        this.param = new HashMap<>();
+        this.value = "";
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public Map<String, Object> getParam() {
+        return param;
+    }
+
+}

+ 6 - 0
src/test/java/cn/com/qmth/examcloud/support/test/BlockType.java

@@ -0,0 +1,6 @@
+package cn.com.qmth.examcloud.support.test;
+
+public enum BlockType {
+
+    TEXT, IMAGE, AUDIO;
+}

+ 162 - 0
src/test/java/cn/com/qmth/examcloud/support/test/Context.java

@@ -0,0 +1,162 @@
+package cn.com.qmth.examcloud.support.test;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.StringUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.nodes.TextNode;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Context {
+
+    private static final Map<String, String> TEXT_PARAM_MAP = new HashMap<>();
+
+    static {
+        TEXT_PARAM_MAP.put("b", "bold");
+        TEXT_PARAM_MAP.put("i", "italic");
+        TEXT_PARAM_MAP.put("strong", "bold");
+        TEXT_PARAM_MAP.put("em", "italic");
+        TEXT_PARAM_MAP.put("u", "underline");
+        TEXT_PARAM_MAP.put("sup", "sup");
+        TEXT_PARAM_MAP.put("sub", "sub");
+    }
+
+    private List<Section> sections;
+
+    private Section currentSection;
+
+    private Map<String, Object> currentTextParam;
+
+    public Context() {
+        this.sections = new ArrayList<>();
+        this.currentTextParam = new HashMap<>();
+    }
+
+    public List<Section> process(String input) {
+        Element body = Jsoup.parseBodyFragment(StringUtils.trimToEmpty(input)).body();
+        switchSection();
+        for (Node child : body.childNodes()) {
+            parseNode(child);
+        }
+        return sections;
+    }
+
+    private void parseNode(Node node) {
+        if (node instanceof TextNode) {
+            TextNode textNode = (TextNode) node;
+            parseText(textNode.text());
+        } else if (node instanceof Element) {
+            Element element = (Element) node;
+            onElementStart(element);
+            for (Node child : element.childNodes()) {
+                parseNode(child);
+            }
+            onElementEnd(element);
+        }
+    }
+
+    private void parseText(String text) {
+        Block block = new Block(BlockType.TEXT);
+        block.setValue(StringEscapeUtils.unescapeHtml(text));
+        block.getParam().putAll(currentTextParam);
+        addBlock(block);
+    }
+
+    private void parseImage(Element element) {
+        Block block = new Block(BlockType.IMAGE);
+
+        String src = element.attr("src");
+        block.setValue(src);
+
+        Map<String, Object> param = new HashMap<>();
+        String height = element.attr("height");
+        String width = element.attr("width");
+        param.put("width", height);
+        param.put("height", width);
+        block.getParam().putAll(param);
+
+        addBlock(block);
+    }
+
+    private void parseAudio(Element element) {
+        //todo 此处为题库特殊处理,待确认
+        String id = element.attr("id");
+        String name = element.attr("name");
+        if (StringUtils.isNotEmpty(id) && StringUtils.isNotEmpty(name)) {
+            Block block = new Block(BlockType.AUDIO);
+            block.setPlayTime(1);
+            block.setValue(element.attr("url"));
+            addBlock(block);
+        }
+    }
+
+    private void onElementStart(Element element) {
+        String name = element.nodeName().toLowerCase();
+        switch (name) {
+            case "div":
+            case "p":
+                switchSection();
+                break;
+            case "br":
+                finishSection();
+                break;
+            case "img":
+                parseImage(element);
+                break;
+            case "audio":// TODO
+            case "a":// TODO
+                parseAudio(element);
+                break;
+            default:
+                String style = TEXT_PARAM_MAP.get(name);
+                if (style != null) {
+                    currentTextParam.put(style, true);
+                }
+        }
+    }
+
+    private void onElementEnd(Element element) {
+        String name = element.nodeName().toLowerCase();
+        switch (name) {
+            case "div":
+            case "p":
+            case "span":
+                finishSection();
+                break;
+            case "img":// TODO
+            case "audio":// TODO
+            case "a":// TODO
+//                finishSection();
+                break;
+            default:
+                String style = TEXT_PARAM_MAP.get(name);
+                if (style != null) {
+                    currentTextParam.remove(style);
+                }
+        }
+    }
+
+    private void addBlock(Block block) {
+        if (currentSection == null) {
+            switchSection();
+        }
+        currentSection.addBlock(block);
+    }
+
+    private void finishSection() {
+        currentSection = null;
+    }
+
+    private void switchSection() {
+        Section section = new Section();
+        sections.add(section);
+        currentSection = section;
+        currentTextParam.clear();
+    }
+
+}

+ 13 - 0
src/test/java/cn/com/qmth/examcloud/support/test/RedisKeyBuilderTest.java

@@ -0,0 +1,13 @@
+package cn.com.qmth.examcloud.support.test;
+
+import cn.com.qmth.examcloud.support.redis.RedisKeyHelper;
+
+public class RedisKeyBuilderTest {
+
+	public static void main(String[] args) {
+
+		System.out.println(RedisKeyHelper.getBuilder().examRecordDataKey(10L));
+		System.out.println(RedisKeyHelper.getBuilder().studentAnswerKey(10L, 1));
+	}
+
+}

+ 21 - 0
src/test/java/cn/com/qmth/examcloud/support/test/Section.java

@@ -0,0 +1,21 @@
+package cn.com.qmth.examcloud.support.test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Section {
+
+    private List<Block> blocks;
+
+    public Section() {
+        this.blocks = new ArrayList<>();
+    }
+
+    public void addBlock(Block block) {
+        this.blocks.add(block);
+    }
+
+    public List<Block> getBlocks() {
+        return blocks;
+    }
+}

+ 19 - 0
src/test/java/cn/com/qmth/examcloud/support/test/Test.java

@@ -0,0 +1,19 @@
+package cn.com.qmth.examcloud.support.test;
+
+import net.sf.json.JSONArray;
+import org.apache.commons.lang.StringUtils;
+
+public class Test {
+
+    public static void main(String[] args) {
+        String input = "<br>普通文本<br><div>这个被div包裹着</div><p>这个被p包裹 着</p><div><b>加粗<i>加粗且斜体<u>加粗且斜体且有下划线</u><b>加粗且斜体且有重复加粗标签</b></i>100<sup>2</sup></b></div>普通文本log<sub>3</sub>";
+//        String t2="<p><strong><em>杆件变形</em></strong><a id='a' name='1' url='http://1.mp3'></a>的基本形式有哪几种?<img src='http://www.baidu.com'/>何谓构件的强度,刚度和稳定性?</p>";
+//        String t2="<p><strong><em>杆件变形</em></strong>的基本形式有哪几种?何谓构件的强度,刚度和稳定性?</p>";
+        Context context = new Context();
+
+        System.out.println(JSONArray.fromObject(context.process(input)));
+
+//        System.out.println(JSONArray.fromObject(context.process(t2)));
+    }
+
+}