Răsfoiți Sursa

merge from release_v4.1.2

deason 3 ani în urmă
părinte
comite
63fce4e9c3
16 a modificat fișierele cu 471 adăugiri și 59 ștergeri
  1. 68 10
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/ExamControlController.java
  2. 12 1
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/ExamQuestionController.java
  3. 43 0
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/TempConfigController.java
  4. 1 2
      examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/client/ExamProcessController.java
  5. 5 1
      examcloud-core-oe-student-base/pom.xml
  6. 32 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/ExamStudentQuestionAnswerInfo.java
  7. 13 6
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamControlService.java
  8. 8 5
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/ExamRecordQuestionsService.java
  9. 13 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/TempConfigService.java
  10. 155 29
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamControlServiceImpl.java
  11. 3 4
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordDataServiceImpl.java
  12. 54 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/ExamRecordQuestionsServiceImpl.java
  13. 60 0
      examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/service/impl/TempConfigServiceImpl.java
  14. 1 0
      examcloud-core-oe-student-starter/pom.xml
  15. 2 1
      examcloud-core-oe-student-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/starter/config/SwaggerConfig.java
  16. 1 0
      examcloud-core-oe-student-starter/src/main/resources/log4j2.xml

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

@@ -6,9 +6,8 @@ import cn.com.qmth.examcloud.commons.util.Util;
 import cn.com.qmth.examcloud.core.oe.student.base.utils.Check;
 import cn.com.qmth.examcloud.core.oe.student.bean.*;
 import cn.com.qmth.examcloud.core.oe.student.bean.client.CourseInfo;
-import cn.com.qmth.examcloud.core.oe.student.service.ExamControlService;
-import cn.com.qmth.examcloud.core.oe.student.service.ExamFileAnswerService;
-import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordDataService;
+import cn.com.qmth.examcloud.core.oe.student.service.*;
+import cn.com.qmth.examcloud.starters.crypto.common.CryptoConstant;
 import cn.com.qmth.examcloud.support.Constants;
 import cn.com.qmth.examcloud.support.enums.FileAnswerAcknowledgeStatus;
 import cn.com.qmth.examcloud.support.examing.ExamFileAnswer;
@@ -32,6 +31,7 @@ import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
 import org.apache.commons.lang.math.RandomUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
@@ -68,14 +68,48 @@ public class ExamControlController extends ControllerSupport {
     @Autowired
     private ExamRecordDataService examRecordDataService;
 
+    @Autowired
+    private ExamRecordQuestionsService examRecordQuestionsService;
+
+    @Autowired
+    private TempConfigService tempConfigService;
+
     /**
      * 开始考试
      */
-    @ApiOperation(value = "开始考试")
+    @Deprecated
+    @ApiOperation(value = "开始考试(待废弃)")
     @GetMapping("/startExam")
     public StartExamInfo startExam(@RequestParam Long examStudentId, HttpServletRequest request) {
         User user = getAccessUser();
-        return examControlService.startExam(examStudentId, user.getUserId(), getIp(request));
+        if (tempConfigService.checkDisabledForOldApi(user.getRootOrgId())) {
+            // 判断学校是否被禁用旧版本API
+            throw new StatusException("403X01", "禁止访问!");
+        }
+
+        return examControlService.startExam(examStudentId, user.getUserId(), getIp(request), null);
+    }
+
+    /**
+     * 开始考试(新)
+     */
+    @ApiOperation(value = "开始考试(不支持“ONLINE”考试类型)")
+    @PostMapping("/unonline/startExam")
+    public StartExamInfo startExamForOther(@RequestParam Long examStudentId, HttpServletRequest request) {
+        User user = getAccessUser();
+        return examControlService.startExam(examStudentId, user.getUserId(), getIp(request), false);
+    }
+
+    /**
+     * 开始考试(新)
+     * 注:需要加、解密
+     */
+    @ApiOperation(value = "开始考试(仅支持“ONLINE”考试类型)")
+    @PostMapping(value = "/online/startExam", consumes = MediaType.TEXT_PLAIN_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
+    public String startExamWithCrypto(@RequestBody String encryptParams, HttpServletRequest request) {
+        User user = getAccessUser();
+        return examControlService.startExamWithCrypto(encryptParams, user, getIp(request),
+                request.getHeader(CryptoConstant.TIMESTAMP));
     }
 
     /**
@@ -88,6 +122,20 @@ public class ExamControlController extends ControllerSupport {
         return examControlService.startAnswer(examRecordDataId, user.getUserId());
     }
 
+    /**
+     * 考生作答(新)
+     * 注:需要加、解密
+     */
+    @ApiOperation(value = "考试过程中-考生试题作答(新)")
+    @PostMapping("/submitQuestionAnswer")
+    public void submitQuestionAnswerWithCrypto(@RequestBody ExamStudentQuestionAnswerInfo data, HttpServletRequest request) {
+        User user = getAccessUser();
+        String referer = request.getHeader("REFERER");
+        String agent = request.getHeader("USER-AGENT");
+        String timestampStr = request.getHeader(CryptoConstant.TIMESTAMP);
+        examRecordQuestionsService.submitQuestionAnswerWithCrypto(data, user, referer, agent, timestampStr);
+    }
+
     /**
      * 断点续考:检查正在进行中的考试
      */
@@ -111,20 +159,30 @@ public class ExamControlController extends ControllerSupport {
     }
 
     /**
-     * 结束考试:交卷..
+     * 结束考试:交卷
      */
-    @ApiOperation(value = "结束考试:交卷")
+    @Deprecated
+    @ApiOperation(value = "结束考试:交卷(待废弃)")
     @GetMapping("/endExam")
     public void endExam(HttpServletRequest request) {
         User user = getAccessUser();
         examControlService.manualEndExam(user.getUserId(), getIp(request));
     }
 
+    /**
+     * 结束考试:交卷(新)
+     * 注:需要加、解密
+     */
+    @ApiOperation(value = "结束考试:交卷(新)")
+    @PostMapping(value = "/endExam", consumes = MediaType.TEXT_PLAIN_VALUE)
+    public void endExamWithCrypto(@RequestBody String encryptParams, HttpServletRequest request) {
+        User user = getAccessUser();
+        examControlService.manualEndExamWithCrypto(encryptParams, user, getIp(request),
+                request.getHeader(CryptoConstant.TIMESTAMP));
+    }
+
     /**
      * 获取考试记录信息
-     *
-     * @param examRecordDataId
-     * @return
      */
     @ApiOperation(value = "获取考试记录信息")
     @GetMapping("/getEndExamInfo")

+ 12 - 1
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/ExamQuestionController.java

@@ -1,8 +1,10 @@
 package cn.com.qmth.examcloud.core.oe.student.api.controller;
 
 import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
 import cn.com.qmth.examcloud.core.oe.student.bean.ExamStudentQuestionInfo;
 import cn.com.qmth.examcloud.core.oe.student.service.ExamRecordQuestionsService;
+import cn.com.qmth.examcloud.core.oe.student.service.TempConfigService;
 import cn.com.qmth.examcloud.support.examing.ExamQuestion;
 import cn.com.qmth.examcloud.web.support.ControllerSupport;
 import io.swagger.annotations.Api;
@@ -27,6 +29,9 @@ public class ExamQuestionController extends ControllerSupport {
     @Autowired
     private ExamRecordQuestionsService examRecordQuestionsService;
 
+    @Autowired
+    private TempConfigService tempConfigService;
+
     /**
      * 将mongodb中的答过的题和redis中的题目列表合并返回给前端
      * 返回给前端时注意将正确答案和得分置成null
@@ -58,10 +63,16 @@ public class ExamQuestionController extends ControllerSupport {
      *
      * @param examQuestionInfos
      */
-    @ApiOperation(value = "考试过程中-考生作答:更新试题作答信息(包括提交试题答案,更新是否标记)")
+    @Deprecated
+    @ApiOperation(value = "考试过程中-考生作答(待废弃)")
     @PostMapping("/submitQuestionAnswer")
     public void submitQuestionAnswer(@RequestBody List<ExamStudentQuestionInfo> examQuestionInfos, HttpServletRequest request) {
         User user = getAccessUser();
+        if (tempConfigService.checkDisabledForOldApi(user.getRootOrgId())) {
+            // 判断学校是否被禁用旧版本API
+            throw new StatusException("403X01", "禁止访问!");
+        }
+
         String referer = request.getHeader("REFERER");
         String agent = request.getHeader("USER-AGENT");
         examRecordQuestionsService.submitQuestionAnswer(user.getUserId(), examQuestionInfos, referer, agent);

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

@@ -0,0 +1,43 @@
+package cn.com.qmth.examcloud.core.oe.student.api.controller;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.api.commons.security.enums.RoleMeta;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.core.oe.student.service.TempConfigService;
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@Api(tags = "临时配置相关接口(待废弃)")
+@RequestMapping("${app.api.oe.student}/temp/config")
+public class TempConfigController extends ControllerSupport {
+
+    @Autowired
+    private TempConfigService tempConfigService;
+
+    @PostMapping("/list")
+    @ApiOperation(value = "获取-被禁用旧版本API的学校列表")
+    public List<Long> getDisabledRootOrgIdsForOldApi() {
+        return tempConfigService.getDisabledRootOrgIdsForOldApi();
+    }
+
+    @PostMapping("/update")
+    @ApiOperation(value = "修改-被禁用旧版本API的学校列表")
+    public void updateDisabledRootOrgIdsForOldApi(@RequestBody List<Long> rootOrgIds) {
+        User accessUser = getAccessUser();
+        if (!hasAnyRoles(accessUser, RoleMeta.SUPER_ADMIN, RoleMeta.ORG_ADMIN)) {
+            throw new StatusException("500403", "没有数据操作权限!");
+        }
+
+        tempConfigService.updateDisabledRootOrgIdsForOldApi(rootOrgIds);
+    }
+
+}

+ 1 - 2
examcloud-core-oe-student-api-provider/src/main/java/cn/com/qmth/examcloud/core/oe/student/api/controller/client/ExamProcessController.java

@@ -19,7 +19,6 @@ import cn.com.qmth.examcloud.web.filestorage.FileStoragePathEnvInfo;
 import cn.com.qmth.examcloud.web.filestorage.YunPathInfo;
 import cn.com.qmth.examcloud.web.redis.RedisClient;
 import cn.com.qmth.examcloud.web.support.ControllerSupport;
-import cn.com.qmth.examcloud.web.support.Naked;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -69,7 +68,7 @@ public class ExamProcessController extends ControllerSupport {
     @PostMapping("/startExam")
     public StartExamInfo startExam(@RequestParam Long examStudentId) {
         User user = getAccessUser();
-        return examControlService.startExam(examStudentId, user.getUserId(), getIp(getRequest()));
+        return examControlService.startExam(examStudentId, user.getUserId(), getIp(getRequest()), null);
     }
 
     @ApiOperation(value = "开始答题")

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

@@ -32,7 +32,11 @@
             <artifactId>examcloud-support</artifactId>
             <version>${project.version}</version>
         </dependency>
-
+        <dependency>
+            <groupId>cn.com.qmth.examcloud.starters</groupId>
+            <artifactId>examcloud-crypto-starter</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>cn.com.qmth.examcloud.rpc</groupId>
             <artifactId>examcloud-exchange-inner-api-client</artifactId>

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

@@ -0,0 +1,32 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+import io.swagger.annotations.ApiModelProperty;
+
+public class ExamStudentQuestionAnswerInfo implements JsonSerializable {
+
+    private static final long serialVersionUID = 8080615797817377990L;
+
+    @ApiModelProperty(value = "作答内容(JSON字符串)", required = true)
+    private String answers;
+
+    @ApiModelProperty(value = "作答内容签名", required = true)
+    private String sign;
+
+    public String getAnswers() {
+        return answers;
+    }
+
+    public void setAnswers(String answers) {
+        this.answers = answers;
+    }
+
+    public String getSign() {
+        return sign;
+    }
+
+    public void setSign(String sign) {
+        this.sign = sign;
+    }
+
+}

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

@@ -15,16 +15,17 @@ public interface ExamControlService {
 
     /**
      * 开始考试
-     *
-     * @param examStudentId
-     * @param userId
      */
-    StartExamInfo startExam(Long examStudentId, Long userId, String ip);
+    StartExamInfo startExam(Long examStudentId, Long userId, String ip, Boolean allowOnline);
+
+    /**
+     * 开始考试(新)
+     * 注:需要加、解密
+     */
+    String startExamWithCrypto(String encryptParams, User user, String requestIP, String timestampStr);
 
     /**
      * 开始答题
-     *
-     * @param examRecordDataId
      */
     StartAnswerInfo startAnswer(Long examRecordDataId, Long userId);
 
@@ -33,6 +34,12 @@ public interface ExamControlService {
      */
     void manualEndExam(Long studentId, String ip);
 
+    /**
+     * 手动交卷(新)
+     * 注:需要加、解密
+     */
+    void manualEndExamWithCrypto(String encryptParams, User user, String requestIP, String timestampStr);
+
     /**
      * 交卷
      *

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

@@ -1,7 +1,9 @@
 package cn.com.qmth.examcloud.core.oe.student.service;
 
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
 import cn.com.qmth.examcloud.core.oe.student.api.request.GetExamRecordQuestionsReq;
 import cn.com.qmth.examcloud.core.oe.student.api.response.GetExamRecordQuestionsResp;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamStudentQuestionAnswerInfo;
 import cn.com.qmth.examcloud.core.oe.student.bean.ExamStudentQuestionInfo;
 import cn.com.qmth.examcloud.question.commons.core.paper.DefaultPaper;
 import cn.com.qmth.examcloud.support.examing.ExamQuestion;
@@ -21,16 +23,11 @@ public interface ExamRecordQuestionsService {
 
     /**
      * 获取
-     *
-     * @param examRecordDataId
-     * @return
      */
     ExamQuestion getExamQuestion(Long examRecordDataId, Integer order);
 
     /**
      * 删除
-     *
-     * @param examRecordDataId
      */
     void deleteExamQuestion(Long examRecordDataId, Integer order);
 
@@ -46,6 +43,12 @@ public interface ExamRecordQuestionsService {
 
     void submitQuestionAnswer(Long studentId, List<ExamStudentQuestionInfo> examQuestionInfos, String referer, String agent);
 
+    /**
+     * 考生作答(新)
+     * 注:需要加、解密
+     */
+    void submitQuestionAnswerWithCrypto(ExamStudentQuestionAnswerInfo data, User user, String referer, String agent, String timestampStr);
+
     GetExamRecordQuestionsResp getExamRecordQuestions(GetExamRecordQuestionsReq req);
 
 }

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

@@ -0,0 +1,13 @@
+package cn.com.qmth.examcloud.core.oe.student.service;
+
+import java.util.List;
+
+public interface TempConfigService {
+
+    List<Long> getDisabledRootOrgIdsForOldApi();
+
+    void updateDisabledRootOrgIdsForOldApi(List<Long> rootOrgIds);
+
+    boolean checkDisabledForOldApi(Long rootOrgId);
+
+}

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

@@ -9,6 +9,9 @@ import cn.com.qmth.examcloud.commons.exception.StatusException;
 import cn.com.qmth.examcloud.commons.util.*;
 import cn.com.qmth.examcloud.commons.util.UUID;
 import cn.com.qmth.examcloud.commons.util.DateUtil.DatePatterns;
+import cn.com.qmth.examcloud.core.basic.api.CryptoConfigCloudService;
+import cn.com.qmth.examcloud.core.basic.api.request.CheckCryptoConfigReq;
+import cn.com.qmth.examcloud.core.basic.api.response.CheckCryptoConfigResp;
 import cn.com.qmth.examcloud.core.oe.admin.api.ExamRecordCloudService;
 import cn.com.qmth.examcloud.core.oe.admin.api.SyncExamDataCloudService;
 import cn.com.qmth.examcloud.core.oe.admin.api.bean.ExamQuestionBean;
@@ -46,6 +49,12 @@ import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionUnitWrap
 import cn.com.qmth.examcloud.question.commons.core.question.QuestionType;
 import cn.com.qmth.examcloud.reports.commons.bean.OnlineExamStudentReport;
 import cn.com.qmth.examcloud.reports.commons.util.ReportsUtil;
+import cn.com.qmth.examcloud.starters.crypto.CryptoProperties;
+import cn.com.qmth.examcloud.starters.crypto.common.CryptoConstant;
+import cn.com.qmth.examcloud.starters.crypto.common.CryptoGroup;
+import cn.com.qmth.examcloud.starters.crypto.common.CryptoHelper;
+import cn.com.qmth.examcloud.starters.crypto.common.FieldPair;
+import cn.com.qmth.examcloud.starters.crypto.service.CryptoFactory;
 import cn.com.qmth.examcloud.support.Constants;
 import cn.com.qmth.examcloud.support.cache.CacheHelper;
 import cn.com.qmth.examcloud.support.cache.bean.*;
@@ -66,6 +75,7 @@ import cn.com.qmth.examcloud.ws.api.request.SendScanQrCodeMessageReq;
 import cn.com.qmth.examcloud.ws.api.request.SendTextReq;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.base.Splitter;
 import main.java.com.upyun.Base64Coder;
 import main.java.com.upyun.UpException;
@@ -143,6 +153,15 @@ public class ExamControlServiceImpl implements ExamControlService {
     @Autowired
     private ExamContinuedRecordRepo examContinuedRecordRepo;
 
+    @Autowired
+    private CryptoConfigCloudService cryptoConfigCloudService;
+
+    @Autowired
+    private CryptoFactory cryptoFactory;
+
+    @Autowired
+    private CryptoProperties cryptoProperties;
+
     private static final String SEPARATOR = "/";
 
     private static final String UNDERLINE = "_";
@@ -155,9 +174,14 @@ public class ExamControlServiceImpl implements ExamControlService {
     // 又拍云签名有效时间(秒)
     private static final Integer SIGN_TIMEOUT = 60;
 
+    /**
+     * 开始考试
+     *
+     * @param allowOnline 是否仅支持“ONLINE”考试类型,为null时都支持
+     */
     @Transactional
     @Override
-    public StartExamInfo startExam(Long examStudentId, Long userId, String ip) {
+    public StartExamInfo startExam(Long examStudentId, Long userId, String ip, Boolean allowOnline) {
         Check.isNull(examStudentId, "examStudentId不能为空");
         Check.isNull(userId, "userId不能为空");
 
@@ -166,7 +190,7 @@ public class ExamControlServiceImpl implements ExamControlService {
         SequenceLockHelper.getLock(sequenceLockKey);
 
         // 开考预处理
-        prepare4Exam(examStudentId, userId);
+        prepare4Exam(examStudentId, userId, allowOnline);
 
         Long studentId = userId;
         long st = System.currentTimeMillis();
@@ -311,6 +335,56 @@ public class ExamControlServiceImpl implements ExamControlService {
         return buildStartExamInfo(examRecordData.getId(), examingSession, examBean, courseBean);
     }
 
+
+    @Override
+    public String startExamWithCrypto(String encryptParams, User user, String requestIP, String timestampStr) {
+        long timestamp;
+        try {
+            timestamp = CryptoHelper.parseTimestamp(timestampStr);
+        } catch (Exception e) {
+            log.error("[header] timestamp is wrong... {}", timestampStr);
+            throw new StatusException("400X01", CryptoConstant.REQUEST_PARAM_ERROR);
+        }
+
+        if (StringUtils.isNotEmpty(user.getSalt())) {
+            // 未启用任何加密方案组合时登录,salt值为空
+            CheckCryptoConfigResp resp = cryptoConfigCloudService.checkCryptoConfig(new CheckCryptoConfigReq(user.getSalt()));
+            if (!resp.isEnable()) {
+                throw new StatusException("403X01", "系统配置修改,请与学校老师联系!");
+            }
+        }
+
+        String key = CryptoHelper.buildKey(
+                new FieldPair("key", user.getKey()),
+                new FieldPair("token", user.getToken()),
+                new FieldPair("timestamp", String.valueOf(timestamp))
+        );
+
+        CryptoGroup cryptoGroup = new CryptoGroup(user.getSalt()).matchKeys(key);
+
+        // 按加密方案组合依次解密(倒序)
+        String jsonParams = cryptoFactory.decrypt(cryptoGroup, encryptParams, true);
+
+        JsonNode jsonNode = new JsonMapper().getNode(jsonParams);
+        if (jsonNode == null || !jsonNode.has("examStudentId") || !jsonNode.has("timestamp")) {
+            log.warn("[param] examStudentId or timestamp is empty...");
+            throw new StatusException("400X01", CryptoConstant.REQUEST_PARAM_ERROR);
+        }
+
+        long timestamp2 = jsonNode.get("timestamp").asLong();
+        if (timestamp != timestamp2) {
+            log.warn("[param] timestamp invalid... {} not equal {}", timestamp, timestamp2);
+            throw new StatusException("400X01", CryptoConstant.REQUEST_PARAM_ERROR);
+        }
+
+        Long examStudentId = jsonNode.get("examStudentId").asLong();
+        StartExamInfo examInfo = this.startExam(examStudentId, user.getUserId(), requestIP, true);
+        String jsonResult = new JsonMapper().toJson(examInfo);
+
+        // 按加密方案组合依次加密
+        return cryptoFactory.encrypt(cryptoGroup, jsonResult, false);
+    }
+
     @Override
     public StartAnswerInfo startAnswer(Long examRecordDataId, Long userId) {
         Check.isNull(examRecordDataId, "examRecordDataId不能为空");
@@ -519,7 +593,7 @@ public class ExamControlServiceImpl implements ExamControlService {
      * @param examStudentId
      * @param userId
      */
-    private void prepare4Exam(Long examStudentId, Long userId) {
+    private void prepare4Exam(Long examStudentId, Long userId, Boolean allowOnline) {
         SysPropertyCacheBean stuClientLoginLimit = CacheHelper.getSysProperty("STU_CLIENT_LOGIN_LIMIT");
         Boolean stuClientLoginLimitBoolean = false;
         if (stuClientLoginLimit.getHasValue()) {
@@ -554,6 +628,20 @@ public class ExamControlServiceImpl implements ExamControlService {
         ExamSettingsCacheBean examSettingsCacheBean = ExamCacheTransferHelper.getCachedExam(examStudent.getExamId(),
                 studentId, examStageId);
 
+        if (allowOnline != null) {
+            if (allowOnline) {
+                // 仅支持“ONLINE”考试类型
+                if (!ExamType.ONLINE.name().equals(examSettingsCacheBean.getExamType())) {
+                    throw new StatusException("440033", "考试类型不正确");
+                }
+            } else {
+                // 不支持“ONLINE”考试类型
+                if (ExamType.ONLINE.name().equals(examSettingsCacheBean.getExamType())) {
+                    throw new StatusException("440033", "考试类型不正确");
+                }
+            }
+        }
+
         StudentCacheBean studentCacheBean = CacheHelper.getStudent(studentId);
 
         CourseCacheBean courseCacheBean = CacheHelper.getCourse(examStudent.getCourseId());
@@ -693,6 +781,42 @@ public class ExamControlServiceImpl implements ExamControlService {
         redisClient.set(examBossKey, examBoss, 60);
     }
 
+    @Override
+    public void manualEndExamWithCrypto(String encryptParams, User user, String requestIP, String timestampStr) {
+        long timestamp;
+        try {
+            timestamp = CryptoHelper.parseTimestamp(timestampStr);
+        } catch (Exception e) {
+            log.error("[header] timestamp is wrong... {}", timestampStr);
+            throw new StatusException("400X01", CryptoConstant.REQUEST_PARAM_ERROR);
+        }
+
+        String key = CryptoHelper.buildKey(
+                new FieldPair("key", user.getKey()),
+                new FieldPair("token", user.getToken()),
+                new FieldPair("timestamp", String.valueOf(timestamp))
+        );
+
+        CryptoGroup cryptoGroup = new CryptoGroup(user.getSalt()).matchKeys(key);
+
+        // 按加密方案组合依次解密(倒序)
+        String jsonParams = cryptoFactory.decrypt(cryptoGroup, encryptParams, true);
+
+        JsonNode jsonNode = new JsonMapper().getNode(jsonParams);
+        if (jsonNode == null || !jsonNode.has("timestamp")) {
+            log.warn("[param] timestamp is empty...");
+            throw new StatusException("400X01", CryptoConstant.REQUEST_PARAM_ERROR);
+        }
+
+        long timestamp2 = jsonNode.get("timestamp").asLong();
+        if (timestamp != timestamp2) {
+            log.warn("[param] timestamp invalid... {} not equal {}", timestamp, timestamp2);
+            throw new StatusException("400X01", CryptoConstant.REQUEST_PARAM_ERROR);
+        }
+
+        this.manualEndExam(user.getUserId(), requestIP);
+    }
+
     @Override
     public void manualEndExam(Long studentId, String ip) {
         String sequenceLockKey = Constants.EXAM_CONTROL_LOCK_PREFIX + studentId;
@@ -809,8 +933,8 @@ public class ExamControlServiceImpl implements ExamControlService {
     public EndExamInfo getEndExamInfo(Long examRecordDataId) {
         ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
         // 如果考试没有最终结束,则返回空值
-        if (null == examRecordData || (examRecordData.getExamRecordStatus() != ExamRecordStatus.EXAM_END
-                && examRecordData.getExamRecordStatus() != ExamRecordStatus.EXAM_OVERDUE)) {
+        if (null == examRecordData) {
+            log.warn("ExamRecordDataCache not exist, maybe has expired! tempExamRecordDataId = {}", examRecordDataId);
             return null;
         }
 
@@ -825,6 +949,8 @@ public class ExamControlServiceImpl implements ExamControlService {
             return endExamInfo;
         }
 
+        log.warn("ExamRecordData status invalid! tempExamRecordDataId = {}, examRecordStatus = {}",
+                examRecordDataId, examRecordData.getExamRecordStatus().name());
         return null;
     }
 
@@ -1162,21 +1288,21 @@ public class ExamControlServiceImpl implements ExamControlService {
      */
     @Override
     public SwitchScreenCountInfo switchScreen(Long examRecordDataId) {
-    	ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(examRecordDataId);
         if (null == examRecordData) {
             throw new StatusException("100001", "找不到相关考试记录");
         }
-        
+
         ExamingSession examSessionInfo = examingSessionService.getExamingSession(examRecordData.getStudentId());
         if (examSessionInfo == null
                 || examSessionInfo.getExamingStatus().equals(ExamingStatus.INFORMAL)) {
             throw new StatusException("101001", "无效的会话,请离开考试");
         }
-        
-        SwitchScreenCountInfo ret=new SwitchScreenCountInfo();
+
+        SwitchScreenCountInfo ret = new SwitchScreenCountInfo();
         //为开启计算切屏次数的不y
-        if(!examSessionInfo.getRecordSwitchScreen()) {
-        	return ret;
+        if (!examSessionInfo.getRecordSwitchScreen()) {
+            return ret;
         }
         ret.setMaxSwitchScreenCount(examSessionInfo.getMaxSwitchScreenCount());
 
@@ -1184,8 +1310,8 @@ public class ExamControlServiceImpl implements ExamControlService {
         int switchScreenCount = null == examRecordData.getSwitchScreenCount() ? 0 : examRecordData.getSwitchScreenCount();
         examRecordData.setSwitchScreenCount(++switchScreenCount);
         ret.setSwitchScreenCount(examRecordData.getSwitchScreenCount());
-        if(ret.getMaxSwitchScreenCount()!=null&&ret.getSwitchScreenCount()>ret.getMaxSwitchScreenCount()) {
-        	examRecordData.setExceedMaxSwitchScreenCount(true);
+        if (ret.getMaxSwitchScreenCount() != null && ret.getSwitchScreenCount() > ret.getMaxSwitchScreenCount()) {
+            examRecordData.setExceedMaxSwitchScreenCount(true);
         }
         examRecordDataService.saveExamRecordDataCache(examRecordDataId, examRecordData);
         return ret;
@@ -1540,20 +1666,20 @@ public class ExamControlServiceImpl implements ExamControlService {
      */
     public void initializeExamRecordSession(ExamingSession examSessionInfo, ExamRecordData examRecordData,
                                             final ExamSettingsCacheBean examBean) {
-    	//切屏设置
-    	Boolean isRecordSwitchScreenCount=false;
-    	Integer maxSwitchScreenCount=null;
-    	OrgPropertyCacheBean ss=CacheHelper.getOrgProperty(examRecordData.getRootOrgId(), "PREVENT_CHEATING_CONFIG");
-    	if(ss!=null&&ss.getHasValue()&&ss.getValue().contains("RECORD_SWITCH_SCREEN")) {
-    		isRecordSwitchScreenCount=true;
-    		ExamPropertyCacheBean sc=CacheHelper.getExamProperty(examBean.getId(), ExamProperties.MAX_SWITCH_SCREEN_COUNT.name());
-    		if(sc!=null&&StringUtils.isNotEmpty(sc.getValue())) {
-    			maxSwitchScreenCount=Integer.valueOf(sc.getValue());
-    		}
-    	}
-    	examSessionInfo.setMaxSwitchScreenCount(maxSwitchScreenCount);
-    	examSessionInfo.setRecordSwitchScreen(isRecordSwitchScreenCount);
-    	
+        //切屏设置
+        Boolean isRecordSwitchScreenCount = false;
+        Integer maxSwitchScreenCount = null;
+        OrgPropertyCacheBean ss = CacheHelper.getOrgProperty(examRecordData.getRootOrgId(), "PREVENT_CHEATING_CONFIG");
+        if (ss != null && ss.getHasValue() && ss.getValue().contains("RECORD_SWITCH_SCREEN")) {
+            isRecordSwitchScreenCount = true;
+            ExamPropertyCacheBean sc = CacheHelper.getExamProperty(examBean.getId(), ExamProperties.MAX_SWITCH_SCREEN_COUNT.name());
+            if (sc != null && StringUtils.isNotEmpty(sc.getValue())) {
+                maxSwitchScreenCount = Integer.valueOf(sc.getValue());
+            }
+        }
+        examSessionInfo.setMaxSwitchScreenCount(maxSwitchScreenCount);
+        examSessionInfo.setRecordSwitchScreen(isRecordSwitchScreenCount);
+
         examSessionInfo.setExamRecordDataId(examRecordData.getId());
         //        examSessionInfo.setStartTime(examRecordData.getStartTime().getTime());//调整为在作答页面时赋值
         examSessionInfo.setExamType(examBean.getExamType());
@@ -1685,7 +1811,7 @@ public class ExamControlServiceImpl implements ExamControlService {
             ReportsUtil.report(new ExamProcessRecordReport(examRecordDataId, ExamProcess.CONTINUE, new Date()));
 
             setAndSaveActiveTime(examRecordDataId, ip);
-            
+
             checkExamInProgressInfo.setExceedMaxSwitchScreenCount(examingRecord.getExceedMaxSwitchScreenCount());
             checkExamInProgressInfo.setSwitchScreenCount(examingRecord.getSwitchScreenCount());
             checkExamInProgressInfo.setMaxSwitchScreenCount(examSessionInfo.getMaxSwitchScreenCount());
@@ -1948,7 +2074,7 @@ public class ExamControlServiceImpl implements ExamControlService {
                     .getExamRecordDataCache(examingSession.getExamRecordDataId());
 
             if ((examRecordData != null && examRecordData.getIsExceed() != null && examRecordData.getIsExceed())
-            		||examRecordData.getExceedMaxSwitchScreenCount()) {// 超过断点最大次数或超过切屏限制的不校验冻结时间
+                    || examRecordData.getExceedMaxSwitchScreenCount()) {// 超过断点最大次数或超过切屏限制的不校验冻结时间
                 return examUsedMilliSeconds;
             }
             long freezeTime = examingSession.getFreezeTime() * 60 * 1000;

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

@@ -358,9 +358,10 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
                 }
 
                 //如果学生作答正确,则客观分累加,答题数累加
-                if (examQuestion.getStudentAnswer() != null
-                        && examQuestion.getCorrectAnswer() != null
+                if (examQuestion.getStudentAnswer() != null && examQuestion.getCorrectAnswer() != null
+                        // && QuestionOptionHelper.isEqualAnswer(examQuestion.getCorrectAnswer(), examQuestion.getStudentAnswer())) {
                         && examQuestion.getStudentAnswer().equals(examQuestion.getCorrectAnswer())) {
+
                     double questionScore = examQuestion.getQuestionScore().doubleValue();
                     BigDecimal bigDecimalQuestionScore = new BigDecimal(Double.toString(questionScore));
                     BigDecimal bigDecimalObjectiveScoreTotal = new BigDecimal(Double.toString(studentObjectiveScoreTotal));
@@ -445,7 +446,6 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
         ExamRecordQuestions examRecordQuestions = examRecordQuestionsService.getExamRecordQuestions(examRecordDataId);
         List<ExamQuestion> examQuestionList = examRecordQuestions.getExamQuestions();
 
-
         //最小维度的小题单元集合
         List<ExamQuestion> questionUnitList = examQuestionList.stream().
                 filter(p -> p.getQuestionId().equals(examQuestion.getQuestionId())).
@@ -462,7 +462,6 @@ public class ExamRecordDataServiceImpl implements ExamRecordDataService {
                 examQuestion.setCorrectAnswer(rightAnswerList.get(i));
             }
         }
-
     }
 
     @Transactional

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

@@ -1,12 +1,15 @@
 package cn.com.qmth.examcloud.core.oe.student.service.impl;
 
 import cn.com.qmth.examcloud.api.commons.enums.ExamType;
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
 import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.util.JsonMapper;
 import cn.com.qmth.examcloud.commons.util.JsonUtil;
 import cn.com.qmth.examcloud.core.oe.student.api.bean.StuExamQuestionBean;
 import cn.com.qmth.examcloud.core.oe.student.api.request.GetExamRecordQuestionsReq;
 import cn.com.qmth.examcloud.core.oe.student.api.response.GetExamRecordQuestionsResp;
 import cn.com.qmth.examcloud.core.oe.student.base.utils.Check;
+import cn.com.qmth.examcloud.core.oe.student.bean.ExamStudentQuestionAnswerInfo;
 import cn.com.qmth.examcloud.core.oe.student.bean.ExamStudentQuestionInfo;
 import cn.com.qmth.examcloud.core.oe.student.dao.ExamRecordQuestionTempRepo;
 import cn.com.qmth.examcloud.core.oe.student.dao.entity.ExamQuestionTempEntity;
@@ -20,6 +23,13 @@ import cn.com.qmth.examcloud.question.commons.core.paper.DefaultQuestionUnitWrap
 import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestion;
 import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionStructure;
 import cn.com.qmth.examcloud.question.commons.core.question.DefaultQuestionUnit;
+import cn.com.qmth.examcloud.question.commons.core.question.QuestionType;
+import cn.com.qmth.examcloud.starters.crypto.CryptoProperties;
+import cn.com.qmth.examcloud.starters.crypto.common.CryptoConstant;
+import cn.com.qmth.examcloud.starters.crypto.common.CryptoGroup;
+import cn.com.qmth.examcloud.starters.crypto.common.CryptoHelper;
+import cn.com.qmth.examcloud.starters.crypto.common.FieldPair;
+import cn.com.qmth.examcloud.starters.crypto.service.CryptoFactory;
 import cn.com.qmth.examcloud.support.Constants;
 import cn.com.qmth.examcloud.support.cache.CacheHelper;
 import cn.com.qmth.examcloud.support.cache.bean.QuestionCacheBean;
@@ -30,6 +40,7 @@ import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
 import cn.com.qmth.examcloud.web.redis.RedisClient;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
+import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
@@ -69,6 +80,12 @@ public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsServic
     @Autowired
     private ExamRecordDataService examRecordDataService;
 
+    @Autowired
+    private CryptoFactory cryptoFactory;
+
+    @Autowired
+    private CryptoProperties cryptoProperties;
+
     @Override
     public void saveExamQuestion(Long examRecordDataId, Integer order, ExamQuestion question) {
         String key = RedisKeyHelper.getBuilder().studentAnswerKey(examRecordDataId, order);
@@ -147,6 +164,12 @@ public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsServic
         for (ExamQuestion examQuestion : examQuestionList) {
             examQuestion.setCorrectAnswer(null);
             examQuestion.setStudentScore(null);
+
+            if (QuestionType.SINGLE_CHOICE == examQuestion.getQuestionType() ||
+                    QuestionType.MULTIPLE_CHOICE == examQuestion.getQuestionType()) {
+                // 选择题 - 转换为新答案格式(兼容旧格式)
+                // examQuestion.setStudentAnswer(QuestionOptionHelper.parseNumbers(examQuestion.getStudentAnswer()));
+            }
         }
         return examQuestionList;
     }
@@ -306,6 +329,37 @@ public class ExamRecordQuestionsServiceImpl implements ExamRecordQuestionsServic
         }
     }
 
+    @Override
+    public void submitQuestionAnswerWithCrypto(ExamStudentQuestionAnswerInfo data, User user, String referer, String agent, String timestampStr) {
+        long timestamp;
+        try {
+            timestamp = CryptoHelper.parseTimestamp(timestampStr);
+        } catch (Exception e) {
+            log.error("[header] timestamp is wrong... {}", timestampStr);
+            throw new StatusException("400X01", CryptoConstant.REQUEST_PARAM_ERROR);
+        }
+
+        String key = CryptoHelper.buildKey(
+                new FieldPair("key", user.getKey()),
+                new FieldPair("token", user.getToken()),
+                new FieldPair("timestamp", String.valueOf(timestamp))
+        );
+
+        CryptoGroup cryptoGroup = new CryptoGroup(user.getSalt()).matchKeys(key);
+
+        // 按加密方案组合依次解密(倒序)
+        String signResult = cryptoFactory.decrypt(cryptoGroup, data.getSign(), true);
+        String answerMd5 = DigestUtils.md5Hex(data.getAnswers());
+        if (!signResult.equals(answerMd5)) {
+            log.warn("[param] answers sign invalid... {} not equal {}", answerMd5, signResult);
+            throw new StatusException("400X01", CryptoConstant.REQUEST_PARAM_ERROR);
+        }
+
+        List<ExamStudentQuestionInfo> answers = new JsonMapper().toList(data.getAnswers(), ExamStudentQuestionInfo.class);
+
+        this.submitQuestionAnswer(user.getUserId(), answers, referer, agent);
+    }
+
     @Override
     public GetExamRecordQuestionsResp getExamRecordQuestions(GetExamRecordQuestionsReq req) {
         ExamRecordQuestions examRecordQuestions = this.getExamRecordQuestions(req.getExamRecordDataId());

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

@@ -0,0 +1,60 @@
+package cn.com.qmth.examcloud.core.oe.student.service.impl;
+
+import cn.com.qmth.examcloud.core.oe.student.service.TempConfigService;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import com.google.common.collect.Lists;
+import org.apache.commons.collections4.CollectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class TempConfigServiceImpl implements TempConfigService {
+
+    private static final Logger log = LoggerFactory.getLogger(TempConfigServiceImpl.class);
+
+    private static final String DISABLED_ORG_ID_CACHE_KEY = "$DISABLED_ORG_ID";
+
+    @Autowired
+    private RedisClient redisClient;
+
+    /**
+     * 获取-被禁用旧版本API的学校列表
+     */
+    @Override
+    public List<Long> getDisabledRootOrgIdsForOldApi() {
+        List<Long> rootOrgIds = redisClient.get(DISABLED_ORG_ID_CACHE_KEY, List.class);
+        if (rootOrgIds == null) {
+            return Lists.newArrayList();
+        }
+
+        return rootOrgIds;
+    }
+
+    /**
+     * 修改-被禁用旧版本API的学校列表
+     */
+    @Override
+    public void updateDisabledRootOrgIdsForOldApi(List<Long> rootOrgIds) {
+        if (CollectionUtils.isEmpty(rootOrgIds)) {
+            redisClient.delete(DISABLED_ORG_ID_CACHE_KEY);
+            log.warn("updateDisabledRootOrgIdsForOldApi ids is empty");
+        } else {
+            redisClient.set(DISABLED_ORG_ID_CACHE_KEY, rootOrgIds);
+            log.warn("updateDisabledRootOrgIdsForOldApi ids={}", rootOrgIds);
+        }
+    }
+
+    /**
+     * 判断学校是否被禁用旧版本API
+     */
+    @Override
+    public boolean checkDisabledForOldApi(Long rootOrgId) {
+        List<Long> rootOrgIds = this.getDisabledRootOrgIdsForOldApi();
+        return rootOrgIds.contains(rootOrgId);
+    }
+
+}

+ 1 - 0
examcloud-core-oe-student-starter/pom.xml

@@ -54,6 +54,7 @@
                 <artifactId>maven-assembly-plugin</artifactId>
                 <configuration>
                     <finalName>examcloud-core-oe-student</finalName>
+                    <skipAssembly>${skipAssembly}</skipAssembly>
                     <descriptors>
                         <descriptor>assembly.xml</descriptor>
                     </descriptors>

+ 2 - 1
examcloud-core-oe-student-starter/src/main/java/cn/com/qmth/examcloud/core/oe/student/starter/config/SwaggerConfig.java

@@ -25,6 +25,7 @@ public class SwaggerConfig {
         List<Parameter> parameters = new ArrayList<>();
         parameters.add(new ParameterBuilder().name("key").modelRef(new ModelRef("String")).parameterType("header").required(false).build());
         parameters.add(new ParameterBuilder().name("token").modelRef(new ModelRef("String")).parameterType("header").required(false).build());
+        parameters.add(new ParameterBuilder().name("timestamp").modelRef(new ModelRef("String")).parameterType("header").required(false).build());
 
         return new Docket(DocumentationType.SWAGGER_2)
                 .groupName("default")
@@ -33,7 +34,7 @@ public class SwaggerConfig {
                 .useDefaultResponseMessages(false)
                 .select()
                 // .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
-                .apis(RequestHandlerSelectors.basePackage("cn.com.qmth.examcloud.core.oe.student.api.controller.client"))
+                .apis(RequestHandlerSelectors.basePackage("cn.com.qmth.examcloud.core.oe.student.api.controller"))
                 .paths(PathSelectors.any())
                 .build();
     }

+ 1 - 0
examcloud-core-oe-student-starter/src/main/resources/log4j2.xml

@@ -37,6 +37,7 @@
     <Loggers>
         <logger name="springfox.documentation" level="WARN"/>
         <logger name="org.springframework" level="WARN"/>
+        <logger name="org.mongodb.driver" level="WARN"/>
         <logger name="org.hibernate" level="WARN"/>
         <logger name="org.apache" level="WARN"/>
         <logger name="org.quartz" level="WARN"/>