package com.qmth.exam.reserve.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qmth.boot.core.collection.PageResult;
import com.qmth.boot.core.concurrent.service.ConcurrentService;
import com.qmth.boot.core.exception.StatusException;
import com.qmth.boot.tools.excel.ExcelReader;
import com.qmth.boot.tools.excel.enums.ExcelType;
import com.qmth.boot.tools.excel.model.DataMap;
import com.qmth.boot.tools.io.ZipWriter;
import com.qmth.exam.reserve.bean.Constants;
import com.qmth.exam.reserve.bean.apply.ApplyRecordCacheBean;
import com.qmth.exam.reserve.bean.applytask.CurrentApplyTaskVO;
import com.qmth.exam.reserve.bean.category.CategoryCacheBean;
import com.qmth.exam.reserve.bean.examsite.ExamSiteCacheBean;
import com.qmth.exam.reserve.bean.login.LoginUser;
import com.qmth.exam.reserve.bean.org.OrgInfo;
import com.qmth.exam.reserve.bean.stdapply.*;
import com.qmth.exam.reserve.bean.timeperiod.TimePeriodExamSiteBean;
import com.qmth.exam.reserve.cache.CacheConstants;
import com.qmth.exam.reserve.cache.impl.ApplyTaskCacheService;
import com.qmth.exam.reserve.cache.impl.CategoryCacheService;
import com.qmth.exam.reserve.cache.impl.ExamSiteCacheService;
import com.qmth.exam.reserve.cache.impl.OrgCacheService;
import com.qmth.exam.reserve.dao.StudentApplyDao;
import com.qmth.exam.reserve.entity.*;
import com.qmth.exam.reserve.entity.base.BaseEntity;
import com.qmth.exam.reserve.enums.AsyncTaskType;
import com.qmth.exam.reserve.enums.CategoryLevel;
import com.qmth.exam.reserve.enums.EventType;
import com.qmth.exam.reserve.enums.Role;
import com.qmth.exam.reserve.service.*;
import com.qmth.exam.reserve.template.execute.AutoAssignStudentService;
import com.qmth.exam.reserve.template.execute.StudentApplyDetailExportService;
import com.qmth.exam.reserve.template.execute.StudentApplyNoFinishExportService;
import com.qmth.exam.reserve.util.DateUtil;
import com.qmth.exam.reserve.util.JsonHelper;
import com.qmth.exam.reserve.util.PageUtil;
import com.qmth.exam.reserve.util.UnionUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.redisson.api.RLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;

import static org.apache.commons.lang3.time.DateUtils.isSameDay;

/**
 * @Description  管理端-预约
 */
@Service
public class StudentApplyServiceImpl extends ServiceImpl<StudentApplyDao, StudentApplyEntity> implements StudentApplyService {

    private static final Logger log = LoggerFactory.getLogger(StudentApplyServiceImpl.class);

    private static final String[] EXCEL_HEADER = new String[]{"学号", "姓名", "证件号", "所属教学点", "预约考点1", "预约时段1", "预约考点2", "预约时段2", "预约考点3",
            "预约时段3", "预约考点4", "预约时段4"};

    private static final int maxApplyNum = 4;

    @Autowired
    private ApplyTaskService applyTaskService;

    @Autowired
    private ExamSiteService examSiteService;

    @Autowired
    private TimePeriodService timePeriodService;

    @Autowired
    private ConcurrentService concurrentService;

    @Autowired
    private ApplyTaskCacheService cacheService;

    @Autowired
    private OperateLogService operateLogService;

    @Autowired
    private CategoryService categoryService;

    @Autowired
    private StudentService studentService;

    @Autowired
    private ExamSiteCacheService examSiteCacheService;

    @Autowired
    private CategoryCacheService categoryCacheService;

    @Autowired
    private MaterialGenerateService materialGenerateService;

    @Autowired
    private ExamRoomService examRoomService;

    @Autowired
    private AsyncTaskService asyncTaskService;

    @Autowired
    private StudentApplyNoFinishExportService studentApplyNoFinishExportService;

    @Autowired
    private StudentApplyDetailExportService studentApplyDetailExportService;

    @Autowired
    private AutoAssignStudentService autoAssignStudentService;

    @Autowired
    private OrgCacheService orgCacheService;

    @Override
    public PageResult<StudentApplyVO> page(StudentApplyReq req) {
        ApplyTaskEntity task = getApplyTask();
        req.setTaskId(task.getId());
        if (req.getTeachingId() != null) {
            List<CategoryVO> examSiteList = examSiteService.listExamSite(req.getTeachingId(), Boolean.FALSE);
            if (examSiteList == null || examSiteList.isEmpty()) {
                return new PageResult<>();
            }
            List<Long> examSiteIds = examSiteList.stream().map(CategoryVO::getId).collect(Collectors.toList());
            req.setExamSiteIds(examSiteIds.isEmpty() ? null : examSiteIds);
        }

        IPage<StudentApplyVO> iPage = baseMapper.page(new Page<>(req.getPageNumber(), req.getPageSize()), req);
        List<StudentApplyVO> records = iPage.getRecords();
        for (StudentApplyVO record : records) {
            AgentAndRoomVO agentRoom = baseMapper.getAgentAndRoom(record.getId());
            record.setAgentName(agentRoom.getAgentName());
            record.setTeachingName(agentRoom.getTeachingName());
            record.setApplyTeachingName(agentRoom.getApplyTeachingName());
            record.setRoomName(agentRoom.getRoomName());
            record.setUserName(agentRoom.getUserName());

            // 是否可取消
            TimePeriodEntity timePeriod = timePeriodService.getById(record.getTimePeriodId());
            Date applyDate = DateUtils.truncate(new Date(timePeriod.getStartTime()), Calendar.DATE);
            Date canCancelDay = DateUtil.addValues(applyDate, Calendar.DAY_OF_MONTH, -task.getAllowApplyCancelDays());
            if (new Date().after(canCancelDay)) {
                record.setCanCancelFlag(Boolean.FALSE);
            } else {
                record.setCanCancelFlag(Boolean.TRUE);
            }
        }
        return PageUtil.of(iPage);
    }

    private ApplyTaskEntity getApplyTask() {
        LambdaQueryWrapper<ApplyTaskEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ApplyTaskEntity::getEnable, Boolean.TRUE);
        ApplyTaskEntity task = applyTaskService.getOne(wrapper);
        if (task == null) {
            throw new StatusException("未开启预约任务");
        }
        return task;
    }

    @Transactional
    @Override
    public void cancel(LoginUser user, Long id) {
        StudentApplyEntity studentApplyEntity = getById(id);
        if (studentApplyEntity == null) {
            log.warn("[cancel] 考生预约信息不存在, id:{} userAccount:{}", id, user.getAccount());
            throw new StatusException("没有预约信息");
        }
        if (studentApplyEntity.getTimePeriodId() == null) {
            log.warn("[cancel] 考生预约的时段为空, id:{} userAccount:{}", id, user.getAccount());
            throw new StatusException("没有预约信息");
        }
        TimePeriodEntity timePeriod = timePeriodService.getById(studentApplyEntity.getTimePeriodId());
        if (timePeriod == null) {
            log.warn("[cancel] 考生预约的时段不存在, timePeriodId:{} userAccount:{}", studentApplyEntity.getTimePeriodId(), user.getAccount());
            throw new StatusException("考试时段不存在");
        }
        ApplyTaskEntity task = getApplyTask();
        Date applyDate = DateUtils.truncate(new Date(timePeriod.getStartTime()), Calendar.DATE);
        Date canCancelDay = DateUtil.addValues(applyDate, Calendar.DAY_OF_MONTH, -task.getAllowApplyCancelDays());
        if (new Date().after(canCancelDay)) {
            throw new StatusException("[cancel] 可取消时间已过，无法在取消");
        }

        StudentEntity student = studentService.getById(studentApplyEntity.getStudentId());
        if(student == null) {
            log.warn("[cancel] 考生不存在, studentId:{} ",  studentApplyEntity.getStudentId());
            throw new StatusException("考生不存在");
        }

        // 教学点管理员，只能取消本教学点的考生
        if (user.getRole().equals(Role.TEACHING) && !user.getCategoryId().equals(student.getCategoryId())) {
            throw new StatusException("[cancel] 只能取消本教学点的考生");
        }

        String studentApplyLockKey = String.format(CacheConstants.LOCK_STUDENT_APPLY, studentApplyEntity.getStudentId());
        RLock studentApplyLock = (RLock) concurrentService.getLock(studentApplyLockKey);
        try {
            if (!studentApplyLock.tryLock()) {
                log.warn("[cancel] 获取锁失败，同一个考生不允许同时操作取消预约, lockKey:{}", studentApplyLockKey);
                throw new StatusException(Constants.SYSTEM_BUSY);
            } else {
                //更新预约取消标志
                studentApplyEntity.setCancel(Boolean.TRUE);
                updateById(studentApplyEntity);

                ApplyRecordCacheBean bean = new ApplyRecordCacheBean();
                bean.setStudentId(studentApplyEntity.getStudentId());
                bean.setExamSiteId(studentApplyEntity.getExamSiteId());
                bean.setTimePeriodId(studentApplyEntity.getTimePeriodId());
                bean.setCancel(Boolean.TRUE);
                bean.setOperateId(user.getId());
                bean.setOperateTime(System.currentTimeMillis());
                bean.setBizId(cacheService.increaseBizId());

                // 推送至预约队列
                /*boolean pushSuccess = cacheService.pushStudentApplyRecordQueue(bean);
                if (!pushSuccess) {
                    log.error("[cancel] 预约消息推送失败，id:{}", id);
                    throw new RuntimeException("取消失败，请稍后再试！");
                }*/
                // 保存至预约缓存
                cacheService.saveStudentApplyRecord(bean);
                // 某考点某时段的“剩余可约数量”（归还1个被占数量）
                cacheService.increaseApplyAvailableCount(studentApplyEntity.getExamSiteId(), studentApplyEntity.getTimePeriodId());
            }
        } catch (Exception e) {
            log.error("[cancel] 取消预约失败, msg:{}", e.getMessage());
            throw new StatusException("取消预约失败，请稍后再试");
        } finally {
            try {
                if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) {
                    studentApplyLock.unlock();
                    log.info("[cancel] 解锁成功,lockKey:{}", studentApplyLock);
                }
            } catch (Exception e) {
                log.warn(e.getMessage());
            }
        }
        //写入日志
        operateLogService.insertOperateLog(user.getId(), EventType.CANCEL_APPLY, JsonHelper.toJson(studentApplyEntity));
    }

    @Transactional
    @Override
    public List<Map<String, Object>> importPreExam(LoginUser user, Long teachingId, Integer level, InputStream inputStream) {
        checkOpenTime(user.getOrgId());
        List<DataMap> lineList;
        ExcelReader reader = ExcelReader.create(ExcelType.XLSX, inputStream, 0);
        try {
            lineList = reader.getDataMapList();
        } catch (Exception e) {
            log.warn("[importPreExam] 读取预考数据失败,msg:{}", e.getMessage());
            throw new StatusException("Excel解析失败,请检查文件格式");
        }
        if (!Arrays.equals(EXCEL_HEADER, reader.getColumnNames())) {
            throw new StatusException("Excel表头错误,请不要修改模版中的表头");
        }
        if (CollectionUtils.isEmpty(lineList)) {
            throw new StatusException("Excel中没有任何考生预约数据");
        }

        List<Map<String, Object>> failRecords = new ArrayList<>();
        RLock lock = (RLock) concurrentService.getLock(CacheConstants.LOCK_AUTO_APPLY);

        try {
            if (!lock.tryLock()) {
                log.warn("[importPreExam] 其他教学点正在执行预约，获取锁失败！lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
                throw new StatusException("系统繁忙,请稍后重试！");
            }
            log.warn("[importPreExam] 获取锁成功！lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);

            CurrentApplyTaskVO task = cacheService.currentApplyTask(user.getOrgId());
            // 开放式预约开始时间
            Long openSelfStartTime = task.getOpenApplyStartTime();
            // 开放式预约结束时间
            Long openSelfEndTime = task.getOpenApplyEndTime();
            long now = System.currentTimeMillis();
            List<StudentImportVO> applyList = new ArrayList<>();
            //教学点缓存
            Map<String, Long> teachingCache = getTeachingCache(level);
            //考点缓存
            Map<String, Long> examSiteCache = getExamSiteCache();
            //考试时段缓存
            Map<String, TimePeriodEntity> timePeriodCache = getTimePeriodCache();
            //考点时段缓存
            Map<Long, Map<String, TimePeriodEntity>> timePeriodExamSiteCache = getTimePeriodExamSiteCache(task.getTaskId());
            //考点所属教学点缓存
            Map<Long, Long> examSiteCategoryCache = getExamSiteCategoryCache();

            for (int i = 0; i < lineList.size(); i++) {
                DataMap line = lineList.get(i);
                List<AgentAndTimeVO> agentTimeList = new ArrayList<>();
                AgentAndTimeVO agentTime = new AgentAndTimeVO();
                StudentImportVO apply = new StudentImportVO();
                StringBuilder msg = new StringBuilder();

                String studentCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[0]));
                if (StringUtils.isBlank(studentCode)) {
                    msg.append(" 学号不能为空");
                }

                String name = trimAndNullIfBlank(line.get(EXCEL_HEADER[1]));
                if (StringUtils.isBlank(name)) {
                    msg.append(" 姓名不能为空");
                }

                String identityNumber = trimAndNullIfBlank(line.get(EXCEL_HEADER[2]));
                if (StringUtils.isBlank(identityNumber)) {
                    msg.append(" 证件号码不能为空");
                }

                //判断考生是否存在
                StudentEntity studentEntity = checkStudent(studentCode, name, identityNumber, task.getTaskId());
                if (studentEntity == null) {
                    msg.append(" 考生信息填写错误");
                    failRecords.add(newError(i + 1, msg.toString()));
                    continue;
                } else {
                    apply.setStudentId(studentEntity.getId());
                    apply.setApplyNumber(studentEntity.getApplyNumber());
                }

                String teachingName = trimAndNullIfBlank(line.get(EXCEL_HEADER[3]));
                if (StringUtils.isBlank(teachingName)) {
                    msg.append(" 考生所属教学点不能为空");
                }
                Long categoryId = teachingCache.get(teachingName);
                if (categoryId == null) {
                    msg.append(" 考生所属教学点不存在");
                    failRecords.add(newError(i + 1, msg.toString()));
                    continue;
                }
                if (!studentEntity.getCategoryId().equals(categoryId)) {
                    msg.append(" 导入考生的所属教学点和系统中的考生教学点不匹配");
                }
                if (user.getRole().equals(Role.TEACHING) && !categoryId.equals(teachingId)) {
                    msg.append(" 非本教学点考生");
                }

                String examSiteName = trimAndNullIfBlank(line.get(EXCEL_HEADER[4]));
                if (StringUtils.isBlank(examSiteName)) {
                    msg.append(" 预约考点1不能为空");
                }
                //考点所属教学点
                Long applyTeachingId;
                Long examSiteId = examSiteCache.get(examSiteName);
                if (examSiteId == null) {
                    msg.append(" 预约考点1不存在");
                } else {
                    applyTeachingId = examSiteCategoryCache.get(examSiteId);
                    if ((now < openSelfStartTime || now > openSelfEndTime) && !categoryId.equals(applyTeachingId)) {
                        msg.append(" 未到自由预约时间,不能预约其他教学点下的考点");
                    }
                    //教学点管理员导入-不能预约其他教学点下的考点
                    if (user.getRole().equals(Role.TEACHING) && !categoryId.equals(applyTeachingId)) {
                        msg.append(" 不能预约其他教学点下的考点");
                    }
                }

                //预约的时段
                String timePeriodStr = trimAndNullIfBlank(line.get(EXCEL_HEADER[5]));
                if (StringUtils.isBlank(timePeriodStr)) {
                    msg.append(" 预约时段1不能为空");
                }
                TimePeriodEntity timePeriodEntity;
                Map<String, TimePeriodEntity> examSiteTimePeriodMap;
                try {
                    //考点下的预约时段
                    examSiteTimePeriodMap = timePeriodExamSiteCache.get(examSiteId);
                    if (examSiteTimePeriodMap != null) {
                        timePeriodCache = examSiteTimePeriodMap;
                    }
                    timePeriodEntity = checkTimePeriod(timePeriodStr, timePeriodCache);
                    agentTime.setAgentId(examSiteId);
                    agentTime.setTimePeriodId(timePeriodEntity.getId());
                    agentTime.setStartTime(timePeriodEntity.getStartTime());
                    agentTimeList.add(agentTime);
                } catch (StatusException e) {
                    msg.append(" ").append(e.getMessage());
                }

                int index = 0;
                for (int num = 1; num <= maxApplyNum; num++) {
                    String agentName = trimAndNullIfBlank(line.get(EXCEL_HEADER[num + 5 + index]));
                    String timePeriodName = trimAndNullIfBlank(line.get(EXCEL_HEADER[num + 6 + index]));
                    if (StringUtils.isBlank(agentName) && StringUtils.isBlank(timePeriodName)) {
                        apply.setAgentTimeList(agentTimeList);
                        applyList.add(apply);
                        if (msg.length() > 0) {
                            failRecords.add(newError(i + 1, msg.toString()));
                        }
                        break;
                    } else {
                        examSiteId = examSiteCache.get(agentName);
                        if (examSiteId == null) {
                            msg.append(" 预约考点").append(num + 1).append("不存在");
                        } else {
                            applyTeachingId = examSiteCategoryCache.get(examSiteId);
                            if ((now < openSelfStartTime || now > openSelfEndTime) && !applyTeachingId.equals(categoryId)) {
                                msg.append(" 未到自由预约时间，不允许预约其他教学点的考点");
                            }
                            //教学点管理员导入-不能预约其他教学点下的考点
                            if (user.getRole().equals(Role.TEACHING) && !categoryId.equals(applyTeachingId)) {
                                msg.append(" 不能预约其他教学点下的考点");
                            }
                        }
                        try {
                            //考点下的预约时段
                            examSiteTimePeriodMap = timePeriodExamSiteCache.get(examSiteId);
                            if (examSiteTimePeriodMap != null) {
                                timePeriodCache = examSiteTimePeriodMap;
                            }
                            timePeriodEntity = checkTimePeriod(timePeriodName, timePeriodCache);
                            agentTime = new AgentAndTimeVO();
                            agentTime.setAgentId(examSiteId);
                            agentTime.setTimePeriodId(timePeriodEntity.getId());
                            agentTime.setStartTime(timePeriodEntity.getStartTime());
                            agentTimeList.add(agentTime);
                        } catch (StatusException e) {
                            msg.append(" ").append(e.getMessage());
                        }
                    }

                    ++index;
                }

                //检测考生是否已经完成预约和超出预约的最大次数
                checkStudentTimePeriod(apply, i, task.getAllowApplyCancelDays(), failRecords);
            }

            //考点容量判断
            checkExamSiteTimeCapacity(applyList, failRecords);

            if (CollectionUtils.isNotEmpty(failRecords)) {
                return failRecords;
            }

            //保存数据
            for (int i = 0; i < applyList.size(); i++) {
                StudentImportVO vo = applyList.get(i);
                try {
                    List<AgentAndTimeVO> agentTimeList = vo.getAgentTimeList();
                    String studentApplyLockKey = String.format(CacheConstants.LOCK_STUDENT_APPLY, vo.getStudentId());
                    RLock studentApplyLock = (RLock) concurrentService.getLock(studentApplyLockKey);
                    try {
                        if (!studentApplyLock.tryLock()) {
                            log.warn("[importPreExam] 获取锁失败,考生在同时操作预约,lockKey:{}", studentApplyLockKey);
                        } else {
                            log.warn("[importPreExam] 获取锁成功,lockKey:{}", studentApplyLockKey);
                            for (AgentAndTimeVO agentTimeVO : agentTimeList) {
                                StudentApplyEntity entity = new StudentApplyEntity();
                                entity.setStudentId(vo.getStudentId());
                                entity.setExamSiteId(agentTimeVO.getAgentId());
                                entity.setTimePeriodId(agentTimeVO.getTimePeriodId());
                                entity.setCancel(Boolean.FALSE);
                                entity.setOperateId(user.getId());
                                //保存考生预约
                                saveOrUpdateStudentApply(entity);

                                // 队列bean
                                ApplyRecordCacheBean bean = new ApplyRecordCacheBean();
                                bean.setStudentId(vo.getStudentId());
                                bean.setExamSiteId(agentTimeVO.getAgentId());
                                bean.setTimePeriodId(agentTimeVO.getTimePeriodId());
                                bean.setCancel(Boolean.FALSE);
                                bean.setOperateId(user.getId());
                                bean.setOperateTime(System.currentTimeMillis());
                                bean.setBizId(cacheService.increaseBizId());

                                // 某考点某时段的“剩余可约数量”（抢占1个数量）
                                boolean takeSuccess = cacheService.decreaseApplyAvailableCount(bean.getExamSiteId(), bean.getTimePeriodId());
                                if (!takeSuccess) {
                                    log.warn("[importPreExam] 预约失败，当前预约时段已约满！examSiteId:{} timePeriodId:{} studentId:{}",
                                            bean.getExamSiteId(), bean.getTimePeriodId(), bean.getStudentId());
                                    //考点
                                    ExamSiteCacheBean examSiteBean = examSiteCacheService.getExamSiteById(bean.getExamSiteId());
                                    //时段
                                    TimePeriodEntity timePeriod = timePeriodService.getById(bean.getTimePeriodId());
                                    String message = MessageFormat.format("第{0}行,预约考点:{1},预约时段:{2}", i + 1, examSiteBean.getExamSiteName(),
                                            DateUtil.getStartAndEndTime(timePeriod.getStartTime(), timePeriod.getEndTime()));
                                    throw new StatusException(message);
                                }

                                // 保存至预约缓存
                                cacheService.saveStudentApplyRecord(bean);
                            }
                        }
                    } catch (Exception e) {
                        log.error("[importPreExam] 导入预约失败,msg:{}", e.getMessage());
                        throw new StatusException("导入预考失败,错误原因:" + e.getMessage());
                    } finally {
                        try {
                            if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) {
                                studentApplyLock.unlock();
                                log.info("[importPreExam] 解锁成功！lockKey:{}", studentApplyLockKey);
                            }
                        } catch (Exception e) {
                            log.warn(e.getMessage());
                        }
                    }

                } catch (StatusException e) {
                    failRecords.add(newError(i + 1, "系统异常"));
                    log.error("[importPreExam] 导入异常", e);
                }
            }

        } catch (StatusException e) {
            log.error("[importPreExam] 导入预考失败,msg:{}", e.getMessage());
            throw new StatusException(e.getMessage());
        } finally {
            try {
                // 解锁前检查当前线程是否持有该锁
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                    lock.unlock();
                    log.info("解锁成功！lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
                }
            } catch (Exception e) {
                log.warn(e.getMessage());
            }
        }
        return failRecords;
    }




    private Map<Long, Map<String, TimePeriodEntity>> getTimePeriodExamSiteCache(Long taskId) {
        //学校管理员设置的所有预约时段
        List<TimePeriodExamSiteBean> timePeriodList = timePeriodService.listTimePeriodByTask(taskId);

        Map<Long, Map<String, TimePeriodEntity>> timePeriodExamSiteCache = new HashMap<>();
        List<TimePeriodExamSiteBean> timePeriodExamSiteBeans = timePeriodService.listTimePeriodByExamSiteId(taskId, null);

        /*// 过滤开启的预约时段
        timePeriodExamSiteBeans = timePeriodExamSiteBeans.stream()
                .filter(TimePeriodExamSiteBean::getEnable)
                .collect(Collectors.toList());*/

        // 按照考点分组
        Map<Long, List<TimePeriodExamSiteBean>> timePeriodMap = timePeriodExamSiteBeans.stream()
                .collect(Collectors.groupingBy(TimePeriodExamSiteBean::getExamSiteId));

        Set<Map.Entry<Long, List<TimePeriodExamSiteBean>>> entries = timePeriodMap.entrySet();
        for (Map.Entry<Long, List<TimePeriodExamSiteBean>> entry : entries) {
            Long examSiteId = entry.getKey();
            List<TimePeriodExamSiteBean> timePeriodExamSiteBeansList = entry.getValue();

            //取并集
            timePeriodExamSiteBeansList = UnionUtil.unionByAttribute(timePeriodExamSiteBeansList, timePeriodList, TimePeriodExamSiteBean::getTimePeriodId);

            // 过滤开启的预约时段
            timePeriodExamSiteBeansList = timePeriodExamSiteBeansList.stream()
                    .filter(TimePeriodExamSiteBean::getEnable)
                    .collect(Collectors.toList());

            // 时段转换为Map key: start_time + end_time
            Map<String, TimePeriodEntity> map = getStringTimePeriodEntityMap(timePeriodExamSiteBeansList);

            timePeriodExamSiteCache.put(examSiteId, map);
        }

        return timePeriodExamSiteCache;
    }

    private Map<String, TimePeriodEntity> getStringTimePeriodEntityMap(List<TimePeriodExamSiteBean> timePeriodExamSiteBeansList) {
        Map<String, TimePeriodEntity> map = new HashMap<>(timePeriodExamSiteBeansList.size());
        for (TimePeriodExamSiteBean bean : timePeriodExamSiteBeansList) {
            TimePeriodEntity entity = new TimePeriodEntity();
            entity.setId(bean.getTimePeriodId());
            entity.setStartTime(bean.getStartTime());
            map.put(bean.getStartTime() + "-" + bean.getEndTime(), entity);
        }
        return map;
    }

    private StudentApplyEntity findStudentApply(StudentApplyEntity studentApply) {
        LambdaQueryWrapper<StudentApplyEntity> lm = new LambdaQueryWrapper<>();
        lm.eq(StudentApplyEntity::getExamSiteId, studentApply.getExamSiteId());
        lm.eq(StudentApplyEntity::getTimePeriodId, studentApply.getTimePeriodId());
        lm.eq(StudentApplyEntity::getStudentId, studentApply.getStudentId());
        return baseMapper.selectOne(lm);
    }


    private void checkStudentTimePeriod(StudentImportVO studentImportVO, int row, int allowCancelDays, List<Map<String, Object>> failRecords) {
        List<AgentAndTimeVO> agentTimeList = studentImportVO.getAgentTimeList();

        //是否重复预约时段
        boolean isDuplicate = agentTimeList.stream()
                .collect(Collectors.groupingBy(AgentAndTimeVO::getTimePeriodId, Collectors.counting()))
                .entrySet()
                .stream()
                .anyMatch(entry -> entry.getValue() > 1);
        if (isDuplicate) {
            failRecords.add(newError(row + 1, "同一个考生预约的时间不能相同"));
        }
        //预约的日期，是否在可取消日期之后
        LocalDate day = LocalDate.now().plusDays(allowCancelDays);
        long epochMilli = day.plusDays(1).atStartOfDay(ZoneId.systemDefault()).minusSeconds(1).toInstant().toEpochMilli();
        for (AgentAndTimeVO time : agentTimeList) {
            if (time.getStartTime() <= epochMilli) {
                failRecords.add(newError(row + 1, "预约的考试时段，只能为" + allowCancelDays + "天之后的时段"));
            }
        }

      /*  数据库的方式
      List<StudentApplyEntity> haveApplyList = listStudentHaveApply(studentImportVO.getStudentId());
        int studentApplyFinishCount = haveApplyList.size();*/

        // 考生已经预约的次数
        int studentApplyFinishCount = cacheService.getStudentApplyFinishCount(studentImportVO.getStudentId());
        List<AgentAndTimeVO> toBeApplyTimeList = new ArrayList<>();

        //考生已完成预约
        if (studentApplyFinishCount >= studentImportVO.getApplyNumber()) {
            studentImportVO.setAgentTimeList(toBeApplyTimeList);
            failRecords.add(newError(row + 1, "考生已完成预约,请先取消考生已预约的信息"));
        }

        //判断是否超过可预约次数
        if (studentImportVO.getApplyNumber() < studentApplyFinishCount + agentTimeList.size()) {
            failRecords.add(newError(row + 1, "超过考生可预约的最大数量:" + studentImportVO.getApplyNumber()));
        }
    }

    private void checkExamSiteTimeCapacity(List<StudentImportVO> applyList, List<Map<String, Object>> failRecords) {
        try {
            //导入的所有时段
            List<AgentAndTimeVO> availableTimeList = new ArrayList<>();
            applyList.forEach(item -> availableTimeList.addAll(item.getAgentTimeList()));

            //按照考点+时段 分组
            Map<Long, Map<Long, List<AgentAndTimeVO>>> agentTimeMap = availableTimeList.stream()
                    .collect(Collectors.groupingBy(AgentAndTimeVO::getAgentId, Collectors.groupingBy(AgentAndTimeVO::getTimePeriodId)));

            for (Map.Entry<Long, Map<Long, List<AgentAndTimeVO>>> examSiteEntry : agentTimeMap.entrySet()) {
                Long examSiteId = examSiteEntry.getKey();
                Map<Long, List<AgentAndTimeVO>> timePeriodMap = examSiteEntry.getValue();
                for (Map.Entry<Long, List<AgentAndTimeVO>> timePeriodEntry : timePeriodMap.entrySet()) {
                    Long timePeriodKey = timePeriodEntry.getKey();

                    //考点下某个时段可预约的容量
                    int availableCount = cacheService.getApplyAvailableCount(examSiteId, timePeriodKey);
                    // 导入的某个考点，某个时段的数量
                    int impTimePeriodNum = timePeriodEntry.getValue().size();

                    if (impTimePeriodNum > availableCount) {
                        ExamSiteCacheBean examSiteBean = examSiteCacheService.getExamSiteById(examSiteId);
                        if (examSiteBean != null) {
                            String examSiteName = examSiteBean.getExamSiteName();
                            TimePeriodEntity timePeriod = timePeriodService.getById(timePeriodKey);
                            String startAndEndTime = DateUtil.getStartAndEndTime(timePeriod.getStartTime(), timePeriod.getEndTime());

                            log.warn("[importPreExam],{} {} 剩余容量为:{},而导入的数量为:{}", examSiteName, startAndEndTime, availableCount, impTimePeriodNum);
                            failRecords.add(newError(0,
                                    examSiteName + " " + startAndEndTime + " 剩余容量为:" + (availableCount) + ",而导入的数量为:" + impTimePeriodNum));
                        }

                    }
                }
            }
        } catch (Exception e) {
            log.error("[importPreExam],导入出错,msg:{}", e.getMessage());
            failRecords.add(newError(0, "导入失败"));
        }
    }


    private TimePeriodEntity checkTimePeriod(String timePeriodStr, Map<String, TimePeriodEntity> timeCache) {
        if (timePeriodStr == null || timePeriodStr.split(" ").length != 2) {
            throw new StatusException(" 预约时段格式不正确");
        }
        String[] arr = timePeriodStr.split("-");
        String startTime = arr[0] + ":00";
        String endTime = startTime.substring(0, startTime.indexOf("日") + 1) + " " + arr[1] + ":00";
        Long startTimeLong = DateUtil.getLongTimeByZHDate(startTime);
        Long endTimeLong = DateUtil.getLongTimeByZHDate(endTime);
        if (timeCache.get(startTimeLong + "-" + endTimeLong) == null) {
            throw new StatusException(" 预约时段不存在");
        }
        return timeCache.get(startTimeLong + "-" + endTimeLong);
    }

    private Map<String, Object> newError(int lineNum, String msg) {
        Map<String, Object> map = new HashMap<>();
        map.put("lineNum", lineNum);
        map.put("msg", msg);
        return map;
    }

    private StudentEntity checkStudent(String studentCode, String studentName, String identityNumber, Long taskId) {
        LambdaQueryWrapper<StudentEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(StudentEntity::getStudentCode, studentCode);
        wrapper.eq(StudentEntity::getName, studentName);
        wrapper.eq(StudentEntity::getIdentityNumber, identityNumber);
        wrapper.eq(StudentEntity::getApplyTaskId, taskId);
        return studentService.getOne(wrapper);
    }

    private String trimAndNullIfBlank(String s) {
        if (StringUtils.isBlank(s)) {
            return null;
        }
        return s.trim();
    }

    private Map<Long, Long> getExamSiteCategoryCache() {
        Map<Long, Long> cache = new HashMap<>();
        LambdaQueryWrapper<ExamSiteEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ExamSiteEntity::getEnable, Boolean.TRUE);
        List<ExamSiteEntity> examSiteList = examSiteService.list(wrapper);
        examSiteList.forEach(site -> cache.put(site.getId(), site.getCategoryId()));
        return cache;
    }

    private Map<String, TimePeriodEntity> getTimePeriodCache() {
        Map<String, TimePeriodEntity> map = new HashMap<>();
        LambdaQueryWrapper<TimePeriodEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(TimePeriodEntity::getApplyTaskId, getApplyTask().getId());
        List<TimePeriodEntity> timeList = timePeriodService.list(wrapper);
        for (TimePeriodEntity time : timeList) {
            map.put(time.getStartTime() + "-" + time.getEndTime(), time);
        }
        return map;
    }

    private Map<String, Long> getExamSiteCache() {
        LambdaQueryWrapper<ExamSiteEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ExamSiteEntity::getEnable, Boolean.TRUE);
        List<ExamSiteEntity> categoryList = examSiteService.list(wrapper);
        return categoryList.stream().collect(Collectors.toMap(ExamSiteEntity::getName, ExamSiteEntity::getId));
    }

    private Map<String, Long> getTeachingCache(Integer level) {
        LambdaQueryWrapper<CategoryEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(CategoryEntity::getEnable, Boolean.TRUE);
        wrapper.eq(CategoryEntity::getLevel, level == null ? CategoryLevel.TEACHING.getValue() : level);
        List<CategoryEntity> categoryList = categoryService.list(wrapper);
        return categoryList.stream().collect(Collectors.toMap(CategoryEntity::getName, CategoryEntity::getId));
    }

    private void checkOpenTime(Long orgId) {
        CurrentApplyTaskVO task = cacheService.currentApplyTask(orgId);
        Date startTime = new Date(task.getSelfApplyStartTime());
        Date endTime = new Date(task.getOpenApplyEndTime());
        Date now = new Date();
        if (now.before(startTime) || now.after(endTime)) {
            throw new StatusException("未到开放时间！");
        }
    }

    @Override
    public void autoAssign(Long taskId, Long operateId) {
        RLock lock = (RLock) concurrentService.getLock(CacheConstants.LOCK_AUTO_APPLY);
        if (lock.isLocked()) {
            log.warn("[autoAssign] 获取锁失败, taskId:{}, lockKey:{}", taskId, CacheConstants.LOCK_AUTO_APPLY);
            throw new StatusException("自动分配执行未结束,请不要重复执行");
        }

        // 自动分配须在第一阶段之后和第三阶段开始之前
        CurrentApplyTaskVO curApplyTask = applyTaskService.currentApplyTask(null);
        Date selfEndTime = new Date(curApplyTask.getSelfApplyEndTime());
        Date openStartTime = new Date(curApplyTask.getOpenApplyStartTime());
        if (!DateUtil.isBetwwen(selfEndTime, openStartTime)) {
            throw new StatusException("自动分配，时间必须要在第一阶段结束之后，第三阶段开始之前");
        }

        //是否有可预约的时段
        List<TimePeriodEntity> timePeriodList = listTimePeriod(taskId);
        timePeriodList = listNoCancelTimePeriod(timePeriodList, taskId);
        if (CollectionUtils.isEmpty(timePeriodList)) {
            log.warn("[autoAssign] 当前时间没有可预约的时段, taskId:{}", taskId);
            throw new StatusException("当前时间没有可预约的时段");
        }

        //写入异步任务
        Map<String, Object> taskMap = asyncTaskService.saveAsyncTask(AsyncTaskType.AUTO_ASSIGN, operateId);
        taskMap.computeIfAbsent("taskId", v -> taskId);
        taskMap.computeIfAbsent("operateId", v -> operateId);
        try {
            autoAssignStudentService.assignTask(taskMap);
        } catch (Exception e) {
            throw new StatusException(e.getMessage());
        }
    }


    private List<ExamSiteEntity> listExamSite(Long teachingId, Long examSiteId) {
        LambdaQueryWrapper<ExamSiteEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ExamSiteEntity::getCategoryId, teachingId);
        wrapper.eq(examSiteId != null, ExamSiteEntity::getId, examSiteId);
        wrapper.eq(ExamSiteEntity::getEnable, Boolean.TRUE);
        return examSiteService.list(wrapper);
    }

    private List<TimePeriodEntity> listNoCancelTimePeriod(List<TimePeriodEntity> timePeriodList, Long taskId) {
        ApplyTaskEntity task = applyTaskService.getById(taskId);
        Long longToday = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(new Date()) + " 00:00:00");
        Date today = new Date(longToday);
        Date otherDay = DateUtil.addValues(today, Calendar.DAY_OF_MONTH, task.getAllowApplyCancelDays());
        Long longOtherDay = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(otherDay) + " 23:59:59");
        return timePeriodList.stream().filter(time -> time.getStartTime() > longOtherDay).collect(Collectors.toList());
    }

    private List<TimePeriodEntity> listTimePeriod(Long taskId) {
        LambdaQueryWrapper<TimePeriodEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(TimePeriodEntity::getApplyTaskId, taskId);
        wrapper.orderByAsc(TimePeriodEntity::getStartTime);
        List<TimePeriodEntity> timeList = timePeriodService.list(wrapper);
        if (timeList.isEmpty()) {
            log.warn("当前任务未配置可用的考试时段,taskId:{}", taskId);
            throw new StatusException("考试时段未设置");
        }
        return timeList;
    }

    @Transactional
    @Override
    public void autoLayout(Long teachingId) {
        OrgInfo org = orgCacheService.currentOrg();
        CurrentApplyTaskVO applyTask = cacheService.currentApplyTask(org.getOrgId());
        String autoLayoutLockKey = String.format(CacheConstants.LOCK_ARRANGE_EXAM, DateUtil.formatShortDateString(new Date()));
        RLock autoLayoutLock = (RLock) concurrentService.getLock(autoLayoutLockKey);

        try {
            if (!autoLayoutLock.tryLock()) {
                log.warn("获取锁失败，已有线程在执行排考！lockKey:{}", autoLayoutLock);
                return;
            }

            log.warn("获取锁成功！lockKey:{}", autoLayoutLockKey);

            // 1.根据当前日期，查询不能取消的时段
            List<TimePeriodEntity> timePeriodList = listTimePeriod(applyTask.getTaskId());
            List<TimePeriodEntity> noCancelTimePeroidList = listNoCancelApplyTimePeriod(timePeriodList, applyTask.getAllowApplyCancelDays());
            if (noCancelTimePeroidList.isEmpty()) {
                log.warn("[autoLayout] 没有待排考的时段");
                return;
            }

            // 2.查询待排考的考生
            List<Long> timePeriodIds = noCancelTimePeroidList.stream()
                    .map(BaseEntity::getId)
                    .collect(Collectors.toList());
            List<StudentApplyEntity> toBeLayoutStudentList = baseMapper.listTimePeriod(timePeriodIds, Boolean.FALSE);
            if (CollectionUtils.isEmpty(toBeLayoutStudentList)) {
                log.warn("[autoLayout] 没有待排考的考生");
                return;
            }

            // 3.开始排考
            Map<Long, List<StudentApplyEntity>> toBeLayoutStudentMap = toBeLayoutStudentList.stream()
                    .collect(Collectors.groupingBy(StudentApplyEntity::getExamSiteId));
            for (Long examSiteId : toBeLayoutStudentMap.keySet()) {
                List<StudentApplyEntity> studentApplyList= toBeLayoutStudentMap.get(examSiteId);
                Map<Long, List<StudentApplyEntity>> timeLayoutStudentMap = studentApplyList.stream()
                        .collect(Collectors.groupingBy(StudentApplyEntity::getTimePeriodId));

                List<ExamRoomEntity> roomList = listExamRoom(examSiteId);
                if (roomList.isEmpty()) {
                    log.warn("{}:未设置考场", examSiteId);
                    return;
                }
                ExamSiteEntity examSite = examSiteService.getById(examSiteId);
                layoutStudentByTimePeriod(applyTask.getTaskId(), examSite, roomList, timeLayoutStudentMap);
            }
        } catch (StatusException e) {
            log.error(e.getMessage());
        } finally {
            try {
                // 解锁前检查当前线程是否持有该锁
                if (autoLayoutLock.isLocked() && autoLayoutLock.isHeldByCurrentThread()) {
                    autoLayoutLock.unlock();
                    log.info("解锁成功！lockKey:{}", autoLayoutLockKey);
                }
            } catch (Exception e) {
                log.warn(e.getMessage());
            }
        }
    }

    private void layoutStudentByTimePeriod(Long taskId, ExamSiteEntity examSite, List<ExamRoomEntity> roomList,
            Map<Long, List<StudentApplyEntity>> timeLayoutStudentMap) {
        for (Long timePeriodId : timeLayoutStudentMap.keySet()) {
            List<StudentApplyEntity> studentApplyList = timeLayoutStudentMap.get(timePeriodId);
            TimePeriodEntity timePeriod = timePeriodService.getById(timePeriodId);
            layoutStudentToRoom(taskId, examSite, roomList, studentApplyList, timePeriod);
        }
    }

    private void layoutStudentToRoom(Long taskId, ExamSiteEntity examSite, List<ExamRoomEntity> roomList,
            List<StudentApplyEntity> studentApplyList, TimePeriodEntity timePeriod) {
        Integer timePeriodOrder = getTimePeriodOrder(taskId, timePeriod);
        for (ExamRoomEntity room : roomList) {
            int num = 0;
            for (Iterator<StudentApplyEntity> iterator = studentApplyList.iterator(); iterator.hasNext(); ) {
                StudentApplyEntity student = iterator.next();
                if (num >= room.getCapacity()) {
                    break;
                }
                //座位号
                String seatNumber = StringUtils.leftPad(String.valueOf(++num), 3, '0');
                student.setExamRoomId(room.getId());
                student.setSeatNumber(seatNumber);
                // 准考证号
                String ticketNum = DateUtil.formatShortDateString(new Date()) + timePeriodOrder + examSite.getCode() + room.getCode() + seatNumber;
                student.setTicketNumber(ticketNum);
                baseMapper.updateById(student);
                iterator.remove();
            }
        }
    }


    private Integer getTimePeriodOrder(Long taskId, TimePeriodEntity timePeriod) {
        List<TimePeriodEntity> timeList = listTimePeriod(taskId);
        List<TimePeriodEntity> sameDayTimeList = listSameDayTimePeriod(timeList, timePeriod.getStartTime());
        for (int i = 0; i < sameDayTimeList.size(); i++) {
            TimePeriodEntity time = sameDayTimeList.get(i);
            if (time.getStartTime().equals(timePeriod.getStartTime()))
                return i + 1;
        }
        return 0;
    }

    private List<TimePeriodEntity> listNoCancelApplyTimePeriod(List<TimePeriodEntity> list, Integer allowApplyCancelDays) {
        // 不能修改预约的日期
        Date noCancelDate = DateUtil.addValues(Calendar.DAY_OF_MONTH, allowApplyCancelDays);
        return list.stream()
                .filter( item -> isSameDay(noCancelDate, new Date(item.getStartTime())))
                .collect(Collectors.toList());
       /*
        List<TimePeriodEntity> noCancelTimePeroidList;
       for (TimePeriodEntity time : list) {
            // 预约的时段 和 不能修改预约的日期
            if (isSameDay(noCancelDate, new Date(time.getStartTime())))
                noCancelTimePeroidList.add(time);
        }*/
    }

    @Override
    public File downloadSignIn(Long teachingId, Long agentId, Long examDate) {
        ApplyTaskEntity task = getApplyTask();
        List<TimePeriodEntity> timePeriodList = listTimePeriod(task.getId());
        List<TimePeriodEntity> sameDayTimePeriodList = listSameDayTimePeriod(timePeriodList, examDate);
        if (sameDayTimePeriodList.isEmpty()) {
            log.warn("[downloadSignIn] 没有查询到当天的时段,examDate:{}", examDate);
            throw new StatusException("没有可下载的签到表");
        }
        List<ExamSiteEntity> examSiteList = listExamSite(teachingId, agentId);
        if (examSiteList == null || examSiteList.isEmpty()) {
            log.warn("[downloadSignIn] teachingId:{},agentId:{} 没有可用的考点", teachingId, agentId);
            throw new StatusException("没有可用的考点");
        }
        File tempDir = new File("temp");
        if (!tempDir.exists()) {
            try {
                boolean success = tempDir.mkdirs();
                if (!success) {
                    log.warn("[downloadSignIn] 临时目录创建失败!");
                }
            } catch (Exception e) {
                log.error("[downloadSignIn] 临时目录创建失败,msg:{}", e.getMessage());
            }
        }

        CategoryCacheBean category = categoryCacheService.getCategoryById(teachingId);
        File zipFile = new File(tempDir, category.getName() + "签到表.zip");
        ZipWriter writer;
        try {
            writer = ZipWriter.create(zipFile);
            List<File> fileList = new ArrayList<>();
            for (ExamSiteEntity examSite : examSiteList) {
                fileList.addAll(downloadByExamSite(writer, task.getName(), examSite, sameDayTimePeriodList));
            }
            if (fileList.isEmpty()) {
                throw new StatusException("暂未排考,请等待排考后下载");
            }
        } catch (IOException e) {
            log.error("[downloadSignIn] 创建文件异常,msg:{}", e.getMessage());
            throw new StatusException("文件写入异常");
        }
        writer.close();
        return zipFile;
    }

    public List<File> downloadByExamSite(ZipWriter writer, String taskName, ExamSiteEntity site, List<TimePeriodEntity> timeList) throws IOException {
        List<File> fileList = new ArrayList<>();
        List<ExamRoomEntity> roomList = listExamRoom(site.getId());
        MaterialTitleInfo title = new MaterialTitleInfo();
        title.setTaskName(taskName);
        title.setSiteName(site.getName());
        for (TimePeriodEntity time : timeList) {
            title.setTimePeriod(DateUtil.getStartAndEndTime(time.getStartTime(), time.getEndTime()));
            for (ExamRoomEntity room : roomList) {
                title.setAddress(room.getAddress());
                title.setRoomCode(room.getCode());
                title.setRoomName(room.getName());
                List<StudentApplyVO> studentList = baseMapper.listStudentApply(time.getId(), room.getId());
                if (!studentList.isEmpty()) {
                    File file = materialGenerateService.generateSignInForm(title, studentList);
                    fileList.add(file);
                    writer.write(file, title.getSiteName(), title.getTimePeriod() + " 第【" + title.getRoomCode() + "】考场" + ".pdf");
                }
            }
        }
        return fileList;
    }

    public List<ExamRoomEntity> listExamRoom(Long examSiteId) {
        LambdaQueryWrapper<ExamRoomEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ExamRoomEntity::getExamSiteId, examSiteId);
        wrapper.eq(ExamRoomEntity::getEnable, Boolean.TRUE);
        wrapper.orderByAsc(ExamRoomEntity::getCode);
        return examRoomService.list(wrapper);
    }

    private List<TimePeriodEntity> listSameDayTimePeriod(List<TimePeriodEntity> timePeriodList, Long startTime) {
        Date day = new Date(startTime);
        List<TimePeriodEntity> resultList = new ArrayList<>();
        for (TimePeriodEntity time : timePeriodList) {
            if (isSameDay(day, new Date(time.getStartTime())))
                resultList.add(time);
        }
        return resultList.stream().sorted(Comparator.comparing(TimePeriodEntity::getStartTime)).collect(Collectors.toList());
    }

    @Override
    public int countApplyFinishForExamSiteAndTimePeriod(Long examSiteId, Long timePeriodId) {
        Integer value = baseMapper.countApplyFinishForExamSiteAndTimePeriod(examSiteId, timePeriodId);
        return value != null ? value : 0;
    }

    @Override
    public List<SignInVO> listSignInDate(Long taskId) {
        ApplyTaskEntity task;
        if (taskId != null) {
            task = applyTaskService.getById(taskId);
        } else {
            LambdaQueryWrapper<ApplyTaskEntity> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(ApplyTaskEntity::getEnable, Boolean.TRUE);
            task = applyTaskService.getOne(wrapper);
        }

        if (task == null) {
            log.warn("[listSignDate] 未开启预约任务");
            return Collections.emptyList();
        }
        List<TimePeriodEntity> timePeriodList = listTimePeriod(task.getId());
        if (timePeriodList.isEmpty()) {
            log.warn("[listSignDate] 未配置考试时段");
            return Collections.emptyList();
        }

        List<SignInVO> signInVoList = new ArrayList<>();
        Date today = DateUtils.truncate(new Date(), Calendar.DATE);
        if (isInTimePeriod(today, timePeriodList)) {
            SignInVO signInVO = new SignInVO();
            signInVO.setExamDate(today.getTime());
            signInVoList.add(signInVO);
        }
        for (int i = 1; i <= task.getAllowApplyCancelDays(); i++) {
            Date otherDay = DateUtils.addDays(today, i);
            if (isInTimePeriod(otherDay, timePeriodList)) {
                SignInVO signInVO = new SignInVO();
                signInVO.setExamDate(otherDay.getTime());
                signInVoList.add(signInVO);
            }
        }

        signInVoList.sort(Comparator.comparing(SignInVO::getExamDate));
        return signInVoList;
    }

    private boolean isInTimePeriod(Date date, List<TimePeriodEntity> timePeriodList) {
        for (TimePeriodEntity timePeriod : timePeriodList) {
            Date day = new Date(timePeriod.getStartTime());
            if (DateUtils.isSameDay(day, date)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public List<String[]> listExamSiteAvailable(List<CategoryVO> examSiteList) {
        List<String[]> availableList = new ArrayList<>();
        ApplyTaskEntity task = getApplyTask();
        List<TimePeriodEntity> timePeriodList = listTimePeriod(task.getId());
        for (TimePeriodEntity timePeriod : timePeriodList) {
            String[] availableArr = getAvailableArr(examSiteList, timePeriod);
            availableList.add(availableArr);
        }
        return availableList;
    }

    private String[] getAvailableArr(List<CategoryVO> examSiteList, TimePeriodEntity timePeriod) {
        String[] availableArr = new String[examSiteList.size() + 1];
        availableArr[0] = DateUtil.getStartAndEndTime(timePeriod.getStartTime(), timePeriod.getEndTime());
        for (int i = 0; i < examSiteList.size(); i++) {
            CategoryVO categoryVO = examSiteList.get(i);
            int applyFinishCount = countApplyFinishForExamSiteAndTimePeriod(categoryVO.getId(), timePeriod.getId());
            //剩余的容量
            availableArr[i + 1] = String.valueOf(categoryVO.getCapacity() - applyFinishCount);
        }
        return availableArr;
    }

    @Override
    public List<StudentApplyExport> listStudentApplyDetail(Long teachingId) {
        OrgInfo org = orgCacheService.currentOrg();
        CurrentApplyTaskVO curApplyTask = cacheService.currentApplyTask(org.getOrgId());
        if (curApplyTask == null) {
            return Collections.emptyList();
        }
        return baseMapper.listStudentApplyDetail(teachingId, curApplyTask.getTaskId());
    }

    @Override
    public List<StudentExport> listStudentNoApply(Long teachingId) {
        return Collections.emptyList();
    }

    @Override
    public void exportNoApplyStudent(Long teachingId, Long operateId) {
        //写入异步任务
        Map<String, Object> taskMap = asyncTaskService.saveAsyncTask(AsyncTaskType.NO_FINISH_APPLY_STUDENT,
                operateId);
        taskMap.computeIfAbsent("teachingId", v -> teachingId);
        taskMap.computeIfAbsent("operateId", v -> operateId);
        try {
            studentApplyNoFinishExportService.exportTask(taskMap);
        } catch (Exception e) {
            throw new StatusException(e.getMessage());
        }
    }

    @Override
    public void exportStudentApplyDetail(Long teachingId, Long operateId) {
        //写入异步任务
        Map<String, Object> taskMap = asyncTaskService.saveAsyncTask(AsyncTaskType.STUDENT_APPLY_EXPORT,
                operateId);
        taskMap.computeIfAbsent("teachingId", v -> teachingId);
        taskMap.computeIfAbsent("operateId", v -> operateId);
        try {
            studentApplyDetailExportService.exportTask(taskMap);
        } catch (Exception e) {
            throw new StatusException(e.getMessage());
        }
    }

    @Transactional
    @Override
    public void saveOrUpdateStudentApply(StudentApplyEntity studentApply) {
        // 预约是否已经存在
        StudentApplyEntity existStudentApply = findStudentApply(studentApply);
        if (existStudentApply != null) {
            existStudentApply.setCancel(Boolean.FALSE);
            baseMapper.updateById(existStudentApply);
        } else {
            baseMapper.insert(studentApply);
        }
    }

}
