package cn.iocoder.yudao.module.system.service.attendance;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.BetweenFormatter;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.Constants;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.distance.GeoUtil;
import cn.iocoder.yudao.module.system.controller.admin.attendance.dto.*;
import cn.iocoder.yudao.module.system.controller.admin.attendance.vo.*;
import cn.iocoder.yudao.module.system.controller.admin.punchrecord.vo.AttendancePunchRecordSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.addressgroup.AttendanceAddressGroupItemDO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.group.AttendanceGroupDO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.groupshift.AttendanceGroupShiftDO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.groupshiftitem.AttendanceGroupShiftItemDO;
import cn.iocoder.yudao.module.system.dal.dataobject.attendance.punchrecord.AttendancePunchRecordDO;
import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.dal.mysql.attendance.group.AttendanceGroupMapper;
import cn.iocoder.yudao.module.system.dal.mysql.attendance.punchrecord.AttendancePunchRecordMapper;
import cn.iocoder.yudao.module.system.dal.mysql.dept.PostMapper;
import cn.iocoder.yudao.module.system.handler.PunchHandler;
import cn.iocoder.yudao.module.system.service.attendance.addressgroup.AttendanceAddressGroupItemService;
import cn.iocoder.yudao.module.system.service.attendance.factorypunchrecord.AttendanceFactoryPunchRecordService;
import cn.iocoder.yudao.module.system.service.attendance.group.AttendanceGroupService;
import cn.iocoder.yudao.module.system.service.attendance.groupshift.AttendanceGroupShiftService;
import cn.iocoder.yudao.module.system.service.attendance.groupshiftitem.AttendanceGroupShiftItemService;
import cn.iocoder.yudao.module.system.service.attendance.groupsystem.AttendanceGroupSystemService;
import cn.iocoder.yudao.module.system.service.attendance.groupuser.AttendanceGroupUserService;
import cn.iocoder.yudao.module.system.service.attendance.punch.PunchService;
import cn.iocoder.yudao.module.system.service.attendance.punch.dto.AttendanceOnTheDayDTO;
import cn.iocoder.yudao.module.system.service.attendance.punchrecord.AttendancePunchRecordService;
import cn.iocoder.yudao.module.system.service.dept.DeptService;
import cn.iocoder.yudao.module.system.service.dict.DictDataService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

import static cn.hutool.core.date.DateUtil.dayOfWeekEnum;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;

/**
 * 考勤 实现类
 */
@Service
@Slf4j
public class AttendanceServiceImpl implements AttendanceService {
    @Resource
    private PunchHandler punchHandler;
    @Resource
    private AttendanceGroupService attendanceGroupService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private AttendanceGroupShiftService attendanceGroupShiftService;
    @Resource
    private AttendanceGroupShiftItemService attendanceGroupShiftItemService;
    @Resource
    private AttendancePunchRecordService attendancePunchRecordService;
    @Resource
    private AttendancePunchRecordMapper attendancePunchRecordMapper;
    @Resource
    private AdminUserService adminUserService;
    @Resource
    private AttendanceFactoryPunchRecordService attendanceFactoryPunchRecordService;
    @Resource
    private AttendanceGroupMapper attendanceGroupMapper;
    @Resource
    private AttendanceGroupSystemService attendanceGroupSystemService;
    @Resource
    private AttendanceGroupUserService attendanceGroupUserService;
    @Resource
    private PostMapper postMapper;
    @Resource
    private DictDataService dictDataService;
    @Resource
    private AttendanceAddressGroupItemService attendanceAddressGroupItemService;
    @Resource
    private DeptService deptService;


    // 定义一些常量以提高代码的可读性和可维护性
    /**
     * 打卡类型(0上班卡 1下班卡 2迟到卡 3早退卡 4未到打卡时间)
     */
    private static final Integer PUNCH_TYPE_UP_WORK = 0;
    private static final Integer PUNCH_TYPE_DOWN_WORK = 1;
    private static final Integer PUNCH_TYPE_BE_LATE = 2;
    private static final Integer PUNCH_TYPE_LEAVE_EARLY = 3;
    private static final Integer PUNCH_TYPE_NOT_COME_YET = 4;

    /**
     * 上下班标识 0上班 1下班
     */
    private static final Integer UP_WORK = 0;
    private static final Integer DOWN_WORK = 1;
    private static final String NAME = "姓名";
    private static final String ATTENDANCE_GROUP = "考勤组";
    private static final String DEPT = "部门";
    private static final String POSITION = "职位";
    private static final String DATE = "日期";
    private static final String SHIFT = "班次";
    private static final String UP_WORK_STR = "上班";
    private static final String DOWN_WORK_STR = "下班";
    private static final String CHECK_IN_TIME = "打卡时间";
    private static final String CHECK_IN_RESULT = "打卡结果";
    private static final String LENGTH_OF_WORK = "工作时长";
    private static final String LENGTH_OF_LATENESS = "迟到时长";
    private static final String EARLY_DEPARTURE_DURATION = "早退时长";
    private static final String NUMBER_OF_LEAVE_REQUESTS_AND_MISSING_CARDS = "请假漏卡次数";
    private static final String NUMBER_OF_ABSENCES_AT_WORK = "上班缺卡次数";
    private static final String NUMBER_OF_MISSING_CARDS_AFTER_GET_OFF_WORK = "下班缺卡次数";
    private static final String NUMBER_OF_DAYS_OF_ATTENDANCE = "出勤天数";
    private static final String REST_DAYS = "休息天数";
    private static final String NUMBER_OF_TIMES_LATE = "迟到次数";
    private static final String NUMBER_OF_EARLY_DEPARTURES = "早退次数";
    private static final String NUMBER_OF_DAYS_ABSENT_FROM_WORK = "旷工天数";

    @Override
    public AttendancePunchPageVO getPunchPage(AttendancePunchPageDTO dto) {
        // -- 获取当前登陆用户 - 如果当前登陆用户状态异常 - 则不允许打卡
        AdminUserDO user = adminUserService.getUser(dto.getUserId());
        dto.setUser(user);
        AttendancePunchPageVO vo = new AttendancePunchPageVO();
        //获取当前登录用户所在群组
        AttendanceGroupDO activationGroup = attendanceGroupService.getByUserId(dto.getUserId());
        //不在考勤组 - 返回回去
        if (activationGroup == null) {
            vo.setInGroup(Constants.FALSE);
            return vo;
        }
        // 策略模式 - 将不同考勤类型的 分散开逻辑 - 后续好拓展
        PunchService punchService = punchHandler.getResource(AttendanceGroupDO.getCodeByType(activationGroup.getType()));
        return punchService.getPunchPage(dto.setActivationGroup(activationGroup));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public AttendancePunchVO punch(AttendancePunchDTO dto) {
        AttendancePunchPageVO pageVO = this.getPunchPage(new AttendancePunchPageDTO().setPunchType(dto.getPunchType())
                .setLongitude(dto.getLongitude()).setLatitude(dto.getLatitude())
                .setFlag(true).setLocalDateTime(dto.getLocalDateTime()).setUserId(dto.getUserId()));
        if (pageVO.getUser() == null || pageVO.getUser().getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) {
            throw exception(USER_NOT_EXISTS);
        }
        // -- 获取当天的考勤记录
        if (Constants.FALSE.equals(pageVO.getInGroup())) {
            throw exception(NOT_IN_THE_ATTENDANCE_GROUP);
        }
        if (Constants.FALSE.equals(pageVO.getTodayNeedAttendance())) {
            throw exception(NO_ATTENDANCE_REQUIRED_ON_THE_DAY);
        }
        if (Constants.FALSE.equals(pageVO.getFieldworkFlag()) && Constants.FALSE.equals(pageVO.getPunchPoint())) {
            throw exception(DID_NOT_ENTER_THE_ATTENDANCE_RANGE);
        }
        if (PUNCH_TYPE_NOT_COME_YET.equals(pageVO.getPunchType())) {
            throw exception(ATTENDANCE_TIME_NOT_ARRIVED);
        }
        AttendancePunchVO vo = new AttendancePunchVO();
        // -- 插入数据到数据库中 - 如果插入成功 更新redis
        List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS = pageVO.getAttendanceOnTheDayDTOS();
        AttendanceOnTheDayDTO dayDTO = attendanceOnTheDayDTOS.get(pageVO.getIndex());
        Integer status = AttendancePunchRecordSaveReqVO.convertStatus(pageVO.getPunchType());


        //更新预打卡记录 - update 如果修改数量为0 则新增
        AttendancePunchRecordSaveReqVO attendancePunchRecordSaveReqVO = new AttendancePunchRecordSaveReqVO()
                .setUserId(dto.getUserId())
                .setDeptId(pageVO.getUser().getDeptId())
                .setAttendanceGroupId(pageVO.getActivationGroup().getId())
                .setAttendanceGroupName(pageVO.getActivationGroup().getGroupName())
                .setAttendanceGroupShiftId(pageVO.getAttendanceGroupShiftDO().getId())
                .setAttendanceGroupShiftName(pageVO.getAttendanceGroupShiftDO().getName())
                .setAttendanceGroupShiftItemId(pageVO.getAttendanceGroupShiftItemId())
                .setType(pageVO.getActivationGroup().getType())
                .setPunchType(dto.getPunchType())
                .setWorkType(pageVO.getType())
                .setLevel(dayDTO.getLevel())
                .setStatus(status)
                .setNextDayFlag(Constants.FALSE)
                .setFieldServiceFlag(
                        (Constants.TRUE.equals(pageVO.getFieldworkFlag()) && Constants.FALSE.equals(pageVO.getPunchPoint()))
                                ? Constants.TRUE : Constants.FALSE)
                .setDayTime(pageVO.getTargetDayStr())
                .setShouldPunchTime(pageVO.getShouldPunchTime())
                .setPunchTime(dto.getLocalDateTime())
                //取绝对值 如果status是负数则变为正数
                .setLateTime(Constants.ONE.equals(status) ? Math.abs(LocalDateTimeUtil.between(dto.getLocalDateTime(), pageVO.getShouldPunchTime(), ChronoUnit.MILLIS)) : 0L)
                .setLeaveEarlyTime(Constants.TWO.equals(status) ? Math.abs(LocalDateTimeUtil.between(dto.getLocalDateTime(), pageVO.getShouldPunchTime(), ChronoUnit.MILLIS)) : 0L)
                .setWorkOvertimeTime(Constants.ZERO.equals(status) && Constants.ONE.equals(pageVO.getPunchType()) ? Math.abs(LocalDateTimeUtil.between(dto.getLocalDateTime(), pageVO.getShouldPunchTime(), ChronoUnit.MILLIS)) : 0L)
                .setRemark(dto.getRemark())
                .setImage(dto.getImage())
                .setPunchAddress(dto.getPunchAddress());
        AttendancePunchRecordDO attendancePunchRecordDO = new AttendancePunchRecordDO();
        BeanUtil.copyProperties(attendancePunchRecordSaveReqVO, attendancePunchRecordDO);

        int updateNum = attendancePunchRecordMapper.update(attendancePunchRecordDO, new LambdaUpdateWrapper<AttendancePunchRecordDO>()
                .eq(AttendancePunchRecordDO::getUserId, dto.getUserId())
                .eq(AttendancePunchRecordDO::getDayTime, pageVO.getTargetDayStr())
                .eq(AttendancePunchRecordDO::getNextDayFlag, Constants.TRUE)
                .in(AttendancePunchRecordDO::getStatus, Arrays.asList(AttendanceOnTheDayDTO.PUNCH_STATUS_UN_PUNCH, AttendanceOnTheDayDTO.ASK_FOR_LEAVE))
                .eq(AttendancePunchRecordDO::getWorkType, attendancePunchRecordSaveReqVO.getWorkType())
                .eq(AttendancePunchRecordDO::getAttendanceGroupShiftItemId, attendancePunchRecordSaveReqVO.getAttendanceGroupShiftItemId())
        );
        Long id = null;
        if (updateNum <= 0) {
            id = attendancePunchRecordService.createPunchRecord(attendancePunchRecordSaveReqVO);
        }
        // -- 新增成功后更新redis
        if (updateNum > 0 || id != null) {
            dayDTO.setPunchTime(dto.getLocalDateTime().format(Constants.REPO_DATE_FORMAT));
            dayDTO.setPunchStatus(attendancePunchRecordSaveReqVO.getStatus());
            dayDTO.setFieldServiceFlag(attendancePunchRecordSaveReqVO.getFieldServiceFlag());
            dayDTO.setPunchAddress(dto.getPunchAddress());
            dayDTO.setRemark(dto.getRemark());
            dayDTO.setImage(dto.getImage());
            dayDTO.setShouldPunchTime(pageVO.getShouldPunchTime());
            dayDTO.setPunchLocalDateTime(dto.getLocalDateTime());
            dayDTO.setLateTime(attendancePunchRecordSaveReqVO.getLateTime());
            dayDTO.setLeaveEarlyTime(attendancePunchRecordSaveReqVO.getLeaveEarlyTime());

            String mapKey = dto.getUserId().toString();
            stringRedisTemplate.opsForHash().put(pageVO.getRedisKey(), mapKey, JSONUtil.toJsonStr(attendanceOnTheDayDTOS));
        }
        vo.setStatus(status);
        vo.setActivationGroup(pageVO.getActivationGroup());
        vo.setAttendanceGroupShiftDO(pageVO.getAttendanceGroupShiftDO());
        vo.setAttendanceOnTheDayDTOS(attendanceOnTheDayDTOS);
        return vo;
    }


    @Override
    public Boolean isHoliday(LocalDateTime localDateTime) {
        String dateStr = localDateTime.format(Constants.REPO_DATE_FORMAT);
        String year = dateStr.substring(0, 4);
        String key = "holiday_" + year;
        Boolean flag = stringRedisTemplate.hasKey(key);
        // 缓存不存在
        if (Boolean.FALSE.equals(flag)) {
            String url = "https://timor.tech/api/holiday/year/" + year;
            String json = HttpUtil.get(url, 3000);
            Map<String, String> map = new HashMap<>();
            JSONObject jsonObject = JSONUtil.parseObj(json);
            Object o = jsonObject.get("holiday");
            //将o转为json对象并且循环获取对象中所有的属性
            JSONObject data = JSONUtil.parseObj(o);
            data.forEach((k, v) -> {
                JSONObject item = JSONUtil.parseObj(v);
                map.put(item.get("date").toString(), item.get("holiday").toString());
            });
            stringRedisTemplate.opsForHash().putAll(key, map);
            // -- 删除去年的
            String lastKey = "holiday_" + (Integer.parseInt(year) - 1);
            stringRedisTemplate.delete(lastKey);
        }
        Object o = stringRedisTemplate.opsForHash().get(key, dateStr);
        if (o != null) {
            return Constants.TRUE_STR.equals(String.valueOf(o));
        }
        return null;
    }


    @Override
    public void calculatePunch(AttendancePunchPageDTO dto, AttendancePunchPageVO vo) {
        LocalDateTime localDateTime = dto.getLocalDateTime();
        LocalDateTime nextDayDateTime = LocalDateTimeUtil.offset(localDateTime, 1, ChronoUnit.DAYS);
        AttendanceGroupDO activationGroup = dto.getActivationGroup();
        vo.setFieldworkFlag(dto.getUser().getFieldworkFlag() == null ? activationGroup.getFieldworkFlag() : dto.getUser().getFieldworkFlag());
        // - 根据经纬度判断是否在对应班组的打卡点上 - 如果是考勤机的话默认就是在打卡点
        List<AttendanceAddressGroupItemDO> addressList = new ArrayList<>();
        addressList.add(new AttendanceAddressGroupItemDO().setLatitude(activationGroup.getLatitude()).setLongitude(activationGroup.getLongitude()).setScope(activationGroup.getScope()));
        vo.setPunchPoint(AttendanceGroupDO.PUNCH_TYPE_ATTENDANCE_MACHINE.equals(dto.getPunchType()) ? Constants.TRUE :
                this.verifyCheckInAddress(Double.parseDouble(dto.getLatitude()), Double.parseDouble(dto.getLongitude()), addressList, activationGroup.getAddressGroupIds()));

        // -- 获取班次
        AttendanceGroupShiftDO attendanceGroupShiftDO = attendanceGroupShiftService.getGroupShift(vo.getAttendanceGroupShiftId());
        // -- 获取班次item
        List<AttendanceGroupShiftItemDO> attendanceGroupShiftItemDOList = attendanceGroupShiftItemService.getGroupShiftItemListByShiftId(vo.getAttendanceGroupShiftId());

        String key = Constants.ATTENDANCE + Constants.UNDERLINE + activationGroup.getId() + Constants.UNDERLINE; // + 时间
        String mapKey = dto.getUserId().toString();


        String yesterdayStr = LocalDateTimeUtil.offset(localDateTime, -1, ChronoUnit.DAYS).format(Constants.REPO_DATE_FORMAT);
        String targetDayStr = localDateTime.format(Constants.REPO_DATE_FORMAT);
        AttendanceGroupShiftItemDO last = CollUtil.getLast(attendanceGroupShiftItemDOList);
        // -- 如果最后的时间是跨天 那么 需要判断当前时间是否在跨天时间段内 - 如果是的话取昨天的的yyyy-MM-dd
        if (Arrays.asList(1, 2).contains(last.getNextDayFlag())) {
            LocalDateTime endOfDay = LocalDateTime.ofInstant(DateUtils.buildHHmmTime(last.getEndTime()).toInstant(), ZoneId.systemDefault()).plusMinutes(last.getAfterPunchTimeDownWork());
            LocalDateTime beginOfDay = LocalDateTimeUtil.beginOfDay(endOfDay);
            if (LocalDateTimeUtil.isIn(localDateTime, beginOfDay, endOfDay)) {
                targetDayStr = yesterdayStr;
            }
        }

        List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS = this.getAttendanceOnTheDay(key, mapKey, targetDayStr, attendanceGroupShiftItemDOList);
        // -- 这里没有的情况 只可能是redis 数据库崩了/或者班次子表没数据 - 暂时先返回不需要考勤
        if (attendanceOnTheDayDTOS.isEmpty()) {
            vo.setTodayNeedAttendance(Constants.FALSE);
            return;
        }

        // 定义是否需要更新redis - 默认不需要更新
        boolean updateRedis = false;
        int index = 0;
        for (AttendanceOnTheDayDTO attendanceOnTheDayDTO : attendanceOnTheDayDTOS) {
            if (AttendanceOnTheDayDTO.PUNCH_STATUS_UN_PUNCH.equals(attendanceOnTheDayDTO.getPunchStatus())) {
                LocalDateTime time = LocalDateTime.ofInstant(DateUtils.buildHHmmTime(attendanceOnTheDayDTO.getTime(), (attendanceOnTheDayDTO.getNextDayFlag() == 0 ? localDateTime : nextDayDateTime)).toInstant(), ZoneId.systemDefault());
                LocalDateTime lastTime = time.minusMinutes(attendanceOnTheDayDTO.getBeforePunchTime());
                LocalDateTime endTime = time.plusMinutes(attendanceOnTheDayDTO.getAfterPunchTime());
                // 确保beforePunchTime/AfterPunchTime是正数 - 判断当前时间与最早打卡时间 如果没到时间则跳过
                if (attendanceOnTheDayDTO.getBeforePunchTime() < Constants.ZERO || attendanceOnTheDayDTO.getAfterPunchTime() < Constants.ZERO || localDateTime.isBefore(lastTime)) {
                    continue; // 跳过当前循环，避免错误 - 如果所有的都错了话 那么返回默认的 未到打卡时间
                }
                if (LocalDateTimeUtil.isIn(localDateTime, lastTime, endTime)) {
                    int punchType = this.determinePunchType(localDateTime, time, UP_WORK.equals(attendanceOnTheDayDTO.getType()));
                    vo.setPunchType(punchType);
                    if (dto.getFlag()) {
                        vo.setType(attendanceOnTheDayDTO.getType());
                        vo.setShouldPunchTime(time);
                        vo.setAttendanceGroupShiftItemId(attendanceOnTheDayDTO.getId());
                        vo.setIndex(index);
                    }
                    break;
                } else {
                    // 如果是未打卡 - 判断当前时间是否在打卡区间内 - 如果不在的话直接set缺卡
                    // - 定时任务还没来得及刷新redis 数据 - 这里需要手动刷新
                    updateRedis = true;
                    attendanceOnTheDayDTO.setPunchStatus(AttendanceOnTheDayDTO.PUNCH_STATUS_MISS);
                }
            }
            index++;
        }
        vo.setTargetDayStr(targetDayStr);
        vo.setAttendanceOnTheDayDTOS(attendanceOnTheDayDTOS);
        if (dto.getFlag()) {
            vo.setRedisKey(key + targetDayStr);
            vo.setAttendanceGroupShiftDO(attendanceGroupShiftDO);
        }
        // 更新redis
        if (updateRedis) {
            stringRedisTemplate.opsForHash().put(key + targetDayStr, mapKey, JSONUtil.toJsonStr(attendanceOnTheDayDTOS));
        }
    }

    @Override
    public Integer verifyCheckInAddress(double latitude, double longitude, List<AttendanceAddressGroupItemDO> addressList, String addressGroupIds) {
        if (StringUtils.isNotEmpty(addressGroupIds)) {
            List<Long> addressGroupIdList = Arrays.stream(addressGroupIds.split(Constants.COMMA)).map(Long::parseLong).collect(Collectors.toList());
            List<AttendanceAddressGroupItemDO> list = attendanceAddressGroupItemService.getListByAddressGroupIds(addressGroupIdList);
            if (!list.isEmpty()) {
                addressList.addAll(list);
            }
        }
        int num = 0;
        for (AttendanceAddressGroupItemDO item : addressList) {
            num = GeoUtil.distance(latitude, longitude, Double.parseDouble(item.getLatitude()), Double.parseDouble(item.getLongitude()), item.getScope());
            if (num == 1) {
                return num;
            }
        }
        return num;
    }


    /**
     * 确定打卡类型 - 上班打卡 忽略秒 比如 8点上班 8点0分59秒打卡不算迟到
     *
     * @param localDateTime
     * @param time
     * @param isUpWork
     * @return
     */
    private int determinePunchType(LocalDateTime localDateTime, LocalDateTime time, boolean isUpWork) {
        if (localDateTime.isAfter(time.plusMinutes(Constants.ONE)) && isUpWork) {
            return PUNCH_TYPE_BE_LATE;
        } else if (time.plusMinutes(1).isAfter(localDateTime) && isUpWork) {
            return PUNCH_TYPE_UP_WORK;
        } else if (localDateTime.isAfter(time) && !isUpWork) {
            return PUNCH_TYPE_DOWN_WORK;
        } else {
            return PUNCH_TYPE_LEAVE_EARLY;
        }
    }

    private List<AttendanceOnTheDayDTO> getAttendanceOnTheDay(String key, String mapKey, String targetDayStr, List<AttendanceGroupShiftItemDO> attendanceGroupShiftItemDOList) {
        List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS = new ArrayList<>();
        // 尝试从Redis获取数据
        try {
            attendanceOnTheDayDTOS = this.loadFromRedis(key, targetDayStr, mapKey);
        } catch (Exception e) {
            // Redis操作异常处理逻辑
            log.error("Load from Redis failed.", e);
        }

        // 如果Redis中没有，则从数据库加载并存入Redis
        if (attendanceOnTheDayDTOS.isEmpty()) {
            attendanceOnTheDayDTOS = this.buildAttendanceOnTheDay(attendanceGroupShiftItemDOList);
            this.saveToRedis(key, targetDayStr, mapKey, attendanceOnTheDayDTOS);
        }
        return attendanceOnTheDayDTOS;
    }

    private List<AttendanceOnTheDayDTO> loadFromRedis(String key, String targetDayStr, String mapKey) {
        Object o = stringRedisTemplate.opsForHash().get(key + targetDayStr, mapKey);
        return o != null ? JSONUtil.toList(o.toString(), AttendanceOnTheDayDTO.class) : new ArrayList<>();
    }

    /**
     * 考勤数据设置两天过期
     *
     * @param key
     * @param targetDayStr
     * @param mapKey
     * @param attendanceOnTheDayDTOS
     */
    private void saveToRedis(String key, String targetDayStr, String mapKey, List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS) {
        stringRedisTemplate.opsForHash().put(key + targetDayStr, mapKey, JSONUtil.toJsonStr(attendanceOnTheDayDTOS));
        //设置缓存 2天
        stringRedisTemplate.expire(key + targetDayStr, 2, TimeUnit.DAYS);
    }

    @Override
    public List<AttendanceOnTheDayDTO> buildAttendanceOnTheDay(List<AttendanceGroupShiftItemDO> attendanceGroupShiftItemDOList) {
        List<AttendanceOnTheDayDTO> attendanceOnTheDayDTOS = new ArrayList<>();
        //上下班时间
        for (AttendanceGroupShiftItemDO attendanceGroupShiftItemDO : attendanceGroupShiftItemDOList) {
            AttendanceOnTheDayDTO dto = new AttendanceOnTheDayDTO();
            dto.setId(attendanceGroupShiftItemDO.getId());
            dto.setKqAttendanceGroupShiftId(attendanceGroupShiftItemDO.getKqAttendanceGroupShiftId());
            dto.setKqAttendanceGroupShiftName(attendanceGroupShiftItemDO.getKqAttendanceGroupShiftName());
            dto.setTime(attendanceGroupShiftItemDO.getBeginTime());
            dto.setPunchTime(StringUtils.EMPTY);
            dto.setType(UP_WORK);
            dto.setLevel(attendanceGroupShiftItemDO.getLevel());
            dto.setBeforePunchTime(attendanceGroupShiftItemDO.getBeforePunchTimeUpWork());
            dto.setAfterPunchTime(attendanceGroupShiftItemDO.getAfterPunchTimeUpWork());
            dto.setPunchStatus(AttendanceOnTheDayDTO.PUNCH_STATUS_UN_PUNCH);
            dto.setFieldServiceFlag(Constants.FALSE);
            dto.setNextDayFlag(attendanceGroupShiftItemDO.getNextDayFlag() == 0 || attendanceGroupShiftItemDO.getNextDayFlag() == 2 ? 0 : 1);
            attendanceOnTheDayDTOS.add(dto);

            dto = new AttendanceOnTheDayDTO();
            dto.setId(attendanceGroupShiftItemDO.getId());
            dto.setKqAttendanceGroupShiftId(attendanceGroupShiftItemDO.getKqAttendanceGroupShiftId());
            dto.setKqAttendanceGroupShiftName(attendanceGroupShiftItemDO.getKqAttendanceGroupShiftName());
            dto.setTime(attendanceGroupShiftItemDO.getEndTime());
            dto.setPunchTime(StringUtils.EMPTY);
            dto.setType(DOWN_WORK);
            dto.setLevel(attendanceGroupShiftItemDO.getLevel());
            dto.setBeforePunchTime(attendanceGroupShiftItemDO.getBeforePunchTimeDownWork());
            dto.setAfterPunchTime(attendanceGroupShiftItemDO.getAfterPunchTimeDownWork());
            dto.setPunchStatus(AttendanceOnTheDayDTO.PUNCH_STATUS_UN_PUNCH);
            dto.setFieldServiceFlag(Constants.FALSE);
            dto.setNextDayFlag(attendanceGroupShiftItemDO.getNextDayFlag() == 0 ? 0 : 1);
            attendanceOnTheDayDTOS.add(dto);
        }
        return attendanceOnTheDayDTOS;
    }

    @Override
    public Map<String, AttendanceStatusByDayVO> statisticsByDay(AttendanceStatisticsByDayDTO dto) {
        //获取当前天 所在月份所有日期
        List<String> dateList = DateUtils.betweenDayList(DateUtil.beginOfMonth(dto.getTime()),
                DateUtil.endOfMonth(dto.getTime()));
        List<AttendancePunchRecordDO> list = attendancePunchRecordMapper.selectList(new LambdaQueryWrapper<AttendancePunchRecordDO>()
                .eq(AttendancePunchRecordDO::getUserId, dto.getUserId())
                .in(AttendancePunchRecordDO::getDayTime, dateList)
                .orderByAsc(AttendancePunchRecordDO::getDayTime)
                .orderByAsc(AttendancePunchRecordDO::getLevel)
                .orderByAsc(AttendancePunchRecordDO::getWorkType));
        Map<String, AttendanceStatusByDayVO> map = new HashMap<>();

        Map<String, List<AttendancePunchRecordDO>> collect = list.stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getDayTime));

        List<Integer> statusList = Arrays.asList(AttendanceOnTheDayDTO.PUNCH_STATUS_LATE,
                AttendanceOnTheDayDTO.PUNCH_STATUS_LEAVE_EARLY,
                AttendanceOnTheDayDTO.PUNCH_STATUS_MISS,
                AttendanceOnTheDayDTO.ASK_FOR_LEAVE
        );
        for (Map.Entry<String, List<AttendancePunchRecordDO>> entry : collect.entrySet()) {
            AttendanceStatusByDayVO attendanceStatusByDayVO = new AttendanceStatusByDayVO();
            int status = 0;
            for (AttendancePunchRecordDO attendancePunchRecordDO : entry.getValue()) {
                if (statusList.contains(attendancePunchRecordDO.getStatus())) {
                    status = 1;
                    break;
                }
            }
            attendanceStatusByDayVO.setStatus(status);
            attendanceStatusByDayVO.setSubmitStatus(Constants.FALSE);
            List<AttendanceByGroupVO> attendanceByGroupVOS = new ArrayList<>();
            Map<String, List<AttendancePunchRecordDO>> itemMap = entry.getValue().stream().collect(Collectors.groupingBy(a -> a.getAttendanceGroupId() + "_" + a.getAttendanceGroupShiftId()));
            for (Map.Entry<String, List<AttendancePunchRecordDO>> stringListEntry : itemMap.entrySet()) {
                if (CollectionUtil.isNotEmpty(stringListEntry.getValue())) {
                    AttendancePunchRecordDO attendancePunchRecordDO = stringListEntry.getValue().get(0);
                    AttendanceByGroupVO attendanceByGroupVO = new AttendanceByGroupVO();
                    attendanceByGroupVO.setGroupName(attendancePunchRecordDO.getAttendanceGroupName());
                    attendanceByGroupVO.setAttendanceGroupShiftName(attendancePunchRecordDO.getAttendanceGroupShiftName());
                    attendanceByGroupVO.setList(stringListEntry.getValue());
                    attendanceByGroupVOS.add(attendanceByGroupVO);
                }
            }
            attendanceStatusByDayVO.setList(attendanceByGroupVOS);
            map.put(entry.getKey(), attendanceStatusByDayVO);
        }
        return map;
    }

    @Override
    public AttendanceStatisticsVO statisticsByCycle(AttendanceStatisticsByCycleDTO dto) {
        AttendanceStatisticsVO vo = new AttendanceStatisticsVO();
        Date thisTime = new Date();
        Date beginTime = dto.getStartTime();
        Date endTime = dto.getEndTime();
        if (beginTime.getTime() > endTime.getTime()) {
            throw exception(THE_START_TIME_CANNOT_BE_GREATER_THAN_THE_END_TIME);
        }
        // -- 如果结束时间大于当前时间 - 那么结束时间 = 当前时间
        if (endTime.getTime() > thisTime.getTime()) {
            endTime = thisTime;
        }
        List<String> dateList = DateUtils.betweenDayList(beginTime, endTime);
        List<AttendancePunchRecordDO> list = attendancePunchRecordMapper.selectList(new LambdaQueryWrapper<AttendancePunchRecordDO>()
                .eq(AttendancePunchRecordDO::getUserId, dto.getUserId())
                .in(AttendancePunchRecordDO::getDayTime, dateList)
                .eq(AttendancePunchRecordDO::getNextDayFlag, Constants.FALSE)
                .orderByAsc(AttendancePunchRecordDO::getCreateTime)
                .orderByAsc(AttendancePunchRecordDO::getWorkType)
                .orderByAsc(AttendancePunchRecordDO::getLevel));
        // ---
        Map<String, List<AttendancePunchRecordDO>> map = list.stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getDayTime, TreeMap::new, Collectors.toList()));
        List<AttendancePunchStatisticsVO> averageWorkingHours = new ArrayList<>();
        List<AttendancePunchStatisticsVO> attendanceDays = new ArrayList<>();
        List<AttendancePunchStatisticsVO> beLateNumber = new ArrayList<>();
        List<AttendancePunchStatisticsVO> earlyDeparturesNumber = new ArrayList<>();
        List<AttendancePunchStatisticsVO> missingCardsNumber = new ArrayList<>();
        List<AttendancePunchStatisticsVO> leaveNumber = new ArrayList<>();
        List<AttendancePunchStatisticsVO> minerDays = new ArrayList<>();
        List<AttendancePunchStatisticsVO> fieldServiceNumber = new ArrayList<>();
        CalculateNum calculateNum = new CalculateNum();

        for (Map.Entry<String, List<AttendancePunchRecordDO>> entry : map.entrySet()) {
            String day = entry.getKey();
            String weekChinese = dayOfWeekEnum(DateUtil.parse(entry.getKey())).toChinese();
            // -- 按照班次子表分组
            Map<Long, List<AttendancePunchRecordDO>> workMap = entry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getAttendanceGroupShiftItemId));
            // -- 出勤天数计算
            AttendancePunchStatisticsVO attendanceDayVO = this.calculateAttendanceDays(day, weekChinese, entry.getValue(), calculateNum);
            if (attendanceDayVO != null) {
                attendanceDays.add(attendanceDayVO);
            }
            // -- 考勤工时计算
            AttendancePunchStatisticsVO averageWorkingHourVO = this.calculateAverageWorkingHour(day, weekChinese, workMap, calculateNum);
            averageWorkingHours.add(averageWorkingHourVO);

            // -- 迟到计算
            List<AttendancePunchStatisticsVO> beLateList = this.calculateBeLate(day, weekChinese, workMap, calculateNum);
            beLateNumber.addAll(beLateList);
            // -- 早退计算
            List<AttendancePunchStatisticsVO> earlyDeparturesList = this.calculateEarlyDepartures(day, weekChinese, workMap, calculateNum);
            earlyDeparturesNumber.addAll(earlyDeparturesList);

            // -- 缺卡计算
            List<AttendancePunchStatisticsVO> missingCardsList = this.calculateMissingCardsList(day, weekChinese, workMap, calculateNum);
            missingCardsNumber.addAll(missingCardsList);

            // -- 请假计算
            List<AttendancePunchStatisticsVO> leaveList = this.calculateLeaveList(day, weekChinese, workMap, calculateNum);
            leaveNumber.addAll(leaveList);

            // -- 旷工计算
            AttendancePunchStatisticsVO miner = this.calculateMiner(day, weekChinese, entry.getValue(), calculateNum);
            if (miner != null) {
                minerDays.add(miner);
            }
            // -- 外勤计算
            List<AttendancePunchStatisticsVO> fieldServiceList = this.calculateFieldService(day, weekChinese, workMap, calculateNum);
            fieldServiceNumber.addAll(fieldServiceList);
        }
        // -- 休息计算
        List<AttendancePunchStatisticsVO> restDays = this.calculateRestDayList(dateList, map.keySet().stream().distinct().collect(Collectors.toList()), calculateNum);
        calculateNum.setAverageWorkingHours(calculateNum.getTotalAttendanceDays() == 0 ? 0 : calculateNum.getTotalWorkingHours() / calculateNum.getTotalAttendanceDays());
        calculateNum.setAverageWorkingHoursStr(DateUtil.formatBetween(calculateNum.getAverageWorkingHours(), BetweenFormatter.Level.MINUTE));
        calculateNum.setTotalLateArrivalsTimeStr(DateUtil.formatBetween(calculateNum.getTotalLateArrivalsTime(), BetweenFormatter.Level.MINUTE));
        calculateNum.setTotalEarlyDeparturesTimeStr(DateUtil.formatBetween(calculateNum.getTotalEarlyDeparturesTime(), BetweenFormatter.Level.MINUTE));
        vo.setAttendanceDays(attendanceDays);
        vo.setAverageWorkingHours(averageWorkingHours);
        vo.setBeLateNumber(beLateNumber);
        vo.setEarlyDeparturesNumber(earlyDeparturesNumber);
        vo.setMissingCardsNumber(missingCardsNumber);
        vo.setLeaveNumber(leaveNumber);
        vo.setMinerDays(minerDays);
        vo.setFieldServiceNumber(fieldServiceNumber);
        vo.setRestDays(restDays);
        vo.setCalculateNum(calculateNum);
        return vo;
    }

    /**
     * 请假计算
     *
     * @param workMap
     * @return
     */
    private String calculateLeaveList(Map<Long, List<AttendancePunchRecordDO>> workMap) {
        int todayLeaveNumber = 0;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 请假计算
                List<AttendancePunchRecordDO> leaveList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.ASK_FOR_LEAVE.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(leaveList)) {
                    todayLeaveNumber += leaveList.size();
                }
            }
        }
        return String.valueOf(todayLeaveNumber);
    }

    /**
     * 请假计算
     *
     * @param workMap
     * @param calculateNum
     * @return
     */
    private void calculateLeaveList(Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        int todayLeaveNumber = 0;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 请假计算
                List<AttendancePunchRecordDO> leaveList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.ASK_FOR_LEAVE.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(leaveList)) {
                    todayLeaveNumber += leaveList.size();
                }
            }
        }
        calculateNum.setTotalLeaveNumber(calculateNum.getTotalLeaveNumber() + todayLeaveNumber);
    }


    /**
     * 请假计算
     *
     * @param day
     * @param weekChinese
     * @param workMap
     * @param calculateNum
     * @return
     */
    private List<AttendancePunchStatisticsVO> calculateLeaveList(String day, String weekChinese, Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        List<AttendancePunchStatisticsVO> list = new ArrayList<>();
        int totalLeaveNumber = 0;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 缺卡计算
                List<AttendancePunchRecordDO> missingCardsList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.ASK_FOR_LEAVE.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(missingCardsList)) {
                    totalLeaveNumber += missingCardsList.size();
                    for (AttendancePunchRecordDO attendancePunchRecordDO : missingCardsList) {
                        AttendancePunchStatisticsVO earlyDepartures = new AttendancePunchStatisticsVO();
                        earlyDepartures.setDay(day);
                        earlyDepartures.setWeek(weekChinese);
                        earlyDepartures.setShouldPunchTime(attendancePunchRecordDO.getShouldPunchTime().format(DateTimeFormatter.ofPattern("HH:mm")));
                        list.add(earlyDepartures);
                    }
                }
            }
        }
        calculateNum.setTotalLeaveNumber(calculateNum.getTotalLeaveNumber() + totalLeaveNumber);
        return list;
    }

    @Override
    public Map<String, TeamAttendanceStatisticsByDayVO> teamStatisticsByDay(TeamAttendanceStatisticsByDayDTO dto) {
        Map<String, TeamAttendanceStatisticsByDayVO> map = new HashMap<>();
        //查询考勤组
//        AttendanceGroupDO attendanceGroupDO = attendanceGroupService.getGroup(dto.getGroupId());
        // - 判断当前用户是否有权限查看
        // TODO: 2024/5/28 这里需要根据数据权限再过滤一下 ？ 或者说移动端不让看 有数据权限的直接在管理后台看
//        if (!dto.getUserId().equals(attendanceGroupDO.getUserId())) {
//            throw exception(NO_PERMISSION_TO_VIEW_CURRENT_ATTENDANCE_GROUP_INFORMATION);
//        }
        List<String> dateList = DateUtils.betweenDayList(DateUtil.beginOfMonth(dto.getTime()),
                DateUtil.endOfMonth(dto.getTime()));
        List<AttendancePunchRecordDO> list = attendancePunchRecordMapper.selectList(new LambdaQueryWrapper<AttendancePunchRecordDO>()
                .eq(AttendancePunchRecordDO::getAttendanceGroupId, dto.getGroupId())
                .in(AttendancePunchRecordDO::getDayTime, dateList)
                .orderByAsc(AttendancePunchRecordDO::getDayTime)
                .orderByAsc(AttendancePunchRecordDO::getLevel)
                .orderByAsc(AttendancePunchRecordDO::getWorkType));
        Map<String, List<AttendancePunchRecordDO>> punchRecorMap = list.stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getDayTime));

        for (Map.Entry<String, List<AttendancePunchRecordDO>> entry : punchRecorMap.entrySet()) {
            Map<Long, List<AttendancePunchRecordDO>> userMap = entry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getUserId));
            Map<Long, List<AttendancePunchRecordDO>> punchMap = entry.getValue().stream().filter(a -> ObjectUtil.isNotEmpty(a.getPunchTime())).collect(Collectors.groupingBy(AttendancePunchRecordDO::getUserId));
            int punchStatusMissNum = 0;
            int lateNum = 0;
            int leaveEarlyNum = 0;
            int fieldworkNum = 0;
            int leaveNum = 0;
            for (AttendancePunchRecordDO attendancePunchRecordDO : entry.getValue()) {
                if (AttendanceOnTheDayDTO.PUNCH_STATUS_MISS.equals(attendancePunchRecordDO.getStatus())) {
                    punchStatusMissNum++;
                }
                if (AttendanceOnTheDayDTO.PUNCH_STATUS_LATE.equals(attendancePunchRecordDO.getStatus())) {
                    lateNum++;
                }
                if (AttendanceOnTheDayDTO.PUNCH_STATUS_LEAVE_EARLY.equals(attendancePunchRecordDO.getStatus())) {
                    leaveEarlyNum++;
                }
                if (Constants.TRUE.equals(attendancePunchRecordDO.getFieldServiceFlag())) {
                    fieldworkNum++;
                }
                if (AttendanceOnTheDayDTO.ASK_FOR_LEAVE.equals(attendancePunchRecordDO.getStatus())) {
                    leaveNum++;
                }
            }
            TeamAttendanceStatisticsByDayVO vo = new TeamAttendanceStatisticsByDayVO();
            vo.setAnswerNum(userMap.keySet().size());
            vo.setPunchNum(punchMap.keySet().size());
            vo.setPunchStatusMissNum(punchStatusMissNum);
            vo.setLateNum(lateNum);
            vo.setLeaveEarlyNum(leaveEarlyNum);
            vo.setFieldworkNum(fieldworkNum);
            vo.setLeaveNum(leaveNum);
            map.put(entry.getKey(), vo);
        }
        return map;
    }

    @Override
    public TeamAttendanceStatisticsByCycleVO tesmStatisticsByCycle(TeamAttendanceStatisticsByCycleDTO dto) {
        TeamAttendanceStatisticsByCycleVO vo = new TeamAttendanceStatisticsByCycleVO();
        //查询考勤组
//        AttendanceGroupDO attendanceGroupDO = attendanceGroupService.getGroup(dto.getGroupId());
        // - 判断当前用户是否有权限查看
        // TODO: 2024/5/28 这里需要根据数据权限再过滤一下 ？ 或者说移动端不让看 有数据权限的直接在管理后台看
//        if (!dto.getUserId().equals(attendanceGroupDO.getUserId())) {
//            throw exception(NO_PERMISSION_TO_VIEW_CURRENT_ATTENDANCE_GROUP_INFORMATION);
//        }
        Date thisTime = new Date();
        Date beginTime = dto.getStartTime();
        Date endTime = dto.getEndTime();
        if (beginTime.getTime() > endTime.getTime()) {
            throw exception(THE_START_TIME_CANNOT_BE_GREATER_THAN_THE_END_TIME);
        }
        // -- 如果结束时间大于当前时间 - 那么结束时间 = 当前时间
        if (endTime.getTime() > thisTime.getTime()) {
            endTime = thisTime;
        }
        List<String> dateList = DateUtils.betweenDayList(beginTime, endTime);
        List<AttendancePunchRecordDO> list = attendancePunchRecordMapper.selectList(new LambdaQueryWrapper<AttendancePunchRecordDO>()
                .eq(AttendancePunchRecordDO::getAttendanceGroupId, dto.getGroupId())
                .in(AttendancePunchRecordDO::getDayTime, dateList)
                .orderByAsc(AttendancePunchRecordDO::getDayTime)
                .orderByAsc(AttendancePunchRecordDO::getLevel)
                .orderByAsc(AttendancePunchRecordDO::getWorkType));


        //获取用户列表
        List<Long> userIds = list.stream().map(AttendancePunchRecordDO::getUserId).distinct().collect(Collectors.toList());
        List<AdminUserDO> userList = adminUserService.getUserList(userIds);
        Map<Long, AdminUserDO> userMap = new HashMap<>();
        if (CollectionUtil.isNotEmpty(userList)) {
            userMap = userList.stream().collect(Collectors.toMap(AdminUserDO::getId, Function.identity()));
        }
        //平均工时
        List<TeamAttendancePunchStatisticsVO> averageWorkingHours = new ArrayList<>();
        //迟到
        List<TeamAttendancePunchStatisticsVO> beLateList = new ArrayList<>();
        //早退
        List<TeamAttendancePunchStatisticsVO> leaveEarlyList = new ArrayList<>();
        //缺卡
        List<TeamAttendancePunchStatisticsVO> missingCardList = new ArrayList<>();
        //旷工
        List<TeamAttendancePunchStatisticsVO> absenteeismList = new ArrayList<>();
        //外勤
        List<TeamAttendancePunchStatisticsVO> fieldServiceList = new ArrayList<>();
        //请假
        List<TeamAttendancePunchStatisticsVO> leaveList = new ArrayList<>();

        Map<Long, List<AttendancePunchRecordDO>> userPunchMap = list.stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getUserId));
        // -- 根据用户分组
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> entry : userPunchMap.entrySet()) {
            // 通用头部 ------------------------------------
            AdminUserDO adminUserDO = userMap.get(entry.getKey());
            TeamAttendancePunchStatisticsVO averageWorkingHourVO = new TeamAttendancePunchStatisticsVO();
            averageWorkingHourVO.setUserId(entry.getKey());
            if (adminUserDO != null) {
                averageWorkingHourVO.setName(adminUserDO.getNickname());
                averageWorkingHourVO.setAvatar(adminUserDO.getAvatar());
            }
            // 通用头部 ------------------------------------

            CalculateNum calculateNum = new CalculateNum();
            //按日期分组
            Map<String, List<AttendancePunchRecordDO>> dayMap = entry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getDayTime));
            for (Map.Entry<String, List<AttendancePunchRecordDO>> dayEntry : dayMap.entrySet()) {
                // -- 按照班次子表分组
                Map<Long, List<AttendancePunchRecordDO>> workMap = dayEntry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getAttendanceGroupShiftItemId));
                this.calculateAverageWorkingHour(workMap, calculateNum);
                // -- 出考勤天数
                this.calculateAttendanceDays(dayEntry.getValue(), calculateNum);
                // -- 迟到
                this.calculateBeLate(workMap, calculateNum);
                // -- 早退
                this.calculateEarlyDepartures(workMap, calculateNum);
                // -- 缺卡
                this.calculateMissingCardsList(workMap, calculateNum);
                // -- 请假
                this.calculateLeaveList(workMap, calculateNum);
                // -- 旷工
                this.calculateMiner(dayEntry.getValue(), calculateNum);
                // -- 外勤
                this.calculateFieldService(workMap, calculateNum);
            }
            calculateNum.setAverageWorkingHours(calculateNum.getTotalAttendanceDays() == 0 ? 0 : calculateNum.getTotalWorkingHours() / calculateNum.getTotalAttendanceDays());
            calculateNum.setAverageWorkingHoursStr(DateUtil.formatBetween(calculateNum.getAverageWorkingHours(), BetweenFormatter.Level.MINUTE));
            calculateNum.setTotalLateArrivalsTimeStr(DateUtil.formatBetween(calculateNum.getTotalLateArrivalsTime(), BetweenFormatter.Level.MINUTE));
            calculateNum.setTotalEarlyDeparturesTimeStr(DateUtil.formatBetween(calculateNum.getTotalEarlyDeparturesTime(), BetweenFormatter.Level.MINUTE));
            averageWorkingHourVO.setCalculateNum(calculateNum);

            // --- 平均工时
            averageWorkingHours.add(averageWorkingHourVO.setTop(calculateNum.getAverageWorkingHoursStr()).setDown("出勤" + calculateNum.getTotalAttendanceDays() + "天"));
            // --- 迟到
            if (calculateNum.getTotalLateArrivalsNumber() > 0) {
                TeamAttendancePunchStatisticsVO item = new TeamAttendancePunchStatisticsVO();
                BeanUtil.copyProperties(averageWorkingHourVO, item);
                beLateList.add(item.setTop(calculateNum.getTotalLateArrivalsNumber() + "次").setDown(calculateNum.getTotalLateArrivalsTimeStr()));
            }
            // --- 早退
            if (calculateNum.getTotalEarlyDeparturesNumber() > 0) {
                TeamAttendancePunchStatisticsVO item = new TeamAttendancePunchStatisticsVO();
                BeanUtil.copyProperties(averageWorkingHourVO, item);
                leaveEarlyList.add(item.setTop(calculateNum.getTotalEarlyDeparturesNumber() + "次").setDown(calculateNum.getTotalEarlyDeparturesTimeStr()));
            }
            // --- 缺卡
            if (calculateNum.getTotalMissingCardsNumber() > 0) {
                TeamAttendancePunchStatisticsVO item = new TeamAttendancePunchStatisticsVO();
                BeanUtil.copyProperties(averageWorkingHourVO, item);
                missingCardList.add(item.setTop(calculateNum.getTotalMissingCardsNumber() + "次").setDown(""));
            }
            // --- 旷工
            if (calculateNum.getTotalMinerDays() > 0) {
                TeamAttendancePunchStatisticsVO item = new TeamAttendancePunchStatisticsVO();
                BeanUtil.copyProperties(averageWorkingHourVO, item);
                absenteeismList.add(item.setTop(calculateNum.getTotalMinerDays() + "天").setDown(""));
            }
            // --- 外勤
            if (calculateNum.getTotalFieldServiceNumber() > 0) {
                TeamAttendancePunchStatisticsVO item = new TeamAttendancePunchStatisticsVO();
                BeanUtil.copyProperties(averageWorkingHourVO, item);
                fieldServiceList.add(item.setTop(calculateNum.getTotalFieldServiceNumber() + "次").setDown(""));
            }

            // --- 请假漏卡
            if (calculateNum.getTotalLeaveNumber() > 0) {
                TeamAttendancePunchStatisticsVO item = new TeamAttendancePunchStatisticsVO();
                BeanUtil.copyProperties(averageWorkingHourVO, item);
                leaveList.add(item.setTop(calculateNum.getTotalLeaveNumber() + "次请假漏卡").setDown(""));
            }

        }

        vo.setAverageWorkingHoursList(averageWorkingHours);
        vo.setBeLateList(beLateList);
        vo.setLeaveEarlyList(leaveEarlyList);
        vo.setMissingCardList(missingCardList);
        vo.setAbsenteeismList(absenteeismList);
        vo.setFieldServiceList(fieldServiceList);
        vo.setLeaveList(leaveList);

        int sum = averageWorkingHours.stream().mapToInt(a -> a.getCalculateNum().getTotalAttendanceDays()).sum();
        TeamAttendanceStatisticsByCycleVO.TeamAttendanceStatisticsNumVO teamAttendanceStatisticsByCycleVO = new TeamAttendanceStatisticsByCycleVO.TeamAttendanceStatisticsNumVO();
        teamAttendanceStatisticsByCycleVO.setAverageWorkingHours(
                sum == 0 ? 0 : averageWorkingHours.stream().mapToLong(a -> a.getCalculateNum().getTotalWorkingHours()).sum() / sum
        );
        vo.setTeamAttendanceStatisticsNumVO(teamAttendanceStatisticsByCycleVO
                .setBeLateNum(beLateList.stream().mapToInt(a -> a.getCalculateNum().getTotalLateArrivalsNumber()).sum())
                .setLeaveEarlyNum(leaveEarlyList.stream().mapToInt(a -> a.getCalculateNum().getTotalEarlyDeparturesNumber()).sum())
                .setMissingCardNum(missingCardList.stream().mapToInt(a -> a.getCalculateNum().getTotalMissingCardsNumber()).sum())
                .setLeaveNum(leaveList.stream().mapToInt(a -> a.getCalculateNum().getTotalLeaveNumber()).sum())
                .setAbsenteeismNum(absenteeismList.stream().mapToInt(a -> a.getCalculateNum().getTotalMinerDays()).sum())
                .setFieldServiceNum(fieldServiceList.stream().mapToInt(a -> a.getCalculateNum().getTotalFieldServiceNumber()).sum())
        );
        return vo;
    }

    @Override
    public List<AttendanceGroupSystemVO> getTheAttendanceGroupToWhichTheCurrentlyLoggedInUserBelongsAndWhetherTheyHaveAdministratorRights(Boolean allFlag) {
        Long userId = getLoginUserId();
        List<AttendanceGroupSystemVO> attendanceGroupSystemVOS = new ArrayList<>();
        List<AttendanceGroupDO> attendanceGroupDOS = new ArrayList<>();
        if (allFlag) {
            attendanceGroupDOS = attendanceGroupMapper.selectList();
        } else {
            attendanceGroupDOS = attendanceGroupSystemService.getGroupSystemByUserId(userId);
        }
        if (CollectionUtil.isEmpty(attendanceGroupDOS)) {
            return attendanceGroupSystemVOS;
        }
        for (AttendanceGroupDO attendanceGroupDO : attendanceGroupDOS) {
            AttendanceGroupSystemVO attendanceGroupSystemVO = new AttendanceGroupSystemVO();
            attendanceGroupSystemVO.setGroupId(attendanceGroupDO.getId());
            attendanceGroupSystemVO.setGroupName(attendanceGroupDO.getGroupName());
            attendanceGroupSystemVOS.add(attendanceGroupSystemVO);
        }
        return attendanceGroupSystemVOS;
    }

    @Override
    public AttendanceRulesVO getRules(Long groupId) {
        AttendanceRulesVO vo = new AttendanceRulesVO();
        AttendanceGroupDO group = attendanceGroupService.getGroup(groupId);
        PunchService punchService = punchHandler.getResource(AttendanceGroupDO.getCodeByType(group.getType()));
        List<AttendanceRulesVO.WorkRules> workRules = punchService.getRule(groupId);
        vo.setFieldworkFlag(group.getFieldworkFlag());
        vo.setWorkRules(workRules);
        vo.setOfficeLocation(group.getAddress());
        vo.setAttendanceGroupDO(group);
        return vo;
    }

    @Override
    public void exportAttendanceExcel(HttpServletResponse response, ExportAttendanceExcelDTO dto) {
        List<AdminUserDO> userList = new ArrayList<>();
        List<Long> userIds = new ArrayList<>();
        if (dto.getMemberRange() == 1) {
            userList = adminUserService.getAllList(null, null, userIds);
        } else if (dto.getMemberRange() == 2) {
            // -- 获取考勤组ids
            userIds = attendanceGroupUserService.getUserIdsByGroupIds(dto.getTargetIds());
            userList = adminUserService.getAllList(null, null, userIds);
        } else {
            userList = adminUserService.getAllList(null, null, dto.getTargetIds());
        }

        // -- 统计
        List<String> dateList = DateUtils.betweenDayList(dto.getStartTime(), dto.getEndTime());
        String hh_mm = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm"));
        String first = CollectionUtil.getFirst(dateList);
        String last = CollectionUtil.getLast(dateList);
        //查询出数据 -- 时间周期 - 用户列表
        List<AttendancePunchRecordDO> list = attendancePunchRecordMapper.statistics(userList.stream().map(AdminUserDO::getId).collect(Collectors.toList()), dateList);
        //获取用户列表
        Map<Long, AdminUserDO> userMap = new HashMap<>();
        if (CollectionUtil.isNotEmpty(userList)) {
            userMap = userList.stream().collect(Collectors.toMap(AdminUserDO::getId, Function.identity()));
        }
        List<PostDO> userPostList = postMapper.selectList();
        // 根据id分组
        Map<Long, PostDO> postMap = userPostList.stream().collect(Collectors.toMap(PostDO::getId, Function.identity()));

        if (dto.getType() == 1) {
            String headTitle = String.format("月度汇总 统计日期：%s 至 %s", first, last);
            String detailedHead = String.format("月度汇总 统计日期：%s 至 %s %s", first, last, hh_mm);
            this.monthlyStatistics(response, userList, dateList, headTitle, detailedHead, list);
        } else {
            String headTitle = String.format("每日统计 统计日期：%s 至 %s", first, last);
            String detailedHead = String.format("报表生成时间：%s %s", last, hh_mm);
            this.dayStatistics(response, userMap, postMap, dateList, headTitle, detailedHead, list);
        }
    }

    @Override
    public AttendanceRulesVO getRulesByUserId() {
        AttendanceGroupDO activationGroup = attendanceGroupService.getByUserId(getLoginUserId());
        if (activationGroup == null) {
            return null;
        }
        return this.getRules(activationGroup.getId());
    }

    @Override
    public Integer getReplacementCardNum() {
        Long userId = getLoginUserId();
        Integer num = 0;
        String key = "ReplacementCardNum" + "_" + userId + "_" + LocalDateTime.now().format(Constants.REPO_DATE_FORMAT);
        String value = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isEmpty(value)) {
            // -- 获取当前用户
            AttendanceGroupDO group = attendanceGroupService.getByUserId(userId);
            if (group != null) {
                num = group.getReplacementCardNum();
            }
            stringRedisTemplate.opsForValue().set(key, num.toString(), 32, TimeUnit.DAYS);
            return num;
        }
        return Integer.valueOf(value);
    }

    @Override
    public void useReplacementCardNum() {
        this.useReplacementCardNum(null);
    }

    @Override
    public void replacementCard(AttendanceReplacementCardDTO dto) {
        Long userId = getLoginUserId();
        //先减去补卡次数
        this.useReplacementCardNum(userId);
        try {
            // -- 找到这条缺卡记录
            LambdaQueryWrapper<AttendancePunchRecordDO> lambdaQueryWrapper = new LambdaQueryWrapper<AttendancePunchRecordDO>()
                    .eq(AttendancePunchRecordDO::getUserId, userId)
                    .eq(AttendancePunchRecordDO::getAttendanceGroupShiftItemId, dto.getAttendanceGroupShiftItemId())
                    .eq(AttendancePunchRecordDO::getWorkType, dto.getWorkType())
                    .eq(AttendancePunchRecordDO::getActualDayTime, dto.getActualDayTime())
                    .eq(AttendancePunchRecordDO::getStatus, AttendanceOnTheDayDTO.PUNCH_STATUS_MISS);

            List<AttendancePunchRecordDO> list = attendancePunchRecordMapper.selectList(lambdaQueryWrapper);
            int update = 0;
            if (CollectionUtil.isNotEmpty(list)) {
                AttendancePunchRecordDO attendancePunchRecordDO = list.get(0);
                attendancePunchRecordDO.setStatus(AttendanceOnTheDayDTO.REPLACEMENT_CARD);
                attendancePunchRecordDO.setPunchTime(attendancePunchRecordDO.getShouldPunchTime());
                update = attendancePunchRecordMapper.updateById(attendancePunchRecordDO);
            }
            if (CollectionUtil.isEmpty(list) || update <= 0) {
                throw exception(CANNOT_FIND_THE_RECORD_THAT_NEEDS_TO_BE_REPLACED);
            }
        } catch (Exception e) {
            // -- 如果有报错的话 - 那么进行回滚操作 - 并且redis 中的补卡次数也要+1 (redis并不会回滚)
            String key = "ReplacementCardNum" + "_" + userId + "_" + LocalDateTime.now().format(Constants.REPO_DATE_FORMAT);
            stringRedisTemplate.opsForValue().increment(key, 1);
            throw exception(ABNORMAL_CARD_REPLENISHMENT);
        }
    }

    @Override
    public JSONObject attendanceMachineCheck(JSONObject object) {
        JSONObject result = new JSONObject().set("Result", 0).set("Msg", "识别通过");
        JSONObject content = new JSONObject();
        content.set("voice_code", -2);
        result.set("Content", content);
        log.info("verifyUser: {}", object);
        try {
            String sn = object.getStr("sn");
            String userId = object.getStr("user_id");
            content.set("user_id", userId);
            // -- 判断当前是否是工厂用户 - 是的话走工厂员工考勤记录即可；否则走考勤组正常打卡
            AdminUserDO user = adminUserService.getUser(Long.valueOf(userId));
            if (user == null || user.getStatus() == 1) {
                content.set("voice_text", "无效用户");
            } else {
                // -- 公司用户
                if (user.getUserType() == 1) {
                    AttendancePunchVO punch = this.punch(new AttendancePunchDTO()
                            .setUserId(Long.valueOf(userId))
                            .setPunchType(AttendanceGroupDO.PUNCH_TYPE_ATTENDANCE_MACHINE)
                            .setSn(sn));
                    content.set("voice_text", punch.getStatus() == 0 ? "打卡成功" : (punch.getStatus() == 1 ? "迟到打卡成功" : "早退打卡成功"));
                } else {
                    LocalDateTime thisTime = LocalDateTime.now();
                    String actualDayTime = thisTime.format(Constants.REPO_DATE_FORMAT);
                    //获取当前登陆用户部门
                    DeptDO deptDO = deptService.getDept(user.getDeptId());
                    // -- 工厂用户
                    attendanceFactoryPunchRecordService.createAttendanceFactoryPunchRecord(new AttendanceFactoryPunchRecordSaveReqVO()
                            .setUserId(user.getId())
                            .setDeptId(user.getDeptId())
                            .setFactoryId(deptDO.getFactoryId())
                            .setSn(sn)
                            .setActualDayTime(actualDayTime)
                            .setPunchTime(thisTime));
                }
            }
        } catch (ServiceException e) {
            content.set("voice_text", e.getMessage());
        } catch (Exception e) {
            content.set("voice_text", "系统错误");
        } finally {
            result.set("Content", content);
            return result;
        }
    }

    @Override
    public void useReplacementCardNum(Long userId) {
        userId = userId == null ? getLoginUserId() : userId;
        String key = "ReplacementCardNum" + "_" + userId + "_" + LocalDateTime.now().format(Constants.REPO_DATE_FORMAT);
        String value = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isNotEmpty(value)) {
            if (Integer.parseInt(value) > 0) {
                stringRedisTemplate.opsForValue().increment(key, -1);
            } else {
                throw exception(INSUFFICIENT_NUMBER_OF_CARD_REFILLS);
            }
            return;
        }
        // -- 如果redis不存在，则从数据库获取
        Integer num = 0;
        AttendanceGroupDO group = attendanceGroupService.getByUserId(userId);
        if (group != null) {
            num = group.getReplacementCardNum();
        }
        if (num > 0) {
            num = num - 1;
            stringRedisTemplate.opsForValue().set(key, num.toString(), 32, TimeUnit.DAYS);
        } else {
            throw exception(INSUFFICIENT_NUMBER_OF_CARD_REFILLS);
        }
    }


    /**
     * 按日导出
     *
     * @param response
     * @param userMap
     * @param postMap
     * @param dateList
     * @param headTitle
     * @param detailedHead
     * @param list
     */
    private void dayStatistics(HttpServletResponse response, Map<Long, AdminUserDO> userMap, Map<Long, PostDO> postMap, List<String> dateList, String headTitle, String detailedHead, List<AttendancePunchRecordDO> list) {
        List<List<String>> data = new ArrayList<>();
        // -- 根据部门分组 - 根据考勤组分组
        Map<Long, List<AttendancePunchRecordDO>> userPunchMap = list.stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getUserId));
        List<DictDataDO> bpmOaLeaveType = dictDataService.getDictDataList(CommonStatusEnum.ENABLE.getStatus(), "bpm_oa_leave_type");
        //根据字典值分组
        Map<String, String> leaveTypeMap = bpmOaLeaveType.stream().collect(Collectors.toMap(DictDataDO::getValue, DictDataDO::getLabel));
        // -- 先计算下要循环几次 - 一个用户一个部门一个考勤组一个班次一个日期 最大的打卡次数 -
        Map<String, List<AttendancePunchRecordDO>> map = list.stream().collect(Collectors.groupingBy(
                a -> a.getUserId() + "_"
                        + a.getDeptId() + "_"
                        + a.getAttendanceGroupId() + "_"
                        + a.getAttendanceGroupShiftId() + "_"
                        + a.getDayTime()));

        int maxSize = map.values().stream().mapToInt(List::size).max().orElse(0);
        // -- 如果膜2大于0
        maxSize = (maxSize % 2) > 0 ? maxSize / 2 + 1 : maxSize / 2;

        //先根据人员分组 - 再根据部门分组 - 再根据考勤组分组 - 再根据日期分组
        long time = System.currentTimeMillis();
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> entry : userPunchMap.entrySet()) {
            AdminUserDO adminUserDO = userMap.get(entry.getKey());
            List<String> postNames = this.getPostNames(adminUserDO, postMap);


            Map<Long, List<AttendancePunchRecordDO>> deptMap = entry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getDeptId));
            for (Map.Entry<Long, List<AttendancePunchRecordDO>> deptEntry : deptMap.entrySet()) {
                Map<Long, List<AttendancePunchRecordDO>> groupMap = deptEntry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getAttendanceGroupId));

                for (Map.Entry<Long, List<AttendancePunchRecordDO>> groupEntry : groupMap.entrySet()) {
                    //按日期分组
                    Map<String, List<AttendancePunchRecordDO>> dayMap = groupEntry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getDayTime, TreeMap::new, Collectors.toList()));

                    for (String dateStr : dateList) {
                        List<AttendancePunchRecordDO> items = dayMap.get(dateStr);
                        Map<Long, List<AttendancePunchRecordDO>> groupShiftMap = new HashMap<>();
                        if (CollectionUtil.isNotEmpty(items)) {
                            groupShiftMap = items.stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getAttendanceGroupShiftId));
                        } else {
                            groupShiftMap.put(-1L, Collections.emptyList());
                        }
                        for (Map.Entry<Long, List<AttendancePunchRecordDO>> groupShiftEntry : groupShiftMap.entrySet()) {
                            List<String> row = new ArrayList<>();
                            row.add(adminUserDO.getNickname());
                            row.add(groupEntry.getValue().get(0).getAttendanceGroupName());
                            row.add(groupEntry.getValue().get(0).getDeptName());
                            row.add(String.join("/", postNames));
                            row.add(dateStr);
                            row.add(CollectionUtil.isEmpty(groupShiftEntry.getValue()) ? "休息" : groupShiftEntry.getValue().get(0).getAttendanceGroupShiftName());
                            Map<Long, List<AttendancePunchRecordDO>> workMap = groupShiftEntry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getAttendanceGroupShiftItemId, TreeMap::new, Collectors.toList()));

                            for (Map.Entry<Long, List<AttendancePunchRecordDO>> groupShiftItemEntry : workMap.entrySet()) {
                                for (AttendancePunchRecordDO attendancePunchRecordDO : groupShiftItemEntry.getValue()) {
                                    row.add(attendancePunchRecordDO.getPunchTime() == null ? "/" : attendancePunchRecordDO.getPunchTime().format(Constants.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND));
                                    String statusStr = attendancePunchRecordDO.getStatus().equals(AttendanceOnTheDayDTO.ASK_FOR_LEAVE) ? "请假-" + leaveTypeMap.get(attendancePunchRecordDO.getLeaveType()) : this.statusToStr(attendancePunchRecordDO.getStatus());
                                    row.add(statusStr);
                                }
                            }
                            if (maxSize > workMap.entrySet().size()) {
                                for (int i = 0; i < maxSize - workMap.entrySet().size(); i++) {
                                    row.add("/");
                                    row.add("/");
                                    row.add("/");
                                    row.add("/");
                                }
                            }

                            //工时
                            String workHour = this.calculateAverageWorkingHour(workMap);
                            row.add(workHour);
                            //迟到时长
                            String beLate = this.calculateBeLate(workMap);
                            row.add(beLate);
                            //早退时长
                            String leaveEarly = this.calculateEarlyDepartures(workMap);
                            row.add(leaveEarly);
                            //请假漏卡次数
                            String leaveList = this.calculateLeaveList(workMap);
                            row.add(leaveList);

                            Map<Integer, Integer> missingCardsMap = this.calculateCommuteMissingCardsList(workMap);
                            row.add(missingCardsMap.get(Constants.ZERO).toString());
                            row.add(missingCardsMap.get(Constants.ONE).toString());
                            data.add(row);
                        }
                    }
                }
            }
        }
        log.info("考勤按日统计耗时:{}", (System.currentTimeMillis() - time));
        // 这里 需要指定写用哪个class去写，然后写到第一个sheet，名字为模板 然后文件流会自动关闭

        try {
            EasyExcel.write(response.getOutputStream())
//            EasyExcel.write("/Users/aikai/Downloads/" + System.currentTimeMillis() + "考勤统计按日导出.xls")
                    .head(generateDailyHead(headTitle, detailedHead, maxSize))
                    .autoCloseStream(false)
                    .excelType(ExcelTypeEnum.XLS)
                    .registerWriteHandler(new CustomCellStyleHandler())
                    .sheet("考勤统计按日导出")
                    .doWrite(data);
            response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("考勤统计", StandardCharsets.UTF_8.name()));
            response.setContentType("application/vnd.ms-excel;charset=UTF-8");

        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 获取岗位名称
     *
     * @param adminUserDO
     * @param postMap
     * @return
     */
    private List<String> getPostNames(AdminUserDO adminUserDO, Map<Long, PostDO> postMap) {
        if (adminUserDO.getPostIds() != null) {
            return adminUserDO.getPostIds().stream()
                    .map(postMap::get)
                    .filter(Objects::nonNull)
                    .map(PostDO::getName)
                    .collect(Collectors.toList());
        }
        return Collections.emptyList();
    }


    /**
     * 状态转换
     *
     * @param status
     * @return
     */
    private String statusToStr(int status) {
        if (status == 0) {
            return "正常";
        } else if (status == 1) {
            return "迟到";
        } else if (status == 2) {
            return "早退";
        } else if (status == 3) {
            return "缺卡";
        } else if (status == 4) {
            return "未打卡";
        } else if (status == 5) {
            return "补卡";
        } else if (status == 6) {
            return "请假";
        }
        return "";
    }

    /**
     * 按月导出
     *
     * @param response
     * @param userList
     * @param dateList
     * @param headTitle
     * @param detailedHead
     * @param list
     */
    private void monthlyStatistics(HttpServletResponse response, List<AdminUserDO> userList, List<String> dateList, String headTitle, String detailedHead, List<AttendancePunchRecordDO> list) {
        List<List<String>> data = new ArrayList<>();

        // -- 根据部门分组 - 根据考勤组分组
        Map<Long, List<AttendancePunchRecordDO>> userPunchMap = list.stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getUserId));

        //获取用户列表
        Map<Long, AdminUserDO> userMap = new HashMap<>();
        if (CollectionUtil.isNotEmpty(userList)) {
            userMap = userList.stream().collect(Collectors.toMap(AdminUserDO::getId, Function.identity()));
        }
        List<PostDO> userPostList = postMapper.selectList();
        // 根据id分组
        Map<Long, PostDO> postMap = userPostList.stream().collect(Collectors.toMap(PostDO::getId, Function.identity()));

        //先根据人员分组 - 再根据部门分组 - 再根据考勤组分组 - 再根据日期分组
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> entry : userPunchMap.entrySet()) {
            AdminUserDO adminUserDO = userMap.get(entry.getKey());
            List<String> postNames = this.getPostNames(adminUserDO, postMap);
            Map<Long, List<AttendancePunchRecordDO>> deptMap = entry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getDeptId));
            for (Map.Entry<Long, List<AttendancePunchRecordDO>> deptEntry : deptMap.entrySet()) {
                Map<Long, List<AttendancePunchRecordDO>> groupMap = deptEntry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getAttendanceGroupId));

                for (Map.Entry<Long, List<AttendancePunchRecordDO>> groupEntry : groupMap.entrySet()) {
                    CalculateNum calculateNum = new CalculateNum();
                    //按日期分组
                    Map<String, List<AttendancePunchRecordDO>> dayMap = groupEntry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getDayTime));
                    for (Map.Entry<String, List<AttendancePunchRecordDO>> dayEntry : dayMap.entrySet()) {
                        // -- 按照班次子表分组
                        Map<Long, List<AttendancePunchRecordDO>> workMap = dayEntry.getValue().stream().collect(Collectors.groupingBy(AttendancePunchRecordDO::getAttendanceGroupShiftItemId));
                        this.calculateAverageWorkingHour(workMap, calculateNum);
                        // -- 出考勤天数
                        this.calculateAttendanceDays(dayEntry.getValue(), calculateNum);
                        // -- 迟到
                        this.calculateBeLate(workMap, calculateNum);
                        // -- 早退
                        this.calculateEarlyDepartures(workMap, calculateNum);
                        // -- 缺卡
                        this.calculateCommuteMissingCardsList(workMap, calculateNum);
                        // -- 旷工
                        this.calculateMiner(dayEntry.getValue(), calculateNum);
                        // -- 请假
                        this.calculateLeaveList(workMap, calculateNum);
                    }
                    //休息天数
                    this.calculateRestDay(dateList, dayMap.keySet().stream().distinct().collect(Collectors.toList()), calculateNum);
                    //考勤总时间中文
                    calculateNum.setTotalWorkingHoursStr(DateUtils.millisecondsToHoursAndMinutes(calculateNum.getTotalWorkingHours()));
                    //迟到总时间中文
                    calculateNum.setTotalLateArrivalsTimeStr(DateUtil.formatBetween(calculateNum.getTotalLateArrivalsTime(), BetweenFormatter.Level.MINUTE));
                    //早退总时间中文
                    calculateNum.setTotalEarlyDeparturesTimeStr(DateUtil.formatBetween(calculateNum.getTotalEarlyDeparturesTime(), BetweenFormatter.Level.MINUTE));

                    List<String> row = new ArrayList<>();
                    row.add(adminUserDO.getNickname());
                    row.add(groupEntry.getValue().get(0).getAttendanceGroupName());
                    row.add(groupEntry.getValue().get(0).getDeptName());
                    row.add(String.join("/", postNames));
                    row.add(String.valueOf(calculateNum.getTotalAttendanceDays()));
                    row.add(String.valueOf(calculateNum.getTotalRestDays()));
                    row.add(String.valueOf(calculateNum.getTotalWorkingHoursStr()));
                    row.add(String.valueOf(calculateNum.getTotalLateArrivalsNumber()));
                    row.add(String.valueOf(calculateNum.getTotalLateArrivalsTimeStr()));

                    row.add(String.valueOf(calculateNum.getTotalEarlyDeparturesNumber()));
                    row.add(String.valueOf(calculateNum.getTotalEarlyDeparturesTimeStr()));
                    row.add(String.valueOf(calculateNum.getTotalLeaveNumber()));
                    row.add(String.valueOf(calculateNum.getTotalUpMissingCardsNumber()));
                    row.add(String.valueOf(calculateNum.getTotalDownMissingCardsNumber()));
                    row.add(String.valueOf(calculateNum.getTotalMinerDays()));
                    data.add(row);
                }
            }
        }

        try {
            EasyExcel.write(response.getOutputStream())
//            EasyExcel.write("/Users/aikai/Downloads/" + System.currentTimeMillis() + "考勤统计按日导出.xls")
                    .head(generateHead(headTitle, detailedHead))
                    .autoCloseStream(false)
                    .excelType(ExcelTypeEnum.XLS)
                    .registerWriteHandler(new CustomCellStyleHandler())
                    .sheet("考勤统计按月导出")
                    .doWrite(data);
            response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("考勤统计", StandardCharsets.UTF_8.name()));
            response.setContentType("application/vnd.ms-excel;charset=UTF-8");

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    public static List<List<String>> generateHead(String headTitle, String detailedHead) {
        List<List<String>> head = new ArrayList<>();
        head.add(Arrays.asList(headTitle, detailedHead, NAME, NAME));
        head.add(Arrays.asList(headTitle, detailedHead, ATTENDANCE_GROUP, ATTENDANCE_GROUP));
        head.add(Arrays.asList(headTitle, detailedHead, DEPT, DEPT));
        head.add(Arrays.asList(headTitle, detailedHead, POSITION, POSITION));
        head.add(Arrays.asList(headTitle, detailedHead, NUMBER_OF_DAYS_OF_ATTENDANCE, NUMBER_OF_DAYS_OF_ATTENDANCE));
        head.add(Arrays.asList(headTitle, detailedHead, REST_DAYS, REST_DAYS));
        head.add(Arrays.asList(headTitle, detailedHead, LENGTH_OF_WORK, LENGTH_OF_WORK));
        head.add(Arrays.asList(headTitle, detailedHead, NUMBER_OF_TIMES_LATE, NUMBER_OF_TIMES_LATE));
        head.add(Arrays.asList(headTitle, detailedHead, LENGTH_OF_LATENESS, LENGTH_OF_LATENESS));
        head.add(Arrays.asList(headTitle, detailedHead, NUMBER_OF_EARLY_DEPARTURES, NUMBER_OF_EARLY_DEPARTURES));
        head.add(Arrays.asList(headTitle, detailedHead, EARLY_DEPARTURE_DURATION, EARLY_DEPARTURE_DURATION));
        head.add(Arrays.asList(headTitle, detailedHead, NUMBER_OF_LEAVE_REQUESTS_AND_MISSING_CARDS, NUMBER_OF_LEAVE_REQUESTS_AND_MISSING_CARDS));
        head.add(Arrays.asList(headTitle, detailedHead, NUMBER_OF_ABSENCES_AT_WORK, NUMBER_OF_ABSENCES_AT_WORK));
        head.add(Arrays.asList(headTitle, detailedHead, NUMBER_OF_MISSING_CARDS_AFTER_GET_OFF_WORK, NUMBER_OF_MISSING_CARDS_AFTER_GET_OFF_WORK));
        head.add(Arrays.asList(headTitle, detailedHead, NUMBER_OF_DAYS_ABSENT_FROM_WORK, NUMBER_OF_DAYS_ABSENT_FROM_WORK));
//        head.add(Arrays.asList(headTitle, detailedHead, "出差时长", "出差时长"));
//        head.add(Arrays.asList(headTitle, detailedHead, "外出时长", "外出时长"));
//        head.add(Arrays.asList(headTitle, detailedHead, "加班-审批单统计", "加班-审批单统计"));
//        head.add(Arrays.asList(headTitle, detailedHead, "加班时长-按加班规则计算", "工作日加班"));
//        head.add(Arrays.asList(headTitle, detailedHead, "加班时长-按加班规则计算", "休息日加班"));
//        head.add(Arrays.asList(headTitle, detailedHead, "加班时长-按加班规则计算", "节假日加班"));
//        head.add(Arrays.asList(headTitle, detailedHead, "考勤结果", "六"));
//        head.add(Arrays.asList(headTitle, detailedHead, "考勤结果", "日"));
//        head.add(Arrays.asList(headTitle, detailedHead, "考勤结果", "1"));
//        head.add(Arrays.asList(headTitle, detailedHead, "考勤结果", "2"));
//        head.add(Arrays.asList(headTitle, detailedHead, "考勤结果", "3"));
//        head.add(Arrays.asList(headTitle, detailedHead, "考勤结果", "4"));
//        head.add(Arrays.asList(headTitle, detailedHead, "考勤结果", "5"));
//        head.add(Arrays.asList(headTitle, detailedHead, "考勤结果", "六"));
//        head.add(Arrays.asList(headTitle, detailedHead, "考勤结果", "日"));
        // 添加更多表头
        return head;
    }

    public static List<List<String>> generateDailyHead(String headTitle, String detailedHead, int size) {
        List<List<String>> head = new ArrayList<>();
        head.add(Arrays.asList(headTitle, detailedHead, NAME, NAME));
        head.add(Arrays.asList(headTitle, detailedHead, ATTENDANCE_GROUP, ATTENDANCE_GROUP));
        head.add(Arrays.asList(headTitle, detailedHead, DEPT, DEPT));
        head.add(Arrays.asList(headTitle, detailedHead, POSITION, POSITION));
        head.add(Arrays.asList(headTitle, detailedHead, DATE, DATE));
        head.add(Arrays.asList(headTitle, detailedHead, SHIFT, SHIFT));
        for (int i = 1; i <= size; i++) {
            head.add(Arrays.asList(headTitle, detailedHead, UP_WORK_STR + i + CHECK_IN_TIME, UP_WORK_STR + i + CHECK_IN_TIME));
            head.add(Arrays.asList(headTitle, detailedHead, UP_WORK_STR + i + CHECK_IN_RESULT, UP_WORK_STR + i + CHECK_IN_RESULT));
            head.add(Arrays.asList(headTitle, detailedHead, DOWN_WORK_STR + i + CHECK_IN_TIME, DOWN_WORK_STR + i + CHECK_IN_TIME));
            head.add(Arrays.asList(headTitle, detailedHead, DOWN_WORK_STR + i + CHECK_IN_RESULT, DOWN_WORK_STR + i + CHECK_IN_RESULT));
        }
//        head.add(Arrays.asList(headTitle, detailedHead, "关联的审批单", "关联的审批单"));
//        head.add(Arrays.asList(headTitle, detailedHead, "出勤天数", "出勤天数"));
//        head.add(Arrays.asList(headTitle, detailedHead, "休息天数", "休息天数"));
        head.add(Arrays.asList(headTitle, detailedHead, LENGTH_OF_WORK, LENGTH_OF_WORK));
//        head.add(Arrays.asList(headTitle, detailedHead, "迟到次数", "迟到次数"));
        head.add(Arrays.asList(headTitle, detailedHead, LENGTH_OF_LATENESS, LENGTH_OF_LATENESS));
//        head.add(Arrays.asList(headTitle, detailedHead, "早退次数", "早退次数"));
        head.add(Arrays.asList(headTitle, detailedHead, EARLY_DEPARTURE_DURATION, EARLY_DEPARTURE_DURATION));
        head.add(Arrays.asList(headTitle, detailedHead, NUMBER_OF_LEAVE_REQUESTS_AND_MISSING_CARDS, NUMBER_OF_LEAVE_REQUESTS_AND_MISSING_CARDS));
        head.add(Arrays.asList(headTitle, detailedHead, NUMBER_OF_ABSENCES_AT_WORK, NUMBER_OF_ABSENCES_AT_WORK));
        head.add(Arrays.asList(headTitle, detailedHead, NUMBER_OF_MISSING_CARDS_AFTER_GET_OFF_WORK, NUMBER_OF_MISSING_CARDS_AFTER_GET_OFF_WORK));
//        head.add(Arrays.asList(headTitle, detailedHead, "旷工天数", "旷工天数"));
        return head;
    }


    /**
     * 外勤计算
     *
     * @param day
     * @param weekChinese
     * @param workMap
     * @param calculateNum
     * @return
     */
    private List<AttendancePunchStatisticsVO> calculateFieldService(String day, String weekChinese, Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        List<AttendancePunchStatisticsVO> list = new ArrayList<>();
        int todayFieldService = 0;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 缺卡计算
                List<AttendancePunchRecordDO> fieldServiceList = workEntry.getValue().stream().filter(a -> Constants.TRUE.equals(a.getFieldServiceFlag())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(fieldServiceList)) {
                    todayFieldService += fieldServiceList.size();
                    for (AttendancePunchRecordDO attendancePunchRecordDO : fieldServiceList) {
                        AttendancePunchStatisticsVO vo = new AttendancePunchStatisticsVO();
                        vo.setDay(day);
                        vo.setWeek(weekChinese);
                        vo.setShouldPunchTime(attendancePunchRecordDO.getShouldPunchTime().format(DateTimeFormatter.ofPattern("HH:mm")));
                        list.add(vo);
                    }
                }
            }
        }
        calculateNum.setTotalFieldServiceNumber(calculateNum.getTotalFieldServiceNumber() + todayFieldService);
        return list;
    }


    private void calculateFieldService(Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        int todayFieldService = 0;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 缺卡计算
                List<AttendancePunchRecordDO> fieldServiceList = workEntry.getValue().stream().filter(a -> Constants.TRUE.equals(a.getFieldServiceFlag())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(fieldServiceList)) {
                    todayFieldService += fieldServiceList.size();
                }
            }
        }
        calculateNum.setTotalFieldServiceNumber(calculateNum.getTotalFieldServiceNumber() + todayFieldService);
    }

    /**
     * 计算矿工
     *
     * @param day
     * @param weekChinese
     * @param calculateNum
     * @return
     */
    private AttendancePunchStatisticsVO calculateMiner(String day, String weekChinese, List<AttendancePunchRecordDO> list, CalculateNum calculateNum) {
        AttendancePunchStatisticsVO vo = null;
        List<AttendancePunchRecordDO> collect = list.stream().filter(a -> a.getStatus().equals(AttendanceOnTheDayDTO.PUNCH_STATUS_MISS)).collect(Collectors.toList());
        if (collect.size() == list.size()) {
            vo = new AttendancePunchStatisticsVO();
            vo.setDay(day);
            vo.setWeek(weekChinese);
            calculateNum.setTotalMinerDays(calculateNum.getTotalMinerDays() + 1);
        }
        return vo;
    }

    private void calculateMiner(List<AttendancePunchRecordDO> list, CalculateNum calculateNum) {
        List<AttendancePunchRecordDO> collect = list.stream().filter(a -> a.getStatus().equals(AttendanceOnTheDayDTO.PUNCH_STATUS_MISS)).collect(Collectors.toList());
        if (collect.size() == list.size()) {
            calculateNum.setTotalMinerDays(calculateNum.getTotalMinerDays() + 1);
        }
    }

    /**
     * 休息时间计算
     *
     * @param dateList
     * @param attendanceTime
     * @param calculateNum
     * @return
     */
    private List<AttendancePunchStatisticsVO> calculateRestDayList(List<String> dateList, List<String> attendanceTime, CalculateNum calculateNum) {
        List<AttendancePunchStatisticsVO> list = new ArrayList<>();
        List<String> subtract = new ArrayList<>(CollectionUtil.subtract(dateList, attendanceTime));
        for (String timeStr : subtract) {
            AttendancePunchStatisticsVO vo = new AttendancePunchStatisticsVO();
            vo.setDay(timeStr);
            vo.setWeek(dayOfWeekEnum(DateUtil.parse(timeStr)).toChinese());
            list.add(vo);
        }
        calculateNum.setTotalRestDays(list.size());
        return list;
    }

    /**
     * 休息时间计算
     *
     * @param dateList
     * @param attendanceTime
     * @param calculateNum
     * @return
     */
    private void calculateRestDay(List<String> dateList, List<String> attendanceTime, CalculateNum calculateNum) {
        List<String> subtract = new ArrayList<>(CollectionUtil.subtract(dateList, attendanceTime));
        calculateNum.setTotalRestDays(subtract.size());
    }

    /**
     * @param day
     * @param weekChinese
     * @param workMap
     * @param calculateNum
     * @return
     */
    private List<AttendancePunchStatisticsVO> calculateMissingCardsList(String day, String weekChinese, Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        List<AttendancePunchStatisticsVO> list = new ArrayList<>();
        int todayMissingCardsNumber = 0;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 缺卡计算
                List<AttendancePunchRecordDO> missingCardsList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.PUNCH_STATUS_MISS.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(missingCardsList)) {
                    todayMissingCardsNumber += missingCardsList.size();
                    for (AttendancePunchRecordDO attendancePunchRecordDO : missingCardsList) {
                        AttendancePunchStatisticsVO earlyDepartures = new AttendancePunchStatisticsVO();
                        earlyDepartures.setDay(day);
                        earlyDepartures.setWeek(weekChinese);
                        earlyDepartures.setShouldPunchTime(attendancePunchRecordDO.getShouldPunchTime().format(DateTimeFormatter.ofPattern("HH:mm")));
                        list.add(earlyDepartures);
                    }
                }
            }
        }
        calculateNum.setTotalMissingCardsNumber(calculateNum.getTotalMissingCardsNumber() + todayMissingCardsNumber);
        return list;
    }

    private void calculateMissingCardsList(Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        int todayMissingCardsNumber = 0;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 缺卡计算
                List<AttendancePunchRecordDO> missingCardsList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.PUNCH_STATUS_MISS.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(missingCardsList)) {
                    todayMissingCardsNumber += missingCardsList.size();
                }
            }
        }
        calculateNum.setTotalMissingCardsNumber(calculateNum.getTotalMissingCardsNumber() + todayMissingCardsNumber);
    }

    private void calculateCommuteMissingCardsList(Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        int todayUpMissingCardsNumber = 0;
        int todayDownMissingCardsNumber = 0;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 缺卡计算
                List<AttendancePunchRecordDO> missingCardsList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.PUNCH_STATUS_MISS.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(missingCardsList)) {
                    long upNum = missingCardsList.stream().filter(a -> Constants.ZERO.equals(a.getWorkType())).count();
                    long downNum = missingCardsList.stream().filter(a -> Constants.ONE.equals(a.getWorkType())).count();
                    todayUpMissingCardsNumber += upNum;
                    todayDownMissingCardsNumber += downNum;
                }
            }
        }
        calculateNum.setTotalUpMissingCardsNumber(calculateNum.getTotalUpMissingCardsNumber() + todayUpMissingCardsNumber);
        calculateNum.setTotalDownMissingCardsNumber(calculateNum.getTotalDownMissingCardsNumber() + todayDownMissingCardsNumber);
    }

    /**
     * 缺卡次数 - 上下班区分开
     *
     * @param workMap
     */
    private Map<Integer, Integer> calculateCommuteMissingCardsList(Map<Long, List<AttendancePunchRecordDO>> workMap) {
        int todayUpMissingCardsNumber = 0;
        int todayDownMissingCardsNumber = 0;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 缺卡计算
                List<AttendancePunchRecordDO> missingCardsList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.PUNCH_STATUS_MISS.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(missingCardsList)) {
                    long upNum = missingCardsList.stream().filter(a -> Constants.ZERO.equals(a.getWorkType())).count();
                    long downNum = missingCardsList.stream().filter(a -> Constants.ONE.equals(a.getWorkType())).count();
                    todayUpMissingCardsNumber += upNum;
                    todayDownMissingCardsNumber += downNum;
                }
            }
        }
        Map<Integer, Integer> map = new HashMap<>();
        map.put(Constants.ZERO, todayUpMissingCardsNumber);
        map.put(Constants.ONE, todayDownMissingCardsNumber);
        return map;
    }

    /**
     * 早退计算
     *
     * @param day
     * @param weekChinese
     * @param workMap
     * @param calculateNum
     * @return
     */
    private List<AttendancePunchStatisticsVO> calculateEarlyDepartures(String day, String weekChinese, Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        List<AttendancePunchStatisticsVO> list = new ArrayList<>();
        int todayEarlyDeparturesNumber = 0;
        long todayEarlyDeparturesTime = 0L;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 早退计算
                List<AttendancePunchRecordDO> earlyDeparturesList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.PUNCH_STATUS_LEAVE_EARLY.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(earlyDeparturesList)) {
                    todayEarlyDeparturesNumber += earlyDeparturesList.size();
                    for (AttendancePunchRecordDO attendancePunchRecordDO : earlyDeparturesList) {
                        AttendancePunchStatisticsVO earlyDepartures = new AttendancePunchStatisticsVO();
                        earlyDepartures.setDay(day);
                        earlyDepartures.setWeek(weekChinese);
                        earlyDepartures.setShouldPunchTime(attendancePunchRecordDO.getShouldPunchTime().format(DateTimeFormatter.ofPattern("HH:mm")));
                        earlyDepartures.setTime("下班早退" + DateUtil.formatBetween(attendancePunchRecordDO.getLeaveEarlyTime(), BetweenFormatter.Level.MINUTE));
                        list.add(earlyDepartures);
                    }
                    todayEarlyDeparturesTime += earlyDeparturesList.stream().mapToLong(AttendancePunchRecordDO::getLeaveEarlyTime).sum();
                }
            }
        }
        calculateNum.setTotalEarlyDeparturesNumber(calculateNum.getTotalEarlyDeparturesNumber() + todayEarlyDeparturesNumber);
        calculateNum.setTotalEarlyDeparturesTime(calculateNum.getTotalEarlyDeparturesTime() + todayEarlyDeparturesTime);
        return list;
    }


    private void calculateEarlyDepartures(Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        int todayEarlyDeparturesNumber = 0;
        long todayEarlyDeparturesTime = 0L;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 早退计算
                List<AttendancePunchRecordDO> earlyDeparturesList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.PUNCH_STATUS_LEAVE_EARLY.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(earlyDeparturesList)) {
                    todayEarlyDeparturesNumber += earlyDeparturesList.size();
                    todayEarlyDeparturesTime += earlyDeparturesList.stream().mapToLong(AttendancePunchRecordDO::getLeaveEarlyTime).sum();
                }
            }
        }
        calculateNum.setTotalEarlyDeparturesNumber(calculateNum.getTotalEarlyDeparturesNumber() + todayEarlyDeparturesNumber);
        calculateNum.setTotalEarlyDeparturesTime(calculateNum.getTotalEarlyDeparturesTime() + todayEarlyDeparturesTime);
    }

    private String calculateEarlyDepartures(Map<Long, List<AttendancePunchRecordDO>> workMap) {
        long todayEarlyDeparturesTime = 0L;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 早退计算
                List<AttendancePunchRecordDO> earlyDeparturesList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.PUNCH_STATUS_LEAVE_EARLY.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(earlyDeparturesList)) {
                    todayEarlyDeparturesTime += earlyDeparturesList.stream().mapToLong(AttendancePunchRecordDO::getLeaveEarlyTime).sum();
                }
            }
        }
        return DateUtil.formatBetween(todayEarlyDeparturesTime, BetweenFormatter.Level.MINUTE);
    }

    /**
     * 计算迟到
     *
     * @param day
     * @param weekChinese
     * @param workMap
     * @param calculateNum
     * @return
     */
    private List<AttendancePunchStatisticsVO> calculateBeLate(String day, String weekChinese, Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        List<AttendancePunchStatisticsVO> list = new ArrayList<>();
        int todayLateArrivalsNumber = 0;
        long todayLateArrivalsTime = 0L;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 迟到计算
                List<AttendancePunchRecordDO> beLateList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.PUNCH_STATUS_LATE.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(beLateList)) {
                    todayLateArrivalsNumber += beLateList.size();
                    for (AttendancePunchRecordDO attendancePunchRecordDO : beLateList) {
                        AttendancePunchStatisticsVO beLateDay = new AttendancePunchStatisticsVO();
                        beLateDay.setDay(day);
                        beLateDay.setWeek(weekChinese);
                        beLateDay.setShouldPunchTime(attendancePunchRecordDO.getShouldPunchTime().format(DateTimeFormatter.ofPattern("HH:mm")));
                        beLateDay.setTime("上班迟到" + DateUtil.formatBetween(attendancePunchRecordDO.getLateTime(), BetweenFormatter.Level.MINUTE));
                        //将时间戳lateTime 转换成 **小时**分格式 不满1小时的时间不显小时
                        list.add(beLateDay);
                    }
                    todayLateArrivalsTime += beLateList.stream().mapToLong(AttendancePunchRecordDO::getLateTime).sum();
                }
            }
        }
        calculateNum.setTotalLateArrivalsNumber(calculateNum.getTotalLateArrivalsNumber() + todayLateArrivalsNumber);
        calculateNum.setTotalLateArrivalsTime(calculateNum.getTotalLateArrivalsTime() + todayLateArrivalsTime);
        return list;
    }


    private void calculateBeLate(Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        int todayLateArrivalsNumber = 0;
        long todayLateArrivalsTime = 0L;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 迟到计算
                List<AttendancePunchRecordDO> beLateList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.PUNCH_STATUS_LATE.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(beLateList)) {
                    todayLateArrivalsNumber += beLateList.size();
                    todayLateArrivalsTime += beLateList.stream().mapToLong(AttendancePunchRecordDO::getLateTime).sum();
                }
            }
        }
        calculateNum.setTotalLateArrivalsNumber(calculateNum.getTotalLateArrivalsNumber() + todayLateArrivalsNumber);
        calculateNum.setTotalLateArrivalsTime(calculateNum.getTotalLateArrivalsTime() + todayLateArrivalsTime);
    }

    /**
     * 迟到时长
     *
     * @param workMap
     * @return
     */
    private String calculateBeLate(Map<Long, List<AttendancePunchRecordDO>> workMap) {
        long todayLateArrivalsTime = 0L;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 迟到计算
                List<AttendancePunchRecordDO> beLateList = workEntry.getValue().stream().filter(a -> AttendanceOnTheDayDTO.PUNCH_STATUS_LATE.equals(a.getStatus())).collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(beLateList)) {
                    todayLateArrivalsTime += beLateList.stream().mapToLong(AttendancePunchRecordDO::getLateTime).sum();
                }
            }
        }
        return DateUtil.formatBetween(todayLateArrivalsTime, BetweenFormatter.Level.MINUTE);
    }

    /**
     * 计算考勤工时
     *
     * @param day
     * @param weekChinese
     * @param workMap
     * @param calculateNum
     * @return
     */
    private AttendancePunchStatisticsVO calculateAverageWorkingHour(String day, String weekChinese, Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        AttendancePunchStatisticsVO vo = new AttendancePunchStatisticsVO();
        long todayWorkingHours = this.calculateAverageWorkingHour(workMap, calculateNum);
        vo.setDay(day);
        vo.setWeek(weekChinese);
        vo.setTime(DateUtil.formatBetween(todayWorkingHours, BetweenFormatter.Level.MINUTE));
        return vo;
    }

    /**
     * 计算工时
     *
     * @param workMap
     * @return
     */
    private String calculateAverageWorkingHour(Map<Long, List<AttendancePunchRecordDO>> workMap) {
        long todayWorkingHours = this.calculateAverageWorkingHourDay(workMap);
        return DateUtil.formatBetween(todayWorkingHours, BetweenFormatter.Level.MINUTE);
    }

    /**
     * 计算考勤工时
     *
     * @param workMap
     * @return
     */
    private long calculateAverageWorkingHour(Map<Long, List<AttendancePunchRecordDO>> workMap, CalculateNum calculateNum) {
        long todayWorkingHours = 0L;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 工时计算 - 有上班和下班两个卡 才计算到工时里面
                if (workEntry.getValue().size() >= 2) {
                    AttendancePunchRecordDO first = CollectionUtil.getFirst(workEntry.getValue());
                    AttendancePunchRecordDO last = CollectionUtil.getLast(workEntry.getValue());
                    if (ObjectUtil.isNotEmpty(first.getPunchTime()) && ObjectUtil.isNotEmpty(last.getPunchTime())) {
                        long time = Math.abs(LocalDateTimeUtil.between(first.getPunchTime(), last.getPunchTime(), ChronoUnit.MILLIS));
                        todayWorkingHours += time;
                    }
                }
            }
        }
        calculateNum.setTotalWorkingHours(calculateNum.getTotalWorkingHours() + todayWorkingHours);
        return todayWorkingHours;
    }

    /**
     * 计算工时 - 当天
     *
     * @param workMap
     * @return
     */
    private long calculateAverageWorkingHourDay(Map<Long, List<AttendancePunchRecordDO>> workMap) {
        long todayWorkingHours = 0L;
        for (Map.Entry<Long, List<AttendancePunchRecordDO>> workEntry : workMap.entrySet()) {
            if (CollectionUtil.isNotEmpty(workEntry.getValue())) {
                // -- 工时计算 - 有上班和下班两个卡 才计算到工时里面
                if (workEntry.getValue().size() >= 2) {
                    AttendancePunchRecordDO first = CollectionUtil.getFirst(workEntry.getValue());
                    AttendancePunchRecordDO last = CollectionUtil.getLast(workEntry.getValue());
                    if (ObjectUtil.isNotEmpty(first.getPunchTime()) && ObjectUtil.isNotEmpty(last.getPunchTime())) {
                        long time = Math.abs(LocalDateTimeUtil.between(first.getPunchTime(), last.getPunchTime(), ChronoUnit.MILLIS));
                        todayWorkingHours += time;
                    }
                }
            }
        }
        return todayWorkingHours;
    }


    /**
     * 计算出勤天数
     *
     * @param day
     * @param weekChinese
     * @param list
     * @param calculateNum
     * @return
     */
    private AttendancePunchStatisticsVO calculateAttendanceDays(String day, String weekChinese, List<AttendancePunchRecordDO> list, CalculateNum calculateNum) {
        AttendancePunchStatisticsVO averageWorkingHourVO = null;
        List<AttendancePunchRecordDO> collect = list.stream().filter(a -> ObjectUtil.isNotEmpty(a.getPunchTime())).collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(collect)) {
            averageWorkingHourVO = new AttendancePunchStatisticsVO();
            averageWorkingHourVO.setDay(day);
            averageWorkingHourVO.setWeek(weekChinese);
            calculateNum.setTotalAttendanceDays(calculateNum.getTotalAttendanceDays() + 1);
        }
        return averageWorkingHourVO;
    }

    private void calculateAttendanceDays(List<AttendancePunchRecordDO> list, CalculateNum calculateNum) {
        List<AttendancePunchRecordDO> collect = list.stream().filter(a -> ObjectUtil.isNotEmpty(a.getPunchTime())).collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(collect)) {
            calculateNum.setTotalAttendanceDays(calculateNum.getTotalAttendanceDays() + 1);
        }
    }

}






















