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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.annotation.Resource;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.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.web.multipart.MultipartFile;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qmth.themis.business.bean.exam.AnswerSubmitBean;
import com.qmth.themis.business.bean.exam.AudioLeftPlayCountSubmitBean;
import com.qmth.themis.business.bean.exam.ExamFileUploadBean;
import com.qmth.themis.business.bean.exam.ExamFinishBean;
import com.qmth.themis.business.bean.exam.ExamPrepareBean;
import com.qmth.themis.business.bean.exam.ExamResultBean;
import com.qmth.themis.business.bean.exam.ExamResumeBean;
import com.qmth.themis.business.bean.exam.ExamStartBean;
import com.qmth.themis.business.bean.exam.StudentPaperStructBean;
import com.qmth.themis.business.cache.ExamActivityRecordCacheUtil;
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.ExamActivityCacheBean;
import com.qmth.themis.business.cache.bean.ExamActivityRecordCacheBean;
import com.qmth.themis.business.cache.bean.ExamCacheBean;
import com.qmth.themis.business.cache.bean.ExamCourseCacheBean;
import com.qmth.themis.business.cache.bean.ExamPaperCacheBean;
import com.qmth.themis.business.cache.bean.ExamStudentAnswerCacheBean;
import com.qmth.themis.business.cache.bean.ExamStudentCacheBean;
import com.qmth.themis.business.cache.bean.ExamStudentPaperStructCacheBean;
import com.qmth.themis.business.cache.bean.ObjectiveAnswerCacheBean;
import com.qmth.themis.business.config.SystemConfig;
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.TEExamDto;
import com.qmth.themis.business.dto.response.TEExamQueryDto;
import com.qmth.themis.business.entity.TBSession;
import com.qmth.themis.business.entity.TBTaskHistory;
import com.qmth.themis.business.entity.TEExam;
import com.qmth.themis.business.entity.TOeExamRecord;
import com.qmth.themis.business.enums.EntryAuthenticationPolicyEnum;
import com.qmth.themis.business.enums.ExamModeEnum;
import com.qmth.themis.business.enums.ExamRecordFieldEnum;
import com.qmth.themis.business.enums.ExamRecordStatusEnum;
import com.qmth.themis.business.enums.FinishExamResultEnum;
import com.qmth.themis.business.enums.FinishTypeEnum;
import com.qmth.themis.business.enums.HardwareTestEnum;
import com.qmth.themis.business.enums.InvigilateMonitorStatusEnum;
import com.qmth.themis.business.enums.InvigilateVerifyEnum;
import com.qmth.themis.business.enums.MonitorStatusSourceEnum;
import com.qmth.themis.business.enums.MonitorVideoSourceEnum;
import com.qmth.themis.business.enums.MqTagEnum;
import com.qmth.themis.business.enums.MqTopicEnum;
import com.qmth.themis.business.enums.ReviewResultEnum;
import com.qmth.themis.business.enums.SystemOperationEnum;
import com.qmth.themis.business.enums.TaskStatusEnum;
import com.qmth.themis.business.enums.WebsocketStatusEnum;
import com.qmth.themis.business.service.MqDtoService;
import com.qmth.themis.business.service.TBTaskHistoryService;
import com.qmth.themis.business.service.TEExamActivityService;
import com.qmth.themis.business.service.TEExamCourseService;
import com.qmth.themis.business.service.TEExamPaperService;
import com.qmth.themis.business.service.TEExamService;
import com.qmth.themis.business.service.TEExamStudentService;
import com.qmth.themis.business.service.TOeExamRecordService;
import com.qmth.themis.business.util.JacksonUtil;
import com.qmth.themis.business.util.OssUtil;
import com.qmth.themis.business.util.RedisUtil;
import com.qmth.themis.business.util.ServletUtil;
import com.qmth.themis.business.util.TencentYunUtil;
import com.qmth.themis.common.exception.BusinessException;
import com.qmth.themis.common.util.IpUtil;

/**
 * @Description: 考试批次 服务实现类
 * @Param:
 * @return:
 * @Author: wangliang
 * @Date: 2020/6/25
 */
@Service
public class TEExamServiceImpl extends ServiceImpl<TEExamMapper, TEExam> implements TEExamService {
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
    @Resource
    TEExamMapper teExamMapper;

    @Resource
    TEExamActivityService teExamActivityService;

    @Resource
    TEExamStudentService teExamStudentService;

    @Resource
    TEExamCourseService teExamCourseService;

    @Resource
    TEExamPaperService teExamPaperService;

    @Resource
    TOeExamRecordService toeExamRecordService;

    @Resource
    RedisUtil redisUtil;

    @Resource
    SystemConfig systemConfig;

    @Resource
    MqDtoService mqDtoService;

    @Resource
    TencentYunUtil tencentYunUtil;

    @Resource
    private TBTaskHistoryService tbTaskHistoryService;

    @Resource
    TEExamPaperService examPaperService;


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

    /**
     * 获取考试待考列表
     *
     * @param studentId
     * @param examId
     * @param orgId
     * @return
     */
    @Override
    public List<TEExamDto> getWaitingExam(Long studentId, Long examId, Long orgId) {
        List<TEExamDto> list = teExamMapper.getWaitingExam(studentId, examId, orgId);
        if (Objects.nonNull(list) && list.size() > 0) {
            Set<Long> examActivityIds = list.stream().map(s -> s.getExamActivityId()).collect(Collectors.toSet());
            List<TEExamActivityDto> teExamActivityList = teExamActivityService.getWaitingExam(studentId, examActivityIds, null);
            Map<Long, Set<TEExamActivityDto>> map = new HashMap<>();
            teExamActivityList.forEach(v -> {
                Set<TEExamActivityDto> teExamActivityDtos = null;
                if (Objects.isNull(map.get(v.getExamId()))) {
                    teExamActivityDtos = new HashSet<>();
                } else {
                    teExamActivityDtos = map.get(v.getExamId());
                }
                teExamActivityDtos.add(v);
                map.put(v.getExamId(), teExamActivityDtos);
                ExamCacheBean examCache = getExamCacheBeanNative(v.getExamId());
                ExamStudentCacheBean examStudentCacheBean = teExamStudentService.getExamStudentCacheBean(v.getExamStudentId());
                v.setLeftExamCount(examCache.getExamCount() - examStudentCacheBean.getAlreadyExamCount());
                if (Objects.nonNull(v.getInProcessLivenessFixedRangeStr())) {
                    String[] longs = v.getInProcessLivenessFixedRangeStr().trim().replaceAll(" ", "").split(",");
                    List inProcessLivenessFixedRange = new ArrayList();
                    for (int i = 0; i < longs.length; i++) {
                        Integer integer = Integer.valueOf(longs[i].trim());
                        inProcessLivenessFixedRange.add(integer);
                    }
                    if (Objects.equals(inProcessLivenessFixedRange.toString().trim().replaceAll(" ", ""), "")) {
                        v.setInProcessLivenessFixedRange(null);
                    } else {
                        v.setInProcessLivenessFixedRange(inProcessLivenessFixedRange);
                    }
                }
                if (Objects.nonNull(v.getMonitorVideoSourceStr()) && !Objects.equals(v.getMonitorVideoSourceStr().toString().trim().replaceAll(" ", ""), "")) {
                    v.setMonitorVideoSource(Arrays.asList(v.getMonitorVideoSourceStr().trim().toUpperCase().replaceAll(" ", "").split(",")));
                    //加入monitorAudioEnable逻辑
                    if (v.getMonitorVideoSource().size() == 4 || (v.getMonitorVideoSourceStr().toUpperCase().contains(MonitorVideoSourceEnum.CLIENT_SCREEN.name()) || v.getMonitorVideoSourceStr().toUpperCase().contains(MonitorVideoSourceEnum.CLIENT_CAMERA.name()))) {
                        v.setMonitorAudioEnable(true);
                    }
                    //加入hardwareTest逻辑
                    if (v.getMonitorVideoSourceStr().toUpperCase().contains(MonitorVideoSourceEnum.CLIENT_CAMERA.name()) || (Objects.nonNull(v.getEntryAuthenticationPolicy()) && (Objects.equals(v.getEntryAuthenticationPolicy(), EntryAuthenticationPolicyEnum.LIVENESS_VERIFY.name()) || Objects.equals(v.getEntryAuthenticationPolicy(), EntryAuthenticationPolicyEnum.FACE_VERIFY_FORCE.name()))) || (Objects.nonNull(v.getCameraPhotoUpload()) && v.getCameraPhotoUpload() == 1)) {
                        List<String> hardwareTest = v.getHardwareTest();
                        if (Objects.isNull(hardwareTest)) {
                            hardwareTest = new ArrayList<>();
                        }
                        hardwareTest.add(HardwareTestEnum.CAMERA.name());
                        //取course缓存
                        ExamCourseCacheBean examCourseCacheBean = teExamCourseService.getExamCourseCacheBean(v.getExamId(), v.getCourseCode());
                        if (Objects.nonNull(examCourseCacheBean) && Objects.nonNull(examCourseCacheBean.getHasAudio()) && examCourseCacheBean.getHasAudio() == 1) {
                            hardwareTest.add(HardwareTestEnum.AUDIOPLAY.name());
                        }
                        v.setHardwareTest(hardwareTest);
                    }
                } else {
                    v.setMonitorVideoSource(null);
                }
            });
            list.forEach(s -> {
                s.setActivities(new ArrayList<>(map.get(s.getId())));
            });
        }
        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("考生IP不被允许");
            }
        }
    }
    /**
     * 开始候考
     */
    @Transactional
    @Override
    public ExamPrepareBean prepare(Long studentId, Long examStudentId) {

        TBSession tbSession = (TBSession) ServletUtil.getRequestSession();
        ExamStudentCacheBean es = teExamStudentService.getExamStudentCacheBean(examStudentId);
        if (es == null) {
            throw new BusinessException("未找到考生");
        }

        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException("考生Id和当前登录用户不一致");
        }
        ExamCacheBean examCache = getExamCacheBeanNative(es.getExamId());

        checkIp(examCache);

        Long unFinishedRecordId = ExamingDataCacheUtil.getUnFinishedRecordId(studentId);
        if (unFinishedRecordId != null) {
            Long recordId = unFinishedRecordId;
            if (examStudentId.equals(ExamRecordCacheUtil.getExamStudentId(recordId))) {//当前考生已存在候考的考试记录
                ExamPaperCacheBean ep = teExamPaperService.getExamPaperCacheBean(ExamRecordCacheUtil.getPaperId(recordId));
                ExamCourseCacheBean ec = teExamCourseService.getExamCourseCacheBean(es.getExamId(), es.getCourseCode());
                ExamPrepareBean prepare = new ExamPrepareBean();
                prepare.setRecordId(recordId);
                prepare.setAudioPlayCount(ep.getAudioPlayCount());
                prepare.setHasAudio((ep.getHasAudio() == null || ep.getHasAudio().intValue() == 0 ? false : true));
                prepare.setObjectiveShuffle(
                        (ec.getObjectiveShuffle() == null || ec.getObjectiveShuffle().intValue() == 0 ? false : true));
                prepare.setOptionShuffle(
                        (ec.getOptionShuffle() == null || ec.getOptionShuffle().intValue() == 0 ? false : true));
                String paperurl = OssUtil.getUrlForPrivateBucket(systemConfig.getOssEnv(3), ep.getPaperPath());
                String structurl = OssUtil.getUrlForPrivateBucket(systemConfig.getOssEnv(3), ep.getStructPath());
                prepare.setPaperUrl(paperurl);
                prepare.setStructUrl(structurl);
                prepare.setMonitorKey(ExamRecordCacheUtil.getMonitorKey(recordId));
                prepare.setMonitorUserId("s_" + tbSession.getId());
                prepare.setMonitorUserSig(tencentYunUtil.getSign(prepare.getMonitorUserId(), SystemConstant.TENCENT_EXPIRE_TIME));
                return prepare;
            }
        }
        if (examCache.getExamCount().intValue() <= es.getAlreadyExamCount().intValue()) {
            throw new BusinessException("没有剩余考试次数");
        }
        Long activityId = es.getExamActivityId();
        ExamActivityCacheBean ac = teExamActivityService.getExamActivityCacheBean(activityId);
        if (ac == null) {
            throw new BusinessException("未找到场次");
        }
        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("没有到允许开考的时间");
            }
            if (now.getTime() > end) {
                throw new BusinessException("允许开考的时间已结束");
            }
        } else {
            Long start = ac.getStartTime() - (ac.getPrepareSeconds() * 1000);
            Long end = ac.getStartTime() + (ac.getOpeningSeconds() * 1000);
            if (now.getTime() < start) {
                throw new BusinessException("没有到允许开考的时间");
            }
            if (now.getTime() > end) {
                throw new BusinessException("允许开考的时间已结束");
            }
        }

        ExamCourseCacheBean ec = teExamCourseService.getExamCourseCacheBean(es.getExamId(), es.getCourseCode());
        if (ec == null) {
            throw new BusinessException("未找到考试科目");
        }
        if (ec.getPaperIds() == null) {
            throw new BusinessException("考试科目未绑定试卷");
        }
        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("未找到试卷：" + paperId);
        }
        if (StringUtils.isBlank(ep.getAnswerPath())) {
            throw new BusinessException("试卷答案未上传：" + paperId);
        }

        // 写入次数
        Integer alreadyExamCount = es.getAlreadyExamCount() + 1;

        Long recordId = toeExamRecordService.saveByPrepare(es.getExamId(), es.getExamActivityId(), examStudentId,
                paperId, alreadyExamCount);

        es.setAlreadyExamCount(alreadyExamCount);

        es.setCurrentRecordId(recordId);

        ExamPrepareBean prepare = new ExamPrepareBean();
        prepare.setRecordId(recordId);
        prepare.setAudioPlayCount(ep.getAudioPlayCount());
        prepare.setHasAudio((ep.getHasAudio() == null || ep.getHasAudio().intValue() == 0 ? false : true));
        prepare.setObjectiveShuffle(
                (ec.getObjectiveShuffle() == null || ec.getObjectiveShuffle().intValue() == 0 ? false : true));
        prepare.setOptionShuffle(
                (ec.getOptionShuffle() == null || ec.getOptionShuffle().intValue() == 0 ? false : true));
        String paperurl = OssUtil.getUrlForPrivateBucket(systemConfig.getOssEnv(3), ep.getPaperPath());
        String structurl = OssUtil.getUrlForPrivateBucket(systemConfig.getOssEnv(3), ep.getStructPath());
        prepare.setPaperUrl(paperurl);
        prepare.setStructUrl(structurl);
        prepare.setMonitorKey(ExamRecordCacheUtil.getMonitorKey(recordId));
        prepare.setMonitorUserId("s_" + tbSession.getId());
        prepare.setMonitorUserSig(tencentYunUtil.getSign(prepare.getMonitorUserId(), SystemConstant.TENCENT_EXPIRE_TIME));

        // 更新考生缓存
        redisUtil.set(RedisKeyHelper.examStudentCacheKey(examStudentId), es);
        //更新场次-考试记录缓存
        ExamActivityRecordCacheUtil.setExamRecordStatus(activityId, recordId, new ExamActivityRecordCacheBean(ExamRecordCacheUtil.getExamStudentId(recordId), ExamRecordCacheUtil.getStatus(recordId)));
        ExamingDataCacheUtil.setUnFinishedRecordId(studentId, recordId);
        //mq发送消息start
        TEStudentCacheDto teStudentCacheDto = (TEStudentCacheDto) redisUtil.getStudent(studentId);
        MqDto mqDto = new MqDto(MqTopicEnum.THEMIS_TOPIC.getCode(), MqTagEnum.STUDENT.name(), SystemOperationEnum.FIRST_PREPARE, MqTagEnum.STUDENT, String.valueOf(teStudentCacheDto.getId()), teStudentCacheDto.getIdentity());
        this.sendOeLogMessage(SystemOperationEnum.FIRST_PREPARE, examStudentId, recordId, mqDto);
        updateExamStudent(examStudentId, alreadyExamCount, recordId);
        //mq发送消息end
        return prepare;
    }

    private void updateExamStudent(Long examStudentId, Integer alreadyExamCount, Long currentRecordId) {
        Map<String, Object> transMap = new HashMap<String, Object>();
        transMap.put("examStudentId", examStudentId);
        transMap.put("alreadyExamCount", alreadyExamCount);
        transMap.put("currentRecordId", currentRecordId);
        MqDto mqDto = new MqDto(MqTopicEnum.THEMIS_TOPIC.getCode(), MqTagEnum.EXAM_STUDNET_UPDATE.name(), transMap, MqTagEnum.EXAM_STUDNET_UPDATE, examStudentId.toString(), examStudentId.toString());
        mqDtoService.assembleSendOneWayMsg(mqDto);
    }

    /**
     * 根据设定几率取出一套试卷
     */
    private int getPaperByWeight(List<Double> paperWeight) {
        // 从1开始
        double r = Math.random();
        for (int i = 0; i < paperWeight.size(); i++) {
            r -= paperWeight.get(i);
            if (r <= 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("未找到考试记录");
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException("未找到考生");
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
        }
        ExamCacheBean exam = getExamCacheBeanNative(es.getExamId());
        Long activityId = es.getExamActivityId();
        ExamActivityCacheBean ac = teExamActivityService.getExamActivityCacheBean(activityId);
        if (ac == null) {
            throw new BusinessException("未找到场次");
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta)) {
            throw new BusinessException("该考试已结束");
        }
        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("没有到允许开考的时间");
            }
            if(!ExamRecordStatusEnum.RESUME_PREPARE.equals(sta)) {//非断点进入的
            	if (now.getTime() > end) {
                    throw new BusinessException("允许开考的时间已结束");
                }
            }
        } else {
            Long start = ac.getStartTime();
            Long end = ac.getStartTime() + (ac.getOpeningSeconds() * 1000);
            if (now.getTime() < start) {
                throw new BusinessException("没有到允许开考的时间");
            }
            if(ExamRecordStatusEnum.RESUME_PREPARE.equals(sta)) {//断点进入的
            	//集中不启用统一收卷，考试快结束时，断点，再登录时， 时间已经超过考试截止时间，断点时，此时应该能登录继续作答
	            Integer forceFinish = ExamRecordCacheUtil.getForceFinish(recordId);
	            if (forceFinish != null && forceFinish.intValue() == 1) {
	                if (now.getTime() > end) {
	                    throw new BusinessException("允许开考的时间已结束");
	                }
	            }
            }else {
            	if (now.getTime() > end) {
                    throw new BusinessException("允许开考的时间已结束");
                }
            }
        }
        

        Long paperId = ExamRecordCacheUtil.getPaperId(recordId);
        ExamPaperCacheBean ep = teExamPaperService.getExamPaperCacheBean(paperId);
        if (ep == null) {
            throw new BusinessException("未找到试卷");
        }

        WebsocketStatusEnum websocketStatusEnum = ExamRecordCacheUtil.getClientWebsocketStatus(recordId);
        if (Objects.nonNull(websocketStatusEnum) && Objects.equals(WebsocketStatusEnum.OFF_LINE, websocketStatusEnum)) {
            throw new BusinessException("客户端网络离线");
        }
        MonitorStatusSourceEnum clientCameraStatus = ExamRecordCacheUtil.getMonitorStatus(recordId, MonitorVideoSourceEnum.CLIENT_CAMERA.name());
        if (Objects.nonNull(clientCameraStatus) && Objects.equals(MonitorStatusSourceEnum.STOP, clientCameraStatus)) {
            throw new BusinessException("客户端摄像头离线");
        }
        MonitorStatusSourceEnum clientScreenStatus = ExamRecordCacheUtil.getMonitorStatus(recordId, MonitorVideoSourceEnum.CLIENT_SCREEN.name());
        if (Objects.nonNull(clientScreenStatus) && Objects.equals(MonitorStatusSourceEnum.STOP, clientScreenStatus)) {
            throw new BusinessException("客户端屏幕离线");
        }
        MonitorStatusSourceEnum mobileFirstStatus = ExamRecordCacheUtil.getMonitorStatus(recordId, MonitorVideoSourceEnum.MOBILE_FIRST.name());
        if (Objects.nonNull(mobileFirstStatus) && Objects.equals(MonitorStatusSourceEnum.STOP, mobileFirstStatus)) {
            throw new BusinessException("移动端第一机位离线");
        }
        MonitorStatusSourceEnum mobileSecondStatus = ExamRecordCacheUtil.getMonitorStatus(recordId, MonitorVideoSourceEnum.MOBILE_SECOND.name());
        if (Objects.nonNull(mobileSecondStatus) && Objects.equals(MonitorStatusSourceEnum.STOP, mobileSecondStatus)) {
            throw new BusinessException("移动端第二机位离线");
        }

        ExamStudentCacheBean examStudentCacheBean = teExamStudentService.getExamStudentCacheBean(examStudentId);
        Integer durationSeconds = Objects.isNull(ExamRecordCacheUtil.getDurationSeconds(recordId)) ? 0 : ExamRecordCacheUtil.getDurationSeconds(recordId);
        //获取断点时间
        Long lastBreakTime = ExamRecordCacheUtil.getLastBreakTime(recordId);
        //获取剩余断点次数
        Integer alreadyBreakCount = Objects.isNull(ExamRecordCacheUtil.getAlreadyBreakCount(recordId)) ? 0 : ExamRecordCacheUtil.getAlreadyBreakCount(recordId);
        Integer leftBreakResumeCount = exam.getBreakResumeCount() - alreadyBreakCount;
        Integer breakExpireSeconds = Objects.isNull(exam.getBreakExpireSeconds()) ? 0 : exam.getBreakExpireSeconds();
        //如果断点时间大于整体断点时间，则强制交卷
        if (Objects.nonNull(lastBreakTime) && (System.currentTimeMillis() - lastBreakTime) / 1000 > breakExpireSeconds) {
            finish(examStudentCacheBean.getStudentId(), recordId, FinishTypeEnum.AUTO.name(), durationSeconds);
        } else {
            //如果断点次数小于0，也强制交卷
            if (leftBreakResumeCount < 0) {
                finish(examStudentCacheBean.getStudentId(), recordId, FinishTypeEnum.AUTO.name(), durationSeconds);
            }
        }
        ret = new ExamStartBean();
        ret.setPaperDecryptSecret(ep.getDecryptSecret());
        ret.setPaperDecryptVector(ep.getDecryptVector());

        // 更新考试记录缓存
        Long firstStartTime = System.currentTimeMillis();
        ExamRecordCacheUtil.setFirstStartTime(recordId, firstStartTime, false);
        ExamRecordCacheUtil.setStatus(recordId, ExamRecordStatusEnum.ANSWERING, false);
        Long lastStartTime = System.currentTimeMillis();
        ExamRecordCacheUtil.setLastStartTime(recordId, lastStartTime, false);
        String[] columns = new String[]{ExamRecordFieldEnum.first_start_time.name(), ExamRecordFieldEnum.status.name(), ExamRecordFieldEnum.last_start_time.name()};
        Object[] values = new Object[]{firstStartTime, ExamRecordStatusEnum.ANSWERING, lastStartTime};
        toeExamRecordService.dataUpdatesMq(recordId, columns, values);
        //更新场次-考试记录缓存
        ExamActivityRecordCacheUtil.setExamRecordStatus(activityId, recordId, new ExamActivityRecordCacheBean(ExamRecordCacheUtil.getExamStudentId(recordId), ExamRecordCacheUtil.getStatus(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 activityMaxDurationSeconds = Objects.nonNull(ac.getMaxDurationSeconds()) ? ac.getMaxDurationSeconds() : null;
                Integer maxDurationSeconds = Objects.nonNull(exam.getMaxDurationSeconds()) ? exam.getMaxDurationSeconds() : null;
                Integer finalMaxDurationSeconds = Objects.nonNull(activityMaxDurationSeconds) ? activityMaxDurationSeconds : maxDurationSeconds;
                Calendar calendar = Calendar.getInstance();
                if (Objects.nonNull(finalMaxDurationSeconds)) {
                    calendar.setTimeInMillis(startTime);
                    calendar.add(Calendar.SECOND, finalMaxDurationSeconds.intValue());
                }
                Integer breakResumeCount = exam.getBreakResumeCount();
                Long finalFinishTime = (calendar.getTime().getTime() / 1000 + (breakResumeCount * breakExpireSeconds) + (breakResumeCount * 180)) * 1000;
                ExamRecordCacheUtil.setExamFinalFinishTime(recordId, finalFinishTime);
                //新增quartz任务,发送mq消息start
                Map<String, Object> prop = new HashMap<>();
                prop.put("oper", "insert");
                prop.put("exam", exam);
                MqDto mqDto = new MqDto(MqTopicEnum.THEMIS_TOPIC.getCode(), MqTagEnum.EXAM_STUDENT.name(), JacksonUtil.parseJson(examStudentCacheBean), MqTagEnum.EXAM_STUDENT, String.valueOf(recordId), prop, es.getName());
                mqDtoService.assembleSendOneWayMsg(mqDto);
                //新增quartz任务,发送mq消息end
            }
        }
        return ret;
    }

    /**
     * 上传个人试卷结构
     */
    @Override
    public StudentPaperStructBean studentPaperStruct(Long studentId, Long recordId, String content) {

        // 校验当前登录用户和参数一致性
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            throw new BusinessException("未找到考试记录");
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException("未找到考生");
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta)) {
            throw new BusinessException("该考试已结束");
        }
        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("未找到考试记录");
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException("未找到考生");
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta)) {
            throw new BusinessException("该考试已结束");
        }
        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);
            }
        }
        // 每次提交，清空得分
        answerCache.setScore(null);
        // 更新考生作答
        redisUtil.set(RedisKeyHelper.examAnswerKey(recordId),
                RedisKeyHelper.examAnswerHashKey(mainNumber, subNumber, subIndex), answerCache);
        // 如果是客观题，重置考试记录客观题得分
        if (answerCache.getObjective()) {
            ExamRecordCacheUtil.setObjectiveScore(recordId, null, false);
            // 发消息计算客观分
            calculateObjectiveScore(recordId, mainNumber, subNumber, subIndex);
        }

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

        return ret;
    }

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

    private void calculateObjectiveScore(Long recordId, Integer mainNumber, Integer subNumber, Integer subIndex) {
        Map<String, Object> transMap = new HashMap<String, Object>();
        transMap.put("recordId", recordId);
        transMap.put("mainNumber", mainNumber);
        transMap.put("subNumber", subNumber);
        transMap.put("subIndex", subIndex);
        // mq发送消息start
        MqDto mqDto = new MqDto(MqTopicEnum.THEMIS_TOPIC.getCode(), MqTagEnum.CALCULATE_OBJECTIVE_SCORE.name(), transMap,
                MqTagEnum.CALCULATE_OBJECTIVE_SCORE, recordId.toString(), recordId.toString());
        mqDtoService.assembleSendOneWayMsg(mqDto);
    }

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

        // 校验当前登录用户和参数一致性
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            throw new BusinessException("未找到考试记录");
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException("未找到考生");
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta)) {
            throw new BusinessException("该考试已结束");
        }
        // 音频剩余播放次数缓存
        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("未找到考试记录");
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException("未找到考生");
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta)) {
            throw new BusinessException("该考试已结束");
        }

        String filePath = "upload" + File.separator + sdf.format(new Date()) + File.separator + uuid() + "." + suffix;
        InputStream in = null;
        try {
            String fileMd5 = DigestUtils.md5Hex(file.getBytes());
            if (!md5.equals(fileMd5)) {
                throw new BusinessException("文件md5不一致");
            }
            in = file.getInputStream();
            OssUtil.ossUploadStream(systemConfig.getOssEnv(3), filePath, in);
            String url = systemConfig.getProperty("aliyun.oss.url") + 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) {
                }
            }
        }
    }

    private String uuid() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    /**
     * 断点恢复
     */
    @SuppressWarnings("unchecked")
    @Override
    public ExamResumeBean resume(Long studentId, Long recordId) {
        TBSession tbSession = (TBSession) ServletUtil.getRequestSession();
        // 校验当前登录用户和参数一致性
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            throw new BusinessException("未找到考试记录");
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException("未找到考生");
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
        }
        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("未找到试卷");
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta)) {
            throw new BusinessException("该考试已结束");
        }
        ExamStudentCacheBean examStudentCacheBean = teExamStudentService.getExamStudentCacheBean(examStudentId);
        Integer durationSeconds = Objects.isNull(ExamRecordCacheUtil.getDurationSeconds(recordId)) ? 0 : ExamRecordCacheUtil.getDurationSeconds(recordId);
        //获取断点时间
        Long lastBreakTime = ExamRecordCacheUtil.getLastBreakTime(recordId);
        //获取剩余断点次数
        Integer alreadyBreakCount = Objects.isNull(ExamRecordCacheUtil.getAlreadyBreakCount(recordId)) ? 0 : ExamRecordCacheUtil.getAlreadyBreakCount(recordId);
        Integer leftBreakResumeCount = ec.getBreakResumeCount() - alreadyBreakCount;
        Integer breakExpireSeconds = Objects.isNull(ec.getBreakExpireSeconds()) ? 0 : ec.getBreakExpireSeconds();
        //如果断点时间大于整体断点时间，则强制交卷
        if (Objects.nonNull(lastBreakTime) && (System.currentTimeMillis() - lastBreakTime) / 1000 > breakExpireSeconds) {
            finish(examStudentCacheBean.getStudentId(), recordId, FinishTypeEnum.AUTO.name(), durationSeconds);
        } else {
            //如果断点次数小于0，也强制交卷
            if (leftBreakResumeCount < 0) {
                finish(examStudentCacheBean.getStudentId(), recordId, FinishTypeEnum.AUTO.name(), durationSeconds);
            }
        }

        ExamResumeBean ret = new ExamResumeBean();
        ret.setDurationSeconds(ExamRecordCacheUtil.getDurationSeconds(recordId));
        ret.setPaperUrl(OssUtil.getUrlForPrivateBucket(systemConfig.getOssEnv(3), ep.getPaperPath()));
        ret.setStructUrl(OssUtil.getUrlForPrivateBucket(systemConfig.getOssEnv(3), ep.getStructPath()));
        ret.setHasAudio((ep.getHasAudio() != null && ep.getHasAudio().intValue() == 1 ? true : false));
        ret.setAudioPlayCount(ep.getAudioPlayCount());
        ret.setMonitorKey(ExamRecordCacheUtil.getMonitorKey(recordId));
        ret.setMonitorUserId("s_" + tbSession.getId());
        ret.setMonitorUserSig(tencentYunUtil.getSign(ret.getMonitorUserId(), SystemConstant.TENCENT_EXPIRE_TIME));

        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);
        }

        Long lastPrepareTime = System.currentTimeMillis();
        ExamRecordCacheUtil.setLastPrepareTime(recordId, lastPrepareTime, false);
        ExamRecordCacheUtil.setStatus(recordId, ExamRecordStatusEnum.RESUME_PREPARE, false);
        String[] columns = new String[]{ExamRecordFieldEnum.last_prepare_time.name(), ExamRecordFieldEnum.status.name()};
        Object[] values = new Object[]{lastPrepareTime, ExamRecordStatusEnum.RESUME_PREPARE};
        toeExamRecordService.dataUpdatesMq(recordId, columns, values);
        //更新场次-考试记录缓存
        ExamActivityRecordCacheUtil.setExamRecordStatus(es.getExamActivityId(), recordId, new ExamActivityRecordCacheBean(ExamRecordCacheUtil.getExamStudentId(recordId), ExamRecordCacheUtil.getStatus(recordId)));
        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) {

        // 校验当前登录用户和参数一致性
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            throw new BusinessException("未找到考试记录");
        }
        Long examStudentId = ExamRecordCacheUtil.getExamStudentId(recordId);
        ExamStudentCacheBean es = (ExamStudentCacheBean) redisUtil
                .get(RedisKeyHelper.examStudentCacheKey(examStudentId));
        if (es == null) {
            throw new BusinessException("未找到考生");
        }
        if (!studentId.equals(es.getStudentId())) {
            throw new BusinessException("考试记录的学生Id和当前登录用户不一致");
        }
        ExamRecordStatusEnum sta = ExamRecordCacheUtil.getStatus(recordId);
        if (ExamRecordStatusEnum.FINISHED.equals(sta) || ExamRecordStatusEnum.PERSISTED.equals(sta)) {
            throw new BusinessException("该考试已结束");
        }

        Date now = new Date();
        ExamFinishBean ret = new ExamFinishBean();
        ret.setFinishTime(now.getTime());
        ret.setObjectiveScore(ExamRecordCacheUtil.getObjectiveScore(recordId));
        ExamCacheBean exam = getExamCacheBeanNative(es.getExamId());
        //页面结果
        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 == 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 (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);
        }
        Long finishTime = System.currentTimeMillis();
        ExamRecordCacheUtil.setFinishTime(recordId, finishTime, false);
        ExamRecordCacheUtil.setDurationSeconds(recordId, durationSeconds, false);
        ExamRecordCacheUtil.setFinishType(recordId, FinishTypeEnum.valueOf(type), false);
        ExamRecordCacheUtil.setStatus(recordId, ExamRecordStatusEnum.FINISHED, false);
        String[] columns = new String[]{ExamRecordFieldEnum.finish_time.name(), ExamRecordFieldEnum.duration_seconds.name(), ExamRecordFieldEnum.finish_type.name(), ExamRecordFieldEnum.status.name()};
        Object[] values = new Object[]{finishTime, durationSeconds, FinishTypeEnum.valueOf(type), ExamRecordStatusEnum.FINISHED};
        toeExamRecordService.dataUpdatesMq(recordId, columns, values);
        //更新场次-考试记录缓存
        ExamActivityRecordCacheUtil.setExamRecordStatus(es.getExamActivityId(), recordId, new ExamActivityRecordCacheBean(ExamRecordCacheUtil.getExamStudentId(recordId), ExamRecordCacheUtil.getStatus(recordId)));
        //更新未完成考试记录id
        TEStudentCacheDto teStudentCacheDto = (TEStudentCacheDto) redisUtil.getStudent(es.getStudentId());
        ExamingDataCacheUtil.deleteUnFinishedRecordId(studentId);
        redisUtil.setStudent(teStudentCacheDto.getId(), teStudentCacheDto);
        //异步持久化
        checkToPersisted(recordId);
        //mq发送消息start
        MqDto mqDto = new MqDto(MqTopicEnum.THEMIS_TOPIC.getCode(), MqTagEnum.STUDENT.name(), SystemOperationEnum.FINISHED, MqTagEnum.STUDENT, String.valueOf(teStudentCacheDto.getId()), teStudentCacheDto.getIdentity());
        this.sendOeLogMessage(SystemOperationEnum.FINISHED, examStudentId, recordId, mqDto);
        //mq发送消息end
        return ret;
    }

    /**
     * 检查条件并发送持久化消息
     *
     * @param recordId
     */
    private void checkToPersisted(Long recordId) {
        ExamRecordStatusEnum status = ExamRecordCacheUtil.getStatus(recordId);
        if (!ExamRecordStatusEnum.FINISHED.equals(status)) {
            return;
        }

        Map<String, Object> transMap = new HashMap<String, Object>();
        transMap.put("recordId", recordId);
        String level = "10s";
        Integer time = SystemConstant.mqDelayLevel.get(level);
        LocalDateTime dt = LocalDateTime.now();
        dt = dt.plusSeconds(Long.parseLong(level.replace("s", "")));
        Map<String, Object> propMap = new HashMap<String, Object>();
        propMap.put("timeOut", time);
        propMap.put("mqExecTime", dt.toInstant(ZoneOffset.of("+8")).toEpochMilli());
        MqDto mqDto = new MqDto(MqTopicEnum.THEMIS_TOPIC.getCode(), MqTagEnum.EXAM_RECORD_PERSISTED.name(),
                transMap, 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();
        TOeExamRecord er = null;
        if (ExamRecordCacheUtil.getId(recordId) == null) {
            er = toeExamRecordService.getById(recordId);
            if (er == null) {
                throw new BusinessException("未找到考试记录");
            }
            ret.setDurationSeconds(er.getDurationSeconds());
            ret.setFinishTime(er.getFinishTime());
            ret.setFinishType(er.getFinishType());
            ret.setObjectiveScore(er.getObjectiveScore());
        } else {
            ret.setDurationSeconds(ExamRecordCacheUtil.getDurationSeconds(recordId));
            ret.setFinishTime(ExamRecordCacheUtil.getFinishTime(recordId));
            ret.setFinishType(ExamRecordCacheUtil.getFinishType(recordId));
            ret.setObjectiveScore(ExamRecordCacheUtil.getObjectiveScore(recordId));
        }

        //页面结果
        ExamCacheBean exam = getExamCacheBeanNative(er.getExamId());
        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 == 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);
        }
        return ret;
    }

    /**
     * 监考端获取考试批次列表
     *
     * @param userId
     * @param orgId
     * @return
     */
    @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("remark", systemOperationEnum.getCode());
        properties.put("examStudentId", examStudentId);
        properties.put("examRecordId", recordId);
        mqDto.setProperties(properties);
        mqDtoService.assembleSendOneWayMsg(mqDto);
        //mq发送消息end
    }

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

    /**
     * 缓存操作
     *
     * @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 * 5;
        String lockKey = SystemConstant.REDIS_LOCK_CALCULATE_SCORE_PREFIX + examId;
        Boolean lock = redisUtil.lock(lockKey, timeOutSecond);
        if (!lock) {
            return;
        }
        TBTaskHistory task = null;
        Long recordId = null;
        try {
            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());
                        }
                        redisUtil.expire(lockKey, timeOutSecond);
                    }
                    task.setProgress(getPercentage(index, total));
                    tbTaskHistoryService.saveOrUpdate(task);
                }
            }
            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());
        } finally {
            redisUtil.releaseLock(lockKey);
        }
        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);
        Double db = Double.valueOf(b);
        BigDecimal bd = new BigDecimal(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);
    }
}
