package com.qmth.teachcloud.mark.service.impl;

import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.io.*;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.FileCopyUtils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
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.itextpdf.text.Document;
import com.itextpdf.text.Image;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfWriter;
import com.qmth.boot.core.concurrent.service.ConcurrentService;
import com.qmth.boot.core.exception.ParameterException;
import com.qmth.boot.tools.excel.ExcelWriter;
import com.qmth.boot.tools.excel.enums.ExcelType;
import com.qmth.boot.tools.io.ZipWriter;
import com.qmth.teachcloud.common.bean.dto.DataPermissionRule;
import com.qmth.teachcloud.common.bean.vo.FilePathVo;
import com.qmth.teachcloud.common.contant.SystemConstant;
import com.qmth.teachcloud.common.entity.*;
import com.qmth.teachcloud.common.enums.ExceptionResultEnum;
import com.qmth.teachcloud.common.enums.PageSizeEnum;
import com.qmth.teachcloud.common.enums.ScanStatus;
import com.qmth.teachcloud.common.enums.mark.MarkPaperStatus;
import com.qmth.teachcloud.common.enums.mark.SubjectiveStatus;
import com.qmth.teachcloud.common.service.*;
import com.qmth.teachcloud.common.util.*;
import com.qmth.teachcloud.mark.bean.UpdateTimeVo;
import com.qmth.teachcloud.mark.bean.archivescore.*;
import com.qmth.teachcloud.mark.bean.omredit.OmrEditDomain;
import com.qmth.teachcloud.mark.bean.omredit.OmrEditPaper;
import com.qmth.teachcloud.mark.bean.scananswer.*;
import com.qmth.teachcloud.mark.bean.scanexaminfo.CheckTask;
import com.qmth.teachcloud.mark.bean.scanexaminfo.ScanExamCheckInfoVo;
import com.qmth.teachcloud.mark.bean.scanexaminfo.ScanExamInfoVo;
import com.qmth.teachcloud.mark.bean.student.AbsentManualUpdateVo;
import com.qmth.teachcloud.mark.bean.student.StudentQuery;
import com.qmth.teachcloud.mark.bean.student.StudentVo;
import com.qmth.teachcloud.mark.dto.UnexistStudentDto;
import com.qmth.teachcloud.mark.dto.mark.ScoreInfo;
import com.qmth.teachcloud.mark.dto.mark.ScoreItem;
import com.qmth.teachcloud.mark.dto.mark.manage.Task;
import com.qmth.teachcloud.mark.dto.mark.score.*;
import com.qmth.teachcloud.mark.entity.*;
import com.qmth.teachcloud.mark.enums.ExamStatus;
import com.qmth.teachcloud.mark.enums.LockType;
import com.qmth.teachcloud.mark.enums.OmrTaskStatus;
import com.qmth.teachcloud.mark.lock.LockService;
import com.qmth.teachcloud.mark.mapper.MarkStudentMapper;
import com.qmth.teachcloud.mark.params.MarkHeaderResult;
import com.qmth.teachcloud.mark.service.*;
import com.qmth.teachcloud.mark.utils.BatchGetDataUtil;
import com.qmth.teachcloud.mark.utils.Calculator;
import com.qmth.teachcloud.mark.utils.ScoreCalculateUtil;

/**
 * <p>
 * 考试考生库 服务实现类
 * </p>
 *
 * @author xf
 * @since 2023-09-22
 */
@Service
public class MarkStudentServiceImpl extends ServiceImpl<MarkStudentMapper, MarkStudent> implements MarkStudentService {

    @Resource
    private SysUserService sysUserService;
    @Resource
    private BasicCourseService basicCourseService;
    @Autowired
    private MarkPaperService markPaperService;
    @Resource
    private MarkPaperPackageService markPaperPackageService;
    @Autowired
    private ScanPackageService scanPackageService;
    @Autowired
    private ScanPaperService scanPaperService;
    @Autowired
    private ScanPaperPageService scanPaperPageService;
    @Autowired
    private ScanOmrTaskService scanOmrTaskService;

    @Autowired
    private ScanAnswerCardService answerCardService;

    @Autowired
    private ScanStudentPaperService studentPaperService;

    @Resource
    private MarkQuestionService markQuestionService;

    @Resource
    private TeachcloudCommonService teachcloudCommonService;

    @Autowired
    private ConcurrentService concurrentService;

    @Resource
    private MarkService markService;

    @Resource
    private LockService lockService;

    @Resource
    private TaskService taskService;

    @Resource
    private BasicRoleDataPermissionService basicRoleDataPermissionService;

    @Autowired
    private MarkSubjectiveScoreService markSubjectiveScoreService;

    @Resource
    private MarkSyncService markSyncService;

    @Resource
    private FileUploadService fileUploadService;
    @Resource
    private MarkTrackService markTrackService;

    @Override
    public List<String> listClassByExamIdAndCourseCode(Long examId, String paperNumber) {
        QueryWrapper<MarkStudent> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(MarkStudent::getExamId, examId).eq(MarkStudent::getPaperNumber, paperNumber);
        List<MarkStudent> markStudentList = this.list(queryWrapper);

        List<String> classNameList = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(markStudentList)) {
            classNameList = markStudentList.stream().filter(m -> StringUtils.isNotBlank(m.getClassName()))
                    .map(MarkStudent::getClassName).distinct().collect(Collectors.toList());
        }
        return classNameList;
    }

    @Override
    public void updateSubjectiveStatusAndScore(Long studentId, SubjectiveStatus status, Double score,
                                               String scoreList) {
        UpdateWrapper<MarkStudent> updateWrapper = new UpdateWrapper<>();
        updateWrapper.lambda().set(MarkStudent::getSubjectiveStatus, status).set(MarkStudent::getSubjectiveScore, score)
                .set(MarkStudent::getSubjectiveScoreList, scoreList).eq(MarkStudent::getId, studentId);
        this.update(updateWrapper);
    }

    @Override
    public void updateSubjectiveStatusAndScore(Long examId, String paperNumber, SubjectiveStatus status, Double score,
                                               String scoreList) {
        UpdateWrapper<MarkStudent> updateWrapper = new UpdateWrapper<>();
        updateWrapper.lambda().set(MarkStudent::getSubjectiveStatus, status).set(MarkStudent::getSubjectiveScore, score)
                .set(MarkStudent::getSubjectiveScoreList, scoreList)
                .set(MarkStudent::getCheckUserId, null)
                .set(MarkStudent::getCheckTime, null).eq(MarkStudent::getExamId, examId)
                .eq(MarkStudent::getPaperNumber, paperNumber);
        this.update(updateWrapper);
    }

    @Override
    public ScanExamInfoVo getScanExamInfo(BasicExam exam, String courseCode, String coursePaperId) {
        SysUser sysUser = (SysUser) ServletUtil.getRequestUser();
        DataPermissionRule dpr = basicRoleDataPermissionService.findDataPermission(sysUser.getSchoolId(),
                sysUser.getId(), ServletUtil.getRequest().getServletPath());
        ScanExamInfoVo vo = new ScanExamInfoVo();
        vo.setId(exam.getId());
        vo.setName(exam.getName());
        vo.getAnswerScan().setCourseCount(
                markPaperService.getCourseCount(exam.getId(), courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        vo.getAnswerScan().setPaperNumberCount(markPaperService.getPaperNumberCount(exam.getId(), courseCode,
                coursePaperId, MarkPaperStatus.FORMAL, dpr));
        vo.getAnswerScan()
                .setTotalCount(getCount(exam.getId(), null, courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        vo.getAnswerScan().setScannedCount(
                getCount(exam.getId(), ScanStatus.SCANNED, courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        vo.getPackageScan().setTotalCount(markPaperPackageService.getPackageCountByExamId(exam.getId(), courseCode,
                coursePaperId, MarkPaperStatus.FORMAL, dpr));
        vo.getPackageScan().setScannedCount(
                scanPackageService.getCount(exam.getId(), courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        return vo;
    }

    @Override
    public IPage<StudentScoreDetailDto> pageStudentScore(Long examId, String paperNumber, String college, String majorName,
                                                         String className, String teacher, Integer filter, String status, Boolean breach, Double startScore,
                                                         Double endScore, Double subjectiveStartScore, Double subjectiveEndScore, Double objectiveStartScore, Double objectiveEndScore,
                                                         Double subScore, Integer objectiveScoreRateLt, String studentName, String studentCode,
                                                         String orderType, String orderField, Integer pageNumber, Integer pageSize) {
        if (startScore != null && endScore == null) {
            throw ExceptionResultEnum.ERROR.exception("请输入试卷总分结束分数值");
        }
        if (objectiveStartScore != null && objectiveEndScore == null) {
            throw ExceptionResultEnum.ERROR.exception("请输入客观题总分结束分数值");
        }
        if (subjectiveStartScore != null && subjectiveEndScore == null) {
            throw ExceptionResultEnum.ERROR.exception("请输入主观题总分结束分数值");
        }
        college = SystemConstant.translateSpecificSign(college);
        majorName = SystemConstant.translateSpecificSign(majorName);
        className = SystemConstant.translateSpecificSign(className);
        teacher = SystemConstant.translateSpecificSign(teacher);

        Page<StudentScoreDetailDto> page = new Page<>(pageNumber, pageSize);
        MarkPaper markPaper = markPaperService.getByExamIdAndPaperNumber(examId, paperNumber);
        Double objectiveScoreLt = objectiveScoreRateLt == null ?
                null :
                Calculator.round(Calculator.divide(Calculator.multiply(markPaper.getObjectiveScore(), Double.parseDouble(String.valueOf(objectiveScoreRateLt))), 100), 2);
        IPage<StudentScoreDetailDto> studentScoreDetailDtoIPage = this.baseMapper.pageStudentScore(page, examId,
                paperNumber, college, majorName, className, teacher, filter, status, breach, startScore, endScore,
                subjectiveStartScore, subjectiveEndScore, objectiveStartScore, objectiveEndScore, subScore,
                objectiveScoreLt, studentName, studentCode, orderType, orderField);
        for (StudentScoreDetailDto scoreDetailDto : studentScoreDetailDtoIPage.getRecords()) {
            // 原图
            scoreDetailDto.setSheetUrls(buildSheetUrls(scoreDetailDto.getStudentId()));
            scoreDetailDto.setSheetPath(null);
            // 状态
            if (ScanStatus.UNEXIST.equals(scoreDetailDto.getScanStatus())) {
                scoreDetailDto.setStatusDisplay("未扫描");
            } else if (ScanStatus.MANUAL_ABSENT.equals(scoreDetailDto.getScanStatus()) || scoreDetailDto.getAbsent() || scoreDetailDto.getOmrAbsent()) {
                scoreDetailDto.setStatusDisplay("缺考");
            } else if (!scoreDetailDto.getAbsent() && !scoreDetailDto.getOmrAbsent() && scoreDetailDto.getUpload() && ScanStatus.SCANNED.equals(scoreDetailDto.getScanStatus())) {
                scoreDetailDto.setStatusDisplay("正常");
            }

            // 主观题检查标记
            scoreDetailDto.setSubjectiveCheckFlag(
                    !scoreDetailDto.getAbsent() && !scoreDetailDto.getOmrAbsent() && scoreDetailDto.getUpload() && ScanStatus.SCANNED.equals(scoreDetailDto.getScanStatus())
                            && SubjectiveStatus.MARKED.equals(scoreDetailDto.getSubjectiveStatus()) && StringUtils.isNotBlank(scoreDetailDto.getSubjectiveScore()) && StringUtils.isNotBlank(
                            scoreDetailDto.getSubjectiveScoreList()));
            // 客观题检查标记
            scoreDetailDto.setObjectiveCheckFlag(
                    !scoreDetailDto.getAbsent() && !scoreDetailDto.getOmrAbsent() && scoreDetailDto.getUpload() && ScanStatus.SCANNED.equals(scoreDetailDto.getScanStatus()) && StringUtils.isNotBlank(
                            scoreDetailDto.getObjectiveScore()) && StringUtils.isNotBlank(scoreDetailDto.getObjectiveScoreList()));

            // 格式化分数
            scoreDetailDto.setObjectiveScore(Calculator.round(scoreDetailDto.getObjectiveScore(), 1));
            scoreDetailDto.setSubjectiveScore(Calculator.round(scoreDetailDto.getSubjectiveScore(), 1));
            scoreDetailDto.setTotalScore(Calculator.round(scoreDetailDto.getTotalScore(), 1));
        }
        return studentScoreDetailDtoIPage;
    }

    @Override
    public List<SheetUrlDto> buildSheetUrls(Long studentId) {
        // 原图
        List<SheetUrlDto> sheetUrls = new ArrayList<>();
        List<StudentPaperDetailDto> studentPaperDetailDtoList = scanPaperService.listStudentPaperDetail(studentId);
        for (int i = 0; i < studentPaperDetailDtoList.size(); i++) {
            StudentPaperDetailDto studentPaperDetailDto = studentPaperDetailDtoList.get(i);
            sheetUrls.add(new SheetUrlDto(
                    2 * (studentPaperDetailDto.getPaperIndex() - 1) + studentPaperDetailDto.getPageIndex(),
                    teachcloudCommonService.filePreview(studentPaperDetailDto.getSheetPath()),
                    studentPaperDetailDto.getRecogData()));
        }
        return sheetUrls;
    }

    private int getCount(Long examId, ScanStatus status, String courseCode, String coursePaperId,
                         MarkPaperStatus markPaperStatus, DataPermissionRule dpr) {
        MarkStudent markStudent = new MarkStudent();
        markStudent.setExamId(examId);
        markStudent.setCourseCode(courseCode);
        markStudent.setCoursePaperId(coursePaperId);
        markStudent.setScanStatus(status);
        markStudent.setMarkPaperStatus(markPaperStatus.name());
        return baseMapper.selectCountByQuery(markStudent, dpr);
    }

    private int getOmrAbsentCount(Long examId, Boolean checked, String courseCode, String coursePaperId,
                                  MarkPaperStatus status, DataPermissionRule dpr) {
        MarkStudent markStudent = new MarkStudent();
        markStudent.setExamId(examId);
        markStudent.setCourseCode(courseCode);
        markStudent.setCoursePaperId(coursePaperId);
        markStudent.setOmrAbsent(true);
        markStudent.setOmrAbsentChecked(checked);
        markStudent.setMarkPaperStatus(status.name());
        return baseMapper.selectCountByQuery(markStudent, dpr);
    }

    private int getIncompleteCount(Long examId, String courseCode, String coursePaperId, MarkPaperStatus status,
                                   DataPermissionRule dpr) {
        MarkStudent markStudent = new MarkStudent();
        markStudent.setExamId(examId);
        markStudent.setCourseCode(courseCode);
        markStudent.setCoursePaperId(coursePaperId);
        markStudent.setIncomplete(true);
        markStudent.setMarkPaperStatus(status.name());
        return baseMapper.selectCountByQuery(markStudent, dpr);
    }

    @Override
    public ScanExamCheckInfoVo checkInfo(BasicExam exam, String courseCode, String coursePaperId) {
        SysUser sysUser = (SysUser) ServletUtil.getRequestUser();
        DataPermissionRule dpr = basicRoleDataPermissionService.findDataPermission(sysUser.getSchoolId(),
                sysUser.getId(), ServletUtil.getRequest().getServletPath());
        Long examId = exam.getId();
        ScanExamCheckInfoVo vo = new ScanExamCheckInfoVo();
        vo.setId(exam.getId());
        vo.setName(exam.getName());
        CheckTask ct = vo.getCheckTask();
        ct.setUnexistCount(
                getCount(examId, ScanStatus.UNEXIST, courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        ct.setUnexistCheckedCount(
                getCount(examId, ScanStatus.MANUAL_ABSENT, courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        ct.setAssignedCount(getAssignedCount(examId, false, courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        ct.setAssignedCheckedCount(
                getAssignedCount(examId, true, courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        ct.setAbsentCheckCount(
                getOmrAbsentCount(examId, false, courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        ct.setAbsentCheckedCount(
                getOmrAbsentCount(examId, true, courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        ct.setObjectiveCheckCount(scanOmrTaskService.getFinishStudentCountByExamAndUserId(examId, courseCode, coursePaperId,
                OmrTaskStatus.WAITING.name(), dpr));
        ct.setObjectiveCheckedCount(scanOmrTaskService.getFinishStudentCountByExamAndUserId(examId, courseCode, coursePaperId,
                OmrTaskStatus.PROCESSED.name(), dpr));
        ct.setIncompleteCount(getIncompleteCount(examId, courseCode, coursePaperId, MarkPaperStatus.FORMAL, dpr));
        return vo;
    }

    /**
     * 根据考生当前绑定的paper刷新考生状态，需要在外部调用处对考生上锁
     */
    @Override
    @Transactional
    public void updateStudentByPaper(@NotNull Long userId, @NotNull Long studentId, @NotNull boolean updateOmrTask) {
        MarkStudent student = this.getById(studentId);
        if (student == null) {
            throw new ParameterException("找不到对应的考生");
        }
        // 重置状态
        student.setIncomplete(false);
        student.setAssigned(false);
        student.setAssignConfirmed(false);
        student.setInvalid(false);
        student.setQuestionFilled(false);
        // student.setOmrAbsent(false);
        int paperCount = 0;
        List<ScanStudentPaper> studentPaperList = studentPaperService.findByStudentId(studentId);
        List<String> objectiveAnswers = new ArrayList<>();
        for (ScanStudentPaper studentPaper : studentPaperList) {
            paperCount++;
            // 获取paper详情更新考生状态
            ScanPaper paper = scanPaperService.getById(studentPaper.getPaperId());
            student.setAssigned(student.getAssigned() || paper.getAssigned());
            student.setInvalid(student.getInvalid() || paper.getInvalid());
            student.setQuestionFilled(student.getQuestionFilled() || paper.getQuestionFilled());
            student.setCardNumber(paper.getCardNumber());
            // 单独判断首张纸正面的识别结果
            if (studentPaper.getPaperIndex() == 1) {
                // 根据识别结果更新考生属性
                ScanPaperPage page = scanPaperPageService.findPaperIdAndIndex(paper.getId(), 1);
                student.setOmrAbsent(page.getAbsent() == null ? false : page.getAbsent().getResult());
                student.setOmrAbsentChecked(false);
                if (student.getOmrAbsent()) {
                    student.setObjectiveScore(null);
                    student.setObjectiveScoreList(null);
                }
                // student.setDevice(batchService.findByPaperId(paper.getId()).getDevice());
            }

            List<ScanPaperPage> scanPaperPages = scanPaperPageService.listByPaperId(studentPaper.getPaperId());
            for (ScanPaperPage scanPaperPage : scanPaperPages) {
                if (scanPaperPage.getQuestion() != null
                        && CollectionUtils.isNotEmpty(scanPaperPage.getQuestion().getResult())) {
                    for (String s : scanPaperPage.getQuestion().getResult()) {
                        if (s.startsWith("?")) {
                            s = s.replace("?", "");
                        }
                        objectiveAnswers.add(s);
                    }
                }
            }
        }
        student.setAnswers(JSON.toJSONString(objectiveAnswers));
        // 更新考生状态
        if (paperCount > 0) {
            ScanAnswerCard answerCard = answerCardService.findByExamAndNumber(student.getExamId(),
                    student.getCardNumber());
            student.setUpload(true);
            student.setIncomplete(paperCount != answerCard.getPaperCount());
            student.setScanStatus(ScanStatus.SCANNED);
            student.setAbsent(false);
            student.setManualAbsent(false);
            // 更新图片数量和图片地址
            updateStudentSheetInfo(student, studentPaperList);
        } else {
            student.setScanStatus(ScanStatus.UNEXIST);
        }
        // student.setUpdaterId(userId);
        // student.setUpdateTime(System.currentTimeMillis());
        this.saveOrUpdate(student);
        // 客观题统分
        this.calculateObjectiveScore(student);

        // 更新课程表中上传人数
        markPaperService.updateUploadCount(student.getExamId(), student.getPaperNumber(),
                this.countUploadedByExamIdAndPaperNumber(student.getExamId(), student.getPaperNumber()));
        markPaperService.updateAbsentCount(student.getExamId(), student.getPaperNumber(),
                this.countAbsentByExamIdAndPaperNumber(student.getExamId(), student.getPaperNumber()));
        if (updateOmrTask) {
            // 清除识别对照任务
            scanOmrTaskService.deleteByStudentId(student.getExamId(), student.getId());
            // 重新生成识别对照任务
            scanOmrTaskService.saveTask(student.getId());
        }
    }

    private MarkStudent updateStudentSheetInfo(MarkStudent student, List<ScanStudentPaper> studentPaperList) {
        List<FilePathVo> filePathVoList = new ArrayList<>();
        for (ScanStudentPaper scanStudentPaper : studentPaperList) {
            List<ScanPaperPage> scanPaperPages = scanPaperPageService.listByPaperId(scanStudentPaper.getPaperId());
            for (ScanPaperPage scanPaperPage : scanPaperPages) {
                String sheetPath = scanPaperPage.getSheetPath();
                if (StringUtils.isNotBlank(sheetPath)) {
                    filePathVoList.add(JSON.parseObject(sheetPath, FilePathVo.class));
                }
            }
        }
        student.setSheetCount(filePathVoList.size());
        student.setSheetPath(JSON.toJSONString(filePathVoList));
        return student;
    }

    @Override
    public MarkStudent findByExamIdAndCoursePaperIdAndStudentCode(Long examId, String coursePaperId, String studentCode) {
        QueryWrapper<MarkStudent> wrapper = new QueryWrapper<>();
        LambdaQueryWrapper<MarkStudent> lw = wrapper.lambda();
        lw.eq(MarkStudent::getExamId, examId);
        lw.eq(MarkStudent::getCoursePaperId, coursePaperId);
        lw.eq(MarkStudent::getStudentCode, studentCode);
        return baseMapper.selectOne(wrapper);
    }

    @Override
    public MarkStudent findByExamIdAndPaperNumberAndStudentCode(Long examId, String paperNumber, String studentCode) {
        QueryWrapper<MarkStudent> wrapper = new QueryWrapper<>();
        LambdaQueryWrapper<MarkStudent> lw = wrapper.lambda();
        lw.eq(MarkStudent::getExamId, examId);
        lw.eq(MarkStudent::getPaperNumber, paperNumber);
        if (StringUtils.isNotBlank(studentCode)) {
            lw.eq(MarkStudent::getStudentCode, studentCode);
        } else {
            lw.last("limit 1");
        }
        return baseMapper.selectOne(wrapper);
    }

    @Override
    public StudentObjectiveDetailDto getObjectiveInspectedTask(Long studentId) {
        MarkStudent markStudent = this.getById(studentId);
        StudentObjectiveDetailDto studentObjectiveDetailDto = new StudentObjectiveDetailDto();
        if (markStudent != null) {
            studentObjectiveDetailDto.setStudentId(markStudent.getId());
            studentObjectiveDetailDto.setStudentName(markStudent.getStudentName());
            studentObjectiveDetailDto.setStudentCode(markStudent.getStudentCode());
            studentObjectiveDetailDto.setExamPlace(markStudent.getExamPlace());
            studentObjectiveDetailDto.setExamRoom(markStudent.getExamRoom());
            studentObjectiveDetailDto.setExamId(markStudent.getExamId());
            studentObjectiveDetailDto.setCourseCode(markStudent.getCourseCode());
            studentObjectiveDetailDto.setCourseName(markStudent.getCourseName());
            studentObjectiveDetailDto.setPaperNumber(markStudent.getPaperNumber());
            studentObjectiveDetailDto
                    .setObjectiveScore(markStudent.getObjectiveScore() != null ? markStudent.getObjectiveScore() : 0);
            studentObjectiveDetailDto.setSubjectiveScore(
                    markStudent.getSubjectiveScore() != null ? markStudent.getSubjectiveScore() : 0);
            studentObjectiveDetailDto.setUpload(markStudent.getUpload());
            studentObjectiveDetailDto.setAbsent(markStudent.getAbsent());
            studentObjectiveDetailDto.setSheetUrls(this.buildSheetUrls(studentId));

            List<MarkQuestion> questions = markQuestionService.listQuestionByExamIdAndPaperNumberAndGroupNumber(
                    markStudent.getExamId(), markStudent.getPaperNumber(), null, true);
            List<String> answers = markStudent.getAnswerList();
            int questionCount = questions.size();
            int answerCount = answers.size();

            List<StudentObjectiveAnswerDto> answerDtoList = new ArrayList<>();
            Map<Integer, String> titles = new HashMap<>();
            // 已设置客观题
            int maxCount = Math.max(questionCount, answerCount);
            for (int i = 0; i < maxCount; i++) {
                MarkQuestion q = questionCount > i ? questions.get(i) : null;
                String answer = answerCount > i ? answers.get(i) : "#";
                StudentObjectiveAnswerDto studentObjectiveAnswerDto = new StudentObjectiveAnswerDto();
                studentObjectiveAnswerDto.setMainNumber(q != null ? q.getMainNumber() : 0);
                studentObjectiveAnswerDto.setSubNumber(q != null ? q.getSubNumber() : 0);
                studentObjectiveAnswerDto.setAnswer(answer);
                studentObjectiveAnswerDto.setExist(q != null && q.getTotalScore() > 0);
                studentObjectiveAnswerDto.setQuestionType(q != null ? q.getQuestionType() : 0);
                answerDtoList.add(studentObjectiveAnswerDto);

                if (q != null) {
                    titles.put(q.getMainNumber(), q.getMainTitle());
                }
            }
            studentObjectiveDetailDto.setAnswers(answerDtoList);
            studentObjectiveDetailDto.setTitles(titles);
        }

        return studentObjectiveDetailDto;
    }

    @Override
    public Boolean saveObjectiveInspectedTask(Long studentId, String answers) {
        Long userId = ServletUtil.getRequestUserId();
        MarkStudent student = this.getById(studentId);
        MarkPaper markPaper = markPaperService.getByExamIdAndPaperNumber(student.getExamId(), student.getPaperNumber());
        // 评卷是否结束
        if (markPaper == null || MarkPaperStatus.FINISH.equals(markPaper.getStatus())) {
            throw ExceptionResultEnum.ERROR.exception("科目已结束评卷，无法打分");
        }
        answers = StringUtils.trimToEmpty(answers);
        if (student != null) {
            student.setAnswers(answers.toUpperCase());
            student.setCheckUserId(userId);
            student.setCheckTime(System.currentTimeMillis());
            return saveUploadStudent(student);
        } else {
            return false;
        }
    }

    @Override
    public int countUploadedByExamIdAndPaperNumber(Long examId, String paperNumber) {
        QueryWrapper<MarkStudent> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(MarkStudent::getExamId, examId).eq(MarkStudent::getPaperNumber, paperNumber)
                .eq(MarkStudent::getUpload, true)
                .eq(MarkStudent::getScanStatus, ScanStatus.SCANNED)
                .eq(MarkStudent::getAbsent, false)
                .eq(MarkStudent::getBreach, false)
                .eq(MarkStudent::getOmrAbsent, false);
        return this.count(queryWrapper);
    }

    @Override
    public boolean updateScanInfo(MarkStudent student) {
        UpdateWrapper<MarkStudent> updateWrapper = new UpdateWrapper<>();
        updateWrapper.lambda().set(MarkStudent::getAnswers, student.getAnswers())
                .set(MarkStudent::getBatchCode, student.getBatchCode()).set(MarkStudent::getAbsent, student.getAbsent())
                .set(MarkStudent::getUploadTime, System.currentTimeMillis())
                .set(MarkStudent::getCardNumber, student.getCardNumber()).eq(MarkStudent::getId, student.getId());
        return this.update(updateWrapper);

    }

    @Override
    public List<MarkStudent> listAbsentOrBreachMarkTaskStudent(Long examId, String paperNumber) {
        return this.baseMapper.listAbsentOrBreachMarkTaskStudent(examId, paperNumber);
    }

    @Override
    public List<MarkStudent> listUnMarkTaskStudent(Long examId, String paperNumber, Integer groupNumber, int pageSize) {
        Page<MarkStudent> page = new Page<>(1, pageSize);
        IPage<MarkStudent> markStudentIPage = this.baseMapper.listUnMarkTaskStudent(page, examId, paperNumber,
                groupNumber);
        return markStudentIPage.getRecords();
    }

    /**
     * 客观题统分 统分场景： 1.后台-成绩检查-客观题检查 2.扫描端-考生图片上传 3.扫描端-客观题二次识别
     *
     * @param student
     * @return
     */
    @Override
    public boolean saveUploadStudent(MarkStudent student) {
        MarkStudent old = this.getById(student.getId());
        if (!student.getAbsent()) {// 正考
            MarkPaper markPaper = markPaperService.getByExamIdAndPaperNumber(student.getExamId(),
                    student.getPaperNumber());
            if (markPaper.getStatus().equals(MarkPaperStatus.FINISH)) {
                markPaperService.updateStatus(markPaper.getExamId(), markPaper.getPaperNumber(), MarkPaperStatus.FORMAL,
                        MarkPaperStatus.FINISH);
            }
        }
        calculateObjectiveScore(student);
        if (student.getAbsent()) {// 转缺考
            student.setObjectiveScore(null);
            student.setObjectiveScoreList(null);
        }
        if (!old.getAbsent() && student.getAbsent()) {// 正考转缺考
            student.setSubjectiveScore(null);
            student.setSubjectiveScoreList(null);
            student.setSubjectiveStatus(SubjectiveStatus.UNMARK);
            this.updateById(student);
        }
        boolean success = this.updateScanInfo(student);
        if (success) {
            markPaperService.updateUploadCount(student.getExamId(), student.getPaperNumber(),
                    this.countUploadedByExamIdAndPaperNumber(student.getExamId(), student.getPaperNumber()));
        }
        return success;
    }

    @Override
    public void calculateObjectiveScore(MarkStudent student) {
        // 缺考状态不统分(人工指定缺考、识别缺考)
        if (!ScanStatus.MANUAL_ABSENT.equals(student.getScanStatus()) && !student.getAbsent() && !student.getOmrAbsent()) {
            ScoreCalculateUtil util = ScoreCalculateUtil.instance(student);
            ScoreInfo info = util.calculate(markQuestionService.listQuestionByExamIdAndPaperNumberAndGroupNumber(
                    student.getExamId(), student.getPaperNumber(), null, true), null);
            student.setObjectiveScore(info.getObjectiveScore());
            student.setScoreList(info.getScoreList(), true);
            this.updateObjectiveScoreAndScoreList(student);
        }
    }

    @Override
    @Transactional
    public void updateStudentAndPaper(@NotNull SysUser user, @NotNull Long id,
                                      @NotNull List<ScanStudentPaper> studentPaperList) {
        if (CollectionUtils.isNotEmpty(studentPaperList)) {
            for (ScanStudentPaper studentPaper : studentPaperList) {
                studentPaper.setStudentId(id);
            }
        }
        // 清空原有绑定关系
        if (studentPaperService.removeByStudentId(id) > 0) {
            // 删除评卷任务
            MarkStudent student = this.getById(id);
            markService.deleteMarkTaskByStudent(student);
        }
        if (CollectionUtils.isNotEmpty(studentPaperList)) {
            // 保存绑定关系
            studentPaperService.saveOrUpdateBatch(studentPaperList);
        }
        // 更新考生状态
        updateStudentByPaper(user.getId(), id, true);
    }

    @Override
    public StudentVo findOne(StudentQuery query) {
        return baseMapper.findOne(query);
    }

    @Override
    public int countByExamIdAndSecretNumber(Long examId, String secretNumber) {
        QueryWrapper<MarkStudent> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(MarkStudent::getExamId, examId).eq(MarkStudent::getSecretNumber, secretNumber);
        return this.count(queryWrapper);
    }

    @Override
    public List<MarkStudent> listByExamIdAndCoursePaperId(Long examId, String coursePaperId) {
        QueryWrapper<MarkStudent> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(MarkStudent::getExamId, examId).eq(MarkStudent::getCoursePaperId, coursePaperId);
        return this.list(queryWrapper);
    }

    @Override
    public IPage<AnswerQueryVo> query(AnswerQueryDomain query) {
        SysUser sysUser = (SysUser) ServletUtil.getRequestUser();
        DataPermissionRule dpr = basicRoleDataPermissionService.findDataPermission(sysUser.getSchoolId(),
                sysUser.getId(), ServletUtil.getRequest().getServletPath());
        // 查询考生分页信息
        query.setMarkPaperStatus(MarkPaperStatus.FORMAL.name());
        IPage<AnswerQueryVo> iPage = baseMapper.queryPage(new Page<>(query.getPageNumber(), query.getPageSize()), query,
                dpr);
        if (CollectionUtils.isNotEmpty(iPage.getRecords())) {
            for (AnswerQueryVo vo : iPage.getRecords()) {
                if (vo.getIsAbsent() != null && vo.getIsAbsent()) {
                    vo.setExamStatus(ExamStatus.ABSENT);
                } else {
                    vo.setExamStatus(ExamStatus.OK);
                }
            }
        }
        if (CollectionUtils.isNotEmpty(iPage.getRecords()) && (query.getWithPaper() != null && query.getWithPaper())) {
            Map<Long, AnswerQueryVo> map = new HashMap<>();

            for (AnswerQueryVo vo : iPage.getRecords()) {
                List<AnswerPaperVo> papers = new ArrayList<>();
                vo.setPapers(papers);
                if (vo.getCardPaperCount() != null) {
                    for (int i = 1; i <= vo.getCardPaperCount(); i++) {
                        AnswerPaperVo pv = new AnswerPaperVo();
                        pv.setNumber(i);
                        papers.add(pv);
                    }
                }
                map.put(vo.getId(), vo);
            }
            // 根据考生id查找绑定paper
            List<Long> studentIds = iPage.getRecords().stream().map(p -> p.getId()).collect(Collectors.toList());
            List<StudentPaperVo> paperList = new BatchGetDataUtil<StudentPaperVo, Long>() {

                @Override
                public List<StudentPaperVo> getData(List<Long> paramList) {
                    return scanPaperService.listByStudentIds(paramList);
                }
            }.getDataForBatch(studentIds, 200);

            if (CollectionUtils.isNotEmpty(paperList)) {
                Map<Long, AnswerPaperVo> paperMap = new HashMap<>();
                for (StudentPaperVo p : paperList) {
                    AnswerQueryVo vo = map.get(p.getStudentId());
                    if (vo == null) {
                        continue;
                    }
                    List<AnswerPaperVo> papers = vo.getPapers();
                    if (papers == null) {
                        continue;
                    }
                    if (papers.size() < p.getNumber()) {
                        continue;
                    }
                    AnswerPaperVo pvo = papers.get(p.getNumber() - 1);
                    pvo.setId(p.getPaperId());
                    pvo.setNumber(p.getNumber());
                    pvo.setAssigned(p.getAssigned());
                    pvo.setInvalid(p.getInvalid());
                    paperMap.put(p.getPaperId(), pvo);
                }
                // 查找page
                List<Long> paperIds = paperList.stream().map(p -> p.getPaperId()).collect(Collectors.toList());
                List<ScanPaperPage> paperPageList = new BatchGetDataUtil<ScanPaperPage, Long>() {

                    @Override
                    public List<ScanPaperPage> getData(List<Long> paramList) {
                        return scanPaperPageService.listByPaperList(paramList);
                    }
                }.getDataForBatch(paperIds, 200);

                if (CollectionUtils.isNotEmpty(paperPageList)) {
                    for (ScanPaperPage p : paperPageList) {
                        AnswerPaperVo pvo = paperMap.get(p.getPaperId());
                        if (pvo == null) {
                            continue;
                        }
                        List<AnswerPageVo> pages = pvo.getPages();
                        if (pages == null) {
                            pages = new ArrayList<>();
                            pvo.setPages(pages);
                        }
                        AnswerPageVo pageVo = new AnswerPageVo();
                        pageVo.setIndex(p.getPageIndex());
                        pageVo.setSheetUri(teachcloudCommonService.filePreview(p.getSheetPath()));
                        if (query.getWithOmrDetail() != null && query.getWithOmrDetail()) {
                            pageVo.setAbsent(p.getAbsent() != null ? p.getAbsent().getResult() : null);
                            pageVo.setBreach(p.getBreach() != null ? p.getBreach().getResult() : null);
                            pageVo.setQuestion(p.getQuestion() != null ? p.getQuestion().getResult() : null);
                            pageVo.setRecogData(p.getRecogData());
                        }
                        pages.add(pageVo);
                    }
                }
            }
        }
        return iPage;
    }

    @Override
    public List<String> summary(AnswerQueryDomain query) {
        SysUser sysUser = (SysUser) ServletUtil.getRequestUser();
        DataPermissionRule dpr = basicRoleDataPermissionService.findDataPermission(sysUser.getSchoolId(),
                sysUser.getId(), ServletUtil.getRequest().getServletPath());

        // 不分页查询考生准考证号
        query.setMarkPaperStatus(MarkPaperStatus.FORMAL.name());
        return baseMapper.querySummary(query, dpr);
    }

    @Transactional
    @Override
    public UpdateTimeVo omrEdit(Long userId, OmrEditDomain domain) {
        MarkStudent student = findByExamIdAndCoursePaperIdAndStudentCode(domain.getExamId(), domain.getCoursePaperId(),
                domain.getStudentCode());
        if (student == null) {
            throw new ParameterException("考生信息未找到");
        }
        concurrentService.getReadWriteLock(LockType.STUDENT + "-" + student.getId()).writeLock().lock();
        try {
            for (OmrEditPaper paperEdit : domain.getPapers()) {
                ScanStudentPaper sp = studentPaperService.findByStudentIdAndPaperNumber(student.getId(),
                        paperEdit.getNumber());
                if (sp == null) {
                    throw new ParameterException("未找到绑定扫描结果");
                }
                ScanPaper paperEntity = scanPaperService.getById(sp.getPaperId());
                if (paperEntity == null) {
                    throw new ParameterException("未找到paper信息结果");
                }
                paperEntity.setUpdaterId(userId);
                paperEntity.setUpdateTime(System.currentTimeMillis());
                List<ScanPaperPage> pages = scanPaperPageService.listByPaperId(paperEntity.getId());
                for (ScanPaperPage pageEntity : pages) {
                    paperEdit.updatePage(pageEntity);
                }
                scanPaperService.savePaperAndPages(paperEntity, pages);
            }
            updateStudentByPaper(userId, student.getId(), true);
            return UpdateTimeVo.create();
        } finally {
            concurrentService.getReadWriteLock(LockType.STUDENT + "-" + student.getId()).writeLock().unlock();
        }
    }

    @Override
    public long countByExamIdAndPaperNumber(Long examId, String paperNumber, String paperType) {
        QueryWrapper<MarkStudent> queryWrapper = new QueryWrapper<>();
        LambdaQueryWrapper<MarkStudent> lambda = queryWrapper.lambda();
        lambda.eq(MarkStudent::getExamId, examId).eq(MarkStudent::getPaperNumber, paperNumber);
        if (StringUtils.isNotBlank(paperType)) {
            lambda.eq(MarkStudent::getPaperType, paperType);
        }
        return this.count(queryWrapper);
    }

    @Transactional
    @Override
    public AbsentManualUpdateVo absentManualUpdate(Long examId, String coursePaperId, String studentCode,
                                                   ScanStatus status) {
        MarkStudent student = findByExamIdAndCoursePaperIdAndStudentCode(examId, coursePaperId, studentCode);
        if (student == null) {
            throw new ParameterException("考生未找到");
        }
        if (ScanStatus.MANUAL_ABSENT.equals(status) && !ScanStatus.UNEXIST.equals(student.getScanStatus())) {
            throw new ParameterException("考生不是未扫描状态");
        }
        LambdaUpdateWrapper<MarkStudent> lw = new LambdaUpdateWrapper<>();
        lw.set(MarkStudent::getScanStatus, status);
        lw.set(MarkStudent::getManualAbsent, ScanStatus.MANUAL_ABSENT.equals(status));
        lw.set(MarkStudent::getAbsent, ScanStatus.MANUAL_ABSENT.equals(status));
        lw.eq(MarkStudent::getId, student.getId());
        update(lw);
        markPaperService.updateAbsentCount(examId, student.getPaperNumber(),
                this.countAbsentByExamIdAndPaperNumber(examId, student.getPaperNumber()));
        return AbsentManualUpdateVo.create(status);
    }

    @Transactional
    @Override
    public UpdateTimeVo confirm(Long examId, String coursePaperId, String studentCode, Boolean omrAbsent) {
        MarkStudent student = findByExamIdAndCoursePaperIdAndStudentCode(examId, coursePaperId, studentCode);
        LambdaUpdateWrapper<MarkStudent> lw = new LambdaUpdateWrapper<>();
        lw.set(MarkStudent::getOmrAbsentChecked, omrAbsent);
        if (omrAbsent) {
            lw.set(MarkStudent::getObjectiveScore, null);
            lw.set(MarkStudent::getObjectiveScoreList, null);
        } else {
            lw.set(MarkStudent::getOmrAbsent, false);
        }
        lw.eq(MarkStudent::getId, student.getId());
        update(lw);
        scanOmrTaskService.deleteByStudentId(student.getExamId(), student.getId());
        if (!omrAbsent) {
            calculateObjectiveScore(student);
            scanOmrTaskService.saveTask(student.getId());
        }
        markPaperService.updateUploadCount(examId, student.getPaperNumber(),
                this.countUploadedByExamIdAndPaperNumber(examId, student.getPaperNumber()));
        markPaperService.updateAbsentCount(examId, student.getPaperNumber(),
                this.countAbsentByExamIdAndPaperNumber(examId, student.getPaperNumber()));
        return UpdateTimeVo.create();
    }

    @Override
    public List<Long> findIdByExamIdAndPaperNumber(Long examId, String paperNumber) {
        return this.baseMapper.findIdByExamIdAndPaperNumber(examId, paperNumber);
    }

    @Override
    public Task getSubjectiveInspectedTask(Long studentId) {
        Task task = null;
        if (studentId != null) {
            Long userId = ServletUtil.getRequestUserId();
            MarkStudent markStudent = this.getById(studentId);
            markService.releaseByStudent(markStudent);
            if (markService.applyStudent(markStudent, userId)) {
                task = taskService.build(studentId);
            }
        }
        return task;
    }

    @Transactional
    @Override
    public void saveSubjectiveInspectedTask(MarkHeaderResult markResult) {
        MarkStudent markStudent = this.getById(markResult.getStudentId());
        if (markStudent == null) {
            throw ExceptionResultEnum.ERROR.exception("考生不存在");
        }
        MarkPaper markPaper = markPaperService.getByExamIdAndPaperNumber(markStudent.getExamId(),
                markStudent.getPaperNumber());
        // 评卷是否结束
        if (markPaper == null || MarkPaperStatus.FINISH.equals(markPaper.getStatus())) {
            throw ExceptionResultEnum.ERROR.exception("科目已结束评卷，无法打分");
        }
        try {
            lockService.watch(LockType.EXAM_SUBJECT, markStudent.getExamId(), markStudent.getPaperNumber());
            lockService.waitlock(LockType.STUDENT, markResult.getStudentId());
            markService.submitHeaderTask(markResult.getGroups(), markStudent);
        } catch (Exception e) {
            throw ExceptionResultEnum.ERROR.exception(e.getMessage());
        } finally {
            lockService.unlock(LockType.STUDENT, markResult.getStudentId());
            lockService.unwatch(LockType.EXAM_SUBJECT, markStudent.getExamId(), markStudent.getPaperNumber());
            markService.releaseByStudent(markStudent);
        }
    }

    @Override
    public IPage<ArchiveStudentVo> studentList(ArchiveStudentQuery query) {
        SysUser sysUser = (SysUser) ServletUtil.getRequestUser();
        // 使用成绩管理列表请求的数据权限
        String postUrl = "/api/admin/mark/archive/score/list";
        DataPermissionRule dpr = basicRoleDataPermissionService.findDataPermission(sysUser.getSchoolId(), sysUser.getId(), postUrl);
        Page<ArchiveStudentVo> page = new Page<>(query.getPageNumber(), query.getPageSize());
        IPage<ArchiveStudentVo> ret = baseMapper.studentList(page, sysUser.getSchoolId(), query, dpr);
        for (ArchiveStudentVo record : ret.getRecords()) {
            List<String> list = new ArrayList<>();
            List<FilePathVo> vos = JSON.parseArray(StringUtils.trimToNull(record.getSheetPath()), FilePathVo.class);
            if (CollectionUtils.isEmpty(vos)) {
                continue;
            }
            for (FilePathVo filePathVo : vos) {
                list.add(JSON.toJSONString(filePathVo));
            }
            record.setSheetUrls(teachcloudCommonService.filePreview(list));
        }
        return ret;
    }

    @Override
    public void scoreExport(ArchiveStudentQuery query, HttpServletResponse response) {
        Long schoolId = Long.valueOf(ServletUtil.getRequestHeaderSchoolId().toString());
        //生成表头
        String[] columnName = new String[]{"学生姓名", "学号", "学院", "班级", "课程代码", "课程名称", "客观分", "主观分",
                "成绩"};
        List<MarkQuestion> oQuestionList = markQuestionService.listQuestionByExamIdAndPaperNumberAndGroupNumber(query.getExamId(), query.getPaperNumber(), null, true);
        List<MarkQuestion> sQuestionList = markQuestionService.listQuestionByExamIdAndPaperNumberAndGroupNumber(query.getExamId(), query.getPaperNumber(), null, false);
        List<String> columnNameList = new ArrayList<>(Arrays.asList(columnName));
        for (MarkQuestion question : oQuestionList) {
            columnNameList.add(question.getMainTitle() + " " + question.getMainNumber() + "-" + question.getSubNumber() + "选项");
            columnNameList.add(question.getMainTitle() + " " + question.getMainNumber() + "-" + question.getSubNumber() + "得分");
        }
        for (MarkQuestion question : sQuestionList) {
            columnNameList.add(question.getMainTitle() + " " + question.getMainNumber() + "-" + question.getSubNumber());
        }
        String[] columnNames = columnNameList.toArray(new String[0]);
        //生成动态内容
        List<String[]> columnValues = new ArrayList<>();
        List<ArchiveStudentVo> ret = baseMapper.studentList(schoolId, query, null);
        for (ArchiveStudentVo s : ret) {
            List<String> valueList = new ArrayList<>();
            valueList.add(s.getStudentName());
            valueList.add(s.getStudentCode());
            valueList.add(s.getCollege());
            valueList.add(s.getClassName());
            valueList.add(s.getCourseCode());
            valueList.add(s.getCourseName());
            valueList.add(s.getObjectiveScore() == null ? "" : s.getObjectiveScore().toString());
            valueList.add(s.getSubjectiveScore() == null ? "" : s.getSubjectiveScore().toString());
            valueList.add(s.getTotalScore() == null ? "" : s.getTotalScore().toString());
            for (ScoreItem item : s.getScoreList(true, oQuestionList)) {
                valueList.add(item.getAnswer());
                valueList.add(item.getScore() == null ? "" : item.getScore().toString());
            }
            for (ScoreItem item : s.getScoreList(false, sQuestionList)) {
                valueList.add(item.getScore().toString());
            }
            String[] columnValue = valueList.toArray(new String[valueList.size()]);
            columnValues.add(columnValue);
        }
        try {
            log.debug("导出Excel开始...");
            response.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode("成绩导出", SystemConstant.CHARSET_NAME) + ".xlsx");
            response.setContentType("application/vnd.ms-excel");
            ServletOutputStream outputStream = response.getOutputStream();
            ExcelWriter writer = ExcelWriter.create(ExcelType.XLSX);
            writer.writeDataArrays("成绩导出", null, columnNames, columnValues.listIterator());
            writer.output(outputStream);
            outputStream.flush();
            outputStream.close();
            log.debug("导出Excel结束");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public ScoreReportVo scoreReport(ArchiveStudentQuery query) {
        SysUser sysUser = (SysUser) ServletUtil.getRequestUser();
        String postUrl = "/api/admin/mark/archive/score/list";
        DataPermissionRule dpr = basicRoleDataPermissionService.findDataPermission(sysUser.getSchoolId(), sysUser.getId(), postUrl);
        ScoreReportVo ret = new ScoreReportVo();
        ret.setOverview(baseMapper.overview(sysUser.getSchoolId(), query, dpr));
        if (ret.getOverview() != null && ret.getOverview().getStudentCount() > 0) {
            double total = ret.getOverview().getStudentCount() - ret.getOverview().getAbsentCount();
            ret.getOverview().setPassRate(
                    Calculator.divide2String(Calculator.multiply(ret.getOverview().getPassCount(), 100), total, 2));
            ret.getOverview().setExcellentRate(Calculator
                    .divide2String(Calculator.multiply(ret.getOverview().getExcellentCount(), 100), total, 2));
            ret.getOverview().setAvgScore(Calculator.round(ret.getOverview().getAvgScore(), 2));
        }
        List<ArchiveStudentVo> studentList = baseMapper.studentList(sysUser.getSchoolId(), query, dpr);
        fillScoreRange(ret, studentList);

        ret.setCollege(baseMapper.college(sysUser.getSchoolId(), query, dpr));
        if (CollectionUtils.isNotEmpty(ret.getCollege())) {
            for (CollegeVo vo : ret.getCollege()) {
                double total = vo.getStudentCount() - vo.getAbsentCount();
                vo.setPassRate(Calculator.divide2String(Calculator.multiply(vo.getPassCount(), 100), total, 2));
                vo.setExcellentRate(
                        Calculator.divide2String(Calculator.multiply(vo.getExcellentCount(), 100), total, 2));
                vo.setAvgScore(Calculator.round(vo.getAvgScore(), 2));
                vo.setMinScore(Calculator.round(vo.getMinScore(), 2));
                vo.setMaxScore(Calculator.round(vo.getMaxScore(), 2));
            }
        }

        ret.setClassData(baseMapper.classData(sysUser.getSchoolId(), query, dpr));
        if (CollectionUtils.isNotEmpty(ret.getClassData())) {
            for (ClassVo vo : ret.getClassData()) {
                double total = vo.getStudentCount() - vo.getAbsentCount();
                vo.setPassRate(Calculator.divide2String(Calculator.multiply(vo.getPassCount(), 100), total, 2));
                vo.setExcellentRate(
                        Calculator.divide2String(Calculator.multiply(vo.getExcellentCount(), 100), total, 2));
                vo.setAvgScore(Calculator.round(vo.getAvgScore(), 2));
                vo.setMinScore(Calculator.round(vo.getMinScore(), 2));
                vo.setMaxScore(Calculator.round(vo.getMaxScore(), 2));
            }
        }

        ret.setTeacher(baseMapper.teacher(sysUser.getSchoolId(), query, dpr));
        if (CollectionUtils.isNotEmpty(ret.getTeacher())) {
            for (TeacherVo vo : ret.getTeacher()) {
                double total = vo.getStudentCount() - vo.getAbsentCount();
                vo.setPassRate(Calculator.divide2String(Calculator.multiply(vo.getPassCount(), 100), total, 2));
                vo.setExcellentRate(
                        Calculator.divide2String(Calculator.multiply(vo.getExcellentCount(), 100), total, 2));
                vo.setAvgScore(Calculator.round(vo.getAvgScore(), 2));
                vo.setMinScore(Calculator.round(vo.getMinScore(), 2));
                vo.setMaxScore(Calculator.round(vo.getMaxScore(), 2));
            }
        }

        ret.setTeacherClass(baseMapper.teacherClass(sysUser.getSchoolId(), query, dpr));
        if (CollectionUtils.isNotEmpty(ret.getTeacherClass())) {
            for (TeacherClassVo vo : ret.getTeacherClass()) {
                double total = vo.getStudentCount() - vo.getAbsentCount();
                vo.setPassRate(Calculator.divide2String(Calculator.multiply(vo.getPassCount(), 100), total, 2));
                vo.setExcellentRate(
                        Calculator.divide2String(Calculator.multiply(vo.getExcellentCount(), 100), total, 2));
                vo.setAvgScore(Calculator.round(vo.getAvgScore(), 2));
            }
        }

        fillObjective(ret, studentList, query.getExamId(), query.getPaperNumber());
        List<Long> studentIds = studentList.stream().map(ArchiveStudentVo::getStudentId).collect(Collectors.toList());
        ret.setSubjective(markSubjectiveScoreService.getSubjectiveVo(studentIds));
        if (CollectionUtils.isNotEmpty(ret.getSubjective())) {
            for (QuestionVo vo : ret.getSubjective()) {
                double total = vo.getStudentCount();
                vo.setScoreRate(Calculator.divide(vo.getScoreCount(), total, 2));
                vo.setFullScoreRate(Calculator.divide(vo.getFullScoreCount(), total, 2));
                vo.setAvgScore(Calculator.round(vo.getAvgScore(), 2));
            }
        }
        return ret;
    }

    @Override
    public void exportUnexist(Long examId, String courseCode, String coursePaperId, HttpServletResponse response) {
        try {
            SysUser sysUser = (SysUser) ServletUtil.getRequestUser();
            DataPermissionRule dpr = basicRoleDataPermissionService.findDataPermission(sysUser.getSchoolId(),
                    sysUser.getId(), ServletUtil.getRequest().getServletPath());
            List<UnexistStudentDto> unexistStudentDtoList = this.baseMapper.listUnexistStudentByExamIdAndCoursePaperId(
                    examId, courseCode, coursePaperId, MarkPaperStatus.FORMAL.name(), dpr);
            ExcelUtil.excelExport("评卷员工作量", UnexistStudentDto.class, unexistStudentDtoList, response);
        } catch (Exception e) {
            throw ExceptionResultEnum.ERROR.exception("导出评卷员工作量失败");
        }
    }

    @Override
    public int countByExamIdAndPaperNumberAndMarkStatus(Long examId, String paperNumber, SubjectiveStatus status) {
        QueryWrapper<MarkStudent> queryWrapper = new QueryWrapper<>();
        LambdaQueryWrapper<MarkStudent> lambdaQueryWrapper = queryWrapper.lambda();
        lambdaQueryWrapper.eq(MarkStudent::getExamId, examId).eq(MarkStudent::getPaperNumber, paperNumber);
        if (status != null) {
            lambdaQueryWrapper.eq(MarkStudent::getSubjectiveStatus, status);
        }
        lambdaQueryWrapper.eq(MarkStudent::getUpload, true).eq(MarkStudent::getAbsent, false)
                .eq(MarkStudent::getBreach, false).eq(MarkStudent::getOmrAbsent, false);
        return this.count(queryWrapper);
    }

    @Override
    public void updateCheckInfo(Long studentId, Long userId) {
        UpdateWrapper<MarkStudent> updateWrapper = new UpdateWrapper<>();
        updateWrapper.lambda().set(MarkStudent::getCheckUserId, userId)
                .set(MarkStudent::getCheckTime, System.currentTimeMillis()).eq(MarkStudent::getId, studentId);
        this.update(updateWrapper);
    }

    @Override
    public int countOmrAbsentStudent(Long examId, String paperNumber, String paperType, boolean isOmrAbsentConfirm) {
        QueryWrapper<MarkStudent> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(MarkStudent::getExamId, examId).eq(MarkStudent::getPaperNumber, paperNumber)
                .eq(MarkStudent::getPaperType, paperType).eq(MarkStudent::getOmrAbsent, true)
                .eq(MarkStudent::getOmrAbsentChecked, isOmrAbsentConfirm);
        return this.count(queryWrapper);
    }

    @Override
    public void scoreReportDownload(JSONObject jsonObject, HttpServletResponse response) {
        String rootPath = null;
        File htmlFile;
        File pdfFile;
        try {
            // 本地保存目录
            File tempFile = SystemConstant.getFileTempDirVar(
                    System.currentTimeMillis() + File.separator + SystemConstant.getNanoId(),
                    SystemConstant.TEMP_PREFIX);
            rootPath = tempFile.getParent();

            // 删除临时目录中创建的临时文件
            if (Objects.nonNull(tempFile)) {
                tempFile.delete();
            }
            // html文件
            String cardHtmlPath = rootPath + File.separator + System.currentTimeMillis() + File.separator + "temp"
                    + SystemConstant.HTML_PREFIX;
            htmlFile = new File(cardHtmlPath);
            if (!htmlFile.exists()) {
                htmlFile.getParentFile().mkdirs();
                htmlFile.createNewFile();
            }
            // 生成html文件
            FileCopyUtils.copy(jsonObject.getString("htmlContent").getBytes(StandardCharsets.UTF_8), htmlFile);

            // pdf文件
            String cardPdfPath = rootPath + File.separator + System.currentTimeMillis() + File.separator + "temp"
                    + SystemConstant.PDF_PREFIX;
            pdfFile = new File(cardPdfPath);
            if (!pdfFile.exists()) {
                pdfFile.getParentFile().mkdirs();
                pdfFile.createNewFile();
            }
            HtmlToPdfUtil.convert(cardHtmlPath, cardPdfPath, PageSizeEnum.A4);
            FileUtil.outputFile(response, pdfFile, "报告" + SystemConstant.PDF_PREFIX);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (Objects.nonNull(rootPath)) {
                ConvertUtil.delFolder(rootPath);
            }
        }
    }

    @Override
    public void deleteByExamIdAndPaperNumber(Long examId, String paperNumber) {
        UpdateWrapper<MarkStudent> updateWrapper = new UpdateWrapper<>();
        updateWrapper.lambda().eq(MarkStudent::getExamId, examId).eq(MarkStudent::getPaperNumber, paperNumber);
        this.remove(updateWrapper);
    }

    @Override
    public boolean calcObjectiveScore(Long examId, String paperNumber) {
        MarkPaper markPaper = markPaperService.getByExamIdAndPaperNumber(examId, paperNumber);
        if (markPaper != null) {
            if (lockService.trylock(LockType.SCORE_CALCULATE, markPaper.getId())) {
                markSyncService.calcObjectiveScore(markPaper);
                return true;
            } else {
                throw ExceptionResultEnum.ERROR.exception("评卷员正在重置");
            }
        }
        return true;
    }

    @Override
    public IPage<MarkStudent> pageByExamAndPaperNumber(Long examId, String paperNumber, int pageNumber, int pageSize) {
        QueryWrapper<MarkStudent> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(MarkStudent::getExamId, examId).eq(MarkStudent::getPaperNumber, paperNumber)
                .orderByAsc(MarkStudent::getId);
        return this.page(new Page<>(pageNumber, pageSize), queryWrapper);
    }

    @Override
    public void updateObjectiveScoreAndScoreList(MarkStudent markStudent) {
        UpdateWrapper<MarkStudent> objectiveUpdateWrapper = new UpdateWrapper<>();
        LambdaUpdateWrapper<MarkStudent> lambdaUpdateWrapper = objectiveUpdateWrapper.lambda();
        lambdaUpdateWrapper.set(MarkStudent::getObjectiveScore, markStudent.getObjectiveScore())
                .set(MarkStudent::getObjectiveScoreList, markStudent.getObjectiveScoreList());
        if (markStudent.getCheckUserId() != null) {
            lambdaUpdateWrapper.set(MarkStudent::getCheckUserId, markStudent.getCheckUserId());
        }
        if (markStudent.getCheckTime() != null) {
            lambdaUpdateWrapper.set(MarkStudent::getCheckTime, markStudent.getCheckTime());
        }
        lambdaUpdateWrapper.eq(MarkStudent::getId, markStudent.getId());
        this.update(objectiveUpdateWrapper);
    }

    @Override
    public boolean updateAssignConfirm(Long studentId, boolean assignConfirm) {
        UpdateWrapper<MarkStudent> updateWrapper = new UpdateWrapper<>();
        updateWrapper.lambda().set(MarkStudent::getAssignConfirmed, assignConfirm).eq(MarkStudent::getId, studentId);
        return this.update(updateWrapper);
    }

    private void fillObjective(ScoreReportVo ret, List<ArchiveStudentVo> studentList, Long examId, String paperNumber) {
        List<MarkQuestion> qs = markQuestionService.listByExamIdAndPaperNumberAndPaperType(examId, paperNumber, null, true);
        Map<String, QuestionVo> map = new HashMap<>();
        List<QuestionVo> list = new ArrayList<>();
        for (ArchiveStudentVo s : studentList) {
            List<ScoreItem> sis = s.getScoreList(true, qs);
            if (CollectionUtils.isNotEmpty(sis)) {
                for (ScoreItem si : sis) {
                    String key = si.getMainNumber() + "-" + si.getSubNumber();
                    QuestionVo vo = map.get(key);
                    if (vo == null) {
                        vo = new QuestionVo();
                        vo.setScoreCount(0);
                        vo.setFullScoreCount(0);
                        vo.setStudentCount(0);
                        vo.setScoreSum(0.0);
                        vo.setMainNumber(si.getMainNumber());
                        vo.setSubNumber(si.getSubNumber());
                        vo.setTitle(si.getTitle());
                        vo.setScore(si.getTotalScore());
                        map.put(key, vo);
                    }
                    vo.setStudentCount(vo.getStudentCount() + 1);
                    vo.setScoreSum(vo.getScoreSum() + si.getScore());
                    if (Objects.nonNull(si.getScore()) && Objects.nonNull(si.getTotalScore()) && si.getScore().doubleValue() == si.getTotalScore().doubleValue()) {
                        vo.setFullScoreCount(vo.getFullScoreCount() + 1);
                    }
                    if (si.getScore() > 0) {
                        vo.setScoreCount(vo.getScoreCount() + 1);
                    }
                }
            }
        }
        if (map.isEmpty()) {
            ret.setObjective(list);
            return;
        }

        list = new ArrayList<>(map.values());
        for (QuestionVo questionVo : list) {
            // 平均分
            if (questionVo.getStudentCount() == null || questionVo.getStudentCount() == 0) {
                questionVo.setAvgScore(0D);
            } else {
                questionVo.setAvgScore(Calculator.round(Calculator.divide(questionVo.getScoreSum(), questionVo.getStudentCount()), 2));
            }
        }

        list.sort(Comparator.comparingInt(QuestionVo::getMainNumber).thenComparingInt(QuestionVo::getSubNumber));

        for (QuestionVo vo : list) {
            double total = vo.getStudentCount();
            vo.setScoreRate(Calculator.divide(vo.getScoreCount(), total, 2));
            vo.setFullScoreRate(Calculator.divide(vo.getFullScoreCount(), total, 2));
        }
        ret.setObjective(list);
    }

    private List<MarkStudent> listByExamIdAndPaperNumberAndNotAbsent(Long examId, String paperNumber) {
        QueryWrapper<MarkStudent> wrapper = new QueryWrapper<>();
        LambdaQueryWrapper<MarkStudent> lw = wrapper.lambda();
        lw.eq(MarkStudent::getExamId, examId);
        lw.eq(MarkStudent::getPaperNumber, paperNumber);
        lw.eq(MarkStudent::getUpload, true);
        lw.eq(MarkStudent::getAbsent, false);
        lw.eq(MarkStudent::getBreach, false);
        lw.eq(MarkStudent::getOmrAbsent, false);

        return this.list(wrapper);
    }

    private void fillScoreRange(ScoreReportVo ret, List<ArchiveStudentVo> list) {
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        List<ScoreRangeVo> scoreRange = new ArrayList<>();
        ret.setScoreRange(scoreRange);
        int total = ret.getOverview().getStudentCount() - ret.getOverview().getAbsentCount();
        scoreRange.add(getScoreRangeVo(total, list, 1.0, 9.5));
        scoreRange.add(getScoreRangeVo(total, list, 10.0, 19.5));
        scoreRange.add(getScoreRangeVo(total, list, 20.0, 29.5));
        scoreRange.add(getScoreRangeVo(total, list, 30.0, 39.5));
        scoreRange.add(getScoreRangeVo(total, list, 40.0, 49.5));
        scoreRange.add(getScoreRangeVo(total, list, 50.0, 59.5));
        scoreRange.add(getScoreRangeVo(total, list, 60.0, 69.5));
        scoreRange.add(getScoreRangeVo(total, list, 70.0, 79.5));
        scoreRange.add(getScoreRangeVo(total, list, 80.0, 89.5));
        scoreRange.add(getScoreRangeVo(total, list, 90.0, 100.0));
    }

    private ScoreRangeVo getScoreRangeVo(int totalCount, List<ArchiveStudentVo> list, Double start, Double end) {
        int count = (int) list.stream().filter(s -> s.getTotalScore() >= start && s.getTotalScore() <= end).count();
        Double rate = 0d;
        if (list.size() != 0 && totalCount != 0) {
            rate = Calculator.divide(count, totalCount, 2);
        }
        ScoreRangeVo vo = new ScoreRangeVo(count, start, end, Calculator.multiply(rate, 100, 0));
        return vo;
    }

    private int getCountByPaperNumber(Long examId, String paperNumber) {
        QueryWrapper<MarkStudent> wrapper = new QueryWrapper<>();
        LambdaQueryWrapper<MarkStudent> lw = wrapper.lambda();
        lw.eq(MarkStudent::getExamId, examId);
        lw.eq(MarkStudent::getPaperNumber, paperNumber);
        return this.count(wrapper);
    }

    @Override
    public int getAssignedCount(Long examId, Boolean checked, String courseCode, String coursePaperId,
                                MarkPaperStatus status, DataPermissionRule dpr) {
        MarkStudent markStudent = new MarkStudent();
        markStudent.setExamId(examId);
        markStudent.setCourseCode(courseCode);
        markStudent.setCoursePaperId(coursePaperId);
        markStudent.setAssigned(true);
        markStudent.setAssignConfirmed(checked);
        markStudent.setMarkPaperStatus(status.name());
        return baseMapper.countAssigned(markStudent, dpr);
    }

    @Override
    public int countAbsentByExamIdAndPaperNumber(Long examId, String paperNumber) {
        QueryWrapper<MarkStudent> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(MarkStudent::getExamId, examId).eq(MarkStudent::getPaperNumber, paperNumber)
                .and(o -> o.eq(MarkStudent::getAbsent, true).or().eq(MarkStudent::getOmrAbsent, true));
        return this.count(queryWrapper);
    }

    @Override
    public List<MarkStudent> listScanCollegeByExamIdAndCourseCodeAndCoursePaperId(Long examId, String courseCode,
                                                                                  String coursePaperId, String status, DataPermissionRule dpr) {
        return this.baseMapper.listScanCollegeByExamIdAndCourseCodeAndCoursePaperId(examId, courseCode, coursePaperId,
                status, dpr);
    }

    @Override
    public int countUnexistByExamIdAndPaperNumberAndPaperType(Long examId, String paperNumber, String paperType) {
        MarkStudent markStudent = new MarkStudent();
        markStudent.setExamId(examId);
        markStudent.setPaperNumber(paperNumber);
        markStudent.setScanStatus(ScanStatus.UNEXIST);
        markStudent.setPaperType(paperType);
        markStudent.setMarkPaperStatus(MarkPaperStatus.FORMAL.name());
        return baseMapper.selectCountByQuery(markStudent, null);
    }

    @Override
    public void updateStudentAnswer(Long studentId) {
        List<String> objectiveAnswers = new ArrayList<>();
        MarkStudent student = this.getById(studentId);
        List<ScanStudentPaper> studentPaperList = studentPaperService.findByStudentId(studentId);
        for (ScanStudentPaper studentPaper : studentPaperList) {
            List<ScanPaperPage> scanPaperPages = scanPaperPageService.listByPaperId(studentPaper.getPaperId());
            for (ScanPaperPage scanPaperPage : scanPaperPages) {
                if (scanPaperPage.getQuestion() != null
                        && CollectionUtils.isNotEmpty(scanPaperPage.getQuestion().getResult())) {
                    for (String s : scanPaperPage.getQuestion().getResult()) {
                        if (s.startsWith("?")) {
                            s = s.replace("?", "");
                        }
                        objectiveAnswers.add(s);
                    }
                }
            }
        }
        student.setAnswers(JSON.toJSONString(objectiveAnswers));
        this.saveOrUpdate(student);
        // 客观题统分
        this.calculateObjectiveScore(student);
    }

    @Override
    public List<MarkStudentScoreVo> listMarkStudentScoreList(Long examId, String paperNumber) {
        MarkPaper markPaper = markPaperService.getByExamIdAndPaperNumber(examId, paperNumber);
        if (!MarkPaperStatus.FINISH.equals(markPaper.getStatus())) {
            throw ExceptionResultEnum.ERROR.exception("科目未结束评卷，不能同步考生成绩");
        }

        List<MarkStudent> markStudentList = this.listByExamIdAndPaperNumberAndNotAbsent(examId, paperNumber);
        List<MarkQuestion> objectiveQuestionList = markQuestionService.listByExamIdAndPaperNumberAndPaperType(examId, paperNumber, null, true);
        List<MarkQuestion> subjectiveQuestionList = markQuestionService.listByExamIdAndPaperNumberAndPaperType(examId, paperNumber, null, false);
        List<MarkStudentScoreVo> markStudentScoreVoList = new ArrayList<>();
        for (MarkStudent markStudent : markStudentList) {
            MarkStudentScoreVo markStudentScoreVo = new MarkStudentScoreVo();
            markStudentScoreVo.setExamId(markStudent.getExamId());
            markStudentScoreVo.setCourseCode(markStudent.getCourseCode());
            markStudentScoreVo.setCourseName(markStudent.getCourseName());
            markStudentScoreVo.setPaperNumber(markStudent.getPaperNumber());
            markStudentScoreVo.setStudentCode(markStudent.getStudentCode());
            markStudentScoreVo.setStudentName(markStudent.getStudentName());
            // 客观题
            markStudentScoreVo.setObjectiveScore(markStudent.getObjectiveScore());
            markStudentScoreVo.setObjectiveScoreList(markStudent.getScoreList(true, objectiveQuestionList));
            // 主观题
            markStudentScoreVo.setSubjectiveScore(markStudent.getSubjectiveScore());
            markStudentScoreVo.setSubjectiveScoreList(markStudent.getScoreList(false, subjectiveQuestionList));

            // 总分
            markStudentScoreVo.setTotalScore(markStudent.getTotalScore());
            List<ScoreItem> totalScoreItemList = new ArrayList<>();
            totalScoreItemList.addAll(markStudentScoreVo.getObjectiveScoreList());
            totalScoreItemList.addAll(markStudentScoreVo.getSubjectiveScoreList());
            totalScoreItemList.stream().sorted(Comparator.comparing(ScoreItem::getMainNumber).thenComparing(ScoreItem::getSubNumber));
            markStudentScoreVo.setTotalScoreList(totalScoreItemList);
            markStudentScoreVoList.add(markStudentScoreVo);
        }
        return markStudentScoreVoList;
    }

    @Override
    public void trackExport(Long examId, String coursePaperId, HttpServletResponse response) {
        {
            List<MarkStudent> list = this.listByExamIdAndCoursePaperId(examId, coursePaperId);
            if (CollectionUtils.isNotEmpty(list)) {
                try {
                    response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("轨迹图.zip", "UTF-8"));
                    ZipWriter writer = ZipWriter.create(response.getOutputStream());
                    ByteArrayOutputStream os = new ByteArrayOutputStream();
                    for (MarkStudent s : list) {
                        if (!s.getUpload()) {
                            continue;
                        }
                        List<FilePathVo> vos = JSON.parseArray(s.getSheetPath(), FilePathVo.class);
                        List<MarkTrack> trackList = markTrackService.listByStudentId(s.getId());
                        Document document = new Document(new Rectangle(PageSize.A3.getHeight(), PageSize.A3.getWidth()));
                        // 本地保存目录
                        File tempFile = SystemConstant.getFileTempParentDirVar(SystemConstant.TEMP_PREFIX);
                        File file = new File(tempFile.getPath() + File.separator + s.getStudentCode() + ".pdf");
                        PdfWriter pdfWriter = PdfWriter.getInstance(document, new FileOutputStream(file));
                        document.open();
                        for (int i = 0; i < vos.size(); i++) {
                            FilePathVo vo = vos.get(i);
                            File sheet = fileUploadService.downloadFile(JSON.toJSONString(vo), vo.getPath());
                            File track = new File(tempFile.getPath() + File.separator + s.getStudentCode() + "-" + (i + 1) + ".jpg");
                            int offsetIndex = i + 1;
                            List<MarkTrack> tracks = trackList.stream().filter(t -> t.getOffsetIndex().equals(offsetIndex)).collect(Collectors.toList());
                            this.createTrack(sheet, track, tracks);
                            Image image = Image.getInstance(track.getPath());
                            image.scaleAbsolute(PageSize.A3.getHeight() - 100, PageSize.A3.getWidth() - 100);
                            if (i != 0) {
                                document.newPage();
                            }
                            document.add(image);
                            sheet.delete();
                            track.delete();
                        }
                        document.close();
                        pdfWriter.close();
                        writer.write(file, s.getStudentCode() + ".pdf");
                    }
                    writer.close();
                } catch (Exception e) {
                    e.printStackTrace();
                    throw new ParameterException("文件下载失败", e);
                }
            }
        }
    }

    private void createTrack(File sheet, File track, List<MarkTrack> trackList) throws IOException {
        {
            FileOutputStream output = null;
            try {
                BufferedImage image = ImageIO.read(new FileInputStream(sheet));
                image = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null).filter(image, null);
                Graphics2D g = image.createGraphics();// 得到图形上下文
                g.setColor(Color.red); // 设置画笔颜色
                // 设置字体
                g.setFont(new Font("微软雅黑", Font.LAYOUT_LEFT_TO_RIGHT, 30));
                // 写入签名
                if (trackList != null && !trackList.isEmpty()) {
                    for (int i = 0; i < trackList.size(); i++) {
                        MarkTrack t = trackList.get(i);
                        BigDecimal x = new BigDecimal(t.getOffsetX());
                        BigDecimal y = new BigDecimal(t.getOffsetY());
                        g.drawString(t.getScore().toString(), x.intValue(), y.intValue());
                    }
                }
                g.dispose();

                output = new FileOutputStream(track);
                ImageIO.write(image, "jpg", output);
            } catch (Exception e) {
                log.error(SystemConstant.LOG_ERROR, e);
            } finally {
                if (Objects.nonNull(output)) {
                    output.flush();
                    output.close();
                }
            }
        }
    }

    @Override
    public void sheetExport(Long examId, String coursePaperId, HttpServletResponse response) {
        List<MarkStudent> list = this.listByExamIdAndCoursePaperId(examId, coursePaperId);
        if (CollectionUtils.isNotEmpty(list)) {
            try {
                response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("原图.zip", "UTF-8"));
                ZipWriter writer = ZipWriter.create(response.getOutputStream());
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                for (MarkStudent s : list) {
                    if (!s.getUpload()) {
                        continue;
                    }
                    List<FilePathVo> vos = JSON.parseArray(s.getSheetPath(), FilePathVo.class);
                    for (int i = 0; i < vos.size(); i++) {
                        FilePathVo vo = vos.get(i);
                        File file = fileUploadService.downloadFile(JSON.toJSONString(vo), vo.getPath());
                        String format = FilenameUtils.getExtension(file.getName());
                        writer.write(file, s.getStudentCode() + "-" + (i + 1) + "." + format);
                        file.delete();
                    }
                }
                writer.close();
            } catch (Exception e) {
                e.printStackTrace();
                throw new ParameterException("文件下载失败", e);
            }
        }
    }

    @Override
    public void updateByBasicExamStudent(BasicExamStudent basicExamStudent, MarkPaper markPaper, Set<String> secretNumberSet) {
        MarkStudent markStudent = this.findByExamIdAndPaperNumberAndStudentCode(basicExamStudent.getExamId(), basicExamStudent.getPaperNumber(), basicExamStudent.getStudentCode());
        BasicCourse basicCourse = basicCourseService.getByCode(basicExamStudent.getCourseCode());
        BasicTeachClazz basicTeachClazz = this.baseMapper.getBasicTeachClazzById(basicExamStudent.getClazzId());
        SysUser sysUser = sysUserService.getById(basicExamStudent.getTeacherId());
        if (markStudent == null) {
            markStudent = new MarkStudent(basicExamStudent.getId(), basicExamStudent.getExamId(), basicExamStudent.getCourseCode(), basicCourse == null ? null : basicCourse.getName(), basicExamStudent.getPaperNumber(), markPaper.getCoursePaperId(), "A", basicExamStudent.getStudentCode(), basicExamStudent.getStudentName(), "", basicExamStudent.getExamPlace(), basicExamStudent.getExamRoom(), basicExamStudent.getCollege(), basicExamStudent.getMajor(), basicTeachClazz == null ? null : basicTeachClazz.getClazzName(), basicExamStudent.getTeacherId(), sysUser == null ? null : sysUser.getRealName(), basicExamStudent.getExamStartTime(), basicExamStudent.getExamEndTime(), markPaper.getUserId());
            if (markStudent.getSecretNumber() == null) {
                markStudent.randomSecretNumber();
                while (secretNumberSet.contains(markStudent.getSecretNumber())
                        || this.countByExamIdAndSecretNumber(markStudent.getExamId(), markStudent.getSecretNumber()) > 0) {
                    markStudent.randomSecretNumber();
                }
            }
        } else {
            markStudent.setStudentName(basicExamStudent.getStudentName());
            markStudent.setCollege(basicExamStudent.getCollege());
            markStudent.setMajorName(basicExamStudent.getMajor());
            markStudent.setClassName(basicTeachClazz == null ? null : basicTeachClazz.getClazzName());
            markStudent.setExamPlace(basicExamStudent.getExamPlace());
            markStudent.setExamRoom(basicExamStudent.getExamRoom());
            markStudent.setTeacherId(basicExamStudent.getTeacherId());
            markStudent.setTeacher(sysUser == null ? null : sysUser.getRealName());
        }
        this.saveOrUpdate(markStudent);
    }

    @Override
    public void deleteByExamIdAndPaperNumberAndStudentCode(Long examId, String paperNumber, String studentCode) {
        UpdateWrapper<MarkStudent> updateWrapper = new UpdateWrapper<>();
        updateWrapper.lambda().eq(MarkStudent::getExamId, examId)
                .eq(MarkStudent::getPaperNumber, paperNumber)
                .eq(MarkStudent::getStudentCode, studentCode);
        this.remove(updateWrapper);
    }

    @Override
    public Integer maxCardNumber(Long examId, String paperNumber, String paperType) {
        return this.baseMapper.maxCardNumber(examId, paperNumber, paperType);
    }

    @Override
    public void scoreDownload(ArchiveStudentQuery query, HttpServletResponse response) {
        Long schoolId = Long.valueOf(ServletUtil.getRequestHeaderSchoolId().toString());
        //生成表头
        String[] columnNames = new String[]{"序号", "班级", "学号", "姓名", "平时(必填)", "实验(必填)", "期末(必填)", "备注"};
        //生成动态内容
        List<String[]> columnValues = new ArrayList<>();
        List<ArchiveStudentVo> ret = baseMapper.studentList(schoolId, query, null);
        for (int i = 0; i < ret.size(); i++) {
            ArchiveStudentVo s = ret.get(i);
            List<String> valueList = new ArrayList<>();
            valueList.add(String.valueOf(i + 1));
            valueList.add(s.getClassName());
            valueList.add(s.getStudentCode());
            valueList.add(s.getStudentName());
            valueList.add("");
            valueList.add("");
            valueList.add(s.getTotalScore() == null ? "" : s.getTotalScore().toString());
            valueList.add("");
            String[] columnValue = valueList.toArray(new String[valueList.size()]);
            columnValues.add(columnValue);
        }
        try {
            String sheetName = "成绩下载";
            log.debug("导出Excel开始...");
            response.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode("成绩下载", SystemConstant.CHARSET_NAME) + ".xlsx");
            response.setContentType("application/vnd.ms-excel");
            ServletOutputStream outputStream = response.getOutputStream();
            ExcelWriter writer = ExcelWriter.create(ExcelType.XLSX);
            writer.writeDataArrays(sheetName, null, columnNames, columnValues.listIterator());

            CellStyle cellStyle1 = writer.createCellStyle();
            org.apache.poi.ss.usermodel.Font font1 = writer.createFont();
            font1.setBold(true);
            font1.setFontHeightInPoints((short) 20);
            cellStyle1.setFont(font1);
            cellStyle1.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
            cellStyle1.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            writer.setCellStyle(sheetName, cellStyle1, 0, new int[]{0, 1, 2, 3, 7});

            CellStyle cellStyle2 = writer.createCellStyle();
            org.apache.poi.ss.usermodel.Font font2 = writer.createFont();
            font2.setBold(true);
            font2.setFontHeightInPoints((short) 15);
            font2.setColor(IndexedColors.RED.getIndex());
            cellStyle2.setFont(font2);
            cellStyle2.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
            cellStyle2.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            writer.setCellStyle(sheetName, cellStyle2, 0, new int[]{4, 5, 6});

            writer.output(outputStream);
            outputStream.flush();
            outputStream.close();
            log.debug("导出Excel结束");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
