|
@@ -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;
|