Ver código fonte

Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	themis-business/src/main/java/com/qmth/themis/business/cache/ExamRecordCacheUtil.java
#	themis-mq/src/main/java/com/qmth/themis/mq/enums/MqTagEnum.java
wangliang 4 anos atrás
pai
commit
5a61d36f7b
20 arquivos alterados com 1106 adições e 193 exclusões
  1. 102 101
      themis-business/pom.xml
  2. 7 3
      themis-business/src/main/java/com/qmth/themis/business/cache/ExamRecordCacheUtil.java
  3. 1 1
      themis-business/src/main/java/com/qmth/themis/business/cache/RedisKeyHelper.java
  4. 530 0
      themis-business/src/main/java/com/qmth/themis/business/cache/bean/ExamCacheBean.java
  5. 22 0
      themis-business/src/main/java/com/qmth/themis/business/cache/bean/ExamStudentAnswerCacheBean.java
  6. 20 6
      themis-business/src/main/java/com/qmth/themis/business/cache/bean/ObjectiveAnswerCacheBean.java
  7. 2 0
      themis-business/src/main/java/com/qmth/themis/business/constant/SystemConstant.java
  8. 2 0
      themis-business/src/main/java/com/qmth/themis/business/enums/MqEnum.java
  9. 3 0
      themis-business/src/main/java/com/qmth/themis/business/service/TEExamService.java
  10. 5 0
      themis-business/src/main/java/com/qmth/themis/business/service/TOeExamRecordService.java
  11. 72 1
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TEExamPaperServiceImpl.java
  12. 21 2
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TEExamServiceImpl.java
  13. 171 63
      themis-business/src/main/java/com/qmth/themis/business/service/impl/TOeExamRecordServiceImpl.java
  14. 2 2
      themis-business/src/main/java/com/qmth/themis/business/templete/impl/TaskExamPaperImportTemplete.java
  15. 23 2
      themis-exam/src/main/java/com/qmth/themis/exam/api/TEExamController.java
  16. 13 8
      themis-exam/src/main/java/com/qmth/themis/exam/start/StartRunning.java
  17. 6 1
      themis-mq/src/main/java/com/qmth/themis/mq/enums/MqGroupEnum.java
  18. 5 2
      themis-mq/src/main/java/com/qmth/themis/mq/enums/MqTagEnum.java
  19. 6 1
      themis-mq/src/main/java/com/qmth/themis/mq/enums/MqTopicEnum.java
  20. 93 0
      themis-mq/src/main/java/com/qmth/themis/mq/templete/impl/CalculateObjectiveScoreConcurrentlyImpl.java

+ 102 - 101
themis-business/pom.xml

@@ -1,105 +1,106 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <groupId>com.qmth.themis.business</groupId>
-    <artifactId>themis-business</artifactId>
-    <version>1.0.0</version>
-    <packaging>jar</packaging>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>com.qmth.themis.business</groupId>
+	<artifactId>themis-business</artifactId>
+	<version>1.0.0</version>
+	<packaging>jar</packaging>
 
-    <parent>
-        <groupId>com.qmth.themis</groupId>
-        <artifactId>themis-service</artifactId>
-        <version>1.0.0</version>
-    </parent>
+	<parent>
+		<groupId>com.qmth.themis</groupId>
+		<artifactId>themis-service</artifactId>
+		<version>1.0.0</version>
+	</parent>
 
-    <dependencies>
-        <dependency>
-            <groupId>com.qmth.themis.common</groupId>
-            <artifactId>themis-common</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>commons-fileupload</groupId>
-            <artifactId>commons-fileupload</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.poi</groupId>
-            <artifactId>poi-ooxml</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.poi</groupId>
-            <artifactId>poi-ooxml-schemas</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.poi</groupId>
-            <artifactId>poi</artifactId>
-        </dependency>
-        <!-- ehcache 缓存 -->
-        <dependency>
-            <groupId>net.sf.ehcache</groupId>
-            <artifactId>ehcache</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-cache</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.springframework.data</groupId>
-            <artifactId>spring-data-redis</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>redis.clients</groupId>
-            <artifactId>jedis</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>com.aliyun.oss</groupId>
-            <artifactId>aliyun-sdk-oss</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>mysql</groupId>
-            <artifactId>mysql-connector-java</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-boot-starter</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>ch.qos.logback</groupId>
-            <artifactId>logback-core</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>ch.qos.logback</groupId>
-            <artifactId>logback-classic</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>io.springfox</groupId>
-            <artifactId>springfox-swagger2</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>io.springfox</groupId>
-            <artifactId>springfox-swagger-ui</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>com.alibaba</groupId>
-            <artifactId>druid</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-aop</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.jetbrains</groupId>
-            <artifactId>annotations</artifactId>
-            <version>13.0</version>
-            <scope>compile</scope>
-        </dependency>
-        <!--        <dependency>-->
-<!--            <groupId>org.apache.rocketmq</groupId>-->
-<!--            <artifactId>rocketmq-spring-boot-starter</artifactId>-->
-<!--        </dependency>-->
-<!--        <dependency>-->
-<!--            <groupId>org.springframework.boot</groupId>-->
-<!--            <artifactId>spring-boot-starter-quartz</artifactId>-->
-<!--        </dependency>-->
-    </dependencies>
+	<dependencies>
+		<dependency>
+			<groupId>com.qmth.themis.common</groupId>
+			<artifactId>themis-common</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>commons-fileupload</groupId>
+			<artifactId>commons-fileupload</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.poi</groupId>
+			<artifactId>poi-ooxml</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.poi</groupId>
+			<artifactId>poi-ooxml-schemas</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.poi</groupId>
+			<artifactId>poi</artifactId>
+		</dependency>
+		<!-- ehcache 缓存 -->
+		<dependency>
+			<groupId>net.sf.ehcache</groupId>
+			<artifactId>ehcache</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-cache</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.data</groupId>
+			<artifactId>spring-data-redis</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>redis.clients</groupId>
+			<artifactId>jedis</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.aliyun.oss</groupId>
+			<artifactId>aliyun-sdk-oss</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>mysql</groupId>
+			<artifactId>mysql-connector-java</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.baomidou</groupId>
+			<artifactId>mybatis-plus-boot-starter</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-core</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-classic</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>io.springfox</groupId>
+			<artifactId>springfox-swagger2</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>io.springfox</groupId>
+			<artifactId>springfox-swagger-ui</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>druid</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-aop</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.jetbrains</groupId>
+			<artifactId>annotations</artifactId>
+			<version>13.0</version>
+			<scope>compile</scope>
+		</dependency>
+		<!-- <dependency> -->
+		<!-- <groupId>org.apache.rocketmq</groupId> -->
+		<!-- <artifactId>rocketmq-spring-boot-starter</artifactId> -->
+		<!-- </dependency> -->
+		<!-- <dependency> -->
+		<!-- <groupId>org.springframework.boot</groupId> -->
+		<!-- <artifactId>spring-boot-starter-quartz</artifactId> -->
+		<!-- </dependency> -->
+	</dependencies>
 </project>

+ 7 - 3
themis-business/src/main/java/com/qmth/themis/business/cache/ExamRecordCacheUtil.java

@@ -41,8 +41,12 @@ public class ExamRecordCacheUtil {
 	public static Double getObjectiveScore(Long recordId) {
 		return (Double) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId), "objectiveScore");
 	}
-
-	public static Date getClientLastSyncTime(Long recordId) {
-		return (Date) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId), "clientLastSyncTime");
+	
+	public static Long getExamId(Long recordId) {
+		return (Long) redisUtil.get(RedisKeyHelper.examRecordCacheKey(recordId), "examId");
+	}
+	
+	public static void setObjectiveScore(Long recordId,Double objectiveScore) {
+		redisUtil.set(RedisKeyHelper.examRecordCacheKey(recordId), "objectiveScore",objectiveScore);
 	}
 }

+ 1 - 1
themis-business/src/main/java/com/qmth/themis/business/cache/RedisKeyHelper.java

@@ -72,7 +72,7 @@ public class RedisKeyHelper {
 		if (subIndex == null) {
 			return mainNumber + underLine + subNumber;
 		} else {
-			return mainNumber + underLine + subNumber + underLine + subIndex + underLine;
+			return mainNumber + underLine + subNumber + underLine + subIndex;
 		}
 	}
 	

+ 530 - 0
themis-business/src/main/java/com/qmth/themis/business/cache/bean/ExamCacheBean.java

@@ -0,0 +1,530 @@
+package com.qmth.themis.business.cache.bean;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import com.qmth.themis.business.enums.EntryAuthenticationPolicyEnum;
+import com.qmth.themis.business.enums.ExamModeEnum;
+import com.qmth.themis.business.enums.InProcessLivenessJudgePolicyEnum;
+import com.qmth.themis.business.enums.ObjectiveScorePolicyEnum;
+import com.qmth.themis.business.enums.RecordSelectStrategyEnum;
+import com.qmth.themis.business.enums.ScoreStatusEnum;
+
+public class ExamCacheBean implements Serializable {
+
+    /**
+	 * 
+	 */
+	private static final long serialVersionUID = 146145141337738358L;
+	// 主键
+	private Long id;
+	
+	//机构ID
+    private Long orgId;
+
+    //批次编码,机构唯一
+    private String code;
+
+    //批次名称
+    private String name;
+
+    //批次标注,可用于多个考试批次归类
+    private String tag;
+
+    //批次开始时间
+    private Date startTime;
+
+    //批次结束时间
+    private Date endTime;
+
+    //考试口令
+    private String shortCode;
+
+    //提前多长时间开始候考(分钟)
+    private Integer prepareSeconds;
+
+    //最短考试时长,相当于考试冻结时间(分钟)
+    private Integer minDurationSeconds;
+
+    //考前须知
+    private String preNotice;
+
+    //考试须知阅读时长(秒)
+    private Integer preNoticeStaySeconds;
+
+    //考后说明,针对一个场次进行设置
+    private String postNotice;
+
+    //允许考试次数
+    private Integer examCount;
+
+    //断点失效时间(秒)
+    private Integer breakExpireSeconds;
+
+    //断点续考次数
+    private Integer breakResumeCount;
+
+    //是否开启/强制客户端视频监控,0:不开启,1:开启,2:强制开启
+    private Integer clientVideoPush;
+
+    //是否开启客户端视频转录,0:不开启,1:开启
+    private Integer clientVideoRecord;
+
+    //是否开启/强制微信小程序监控,0:不开启,1:开启,2:强制开启
+    private Integer wxappVideoPush;
+
+    //是否开启微信小程序视频转录,0:不开启,1:开启
+    private Integer wxappVideoRecord;
+
+    //是否允许使用摄像头拍照答题,0:不允许,1:允许
+    private Integer cameraPhotoUpload;
+
+    //是否允许使用微信拍照答题,0:不允许,1:允许
+    private Integer wxappPhotoUpload;
+
+    //重考是否审批,0:不审批,1:审批
+    private Integer reexamAuditing;
+
+    //交卷后是否显示客观得分,0:不显示,1:显示
+    private Integer showObjectiveScore;
+
+    //模式,0:集中统一,1:随到随考
+    private ExamModeEnum mode;
+
+    //是否启用,0:停用,1:启用
+    private Integer enable;
+
+    //是否归档,0:不归档,1:归档
+    private Integer archived;
+
+    //允许开考开放时长(分钟),相当于迟到时间
+    private Integer openingSeconds;
+
+    //最大考试时长(分钟)
+    private Integer maxDurationSeconds;
+
+    //是否在结束时间集中强制收卷,0:不强制,1:强制
+    private Integer forceFinish;
+
+    //开考身份验证策略,off:关闭,face_verify_optional:非强制人脸验证,face_verify_force:强制人脸验证,liveness:活体验证
+    private EntryAuthenticationPolicyEnum entryAuthenticationPolicy;
+
+    //考试过程中人脸检测是否开启,0:不开启,1:开启
+    private Integer inProcessFaceVerify;
+
+    //考试过程中人脸检测是否忽略陌生人 ,0:不忽略,1:忽略
+    private Integer inProcessFaceStrangerIgnore;
+
+    //考试过程中是否启用活体检测 ,0:不启用,1:启用
+    private Integer inProcessLivenessVerify;
+
+    //考试过程中活体检测间隔时间
+    private Integer inProcessLivenessIntervalSeconds;
+
+    //考试过程中活体检测重试次数
+    private Integer inProcessLivenessRetryCount;
+
+    //考试过程中活体检测结果判定规则,any:任意一次通过,all:全部都要通过,more:通过次数大于失败次数
+    private InProcessLivenessJudgePolicyEnum inProcessLivenessJudgePolicy;
+
+    //多次考试记录的选择逻辑,highest_total_score:全部阅卷后取最高分,highest_objective_score:客观分最高,latest:最后一次提交
+    private RecordSelectStrategyEnum recordSelectStrategy;
+
+    //是否IP段限制,0:不允许,1:允许
+    private Integer enableIpLimit;
+
+    //允许IP段
+    private String ipAllow;
+
+    //算分状态,never:从未算分,calculating:正在算分,finish:算分完成
+    private ScoreStatusEnum scoreStatus;
+
+    //是否启用开考口令,0:不启用,1:启用
+    private Integer enableShortCode;
+
+    //是否允许断点续考,0:不允许,1:允许
+    private Integer enableBreak;
+
+    //客观题给分策略,equal: 全对给分,partial: 漏选给一半分
+    private ObjectiveScorePolicyEnum objectiveScorePolicy;
+
+    //开启监控的视频源
+    private String monitorVideoSource;
+
+    //是否开始监控转录,0:开启,1:不开启
+    private Integer monitorRecord;
+
+    public String getMonitorVideoSource() {
+        return monitorVideoSource;
+    }
+
+    public void setMonitorVideoSource(String monitorVideoSource) {
+        this.monitorVideoSource = monitorVideoSource;
+    }
+
+    public Integer getMonitorRecord() {
+        return monitorRecord;
+    }
+
+    public void setMonitorRecord(Integer monitorRecord) {
+        this.monitorRecord = monitorRecord;
+    }
+
+    public Integer getEnableBreak() {
+        return enableBreak;
+    }
+
+    public void setEnableBreak(Integer enableBreak) {
+        this.enableBreak = enableBreak;
+    }
+
+    public Integer getEnableShortCode() {
+        return enableShortCode;
+    }
+
+    public void setEnableShortCode(Integer enableShortCode) {
+        this.enableShortCode = enableShortCode;
+    }
+
+    public ExamModeEnum getMode() {
+        return mode;
+    }
+
+    public void setMode(ExamModeEnum mode) {
+        this.mode = mode;
+    }
+
+    public Integer getEnable() {
+        return enable;
+    }
+
+    public void setEnable(Integer enable) {
+        this.enable = enable;
+    }
+
+    public Integer getArchived() {
+        return archived;
+    }
+
+    public void setArchived(Integer archived) {
+        this.archived = archived;
+    }
+
+    public Integer getOpeningSeconds() {
+        return openingSeconds;
+    }
+
+    public void setOpeningSeconds(Integer openingSeconds) {
+        this.openingSeconds = openingSeconds;
+    }
+
+    public Integer getMaxDurationSeconds() {
+        return maxDurationSeconds;
+    }
+
+    public void setMaxDurationSeconds(Integer maxDurationSeconds) {
+        this.maxDurationSeconds = maxDurationSeconds;
+    }
+
+    public Integer getForceFinish() {
+        return forceFinish;
+    }
+
+    public void setForceFinish(Integer forceFinish) {
+        this.forceFinish = forceFinish;
+    }
+
+    public EntryAuthenticationPolicyEnum getEntryAuthenticationPolicy() {
+        return entryAuthenticationPolicy;
+    }
+
+    public void setEntryAuthenticationPolicy(EntryAuthenticationPolicyEnum entryAuthenticationPolicy) {
+        this.entryAuthenticationPolicy = entryAuthenticationPolicy;
+    }
+
+    public Integer getInProcessFaceVerify() {
+        return inProcessFaceVerify;
+    }
+
+    public void setInProcessFaceVerify(Integer inProcessFaceVerify) {
+        this.inProcessFaceVerify = inProcessFaceVerify;
+    }
+
+    public Integer getInProcessFaceStrangerIgnore() {
+        return inProcessFaceStrangerIgnore;
+    }
+
+    public void setInProcessFaceStrangerIgnore(Integer inProcessFaceStrangerIgnore) {
+        this.inProcessFaceStrangerIgnore = inProcessFaceStrangerIgnore;
+    }
+
+    public Integer getInProcessLivenessVerify() {
+        return inProcessLivenessVerify;
+    }
+
+    public void setInProcessLivenessVerify(Integer inProcessLivenessVerify) {
+        this.inProcessLivenessVerify = inProcessLivenessVerify;
+    }
+
+    public Integer getInProcessLivenessIntervalSeconds() {
+        return inProcessLivenessIntervalSeconds;
+    }
+
+    public void setInProcessLivenessIntervalSeconds(Integer inProcessLivenessIntervalSeconds) {
+        this.inProcessLivenessIntervalSeconds = inProcessLivenessIntervalSeconds;
+    }
+
+    public Integer getInProcessLivenessRetryCount() {
+        return inProcessLivenessRetryCount;
+    }
+
+    public void setInProcessLivenessRetryCount(Integer inProcessLivenessRetryCount) {
+        this.inProcessLivenessRetryCount = inProcessLivenessRetryCount;
+    }
+
+    public InProcessLivenessJudgePolicyEnum getInProcessLivenessJudgePolicy() {
+        return inProcessLivenessJudgePolicy;
+    }
+
+    public void setInProcessLivenessJudgePolicy(InProcessLivenessJudgePolicyEnum inProcessLivenessJudgePolicy) {
+        this.inProcessLivenessJudgePolicy = inProcessLivenessJudgePolicy;
+    }
+
+    public RecordSelectStrategyEnum getRecordSelectStrategy() {
+        return recordSelectStrategy;
+    }
+
+    public void setRecordSelectStrategy(RecordSelectStrategyEnum recordSelectStrategy) {
+        this.recordSelectStrategy = recordSelectStrategy;
+    }
+
+    public Integer getEnableIpLimit() {
+        return enableIpLimit;
+    }
+
+    public void setEnableIpLimit(Integer enableIpLimit) {
+        this.enableIpLimit = enableIpLimit;
+    }
+
+    public String getIpAllow() {
+        return ipAllow;
+    }
+
+    public void setIpAllow(String ipAllow) {
+        this.ipAllow = ipAllow;
+    }
+
+    public ScoreStatusEnum getScoreStatus() {
+        return scoreStatus;
+    }
+
+    public void setScoreStatus(ScoreStatusEnum scoreStatus) {
+        this.scoreStatus = scoreStatus;
+    }
+
+    public static long getSerialVersionUID() {
+        return serialVersionUID;
+    }
+
+    public Long getOrgId() {
+        return orgId;
+    }
+
+    public void setOrgId(Long orgId) {
+        this.orgId = orgId;
+    }
+
+    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 getTag() {
+        return tag;
+    }
+
+    public void setTag(String tag) {
+        this.tag = tag;
+    }
+
+    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 String getShortCode() {
+        return shortCode;
+    }
+
+    public void setShortCode(String shortCode) {
+        this.shortCode = shortCode;
+    }
+
+    public Integer getPrepareSeconds() {
+        return prepareSeconds;
+    }
+
+    public void setPrepareSeconds(Integer prepareSeconds) {
+        this.prepareSeconds = prepareSeconds;
+    }
+
+    public Integer getMinDurationSeconds() {
+        return minDurationSeconds;
+    }
+
+    public void setMinDurationSeconds(Integer minDurationSeconds) {
+        this.minDurationSeconds = minDurationSeconds;
+    }
+
+    public String getPreNotice() {
+        return preNotice;
+    }
+
+    public void setPreNotice(String preNotice) {
+        this.preNotice = preNotice;
+    }
+
+    public Integer getPreNoticeStaySeconds() {
+        return preNoticeStaySeconds;
+    }
+
+    public void setPreNoticeStaySeconds(Integer preNoticeStaySeconds) {
+        this.preNoticeStaySeconds = preNoticeStaySeconds;
+    }
+
+    public String getPostNotice() {
+        return postNotice;
+    }
+
+    public void setPostNotice(String postNotice) {
+        this.postNotice = postNotice;
+    }
+
+    public Integer getExamCount() {
+        return examCount;
+    }
+
+    public void setExamCount(Integer examCount) {
+        this.examCount = examCount;
+    }
+
+    public Integer getBreakExpireSeconds() {
+        return breakExpireSeconds;
+    }
+
+    public void setBreakExpireSeconds(Integer breakExpireSeconds) {
+        this.breakExpireSeconds = breakExpireSeconds;
+    }
+
+    public Integer getBreakResumeCount() {
+        return breakResumeCount;
+    }
+
+    public void setBreakResumeCount(Integer breakResumeCount) {
+        this.breakResumeCount = breakResumeCount;
+    }
+
+    public Integer getClientVideoPush() {
+        return clientVideoPush;
+    }
+
+    public void setClientVideoPush(Integer clientVideoPush) {
+        this.clientVideoPush = clientVideoPush;
+    }
+
+    public Integer getClientVideoRecord() {
+        return clientVideoRecord;
+    }
+
+    public void setClientVideoRecord(Integer clientVideoRecord) {
+        this.clientVideoRecord = clientVideoRecord;
+    }
+
+    public Integer getWxappVideoPush() {
+        return wxappVideoPush;
+    }
+
+    public void setWxappVideoPush(Integer wxappVideoPush) {
+        this.wxappVideoPush = wxappVideoPush;
+    }
+
+    public Integer getWxappVideoRecord() {
+        return wxappVideoRecord;
+    }
+
+    public void setWxappVideoRecord(Integer wxappVideoRecord) {
+        this.wxappVideoRecord = wxappVideoRecord;
+    }
+
+    public Integer getCameraPhotoUpload() {
+        return cameraPhotoUpload;
+    }
+
+    public void setCameraPhotoUpload(Integer cameraPhotoUpload) {
+        this.cameraPhotoUpload = cameraPhotoUpload;
+    }
+
+    public Integer getWxappPhotoUpload() {
+        return wxappPhotoUpload;
+    }
+
+    public void setWxappPhotoUpload(Integer wxappPhotoUpload) {
+        this.wxappPhotoUpload = wxappPhotoUpload;
+    }
+
+    public Integer getReexamAuditing() {
+        return reexamAuditing;
+    }
+
+    public void setReexamAuditing(Integer reexamAuditing) {
+        this.reexamAuditing = reexamAuditing;
+    }
+
+    public Integer getShowObjectiveScore() {
+        return showObjectiveScore;
+    }
+
+    public void setShowObjectiveScore(Integer showObjectiveScore) {
+        this.showObjectiveScore = showObjectiveScore;
+    }
+
+    public ObjectiveScorePolicyEnum getObjectiveScorePolicy() {
+        return objectiveScorePolicy;
+    }
+
+    public void setObjectiveScorePolicy(ObjectiveScorePolicyEnum objectiveScorePolicy) {
+        this.objectiveScorePolicy = objectiveScorePolicy;
+    }
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+    
+    
+}

+ 22 - 0
themis-business/src/main/java/com/qmth/themis/business/cache/bean/ExamStudentAnswerCacheBean.java

@@ -24,6 +24,12 @@ public class ExamStudentAnswerCacheBean implements Serializable {
 
 	// 作答时长 秒
 	private Integer duration;
+	
+	//得分
+	private Double score;
+	
+	//是否客观题
+	private Boolean objective;
 
 	public String getContent() {
 		return content;
@@ -73,5 +79,21 @@ public class ExamStudentAnswerCacheBean implements Serializable {
 		this.subIndex = subIndex;
 	}
 
+	public Double getScore() {
+		return score;
+	}
+
+	public void setScore(Double score) {
+		this.score = score;
+	}
+
+	public Boolean getObjective() {
+		return objective;
+	}
+
+	public void setObjective(Boolean objective) {
+		this.objective = objective;
+	}
+
 	
 }

+ 20 - 6
themis-business/src/main/java/com/qmth/themis/business/cache/bean/ObjectiveAnswerCacheBean.java

@@ -2,6 +2,8 @@ package com.qmth.themis.business.cache.bean;
 
 import java.io.Serializable;
 
+import com.alibaba.fastjson.JSONArray;
+
 /**
  * 客观题标答缓存
  * 
@@ -20,8 +22,11 @@ public class ObjectiveAnswerCacheBean implements Serializable {
 	// 题型
 	private Integer structType;
 
-	// 标答
-	private String rightAnswer;
+	// 选择题标答
+	private JSONArray choiceAnswer;
+
+	// 判断题标答
+	private Boolean boolAnswer;
 
 	public Double getScore() {
 		return score;
@@ -39,12 +44,21 @@ public class ObjectiveAnswerCacheBean implements Serializable {
 		this.structType = structType;
 	}
 
-	public String getRightAnswer() {
-		return rightAnswer;
+	public JSONArray getChoiceAnswer() {
+		return choiceAnswer;
+	}
+
+	public void setChoiceAnswer(JSONArray choiceAnswer) {
+		this.choiceAnswer = choiceAnswer;
+	}
+
+	public Boolean getBoolAnswer() {
+		return boolAnswer;
 	}
 
-	public void setRightAnswer(String rightAnswer) {
-		this.rightAnswer = rightAnswer;
+	public void setBoolAnswer(Boolean boolAnswer) {
+		this.boolAnswer = boolAnswer;
 	}
 
+	
 }

+ 2 - 0
themis-business/src/main/java/com/qmth/themis/business/constant/SystemConstant.java

@@ -88,6 +88,8 @@ public class SystemConstant {
     public static final long REDIS_CACHE_TIME_OUT = 30L;
     //学生锁
     public static final String REDIS_LOCK_STUDENT_PREFIX = "lock:student:student_id_";
+    //计算客观分总分锁
+    public static final String REDIS_LOCK_TOTAL_OBJECTIVE_SCORE_PREFIX = "lock:total_objective_score:record_id_";
     /**
      * redis过期时间
      */

+ 2 - 0
themis-business/src/main/java/com/qmth/themis/business/enums/MqEnum.java

@@ -49,6 +49,8 @@ public enum MqEnum {
      * quartz
      */
     QUARTZ_LOG(7, "quartz任务"),
+    
+    EXAM(8, "考生端消息"),
 
     /**
      * websocket强行交卷

+ 3 - 0
themis-business/src/main/java/com/qmth/themis/business/service/TEExamService.java

@@ -12,6 +12,7 @@ import com.qmth.themis.business.bean.exam.ExamFinishBean;
 import com.qmth.themis.business.bean.exam.ExamPrepareBean;
 import com.qmth.themis.business.bean.exam.ExamResumeBean;
 import com.qmth.themis.business.bean.exam.ExamStartBean;
+import com.qmth.themis.business.cache.bean.ExamCacheBean;
 import com.qmth.themis.business.entity.TEExam;
 
 /**
@@ -126,4 +127,6 @@ public interface TEExamService extends IService<TEExam> {
 	 * @return
 	 */
 	public ExamFinishBean finish(Long studentId, Long recordId, String type, Integer durationSeconds);
+
+	ExamCacheBean getExamCacheBean(Long examId);
 }

+ 5 - 0
themis-business/src/main/java/com/qmth/themis/business/service/TOeExamRecordService.java

@@ -25,4 +25,9 @@ public interface TOeExamRecordService extends IService<TOeExamRecord> {
     public Map getUnFinishExam(Long studentId, Long examId, Long orgId);
 
     Long saveByPrepare(Long examId, Long examActivityId, Long examStudentId, Long paperId, Integer serialNumber);
+
+	/**计算客观分
+	 * @param param
+	 */
+	void calculateObjectiveScore(Map<String, Object> param);
 }

+ 72 - 1
themis-business/src/main/java/com/qmth/themis/business/service/impl/TEExamPaperServiceImpl.java

@@ -11,6 +11,8 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.qmth.themis.business.cache.bean.ExamPaperCacheBean;
 import com.qmth.themis.business.cache.bean.ObjectiveAnswerCacheBean;
@@ -31,6 +33,7 @@ import com.qmth.themis.common.util.FileUtil;
  */
 @Service
 public class TEExamPaperServiceImpl extends ServiceImpl<TEExamPaperMapper, TEExamPaper> implements TEExamPaperService {
+	private static String underLine = "_";
     @Resource
     SystemConfig systemConfig;
 	@Resource
@@ -102,10 +105,78 @@ public class TEExamPaperServiceImpl extends ServiceImpl<TEExamPaperMapper, TEExa
             FileUtil.decryptFile(answerEncryptFile, answerFile, ep.getDecryptSecret(), ep.getDecryptVector());
             String structJson=FileUtil.readFileContent(structFile);
             String answerjson=FileUtil.readFileContent(answerFile);
+            return buildCache(structJson, answerjson);
         } finally {
             FileUtil.deleteFolder(dir);
         }
-		return ret;
+	}
+	
+	
+	private Map<String,ObjectiveAnswerCacheBean> buildCache(String structStr,String answerStr){
+		Map<String,ObjectiveAnswerCacheBean> map=new HashMap<String,ObjectiveAnswerCacheBean>();
+		JSONObject answerJson = JSONObject.parseObject(answerStr);
+        JSONArray answerdetails = answerJson.getJSONArray("details");
+        JSONObject structJson = JSONObject.parseObject(structStr);
+        JSONArray structdetails = structJson.getJSONArray("details");
+        for (int i = 0; i < answerdetails.size(); i++) {
+        	Integer mainNum=answerdetails.getJSONObject(i).getInteger("number");
+            JSONArray answerdetailquestions = answerdetails.getJSONObject(i).getJSONArray("questions");
+            JSONArray structdetailquestions = structdetails.getJSONObject(i).getJSONArray("questions");
+            for (int j = 0; j < answerdetailquestions.size(); j++) {
+                JSONObject answerquestion = answerdetailquestions.getJSONObject(j);
+                JSONObject structquestion = structdetailquestions.getJSONObject(j);
+                Integer subNum=structquestion.getInteger("number");
+                Double score=structquestion.getDouble("score");
+                Integer structType=structquestion.getInteger("structType");
+                if (structType.intValue() == 1
+                        || structType.intValue() == 2
+                        || structType.intValue() == 3) {
+                	ObjectiveAnswerCacheBean bean=new ObjectiveAnswerCacheBean();
+                	bean.setScore(score);
+                	bean.setStructType(structType);
+	                if (structType.intValue() == 1
+	                        || structType.intValue() == 2) {
+	                    JSONArray answer = answerquestion.getJSONArray("answer");
+	                    bean.setChoiceAnswer(answer);
+	                }
+	                if (structType.intValue() == 3) {
+	                    Boolean answer = answerquestion.getBoolean("answer");
+	                    bean.setBoolAnswer(answer);
+	                }
+	                map.put(mainNum + underLine + subNum, bean);
+                }
+            	if (structType.intValue() == 6) {
+                        JSONArray answersubQuestions = answerquestion.getJSONArray("subQuestions");
+                        JSONArray structsubQuestions = structquestion.getJSONArray("subQuestions");
+                    for (int k = 0; k < answersubQuestions.size(); k++) {
+                    	JSONObject answersubquestion = answersubQuestions.getJSONObject(k);
+                    	JSONObject structsubquestion = structsubQuestions.getJSONObject(k);
+                        Integer subIndex=structsubquestion.getInteger("number");
+                        Double subScore=structsubquestion.getDouble("score");
+                        Integer subStructType=structsubquestion.getInteger("structType");
+
+                        if (subStructType.intValue() == 1
+                                || subStructType.intValue() == 2
+                                || subStructType.intValue() == 3) {
+                        	ObjectiveAnswerCacheBean bean=new ObjectiveAnswerCacheBean();
+                        	bean.setScore(subScore);
+                        	bean.setStructType(subStructType);
+	                        if (subStructType.intValue() == 1
+	                                || subStructType.intValue() == 2) {
+	                            JSONArray answer = answersubquestion.getJSONArray("answer");
+	                            bean.setChoiceAnswer(answer);
+	                        }
+	                        if (subStructType.intValue() == 3) {
+	                            Boolean answer = answersubquestion.getBoolean("answer");
+	                            bean.setBoolAnswer(answer);
+	                        }
+	                        map.put(mainNum + underLine + subNum + underLine + subIndex, bean);
+                        }
+                    }
+                }
+            }
+        }
+		return map;
 	}
     private String uuid() {
         return UUID.randomUUID().toString().replaceAll("-", "");

+ 21 - 2
themis-business/src/main/java/com/qmth/themis/business/service/impl/TEExamServiceImpl.java

@@ -16,6 +16,8 @@ import javax.annotation.Resource;
 
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
@@ -30,6 +32,7 @@ import com.qmth.themis.business.bean.exam.ExamStartBean;
 import com.qmth.themis.business.cache.ExamRecordCacheUtil;
 import com.qmth.themis.business.cache.RedisKeyHelper;
 import com.qmth.themis.business.cache.bean.ExamActivityCacheBean;
+import com.qmth.themis.business.cache.bean.ExamCacheBean;
 import com.qmth.themis.business.cache.bean.ExamCourseCacheBean;
 import com.qmth.themis.business.cache.bean.ExamPaperCacheBean;
 import com.qmth.themis.business.cache.bean.ExamStudentAnswerCacheBean;
@@ -349,13 +352,16 @@ public class TEExamServiceImpl extends ServiceImpl<TEExamMapper, TEExam> impleme
 			answerCache.setVersion(version);
 			answerCache.setDuration(durationSeconds);
 		}
+		//每次提交,清空得分
+		answerCache.setScore(null);
 		// 更新考生作答
 		redisUtil.set(RedisKeyHelper.examAnswerKey(recordId),
 				RedisKeyHelper.examAnswerHashKey(mainNumber, subNumber, subIndex), answerCache);
-		//计算客观
-//		calculateObjectiveScore();
+		//重置考试记录客观题得
+		ExamRecordCacheUtil.setObjectiveScore(recordId, null);
 		return version;
 	}
+	
 
 	@Override
 	public Integer audioLeftPlayCountSubmit(Long studentId, Long recordId, String key, Integer count) {
@@ -542,4 +548,17 @@ public class TEExamServiceImpl extends ServiceImpl<TEExamMapper, TEExam> impleme
 		ret.setReviewResult("");
 		return null;
 	}
+	
+	@Cacheable(value = "exam", key = "#examId", unless = "#result == null")
+	@Override
+	public ExamCacheBean getExamCacheBean(Long examId) {
+		ExamCacheBean ret=null;
+		TEExam exam = getById(examId);
+		if (exam == null) {
+			return ret;
+		}
+		ret = new ExamCacheBean();
+		BeanUtils.copyProperties(exam, ret);
+		return ret;
+	}
 }

+ 171 - 63
themis-business/src/main/java/com/qmth/themis/business/service/impl/TOeExamRecordServiceImpl.java

@@ -1,5 +1,6 @@
 package com.qmth.themis.business.service.impl;
 
+import java.math.BigDecimal;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
@@ -7,14 +8,23 @@ import java.util.Objects;
 
 import javax.annotation.Resource;
 
-import com.qmth.themis.business.enums.ExamRecordStatusEnum;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import com.alibaba.fastjson.JSONArray;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.qmth.themis.business.cache.ExamRecordCacheUtil;
 import com.qmth.themis.business.cache.RedisKeyHelper;
+import com.qmth.themis.business.cache.bean.ExamCacheBean;
+import com.qmth.themis.business.cache.bean.ExamStudentAnswerCacheBean;
+import com.qmth.themis.business.cache.bean.ObjectiveAnswerCacheBean;
+import com.qmth.themis.business.constant.SystemConstant;
 import com.qmth.themis.business.dao.TOeExamRecordMapper;
 import com.qmth.themis.business.entity.TOeExamRecord;
+import com.qmth.themis.business.enums.ExamRecordStatusEnum;
+import com.qmth.themis.business.enums.ObjectiveScorePolicyEnum;
+import com.qmth.themis.business.service.TEExamPaperService;
+import com.qmth.themis.business.service.TEExamService;
 import com.qmth.themis.business.service.TOeExamRecordService;
 import com.qmth.themis.business.util.RedisUtil;
 import com.qmth.themis.common.util.SimpleBeanUtil;
@@ -34,7 +44,13 @@ public class TOeExamRecordServiceImpl extends ServiceImpl<TOeExamRecordMapper, T
 
     @Resource
     RedisUtil redisUtil;
+    
+	@Resource
+	TEExamPaperService examPaperService;
 
+	@Resource
+	TEExamService examService;
+    
     /**
      * 获取考试未完列表
      *
@@ -89,66 +105,158 @@ public class TOeExamRecordServiceImpl extends ServiceImpl<TOeExamRecordMapper, T
         }
     }
 
-    @Transactional 
-    @Override
-    public Long saveByPrepare(Long examId,Long examActivityId,Long examStudentId,Long paperId,Integer serialNumber) {
-    	TOeExamRecord er=new TOeExamRecord();
-    	er.setExamId(examId);
-    	er.setExamActivityId(examActivityId);
-    	er.setExamStudentId(examStudentId);
-    	er.setPaperId(paperId);
-    	er.setSerialNumber(serialNumber);
-    	er.setFirstPrepareTime(new Date());
-    	er.setStatus(ExamRecordStatusEnum.first_prepare);
-    	saveOrUpdate(er);
-    	redisUtil.set(RedisKeyHelper.examRecordCacheKey(er.getId()), SimpleBeanUtil.objectToMap(er));
-    	return er.getId();
-    }
-     
-    
-    
-//    private void copy(ExamRecordCacheBean tb,TOeExamRecord sb) {
-//    	tb.setId(sb.getId());
-//    	tb.setExamId(sb.getExamId());
-//    	tb.setExamActivityId(tb.getExamActivityId());
-//    	tb.setPaperId(sb.getPaperId());
-//    	tb.setAnswerPath(sb.getAnswerPath());
-//    	tb.setStatus(sb.getStatus());
-//    	tb.setFirstPrepareTime(sb.getFirstPrepareTime());
-//    	tb.setFirstStartTime(sb.getFirstStartTime());
-//    	tb.setLastPrepareTime(sb.getLastPrepareTime());
-//    	tb.setLastBreakTime(sb.getLastBreakTime());
-//    	tb.setLastStartTime(sb.getLastStartTime());
-//    	tb.setLeftBreakResumeCount(sb.getLeftBreakResumeCount());
-//    	tb.setClientCurrentIp(sb.getClientCurrentIp());
-//    	tb.setClientLastSyncTime(sb.getClientLastSyncTime());
-//    	tb.setClientVideoPushKey(sb.getClientVideoPushKey());
-//    	tb.setClientVideoPushStatus(sb.getClientVideoPushStatus());
-//    	tb.setClientWebsocketId(sb.getClientWebsocketId());
-//    	tb.setClientWebsocketStatus(sb.getClientWebsocketStatus());
-//    	tb.setWxappLastSyncTime(sb.getWxappLastSyncTime());
-//    	tb.setWarningCount(sb.getWarningCount());
-//    	tb.setWxappVideoPushKey(sb.getWxappVideoPushKey());
-//    	tb.setWxappVideoPushStatus(sb.getWxappVideoPushStatus());
-//    	tb.setWxappWebsocketId(sb.getWxappWebsocketId());
-//    	tb.setWxappWebsocketStatus(sb.getWxappWebsocketStatus());
-//    	tb.setAnswerProgress(sb.getAnswerProgress());
-//    	tb.setDurationSeconds(sb.getDurationSeconds());
-//    	tb.setFinishTime(sb.getFinishTime());
-//    	tb.setFinishType(sb.getFinishType());
-//    	tb.setWarningCount(sb.getWarningCount());
-//    	tb.setReviewResult(sb.getReviewResult());
-//    	tb.setObjectiveScore(sb.getObjectiveScore());
-//    	tb.setPaperDownload(sb.getPaperDownload());
-//    	tb.setBreachStatus(sb.getBreachStatus());
-//    	tb.setPaperStruct(sb.getPaperStruct());
-//    	tb.setPaperStructUpload(sb.getPaperStructUpload());
-//    	tb.setSerialNumber(sb.getSerialNumber());
-//    	tb.setLastBreakId(sb.getLastBreakId());
-//    	tb.setEntryAuthenticationId(sb.getEntryAuthenticationId());
-//    	tb.setEntryAuthenticationResult(sb.getEntryAuthenticationResult());
-//    	tb.setInProcessFaceVerifyStatus(sb.getInProcessFaceVerifyStatus());
-//    	tb.setInProcessLivenessVerifyCount(sb.getInProcessLivenessVerifyCount());
-//    	tb.setInProcessLivenessVerifyStatus(sb.getInProcessLivenessVerifyStatus());
-//    }
+    @Transactional
+	@Override
+	public Long saveByPrepare(Long examId, Long examActivityId, Long examStudentId, Long paperId,
+			Integer serialNumber) {
+		TOeExamRecord er = new TOeExamRecord();
+		er.setExamId(examId);
+		er.setExamActivityId(examActivityId);
+		er.setExamStudentId(examStudentId);
+		er.setPaperId(paperId);
+		er.setSerialNumber(serialNumber);
+		er.setFirstPrepareTime(new Date());
+		er.setStatus(ExamRecordStatusEnum.first_prepare);
+		saveOrUpdate(er);
+		redisUtil.set(RedisKeyHelper.examRecordCacheKey(er.getId()), SimpleBeanUtil.objectToMap(er));
+		return er.getId();
+	}
+
+	/**
+	 *计算客观分
+	 */
+	@Override
+	public void calculateObjectiveScore(Map<String, Object> param) {
+		Long recordId = (Long) param.get("recordId");
+		Integer mainNumber = (Integer) param.get("mainNumber");
+		Integer subNumber = (Integer) param.get("subNumber");
+		Integer subIndex = (Integer) param.get("subIndex");
+		Long paperId = ExamRecordCacheUtil.getPaperId(recordId);
+		Map<String, ObjectiveAnswerCacheBean> map = examPaperService.getObjectiveAnswerCacheBean(paperId);
+		if (map == null || map.size() == 0) {
+			log.error("no ObjectiveAnswerCacheBean for calculateObjectiveScore");
+			return;
+		}
+		String key = RedisKeyHelper.examAnswerHashKey(mainNumber, subNumber, subIndex);
+		ObjectiveAnswerCacheBean cb = map.get(key);
+		if(cb==null) {
+			return;
+		}
+		ExamStudentAnswerCacheBean answer = (ExamStudentAnswerCacheBean) redisUtil.get(
+				RedisKeyHelper.examAnswerKey(recordId),
+				RedisKeyHelper.examAnswerHashKey(mainNumber, subNumber, subIndex));
+		if (answer == null) {
+			log.error("no ExamStudentAnswerCacheBean for calculateObjectiveScore");
+			return;
+		}
+
+		if (cb.getStructType().intValue() == 1) {
+			if (checkSingleChoice(answer.getContent(), cb.getChoiceAnswer())) {
+				answer.setScore(cb.getScore());
+			} else {
+				answer.setScore(0.0);
+			}
+		}
+		if (cb.getStructType().intValue() == 2) {
+			ExamCacheBean ec = examService.getExamCacheBean(ExamRecordCacheUtil.getExamId(recordId));
+			int r = checkMultipleChoice(answer.getContent(), cb.getChoiceAnswer());
+			if (ec.getObjectiveScorePolicy().equals(ObjectiveScorePolicyEnum.equal)) {// 全对给分
+				if (r == 1) {
+					answer.setScore(cb.getScore());
+				} else {
+					answer.setScore(0.0);
+				}
+			}
+			if (ec.getObjectiveScorePolicy().equals(ObjectiveScorePolicyEnum.partial)) {// 漏选半分
+				if (r == 1) {
+					answer.setScore(cb.getScore());
+				} else if (r == 0) {
+					BigDecimal b = new BigDecimal(cb.getScore()).divide(new BigDecimal("2"), 1, BigDecimal.ROUND_UP);
+					answer.setScore(b.doubleValue());
+				} else {
+					answer.setScore(0.0);
+				}
+			}
+		}
+		if (cb.getStructType().intValue() == 3) {
+			if (Boolean.parseBoolean(answer.getContent())==cb.getBoolAnswer().booleanValue()) {
+				answer.setScore(cb.getScore());
+			} else {
+				answer.setScore(0.0);
+			}
+		}
+
+		answer.setObjective(true);
+		// 更新分数
+		redisUtil.set(RedisKeyHelper.examAnswerKey(recordId),
+				RedisKeyHelper.examAnswerHashKey(mainNumber, subNumber, subIndex), answer);
+
+		// 计算客观分总分
+		calculateTotalObjectiveScore(recordId, map);
+	}
+
+	@SuppressWarnings("unchecked")
+	private void calculateTotalObjectiveScore(Long recordId, Map<String, ObjectiveAnswerCacheBean> map) {
+		String lockKey = SystemConstant.REDIS_LOCK_TOTAL_OBJECTIVE_SCORE_PREFIX + recordId;
+		try {
+			Boolean lock = redisUtil.lock(lockKey, SystemConstant.REDIS_CACHE_TIME_OUT);
+			if (lock) {
+				Map<String,ExamStudentAnswerCacheBean> as=redisUtil.getHashEntries(RedisKeyHelper.examAnswerKey(recordId));
+				if(as!=null&&as.size()>0) {
+					Double total=0.0;
+					for(ExamStudentAnswerCacheBean sa:as.values()) {
+						if(sa.getObjective()!=null&&sa.getObjective()) {//是客观题
+							if(sa.getScore()!=null) {//有分值
+								total=total+sa.getScore();
+							}else {
+								total=null;
+								break;
+							}
+						}
+					}
+					if(total!=null) {
+						ExamRecordCacheUtil.setObjectiveScore(recordId, total);
+					}
+				}
+			}
+		} finally {
+			redisUtil.releaseLock(lockKey);
+		}
+	}
+
+	private boolean checkSingleChoice(String answer, JSONArray ar) {
+		int a = Integer.parseInt(answer);
+		for (int i = 0; i < ar.size(); i++) {
+			if (a == ar.getIntValue(i)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * @param answer
+	 * @param ar
+	 * @return -1 有错选项,0-无措选项 未答全 1-全对
+	 */
+	private int checkMultipleChoice(String answer, JSONArray ar) {
+		int yes = 0;
+		int no = 0;
+		String[] ss = answer.split(",");
+		for (String an : ss) {
+			if (checkSingleChoice(an, ar)) {
+				yes++;
+				continue;
+			} else {
+				no++;
+			}
+		}
+		if (yes == ar.size()) {
+			return 1;
+		}
+		if (yes != 0 && no == 0) {
+			return 0;
+		}
+		return -1;
+	}
 }

+ 2 - 2
themis-business/src/main/java/com/qmth/themis/business/templete/impl/TaskExamPaperImportTemplete.java

@@ -408,8 +408,8 @@ public class TaskExamPaperImportTemplete implements TaskImportTemplete {
                                 + "大题第" + (j + 1) + "小题子题数量不一致");
                     }
                     for (int k = 0; k < answersubQuestions.size(); k++) {
-                        JSONObject answersubquestion = answerdetailquestions.getJSONObject(k);
-                        JSONObject structsubquestion = structdetailquestions.getJSONObject(k);
+                        JSONObject answersubquestion = answersubQuestions.getJSONObject(k);
+                        JSONObject structsubquestion = structsubQuestions.getJSONObject(k);
                         if (structsubquestion.getInteger("structType").intValue() == 1
                                 || structsubquestion.getInteger("structType").intValue() == 2) {
                             JSONArray answer = answersubquestion.getJSONArray("answer");

+ 23 - 2
themis-exam/src/main/java/com/qmth/themis/exam/api/TEExamController.java

@@ -21,6 +21,7 @@ import com.qmth.themis.business.bean.exam.ExamStartBean;
 import com.qmth.themis.business.constant.SystemConstant;
 import com.qmth.themis.business.entity.TEExam;
 import com.qmth.themis.business.entity.TEStudent;
+import com.qmth.themis.business.enums.MqEnum;
 import com.qmth.themis.business.service.TEExamService;
 import com.qmth.themis.business.util.RedisUtil;
 import com.qmth.themis.common.enums.ExceptionResultEnum;
@@ -28,6 +29,10 @@ import com.qmth.themis.common.exception.BusinessException;
 import com.qmth.themis.common.util.Result;
 import com.qmth.themis.common.util.ResultUtil;
 import com.qmth.themis.exam.util.ServletUtil;
+import com.qmth.themis.mq.dto.MqDto;
+import com.qmth.themis.mq.enums.MqTagEnum;
+import com.qmth.themis.mq.enums.MqTopicEnum;
+import com.qmth.themis.mq.service.MqDtoService;
 
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -44,7 +49,9 @@ public class TEExamController {
 	TEExamService teExamService;
 	@Resource
 	RedisUtil redisUtil;
-
+	@Resource
+	MqDtoService mqDtoService;
+	
 	@ApiOperation(value = "验证考试口令接口")
 	@RequestMapping(value = "/shortCode", method = RequestMethod.GET)
 	@ApiResponses({ @ApiResponse(code = 200, message = "考试信息", response = TEExam.class) })
@@ -134,7 +141,10 @@ public class TEExamController {
 			throw new BusinessException(ExceptionResultEnum.REQUEST_AWAIT);
 		}
 		try {
-			return teExamService.answerSubmit(teStudent.getId(), recordId,mainNumber,subNumber,subIndex,answer,version,durationSeconds);
+			Long ret=teExamService.answerSubmit(teStudent.getId(), recordId,mainNumber,subNumber,subIndex,answer,version,durationSeconds);
+			//发消息计算客观分
+			calculateObjectiveScore(recordId,mainNumber, subNumber, subIndex);
+			return ret;
 		} finally {
 			redisUtil.releaseLock(lockKey);
 		}
@@ -214,4 +224,15 @@ public class TEExamController {
 			redisUtil.releaseLock(lockKey);
 		}
 	}
+	
+	private void calculateObjectiveScore(Long recordId,Integer mainNumber,Integer subNumber,Integer subIndex) {
+		Map<String, Object> transMap = new HashMap<String, Object>();
+		transMap.put("recordId", recordId);
+		transMap.put("mainNumber", mainNumber);
+        transMap.put("subNumber", subNumber);
+        transMap.put("subIndex", subIndex);
+        //mq发送消息start
+        MqDto mqDto = new MqDto(MqTopicEnum.normal.getCode(), MqTagEnum.calculateObjectiveScore.name(), transMap, MqEnum.EXAM, null, null);
+        mqDtoService.assembleSendOneWayMsg(mqDto);
+	}
 }

+ 13 - 8
themis-exam/src/main/java/com/qmth/themis/exam/start/StartRunning.java

@@ -1,22 +1,24 @@
 package com.qmth.themis.exam.start;
 
+import javax.annotation.Resource;
+
+import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
 import com.qmth.themis.business.constant.SystemConstant;
 import com.qmth.themis.mq.enums.MqGroupEnum;
 import com.qmth.themis.mq.enums.MqTagEnum;
 import com.qmth.themis.mq.enums.MqTopicEnum;
 import com.qmth.themis.mq.listener.RocketMessageConsumer;
+import com.qmth.themis.mq.templete.impl.CalculateObjectiveScoreConcurrentlyImpl;
 import com.qmth.themis.mq.templete.impl.SessionConcurrentlyImpl;
 import com.qmth.themis.mq.templete.impl.TaskConcurrentlyImpl;
 import com.qmth.themis.mq.templete.impl.UserLogConcurrentlyImpl;
 import com.qmth.themis.mq.templete.impl.WebsocketUnNormalConcurrentlyImpl;
-import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.CommandLineRunner;
-import org.springframework.stereotype.Component;
-
-import javax.annotation.Resource;
 
 /**
  * @Description: 服务启动时初始化运行,哪个微服务模块需要则拿此模版去用
@@ -78,6 +80,9 @@ public class StartRunning implements CommandLineRunner {
         /**
          * websocket mq end
          */
+        
+        //计算客观分
+        rocketMessageConsumer.setRocketMQConsumer(nameServer, MqGroupEnum.normalGroup.getCode(), MqTopicEnum.normal.getCode(), MqTagEnum.calculateObjectiveScore.name(), MessageModel.CLUSTERING, new CalculateObjectiveScoreConcurrentlyImpl());
         SystemConstant.initTempFiles();
         log.info("服务器启动时执行 end");
     }

+ 6 - 1
themis-mq/src/main/java/com/qmth/themis/mq/enums/MqGroupEnum.java

@@ -82,7 +82,12 @@ public enum MqGroupEnum {
     /**
      * quartz 考场 group
      */
-    quartzConsumerExamActivityGroup("themis-group-quartz-examActivity");
+    quartzConsumerExamActivityGroup("themis-group-quartz-examActivity"),
+	
+    /**
+     * 通用分组
+     */
+    normalGroup("themis-group-normal");
 
     private MqGroupEnum(String code) {
         this.code = code;

+ 5 - 2
themis-mq/src/main/java/com/qmth/themis/mq/enums/MqTagEnum.java

@@ -22,6 +22,9 @@ public enum MqTagEnum {
     roomCodeImport,
     examPaperImport,
     oe,
-    unNormal,
-    examActivity;
+    examActivity,
+    /**
+     * 计算客观分
+     */
+    calculateObjectiveScore;
 }

+ 6 - 1
themis-mq/src/main/java/com/qmth/themis/mq/enums/MqTopicEnum.java

@@ -32,7 +32,12 @@ public enum MqTopicEnum {
     /**
      * quartz topic
      */
-    quartzTopic("themis-topic-quartz");
+    quartzTopic("themis-topic-quartz"),
+	
+	/**
+	 * 通用消息
+	 */
+	normal("themis-topic-normal");
 
     private MqTopicEnum(String code){
         this.code = code;

+ 93 - 0
themis-mq/src/main/java/com/qmth/themis/mq/templete/impl/CalculateObjectiveScoreConcurrentlyImpl.java

@@ -0,0 +1,93 @@
+package com.qmth.themis.mq.templete.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
+import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
+import org.apache.rocketmq.common.message.MessageExt;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.google.gson.Gson;
+import com.qmth.themis.business.constant.SpringContextHolder;
+import com.qmth.themis.business.constant.SystemConstant;
+import com.qmth.themis.business.entity.TMRocketMessage;
+import com.qmth.themis.business.service.TMRocketMessageService;
+import com.qmth.themis.business.service.TOeExamRecordService;
+import com.qmth.themis.business.util.JacksonUtil;
+import com.qmth.themis.business.util.RedisUtil;
+import com.qmth.themis.common.contanst.Constants;
+import com.qmth.themis.mq.dto.MqDto;
+import com.qmth.themis.mq.templete.Concurrently;
+
+/**
+ * 计算客观分
+ * 
+ * @Description:
+ * @Author: xiatian
+ * @Date: 2020-07-30
+ */
+@Service
+public class CalculateObjectiveScoreConcurrentlyImpl implements Concurrently {
+	private final static Logger log = LoggerFactory.getLogger(CalculateObjectiveScoreConcurrentlyImpl.class);
+
+	private RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class);
+	private TOeExamRecordService examRecordService = SpringContextHolder.getBean(TOeExamRecordService.class);
+	private TMRocketMessageService tmRocketMessageService = SpringContextHolder.getBean(TMRocketMessageService.class);
+	@Override
+	@Transactional
+	public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
+			ConsumeConcurrentlyContext consumeConcurrentlyContext) {
+		
+		MqDto mqDto = null;
+		try {
+			long threadId = Thread.currentThread().getId();
+			String threadName = Thread.currentThread().getName();
+			Gson gson = new Gson();
+			for (MessageExt messageExt : msgs) {
+				log.debug(":{}-:{} CalculateObjectiveScore 重试次数:{}", threadId, threadName,
+						messageExt.getReconsumeTimes());
+				mqDto = JacksonUtil.readJson(new String(messageExt.getBody(), Constants.CHARSET), MqDto.class);
+				log.debug(":{}-:{} CalculateObjectiveScore 接收到的消息:{}", threadId, threadName,
+						JacksonUtil.parseJson(mqDto));
+				int reconsumeTime = messageExt.getReconsumeTimes();
+				if (reconsumeTime >= SystemConstant.MAXRECONSUMETIMES) {
+					 //超过最大重试次数,保存到数据库,后续可以发短信通知系统管理人员
+                    mqDto.setAck(SystemConstant.POSION_ACK_TYPE);
+                    TMRocketMessage tmRocketMessage = gson.fromJson(gson.toJson(mqDto), TMRocketMessage.class);
+                    tmRocketMessageService.saveOrUpdate(tmRocketMessage);
+                    redisUtil.delete(SystemConstant.MQ_TOPIC_BUFFER_LIST, mqDto.getId());
+				} else {
+					if (Objects.nonNull(mqDto.getAck()) && mqDto.getAck().intValue() != SystemConstant.STANDARD_ACK_TYPE
+							&& Objects.nonNull(redisUtil.get(SystemConstant.MQ_TOPIC_BUFFER_LIST, mqDto.getId()))
+							&& redisUtil.lock(SystemConstant.REDIS_LOCK_MQ_PREFIX + mqDto.getId(),
+									SystemConstant.REDIS_LOCK_MQ_TIME_OUT)) {
+						log.debug(":{}-:{} 更新db", threadId, threadName);
+						Map<String, Object> param = (Map<String, Object>) mqDto.getBody();
+						examRecordService.calculateObjectiveScore(param);
+						mqDto.setAck(SystemConstant.STANDARD_ACK_TYPE);
+						TMRocketMessage tmRocketMessage = gson.fromJson(gson.toJson(mqDto), TMRocketMessage.class);
+						tmRocketMessage.setBody(JacksonUtil.parseJson(tmRocketMessage.getBody()));
+						tmRocketMessageService.saveOrUpdate(tmRocketMessage);
+						redisUtil.delete(SystemConstant.MQ_TOPIC_BUFFER_LIST, mqDto.getId());
+						return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
+					} else {
+						log.debug(":{}-:{} 消息ack未确认,重发", threadId, threadName);
+						return ConsumeConcurrentlyStatus.RECONSUME_LATER;// 重试
+					}
+				}
+			}
+		} catch (Exception e) {
+			return ConsumeConcurrentlyStatus.RECONSUME_LATER;// 重试
+		} finally {
+			if (Objects.nonNull(mqDto)) {
+				redisUtil.releaseLock(SystemConstant.REDIS_LOCK_MQ_PREFIX + mqDto.getId());
+			}
+		}
+		return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;// 成功
+	}
+}