소스 검색

代码迁移

lideyin 5 년 전
부모
커밋
5095da7f33

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

@@ -0,0 +1,28 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import java.io.Serializable;
+
+/**
+ * 考生作答记录扩展类
+ * @author lideyin
+ * @date 2019年5月14日 下午3:29:59
+ * @description
+ */
+public class ExamQuestionAnswerExtensionInfo implements Serializable {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 6459207019695307248L;
+	@ApiModelProperty(value = "小题显示序号")
+    private Integer subNumber;
+	public Integer getSubNumber() {
+		return subNumber;
+	}
+	public void setSubNumber(Integer subNumber) {
+		this.subNumber = subNumber;
+	}
+	
+}

+ 59 - 51
examcloud-core-oe-student-service/src/main/java/cn/com/qmth/examcloud/core/oe/student/bean/GetUpyunSignatureReq.java

@@ -7,55 +7,63 @@ import javax.validation.constraints.NotNull;
 
 public class GetUpyunSignatureReq implements JsonSerializable {
 
-	/**
-	 * 
-	 */
-	private static final long serialVersionUID = -3761016079101373608L;
-	@NotNull(message = "考试记录DataID不能为空")
-	@ApiModelProperty(required = true,value = "考试记录DataID")
-	private Integer examRecordDataId;
-	@NotNull(message = "题号不能为空")
-	@ApiModelProperty(required = true,value = "考试试题号")
-	private Integer order;
-	@NotNull(message = "文件MD5不能为空")
-	@ApiModelProperty(required = true,value = "文件MD5")
-	private String fileMd5;
-	@NotNull(message = "文件后缀不能为空")
-	@ApiModelProperty(required = true,value = "文件后缀")
-	private String fileSuffix;
-	@ApiModelProperty(value = "文件名自定义参数")
-	private String ext;
-
-	public Integer getExamRecordDataId() {
-		return examRecordDataId;
-	}
-	public void setExamRecordDataId(Integer examRecordDataId) {
-		this.examRecordDataId = examRecordDataId;
-	}
-
-	public Integer getOrder() {
-		return order;
-	}
-	public void setOrder(Integer order) {
-		this.order = order;
-	}
-	public String getFileMd5() {
-		return fileMd5;
-	}
-	public void setFileMd5(String fileMd5) {
-		this.fileMd5 = fileMd5;
-	}
-	public String getFileSuffix() {
-		return fileSuffix;
-	}
-	public void setFileSuffix(String fileSuffix) {
-		this.fileSuffix = fileSuffix;
-	}
-	public String getExt() {
-		return ext;
-	}
-	public void setExt(String ext) {
-		this.ext = ext;
-	}
-	
+    /**
+     *
+     */
+    private static final long serialVersionUID = -3761016079101373608L;
+    @NotNull(message = "考试记录DataID不能为空")
+    @ApiModelProperty(required = true, value = "考试记录DataID")
+    private Long examRecordDataId;
+    @NotNull(message = "题号不能为空")
+    @ApiModelProperty(required = true, value = "考试试题号")
+    private Integer order;
+    @NotNull(message = "文件MD5不能为空")
+    @ApiModelProperty(required = true, value = "文件MD5")
+    private String fileMd5;
+    @NotNull(message = "文件后缀不能为空")
+    @ApiModelProperty(required = true, value = "文件后缀")
+    private String fileSuffix;
+    @ApiModelProperty(value = "文件名自定义参数")
+    private String ext;
+
+    public Long getExamRecordDataId() {
+        return examRecordDataId;
+    }
+
+    public void setExamRecordDataId(Long examRecordDataId) {
+        this.examRecordDataId = examRecordDataId;
+    }
+
+    public Integer getOrder() {
+        return order;
+    }
+
+    public void setOrder(Integer order) {
+        this.order = order;
+    }
+
+    public String getFileMd5() {
+        return fileMd5;
+    }
+
+    public void setFileMd5(String fileMd5) {
+        this.fileMd5 = fileMd5;
+    }
+
+    public String getFileSuffix() {
+        return fileSuffix;
+    }
+
+    public void setFileSuffix(String fileSuffix) {
+        this.fileSuffix = fileSuffix;
+    }
+
+    public String getExt() {
+        return ext;
+    }
+
+    public void setExt(String ext) {
+        this.ext = ext;
+    }
+
 }

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

@@ -0,0 +1,52 @@
+package cn.com.qmth.examcloud.core.oe.student.bean;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class UploadedFileAnswerInfo extends ExamQuestionAnswerExtensionInfo{
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -8684452576786540515L;
+	@ApiModelProperty(value = "考试记录DataID")
+	private Long examRecordDataId;
+//	@ApiModelProperty(value = "考试试题ID")
+//	private String questionId;
+	@ApiModelProperty(value = "考生答案")
+	private String studentAnswer;
+	@ApiModelProperty(value = "大题号")
+	private Integer mainNumber;
+	@ApiModelProperty(value = "小题号")
+    private Integer order;
+	
+	public Long getExamRecordDataId() {
+		return examRecordDataId;
+	}
+	public void setExamRecordDataId(Long examRecordDataId) {
+		this.examRecordDataId = examRecordDataId;
+	}
+//	public String getQuestionId() {
+//		return questionId;
+//	}
+//	public void setQuestionId(String questionId) {
+//		this.questionId = questionId;
+//	}
+	public String getStudentAnswer() {
+		return studentAnswer;
+	}
+	public void setStudentAnswer(String studentAnswer) {
+		this.studentAnswer = studentAnswer;
+	}
+	public Integer getMainNumber() {
+		return mainNumber;
+	}
+	public void setMainNumber(Integer mainNumber) {
+		this.mainNumber = mainNumber;
+	}
+	public Integer getOrder() {
+		return order;
+	}
+	public void setOrder(Integer order) {
+		this.order = order;
+	}
+}

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

@@ -4,8 +4,10 @@ import cn.com.qmth.examcloud.api.commons.enums.ExamSpecialSettingsType;
 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.*;
 import cn.com.qmth.examcloud.commons.util.UUID;
-import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.core.basic.api.bean.CourseBean;
+import cn.com.qmth.examcloud.core.oe.student.base.bean.ExamQuestion;
 import cn.com.qmth.examcloud.core.oe.student.base.bean.ExamRecordQuestions;
 import cn.com.qmth.examcloud.core.oe.student.base.utils.CommonUtil;
 import cn.com.qmth.examcloud.core.oe.student.base.utils.QuestionTypeUtil;
@@ -36,19 +38,30 @@ import cn.com.qmth.examcloud.support.examing.ExamingSession;
 import cn.com.qmth.examcloud.support.examing.ExamingStatus;
 import cn.com.qmth.examcloud.support.helper.ExamCacheTransferHelper;
 import cn.com.qmth.examcloud.support.helper.FaceBiopsyHelper;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
 import cn.com.qmth.examcloud.web.exception.SequenceLockException;
 import cn.com.qmth.examcloud.web.helpers.GlobalHelper;
 import cn.com.qmth.examcloud.web.helpers.SequenceLockHelper;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import com.google.common.base.Splitter;
+import main.java.com.upyun.Base64Coder;
+import main.java.com.upyun.UpException;
+import main.java.com.upyun.UpYunUtils;
 import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang.math.RandomUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.time.DateUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * @author chenken
@@ -85,6 +98,19 @@ public class ExamControlServiceImpl implements ExamControlService {
     @Autowired
     private ExamFaceLivenessVerifyService examFaceLivenessVerifyService;
 
+    @Autowired
+    private RedisClient redisClient;
+
+    private static final String SEPARATOR = "/";
+
+    private static final String UNDERLINE = "_";
+
+    // 又拍云音频答案上传目录
+    private static final String OE_ANSWER_FILE_PATH = "oe-answer-file";
+    private static final String SESSION_TIMEOUT = "$core.basic.sessionTimeout";
+    // 又拍云签名有效时间(秒)
+    private static final Integer SIGN_TIMEOUT = 60;
+
     @Transactional
     @Override
     public StartExamInfo startExam(Long examStudentId, User user) {
@@ -286,9 +312,93 @@ public class ExamControlServiceImpl implements ExamControlService {
 
     @Override
     public CheckQrCodeInfo checkQrCode(String qrCode) {
-        return null;
+        String str;
+        str = UrlUtil.decode(qrCode);
+        Map<String, String> map = Splitter.on("&").withKeyValueSeparator("=").split(str);
+        String examStudentId = map.get("examStudentId");
+        String examRecordDataId = map.get("examRecordDataId");
+        String order = map.get("order");
+        String key = map.get("key");
+        String token = map.get("token");
+        // 需要签名的参数
+        StringBuffer sourStr = new StringBuffer();
+        sourStr.append(order).append(UNDERLINE).append(examRecordDataId).append(UNDERLINE).append(key).append(UNDERLINE)
+                .append(examStudentId);
+        // 签名
+        byte[] bytes = SHA256.encode(sourStr.toString());
+        String hexAscii = ByteUtil.toHexAscii(bytes);
+        if (!hexAscii.equals(token)) {
+            throw new StatusException("100005", "无效的二维码");
+        }
+
+        ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(Long.valueOf(examRecordDataId));
+        // 判断考生是否存在
+        if (!examRecordData.getExamStudentId().equals(Long.valueOf(examStudentId))) {
+            throw new StatusException("100012", "考生不存在");
+        }
+
+        ExamingSession examSessionInfo = examingSessionService.getExamingSession(examRecordData.getStudentId());
+        String clientId;
+        // 未开启环境检测,才进行如下校验
+        if (!isTestDev(Long.valueOf(examRecordDataId))) {
+            // 非环境检测,clientId即examRecordDataId
+            clientId = examRecordDataId;
+            // 判断考试是否结束
+            if (examSessionInfo == null) {
+                throw new StatusException("100006", "考试已结束");
+            }
+            if (examSessionInfo.getExamRecordDataId().longValue() != Long.valueOf(examRecordDataId).longValue()
+                    || examSessionInfo.getExamStudentId().longValue() != Long.valueOf(examStudentId).longValue()) {
+                throw new StatusException("100008", "无效的二维码");
+            }
+        } else {
+            // 环境检测时,clientId即用户id
+            clientId = key.substring(key.lastIndexOf("_") + 1);
+        }
+
+        // 校验通过
+        CheckQrCodeInfo res = new CheckQrCodeInfo();
+        res.setExamRecordDataId(Long.valueOf(examRecordDataId));
+        res.setExamStudentId(Long.valueOf(examStudentId));
+        res.setKey(key);
+        int sessionTimeout = PropertyHolder.getInt(SESSION_TIMEOUT, 3600);
+        User user = redisClient.get(key, User.class, sessionTimeout);
+        if (null == user) {
+            throw new StatusException("100007", "登录信息已失效");
+        }
+        res.setToken(user.getToken());
+        CourseCacheBean courseBean = ExamCacheTransferHelper.getCachedCourse(examRecordData.getCourseId());
+
+        res.setCourseId(courseBean.getId());
+        res.setCourseName(courseBean.getName());
+
+        ExamRecordQuestions examRecordQuestions = examRecordQuestionsService.getExamRecordQuestions(
+                Long.valueOf(examRecordDataId), examSessionInfo.getQuestionCount());
+
+        List<ExamQuestion> examQuestionList = examRecordQuestions.getExamQuestions();
+
+        if (examRecordQuestions == null || examQuestionList == null || examQuestionList.isEmpty()) {
+            throw new StatusException("100008", "无效的二维码");
+        }
+        List<ExamQuestion> filterList = examQuestionList.stream()
+                .filter(p -> p.getOrder().equals(Integer.valueOf(order))).collect(Collectors.toList());
+        if (filterList == null || filterList.isEmpty()) {
+            throw new StatusException("100008", "无效的二维码");
+        }
+        ExamQuestion eqe = filterList.get(0);
+
+        res.setQuestionOrder(eqe.getOrder());
+        res.setQuestionMainNumber(eqe.getMainNumber());
+        res.setSubNumber(getSubNumber(examRecordQuestions, Integer.valueOf(order)));
+        try {
+            this.sendScanQrCodeToWebSocket(clientId, Long.valueOf(examRecordDataId), Integer.valueOf(order));
+        } catch (Exception e) {
+            throw new StatusException("100011", "消息通知失败", e);
+        }
+        return res;
     }
 
+
     /**
      * 发送消息到websocket
      *
@@ -319,6 +429,125 @@ public class ExamControlServiceImpl implements ExamControlService {
 
     @Override
     public UpyunSignatureInfo getUpyunSignature(GetUpyunSignatureReq req) {
+        UpyunSignatureInfo u = new UpyunSignatureInfo();
+        try {
+            ExamRecordData examRecordData = examRecordDataService.getExamRecordDataCache(req.getExamRecordDataId());
+            String md5 = req.getFileMd5();
+            Date signDate = null;
+            Date now = new Date();
+            Date expirationDate = DateUtils.addSeconds(now, SIGN_TIMEOUT);
+
+            StringBuffer filePath = new StringBuffer();
+
+            filePath.append(SEPARATOR).append(OE_ANSWER_FILE_PATH).append(SEPARATOR)
+                    .append(examRecordData.getExamStudentId()).append(SEPARATOR).append(req.getExamRecordDataId())
+                    .append(SEPARATOR).append(req.getOrder()).append(SEPARATOR)
+                    .append(examRecordData.getExamStudentId()).append(UNDERLINE).append(req.getExamRecordDataId())
+                    .append(UNDERLINE).append(req.getOrder()).append(UNDERLINE).append(System.currentTimeMillis())
+                    .append(RandomUtils.nextInt(8999) + 1000);
+
+            if (StringUtils.isNotEmpty(req.getExt())) {
+                filePath.append(UNDERLINE).append(req.getExt());
+            }
+            filePath.append(".").append(req.getFileSuffix());
+
+            long expiration = expirationDate.getTime() / 1000;
+            String bucketName=PropertyHolder.getString("$upyun.site.1.bucketName");
+            String userName=PropertyHolder.getString("$upyun.site.1.userName");
+            String password=PropertyHolder.getString("$upyun.site.1.password");
+
+            String policy = policy(bucketName, expiration, filePath.toString(), signDate, md5);
+            String sign = sign("POST", getGMTDate(signDate), bucketName, policy, userName, UpYunUtils.md5(password),
+                    md5);
+            u.setPolicy(policy);
+            u.setSignature(sign);
+            u.setFilePath(filePath.toString());
+
+            String bucketUrl="https://v0.api.upyun.com";
+            String upyunFileUrl=PropertyHolder.getString("$upyun.site.1.domain");
+            u.setUploadUrl(UrlUtil.joinUrl(bucketUrl, bucketName));
+            u.setUpyunFileDomain(upyunFileUrl);
+        } catch (UpException e) {
+            throw new StatusException("100003", "获取又拍云签名失败");
+        }
+        return u;
+    }
+    /**
+     * @param bucketName //不能为空
+     * @param expiration //不能为空
+     * @param filePath   //不能为空
+     * @param date       为空时,以又怕云时间和expiration比较,不为空时以此date和expiration比较
+     * @param md5        //可以为空
+     * @return
+     */
+    private String policy(String bucketName, Long expiration, String filePath, Date date, String md5) {
+        Map<String, Object> paramMap = new HashMap<String, Object>();
+        // 不能为空
+        paramMap.put("bucket", bucketName);
+        Date expirationDate = DateUtils.addSeconds(new Date(), SIGN_TIMEOUT);
+        // 不能为空
+        paramMap.put("expiration", expirationDate.getTime() / 1000);
+        paramMap.put("save-key", filePath);
+        // 为空时,以又怕云时间和expiration比较,不为空时以此date和expiration比较
+        paramMap.put("date", date);
+        // 可以为空
+        paramMap.put("content-md5", md5);
+        String policy = UpYunUtils.getPolicy(paramMap);
+        return policy;
+    }
+
+    private String getGMTDate(Date d) {
+        if (d == null) {
+            return null;
+        }
+        SimpleDateFormat formater = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
+        formater.setTimeZone(TimeZone.getTimeZone("GMT+8"));
+        return formater.format(d);
+    }
+
+    /**
+     * 必须和policy中date一致,可以都为空.GMTDate
+     *
+     * @param method
+     * @param date
+     * @param bucketName
+     * @param policy
+     * @param userName
+     * @param password
+     * @param md5
+     * @return
+     * @throws UpException
+     */
+    private String sign(String method, String date, String bucketName, String policy, String userName, String password,
+                        String md5) throws UpException {
+
+        StringBuilder sb = new StringBuilder();
+        String sp = "&";
+        sb.append(method);
+        sb.append(sp);
+        sb.append(SEPARATOR + bucketName);
+        if (date != null) {
+            sb.append(sp);
+            sb.append(date);
+        }
+        sb.append(sp);
+        sb.append(policy);
+        if (md5 != null && md5.length() > 0) {
+            sb.append(sp);
+            sb.append(md5);
+        }
+        String raw = sb.toString().trim();
+        byte[] hmac = null;
+        try {
+            hmac = UpYunUtils.calculateRFC2104HMACRaw(password, raw);
+        } catch (Exception e) {
+            throw new UpException("calculate SHA1 wrong.");
+        }
+
+        if (hmac != null) {
+            return "UPYUN " + userName + ":" + Base64Coder.encodeLines(hmac).trim();
+        }
+
         return null;
     }
 
@@ -780,4 +1009,67 @@ public class ExamControlServiceImpl implements ExamControlService {
 
         return examUsedMilliSeconds;
     }
+
+    private Integer getSubNumber(ExamRecordQuestions examRecordQuestions, Integer order) {
+        List<UploadedFileAnswerInfo> list = getReSortedQuestionList(examRecordQuestions);
+        for (UploadedFileAnswerInfo info : list) {
+            if (order.intValue() == info.getOrder().intValue()) {
+                return info.getSubNumber();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 根据考试记录id获取所有已经重排序号的考试列表
+     *
+     * @param examRecordQuestions
+     * @return
+     */
+    private List<UploadedFileAnswerInfo> getReSortedQuestionList(ExamRecordQuestions examRecordQuestions) {
+        // 所有试题集合,包括非音频题
+        List<UploadedFileAnswerInfo> resultList = new ArrayList<UploadedFileAnswerInfo>();
+
+        if (null == examRecordQuestions || null == examRecordQuestions.getExamQuestions()
+                || examRecordQuestions.getExamQuestions().isEmpty()) {
+            return resultList;
+        }
+
+        List<ExamQuestion> examQuestionAnswerList = examRecordQuestions.getExamQuestions();
+        // 按大题号分组并排序
+        Map<Integer, List<ExamQuestion>> sortedMainNumberList = examQuestionAnswerList.stream()
+                .sorted(Comparator.comparing(ExamQuestion::getMainNumber))
+                .collect(Collectors.groupingBy(ExamQuestion::getMainNumber, Collectors.toList()));
+        for (Integer mainNum : sortedMainNumberList.keySet()) {
+            // 当前大题下的小题集合
+            List<ExamQuestion> subEmptyExamQuestionAnswerList = sortedMainNumberList.get(mainNum);
+            // 按order对小题进行重新排序
+            subEmptyExamQuestionAnswerList.sort(Comparator.comparing(ExamQuestion::getOrder));
+            for (int i = 0; i < subEmptyExamQuestionAnswerList.size(); i++) {
+                ExamQuestion curExamQuestion = subEmptyExamQuestionAnswerList.get(i);
+                UploadedFileAnswerInfo info = new UploadedFileAnswerInfo();
+                info.setExamRecordDataId(curExamQuestion.getExamRecordDataId());
+                info.setMainNumber(curExamQuestion.getMainNumber());
+                info.setOrder(curExamQuestion.getOrder());
+                // info.setQuestionId(curExamQuestion.getQuestionId());
+                info.setSubNumber(i + 1);// 每个大题下,给小题序号重新赋值
+                resultList.add(info);
+            }
+        }
+        return resultList;
+    }
+
+    /**
+     * 根据考试记录id判断是否开启环境检测
+     *
+     * @param examRecordDataId
+     * @return
+     */
+    private boolean isTestDev(Long examRecordDataId) {
+        // 用于环境检测的考试记录id
+        SysPropertyCacheBean examRecordDataIdObject = CacheHelper.getSysProperty("oe.testDev.examRecordDataId");
+        // 是否开启了环境检测(请求的考试记录id等于用于环境检测的考试记录时,则认为开启了环境检测)
+        return examRecordDataIdObject.getHasValue()
+                && examRecordDataId.equals(Long.valueOf(examRecordDataIdObject.getValue().toString()));
+    }
 }