package com.qmth.themis.business.service.impl;

import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qmth.themis.business.bean.admin.OpenExamBean;
import com.qmth.themis.business.bean.exam.*;
import com.qmth.themis.business.bean.result.VideoQueryResult;
import com.qmth.themis.business.cache.ExamBreakCacheUtil;
import com.qmth.themis.business.cache.ExamRecordCacheUtil;
import com.qmth.themis.business.cache.ExamingDataCacheUtil;
import com.qmth.themis.business.cache.RedisKeyHelper;
import com.qmth.themis.business.cache.bean.*;
import com.qmth.themis.business.constant.SpringContextHolder;
import com.qmth.themis.business.constant.SystemConstant;
import com.qmth.themis.business.dao.TEExamMapper;
import com.qmth.themis.business.dto.MqDto;
import com.qmth.themis.business.dto.cache.TEStudentCacheDto;
import com.qmth.themis.business.dto.response.TEExamActivityDto;
import com.qmth.themis.business.dto.response.TEExamActivityWaitDto;
import com.qmth.themis.business.dto.response.TEExamQueryDto;
import com.qmth.themis.business.dto.response.TEExamWaitDto;
import com.qmth.themis.business.entity.*;
import com.qmth.themis.business.enums.*;
import com.qmth.themis.business.service.*;
import com.qmth.themis.business.util.*;
import com.qmth.themis.common.contanst.Constants;
import com.qmth.themis.common.enums.ExceptionResultEnum;
import com.qmth.themis.common.exception.BusinessException;
import com.qmth.themis.common.util.GsonUtil;
import com.qmth.themis.common.util.IpUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @Description: 考试批次 服务实现类
 * @Param:
 * @return:
 * @Author: wangliang
 * @Date: 2020/6/25
 */
@Service
public class TEExamServiceImpl extends ServiceImpl<TEExamMapper, TEExam> implements TEExamService {

    @Resource
    TEExamMapper teExamMapper;

    @Resource
    TEExamActivityService teExamActivityService;

    @Resource
    TEExamStudentService teExamStudentService;

    @Resource
    TEExamCourseService teExamCourseService;

    @Resource
    TEExamPaperService teExamPaperService;

    @Resource
    TOeExamRecordService toeExamRecordService;

    @Resource
    RedisUtil redisUtil;

    @Resource
    OssUtil ossUtil;

    @Resource
    MqDtoService mqDtoService;

    @Resource
    TencentYunUtil tencentYunUtil;

    @Resource
    private TBTaskHistoryService tbTaskHistoryService;

    @Resource
    TEExamPaperService examPaperService;

    @Resource
    MqUtil mqUtil;

    @Resource
    TGErrorService tgErrorService;

    @Resource
    TOeExamBreakHistoryService tOeExamBreakHistoryService;

    @Resource
    ThemisCacheService themisCacheService;

    @Resource
    TEExamService teExamService;

    /**
     * 查询考试批次
     *
     * @param iPage
     * @param userId
     * @param id
     * @param code
     * @param name
     * @param mode
     * @param enable
     * @param orgId
     * @param type
     * @param finished
     * @return
     */
    @SuppressWarnings("rawtypes")
    @Override
    public IPage<TEExamQueryDto> examQuery(IPage<Map> iPage, Long userId, Long id, String code, String name,
                                           String mode, Integer enable, Long orgId, String type, Boolean finished) {
        return teExamMapper.examQuery(iPage, userId, id, code, name, mode, enable, orgId, type, finished);
    }

    /**
     * 根据考试批次编码查询考场接口
     *
     * @param code
     * @return
     */
    @Override
    public List<Map> examroomQuery(String code) {
        return teExamMapper.examroomQuery(code);
    }

    /**
     * 获取考试待考列表
     *
     * @param studentId
     * @param examId
     * @param orgId
     * @return
     */
    @Override
    public List<TEExamWaitDto> getWaitingExam(Long studentId, Long examId, Long orgId) {
        List<TEExamWaitDto> list = new ArrayList<>();
        if (Objects.isNull(examId)) {
            Map<String, Set<Long>> examList = themisCacheService.getOrgExamListCache(orgId.toString());
            if (!CollectionUtils.isEmpty(examList)) {
                Set<Long> examActivityIds = new HashSet<>();
                for (Map.Entry<String, Set<Long>> entry : examList.entrySet()) {
                    examActivityIds.addAll(entry.getValue());
                }
                List<TEExamActivityWaitDto> teExamActivityWaitList = teExamActivityService.getWaitingExam(studentId, examActivityIds, orgId);
                list = this.waitingExamCommon(list, teExamActivityWaitList);
            }
        } else {
            Set<Long> examActivityIdSet = themisCacheService.getOrgExamListCache(orgId.toString(), examId.toString());
            if (!CollectionUtils.isEmpty(examActivityIdSet)) {
                List<TEExamActivityWaitDto> teExamActivityWaitList = teExamActivityService.getWaitingExam(studentId, examActivityIdSet, orgId);
                list = this.waitingExamCommon(list, teExamActivityWaitList);
            }
        }
        Collections.sort(list);
        return list;
    }

    /**
     * 获取待考列表公用
     *
     * @param list
     * @param teExamActivityWaitList
     * @return
     */
    protected List<TEExamWaitDto> waitingExamCommon(List<TEExamWaitDto> list, List<TEExamActivityWaitDto> teExamActivityWaitList) {
        if (!CollectionUtils.isEmpty(teExamActivityWaitList)) {
            Map<Long, Set<TEExamActivityWaitDto>> map = new HashMap<>();
            Map<Long, TEExamWaitDto> setMap = new LinkedHashMap<>();
            teExamActivityWaitList.forEach(i -> {
                ExamCacheBean examCache = getExamCacheBeanNative(i.getExamId());
                TEExamWaitDto teExamWaitDto = new TEExamWaitDto(i.getExamId(), i.getExamActivityId(), examCache.getName(), examCache.getCode());
                setMap.put(i.getExamId(), teExamWaitDto);
                Set<TEExamActivityWaitDto> teExamActivityWaitDtos = Objects.nonNull(map.get(i.getExamId())) ? map.get(i.getExamId()) : new LinkedHashSet<>();
                teExamActivityWaitDtos.add(i);
                map.put(i.getExamId(), teExamActivityWaitDtos);
                ExamStudentCacheBean examStudentCacheBean = teExamStudentService.getExamStudentCacheBean(i.getExamStudentId());
                Integer leftExamCount = examCache.getExamCount().intValue() - examStudentCacheBean.getAlreadyExamCount().intValue() < 0 ? 0 : examCache.getExamCount().intValue() - examStudentCacheBean.getAlreadyExamCount().intValue();
                i.setLeftExamCount(leftExamCount);
            });
            setMap.forEach((k, v) -> {
                v.setActivities(new ArrayList<>(map.get(k)));
                list.add(v);
            });
        }
        return list;
    }

    private void checkIp(ExamCacheBean examCache) {
        if (examCache.getEnableIpLimit() != null && examCache.getEnableIpLimit().intValue() == 1) {
            String ipAllows = examCache.getIpAllow();
            //ip限制判断
            String ip = IpUtil.getRemoteIp(ServletUtil.getRequest());
            boolean allow = false;
            for (String ipAllow : ipAllows.split(",")) {
                String reg = "^" + ipAllow.replaceAll("\\*", "[0-9]{1,3}").replaceAll("\\.", "\\\\.") + "$";
                Pattern p = Pattern.compile(reg);
                Matcher matcher = p.matcher(ip);
                if (matcher.matches()) {
                    allow = true;
                    break;
                }
            }
            if (!allow) {
                throw new BusinessException(ExceptionResultEnum.EXAM_STATUS_NOT_ALLOW_IP);
            }
        }
    }

    /**
     * 开始候考
     */
    @Transactional
    @Override
    public ExamPrepareBean prepare(Long studentId, Long examStudentId) {
        TBSession tbSession = (TBSession) ServletUtil.getRequestSession();
        String monitorUserId = "s_" + tbSession.getId();
        ExamStudentCacheBean es = teExamStudentService.getExamStudentCacheBean(examStudentId);
        if (es == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_STUDENT);
        }

        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException(ExceptionResultEnum.EXAM_ID_NOT_EQUALY);
        }
        //停用
        if (es.getEnable().intValue() == 0) {
            throw new BusinessException(ExceptionResultEnum.EXAM_STUDENT_ENABLE);
        }
        ExamCacheBean examCache = getExamCacheBeanNative(es.getExamId());
        if (examCache.getEnable().intValue() == 0) {
            throw new BusinessException(ExceptionResultEnum.EXAM_ENABLE);
        }
        if (ExamingDataCacheUtil.getUnFinishedRecordId(studentId) != null) {
            throw new BusinessException(ExceptionResultEnum.EXAM_RECORD_UNFINISHED);
        }
        checkIp(examCache);

        Long activityId = es.getExamActivityId();
        Long examId = es.getExamId();
        Long currentRecordId = es.getCurrentRecordId();
        TOeExamRecord tOeExamRecord = null;
        if (Objects.nonNull(currentRecordId)) {
            tOeExamRecord = toeExamRecordService.getCacheExamRecord(currentRecordId);
            if (Objects.isNull(tOeExamRecord)) {
                tOeExamRecord = toeExamRecordService.getExamRecordForDb(examId, activityId, examStudentId, ExamRecordStatusEnum.FIRST_PREPARE);
            }
        } else {
            tOeExamRecord = toeExamRecordService.getExamRecordForDb(examId, activityId, examStudentId, ExamRecordStatusEnum.FIRST_PREPARE);
        }
        if (Objects.nonNull(tOeExamRecord) && Objects.nonNull(tOeExamRecord.getStatus()) && (ExamRecordStatusEnum.FINISHED == tOeExamRecord.getStatus() || ExamRecordStatusEnum.PERSISTED == tOeExamRecord.getStatus() || tOeExamRecord.getStatus() == ExamRecordStatusEnum.JUNK)) {
            throw new BusinessException(ExceptionResultEnum.EXAM_FINISH);
        }
        Long unFinishedRecordId = Objects.nonNull(tOeExamRecord) ? tOeExamRecord.getId() : null;
        if (Objects.nonNull(unFinishedRecordId)) {
            Long recordId = unFinishedRecordId;
            if (examStudentId.equals(ExamRecordCacheUtil.getExamStudentId(recordId))) {//当前考生已存在候考的考试记录
                ExamPaperCacheBean ep = teExamPaperService
                        .getExamPaperCacheBean(ExamRecordCacheUtil.getPaperId(recordId));
                ExamCourseCacheBean ec = teExamCourseService.getExamCourseCacheBean(es.getExamId(), es.getCourseCode());
                ExamStudentCacheBean examStudentCacheBean = teExamStudentService.getExamStudentCacheBean(examStudentId);
                ExamActivityCacheBean ac = teExamActivityService.getExamActivityCacheBean(es.getExamActivityId());
                ExamPrepareBean prepare = new ExamPrepareBean(recordId,
                        (ec.getObjectiveShuffle() == null || ec.getObjectiveShuffle().intValue() == 0 ? false : true),
                        (ec.getOptionShuffle() == null || ec.getOptionShuffle().intValue() == 0 ? false : true),
                        (ep.getHasAudio() == null || ep.getHasAudio().intValue() == 0 ? false : true),
                        ep.getAudioPlayCount(), ExamRecordCacheUtil.getMonitorKey(recordId), monitorUserId,
                        tencentYunUtil.getSign(monitorUserId, SystemConstant.TENCENT_EXPIRE_TIME),
                        tencentYunUtil.getAppId(), examCache.getExamStudentCallEnable());
                ExamRecordStatusEnum statusEnum = ExamRecordCacheUtil.getStatus(recordId);
                boolean cache = false;
                if (Objects.nonNull(statusEnum) && !Objects.equals(statusEnum, ExamRecordStatusEnum.RESUME_PREPARE)) {
                    ExamRecordCacheUtil.updateExamRecordCache(recordId, examCache, ac, ec);
                } else {
                    cache = true;
                }

                SysConfig sysConfig = themisCacheService.addSysConfigCache(SystemConstant.MONITOR_CONFIG_PREFIX);
                Optional.ofNullable(sysConfig).orElseThrow(() -> new BusinessException("未配置监控前缀"));
                TEExamActivityDto teExamActivityDto = new TEExamActivityDto(examCache, ac, examStudentCacheBean,
                        examStudentId, ec, recordId, sysConfig.getConfigValue(), cache);
                prepare.setTeExamActivityDto(teExamActivityDto);
                ExamingDataCacheUtil.setExamingRecordId(studentId, recordId);
                return prepare;
            }
        }
        if (examCache.getExamCount().intValue() <= es.getAlreadyExamCount().intValue()) {
            throw new BusinessException(ExceptionResultEnum.EXAM_LEFT_COUNT_IS_ZERO);
        }
        ExamActivityCacheBean ac = teExamActivityService.getExamActivityCacheBean(activityId);
        if (ac == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_ACTIVITY);
        }
        if (ac.getEnable().intValue() == 0) {
            throw new BusinessException(ExceptionResultEnum.EXAM_ACTIVITY_ENABLE);
        }
        Date now = new Date();
        if (ExamModeEnum.ANYTIME.equals(examCache.getMode())) {
            Long start = ac.getStartTime() - (ac.getPrepareSeconds() * 1000);
            Long end = ac.getFinishTime();
            if (now.getTime() < start) {
                throw new BusinessException(ExceptionResultEnum.NOT_ALLOW_MAX_START_TIME);
            }
            if (now.getTime() > end) {
                throw new BusinessException(ExceptionResultEnum.MAX_START_TIME_FINISHED);
            }
        } else {
            Long start = ac.getStartTime() - (ac.getPrepareSeconds() * 1000);
            Integer openingSecondsTemp = ac.getOpeningSeconds();
            openingSecondsTemp = Objects.nonNull(openingSecondsTemp) && openingSecondsTemp.intValue() == 0 ? SystemConstant.DEFAULT_OPENING_SECONDS : openingSecondsTemp;
            Long end = ac.getStartTime() + (openingSecondsTemp * 1000);
            if (now.getTime() < start) {
                throw new BusinessException(ExceptionResultEnum.NOT_ALLOW_MAX_START_TIME);
            }
            if (now.getTime() > end) {
                throw new BusinessException(ExceptionResultEnum.MAX_START_TIME_FINISHED);
            }
        }

        ExamCourseCacheBean ec = teExamCourseService.getExamCourseCacheBean(es.getExamId(), es.getCourseCode());
        if (ec == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_COURSE);
        }
        if (ec.getPaperIds() == null) {
            throw new BusinessException(ExceptionResultEnum.EXAM_COURSE_OR_EXAM_PAPER_NOT_BIND);
        }
        //        if (ec.getHasAnswer() == null || ec.getHasAnswer().intValue() == 0) {
        //            throw new BusinessException("考试科目答案未补全");
        //        }
        // 根据权重选中试卷
        Long paperId = ec.getPaperIds().get(getPaperByWeight(ec.getPaperWeight()));
        ExamPaperCacheBean ep = teExamPaperService.getExamPaperCacheBean(paperId);
        if (ep == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_PAPER + "：" + paperId);
        }
        //        if (StringUtils.isBlank(ep.getAnswerPath())) {
        //            throw new BusinessException("试卷答案未上传：" + paperId);
        //        }

        tOeExamRecord = toeExamRecordService
                .saveByPrepare(es.getExamId(), es.getExamActivityId(), examStudentId, paperId,
                        es.getAlreadyExamCount() + 1, studentId, ec.getMobilePhotoUpload());
        Long recordId = tOeExamRecord.getId();
        es.setCurrentRecordId(recordId);
        // 更新考生缓存
        redisUtil.set(RedisKeyHelper.examStudentCacheKey(examStudentId), es);
        updateExamStudent(examStudentId, null, recordId);

        ExamPrepareBean prepare = new ExamPrepareBean(recordId,
                (ec.getObjectiveShuffle() == null || ec.getObjectiveShuffle().intValue() == 0 ? false : true),
                (ec.getOptionShuffle() == null || ec.getOptionShuffle().intValue() == 0 ? false : true),
                (ep.getHasAudio() == null || ep.getHasAudio().intValue() == 0 ? false : true), ep.getAudioPlayCount(),
                ExamRecordCacheUtil.getMonitorKey(recordId), monitorUserId,
                tencentYunUtil.getSign(monitorUserId, SystemConstant.TENCENT_EXPIRE_TIME),
                tencentYunUtil.getAppId(), examCache.getExamStudentCallEnable());

        SysConfig sysConfig = themisCacheService.addSysConfigCache(SystemConstant.MONITOR_CONFIG_PREFIX);
        Optional.ofNullable(sysConfig).orElseThrow(() -> new BusinessException("未配置监控前缀"));
        TEExamActivityDto teExamActivityDto = teExamActivityService
                .getWaitingExam(activityId, examStudentId, es.getCourseCode(),
                        sysConfig.getConfigValue(), recordId);
        prepare.setTeExamActivityDto(teExamActivityDto);

        // 更新考生缓存
        redisUtil.set(RedisKeyHelper.examStudentCacheKey(examStudentId), es);
        //更新场次-考试记录缓存
        //        ExamActivityRecordCacheUtil.setExamRecordStatus(activityId, recordId);
        //        ExamingDataCacheUtil.setUnFinishedRecordId(studentId, recordId);
        //mq发送消息start
        TEStudentCacheDto teStudentCacheDto = themisCacheService.addStudentAccountCache(es.getStudentId());
        MqDto mqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.STUDENT.name(), SystemOperationEnum.FIRST_PREPARE, MqTagEnum.STUDENT, String.valueOf(teStudentCacheDto.getId()), IpUtil.getRemoteIp(ServletUtil.getRequest()));
        this.sendOeLogMessage(SystemOperationEnum.FIRST_PREPARE, examStudentId, recordId, mqDto);
        //mq发送消息end

        //最后开考时间发送定时任务
        //删除再新增quartz任务,发送mq消息start
        Map<String, Object> prop = new HashMap<>();
        prop.put(SystemConstant.OPER, SystemConstant.INSERT);
        prop.put(SystemConstant.EXAM, teExamService.getById(es.getExamId()));
        MqDto mqDtoFinalPrepare = new MqDto(mqUtil.getTopic(), MqTagEnum.EXAM_ACTIVITY_FINAL_PREPARE_TIME.name(), JacksonUtil.parseJson(tOeExamRecord), MqTagEnum.EXAM_ACTIVITY_FINAL_PREPARE_TIME, String.valueOf(tOeExamRecord.getId()), prop, String.valueOf(tOeExamRecord.getId()));
        mqDtoService.assembleSendAsyncOrderMsg(mqDtoFinalPrepare);
        //删除再新增quartz任务,发送mq消息end
        return prepare;
    }

    private void updateExamStudent(Long examStudentId, Integer alreadyExamCount, Long currentRecordId) {
        Map<String, Object> transMap = new HashMap<String, Object>();
        transMap.put(SystemConstant.EXAM_STUDENT_ID, examStudentId);
        if (Objects.nonNull(alreadyExamCount)) {
            transMap.put("alreadyExamCount", alreadyExamCount);
        }
        if (Objects.nonNull(currentRecordId)) {
            transMap.put("currentRecordId", currentRecordId);
        }
        MqDto mqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.EXAM_STUDNET_UPDATE.name(), transMap,
                MqTagEnum.EXAM_STUDNET_UPDATE, examStudentId.toString(), examStudentId.toString());
        mqDtoService.assembleSendAsyncMsg(mqDto);
    }

    /**
     * 根据设定几率取出一套试卷
     */
    private int getPaperByWeight(List<Double> paperWeight) {
        BigDecimal r = BigDecimal.valueOf(new Random().nextInt(101));
        for (int i = 0; i < paperWeight.size(); i++) {
            r = r.subtract(BigDecimal.valueOf(paperWeight.get(i)));
            if (r.doubleValue() <= 0.0d) {
                return i;// 选中
            }
        }
        return -1;
    }

    /**
     * 开始答题
     */
    @Override
    public ExamStartBean start(Long studentId, Long recordId) {
        ExamStartBean ret = null;

        // 校验当前登录用户和参数一致性
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_RECORD);
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean examStudentCacheBean = teExamStudentService.getExamStudentCacheBean(examStudentId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_STUDENT);
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException(ExceptionResultEnum.EXAM_ID_NOT_EQUALY);
        }
        //停用
        if (es.getEnable().intValue() == 0) {
            throw new BusinessException(ExceptionResultEnum.EXAM_STUDENT_ENABLE);
        }
        Boolean finished = toeExamRecordService.examBreakLogic(recordId, false);
        if (finished) {
            throw new BusinessException(ExceptionResultEnum.EXAM_FINISH);
        }
        ExamCacheBean exam = getExamCacheBeanNative(es.getExamId());
        Long activityId = es.getExamActivityId();
        ExamActivityCacheBean ac = teExamActivityService.getExamActivityCacheBean(activityId);
        if (ac == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_ACTIVITY);
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta) || sta == ExamRecordStatusEnum.JUNK) {
            throw new BusinessException(ExceptionResultEnum.EXAM_FINISH);
        }
        Date now = new Date();
        if (ExamModeEnum.ANYTIME.equals(exam.getMode())) {
            Long start = ac.getStartTime();
            Long end = ac.getFinishTime();
            if (now.getTime() < start) {
                throw new BusinessException(ExceptionResultEnum.NOT_ALLOW_MAX_START_TIME);
            }
            if (!ExamRecordStatusEnum.RESUME_PREPARE.equals(sta)) {//非断点进入的
                if (now.getTime() > end) {
                    throw new BusinessException(ExceptionResultEnum.MAX_START_TIME_FINISHED);
                }
            }
        } else {
            Long start = ac.getStartTime();
            if (now.getTime() < start) {
                throw new BusinessException(ExceptionResultEnum.NOT_ALLOW_MAX_START_TIME);
            }
            if (ExamRecordStatusEnum.RESUME_PREPARE.equals(sta)) {//断点进入的
                //集中不启用统一收卷，考试快结束时，断点，再登录时， 时间已经超过考试截止时间，断点时，此时应该能登录继续作答
                Integer forceFinish = ExamRecordCacheUtil.getForceFinish(recordId);
                if (forceFinish != null && forceFinish.intValue() == 1) {
                    if (now.getTime() > ac.getFinishTime()) {
                        throw new BusinessException(ExceptionResultEnum.FORCE_FINISH);
                    }
                }
            } else {
                Integer openingSecondsTemp = ac.getOpeningSeconds();
                openingSecondsTemp = Objects.nonNull(openingSecondsTemp) && openingSecondsTemp.intValue() == 0 ? SystemConstant.DEFAULT_OPENING_SECONDS : openingSecondsTemp;
                Long end = ac.getStartTime() + (openingSecondsTemp * 1000);
                if (now.getTime() > end) {
                    throw new BusinessException(ExceptionResultEnum.MAX_START_TIME_FINISHED);
                }
            }
        }

        Long paperId = ExamRecordCacheUtil.getPaperId(recordId);
        ExamPaperCacheBean ep = teExamPaperService.getExamPaperCacheBean(paperId);
        if (ep == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_PAPER);
        }

        WebsocketStatusEnum websocketStatusEnum = ExamRecordCacheUtil.getClientWebsocketStatus(recordId);
        if (Objects.nonNull(websocketStatusEnum) && Objects.equals(WebsocketStatusEnum.OFF_LINE, websocketStatusEnum)) {
            throw new BusinessException(ExceptionResultEnum.CLIENT_NET_OFFLINE);
        }

        if (Objects.nonNull(exam.getMonitorVideoSource())) {
            String[] strs = exam.getMonitorVideoSource().split(",");
            for (int i = 0; i < strs.length; i++) {
                MonitorStatusSourceEnum clientCameraStatus = ExamRecordCacheUtil
                        .getMonitorStatus(recordId, MonitorVideoSourceEnum.valueOf(strs[i]));
                if (Objects.isNull(clientCameraStatus) || Objects
                        .equals(MonitorStatusSourceEnum.STOP, clientCameraStatus)) {
                    switch (strs[i]) {
                        case "CLIENT_CAMERA":
                            throw new BusinessException(ExceptionResultEnum.CLIENT_CAMERA_OFFLINE);
                        case "CLIENT_SCREEN":
                            throw new BusinessException(ExceptionResultEnum.CLIENT_SCREEN_OFFLINE);
                        case "MOBILE_FIRST":
                            throw new BusinessException(ExceptionResultEnum.MOBILE_FIRST_OFFLINE);
                        default:
                            throw new BusinessException(ExceptionResultEnum.MOBILE_SECOND_OFFLINE);
                    }
                }
            }
        }
        ret = new ExamStartBean();
        ret.setPaperDecryptSecret(ep.getDecryptSecret());
        ret.setPaperDecryptVector(ep.getDecryptVector());

        boolean lock = false;
        for (int i = 0; i < SystemConstant.MAX_EXAM_STATUS_COUNT; i++) {
            lock = redisUtil.lock(SystemConstant.REDIS_LOCK_EXAM_STATUS_PREFIX + recordId,
                    SystemConstant.REDIS_LOCK_EXAM_STATUS_TIME_OUT);
            if (lock) {
                try {
                    // 更新考试记录缓存
                    Long firstStartTime = ExamRecordCacheUtil.getFirstStartTime(recordId), lastStartTime = null;
                    Long timestamp = System.currentTimeMillis();
                    ExamRecordCacheUtil.setStatus(recordId, ExamRecordStatusEnum.ANSWERING, timestamp);
                    if (Objects.isNull(firstStartTime)) {
                        ExamRecordCacheUtil.setFirstStartTime(recordId, timestamp);
                    } else {
                        lastStartTime = timestamp;
                        ExamRecordCacheUtil.setLastStartTime(recordId, timestamp);
                    }
                    Long breakId = ExamRecordCacheUtil.getLastBreakId(recordId);
                    ExamRecordStatusEnum examRecordStatusEnum = ExamRecordCacheUtil.getStatus(recordId);
                    if (Objects.nonNull(examRecordStatusEnum)
                            && examRecordStatusEnum == ExamRecordStatusEnum.ANSWERING) {
                        if (Objects.nonNull(breakId) && Objects.nonNull(lastStartTime)) {
                            ExamBreakCacheUtil.setStartTime(breakId, lastStartTime);
                            tOeExamBreakHistoryService.sendExamRecordBreakHistoryDataMq(breakId);
                        }
                        toeExamRecordService.sendExamRecordDataSaveMq(recordId, timestamp);
                        //更新场次-考试记录缓存
                        //                        ExamActivityRecordCacheUtil.setExamRecordStatus(activityId, recordId);
                    } else {
                        throw new BusinessException(ExceptionResultEnum.EXAM_STATUS_ALREADY_UPDATE);
                    }
                    break;
                } finally {
                    if (Objects.nonNull(recordId)) {
                        redisUtil.releaseLock(SystemConstant.REDIS_LOCK_EXAM_STATUS_PREFIX + recordId);
                    }
                }
            } else {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        if (!lock) {
            tgErrorService.saveExamTgError(recordId, "start");
            throw new BusinessException(ExceptionResultEnum.EXAM_STATUS_UPDATE_ERROR);
        }

        if (!ExamRecordStatusEnum.RESUME_PREPARE.equals(sta)) {//非断点进入的
            // 写入次数
            es.setAlreadyExamCount(es.getAlreadyExamCount() + 1);
            // 更新考生缓存
            redisUtil.set(RedisKeyHelper.examStudentCacheKey(examStudentId), es);
            updateExamStudent(examStudentId, es.getAlreadyExamCount(), recordId);
        }

        //非强制交卷，换算最终交卷时间并生成一次性延时任务
        if (Objects.nonNull(exam.getForceFinish()) && exam.getForceFinish().intValue() == 0) {
            Long date = ExamRecordCacheUtil.getExamFinalFinishTime(recordId);
            if (Objects.isNull(date)) {
                Long startTime = ExamRecordCacheUtil.getStartTime(recordId);
                Integer maxDurationSeconds = ExamRecordCacheUtil.getMaxDurationSeconds(recordId);
                Calendar calendar = Calendar.getInstance();
                if (Objects.nonNull(maxDurationSeconds)) {
                    calendar.setTimeInMillis(startTime);
                    calendar.add(Calendar.SECOND, maxDurationSeconds.intValue());
                }
                Integer breakResumeCount = exam.getBreakResumeCount();
                Integer breakExpireSeconds = Objects.isNull(exam.getBreakExpireSeconds()) ?
                        0 :
                        exam.getBreakExpireSeconds();
                Long finalFinishTime =
                        (calendar.getTime().getTime() / 1000 + (breakResumeCount * breakExpireSeconds) + (
                                breakResumeCount * 180)) * 1000;
                if (Objects.nonNull(finalFinishTime) && finalFinishTime.longValue() > System.currentTimeMillis()) {
                    ExamRecordCacheUtil.setExamFinalFinishTime(recordId, finalFinishTime);
                    //新增quartz任务,发送mq消息start
                    Map<String, Object> prop = new HashMap<>();
                    prop.put(SystemConstant.OPER, SystemConstant.INSERT);
                    prop.put(SystemConstant.EXAM, exam);
                    MqDto mqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.EXAM_STUDENT.name(),
                            JacksonUtil.parseJson(examStudentCacheBean), MqTagEnum.EXAM_STUDENT, String.valueOf(recordId),
                            prop, es.getName());
                    mqDtoService.assembleSendAsyncMsg(mqDto);
                    //新增quartz任务,发送mq消息end
                }
            }
        }
        return ret;
    }

    /**
     * 上传个人试卷结构
     */
    @Override
    public StudentPaperStructBean studentPaperStruct(Long studentId, Long recordId, String content) {
        // 校验当前登录用户和参数一致性
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_RECORD);
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_STUDENT);
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException(ExceptionResultEnum.EXAM_ID_NOT_EQUALY);
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta) || sta == ExamRecordStatusEnum.JUNK) {
            throw new BusinessException(ExceptionResultEnum.EXAM_FINISH);
        }
        ExamStudentPaperStructCacheBean struct = new ExamStudentPaperStructCacheBean();
        struct.setContent(content);
        struct.setTime(System.currentTimeMillis());
        // 更新考生试卷结构
        redisUtil.set(RedisKeyHelper.studentPaperStructKey(recordId), struct);
        StudentPaperStructBean ret = new StudentPaperStructBean();
        ret.setUpdateTime(struct.getTime());
        return ret;
    }

    /**
     * 提交作答结果
     */
    @Override
    public AnswerSubmitBean answerSubmit(Long studentId, Long recordId, Integer mainNumber, Integer subNumber,
                                         Integer subIndex, String answer, Long version, Integer durationSeconds) {
        // 校验当前登录用户和参数一致性
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_RECORD);
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_STUDENT);
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException(ExceptionResultEnum.EXAM_ID_NOT_EQUALY);
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta) || sta == ExamRecordStatusEnum.JUNK) {
            throw new BusinessException(ExceptionResultEnum.EXAM_FINISH);
        } else if (sta != ExamRecordStatusEnum.ANSWERING) {
            throw new BusinessException(ExceptionResultEnum.EXAM_STATUS_ERROR);
        }
        ExamStudentAnswerCacheBean answerCache = (ExamStudentAnswerCacheBean) redisUtil
                .get(RedisKeyHelper.examAnswerKey(recordId),
                        RedisKeyHelper.examAnswerHashKey(mainNumber, subNumber, subIndex));
        if (answerCache == null || Objects.isNull(answerCache.getVersion())) {
            answerCache = new ExamStudentAnswerCacheBean();
            answerCache.setMainNumber(mainNumber);
            answerCache.setSubNumber(subNumber);
            answerCache.setSubIndex(subIndex);
            answerCache.setAnswer(answer);
            answerCache.setVersion(version);
            answerCache.setDurationSeconds(durationSeconds);
            answerCache.setObjective(
                    checkIsObjective(ExamRecordCacheUtil.getPaperId(recordId), mainNumber, subNumber, subIndex));

        } else {
            if (version.longValue() > answerCache.getVersion().longValue()) {
                answerCache.setAnswer(answer);
                answerCache.setVersion(version);
                answerCache.setDurationSeconds(durationSeconds);
            }
        }
        Integer hasAnswerFile = Objects.nonNull(ExamRecordCacheUtil.getHasAnswerFile(recordId)) ?
                ExamRecordCacheUtil.getHasAnswerFile(recordId) :
                0;
        if (hasAnswerFile.intValue() == 1) {
            calculateObjectiveScore(answerCache, recordId);
        }
        // 更新考生作答
        redisUtil.set(RedisKeyHelper.examAnswerKey(recordId),
                RedisKeyHelper.examAnswerHashKey(mainNumber, subNumber, subIndex), answerCache);

        AnswerSubmitBean ret = new AnswerSubmitBean();
        ret.setVersion(version);

        return ret;
    }

    /**
     * 统分
     *
     * @param answer
     * @param recordId
     */
    private void calculateObjectiveScore(ExamStudentAnswerCacheBean answer, Long recordId) {
        if (answer.getObjective() == null || !answer.getObjective()) {
            return;
        }
        answer.setScore(0.0);
        Integer mainNumber = answer.getMainNumber();
        Integer subNumber = answer.getSubNumber();
        Integer subIndex = answer.getSubIndex();
        Long paperId = ExamRecordCacheUtil.getPaperId(recordId);
        String key = RedisKeyHelper.examAnswerHashKey(mainNumber, subNumber, subIndex);

        //整卷客观题标答缓存集合
        Map<String, ObjectiveAnswerCacheBean> map = examPaperService.getObjectiveAnswerCacheBean(paperId);
        if (map == null || map.size() == 0) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_PAPER_OBJECTIVE_ANSWER);
        }
        //客观题标答缓存
        ObjectiveAnswerCacheBean cb = map.get(key);
        if (cb == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_QUESTION_OBJECTIVE_ANSWER);
        }
        if (cb.getStructType().intValue() == 1) {
            if (Objects.nonNull(cb.getChoiceAnswer()) && cb.getChoiceAnswer().size() > 0) {
                if (checkSingleChoice(answer.getAnswer(), cb.getChoiceAnswer())) {
                    answer.setScore(cb.getScore());
                } else {
                    answer.setScore(0.0);
                }
            } else {
                answer.setScore(0.0);
            }
        }
        if (cb.getStructType().intValue() == 2) {
            ExamCacheBean ec = getExamCacheBeanNative(ExamRecordCacheUtil.getExamId(recordId));
            if (Objects.nonNull(cb.getChoiceAnswer()) && cb.getChoiceAnswer().size() > 0) {
                int r = checkMultipleChoice(answer.getAnswer(), 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 = BigDecimal.valueOf(cb.getScore()).divide(new BigDecimal("2"), 1, BigDecimal.ROUND_UP);
                        answer.setScore(b.doubleValue());
                    } else {
                        answer.setScore(0.0);
                    }
                }
            } else {
                answer.setScore(0.0);
            }
        }
        if (cb.getStructType().intValue() == 3) {
            if (StringUtils.isNotBlank(answer.getAnswer())) {
                String answerStr = answer.getAnswer().replaceAll("\n", "");
                if (Objects.nonNull(answerStr) && Objects.nonNull(cb.getBoolAnswer()) && Boolean.parseBoolean(answerStr) == cb.getBoolAnswer().booleanValue()) {
                    answer.setScore(cb.getScore());
                } else {
                    answer.setScore(0.0);
                }
            }
        }
    }

    private boolean checkSingleChoice(String answerArray, JSONArray ar) {
        JSONArray an = JSONArray.parseArray(answerArray);
        if (an.size() == 0) {
            return false;
        }
        int a = an.getIntValue(0);
        return checkSingleChoice(a, ar);
    }

    private boolean checkSingleChoice(int answer, JSONArray ar) {
        for (int i = 0; i < ar.size(); i++) {
            if (answer == 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;
        JSONArray an = JSONArray.parseArray(answer);
        for (int i = 0; i < an.size(); i++) {
            if (checkSingleChoice(an.getIntValue(i), ar)) {
                yes++;
                continue;
            } else {
                no++;
            }
        }
        if (yes == ar.size() && yes == an.size()) {
            return 1;
        }
        if (yes != 0 && no == 0) {
            return 0;
        }
        return -1;
    }

    private boolean checkIsObjective(Long paperId, Integer mainNumber, Integer subNumber, Integer subIndex) {
        Map<String, ObjectiveAnswerCacheBean> map = examPaperService.getObjectiveAnswerCacheBean(paperId);
        if (map == null || map.size() == 0) {
            return false;
        }
        String key = RedisKeyHelper.examAnswerHashKey(mainNumber, subNumber, subIndex);
        ObjectiveAnswerCacheBean cb = map.get(key);
        if (cb == null) {
            return false;
        }
        return true;
    }

    /**
     * 更新音频剩余播放次数
     */
    @Override
    public AudioLeftPlayCountSubmitBean audioLeftPlayCountSubmit(Long studentId, Long recordId, String key,
                                                                 Integer count) {

        // 校验当前登录用户和参数一致性
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_RECORD);
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_STUDENT);
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException(ExceptionResultEnum.EXAM_ID_NOT_EQUALY);
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta) || sta == ExamRecordStatusEnum.JUNK) {
            throw new BusinessException(ExceptionResultEnum.EXAM_FINISH);
        }
        // 音频剩余播放次数缓存
        redisUtil.set(RedisKeyHelper.audioLeftPlayCountKey(recordId), key, count);
        AudioLeftPlayCountSubmitBean ret = new AudioLeftPlayCountSubmitBean();
        ret.setCount(count);
        return ret;
    }

    /**
     * 文件上传
     */
    @Override
    public ExamFileUploadBean fileUpload(Long studentId, Long recordId, MultipartFile file, String suffix, String md5) {
        // 校验当前登录用户和参数一致性
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_RECORD);
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_STUDENT);
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException(ExceptionResultEnum.EXAM_ID_NOT_EQUALY);
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta) || sta == ExamRecordStatusEnum.JUNK) {
            throw new BusinessException(ExceptionResultEnum.EXAM_FINISH);
        }

        String filePath =
                "upload" + File.separator + DateUtil.format(new Date(), Constants.DATE_PATTERN) + File.separator + SystemConstant.getNanoId() + "."
                        + suffix;
        InputStream in = null;
        try {
            in = file.getInputStream();
            ossUtil.upload(true, filePath, in, md5);
            String url = ossUtil.getAliYunOssPublicDomain().getPublicUrl() + File.separator + filePath;
            ExamFileUploadBean ret = new ExamFileUploadBean();
            ret.setUrl(url);
            ret.setUploadTime(System.currentTimeMillis());
            return ret;
        } catch (IOException e) {
            throw new BusinessException("文件读取出错");
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                }
            }
        }
    }

    /**
     * 断点恢复
     */
    @SuppressWarnings("unchecked")
    @Override
    public ExamResumeBean resume(Long studentId, Long recordId) {
        TBSession tbSession = (TBSession) ServletUtil.getRequestSession();
        // 校验当前登录用户和参数一致性
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_RECORD);
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_STUDENT);
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException(ExceptionResultEnum.EXAM_ID_NOT_EQUALY);
        }
        //停用
        if (es.getEnable().intValue() == 0) {
            throw new BusinessException(ExceptionResultEnum.EXAM_STUDENT_ENABLE);
        }
        Long examId = ExamRecordCacheUtil.getExamId(recordId);
        ExamCacheBean ec = getExamCacheBeanNative(examId);//考试缓存

        checkIp(ec);

        Long paperId = ExamRecordCacheUtil.getPaperId(recordId);
        ExamPaperCacheBean ep = teExamPaperService.getExamPaperCacheBean(paperId);
        if (ep == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_PAPER);
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta) || sta == ExamRecordStatusEnum.JUNK) {
            throw new BusinessException(ExceptionResultEnum.EXAM_FINISH);
        }

        ExamResumeBean ret = null;
        Boolean finished = toeExamRecordService.examBreakLogic(recordId, false);
        if (finished) {
            throw new BusinessException(ExceptionResultEnum.EXAM_FINISH);
        }
        ret = new ExamResumeBean();
        ret.setDurationSeconds(ExamRecordCacheUtil.getDurationSeconds(recordId));
        //        ret.setPaperUrl(ossUtil.getPrivateUrl(ep.getPaperPath()));
        //        ret.setStructUrl(ossUtil.getPrivateUrl(ep.getStructPath()));
        ret.setHasAudio((ep.getHasAudio() != null && ep.getHasAudio().intValue() == 1 ? true : false));
        ret.setAudioPlayCount(ep.getAudioPlayCount());
        ret.setMonitorAppId(tencentYunUtil.getAppId());
        ret.setMonitorKey(ExamRecordCacheUtil.getMonitorKey(recordId));
        ret.setMonitorUserId("s_" + tbSession.getId());
        ret.setMonitorUserSig(tencentYunUtil.getSign(ret.getMonitorUserId(), SystemConstant.TENCENT_EXPIRE_TIME));
        ret.setExamStudentCallEnable(ec.getExamStudentCallEnable());

        ExamActivityCacheBean ac = teExamActivityService.getExamActivityCacheBean(es.getExamActivityId());
        ExamCourseCacheBean examCourseCacheBean = teExamCourseService
                .getExamCourseCacheBean(es.getExamId(), es.getCourseCode());

        SysConfig sysConfig = themisCacheService.addSysConfigCache(SystemConstant.MONITOR_CONFIG_PREFIX);
        Optional.ofNullable(sysConfig).orElseThrow(() -> new BusinessException("未配置监控前缀"));
        TEExamActivityDto teExamActivityDto = new TEExamActivityDto(ec, ac, es, examStudentId, examCourseCacheBean,
                recordId, sysConfig.getConfigValue(), true);
        ret.setTeExamActivityDto(teExamActivityDto);

        ExamStudentPaperStructCacheBean struct = (ExamStudentPaperStructCacheBean) redisUtil
                .get(RedisKeyHelper.studentPaperStructKey(recordId));
        if (struct != null) {
            ret.setStudentPaperStruct(struct.getContent());
        }
        Map<String, ExamStudentAnswerCacheBean> answers = redisUtil
                .getHashEntries(RedisKeyHelper.examAnswerKey(recordId));
        if (answers != null && answers.size() > 0) {
            ret.setAnswerList(sortAnswers(answers));
        }
        Map<String, Integer> audioLeftPlayCounts = redisUtil
                .getHashEntries(RedisKeyHelper.audioLeftPlayCountKey(recordId));
        if (audioLeftPlayCounts != null && audioLeftPlayCounts.size() > 0) {
            ret.setAudioLeftPlayCount(audioLeftPlayCounts);
        }

        boolean lock = false;
        for (int i = 0; i < SystemConstant.MAX_EXAM_STATUS_COUNT; i++) {
            lock = redisUtil.lock(SystemConstant.REDIS_LOCK_EXAM_STATUS_PREFIX + recordId,
                    SystemConstant.REDIS_LOCK_EXAM_STATUS_TIME_OUT);
            if (lock) {
                try {
                    Long lastPrepareTime = System.currentTimeMillis();
                    ExamRecordCacheUtil.setLastPrepareTime(recordId, lastPrepareTime);
                    ExamRecordCacheUtil.setStatus(recordId, ExamRecordStatusEnum.RESUME_PREPARE, lastPrepareTime);
                    ExamRecordStatusEnum examRecordStatusEnum = ExamRecordCacheUtil.getStatus(recordId);
                    Long breakId = ExamRecordCacheUtil.getLastBreakId(recordId);
                    if (Objects.nonNull(examRecordStatusEnum)
                            && examRecordStatusEnum == ExamRecordStatusEnum.RESUME_PREPARE) {
                        if (Objects.nonNull(breakId)) {
                            ExamBreakCacheUtil.setPrepareTime(breakId, lastPrepareTime);
                            tOeExamBreakHistoryService.sendExamRecordBreakHistoryDataMq(breakId);
                        }
                        toeExamRecordService.sendExamRecordDataSaveMq(recordId, lastPrepareTime);
                        //更新场次-考试记录缓存
                        //                        ExamActivityRecordCacheUtil.setExamRecordStatus(es.getExamActivityId(), recordId);
                    } else {
                        throw new BusinessException(ExceptionResultEnum.EXAM_STATUS_ALREADY_UPDATE);
                    }
                    break;
                } finally {
                    if (Objects.nonNull(recordId)) {
                        redisUtil.releaseLock(SystemConstant.REDIS_LOCK_EXAM_STATUS_PREFIX + recordId);
                    }
                }
            } else {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        if (!lock) {
            tgErrorService.saveExamTgError(recordId, "resume");
            throw new BusinessException(ExceptionResultEnum.EXAM_STATUS_UPDATE_ERROR);
        }
        //mq发送消息start
        TEStudentCacheDto teStudentCacheDto = themisCacheService.addStudentAccountCache(es.getStudentId());
        MqDto mqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.STUDENT.name(), SystemOperationEnum.RESUME_PREPARE, MqTagEnum.STUDENT, String.valueOf(teStudentCacheDto.getId()), IpUtil.getRemoteIp(ServletUtil.getRequest()));
        this.sendOeLogMessage(SystemOperationEnum.RESUME_PREPARE, examStudentId, recordId, mqDto);
        //mq发送消息end
        return ret;
    }

    /**
     * 作答排序
     *
     * @param answers
     * @return
     */
    private List<ExamStudentAnswerCacheBean> sortAnswers(Map<String, ExamStudentAnswerCacheBean> answers) {
        List<ExamStudentAnswerCacheBean> ret = new ArrayList<ExamStudentAnswerCacheBean>(answers.values());
        Collections.sort(ret, new Comparator<ExamStudentAnswerCacheBean>() {

            @Override
            public int compare(ExamStudentAnswerCacheBean o1, ExamStudentAnswerCacheBean o2) {
                if (o1.getMainNumber() > o2.getMainNumber()) {
                    return 1;
                } else if (o1.getMainNumber() < o2.getMainNumber()) {
                    return -1;
                } else {
                    if (o1.getSubNumber() > o2.getSubNumber()) {
                        return 1;
                    } else if (o1.getSubNumber() < o2.getSubNumber()) {
                        return -1;
                    } else {
                        if (o1.getSubIndex() == null || o2.getSubIndex() == null) {
                            return 0;
                        } else {
                            if (o1.getSubIndex() > o2.getSubIndex()) {
                                return 1;
                            } else if (o1.getSubIndex() < o2.getSubIndex()) {
                                return -1;
                            } else {
                                return 0;
                            }
                        }
                    }
                }
            }

        });
        return ret;
    }

    /**
     * 结束考试
     */
    @Override
    public ExamFinishBean finish(Long studentId, Long recordId, String type, Integer durationSeconds) {
        String lockKey = SystemConstant.REDIS_LOCK_FINISH_EXAM_PREFIX + recordId;
        Boolean lock = redisUtil.lock(lockKey, SystemConstant.REDIS_CACHE_TIME_OUT);
        if (!lock) {
            Date now = new Date();
            ExamFinishBean ret = new ExamFinishBean();
            ret.setFinishTime(now.getTime());
            ret.setStatus(FinishExamResultEnum.NORMAL);
            return ret;
        }
        try {
            return diposeFinish(studentId, recordId, type, durationSeconds);
        } finally {
            redisUtil.releaseLock(lockKey);
        }
    }

    @SuppressWarnings("unchecked")
    private Double calculateTotalObjectiveScore(Long recordId) {
        Double total = 0.0;
        Map<String, ExamStudentAnswerCacheBean> as = redisUtil.getHashEntries(RedisKeyHelper.examAnswerKey(recordId));
        if (as != null && as.size() > 0) {
            for (ExamStudentAnswerCacheBean sa : as.values()) {
                if (sa.getObjective() != null && sa.getObjective()) {//是客观题
                    if (sa.getScore() != null) {//有分值
                        total = total + sa.getScore();
                    }
                }
            }
        }
        ExamRecordCacheUtil.setObjectiveScore(recordId, total);
        return total;
    }

    private ExamFinishBean diposeFinish(Long studentId, Long recordId, String type, Integer durationSeconds) {
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        String monitorRecord = ExamRecordCacheUtil.getMonitorRecord(recordId);
        // 校验当前登录用户和参数一致性
        if (examStudentId == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_RECORD);
        }
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_STUDENT);
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException(ExceptionResultEnum.EXAM_ID_NOT_EQUALY);
        }

        Date now = new Date();
        ExamFinishBean ret = new ExamFinishBean();
        ret.setFinishTime(now.getTime());
        ret.setObjectiveScore(calculateTotalObjectiveScore(recordId));

        if (ExamRecordCacheUtil.getId(recordId) == null || examStudentId == null) {
            ret.setStatus(FinishExamResultEnum.NORMAL);
            return ret;
        }

        ExamCacheBean exam = getExamCacheBeanNative(es.getExamId());
        ret.setPostNotice(exam.getPostNotice());
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta) || sta == ExamRecordStatusEnum.JUNK) {
            ret.setStatus(FinishExamResultEnum.NORMAL);
            return ret;
        }

        //页面结果
        Integer hasAnswerFile = Objects.nonNull(ExamRecordCacheUtil.getHasAnswerFile(recordId)) ?
                ExamRecordCacheUtil.getHasAnswerFile(recordId) :
                0;
        if (hasAnswerFile.intValue() == 1) {
            if (exam.getShowObjectiveScore() != null && exam.getShowObjectiveScore().intValue() == 1) {//实时出分
                if (InvigilateVerifyEnum.NOW.equals(exam.getInvigilateVerify())) {//实时审核
                    Integer warningCount = ExamRecordCacheUtil.getWarningCount(recordId);
                    Integer breachStatus = Objects.isNull(ExamRecordCacheUtil.getBreachStatus(recordId)) ?
                            1 :
                            ExamRecordCacheUtil.getBreachStatus(recordId);
                    if (warningCount != null && warningCount.intValue() > 0) {//有预警
                        if (breachStatus.intValue() == 0) {//违纪结果是false
                            ret.setReviewResult(ReviewResultEnum.PASS);
                            if (ret.getObjectiveScore() == null) {
                                ret.setStatus(FinishExamResultEnum.SCORE_CALCULATE);
                            } else {
                                ret.setStatus(FinishExamResultEnum.SCORE_VIEW);
                            }
                        } else if (breachStatus.intValue() == 1) {//违纪结果是true
                            ret.setReviewResult(ReviewResultEnum.UN_PASS);
                            ret.setStatus(FinishExamResultEnum.BREACH);
                        }
                    } else {
                        if (Objects.nonNull(exam.getInvigilateVerify()) && exam.getInvigilateVerify()
                                .equals(InvigilateVerifyEnum.NOW)) {
                            ret.setStatus(FinishExamResultEnum.AUDITING);
                        } else if (Objects.nonNull(exam.getInvigilateVerify()) && exam.getInvigilateVerify()
                                .equals(InvigilateVerifyEnum.LATER)) {
                            ret.setStatus(FinishExamResultEnum.SCORE_VIEW);
                        }
                    }
                } else {
                    if (ret.getObjectiveScore() == null) {
                        ret.setStatus(FinishExamResultEnum.SCORE_CALCULATE);
                    } else {
                        ret.setStatus(FinishExamResultEnum.SCORE_VIEW);
                    }
                }
            } else {//非实时出分
                ret.setStatus(FinishExamResultEnum.NORMAL);
            }
        } else {
            ret.setStatus(FinishExamResultEnum.NORMAL);
        }
        boolean lock = false;
        for (int i = 0; i < SystemConstant.MAX_EXAM_STATUS_COUNT; i++) {
            lock = redisUtil.lock(SystemConstant.REDIS_LOCK_EXAM_STATUS_PREFIX + recordId,
                    SystemConstant.REDIS_LOCK_EXAM_STATUS_TIME_OUT);
            if (lock) {
                try {
                    Long finishTime = System.currentTimeMillis();
                    ExamRecordCacheUtil.setFinishTime(recordId, finishTime);
                    ExamRecordCacheUtil.setDurationSeconds(recordId, durationSeconds);
                    ExamRecordCacheUtil.setFinishType(recordId, FinishTypeEnum.valueOf(type));
                    ExamRecordCacheUtil.setStatus(recordId, ExamRecordStatusEnum.FINISHED, finishTime);
                    ExamRecordStatusEnum examRecordStatusEnum = ExamRecordCacheUtil.getStatus(recordId);
                    if (Objects.nonNull(examRecordStatusEnum)
                            && examRecordStatusEnum == ExamRecordStatusEnum.FINISHED) {
                        toeExamRecordService.sendExamRecordDataSaveMq(recordId, finishTime);
                        //更新场次-考试记录缓存
                        //                        ExamActivityRecordCacheUtil.setExamRecordStatus(es.getExamActivityId(), recordId);
                    } else {
                        throw new BusinessException(ExceptionResultEnum.EXAM_STATUS_ALREADY_UPDATE);
                    }
                    break;
                } finally {
                    if (Objects.nonNull(recordId)) {
                        redisUtil.releaseLock(SystemConstant.REDIS_LOCK_EXAM_STATUS_PREFIX + recordId);
                    }
                }
            } else {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        if (!lock) {
            tgErrorService.saveExamTgError(recordId, "diposeFinish");
            throw new BusinessException(ExceptionResultEnum.EXAM_STATUS_UPDATE_ERROR);
        }
        //更新未完成考试记录id
        TEStudentCacheDto teStudentCacheDto = themisCacheService.addStudentAccountCache(es.getStudentId());
        Map<String, Object> properties = new HashMap<>();
//        properties.put(SystemConstant.REMOVE_WEBSOCKET, true);//客户端加入补救措施后后台可以不强制退出websocket

        MqDto clientMqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.OE_WEBSOCKET_EXAM_STOP.name(),
                recordId, MqTagEnum.OE_WEBSOCKET_EXAM_STOP, String.valueOf(recordId), properties,
                String.valueOf(recordId));
        mqDtoService.assembleSendAsyncOrderMsg(clientMqDto);

        MqDto mobileMqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.EXAM_STOP.name(), recordId,
                MqTagEnum.EXAM_STOP, String.valueOf(recordId), properties, String.valueOf(recordId));
        mqDtoService.assembleSendAsyncOrderMsg(mobileMqDto);

        //异步持久化
        checkToPersisted(recordId, studentId);
        //mq发送消息start
        MqDto mqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.STUDENT.name(),
                SystemOperationEnum.FINISHED, MqTagEnum.STUDENT, String.valueOf(teStudentCacheDto.getId()),
                IpUtil.getRemoteIp(ServletUtil.getRequest()));
        this.sendOeLogMessage(SystemOperationEnum.FINISHED, examStudentId, recordId, mqDto);
        //mq发送消息end

        if (Objects.nonNull(monitorRecord) && !Objects.equals(monitorRecord.trim().replaceAll(" ", ""), "")) {
            //发送腾讯云回调延时mq消息start
            Map<String, Object> tranMap = mqDtoService
                    .buildMqDelayMsg(tencentYunUtil.getCallbackTime());
            tranMap.put(SystemConstant.RECORD_ID, recordId);
            MqDto mqDtoTencentVideo = new MqDto(mqUtil.getTopic(), MqTagEnum.TENCENT_VIDEO.name(),
                    MqTagEnum.TENCENT_VIDEO.name(), MqTagEnum.TENCENT_VIDEO, String.valueOf(recordId), tranMap,
                    String.valueOf(recordId));
            mqDtoService.assembleSendAsyncDelayMsg(mqDtoTencentVideo);
            //发送腾讯云回调延时mq消息end
        }

        //解散房间
        tencentYunUtil.dismissRoomByStrRoomId(tencentYunUtil.getSecretId(),
                tencentYunUtil.getSecretKey(),
                tencentYunUtil.getTrtcQueryUrl(),
                tencentYunUtil.getTrtcRegion(),
                Long.parseLong(tencentYunUtil.getAppId()),
                String.valueOf(recordId));
        return ret;
    }

    /**
     * 检查条件并发送持久化消息
     *
     * @param recordId
     */
    private void checkToPersisted(Long recordId, Long studentId) {
        ExamRecordStatusEnum status = ExamRecordCacheUtil.getStatus(recordId);
        if (Objects.isNull(status)) {
            status = SystemConstant.getExamStatus(recordId);
        }
        if (Objects.isNull(status) || !ExamRecordStatusEnum.FINISHED.equals(status)) {
            return;
        }
        Map<String, Object> propMap = mqDtoService.buildMqDelayMsg("10s");
        propMap.put(SystemConstant.RECORD_ID, recordId);
        propMap.put(SystemConstant.STUDENT_ID, studentId);
        MqDto mqDto = new MqDto(mqUtil.getTopic(), MqTagEnum.EXAM_RECORD_PERSISTED.name(), propMap,
                MqTagEnum.EXAM_RECORD_PERSISTED, recordId.toString(), propMap, recordId.toString());
        mqDtoService.assembleSendAsyncDelayMsg(mqDto);
    }

    @Cacheable(value = "exam", key = "#examId", unless = "#result == null")
    @Override
    public ExamCacheBean getExamCacheBean(Long examId) {
        return cacheOperation(examId);
    }

    private ExamCacheBean getExamCacheBeanNative(Long examId) {
        TEExamService examService = SpringContextHolder.getBean(TEExamService.class);
        return examService.getExamCacheBean(examId);
    }

    /**
     * 更新批次缓存
     *
     * @param examId
     * @return
     */
    @Override
    @CachePut(value = "exam", key = "#examId", condition = "#result != null")
    public ExamCacheBean updateExamCacheBean(Long examId) {
        return cacheOperation(examId);
    }

    /**
     * 删除批次缓存
     *
     * @param examId
     */
    @Override
    @CacheEvict(value = "exam", key = "#examId")
    public void deleteExamCacheBean(Long examId) {

    }

    /**
     * 查询交卷结果
     */
    @Override
    public ExamResultBean result(Long recordId) {
        ExamResultBean ret = new ExamResultBean();
        Integer warningCount = null;
        Integer breachStatus = null;
        Integer hasAnswerFile = null;
        Long examId = null;
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            TOeExamRecord er = toeExamRecordService.getById(recordId);
            if (er == null) {
                throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_RECORD);
            }
            ret.setDurationSeconds(er.getDurationSeconds());
            ret.setFinishTime(er.getFinishTime());
            ret.setFinishType(er.getFinishType());
            ret.setObjectiveScore(er.getObjectiveScore());
            warningCount = er.getWarningCount();
            breachStatus = er.getBreachStatus();
            hasAnswerFile = er.getHasAnswerFile();
            examId = er.getExamId();
        } else {
            ret.setDurationSeconds(ExamRecordCacheUtil.getDurationSeconds(recordId));
            ret.setFinishTime(ExamRecordCacheUtil.getFinishTime(recordId));
            ret.setFinishType(ExamRecordCacheUtil.getFinishType(recordId));
            ret.setObjectiveScore(ExamRecordCacheUtil.getObjectiveScore(recordId));
            warningCount = ExamRecordCacheUtil.getWarningCount(recordId);
            breachStatus = ExamRecordCacheUtil.getBreachStatus(recordId);
            hasAnswerFile = Objects.nonNull(ExamRecordCacheUtil.getHasAnswerFile(recordId)) ?
                    ExamRecordCacheUtil.getHasAnswerFile(recordId) :
                    0;
            examId = ExamRecordCacheUtil.getExamId(recordId);
        }

        ExamCacheBean exam = getExamCacheBeanNative(examId);
        ret.setPostNotice(exam.getPostNotice());
        //页面结果
        if (hasAnswerFile.intValue() == 1) {
            if (exam.getShowObjectiveScore() != null && exam.getShowObjectiveScore().intValue() == 1) {//实时出分
                if (InvigilateVerifyEnum.NOW.equals(exam.getInvigilateVerify())) {//实时审核
                    if (warningCount != null && warningCount.intValue() > 0) {//有预警
                        if (breachStatus == null) {//无违纪结果
                            ret.setStatus(FinishExamResultEnum.AUDITING);
                        } else if (breachStatus.intValue() == 0) {//违纪结果是false
                            ret.setReviewResult(ReviewResultEnum.PASS);
                            if (ret.getObjectiveScore() == null) {
                                ret.setStatus(FinishExamResultEnum.SCORE_CALCULATE);
                            } else {
                                ret.setStatus(FinishExamResultEnum.SCORE_VIEW);
                            }
                        } else if (breachStatus.intValue() == 1) {//违纪结果是true
                            ret.setReviewResult(ReviewResultEnum.UN_PASS);
                            ret.setStatus(FinishExamResultEnum.BREACH);
                        }
                    }
                } else {
                    if (ret.getObjectiveScore() == null) {
                        ret.setStatus(FinishExamResultEnum.SCORE_CALCULATE);
                    } else {
                        ret.setStatus(FinishExamResultEnum.SCORE_VIEW);
                    }
                }
            } else {//非实时出分
                ret.setStatus(FinishExamResultEnum.NORMAL);
            }
        } else {
            ret.setStatus(FinishExamResultEnum.NORMAL);
        }
        return ret;
    }

    /**
     * 试卷下载
     *
     * @param recordId
     * @return
     */
    @Override
    public ExamPaperDownloadBean paperDownload(Long recordId) {
        Long paperId = ExamRecordCacheUtil.getPaperId(recordId);
        ExamPaperCacheBean ep = teExamPaperService.getExamPaperCacheBean(paperId);
        if (Objects.isNull(ep)) {
            throw new BusinessException(ExceptionResultEnum.NOT_FOUND_EXAM_PAPER);
        }
        ExamPaperDownloadBean ret = new ExamPaperDownloadBean();
        ret.setPaperUrl(ossUtil.getPrivateUrl(ep.getPaperPath()));
        ret.setPaperMd5(ep.getPaperMd5());
        ret.setStructUrl(ossUtil.getPrivateUrl(ep.getStructPath()));
        ret.setStructMd5(ep.getStructMd5());
        return ret;
    }

    /**
     * 监考端获取考试批次列表
     *
     * @param userId
     * @param orgId
     * @return
     */
    @SuppressWarnings("rawtypes")
    @Override
    public List<Map> examList(Long userId, Long orgId) {
        return teExamMapper.examList(userId, orgId);
    }

    /**
     * 发送考试轨迹消息
     *
     * @param systemOperationEnum
     * @param examStudentId
     * @param recordId
     * @param mqDto
     */
    @Override
    public void sendOeLogMessage(SystemOperationEnum systemOperationEnum, Long examStudentId, Long recordId,
                                 MqDto mqDto) {
        //mq发送消息start
        Map<String, Object> properties = new HashMap<>();
        properties.put(SystemConstant.REMARK, systemOperationEnum.getCode());
        properties.put(SystemConstant.EXAM_STUDENT_ID, examStudentId);
        properties.put(SystemConstant.EXAM_RECORD_ID, recordId);
        mqDto.setProperties(properties);
        mqDtoService.assembleSendAsyncMsg(mqDto);
        //mq发送消息end
    }

    /**
     * 考试批次权限过滤
     *
     * @param userId
     * @param orgId
     * @param type
     * @return
     */
    @Override
    public List<TEExam> examPrivilegeQuery(Long userId, Long orgId, String type) {
        return teExamMapper.examPrivilegeQuery(userId, orgId, type);
    }

    /**
     * 缓存操作
     *
     * @param examId
     * @return
     */
    public ExamCacheBean cacheOperation(Long examId) {
        ExamCacheBean ret = null;
        TEExam exam = getById(examId);
        if (exam == null) {
            return ret;
        }
        ret = new ExamCacheBean();
        BeanUtils.copyProperties(exam, ret);
        String[] strs = null;
        if (Objects.nonNull(ret.getMonitorVideoSource())) {
            strs = ret.getMonitorVideoSource().split(",");
            if (strs.length == 4 || (
                    ret.getMonitorVideoSource().toUpperCase().contains(MonitorVideoSourceEnum.CLIENT_SCREEN.name())
                            || ret.getMonitorVideoSource().toUpperCase()
                            .contains(MonitorVideoSourceEnum.CLIENT_CAMERA.name()))) {
                ret.setMonitorAudioEnable(true);
            }
        }
        return ret;
    }

    /**
     * 重新算分
     *
     * @param examId
     * @param taskId
     */
    @Override
    public void calculateScore(Long examId, Long taskId) {
        int timeOutSecond = 60 * 60 * 5;
        String lockKey = SystemConstant.REDIS_LOCK_CALCULATE_SCORE_PREFIX + examId;
        String multLockKey = SystemConstant.REDIS_LOCK_CALCULATE_SCORE_PAPER_IMPORT_PREFIX + examId;

        TBTaskHistory task = null;
        Long recordId = null;
        try {
            Boolean lock = redisUtil.lock(lockKey, timeOutSecond);
            if (!lock) {
                throw new BusinessException("该考试批次有正在进行的重新算分");
            }

            //重新算分和试卷导入互斥锁
            Boolean multLock = redisUtil.lock(multLockKey, timeOutSecond);
            if (!multLock) {
                throw new BusinessException("该考试批次有正在进行的试卷导入");
            }

            TEExamService examService = SpringContextHolder.getBean(TEExamService.class);
            teExamMapper.updateScoreStatus(ScoreStatusEnum.CALCULATING, examId);
            teExamMapper.updateScoreProgress(0.0, examId);
            examService.updateExamCacheBean(examId);
            task = tbTaskHistoryService.getById(taskId);
            task.setStatus(TaskStatusEnum.RUNNING);
            tbTaskHistoryService.saveOrUpdate(task);
            Long startId = 0L;
            Long index = 0L;
            Long total = toeExamRecordService.getCountByExamId(examId);
            if (total > 0) {
                for (; ; ) {
                    List<TOeExamRecord> list = toeExamRecordService.getListByExamIdAndStartId(examId, startId);
                    if (list == null || list.size() == 0) {
                        break;
                    }
                    startId = list.get(list.size() - 1).getId();
                    for (TOeExamRecord rc : list) {
                        index++;
                        if (ExamRecordStatusEnum.PERSISTED.equals(rc.getStatus())) {
                            recordId = rc.getId();
                            toeExamRecordService.calculateScore(rc.getId());
                        }
                    }
                    Double progress = getPercentage(index, total);
                    task.setProgress(progress);
                    teExamMapper.updateScoreProgress(progress, examId);
                    tbTaskHistoryService.saveOrUpdate(task);
                }
            }
            teExamMapper.updateScoreStatus(ScoreStatusEnum.FINISH, examId);
            teExamMapper.updateScoreProgress(100.0, examId);
            examService.updateExamCacheBean(examId);
            task.setSummary("处理成功");
            task.setProgress(100.0);
            task.setStatus(TaskStatusEnum.FINISH);
            task.setFinishTime(System.currentTimeMillis());
        } catch (Exception e) {
            log.error("重新算分出错 recordId:" + (recordId == null ? "" : recordId), e);
            task.setSummary("处理出错");
            task.setStatus(TaskStatusEnum.FINISH);
            task.setFinishTime(System.currentTimeMillis());
            JSONObject json = new JSONObject();
            String reportFilePath = DateUtil.format(new Date(), Constants.DATE_PATTERN) + "/" + SystemConstant.getNanoId() + SystemConstant.TXT_PREFIX;
            json.put(SystemConstant.PATH, reportFilePath);
            json.put(SystemConstant.TYPE, SystemConstant.OSS);
            ossUtil.upload(false, reportFilePath, e.getMessage());
            task.setReportFilePath(json.toJSONString());
        } finally {
            redisUtil.releaseLock(lockKey);
            redisUtil.releaseLock(multLockKey);
        }
        tbTaskHistoryService.saveOrUpdate(task);
    }

    private Double getPercentage(Long a, Long b) {
        if (a == null) {
            a = 0L;
        }
        if (b == null) {
            return null;
        }
        if (b == 0) {
            return null;
        }
        Double da = Double.valueOf(a * 100);
        Double db = Double.valueOf(b);
        BigDecimal bd = BigDecimal.valueOf(da / db);
        Double tem = bd.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
        return tem;
    }

    @Transactional
    @Override
    public void updateInvigilateMonitorStatus(InvigilateMonitorStatusEnum monitorStatus, Long examId) {
        teExamMapper.updateInvigilateMonitorStatus(monitorStatus, examId);
    }

    @Transactional
    @Override
    public void updateScoreStatus(ScoreStatusEnum scoreStatus, Long examId) {
        teExamMapper.updateScoreStatus(scoreStatus, examId);
    }

    @Override
    public IPage<OpenExamBean> examQueryForOpen(Page<OpenExamBean> ipage, Long examId, String examCode) {
        return teExamMapper.examQueryForOpen(ipage, examId, examCode);
    }

    /**
     * 缓存转换
     *
     * @param examCacheBean
     * @return
     */
    @Override
    public TEExam cacheConvert(ExamCacheBean examCacheBean) {
        return GsonUtil.fromJson(GsonUtil.toJson(examCacheBean), TEExam.class);
    }

    /**
     * 视频存储查询接口
     *
     * @param iPage
     * @param examName
     * @param orgName
     * @param type
     * @return
     */
    @Override
    public IPage<VideoQueryResult> videoQuery(IPage<Map> iPage, String examName, String orgName, VideoTypeEnum type) {
        return teExamMapper.videoQuery(iPage, examName, orgName, Objects.nonNull(type) ? type.name() : null);
    }
}