package jnpf.util.attendance;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.ObjectMapper;
import jnpf.attendance.dto.MonthStatsDailySituationVo;
import jnpf.base.vo.PageListVO;
import jnpf.base.vo.PaginationVO;
import jnpf.entity.attendance.*;
import jnpf.enums.attendance.*;
import jnpf.model.attendance.dto.SalaryAttendanceSupportDto;
import jnpf.model.attendance.model.*;
import jnpf.model.attendance.vo.*;
import jnpf.model.attendance.vo.attendance.*;
import jnpf.model.common.DateRangeDto;
import jnpf.model.common.DateRangeDto1;
import jnpf.model.cultivate.CultivatePage;
import jnpf.permission.eum.v2.UserWorkStatusEnums;
import jnpf.permission.vo.v2.user.UserBoundVO;
import jnpf.util.*;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.springframework.beans.BeanUtils;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static cn.hutool.core.collection.CollUtil.newArrayList;
import static com.alibaba.fastjson.JSON.parseArray;
import static jnpf.enums.attendance.StatisticsEnumUtil.TabDataTypeEnum.values;

@Slf4j
public class DayStatisticsUtils {
    /**
     * 获取表头
     *
     * @return 表头
     */
    public static List<String> getHead() {
        List<String> head1 = new ArrayList<>();
        head1.add("成员");
        head1.add("考勤组");
        head1.add("部门");
        head1.add("岗位");
        head1.add("日期");
        head1.add("班次时间");
        head1.add("打卡时间");
        head1.add("其余打卡时间");
        return head1;
    }

    /**
     * 获取列宽
     *
     * @return 列宽
     */
    public static List<Integer> getWidths() {
        List<Integer> newWidths = new ArrayList<>();
        newWidths.add(100);
        newWidths.add(100);
        newWidths.add(100);
        newWidths.add(100);
        newWidths.add(100);
        newWidths.add(150);
        newWidths.add(150);
        newWidths.add(300);
        return newWidths;
    }

    /**
     * 筛选班次打卡记录
     *
     * @param clockInResultList  筛选打卡记录集合
     * @param statisticsTypeEnum 考勤统计类型
     */
    public static List<ClockInResult> getClockInResultFilterList(List<ClockInResult> clockInResultList, StatisticsEnumUtil.StatisticsTypeEnum statisticsTypeEnum) {
        List<ClockInResult> clockInResultFilterList = newArrayList();
        switch (statisticsTypeEnum) {
            case CD: {
                clockInResultFilterList = clockInResultList.stream().filter(m -> m.getClockInStatus().equals(ClockInStatusEnum.WORK_LATE.getValue()) && m.getClockInType().equals(ConstantUtil.ON_WORK)).collect(Collectors.toList());
                break;
            }
            case QK: {
                clockInResultFilterList = clockInResultList.stream().filter(m -> m.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue()) && m.getAbsence().equals(ConstantUtil.NUM_FALSE)).collect(Collectors.toList());
                break;
            }
            case ZT: {
                clockInResultFilterList = clockInResultList.stream().filter(m -> m.getClockInStatus().equals(ClockInStatusEnum.HOME_EARLY.getValue()) && m.getClockInType().equals(ConstantUtil.OFF_WORK)).collect(Collectors.toList());
                break;
            }
            case KG: {
                clockInResultFilterList = clockInResultList.stream().filter(m -> m.getDeleteMark().equals(ConstantUtil.NUM_FALSE) && m.getAbsence().equals(ConstantUtil.NUM_TRUE)).collect(Collectors.toList());
                break;
            }
            case JD:
            case JB: {
                clockInResultFilterList = clockInResultList;
                break;
            }
            case WQ: {
                clockInResultFilterList = clockInResultList.stream().filter(m -> m.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE)).collect(Collectors.toList());
                break;
            }
            case BK: {
                clockInResultFilterList = clockInResultList.stream().filter(m -> m.getRepaired().equals(1)).collect(Collectors.toList());
                break;
            }
            case SJCQ: {
                clockInResultFilterList = clockInResultList.stream().filter(m -> m.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) || m.getClockInStatus().equals(ClockInStatusEnum.NORMAL.getValue()) || m.getClockInStatus().equals(ClockInStatusEnum.WORK_LATE.getValue()) || m.getClockInStatus().equals(ClockInStatusEnum.HOME_EARLY.getValue())).collect(Collectors.toList());
                break;
            }

        }
        return clockInResultFilterList;
    }

    /**
     * 筛选考勤统计类型
     *
     * @param groupList          筛选班次记录集合
     * @param statisticsTypeEnum 考勤统计类型
     */
    public static List<UserDaySituationData> getWebClassesFilterList(List<UserDaySituationData> groupList, StatisticsEnumUtil.StatisticsTypeEnum statisticsTypeEnum) {
        List<UserDaySituationData> groupFilterList = newArrayList();
        switch (statisticsTypeEnum) {
            case QB: {
                break;
            }

            case XX: {
                groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).collect(Collectors.toList());
                break;
            }
            case CD:
            case KG:
            case ZT: {
                groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())).collect(Collectors.toList());
                break;
            }
            case YCQ: {
                groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList());
                break;
            }
            case QK:
            case BK:
            case WQ: {
                groupFilterList = groupList.stream().filter(m -> (m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode()))).collect(Collectors.toList());
                break;
            }
            case QJ: {
                groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList());
                break;
            }
            case JD: {
                groupFilterList = groupList.stream().filter(m -> !m.getSelfGroup().equals(SecondmentTypeEnum.NOT.getCode())).collect(Collectors.toList());
                break;
            }
            case JB: {
                groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())).collect(Collectors.toList());
                break;
            }
            case JQ: {
                groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.HOLIDAYS.getCode())).collect(Collectors.toList());
                break;
            }
            case CC: {
                groupFilterList = groupList.stream().filter(m -> (m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) && (m.getApplyViewEnable().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()) && m.getValidDuration() > 0)).collect(Collectors.toList());
                break;
            }
            case WC: {
                groupFilterList = groupList.stream().filter(m -> (m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) && (m.getApplyViewEnable().equals(AttendanceTypeEnum.STEP_OUT.getCode()) && m.getValidDuration() > 0)).collect(Collectors.toList());
                break;
            }
        }
        return groupFilterList;
    }

    /**
     * 判断两个日期是否是同一天
     *
     * @param date1 第一个日期
     * @param date2 第二个日期
     * @return 如果是同一天，则返回true；否则返回false
     */
    public static boolean isSameDay(Date date1, Date date2) {
        Calendar calendar1 = Calendar.getInstance();
        Calendar calendar2 = Calendar.getInstance();
        calendar1.setTime(date1);
        calendar2.setTime(date2);
        calendar1.set(Calendar.HOUR_OF_DAY, 0);
        calendar1.set(Calendar.MINUTE, 0);
        calendar1.set(Calendar.SECOND, 0);
        calendar1.set(Calendar.MILLISECOND, 0);
        calendar2.set(Calendar.HOUR_OF_DAY, 0);
        calendar2.set(Calendar.MINUTE, 0);
        calendar2.set(Calendar.SECOND, 0);
        calendar2.set(Calendar.MILLISECOND, 0);
        return calendar1.getTimeInMillis() == calendar2.getTimeInMillis();
    }

    /**
     * 获取月份列表
     *
     * @param start 开始月份
     * @param end   结束月份
     * @return 月份列表
     */
    public static List<String> generateMonthList(String start, String end) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
        YearMonth startYearMonth = YearMonth.parse(start, formatter);
        YearMonth endYearMonth = YearMonth.parse(end, formatter);
        List<String> monthList = new ArrayList<>();
        YearMonth current = startYearMonth;
        while (!current.isAfter(endYearMonth)) {
            monthList.add(current.format(formatter));
            current = current.plusMonths(1);
        }
        return monthList;
    }

    /**
     * 根据月份获取日期列表
     *
     * @param monthString 月份
     * @param yyyyMM
     * @return 日期列表
     */
    public static List<LocalDate> getDatesForMonth(String monthString, String yyyyMM) {
        DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern(yyyyMM);
        YearMonth yearMonth = YearMonth.parse(monthString, monthFormatter);
        LocalDate firstDayOfMonth = yearMonth.atDay(1);
        //判断是不是挡墙月，当前月截止日期是当天
        LocalDate lastDayOfMonth;
        if (monthString.equals(DateUtil.format(new Date(), yyyyMM))) {
            lastDayOfMonth = LocalDate.now();
        } else {
            lastDayOfMonth = yearMonth.atEndOfMonth();
        }
        List<LocalDate> dateList = new ArrayList<>();
        LocalDate currentDate = firstDayOfMonth;
        while (!currentDate.isAfter(lastDayOfMonth)) {
            dateList.add(currentDate);
            currentDate = currentDate.plusDays(1);
        }
        return dateList;
    }

    public static List<DayStatisticsDataVo> getStatisticsDataPackage(StatisticsDataQueryVo dayStatisticsDataVo) {
        StatisticsEnumUtil.TabStaBlockEnum[] values = StatisticsEnumUtil.TabStaBlockEnum.values();
        List<DayStatisticsDataVo> statisticsDataVoList = new ArrayList<>();
        for (StatisticsEnumUtil.TabStaBlockEnum value : values) {
            Integer count = 0;
            if (Objects.nonNull(dayStatisticsDataVo)) {
                switch (value) {
                    case QB:
                        count = dayStatisticsDataVo.getTotalCount();
                        break;
                    case ZC:
                        count = dayStatisticsDataVo.getNormalCount();
                        break;
                    case CD:
                        count = dayStatisticsDataVo.getLateCount();
                        break;
                    case ZT:
                        count = dayStatisticsDataVo.getEarlyLeaveCount();
                        break;
                    case WPB:
                        count = dayStatisticsDataVo.getNotSchedulingCount();
                        break;
                    case QK:
                        count = dayStatisticsDataVo.getAbsenceCardCount();
                        break;
                    case QQ:
                        count = dayStatisticsDataVo.getAbsenceCount();
                        break;
                    case QJ:
                        count = dayStatisticsDataVo.getLeaveCount();
                        break;
                    case CC:
                        count = dayStatisticsDataVo.getBusCount();
                        break;
                    case WC:
                        count = dayStatisticsDataVo.getOutCount();
                        break;
                    case JD:
                        count = dayStatisticsDataVo.getSelfCount();
                        break;
                    case JB:
                        count = dayStatisticsDataVo.getOvertimeCount();
                        break;
                }
            }
            statisticsDataVoList.add(DayStatisticsDataVo.builder().typeId(value.getCode()).count(count).staName(value.getMsg()).build());
        }
        return statisticsDataVoList;
    }

    /**
     * 获取用户每个请假类型的数据
     *
     * @param attendanceRatio  出勤换算比
     * @param leaveForApplyIds 用户当日请假申请ID集合
     * @param leaveAllList     请假数据集合
     * @return 用户请假数据
     */
    public static Map<String, LeaveTypeStaModel> getUserLeaveTypeSum(BigDecimal attendanceRatio,
                                                                     List<LeaveSituationData> leaveForApplyIds, List<FtbAttendanceDailyRule> leaveAllList) {
        Map<String, LeaveTypeStaModel> userLeaveTypeMap = new HashMap<>();
        Map<String, List<LeaveSituationData>> leaveTypeMap = leaveForApplyIds.stream().filter(m -> StringUtil.isNotEmpty(m.getLeaveId())).collect(Collectors.groupingBy(LeaveSituationData::getLeaveId));
        for (Map.Entry<String, List<LeaveSituationData>> entry : leaveTypeMap.entrySet()) {
            LeaveTypeStaModel leaveTypeStaModel = new LeaveTypeStaModel();
            leaveTypeStaModel.setIsRest(entry.getValue().get(0).getIsRest());
            List<String> applyIds = entry.getValue().stream().map(LeaveSituationData::getApplyId).distinct().collect(Collectors.toList());
            List<FtbAttendanceDailyRule> leaveAllListFilter = leaveAllList.stream().filter(m -> applyIds.contains(m.getApplyId())).collect(Collectors.toList());
            if (StringUtil.isEmpty(leaveAllListFilter)) {
                continue;
            }
            BigDecimal totalHours = BigDecimal.ZERO;
            BigDecimal totalDays = BigDecimal.ZERO;
            List<LeaveTypeStaDetailsModel> applyList = new ArrayList<>();
            for (FtbAttendanceDailyRule dailyRule : leaveAllListFilter) {
                BigDecimal leaveHours = DateConvertUtil.minuteConvert(new BigDecimal(dailyRule.getValidDuration()), 1, 2, null);
                BigDecimal leaveDays;
                // 划线排班计算天数需要根据出勤换算比计算
                if (dailyRule.getFixedMark().equals(2)) {
                    leaveDays = totalHours.divide(attendanceRatio, 2, RoundingMode.HALF_UP).compareTo(BigDecimal.ONE) > 0 ?
                            BigDecimal.ONE : totalHours.divide(attendanceRatio, 2, RoundingMode.HALF_UP);
                    if (leaveDays.compareTo(BigDecimal.ONE) > 0) {
                        leaveDays = BigDecimal.ONE;
                    }
                } else {
                    leaveDays = dailyRule.getLeaveDay();
                }
                totalHours = totalHours.add(leaveHours);
                totalDays = totalDays.add(leaveDays);
                LeaveTypeStaDetailsModel model = new LeaveTypeStaDetailsModel();
                model.setApplyId(dailyRule.getApplyId());
                model.setLeaveDays(leaveDays);
                model.setLeaveHours(leaveHours);
                model.setLeavePayrollHours(leaveHours);
                applyList.add(model);
            }
            leaveTypeStaModel.setLeaveHours(totalHours);
            leaveTypeStaModel.setLeavePayrollHours(totalHours);
            leaveTypeStaModel.setLeaveDays(totalDays);
            leaveTypeStaModel.setApplyList(applyList);
            userLeaveTypeMap.put(entry.getKey(), leaveTypeStaModel);
        }
        return userLeaveTypeMap;
    }

    /**
     * 获取用户每个请假类型的数据
     *
     * @param leaveTypeMap          请假类型聚合数据
     * @param startDate             开始时间
     * @param endDate               结束时间
     * @return 用户请假数据
     */
    public static Map<String, LeaveTypeStaModel> getUserLeaveTypeSum(Map<String, List<LeaveSituationData>> leaveTypeMap, Date startDate, Date endDate) {
        Map<String, LeaveTypeStaModel> userLeaveTypeMap = new HashMap<>();
        for (Map.Entry<String, List<LeaveSituationData>> entry : leaveTypeMap.entrySet()) {
            List<LeaveSituationData> dataList = entry.getValue();
            if (CollUtil.isEmpty(dataList)) {
                continue;
            }
            // 构建请假类型统计模型
            LeaveTypeStaModel model = buildLeaveTypeModel(dataList, startDate, endDate);
            model.setIsRest(dataList.get(0).getIsRest());
            userLeaveTypeMap.put(entry.getKey(), model);
        }
        return userLeaveTypeMap;
    }

    /**
     * 构建请假类型统计模型
     */
    private static LeaveTypeStaModel buildLeaveTypeModel(List<LeaveSituationData> dataList, Date startDate, Date endDate) {
        LeaveTypeStaModel model = new LeaveTypeStaModel();
        LeaveStatisticsTotal stats = new LeaveStatisticsTotal();
        for (LeaveSituationData data : dataList) {
            processLeaveData(data, startDate, endDate, stats);
        }
        model.setLeaveDays(stats.getDaysTotal());
        model.setLeaveHours(stats.getHoursTotal());
        model.setLeavePayrollHours(stats.getPayrollHoursTotal());
        model.setApplyList(stats.getDetailList());
        return model;
    }

    /**
     * 处理单条请假数据
     */
    private static void processLeaveData(LeaveSituationData data, Date startDate, Date endDate, LeaveStatisticsTotal stats) {
        // 过滤老数据
        if (StrUtil.isEmpty(data.getBalanceJsonNew())) {
            return;
        }
        // 解析班次信息
        LeaveShiftVo shiftVo = JsonUtil.getJsonToBean(data.getShiftInvolved(), LeaveShiftVo.class);
        if (Objects.isNull(shiftVo)) {
            return;
        }
        // 移除无效的日程规则
        shiftVo.getDailyRuleVos().removeIf(rule -> Objects.isNull(rule.getFromType()));
        // 计算请假数据
        ProcessResult calcResult = calculateLeaveData(data, shiftVo, startDate, endDate);
        // 更新统计
        if (calcResult != null && calcResult.getHours().compareTo(BigDecimal.ZERO) > 0) {
            stats.add(calcResult);
            // 添加明细
            LeaveTypeStaDetailsModel detail = new LeaveTypeStaDetailsModel();
            detail.setApplyId(data.getApplyId());
            detail.setLeaveDays(calcResult.getDays());
            detail.setLeaveHours(calcResult.getHours());
            detail.setLeavePayrollHours(calcResult.getPayrollHours());
            stats.addDetail(detail);
        }
    }

    /**
     * 计算请假数据（核心逻辑）
     */
    private static ProcessResult calculateLeaveData(LeaveSituationData data, LeaveShiftVo shiftVo, Date startDate, Date endDate) {
        // 检查出勤换算比
        if (Objects.isNull(shiftVo.getAttendanceRatio())) {
            return null;
        }
        // 过滤请假班次规则（toType=3表示请假）
        List<DailyRuleResultVo> validRules = shiftVo.getDailyRuleVos().stream().filter(rule -> rule.getToType() == 3).collect(Collectors.toList());
        // 判断是否命中班次
        boolean hasHitRules = CollUtil.isNotEmpty(validRules);
        if (!hasHitRules) {
            // 未命中班次，按基础规则计算
            return calculateWithoutHitRules(data, shiftVo, startDate, endDate);
        } else {
            // 命中班次，按班次规则计算
            return calculateWithHitRules(data, shiftVo, startDate);
        }
    }

    /**
     * 未命中班次的计算逻辑
     */
    private static ProcessResult calculateWithoutHitRules(LeaveSituationData data, LeaveShiftVo shiftVo, Date startDate, Date endDate) {
        int unit = data.getUnit();
        if (unit == 1) {
            // 小时假
            MutablePair<Date, Date> entityInterval = MutablePair.of(data.getStartTime(), data.getEndTime());
            MutablePair<Date, Date> filterInterval = MutablePair.of(startDate, endDate);
            BigDecimal hitHours = calculateIntersectionHours(shiftVo.getAttendanceRatio(), entityInterval, filterInterval);
            BigDecimal hitDays = hitHours.divide(shiftVo.getAttendanceRatio(), 2, RoundingMode.HALF_UP);
            return new ProcessResult(hitDays, hitHours, hitHours);
        } else if (unit == 2) {
            // 全天假
            BigDecimal hitHours = shiftVo.getAttendanceRatio();
            return new ProcessResult(BigDecimal.ONE, hitHours, hitHours);
        } else {
            // 半天假
            BigDecimal hitHours = shiftVo.getAttendanceRatio().divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP);
            // 请假开始日期和结束日期-跟统计日期是同一天
            if (DateUtil.isSameDay(data.getStartTime(), startDate) && DateUtil.isSameDay(data.getEndTime(), startDate)) {
                if (data.getStartTimeType().equals(2) || data.getEndTimeType().equals(1)) {
                    return new ProcessResult(new BigDecimal("0.5"), hitHours, hitHours);
                } else {
                    return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio());
                }
            }
            // 请假开始日期跟统计日期不是同一天，和结束日期是同一天
            if (!DateUtil.isSameDay(data.getStartTime(), startDate) && DateUtil.isSameDay(data.getEndTime(), startDate)) {
                if (data.getEndTimeType().equals(1)) {
                    return new ProcessResult(new BigDecimal("0.5"), hitHours, hitHours);
                } else {
                    return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio());
                }
            }
            // 请假开始日期跟统计日期是同一天，和结束日期不是同一天
            if (DateUtil.isSameDay(data.getStartTime(), startDate) && !DateUtil.isSameDay(data.getEndTime(), startDate)) {
                if (data.getStartTimeType().equals(2)) {
                    return new ProcessResult(new BigDecimal("0.5"), hitHours, hitHours);
                } else {
                    return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio());
                }
            }
            return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio());
        }
    }

    /**
     * 命中班次的计算逻辑
     */
    private static ProcessResult calculateWithHitRules(LeaveSituationData data, LeaveShiftVo shiftVo, Date startDate) {
        int unit = data.getUnit();
        List<DailyRuleResultVo> todayRules = getRulesByDate(shiftVo.getDailyRuleVos(), startDate);
        if (unit == 1) {
            // 小时假
            return calculateHourlyLeaveWithRules(data, shiftVo, todayRules, startDate);
        } else if (unit == 2) {
            // 全天假
            return calculateFullDayLeaveWithRules(data, shiftVo, todayRules, startDate);
        } else {
            // 半天假
            return calculateHalfDayLeaveWithRules(shiftVo, todayRules);
        }
    }

    /**
     * 小时假 - 命中班次计算
     */
    private static ProcessResult calculateHourlyLeaveWithRules(LeaveSituationData data, LeaveShiftVo shiftVo, List<DailyRuleResultVo> todayRules, Date startDate) {
        // 判断是否同一天
        boolean isSameDay = DateUtil.isSameDay(data.getStartTime(), data.getEndTime());
        if (isSameDay) {
            // 未跨日
            if (CollUtil.isEmpty(todayRules)) {
                // 未命中班次
                BigDecimal hours = DateConvertUtil.dateConvert(data.getStartTime(), data.getEndTime(), 2, 2, null);
                if (hours.compareTo(shiftVo.getAttendanceRatio()) > 0) {
                    return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio());
                } else {
                    BigDecimal hitDays = hours.divide(shiftVo.getAttendanceRatio(), 2, RoundingMode.HALF_UP);
                    return new ProcessResult(hitDays, hours, hours);
                }
            } else {
                // 命中班次
                return calculateFromRules(todayRules, shiftVo.getAttendanceRatio());
            }
        } else {
            // 跨日处理
            return calculateCrossDayHourlyLeave(data, shiftVo, todayRules, startDate);
        }
    }

    /**
     * 小时假 - 跨日计算
     */
    private static ProcessResult calculateCrossDayHourlyLeave(LeaveSituationData data, LeaveShiftVo shiftVo, List<DailyRuleResultVo> todayRules, Date startDate) {
        if (DateUtil.isSameDay(data.getEndTime(), startDate)) {
            // 结束时间是今天，检查昨天
            Date yesterday = DateUtil.offsetDay(startDate, -1);
            List<DailyRuleResultVo> yesterdayRules = getRulesByDate(shiftVo.getDailyRuleVos(), yesterday);
            return handleCrossDayRules(todayRules, yesterdayRules, shiftVo.getAttendanceRatio());
        } else {
            // 结束时间是明天，检查明天
            Date nextDay = DateUtil.offsetDay(startDate, 1);
            List<DailyRuleResultVo> nextDayRules = getRulesByDate(shiftVo.getDailyRuleVos(), nextDay);
            return handleCrossDayRules(todayRules, nextDayRules, shiftVo.getAttendanceRatio());
        }
    }

    /**
     * 处理跨日班次规则
     */
    private static ProcessResult handleCrossDayRules(List<DailyRuleResultVo> todayRules, List<DailyRuleResultVo> otherDayRules, BigDecimal attendanceRatio) {
        boolean todayEmpty = CollUtil.isEmpty(todayRules);
        boolean otherEmpty = CollUtil.isEmpty(otherDayRules);
        if (todayEmpty && otherEmpty) {
            // 都无班次，跳过
            return null;
        } else if (!todayEmpty && otherEmpty) {
            // 只有今天有
            return calculateFromRules(todayRules, attendanceRatio);
        } else if (todayEmpty) {
            // 只有其他天有，跳过
            return null;
        } else {
            // 两天都有，只算今天
            return calculateFromRules(todayRules, attendanceRatio);
        }
    }

    /**
     * 全天假 - 命中班次计算
     */
    private static ProcessResult calculateFullDayLeaveWithRules(LeaveSituationData data, LeaveShiftVo shiftVo, List<DailyRuleResultVo> todayRules, Date startDate) {
        if (DateUtil.isSameDay(data.getEndTime(), data.getStartTime())) {
            // 单天请假
            if (CollUtil.isEmpty(todayRules)) {
                BigDecimal hours = DateConvertUtil.dateConvert(data.getStartTime(), data.getEndTime(), 2, 2, null);
                if (hours.compareTo(shiftVo.getAttendanceRatio()) > 0) {
                    return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio());
                } else {
                    BigDecimal hitDays = hours.divide(shiftVo.getAttendanceRatio(), 2, RoundingMode.HALF_UP);
                    return new ProcessResult(hitDays, hours, hours);
                }
            } else {
                return calculateFromRules(todayRules, shiftVo.getAttendanceRatio());
            }
        } else {
            // 多天请假
            if (CollUtil.isEmpty(shiftVo.getDailyRuleVos())) {
                // 从未命中过班次
                return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio());
            } else if (CollUtil.isNotEmpty(todayRules)) {
                return calculateFromRules(todayRules, shiftVo.getAttendanceRatio());
            } else {
                // 命中过但今天未命中
                return null;
            }
        }
    }

    /**
     * 半天假 - 命中班次计算
     */
    private static ProcessResult calculateHalfDayLeaveWithRules(LeaveShiftVo shiftVo, List<DailyRuleResultVo> todayRules) {
        if (CollUtil.isNotEmpty(shiftVo.getDailyRuleVos()) && CollUtil.isEmpty(todayRules)) {
            return new ProcessResult(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
        }
        if (CollUtil.isEmpty(todayRules)) {
            BigDecimal hitHours = shiftVo.getAttendanceRatio().divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP);
            return new ProcessResult(new BigDecimal("0.5"), hitHours, hitHours);
        }
        return calculateFromRules(todayRules, shiftVo.getAttendanceRatio());
    }

    /**
     * 从规则列表计算汇总
     */
    private static ProcessResult calculateFromRules(List<DailyRuleResultVo> rules, BigDecimal attendanceRatio) {
        BigDecimal hitHours = sumRuleField(rules, DailyRuleResultVo::getDuration, attendanceRatio);
        BigDecimal hitPayrollHours = sumRuleField(rules, DailyRuleResultVo::getPayrollHours, attendanceRatio);
        BigDecimal hitDays = sumRuleField(rules, DailyRuleResultVo::getLeaveDays, attendanceRatio);
        return new ProcessResult(hitDays, hitHours, hitPayrollHours);
    }

    /**
     * 汇总规则字段
     */
    private static BigDecimal sumRuleField(List<DailyRuleResultVo> rules, Function<DailyRuleResultVo, BigDecimal> fieldExtractor, BigDecimal attendanceRatio) {
        BigDecimal reduce = rules.stream().map(rule -> Optional.ofNullable(fieldExtractor.apply(rule)).orElse(BigDecimal.ZERO)).reduce(BigDecimal.ZERO, BigDecimal::add);
        if (reduce.compareTo(BigDecimal.ZERO) == 0) {
            BigDecimal hours = rules.stream().map(rule -> Optional.ofNullable(rule.getDuration()).orElse(BigDecimal.ZERO)).reduce(BigDecimal.ZERO, BigDecimal::add);
            reduce = hours.compareTo(attendanceRatio) < 0 ? hours.divide(attendanceRatio, 2, RoundingMode.HALF_UP) : BigDecimal.ONE;
        }
        return reduce;
    }

    /**
     * 按日期过滤规则
     */
    private static List<DailyRuleResultVo> getRulesByDate(List<DailyRuleResultVo> rules, Date targetDate) {
        return rules.stream().filter(rule -> DateUtil.isSameDay(rule.getDate(), targetDate)).collect(Collectors.toList());
    }

    /**
     * 计算两个时间区间的交集小时数
     *
     * @param attendanceRatio 出勤换算比
     * @param entityInterval  实体时间区间 (left=开始时间, right=结束时间)
     * @param filterInterval  筛选时间区间 (left=开始时间, right=结束时间)
     * @return 交集小时数，无交集返回BigDecimal.ZERO
     * @throws IllegalArgumentException 参数不合法时抛出异常
     */
    public static BigDecimal calculateIntersectionHours(BigDecimal attendanceRatio, MutablePair<Date, Date> entityInterval, MutablePair<Date, Date> filterInterval) {
        // 参数校验
        if (entityInterval == null || filterInterval == null) {
            throw new IllegalArgumentException("时间区间参数不能为空");
        }
        Date entityStart = entityInterval.getLeft();
        Date entityEnd = entityInterval.getRight();
        Date filterStart = filterInterval.getLeft();
        Date filterEnd = filterInterval.getRight();
        if (entityStart == null || entityEnd == null || filterStart == null || filterEnd == null) {
            throw new IllegalArgumentException("时间区间中的时间不能为空");
        }
        // 时间顺序校验
        if (entityStart.after(entityEnd)) {
            throw new IllegalArgumentException("实体时间区间开始时间不能晚于结束时间");
        }
        if (filterStart.after(filterEnd)) {
            throw new IllegalArgumentException("筛选时间区间开始时间不能晚于结束时间");
        }
        // 计算交集起止时间
        Date intersectionStart = entityStart.after(filterStart) ? entityStart : filterStart;
        Date intersectionEnd = entityEnd.before(filterEnd) ? entityEnd : filterEnd;
        // 检查是否有交集
        if (intersectionStart.after(intersectionEnd)) {
            return BigDecimal.ZERO;
        }
        // 计算毫秒差并转换为小时
        long diffMillis = intersectionEnd.getTime() - intersectionStart.getTime();
        BigDecimal bigDecimal = new BigDecimal(diffMillis).divide(new BigDecimal(3600000), 2, RoundingMode.HALF_UP);
        return bigDecimal.compareTo(attendanceRatio) > 0 ? attendanceRatio : bigDecimal;
    }

    /**
     * 判断班次是否旷工
     *
     * @param clockInResultList 班次打卡结果信息
     * @return 是否旷工
     */
    public static Boolean isShiftsJsonAbsence1(List<ShiftsJsonClockInResultVo> clockInResultList) {
        boolean isAbsence = Boolean.FALSE;
        if (CollUtil.isNotEmpty(clockInResultList)) {
            List<Integer> isAbsenceList = clockInResultList.stream().map(ShiftsJsonClockInResultVo::getAbsence).filter(absence -> absence.equals(ConstantUtil.NUM_TRUE)).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(isAbsenceList)) {
                isAbsence = Boolean.TRUE;
            }
        }
        return isAbsence;
    }

    /**
     * 生成日常情况横坐标
     *
     * @return 日常情况横坐标
     */
    public static List<MonthStatsDailySituationVo> createDailySituationDirectory(MonthStatsDailySituationQueryVo dailySituation) {
        List<MonthStatsDailySituationVo> teamTabVoList = new ArrayList<>();
        StatisticsEnumUtil.DailySituationEnum[] values = StatisticsEnumUtil.DailySituationEnum.values();
        for (StatisticsEnumUtil.DailySituationEnum value : values) {
            MonthStatsDailySituationVo teamTabVo = new MonthStatsDailySituationVo();
            teamTabVo.setTypeName(value.getMsg());
            switch (value) {
                case ZC:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getNormalDays() : BigDecimal.ZERO);
                    break;
                case CD:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getLateDays() : BigDecimal.ZERO);
                    break;
                case ZT:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getEarlyLeaveDays() : BigDecimal.ZERO);
                    break;
                case WPB:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getNotSchedulingDays() : BigDecimal.ZERO);
                    break;
                case QK:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getAbsenceCardDays() : BigDecimal.ZERO);
                    break;
                case QQ:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getAbsenceDays() : BigDecimal.ZERO);
                    break;
                case QJ:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getLeaveDays() : BigDecimal.ZERO);
                    break;
                case CC:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getBusDays() : BigDecimal.ZERO);
                    break;
                case WC:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getOutDays() : BigDecimal.ZERO);
                    break;
                case JD:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getSelfDays() : BigDecimal.ZERO);
                    break;
                case JB:
                    teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getOvertimeDays() : BigDecimal.ZERO);
                    break;
            }
            teamTabVoList.add(teamTabVo);
        }
        return teamTabVoList;
    }

    /**
     * 获取用户列表
     *
     * @param workStatusEnum     用户在职状态
     * @return 获取用户列表
     */
    public static List<UserWorkStatusEnums> getWorkStatusEnumList(StatisticsEnumUtil.WorkStatusEnum workStatusEnum) {
        //排除掉无效状态
        List<UserWorkStatusEnums> workStatusEnumList = new ArrayList<>();
        if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.QB)) {
            workStatusEnumList.addAll(List.of(UserWorkStatusEnums.ONBOARDING_FAILED, UserWorkStatusEnums.PRE_ONBOARDING, UserWorkStatusEnums.TRIAL, UserWorkStatusEnums.FULL_TIME, UserWorkStatusEnums.PENDING_RESIGNATION, UserWorkStatusEnums.RESIGNED));
        } else if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.YRZ)) {
            // 入职失败要归属到预入职
            workStatusEnumList.add(UserWorkStatusEnums.ONBOARDING_FAILED);
            workStatusEnumList.add(UserWorkStatusEnums.PRE_ONBOARDING);
        } else if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.SY)) {
            workStatusEnumList.add(UserWorkStatusEnums.TRIAL);
        } else if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.ZS)) {
            workStatusEnumList.add(UserWorkStatusEnums.FULL_TIME);
        } else if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.DLZ)) {
            workStatusEnumList.add(UserWorkStatusEnums.PENDING_RESIGNATION);
        } else if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.LZ)) {
            workStatusEnumList.add(UserWorkStatusEnums.RESIGNED);
        }
        return workStatusEnumList;
    }

    /**
     * 获取交集用户
     *
     * @param userIds    用户ID集合
     * @param reqUserIds 用户ID集合
     * @return 返回交集数据ID集合
     */
    public static List<String> getUserList(List<String> userIds, List<String> reqUserIds) {
        if (CollUtil.isEmpty(userIds)) {
            return new ArrayList<>();
        }
        if (CollUtil.isNotEmpty(reqUserIds)) {
            if (CollUtil.isEmpty(CollectionUtils.intersection(userIds, reqUserIds))) {
                return new ArrayList<>();
            } else {
                return (List<String>) CollectionUtils.intersection(userIds, reqUserIds);
            }
        } else {
            return userIds;
        }
    }

    /**
     * 获取排班类型
     *
     * @param attendanceType 排班类型
     * @return 排班类型
     */
    public static Integer getType(Integer attendanceType) {
        if (AttendanceTypeEnum.LEAVE.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.LEAVE.getCode();
        }
        if (AttendanceTypeEnum.ORDINARY.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.NORMAL.getCode();
        }
        if (AttendanceTypeEnum.REST.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.REST.getCode();
        }
        if (AttendanceTypeEnum.HOLIDAYS.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.PAID_HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.BUSINESS_TRIP.getCode();
        }
        if (AttendanceTypeEnum.STEP_OUT.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.STEP_OUT.getCode();
        }
        return null;
    }

    public static void enclosureData(DayStatisticsQueryVo item, DayStatisticsShiftsJsonVo shiftsJsonVo, List<String> clockTimeList, StringBuilder clockTimeStr) {
        if (CollUtil.isNotEmpty(shiftsJsonVo.getClockInResultList())) {
            if (isShiftsJsonAbsence1(shiftsJsonVo.getClockInResultList())) {
                clockTimeList.add("旷工");
                clockTimeStr.append("旷工").append("|");
            } else {
                StringBuilder sb = new StringBuilder();
                List<ShiftsJsonClockInResultVo> onClockInResultList = shiftsJsonVo.getClockInResultList().stream().filter(m -> m.getClockInType().equals(ConstantUtil.ON_WORK)).collect(Collectors.toList());
                ShiftsJsonClockInResultVo on = CollUtil.isNotEmpty(onClockInResultList) ? onClockInResultList.get(0) : null;
                List<ShiftsJsonClockInResultVo> offClockInResultList = shiftsJsonVo.getClockInResultList().stream().filter(m -> m.getClockInType().equals(ConstantUtil.OFF_WORK)).collect(Collectors.toList());
                ShiftsJsonClockInResultVo off = CollUtil.isNotEmpty(offClockInResultList) ? offClockInResultList.get(0) : null;
                if (Objects.nonNull(on)) {
                    if (on.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) {
                        sb.append("缺卡").append("~");
                    } else if ((on.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) && Objects.isNull(on.getClockInTime()))) {
                        sb.append(DateUtil.format(shiftsJsonVo.getInPoint(), "HH:mm")).append("~");
                    } else {
                        sb.append(DateUtil.format(on.getClockInTime(), "HH:mm")).append("~");
                    }
                }
                if (Objects.nonNull(off)) {
                    if (off.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) {
                        sb.append("缺卡");
                    } else if ((off.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) && Objects.isNull(off.getClockInTime()))) {
                        sb.append(isSameDay(item.getDate(), shiftsJsonVo.getOutPoint()) ? DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm") : "(次日)" + DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm"));
                    } else {
                        sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm"));
                    }
                }
                clockTimeList.add(sb.toString());
                clockTimeStr.append(sb).append("|");
            }
        }
    }

    public static void enclosurePayrollData(DayPayrollStatisticsQueryVo item, DayStatisticsShiftsJsonVo shiftsJsonVo, List<String> clockTimeList, StringBuilder clockTimeStr) {
        if (CollUtil.isNotEmpty(shiftsJsonVo.getClockInResultList())) {
            if (isShiftsJsonAbsence1(shiftsJsonVo.getClockInResultList())) {
                clockTimeList.add("旷工");
                clockTimeStr.append("旷工").append("|");
            } else {
                StringBuilder sb = new StringBuilder();
                List<ShiftsJsonClockInResultVo> onClockInResultList = shiftsJsonVo.getClockInResultList().stream().filter(m -> m.getClockInType().equals(ConstantUtil.ON_WORK)).collect(Collectors.toList());
                ShiftsJsonClockInResultVo on = CollUtil.isNotEmpty(onClockInResultList) ? onClockInResultList.get(0) : null;
                List<ShiftsJsonClockInResultVo> offClockInResultList = shiftsJsonVo.getClockInResultList().stream().filter(m -> m.getClockInType().equals(ConstantUtil.OFF_WORK)).collect(Collectors.toList());
                ShiftsJsonClockInResultVo off = CollUtil.isNotEmpty(offClockInResultList) ? offClockInResultList.get(0) : null;
                if (Objects.nonNull(on)) {
                    if (on.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) {
                        sb.append("缺卡").append("~");
                    } else if ((on.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) && Objects.isNull(on.getClockInTime()))) {
                        sb.append(DateUtil.format(shiftsJsonVo.getInPoint(), "HH:mm")).append("~");
                    } else {
                        sb.append(DateUtil.format(on.getClockInTime(), "HH:mm")).append("~");
                    }
                }
                if (Objects.nonNull(off)) {
                    if (off.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) {
                        sb.append("缺卡");
                    } else if ((off.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) && Objects.isNull(off.getClockInTime()))) {
                        sb.append(isSameDay(item.getDate(), shiftsJsonVo.getOutPoint()) ? DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm") : "(次日)" + DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm"));
                    } else {
                        sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm"));
                    }
                }
                clockTimeList.add(sb.toString());
                clockTimeStr.append(sb).append("|");
            }
        }
    }

    /**
     * 封装考勤组用户月统计提醒数据
     *
     * @param teamMonthStatisticsList 月统计数据
     * @return 月统计提醒数据
     */
    public static Map<String, TeamMonthNoticeModel> getTeamMonthNoticeModelMap(List<TeamMonthStatisticsNoticeQueryVo> teamMonthStatisticsList) {
        Map<String, TeamMonthNoticeModel> teamMonthNoticeModelMap = new HashMap<>();
        for (TeamMonthStatisticsNoticeQueryVo item : teamMonthStatisticsList) {
            TeamMonthNoticeModel teamMonthNoticeModel = TeamMonthNoticeModel.builder().build();
            BeanUtils.copyProperties(item, teamMonthNoticeModel);
            teamMonthNoticeModelMap.put(item.getGroupId(), teamMonthNoticeModel);
        }
        return teamMonthNoticeModelMap;
    }

    /**
     * 封装日统计数据生成通知消息
     */
    public static String getClockInResultStr(boolean isOvertime, DayStatisticsNoticeQueryVo item, ShiftsJsonClockInResultVo on, ShiftsJsonClockInResultVo off) {
        StringBuilder sb = new StringBuilder();
        if (Objects.nonNull(on)) {
            if (on.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) {
                sb.append("<span style='color:#F8593C'>缺卡</span>");
            } else if (on.getClockInStatus().equals(ClockInStatusEnum.NORMAL.getValue())) {
                sb.append(DateUtil.format(on.getClockInTime(), "HH:mm"));
                if (isOvertime) {
                    sb.append("<span style='color:#3C6DF8'>(加班)-</span>");
                }
                sb.append("<span style='color:#3C6DF8'>(正常)</span>");
            } else if (on.getClockInStatus().equals(ClockInStatusEnum.WORK_LATE.getValue())) {
                sb.append(DateUtil.format(on.getClockInTime(), "HH:mm"));
                sb.append("<span style='color:#F8593C'>(迟到)</span>");
            } else if (on.getClockInStatus().equals(ClockInStatusEnum.HOME_EARLY.getValue())) {
                sb.append(DateUtil.format(on.getClockInTime(), "HH:mm"));
                sb.append("<span style='color:#F8593C'>(早退)</span>");
            } else if (on.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue())) {
                sb.append(DateUtil.format(on.getEffectiveTime(), "HH:mm"));
                sb.append("<span style='color:#3C6DF8'>(无需打卡) </span>");
            }
            sb.append("至");
        }
        if (Objects.nonNull(off)) {
            if (off.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) {
                sb.append("<span style='color:#F8593C'>缺卡</span>");
            } else if (off.getClockInStatus().equals(ClockInStatusEnum.NORMAL.getValue())) {
                if (off.getAbsence().equals(ConstantUtil.NUM_TRUE)) {
                    sb.append("<span style='color:#F8593C'>旷工</span>");
                } else {
                    if (off.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE)) {
                        sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm"));
                        sb.append("<span style='color:#69D433'>外勤</span>");
                    } else {
                        sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm"));
                        sb.append("<span style='color:#3C6DF8'>(正常) </span>");
                    }
                }
            } else if (off.getClockInStatus().equals(ClockInStatusEnum.WORK_LATE.getValue())) {
                sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm"));
                sb.append("<span style='color:#F8593C'>(迟到)</span>");
            } else if (off.getClockInStatus().equals(ClockInStatusEnum.HOME_EARLY.getValue())) {
                sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm"));
                sb.append("<span style='color:#F8593C'>(早退)</span>");
            } else if (off.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue())) {
                sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getEffectiveTime(), "HH:mm"));
                sb.append("<span style='color:#3C6DF8'>(无需打卡) </span>");
            }
        }
        return sb.toString();
    }

    public static String getClockInMethod(Integer deviceType) {
        if (null == deviceType) {
            return "";
        }
        switch (deviceType) {
            case ConstantUtil.DEVICE_PLACE:
                return "地点打卡";
            case ConstantUtil.DEVICE_WIFI:
                return "Wi-Fi打卡";
            case ConstantUtil.DEVICE_MACHINE:
                return "考勤机打卡";
            default:
                return "";
        }
    }

    public static ClockClassRecord clock2ClockClassRecord(ClockClassRecord vo, Map<String, List<AttendanceClockInPic>> clockInPicMap) {
        vo.setPictureList(clockInPicMap.getOrDefault(vo.getClockInId(), new ArrayList<>()).stream().map(AttendanceClockInPic::getPicUrl).collect(Collectors.toList()));
        return vo;
    }

    /**
     * 获取借调次数
     *
     * @param userSecondRecordList 用户借调数据
     */
    public static Integer handleDateJsonObject(DateRangeDto dateRangeDto, List<UserSecondRecord> userSecondRecordList) throws Exception {
        int count = 0;
        for (UserSecondRecord item : userSecondRecordList) {
            if (com.github.pagehelper.util.StringUtil.isNotEmpty(item.getTimeJson())) {
                List<SecondmentDateVo> dateVoList = JsonUtil.getJsonToList(item.getTimeJson(), SecondmentDateVo.class);
                //判断借调时间段是否在筛选日期内
                if (CollUtil.isNotEmpty(dateVoList)) {
                    for (SecondmentDateVo secondmentDateVo : dateVoList) {
                        DateTime startDate1 = new DateTime(secondmentDateVo.getStartTime());
                        DateTime endDate1 = new DateTime(secondmentDateVo.getEndTime());
                        DateTime startDate2 = new DateTime(dateRangeDto.getStartDate());
                        DateTime endDate2 = new DateTime(dateRangeDto.getEndDate());
                        HashMap<String, DateTime> hashMap = getOverlap(startDate1, endDate1, startDate2, endDate2, false);
                        if (CollUtil.isNotEmpty(hashMap)) {
                            count += 1;
                        }
                    }
                }
            }
        }
        return count;
    }

    /**
     * 判断2个时间段是否有重叠（交集）
     *
     * @param startDate1 时间段1开始时间
     * @param endDate1   时间段1结束时间
     * @param startDate2 时间段2开始时间
     * @param endDate2   时间段2结束时间
     * @param isStrict   是否严格重叠，true 严格，没有任何相交或相等；false 不严格，可以首尾相等
     * @return ashMap<String, DateTime> key  startDate endDate
     */
    public static HashMap<String, DateTime> getOverlap(DateTime startDate1, DateTime endDate1, DateTime startDate2, DateTime endDate2, boolean isStrict) throws Exception {
        Objects.requireNonNull(startDate1, "startDate1");
        Objects.requireNonNull(endDate1, "endDate1");
        Objects.requireNonNull(startDate2, "startDate2");
        Objects.requireNonNull(endDate2, "endDate2");
        return setOverlap(startDate1, endDate1, startDate2, endDate2, isStrict);
    }

    public static HashMap<String, DateTime> setOverlap(DateTime startDate1, DateTime endDate1, DateTime startDate2, DateTime endDate2, boolean isStrict) throws Exception {
        return DateConvertUtil.setOverlap(startDate1, endDate1, startDate2, endDate2, isStrict);
    }

    /**
     * 封装数据
     *
     * @param dataTypeEnum 查询类型
     * @param queryVoList  日统计数据
     * @param dataDetailsMap  用户异常数据
     * @param boundVoMap   用户数据集合
     */
    public static List<AppTeamStatisticsListVo> dateEncapsulationV2(StatisticsEnumUtil.TabDataTypeEnum dataTypeEnum, List<MonthStatisticsQueryVo> queryVoList, Map<String, List<Object>> dataDetailsMap, Map<String, UserBoundVO> boundVoMap) {
        List<AppTeamStatisticsListVo> statisticsListVoList = new ArrayList<>();
        for (MonthStatisticsQueryVo monthStatisticsVo : queryVoList) {
            AppTeamStatisticsListVo statisticsListVo = new AppTeamStatisticsListVo();
            statisticsListVo.setUserId(monthStatisticsVo.getUserId());
            if (!boundVoMap.containsKey(monthStatisticsVo.getUserId())) {
                continue;
            }
            UserBoundVO userInfoVo = boundVoMap.get(monthStatisticsVo.getUserId());
            statisticsListVo.setUserHeadSculpture(com.github.pagehelper.util.StringUtil.isNotEmpty(userInfoVo.getHeadIcon()) ? UploaderUtil.uploaderImg(userInfoVo.getHeadIcon()) : null);
            statisticsListVo.setUserName(com.github.pagehelper.util.StringUtil.isNotEmpty(userInfoVo.getUserName()) ? userInfoVo.getUserName() : com.github.pagehelper.util.StringUtil.isNotEmpty(userInfoVo.getNickname()) ? userInfoVo.getNickname() : null);
            statisticsListVo.setIsSecond(0);
            statisticsListVo.setNumber(dataTypeEnum.getTimes(monthStatisticsVo));
            statisticsListVo.setDetalList(dataDetailsMap.getOrDefault(monthStatisticsVo.getUserId() + "&" + monthStatisticsVo.getGroupId(), new ArrayList<>()));
            statisticsListVoList.add(statisticsListVo);
        }
        return statisticsListVoList;
    }

    /**
     * 根据出勤类型枚举，生成类型列表数据
     *
     * @return 列表数据
     */
    public static List<AppStatisticsTeamTabVo> createTabDataDirectory() {
        List<AppStatisticsTeamTabVo> teamTabVoList = new ArrayList<>();
        StatisticsEnumUtil.TabDataTypeEnum[] values = values();
        for (StatisticsEnumUtil.TabDataTypeEnum value : values) {
            AppStatisticsTeamTabVo teamTabVo = new AppStatisticsTeamTabVo();
            teamTabVo.setName(value.getMsg());
            teamTabVo.setDataType(value.getCode());
            teamTabVo.setDataList(newArrayList());
            teamTabVoList.add(teamTabVo);
        }
        return teamTabVoList;
    }

    /**
     * @param allRuleIds  当天的考勤规则ids
     * @param day         当天日期 判断次日使用
     * @param clockInList 打卡的所有列表
     * @param clockIns    所有的打卡结果记录
     * @return 集合
     */
    public static List<ClockClassRecord> setOldDataNew(List<String> allRuleIds, Date day, List<ClockClassRecord> clockInList, List<ClockClassRecord> clockIns) {
        // 过滤出打卡结果不是空的
        if (CollUtil.isNotEmpty(clockInList)) {
            List<ClockClassRecord> collect = clockInList.stream().filter(v -> allRuleIds.contains(v.getRuleId())).collect(Collectors.toList());
            if (!collect.isEmpty()) {
                List<Integer> indexList = new ArrayList<>();
                collect.forEach(v -> indexList.add(clockInList.indexOf(v)));
                Integer minDelIndex = null;
                Integer maxDelIndex = null;
                // 获取最大 和 最小的 index
                int minIndex = indexList.get(0);
                int maxIndex = indexList.get(indexList.size() - 1);
                if (minIndex > 0) {
                    // 往上查找不在 allRuleIds 中的 ruleId 不为空的记录 并移除往上所有记录
                    for (int i = indexList.get(0) - 1; i > 0; i--) {
                        ClockClassRecord vo = clockInList.get(i);
                        if (vo.getRuleId() != null && !allRuleIds.contains(vo.getRuleId())) {
                            minDelIndex = i;
                            break;
                        }
                    }
                }
                if (maxIndex < clockInList.size() - 1) {
                    // 往下查找不在 allRuleIds 中的 ruleId 不为空的记录 并移除往下所有记录
                    for (int i = indexList.get(0) + 1; i < clockInList.size(); i++) {
                        ClockClassRecord vo = clockInList.get(i);
                        if (vo.getRuleId() != null && !allRuleIds.contains(vo.getRuleId())) {
                            maxDelIndex = i;
                            break;
                        }
                    }
                }
                // 排除 ruleId 在 ruleIds 中的记录 和 minDelIndex 往上的记录 和 maxDelIndex 往下的记录
                List<ClockClassRecord> removeList = new ArrayList<>();
                if (null != minDelIndex) {
                    removeList.addAll(clockInList.subList(0, minDelIndex));
                }
                if (null != maxDelIndex) {
                    removeList.addAll(clockInList.subList(maxDelIndex, clockInList.size() - 1));
                }
                clockInList.removeAll(removeList);
                // 因为传入的数据都是符合本考勤组的数据，所以只用过滤空
                clockInList.removeIf(v -> null != v.getRuleId());
            }
            if (!clockInList.isEmpty()) {
                // 转换数据 clockInTime 格式化, 打卡方式 转换成文字
                clockInList.forEach(v -> {
                    String str = DateDetail.getDate2Str(day, DateDetail.DF).equals(DateDetail.getDate2Str(v.getClockInTime(), DateDetail.DF)) ? "" : "次日";
                    v.setClockInTimeStr(str + DateDetail.getDate2Str(v.getClockInTime(), DateDetail.DF10));
                    v.setClockInMethod(getClockInMethod(null == v.getClockInMethod() || v.getClockInMethod().isEmpty() ? ConstantUtil.DEVICE_UNKNOWN : Integer.parseInt(v.getClockInMethod())));
                });

                // 将clockInList插入到今日出勤中[插入到两个班次时间之间]
                if (clockIns.isEmpty()) {
                    clockIns.addAll(clockInList.stream().filter(v -> null == v.getRuleId() || allRuleIds.contains(v.getRuleId())).sorted(Comparator.comparing(ClockClassRecord::getClockInTime)).collect(Collectors.toList()));
                } else {
                    Map<ClockClassRecord, List<ClockClassRecord>> map = new HashMap<>();
                    for (int i = 0; i < clockIns.size(); i++) {
                        ClockClassRecord v1 = clockIns.get(i);
                        if (Objects.isNull(v1.getWorkDate())) {
                            continue;
                        }
                        ClockClassRecord v2 = null;
                        if (i + 1 < clockIns.size()) {
                            v2 = clockIns.get(i + 1);
                        }
                        if (i == 0) {
                            // 判断小于v1的clockInList
                            try {
                                List<ClockClassRecord> list = clockInList.stream().filter(clockIn -> clockIn.getClockInTime().before(v1.getWorkDate())).collect(Collectors.toList());
                                map.put(null, list);
                            } catch (Exception e) {
                                log.error("判断小于v1的clockInList异常：{}", e.getMessage());
                            }
                        }
                        if (null != v2) {
                            ClockClassRecord v = v2;
                            // 如果v2不为空, 判断哪些clockInList在v1, v2班次之间
                            List<ClockClassRecord> list = clockInList.stream().filter(clockIn -> clockIn.getClockInTime().after(v1.getWorkDate()) && clockIn.getClockInTime().before(v.getWorkDate())).collect(Collectors.toList());
                            map.putIfAbsent(v1, list);
                        } else {
                            // 如果v2为空, 判断哪些clockInList大于v1班次
                            List<ClockClassRecord> list = clockInList.stream().filter(clockIn -> clockIn.getClockInTime().after(v1.getWorkDate())).collect(Collectors.toList());
                            map.putIfAbsent(v1, list);

                        }
                    }
                    map.keySet().forEach(key -> {
                        List<ClockClassRecord> list = map.get(key).stream().sorted(Comparator.comparing(ClockClassRecord::getClockInTime)).collect(Collectors.toList());
                        if (key == null) {
                            clockIns.addAll(0, list);
                        } else {
                            int index = clockIns.indexOf(key);
                            if (index + 1 > clockIns.size() - 1) {
                                clockIns.addAll(list);
                            } else {
                                clockIns.addAll(index + 1, list);
                            }
                        }
                    });
                }
            }
        }
        return clockIns;
    }

    public static void setClockTime(String groupId, Map<String, ClockInExportVo> keyMap, String userId, String day, List<ClockClassRecord> dailyClockInVos, SimpleDateFormat ymd, SimpleDateFormat hms) {
        // 打卡集合
        if (null != dailyClockInVos && !dailyClockInVos.isEmpty()) {
            StringBuilder clockTime = new StringBuilder();
            StringBuilder otherClockTime = new StringBuilder();
            List<String> oldRuleIds = new ArrayList<>();
            List<String> oldClockRuleIds = new ArrayList<>();
            // 当天旷工 将打卡信息的userId,date,groupId组合为key
            ClockInExportVo clockInExportVo = keyMap.get(userId + "-" + day + "-" + groupId);
            int index = 0;
            int count = Integer.parseInt(dailyClockInVos.stream().filter(m -> m.getIsEffectiveClock() == 1).count() + "");
            if (null != clockInExportVo) {
                if (null != clockInExportVo.getClockTime()) {
                    clockTime.append(clockInExportVo.getClockTime());
                }
                boolean last = false;
                for (int i = 0; i < dailyClockInVos.size(); i++) {
                    ClockClassRecord clockClassRecord = dailyClockInVos.get(i);
                    if (1 == clockClassRecord.getIsEffectiveClock()) {
                        index++;
                        if (1 == clockClassRecord.getAbsence()) {
                            if (!oldRuleIds.contains(clockClassRecord.getRuleId())) {
                                last = true;
                                oldRuleIds.add(clockClassRecord.getRuleId());
                                if (i < 1) {
                                    clockTime.append("旷工");
                                } else {
                                    // 旷工 ==》会有 旷工|旷工
                                    clockTime.append(" | 旷工");
                                }
                            }
                        } else {
                            // 打卡去重
                            if (last) {
                                clockTime.append(" | ");
                            }
                            last = false;
                            // 组装 09:01-14:01 | 17:55-次日08:05
                            if (null != clockClassRecord.getClockInStatus()) {

                                if (!oldRuleIds.contains(clockClassRecord.getRuleId())) {

                                    if (!oldClockRuleIds.contains(clockClassRecord.getRuleId())) {
                                        oldClockRuleIds.add(clockClassRecord.getRuleId());
                                        // 该组出勤记录第一次进入
                                        if (-1 == clockClassRecord.getClockInStatus()) {
                                            if (!oldRuleIds.contains(clockClassRecord.getRuleId())) {
                                                // 没被记录为旷工时打缺卡
                                                clockTime.append("缺卡");
                                                addSplicing(index, count, clockTime);
                                            }
                                        } else if (-2 == clockClassRecord.getClockInStatus()) {
                                            clockTime.append("无需打卡");
                                            addSplicing(index, count, clockTime);
                                        } else {
                                            if (null != clockClassRecord.getClockInTime()) {
                                                // 比对是否在次日
                                                if (!day.equals(ymd.format(clockClassRecord.getClockInTime()))) {
                                                    clockTime.append("次日");
                                                }
                                                clockTime.append(hms.format(clockClassRecord.getClockInTime()));
                                            }
                                            addSplicing(index, count, clockTime);
                                        }
                                    } else {
                                        if (-2 == clockClassRecord.getClockInStatus()) {
                                            clockTime.append("无需打卡");
                                            addSplicing(index, count, clockTime);
                                        } else {
                                            // 不是第一次进入
                                            if (-1 == clockClassRecord.getClockInStatus()) {
                                                clockTime.append("缺卡");
                                                addSplicing(index, count, clockTime);
                                            } else {
                                                if (null != clockClassRecord.getClockInTime()) {
                                                    // 比对是否在次日
                                                    if (!day.equals(ymd.format(clockClassRecord.getClockInTime()))) {
                                                        clockTime.append("次日");
                                                    }
                                                    clockTime.append(hms.format(clockClassRecord.getClockInTime()));
                                                }
                                                addSplicing(index, count, clockTime);
                                            }
                                        }
                                    }
                                }
                            } else {
                                // 其他打卡
                                // 比对是否在次日
                                if (null != clockClassRecord.getClockInTime()) {
                                    if (!day.equals(ymd.format(clockClassRecord.getClockInTime()))) {
                                        otherClockTime.append("次日");
                                    }
                                    otherClockTime.append(hms.format(clockClassRecord.getClockInTime())).append(" ");
                                }
                            }
                        }
                    } else {
                        if (null != clockClassRecord.getClockInTime()) {
                            // 其他打卡
                            otherClockTime.append(hms.format(clockClassRecord.getClockInTime())).append(" ");
                        }
                    }
                }
                clockInExportVo.setClockTime(clockTime.toString());
                clockInExportVo.setOtherClockTime(otherClockTime.toString());
            }
            // 暂时不管没有命中情况
        }
    }

    /**
     * 添加拼接符
     *
     * @param i         当前次数从0开始
     * @param size      总个数
     * @param clockTime 结果
     */
    public static void addSplicing(int i, int size, StringBuilder clockTime) {
        if (i % 2 != 0) {
            clockTime.append("-");
        } else {
            if (i != size) {
                clockTime.append(" | ");
            }
        }
    }

    public static void checkOrdinary(List<UserRuleListVo> dayList, RemarkVo remarkVo) {
        // 假设当天是班次时间1 9:00 - 18:00 请假12：00 - 14:00 会生成本组出勤规则==》1. 9-12 普班 班次1 班次时间1 9:00 - 18:00   2.12-14  假  3. 14-18 普班 班次1 班次时间1 9:00 - 18:00  当为这个时，需要拼接为  9：00-12:00|14：00-18:00（12:00-14:00请假）
        // 假设当天是班次时间1 9:00 - 18:00 请假9：00 - 12:00 会生成本组出勤规则==》1. 9-12 假   2. 12-18 普班 班次1 班次时间1 9:00 - 18:00  当为这个时，需要拼接为 12：00-18:00（9:00-12:00请假）
        // 假设当天是班次时间1 9:00 - 18:00 借调9：00 - 14:00 会生成本组出勤规则==》1. （借调回岗时间）14-18 普班 班次1 班次时间1 9:00 - 18:00  当为这个时，需要拼接为    14：00-18:00
        // 过滤出普班和加班的出勤规则，当加班时对比时间全部加入，当普班时时间相同只加一个
        List<UserRuleListVo> shiftList = dayList.stream().filter(dayRule -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), dayRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), dayRule.getAttendanceType())).collect(Collectors.toList()).stream().sorted(Comparator.comparing(UserRuleListVo::getInPoint)).collect(Collectors.toList());

        if (!shiftList.isEmpty()) {
            SimpleDateFormat formatter = new SimpleDateFormat("HH:mm");
            // 班次日期
            String day = dayList.get(0).getDay();
            SimpleDateFormat ymd = new SimpleDateFormat("yyyy-MM-dd");
            // 9:00-12:00 | 14:00-18:00
            for (int i = 0; i < shiftList.size(); i++) {
                UserRuleListVo vo = shiftList.get(i);

                if (i == 0) {
                    if (!day.equals(ymd.format(vo.getInPoint()))) {
                        remarkVo.getShiftPeriod().append("次日");
                    }
                    remarkVo.getShiftPeriod().append(formatter.format(vo.getInPoint())).append("-");
                    if (!day.equals(ymd.format(vo.getOutPoint()))) {
                        remarkVo.getShiftPeriod().append("次日");
                    }
                    remarkVo.getShiftPeriod().append(formatter.format(vo.getOutPoint()));
                } else {
                    remarkVo.getShiftPeriod().append(" | ");
                    if (!day.equals(ymd.format(vo.getInPoint()))) {
                        remarkVo.getShiftPeriod().append("次日");
                    }
                    remarkVo.getShiftPeriod().append(formatter.format(vo.getInPoint())).append("-");
                    if (!day.equals(ymd.format(vo.getOutPoint()))) {
                        remarkVo.getShiftPeriod().append("次日");
                    }
                    remarkVo.getShiftPeriod().append(formatter.format(vo.getOutPoint()));
                }
            }
        }
    }

    public static void checkLeave(List<UserRuleListVo> dayList, RemarkVo remarkVo) {
        List<UserRuleListVo> leave = dayList.stream().filter(dayRule -> Objects.equals(AttendanceTypeEnum.LEAVE.getCode(), dayRule.getAttendanceType())).collect(Collectors.toList()).stream().sorted(Comparator.comparing(UserRuleListVo::getInPoint)).collect(Collectors.toList());
        SimpleDateFormat formatter = new SimpleDateFormat("HH:mm");
        // 组装请假
        if (!leave.isEmpty()) {
            // 如果当天全是请假，那么通过请假命中的时段组装当天上班时段
            if (remarkVo.getShiftPeriod().toString().isEmpty()) {
                for (int i = 0; i < leave.size(); i++) {
                    UserRuleListVo vo = leave.get(i);
                    if (i == 0) {
                        remarkVo.getShiftPeriod().append(formatter.format(vo.getInPoint())).append("-").append(formatter.format(vo.getOutPoint()));
                    } else {
                        remarkVo.getShiftPeriod().append(" | ").append(formatter.format(vo.getInPoint())).append("-").append(formatter.format(vo.getOutPoint()));
                    }
                }
            }

            int size = (int) dayList.stream().filter(dayRule -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), dayRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), dayRule.getAttendanceType())).count();
            boolean b = size == 0;
            // 当天没有普班
            List<Boolean> leaveBoolean = new ArrayList<>();
            // 组装请假时段 09:00-18:00（09:00-10:00请假）09:00-18:00（当天请假）09:00-18:00（上半天请假）
            leave.forEach(v -> {
                if (null != v.getApplyUnit()) {
                    boolean s = false;
                    switch (v.getApplyUnit()) {
                        // 申请单位（1小时 2日 3半日）
                        case 1:
                            remarkVo.getShiftPeriod().append("（").append(formatter.format(v.getInPoint())).append("-").append(formatter.format(v.getOutPoint())).append("请假）");
                            s = true;
                            break;
                        case 2:
                            remarkVo.getShiftPeriod().append("（当天请假 ）");
                            remarkVo.getClockTime().append("无需打卡");
                            break;
                        case 3:
                            // 上半天 下半天 班次类型 0全天班次 1上午班次 2下午班次
                            remarkVo.getShiftPeriod().append("（").append(0 == v.getSchedulesType() ? "当天请假）" : 1 == v.getSchedulesType() ? "上半天请假）" : "下半天请假）");
                            if (0 == v.getSchedulesType()) {
                                remarkVo.getClockTime().append("无需打卡");
                            }
                        default:
                            break;
                    }
                    leaveBoolean.add(s);
                }
            });
            if (b && leaveBoolean.stream().allMatch(Boolean::booleanValue)) {
                // 仅小时假使用
                remarkVo.getClockTime().append("无需打卡");
            }
        }
    }

    /**
     * 判断给定的开始时间和结束时间是否跨月
     *
     * @param startDate 开始时间
     * @param endDate   结束时间
     * @return 如果跨月则返回true，否则返回false
     */
    public static boolean isCrossMonth(LocalDateTime startDate, LocalDateTime endDate) {
        return !startDate.getMonth().equals(endDate.getMonth());
    }

    /**
     * 处理借调记录(json对象)
     *
     * @param timeAllList          借调记录集合
     * @param userSecondRecordList 借调记录集合
     */
    public static void handleDateJsonObject(DateRangeDto dateRangeDto, List<TimeJson> timeAllList, List<UserSecondRecord> userSecondRecordList) throws Exception {
        if (CollUtil.isNotEmpty(userSecondRecordList)) {
            for (UserSecondRecord item : userSecondRecordList) {
                if (StringUtil.isNotEmpty(item.getTimeJson())) {
                    String dbGroupUserTimeJson = item.getTimeJson();
                    List<SecondmentDateVo> dateVoList = JsonUtil.getJsonToList(dbGroupUserTimeJson, SecondmentDateVo.class);
                    if (CollUtil.isNotEmpty(dateVoList)) {
                        dateVoList = dateVoList.stream().filter(distinctByKey(item1 -> item1.getStartTime() + "_" + item1.getEndTime())).collect(Collectors.toList());
                    }
                    //判断借调时间段是否在筛选日期内
                    if (CollUtil.isNotEmpty(dateVoList)) {
                        for (SecondmentDateVo secondmentDateVo : dateVoList) {
                            DateTime startDate1 = new DateTime(secondmentDateVo.getStartTime());
                            DateTime endDate1 = new DateTime(secondmentDateVo.getEndTime());
                            DateTime startDate2 = new DateTime(dateRangeDto.getStartDate());
                            DateTime endDate2 = new DateTime(dateRangeDto.getEndDate());
                            HashMap<String, DateTime> hashMap = getOverlap(startDate1, endDate1, startDate2, endDate2, false);
                            if (CollUtil.isNotEmpty(hashMap)) {
                                TimeJson json = new TimeJson();
                                json.setApprovalId(secondmentDateVo.getApprovalId());
                                json.setStartTime(secondmentDateVo.getStartTime());
                                json.setEndTime(secondmentDateVo.getEndTime());
                                json.setGroupId(item.getGroupId());
                                timeAllList.add(json);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * 根据指定键去重
     */
    private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
        Set<Object> seen = new HashSet<>();
        return t -> seen.add(keyExtractor.apply(t));
    }

    /**
     * 获取两个时间范围的交集
     *
     * @param start1 时间1的开始时间
     * @param end1 时间1的结束时间
     * @param start2 时间2的开始时间
     * @param end2 时间2的结束时间
     * @return 时间范围的交集
     */
    public static DateRangeDto1 getIntersection(LocalDateTime start1, LocalDateTime end1, LocalDateTime start2, LocalDateTime end2) {
        // 首先，确保每个时间范围的开始时间早于或等于结束时间
        if (start1.isAfter(end1) || start2.isAfter(end2)) {
            throw new IllegalArgumentException("时间范围无效：开始时间不能晚于结束时间");
        }
        // 确定交集的起始时间和结束时间
        LocalDateTime intersectionStart = start1.isAfter(start2) ? start1 : start2;
        LocalDateTime intersectionEnd = end1.isBefore(end2) ? end1 : end2;
        // 检查是否有交集
        if (intersectionStart.isAfter(intersectionEnd)) {
            // 没有交集，返回null或空数组（这里返回空数组）
            return null;
        } else {
            // 有交集，返回交集的开始和结束时间
            return new DateRangeDto1(intersectionStart, intersectionEnd);
        }
    }

    /**
     * 判断班次是否旷工
     */
    public static Boolean isAbsence(List<ClockInResult> clockInResultList) {
        boolean isAbsence = Boolean.FALSE;
        if (CollUtil.isNotEmpty(clockInResultList)) {
            //判断是否旷工
            List<Integer> isAbsenceList = clockInResultList.stream().filter(m -> m.getDeleteMark().equals(ConstantUtil.NUM_FALSE) && m.getAbsence().equals(ConstantUtil.NUM_TRUE)).map(ClockInResult::getAbsence).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(isAbsenceList)) {
                isAbsence = Boolean.TRUE;
            }
        }
        return isAbsence;
    }

    /**
     * 是否手动变更为旷工，并且班次没有缺卡
     */
    public static Boolean isHeadAbsence(List<ClockInResultRecordVo> clockInResultList) {
        boolean isAbsence = Boolean.FALSE;
        if (CollUtil.isNotEmpty(clockInResultList)) {
            //判断是否旷工
            List<Integer> isAbsenceList = clockInResultList.stream().filter(m -> m.getDeleteMark().equals(ConstantUtil.NUM_FALSE) && m.getAbsence().equals(ConstantUtil.NUM_TRUE)).map(ClockInResultRecordVo::getAbsence).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(isAbsenceList)) {
                isAbsence = Boolean.TRUE;
            }
        }
        return isAbsence;
    }

    /**
     * 获取每天的出勤状态
     *
     * @param dayStatisticsQueryVos 日统计数据
     * @return List<Integer>
     */
    public static List<Integer> getClockStatus(List<DayStatisticsQueryVo> dayStatisticsQueryVos) {
        List<Integer> clockStatus = new ArrayList<>();
        if (CollUtil.isNotEmpty(dayStatisticsQueryVos)) {
            if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getLateTimes() != null && item.getLateTimes() > 0)) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.CD.getCode());
            }
            if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getAbsenceCardTimes() != null && item.getAbsenceCardTimes() > 0)) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.QK.getCode());
            }
            if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getEarlyLeaveTimes() != null && item.getEarlyLeaveTimes() > 0)) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.ZT.getCode());
            }
            if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getAbsenceTimes() != null && item.getAbsenceTimes() > 0)) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.KG.getCode());
            }
            if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getOutworkTimes() != null && item.getOutworkTimes() > 0)) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.WQ.getCode());
            }
            if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getOvertimeTimes() != null && item.getOvertimeTimes() > 0)) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.JB.getCode());
            }
            if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getSelfGroup() != null && item.getSelfGroup().equals(ConstantUtil.NUM_TRUE))) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.JD.getCode());
            }
            if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getLeaveBatchNumber() != null && StringUtil.isNotEmpty(item.getLeaveBatchNumber()))) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.QJ.getCode());
            }
            if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getBusDays() != null && item.getBusDays().compareTo(BigDecimal.ZERO) > 0)) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.CC.getCode());
            }
            if (dayStatisticsQueryVos.stream().anyMatch(item -> (item.getOutDays() != null && item.getOutDays().compareTo(BigDecimal.ZERO) > 0) || (item.getOutHours() != null && item.getOutHours().compareTo(BigDecimal.ZERO) > 0))) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.WC.getCode());
            }
            if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getRestTimes() != null && item.getRestTimes() > 0)) {
                clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.XX.getCode());
            }
        }
        return clockStatus;
    }

    /**
     * 处理出差
     *
     * @param dayList  每日出勤规则
     * @param remarkVo 组装信息
     */
    public static void checkBusinessTrip(List<UserRuleListVo> dayList, RemarkVo remarkVo) {
        List<UserRuleListVo> businessTrip = dayList.stream().filter(dayRule -> Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), dayRule.getApplyViewEnable())).collect(Collectors.toList()).stream().sorted(Comparator.comparing(UserRuleListVo::getInPoint)).collect(Collectors.toList());
        // 组装请假
        if (!businessTrip.isEmpty()) {
            // 如果当天全是外出，那么通过外出命中的时段组装当天上班时段
            if (remarkVo.getShiftPeriod().toString().isEmpty()) {
                SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
                for (int i = 0; i < businessTrip.size(); i++) {
                    UserRuleListVo vo = businessTrip.get(i);
                    if (i == 0) {
                        remarkVo.getShiftPeriod().append(formatter.format(vo.getInPoint())).append("-").append(formatter.format(vo.getOutPoint()));
                    } else {
                        remarkVo.getShiftPeriod().append(" | ").append(formatter.format(vo.getInPoint())).append("-").append(formatter.format(vo.getOutPoint()));
                    }
                }

            }
            // 组装请假时段 09:00-18:00（当天出差）
            remarkVo.getShiftPeriod().append("（当天出差）");
        }
    }

    //按照用户名拼音首字母排序
    public static List<ClockUser> getCollect(List<ClockUser> clockUserList) {
        return clockUserList.stream().sorted(Comparator.comparing(ClockUser::getIdentifying)).collect(Collectors.toList());
    }

    /**
     * 将列表按照指定大小进行分割
     *
     * @param list 需要分割的列表
     * @param size 每个子列表的大小
     * @param <T>  列表元素的类型
     * @return 分割后的子列表集合
     */
    public static <T> List<List<T>> partition(List<T> list, int size) {
        return IntStream.iterate(0, i -> i < list.size(), i -> i + size).mapToObj(i -> list.subList(i, Math.min(i + size, list.size()))).collect(Collectors.toList());
    }

    /**
     * 将 DayStatisticsPageListVo 对象的属性字段名和属性值解析成 Map
     *
     * @param dayStaVo DayStatisticsPageListVo 对象
     * @param aTrue     是否日统计
     * @return 字段名和属性值的映射关系
     */
    @SneakyThrows
    public static <T> Map<String, Object> getObjectFieldMap(T dayStaVo, Boolean aTrue) {
        Map<String, Object> fieldMap = new HashMap<>();
        Class<?> clazz = dayStaVo.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            if ("customLeaveList".equals(field.getName()) && Objects.nonNull(field.get(dayStaVo))) {
                Object fieldValue = field.get(dayStaVo);
                List<LeaveTypeJsonData> customLeaveList = new ArrayList<>();
                if (fieldValue instanceof List<?>) {
                    ObjectMapper mapper = new ObjectMapper();
                    List<?> rawList = (List<?>) fieldValue;
                    for (Object item : rawList) {
                        if (item instanceof LeaveTypeJsonData) {
                            customLeaveList.add((LeaveTypeJsonData) item);
                        } else {
                            // 如果是Map类型，可以转换
                            LeaveTypeJsonData data = mapper.convertValue(item, LeaveTypeJsonData.class);
                            customLeaveList.add(data);
                        }
                    }
                }
                if (CollUtil.isNotEmpty(customLeaveList)) {
                    customLeaveList.forEach(item -> {
                        fieldMap.put(item.getField(), aTrue ? item.getLeaveHours() : item.getLeaveDays());
                    });
                }
            }
            ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
            // 排除掉集合字段
            if (!Collection.class.isAssignableFrom(field.getType()) && annotation != null) {
                if (String.class.isAssignableFrom(field.getType())) {
                    fieldMap.put(field.getName(), Objects.nonNull(field.get(dayStaVo)) ? field.get(dayStaVo).toString().replaceAll("~", "-") : "");
                } else {
                    fieldMap.put(field.getName(), field.get(dayStaVo));
                }
            }
        }

        return fieldMap;
    }

    /**
     * 获取日统计导出数据列表
     *
     * @param dayStaVo           查询参数
     * @param leaveHeadersMap    请假头
     * @return 数据列表
     */
    public static Map<Integer, Object> getDayStaDataList(DayStatisticsPageListVo dayStaVo, LinkedHashMap<String, Integer> leaveHeadersMap) {
        Map<String, String> headerFieldFixMap = new HashMap<>();
        headerFieldFixMap.put("workStatus", "workStatusStr");
        headerFieldFixMap.put("shiftList", "shiftStr");
        headerFieldFixMap.put("shiftTimeList", "shiftTimeStr");
        headerFieldFixMap.put("clockTimeList", "clockTimeStr");
        Map<String, Object> headerFieldMap = getObjectFieldMap(dayStaVo, Boolean.TRUE);
        if (CollUtil.isNotEmpty(dayStaVo.getCustomLeaveList())) {
            List<LeaveTypeJsonData> customLeaveList = dayStaVo.getCustomLeaveList();
            customLeaveList.forEach(jsonData -> {
                headerFieldMap.put(jsonData.getField(), Objects.isNull(jsonData.getLeaveHours()) ? "0" : jsonData.getLeaveHours());
            });
        }
        AtomicInteger atomicInteger = new AtomicInteger(0);
        Map<Integer, Object> dataList = new HashMap<>();
        leaveHeadersMap.forEach((key, value) -> {
            Object orDefault = headerFieldMap.getOrDefault(headerFieldFixMap.getOrDefault(key, key), headerFieldFixMap.containsKey(key) ? "" : 0);
            dataList.put(atomicInteger.getAndIncrement(), orDefault);
        });
        return dataList;
    }

    /**
     * 获取月统计导出数据列表
     *
     * @param monthStaVo           查询参数
     * @param leaveHeadersMap      请假头
     * @return 数据列表
     */
    public static Map<Integer, Object> getMonthStaDataList(MonthStatisticsPageListVo monthStaVo, Map<String, Integer> leaveHeadersMap) {
        Map<String, String> headerFieldFixMap = new HashMap<>();
        headerFieldFixMap.put("workStatus", "workStatusStr");
        headerFieldFixMap.put("cycleList", "cycleStr");
        Map<String, Object> headerFieldMap = getObjectFieldMap(monthStaVo, Boolean.TRUE);
        if (CollUtil.isNotEmpty(monthStaVo.getCustomLeaveList())) {
            List<LeaveTypeJsonData> customLeaveList = monthStaVo.getCustomLeaveList();
            customLeaveList.forEach(jsonData -> {
                headerFieldMap.put(jsonData.getField(), Objects.isNull(jsonData.getLeaveDays()) ? "0" : jsonData.getLeaveDays());
            });
        }
        AtomicInteger atomicInteger = new AtomicInteger(0);
        Map<Integer, Object> dataList = new HashMap<>();
        leaveHeadersMap.forEach((key, value) -> {
            Object orDefault = headerFieldMap.getOrDefault(headerFieldFixMap.getOrDefault(key, key), headerFieldFixMap.containsKey(key) ? "" : 0);
            dataList.put(atomicInteger.getAndIncrement(), orDefault);
        });
        return dataList;
    }

    /**
     * 获取日统计自定义请假数据
     *
     * @param customLeaveList 自定义请假数据
     * @return 自定义请假数据
     */
    public static List<LeaveTypeJsonData> getDayCustomLeaveList(List<LinkedHashMap<String, Object>> customLeaveList) {
        if (CollUtil.isEmpty(customLeaveList)) {
            return new ArrayList<>();
        }
        ObjectMapper mapper = new ObjectMapper();
        List<LeaveTypeJsonData> customLeaveJsonList = customLeaveList.stream().map(item -> mapper.convertValue(item, LeaveTypeJsonData.class)).collect(Collectors.toList());
        Map<String, List<LeaveTypeJsonData>> customLeaveMap = customLeaveJsonList.stream().collect(Collectors.groupingBy(LeaveTypeJsonData::getField));
        List<LeaveTypeJsonData> typeJsonDataArrayList = new ArrayList<>(customLeaveMap.size());
        customLeaveMap.forEach((field, customizeTableVo) -> {
            if (CollUtil.isEmpty(customizeTableVo)) {
                return;
            }
            LeaveTypeJsonData firstItem = customizeTableVo.get(0);
            BigDecimal totalLeaveDays = customizeTableVo.stream().map(LeaveTypeJsonData::getLeaveDays).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
            BigDecimal totalLeaveHours = customizeTableVo.stream().map(LeaveTypeJsonData::getLeaveHours).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
            BigDecimal totalLeavePayrollHours = customizeTableVo.stream().map(LeaveTypeJsonData::getLeavePayrollHours).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
            LeaveTypeJsonData builder = LeaveTypeJsonData.builder()
                    .field(firstItem.getField())
                    .fieldName(firstItem.getFieldName())
                    .headName(firstItem.getHeadName())
                    .leaveDays(totalLeaveDays)
                    .leaveHours(totalLeaveHours)
                    .leavePayrollHours(totalLeavePayrollHours)
                    .applyList(firstItem.getApplyList())
                    .build();
            typeJsonDataArrayList.add(builder);
        });
        return typeJsonDataArrayList;
    }

    /**
     * 获取日统计自定义数据
     *
     * @param dayMinutesList 自定义数据
     * @return 自定义数据
     */
    public static <T> List<T> getCustomDataList(List<List<LinkedHashMap<String, BigDecimal>>> dayMinutesList, Class<T> clazz) {
        List<T> result = new ArrayList<>();
        if (CollUtil.isEmpty(dayMinutesList)) {
            return result;
        }
        for (List<LinkedHashMap<String, BigDecimal>> item : dayMinutesList) {
            item.forEach(linkedHashMap -> {
                ObjectMapper mapper = new ObjectMapper();
                T data = mapper.convertValue(linkedHashMap, clazz);
                result.add(data);
            });
        }
        return result;
    }

    /**
     * 获取月统计自定义请假数据
     *
     * @param customLeaveList 自定义请假数据
     * @return 自定义请假数据
     */
    public static List<LeaveTypeJsonData> getMonthCustomLeaveList(List<LinkedHashMap<String, Object>> customLeaveList) {
        if (CollUtil.isEmpty(customLeaveList)) {
            return new ArrayList<>();
        }
        List<LeaveTypeJsonData> customLeaveJsonList = new ArrayList<>();
        ObjectMapper mapper = new ObjectMapper();
        for (LinkedHashMap<String, Object> item : customLeaveList) {
            // 移除不需要的字段，避免影响原始数据
            LinkedHashMap<String, Object> filteredItem = new LinkedHashMap<>(item);
            // 转换为目标对象
            LeaveTypeJsonData data = mapper.convertValue(filteredItem, LeaveTypeJsonData.class);
            customLeaveJsonList.add(data);
        }
        List<LeaveTypeJsonData> typeJsonDataArrayList = new ArrayList<>();
        Map<String, List<LeaveTypeJsonData>> customLeaveMap = customLeaveJsonList.stream().collect(Collectors.groupingBy(LeaveTypeJsonData::getField));
        customLeaveMap.forEach((field, customizeTableVo) -> {
            LeaveTypeJsonData builder = LeaveTypeJsonData.builder()
                    .field(customizeTableVo.get(0).getField())
                    .fieldName(customizeTableVo.get(0).getFieldName())
                    .headName(customizeTableVo.get(0).getHeadName())
                    .leaveDays(customizeTableVo.stream().map(LeaveTypeJsonData::getLeaveDays).reduce(BigDecimal.ZERO, BigDecimal::add))
                    .leaveHours(customizeTableVo.stream().map(LeaveTypeJsonData::getLeaveHours).reduce(BigDecimal.ZERO, BigDecimal::add))
                    .leavePayrollHours(customizeTableVo.stream().map(LeaveTypeJsonData::getLeavePayrollHours).reduce(BigDecimal.ZERO, BigDecimal::add))
                    .applyList(customizeTableVo.stream().flatMap(item -> item.getApplyList().stream()).collect(Collectors.toList()))
                    .build();
            typeJsonDataArrayList.add(builder);
        });
        return typeJsonDataArrayList;
    }

    /**
     * 获取日范围
     *
     * @param startDate 开始时间
     * @param endDate 结束时间
     * @param formatter 时间格式
     */
    public static String formatDayRange(Date startDate, Date endDate, DateTimeFormatter formatter) {
        return DateUtil.format(startDate, formatter) + "至" + DateUtil.format(endDate, formatter);
    }

    /**
     * 处理打卡结果
     *
     * @param results 打卡结果
     * @param userRuleRecord 用户规则
     * @param overtimeRecord 加班记录
     */
    public static void processClockInResults(List<ClockInResultRecordVo> results, UserRuleRecord userRuleRecord, OvertimeRecord overtimeRecord) {
        Date startTime = null, endTime = null;
        if (results.size() == 2) {
            for (ClockInResultRecordVo result : results) {
                if (Objects.equals(result.getClockInType(), ConstantUtil.ON_WORK)) {
                    overtimeRecord.setOnIsAbsenceCard(isAbsenceCard(result));
                    startTime = getEffectiveTime(result, userRuleRecord.getInPoint());
                } else {
                    overtimeRecord.setOffIsAbsenceCard(isAbsenceCard(result));
                    endTime = getEffectiveTime(result, userRuleRecord.getOutPoint());
                }
            }
        } else if (results.size() == 1) {
            ClockInResultRecordVo result = results.get(0);
            boolean isOnWork = Objects.equals(result.getClockInType(), ConstantUtil.ON_WORK);
            if (isOnWork) {
                overtimeRecord.setOnIsAbsenceCard(isAbsenceCard(result));
                startTime = getEffectiveTime(result, userRuleRecord.getInPoint());
                endTime = userRuleRecord.getOutPoint();
                overtimeRecord.setOffIsAbsenceCard(1);
            } else {
                overtimeRecord.setOnIsAbsenceCard(1);
                startTime = userRuleRecord.getInPoint();
                endTime = getEffectiveTime(result, userRuleRecord.getOutPoint());
                overtimeRecord.setOffIsAbsenceCard(isAbsenceCard(result));
            }
        }
        overtimeRecord.setOvertimeDate(userRuleRecord.getDay());
        overtimeRecord.setStartTime(startTime);
        overtimeRecord.setEndTime(endTime);
        if (overtimeRecord.getOnIsAbsenceCard().equals(ConstantUtil.NUM_FALSE) && overtimeRecord.getOffIsAbsenceCard().equals(ConstantUtil.NUM_FALSE)) {
            overtimeRecord.setOvertimeDuration(DateConvertUtil.dateConvert(Objects.requireNonNull(startTime), Objects.requireNonNull(endTime), 2, 2, null));
        }
    }

    public static void handleEmptyClockInResults(UserRuleRecord userRuleRecord, OvertimeRecord overtimeRecord) {
        overtimeRecord.setOvertimeDate(userRuleRecord.getDay());
        overtimeRecord.setStartTime(userRuleRecord.getInPoint());
        overtimeRecord.setEndTime(userRuleRecord.getOutPoint());
        overtimeRecord.setOnIsAbsenceCard(1);
        overtimeRecord.setOffIsAbsenceCard(1);
    }

    /**
     * 判断是否为补卡
     *
     * @param result 打卡结果
     * @return 是否为补卡
     */
    private static Integer isAbsenceCard(ClockInResultRecordVo result) {
        return result.getClockInStatus() != null && result.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue()) ? 1 : 0;
    }

    /**
     * 获取有效的时间
     *
     * @param result 打卡结果
     * @param defaultTime 默认时间
     * @return 有效时间
     */
    public static Date getEffectiveTime(ClockInResultRecordVo result, Date defaultTime) {
        return Objects.nonNull(result.getEffectiveTime()) ? result.getEffectiveTime() : defaultTime;
    }

    /**
     * 安全地执行加法运算
     *
     * @param a 数字1
     * @param b 数字2
     * @return 结果
     */
    public static BigDecimal safeAdd(BigDecimal a, BigDecimal b) {
        if (a == null) {
            a = BigDecimal.ZERO;
        }
        if (b == null) {
            b = BigDecimal.ZERO;
        }
        return a.add(b);
    }

    /**
     * 获取默认值
     *
     * @param value 值
     * @param defaultValue 默认值
     * @return 默认值
     */
    public static <T extends Number> int defaultIfNull(T value, int defaultValue) {
        return value == null ? defaultValue : value.intValue();
    }

    /**
     * 计算字符串中逗号分隔的元素个数
     *
     * @param str 输入字符串
     * @return 元素个数
     */
    public static int countSplitElements(String str) {
        if (StrUtil.isBlank(str)) {
            return 0;
        }
        String trimmed = str.trim();
        if (trimmed.isEmpty()) {
            return 0;
        }
        if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
            List<BatchNumberResult> shiftsJsonVoList = parseArray(trimmed, BatchNumberResult.class);
            return Math.toIntExact(shiftsJsonVoList.stream().map(BatchNumberResult::getBatchNumberId).distinct().count());
        } else {
            return trimmed.split(",").length;
        }
    }

    /**
     * 获取加班数据
     *
     * @param attendanceRatio1 出勤换算比
     * @param dataList         数据列表
     * @return 处理结果
     */
    private static Map<OvertimeType, ProcessResult> processOvertime(BigDecimal attendanceRatio1, List<UserDaySituationData> dataList) {
        Map<OvertimeType, ProcessResult> resultMap = new HashMap<>();
        Arrays.stream(OvertimeType.values()).forEach(type -> resultMap.put(type, new ProcessResult()));
        try {
            List<UserDaySituationData> overtimeList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.JB);
            for (UserDaySituationData result : overtimeList) {
                if (CollUtil.isEmpty(result.getClockInResultList())) {
                    continue;
                }
                List<ClockInResult> clockInResults = getClockInResultFilterList(result.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.JB);
                if (CollUtil.isEmpty(clockInResults) || clockInResults.size() != 2) {
                    continue;
                }
                // 排除加班未打卡的记录
                boolean hasNoClock = clockInResults.stream().map(ClockInResult::getClockInStatus).collect(Collectors.toList()).contains(ClockInStatusEnum.NO_CLOCK.getValue());
                if (hasNoClock) {
                    continue;
                }
                ClockInResult on = clockInResults.stream().filter(m -> ConstantUtil.ON_WORK.equals(m.getClockInType())).findFirst().orElse(null);
                ClockInResult off = clockInResults.stream().filter(m -> ConstantUtil.OFF_WORK.equals(m.getClockInType())).findFirst().orElse(null);
                // 得到加班时长
                if (on != null && off != null) {
                    BigDecimal hours = DateConvertUtil.dateConvert(on.getEffectiveTime(), off.getEffectiveTime(), 2, 2, null);
                    if (StrUtil.isBlank(result.getPeriodInfo())) {
                        continue;
                    }
                    OvertimeRuleDetailVo overtimeRuleDetailVo = JSONObject.parseObject(result.getPeriodInfo(), OvertimeRuleDetailVo.class);
                    if (overtimeRuleDetailVo.getOvertimeType().equals(OvertimeType.WORK_DAY.getValue())) {
                        ProcessResult workDayInfo = resultMap.get(OvertimeType.WORK_DAY);
                        workDayInfo.setCount(workDayInfo.getCount() + 1);
                        workDayInfo.setHours(workDayInfo.getHours().add(hours));
                        workDayInfo.setDays(workDayInfo.getDays().add(hours.divide(attendanceRatio1, 2, RoundingMode.HALF_UP)));
                    }
                    if (overtimeRuleDetailVo.getOvertimeType().equals(OvertimeType.REST_DAY.getValue())) {
                        ProcessResult restDayInfo = resultMap.get(OvertimeType.REST_DAY);
                        restDayInfo.setCount(restDayInfo.getCount() + 1);
                        restDayInfo.setHours(restDayInfo.getHours().add(hours));
                        restDayInfo.setDays(restDayInfo.getDays().add(hours.divide(attendanceRatio1, 2, RoundingMode.HALF_UP)));
                    }
                    if (overtimeRuleDetailVo.getOvertimeType().equals(OvertimeType.HOLIDAY.getValue())) {
                        ProcessResult holidayInfo = resultMap.get(OvertimeType.HOLIDAY);
                        holidayInfo.setCount(holidayInfo.getCount() + 1);
                        holidayInfo.setHours(holidayInfo.getHours().add(hours));
                        holidayInfo.setDays(holidayInfo.getDays().add(hours.divide(attendanceRatio1, 2, RoundingMode.HALF_UP)));
                    }
                }
            }
            return resultMap;
        } catch (Exception e) {
            log.error("获取加班数据时发生异常", e);
            return resultMap;
        }
    }

    /**
     * 获取外勤数据
     *
     * @param dataList         数据列表
     * @param schedulesTypeMap 排班类型数据
     * @return 外勤数据
     */
    private static ProcessResult processOutworkClock(List<UserDaySituationData> dataList, Map<Integer, MutablePair<BigDecimal, MutablePair<BigDecimal, BigDecimal>>> schedulesTypeMap) {
        ProcessResult outworkResult = new ProcessResult();
        try {
            List<UserDaySituationData> outworkList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.WQ);
            for (UserDaySituationData itemRule : outworkList) {
                if (CollUtil.isEmpty(itemRule.getClockInResultList())) {
                    continue;
                }
                if (isAbsence(itemRule.getClockInResultList())) {
                    continue;
                }
                List<ClockInResult> clockInResults = getClockInResultFilterList(itemRule.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.WQ);
                if (CollUtil.isEmpty(clockInResults)) {
                    continue;
                }
                outworkResult.setCount(outworkResult.getCount() + clockInResults.size());
                // 只有普通班才会计算小时数和天数
                if (itemRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                    continue;
                }
                boolean isAllOutwork = clockInResults.stream().allMatch(m -> m.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE));
                if (!isAllOutwork) {
                    continue;
                }
                // 根据实际打卡记录计算小时数和天数
                Date actualAttendStartTime = null;
                Date actualAttendEndTime = null;
                Integer restMinute = 0;
                for (ClockInResult result : clockInResults) {
                    if (ConstantUtil.ON_WORK.equals(result.getClockInType())) {
                        if (Objects.nonNull(itemRule.getInPoint())) {
                            actualAttendStartTime = Objects.nonNull(result.getClockInTime()) ? (result.getClockInTime().before(itemRule.getInPoint()) ? itemRule.getInPoint() : result.getClockInTime()) : result.getEffectiveTime();
                        }
                    } else if (ConstantUtil.OFF_WORK.equals(result.getClockInType())) {
                        if (Objects.nonNull(itemRule.getOutPoint())) {
                            actualAttendEndTime = Objects.nonNull(result.getClockInTime()) ? (result.getClockInTime().after(itemRule.getOutPoint()) ? itemRule.getOutPoint() : result.getClockInTime()) : result.getEffectiveTime();
                        }
                        restMinute = result.getRestMinute();
                    }
                }
                if (actualAttendStartTime != null && actualAttendEndTime != null) {
                    // 减去休息分钟数
                    BigDecimal minute = DateConvertUtil.dateConvert(actualAttendStartTime, actualAttendEndTime, 1, 2, null).subtract(new BigDecimal(restMinute));
                    BigDecimal hours = DateConvertUtil.minuteConvert(minute, 1, 2, null);
                    outworkResult.setHours(outworkResult.getHours().add(hours));
                    // 获取计薪工时占比
                    BigDecimal payrollProportion;
                    // 是否为划线排班
                    if (schedulesTypeMap.containsKey(-1)) {
                        BigDecimal days = hours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP);
                        outworkResult.setDays(outworkResult.getDays().add(days));
                        payrollProportion = schedulesTypeMap.getOrDefault(-1, MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight();
                    } else {
                        //获取小时数跟天数的占比
                        BigDecimal proportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getLeft();
                        if (proportion.compareTo(BigDecimal.ZERO) == 0) {
                            continue;
                        }
                        outworkResult.setDays(outworkResult.getDays().add(minute.divide(proportion, 2, RoundingMode.HALF_UP)));
                        payrollProportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight();
                    }
                    // 设置实际计薪工时
                    if (payrollProportion.compareTo(BigDecimal.ZERO) == 0) {
                        continue;
                    }
                    // 设置外勤计薪工时
                    outworkResult.setPayrollHours(outworkResult.getPayrollHours().add(minute.multiply(payrollProportion).setScale(2, RoundingMode.HALF_UP)));
                }
            }
        } catch (Exception e) {
            log.error("获取外勤数据时发生异常", e);
            return outworkResult;
        }
        return outworkResult;
    }

    /**
     * 获取旷工数据
     *
     * @param dataList         数据列表
     * @param schedulesTypeMap 排班类型数据
     * @return 旷工数据
     */
    private static ProcessResult processAbsence(List<UserDaySituationData> dataList, Map<Integer, MutablePair<BigDecimal, MutablePair<BigDecimal, BigDecimal>>> schedulesTypeMap) {
        ProcessResult result = new ProcessResult();
        try {
            List<UserDaySituationData> absenceList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.KG);
            for (UserDaySituationData itemRule : absenceList) {
                if (CollUtil.isEmpty(itemRule.getClockInResultList())) {
                    continue;
                }
                if (!isAbsence(itemRule.getClockInResultList())) {
                    continue;
                }
                BigDecimal hours = DateConvertUtil.minuteConvert(new BigDecimal(itemRule.getValidDuration()), 1, 2, null);
                result.setCount(result.getCount() + 1);
                result.setMinutes(result.getMinutes() + itemRule.getValidDuration());
                result.setHours(result.getHours().add(hours));
                // 获取计薪工时占比
                BigDecimal payrollProportion;
                // 是否为划线排班
                if (schedulesTypeMap.containsKey(-1)) {
                    BigDecimal days = hours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP);
                    result.setDays(result.getDays().add(days));
                    payrollProportion = schedulesTypeMap.getOrDefault(-1, MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight();
                } else {
                    //获取小时数跟天数的占比
                    BigDecimal proportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getLeft();
                    if (proportion.compareTo(BigDecimal.ZERO) == 0) {
                        continue;
                    }
                    result.setDays(result.getDays().add(BigDecimal.valueOf(itemRule.getValidDuration()).divide(proportion, 2, RoundingMode.HALF_UP)));
                    payrollProportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight();
                }
                // 设置实际计薪工时
                if (payrollProportion.compareTo(BigDecimal.ZERO) == 0) {
                    continue;
                }
                // 设置旷工计薪工时
                result.setPayrollHours(result.getPayrollHours().add(BigDecimal.valueOf(itemRule.getValidDuration()).multiply(payrollProportion).setScale(2, RoundingMode.HALF_UP)));
            }
        } catch (Exception e) {
            log.error("获取旷工数据时发生异常", e);
            return result;
        }
        return result;
    }

    /**
     * 获取缺卡数据
     *
     * @param attendanceRatio  出勤换算比
     * @param dataList         数据列表
     * @param schedulesTypeMap 排班类型
     * @return 缺卡数据
     */
    private static ProcessResult processAbsenceCard(BigDecimal attendanceRatio, List<UserDaySituationData> dataList, Map<Integer, MutablePair<BigDecimal, MutablePair<BigDecimal, BigDecimal>>> schedulesTypeMap) {
        ProcessResult result = new ProcessResult();
        try {
            List<UserDaySituationData> absenceCardList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.QK);
            for (UserDaySituationData itemRule : absenceCardList) {
                if (CollUtil.isEmpty(itemRule.getClockInResultList())) {
                    continue;
                }
                if (isAbsence(itemRule.getClockInResultList())) {
                    continue;
                }
                List<ClockInResult> clockInResults = getClockInResultFilterList(itemRule.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.QK);
                if (CollUtil.isEmpty(clockInResults)) {
                    continue;
                }
                result.setCount(result.getCount() + clockInResults.size());
                if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(itemRule.getAttendanceType())) {
                    BigDecimal hours = DateConvertUtil.dateConvert(itemRule.getInPoint(), itemRule.getOutPoint(), 2, 2, null);
                    result.setHours(result.getHours().add(hours));
                    result.setDays(result.getDays().add(hours.divide(attendanceRatio, 2, RoundingMode.HALF_UP)));
                } else {
                    BigDecimal hours = DateConvertUtil.minuteConvert(new BigDecimal(itemRule.getValidDuration()), 1, 2, null);
                    result.setHours(result.getHours().add(hours));
                    // 获取计薪工时占比
                    BigDecimal payrollProportion;
                    // 是否为划线排班
                    if (schedulesTypeMap.containsKey(-1)) {
                        BigDecimal days = hours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP);
                        result.setDays(result.getDays().add(days));
                        payrollProportion = schedulesTypeMap.getOrDefault(-1, MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight();
                    } else {
                        //获取小时数跟天数的占比
                        BigDecimal proportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getLeft();
                        if (proportion.compareTo(BigDecimal.ZERO) == 0) {
                            continue;
                        }
                        result.setDays(result.getDays().add(BigDecimal.valueOf(itemRule.getValidDuration()).divide(proportion, 2, RoundingMode.HALF_UP)));
                        payrollProportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight();
                    }
                    // 设置实际计薪工时
                    if (payrollProportion.compareTo(BigDecimal.ZERO) == 0) {
                        continue;
                    }
                    // 设置缺卡计薪工时
                    result.setPayrollHours(result.getPayrollHours().add(BigDecimal.valueOf(itemRule.getValidDuration()).multiply(payrollProportion).setScale(2, RoundingMode.HALF_UP)));
                }
            }
        } catch (Exception e) {
            log.error("获取缺卡数据时发生异常", e);
            return result;
        }
        return result;
    }

    /**
     * 获取补卡数据
     * @param dataList 出勤数据
     * @return 补卡数据
     */
    private static int processMakeUpCard(List<UserDaySituationData> dataList) {
        List<UserDaySituationData> makeUpCardList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.BK);
        int makeUpCardTimes = 0;
        try {
            for (UserDaySituationData item : makeUpCardList) {
                if (CollUtil.isEmpty(item.getClockInResultList())) {
                    continue;
                }
                if (isAbsence(item.getClockInResultList())) {
                    continue;
                }
                List<ClockInResult> clockInResults = getClockInResultFilterList(item.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.BK);
                if (CollUtil.isNotEmpty(clockInResults)) {
                    makeUpCardTimes += clockInResults.size();
                }
            }
        } catch (Exception e) {
            log.error("获取补卡数据时发生异常", e);
            return makeUpCardTimes;
        }
        return makeUpCardTimes;
    }

    /**
     * 获取迟到数据
     * @param dataList 出勤数据
     * @return 迟到数据
     */
    private static ProcessTimeResult processLate(List<UserDaySituationData> dataList) {
        List<UserDaySituationData> lateList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.CD);
        ProcessTimeResult result = new ProcessTimeResult();
        try {
            for (UserDaySituationData item : lateList) {
                if (CollUtil.isEmpty(item.getClockInResultList())) {
                    continue;
                }
                if (isAbsence(item.getClockInResultList())) {
                    continue;
                }
                List<ClockInResult> clockInResults = getClockInResultFilterList(item.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.CD);
                if (CollUtil.isNotEmpty(clockInResults)) {
                    result.setCount(result.getCount() + 1);
                    for (ClockInResult clockInResult : clockInResults) {
                        result.setTime(result.getTime().add(Objects.nonNull(clockInResult.getAbnormalMinute()) ?
                                DateConvertUtil.minuteConvert(new BigDecimal(clockInResult.getAbnormalMinute()), 1, 2, null) : BigDecimal.ZERO));
                    }
                }
            }
        } catch (Exception e) {
            log.error("获取迟到数据时发生异常", e);
            return result;
        }
        return result;
    }

    /**
     * 获取早退数据
     *
     * @param dataList  出勤数据
     * @return 早退数据
     */
    private static ProcessTimeResult processEarlyLeave(List<UserDaySituationData> dataList) {
        List<UserDaySituationData> earlyLeaveList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.ZT);
        ProcessTimeResult result = new ProcessTimeResult();
        try {
            for (UserDaySituationData item : earlyLeaveList) {
                if (CollUtil.isEmpty(item.getClockInResultList())) {
                    continue;
                }
                if (isAbsence(item.getClockInResultList())) {
                    continue;
                }
                List<ClockInResult> clockInResults = getClockInResultFilterList(item.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.ZT);
                if (CollUtil.isNotEmpty(clockInResults)) {
                    result.setCount(result.getCount() + 1);
                    for (ClockInResult clockInResult : clockInResults) {
                        result.setTime(result.getTime().add(Objects.nonNull(clockInResult.getAbnormalMinute()) ? DateConvertUtil.secondConvert(clockInResult.getAbnormalMinute(), 1, 2, null) : BigDecimal.ZERO));
                    }
                }
            }
        } catch (Exception e) {
            log.error("获取早退数据时发生异常", e);
            return result;
        }
        return result;
    }

    /**
     * 获取应出勤\实际出勤数据
     *
     * @param dataList         用户出勤数据
     * @param schedulesTypeMap 排班类型
     * @param statistics       考勤统计
     */
    public static void processShouldAttendData(List<UserDaySituationData> dataList, Map<Integer, MutablePair<BigDecimal, MutablePair<BigDecimal, BigDecimal>>> schedulesTypeMap, AttendanceDayStatistics statistics) {
        List<UserDaySituationData> shouldAttendList = DayStatisticsUtils.getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.YCQ);
        ProcessResult shouldAttendResult = new ProcessResult();
        ProcessResult actualAttendResult = new ProcessResult();
        try {
            // 计算应出勤天数
            if (CollUtil.isNotEmpty(shouldAttendList)) {
                // 获取应出勤天数
                shouldAttendResult.setDays(getShouldDay(shouldAttendList, schedulesTypeMap));
            }
            // 判断当天是否为划线排班
            boolean isUnderlineScheduling = schedulesTypeMap.containsKey(-1);
            // 设置应计薪工时
            shouldAttendResult.setPayrollHours(schedulesTypeMap.values().stream().map(MutablePair::getRight).map(MutablePair::getLeft).reduce(BigDecimal::add).orElse(BigDecimal.ZERO));
            for (UserDaySituationData itemRule : shouldAttendList) {
                shouldAttendResult.setHours(shouldAttendResult.getHours().add(DateConvertUtil.minuteConvert(new BigDecimal(itemRule.getValidDuration()), 1, 2, null).stripTrailingZeros()));
                // 请假不会扣减时段工时换算比，非划线排班
                if (!isUnderlineScheduling && Objects.isNull(itemRule.getPeriodWorkDay())) {
                    continue;
                }
                if (CollUtil.isEmpty(itemRule.getClockInResultList())) {
                    continue;
                }
                if (DayStatisticsUtils.isAbsence(itemRule.getClockInResultList())) {
                    continue;
                }
                // 没有缺卡并且不是旷工才算实际出勤
                List<ClockInResult> clockInResults = DayStatisticsUtils.getClockInResultFilterList(itemRule.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.SJCQ);
                if (CollUtil.isEmpty(clockInResults) || clockInResults.size() != 2) {
                    continue;
                }
                Date actualAttendStartTime = null;
                Date actualAttendEndTime = null;
                Integer restMinute = 0;
                for (ClockInResult result : clockInResults) {
                    if (ConstantUtil.ON_WORK.equals(result.getClockInType())) {
                        if (Objects.nonNull(itemRule.getInPoint())) {
                            actualAttendStartTime = Objects.nonNull(result.getClockInTime()) ? (result.getClockInTime().before(itemRule.getInPoint()) ? itemRule.getInPoint() : result.getClockInTime()) : result.getEffectiveTime();
                        }
                    } else if (ConstantUtil.OFF_WORK.equals(result.getClockInType())) {
                        if (Objects.nonNull(itemRule.getOutPoint())) {
                            actualAttendEndTime = Objects.nonNull(result.getClockInTime()) ? (result.getClockInTime().after(itemRule.getOutPoint()) ? itemRule.getOutPoint() : result.getClockInTime()) : result.getEffectiveTime();
                        }
                        restMinute = result.getRestMinute();
                    }
                }
                if (actualAttendStartTime != null && actualAttendEndTime != null) {
                    // 减去休息分钟数
                    BigDecimal minute = DateConvertUtil.dateConvert(actualAttendStartTime, actualAttendEndTime, 1, 2, null).subtract(new BigDecimal(restMinute));
                    BigDecimal hours = DateConvertUtil.minuteConvert(minute, 1, 2, null);
                    actualAttendResult.setHours(actualAttendResult.getHours().add(hours));
                    // 获取计薪工时占比
                    BigDecimal payrollProportion;
                    // 是否划线排班
                    if (isUnderlineScheduling) {
                        BigDecimal days = hours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP);
                        // 计算实际出勤天数
                        actualAttendResult.setDays(actualAttendResult.getDays().add(days));
                        payrollProportion = schedulesTypeMap.getOrDefault(-1, MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight();
                    } else {
                        // 计算小时数跟天数的占比
                        BigDecimal proportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getLeft();
                        if (proportion.compareTo(BigDecimal.ZERO) == 0) {
                            continue;
                        }
                        // 计算实际出勤天数
                        actualAttendResult.setDays(actualAttendResult.getDays().add(minute.divide(proportion, 2, RoundingMode.HALF_UP)));
                        payrollProportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight();
                    }
                    // 设置实际计薪工时
                    if (payrollProportion.compareTo(BigDecimal.ZERO) == 0) {
                        continue;
                    }
                    actualAttendResult.setPayrollHours(actualAttendResult.getPayrollHours().add(minute.multiply(payrollProportion).setScale(2, RoundingMode.HALF_UP)));
                }
            }
        } catch (Exception e) {
            log.error("获取应出勤||实际出勤数据时发生异常", e);
        }
        statistics.setShouldAttendDays(shouldAttendResult.getDays());
        statistics.setShouldAttendHours(shouldAttendResult.getHours());
        statistics.setShouldAttendPayrollHours(shouldAttendResult.getPayrollHours());
        statistics.setActualAttendDays(actualAttendResult.getDays().compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : actualAttendResult.getDays());
        statistics.setActualAttendHours(actualAttendResult.getHours());
        statistics.setActualAttendPayrollHours(actualAttendResult.getPayrollHours());
    }

    /**
     * 获取有效出勤数据
     *
     * @param dataList         用户出勤数据
     * @param schedulesTypeMap 排班类型
     * @param statistics       考勤统计
     */
    public static void processEffectiveAttendData(List<UserDaySituationData> dataList, Map<Integer, MutablePair<BigDecimal, MutablePair<BigDecimal, BigDecimal>>> schedulesTypeMap, AttendanceDayStatistics statistics) {
        List<UserDaySituationData> shouldAttendList = DayStatisticsUtils.getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.YCQ);
        ProcessResult effectiveAttendResult = new ProcessResult();
        try {
            // 判断当天是否为划线排班
            boolean isUnderlineScheduling = schedulesTypeMap.containsKey(-1);
            for (UserDaySituationData itemRule : shouldAttendList) {
                // 请假不会扣减时段工时换算比，非划线排班
                if (!isUnderlineScheduling && Objects.isNull(itemRule.getPeriodWorkDay())) {
                    continue;
                }
                if (CollUtil.isEmpty(itemRule.getClockInResultList())) {
                    continue;
                }
                if (DayStatisticsUtils.isAbsence(itemRule.getClockInResultList())) {
                    continue;
                }
                // 没有缺卡并且不是旷工才算实际出勤
                List<ClockInResult> clockInResults = DayStatisticsUtils.getClockInResultFilterList(itemRule.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.SJCQ);
                if (CollUtil.isEmpty(clockInResults) || clockInResults.size() != 2) {
                    continue;
                }
                Date effectiveAttendStartTime = null;
                Date effectiveAttendEndTime = null;
                Integer restMinute = 0;
                for (ClockInResult result : clockInResults) {
                    if (ConstantUtil.ON_WORK.equals(result.getClockInType())) {
                        if (Objects.nonNull(itemRule.getInPoint())) {
                            effectiveAttendStartTime = result.getEffectiveTime();
                        }
                    } else if (ConstantUtil.OFF_WORK.equals(result.getClockInType())) {
                        if (Objects.nonNull(itemRule.getOutPoint())) {
                            effectiveAttendEndTime = result.getEffectiveTime();
                        }
                        restMinute = result.getRestMinute();
                    }
                }
                if (effectiveAttendStartTime != null && effectiveAttendEndTime != null) {
                    // 减去休息分钟数
                    BigDecimal minute = DateConvertUtil.dateConvert(effectiveAttendStartTime, effectiveAttendEndTime, 1, 2, null).subtract(new BigDecimal(restMinute));
                    BigDecimal hours = DateConvertUtil.minuteConvert(minute, 1, 2, null);
                    effectiveAttendResult.setHours(effectiveAttendResult.getHours().add(DateConvertUtil.minuteConvert(minute, 1, 2, null)));
                    // 获取计薪工时占比
                    BigDecimal payrollProportion;
                    // 是否划线排班
                    if (isUnderlineScheduling) {
                        BigDecimal days = hours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP);
                        //计算实际出勤天数
                        effectiveAttendResult.setDays(effectiveAttendResult.getDays().add(days));
                        payrollProportion = schedulesTypeMap.getOrDefault(-1, MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight();
                    } else {
                        //计算小时数跟天数的占比
                        BigDecimal proportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getLeft();
                        if (proportion.compareTo(BigDecimal.ZERO) == 0) {
                            continue;
                        }
                        //计算有效出勤天数
                        effectiveAttendResult.setDays(effectiveAttendResult.getDays().add(minute.divide(proportion, 2, RoundingMode.HALF_UP)));
                        payrollProportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight();
                    }
                    // 设置实际计薪工时
                    if (payrollProportion.compareTo(BigDecimal.ZERO) == 0) {
                        continue;
                    }
                    // 设置实际计薪工时
                    effectiveAttendResult.setPayrollHours(effectiveAttendResult.getPayrollHours().add(minute.multiply(payrollProportion).setScale(2, RoundingMode.HALF_UP)));
                }
            }
        } catch (Exception e) {
            log.error("获取有效出勤数据时发生异常", e);
        }
        statistics.setEffectiveAttendDays(effectiveAttendResult.getDays().compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : effectiveAttendResult.getDays());
        statistics.setEffectiveAttendHours(effectiveAttendResult.getHours());
        statistics.setEffectiveAttendPayrollHours(effectiveAttendResult.getPayrollHours());
    }

    /**
     * 获取应出勤天数
     *
     * @param dataList         排班数据
     * @param schedulesTypeMap 排班类型
     * @return 应出勤天数
     */
    private static BigDecimal getShouldDay(List<UserDaySituationData> dataList, Map<Integer, MutablePair<BigDecimal, MutablePair<BigDecimal, BigDecimal>>> schedulesTypeMap) {
        List<UserDaySituationData> dailyRuleList = dataList.stream().filter(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList());
        Map<Integer, List<UserDaySituationData>> classTypeMap = dailyRuleList.stream().filter(itemRule -> Objects.nonNull(itemRule.getSchedulesType())).collect(Collectors.groupingBy(UserDaySituationData::getSchedulesType));
        // 判断当天是否为划线排班
        boolean isUnderlineScheduling = schedulesTypeMap.containsKey(-1);
        BigDecimal shouldHours = BigDecimal.ZERO;
        BigDecimal shouldDay = BigDecimal.ZERO;
        for (Map.Entry<Integer, List<UserDaySituationData>> entry : classTypeMap.entrySet()) {
            // 如果是划线排班，天数需要根据出勤换算比来计算
            if (isUnderlineScheduling) {
                Integer validDuration = entry.getValue().stream().map(UserDaySituationData::getValidDuration).filter(Objects::nonNull).reduce(0, Integer::sum);
                shouldHours = shouldHours.add(DateConvertUtil.minuteConvert(new BigDecimal(validDuration), 1, 2, null));
                BigDecimal days = shouldHours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP).compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : shouldHours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP);
                shouldDay = shouldDay.add(days);
                continue;
            }
            // 请假是否全覆盖 全天班、上午班、下午班
            boolean isHasAllLeave = entry.getValue().stream().allMatch(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode()));
            if (isHasAllLeave) {
                shouldDay = shouldDay.add(entry.getValue().stream().map(UserDaySituationData::getLeaveDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
            } else {
                // 全天班、上午班、下午班班次内是否有请假，有的话，普通班的天数只会会统计一次
                boolean isHasLeave = entry.getValue().stream().anyMatch(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode()));
                if (isHasLeave) {
                    shouldDay = shouldDay.add(entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).findFirst().orElse(BigDecimal.ZERO));
                    entry.getValue().removeIf(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()));
                    if (CollUtil.isNotEmpty(entry.getValue())) {
                        shouldDay = shouldDay.add(entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
                    }
                } else {
                    // 公休天数+普班天数
                    shouldDay = shouldDay.add(entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
                }
            }
        }
        return shouldDay.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : shouldDay;
    }

    /**
     * 统计项处理
     *
     * @param attendanceRatio  出勤换算比
     * @param dataList         数据
     * @param schedulesTypeMap 排班类型
     * @param statistics       统计
     */
    public static void processStatisticalItems(BigDecimal attendanceRatio, List<UserDaySituationData> dataList, Map<Integer, MutablePair<BigDecimal, MutablePair<BigDecimal, BigDecimal>>> schedulesTypeMap, AttendanceDayStatistics statistics) {
        // 迟到处理
        ProcessTimeResult lateResult = processLate(dataList);
        statistics.setLateTimes(lateResult.getCount());
        statistics.setLateMinutes(lateResult.getTime());
        // 早退处理
        ProcessTimeResult earlyLeaveResult = processEarlyLeave(dataList);
        statistics.setEarlyLeaveTimes(earlyLeaveResult.getCount());
        statistics.setEarlyLeaveMinutes(earlyLeaveResult.getTime());
        // 缺卡处理
        ProcessResult absenceCardResult = processAbsenceCard(attendanceRatio, dataList, schedulesTypeMap);
        statistics.setAbsenceCardTimes(absenceCardResult.getCount());
        statistics.setAbsenceCardHours(absenceCardResult.getHours());
        statistics.setAbsenceCardPayrollHours(absenceCardResult.getPayrollHours());
        statistics.setAbsenceCardDays(absenceCardResult.getDays().compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : absenceCardResult.getDays());
        // 旷工处理
        ProcessResult absenceResult = processAbsence(dataList, schedulesTypeMap);
        statistics.setAbsenceTimes(absenceResult.getCount());
        statistics.setAbsenceHours(absenceResult.getHours());
        statistics.setAbsencePayrollHours(absenceResult.getPayrollHours());
        statistics.setAbsenceDays(absenceResult.getDays().compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : absenceResult.getDays());
        // 补卡处理
        statistics.setMakeUpCardTimes(processMakeUpCard(dataList));
        // 外勤打卡处理
        ProcessResult outworkResult = processOutworkClock(dataList, schedulesTypeMap);
        statistics.setOutworkTimes(outworkResult.getCount());
        statistics.setOutworkHours(outworkResult.getHours());
        statistics.setOutworkPayrollHours(outworkResult.getPayrollHours());
        statistics.setOutworkDays(outworkResult.getDays().compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : outworkResult.getDays());
        // 加班处理
        Map<OvertimeType, ProcessResult> overtimeResultMap = processOvertime(attendanceRatio, dataList);
        // 总加班汇总
        statistics.setOvertimeTimes(overtimeResultMap.values().stream().mapToInt(ProcessResult::getCount).sum());
        statistics.setOvertimeHours(overtimeResultMap.values().stream().map(ProcessResult::getHours).reduce(BigDecimal.ZERO, BigDecimal::add));
        statistics.setOvertimeDays(overtimeResultMap.values().stream().map(ProcessResult::getDays).reduce(BigDecimal.ZERO, BigDecimal::add));
        // 工作日加班处理
        statistics.setWeekdayOvertimeTimes(overtimeResultMap.get(OvertimeType.WORK_DAY).getCount());
        statistics.setWeekdayOvertimeHours(overtimeResultMap.get(OvertimeType.WORK_DAY).getHours());
        statistics.setWeekdayOvertimeDays(overtimeResultMap.get(OvertimeType.WORK_DAY).getDays());
        // 节假日加班处理
        statistics.setHolidaysOvertimeTimes(overtimeResultMap.get(OvertimeType.HOLIDAY).getCount());
        statistics.setHolidaysOvertimeHours(overtimeResultMap.get(OvertimeType.HOLIDAY).getHours());
        statistics.setHolidaysOvertimeDays(overtimeResultMap.get(OvertimeType.HOLIDAY).getDays());
        // 公休日加班处理
        statistics.setPublicHolidaysOvertimeTimes(overtimeResultMap.get(OvertimeType.REST_DAY).getCount());
        statistics.setPublicHolidaysOvertimeHours(overtimeResultMap.get(OvertimeType.REST_DAY).getHours());
        statistics.setPublicHolidaysOvertimeDays(overtimeResultMap.get(OvertimeType.REST_DAY).getDays());
        // 公休处理
        ProcessResult publicHolidaysResult = processPublicHolidays(attendanceRatio, dataList);
        statistics.setPublicHolidaysHours(publicHolidaysResult.getHours());
        statistics.setPublicHolidaysPayrollHours(publicHolidaysResult.getPayrollHours());
        statistics.setPublicHolidaysDays(publicHolidaysResult.getDays());
    }

    /**
     * 公休处理
     *
     * @param attendanceRatio  出勤换算比
     * @param dataList         数据
     * @return 公休处理结果
     */
    private static ProcessResult processPublicHolidays(BigDecimal attendanceRatio, List<UserDaySituationData> dataList) {
        ProcessResult result = new ProcessResult();
        try {
            List<UserDaySituationData> publicHolidaysList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.XX);
            for (UserDaySituationData daySituationData : publicHolidaysList) {
                BigDecimal hours, payrollHours, days;
                if (Objects.isNull(daySituationData.getSchedulesType())) {
                    continue;
                }
                if (daySituationData.getSchedulesType().equals(0)) {
                    hours = attendanceRatio;
                    payrollHours = hours;
                    days = BigDecimal.ONE;
                } else {
                    hours = DateConvertUtil.minuteConvert(new BigDecimal(daySituationData.getValidDuration()), 1, 2, null);
                    payrollHours = daySituationData.getPayrollHours();
                    days = Objects.isNull(daySituationData.getPeriodWorkDay()) ? BigDecimal.ZERO : daySituationData.getPeriodWorkDay();
                }
                result.setHours(result.getHours().add(hours));
                result.setPayrollHours(result.getPayrollHours().add(payrollHours));
                result.setDays(result.getDays().add(days));
            }
        } catch (Exception e) {
            log.error("公休处理数据时发生异常", e);
            return result;
        }
        return result;
    }

    /**
     * 获取出差外出时长
     *
     * @param busOrOutApproveVoList 出差外出列表
     * @param dayStaVo              统计结果
     * @param attendanceRatio       考勤比例
     */
    public static void processBusOrOutData(List<OutOrBusApproveVo> busOrOutApproveVoList, AttendanceDayStatistics dayStaVo, BigDecimal attendanceRatio) {
        if (CollUtil.isEmpty(busOrOutApproveVoList)) {
            return;
        }
        dayStaVo.setBusHours(BigDecimal.ZERO);
        dayStaVo.setBusDays(BigDecimal.ZERO);
        dayStaVo.setOutHours(BigDecimal.ZERO);
        dayStaVo.setOutDays(BigDecimal.ZERO);
        // 出差
        List<OutOrBusApproveVo> busApproveVoList = busOrOutApproveVoList.stream().filter(item -> item.getType().equals(2)).collect(Collectors.toList());
        Date day = dayStaVo.getDate();
        if (CollUtil.isNotEmpty(busApproveVoList)) {
            dayStaVo.setBusBatchNumber(busApproveVoList.stream().map(OutOrBusApproveVo::getId).distinct().collect(Collectors.joining(",")));
            busApproveVoList.forEach(item -> {
                BigDecimal hoursForDate = item.getDurationForDate(day, Boolean.FALSE);
                dayStaVo.setBusDays(dayStaVo.getOutDays().add(hoursForDate.compareTo(attendanceRatio) >= 0 ? BigDecimal.ONE : hoursForDate.divide(attendanceRatio, 2, RoundingMode.HALF_UP)));
                dayStaVo.setBusHours(dayStaVo.getOutHours().add(hoursForDate.compareTo(attendanceRatio) >= 0 ? attendanceRatio : hoursForDate));
            });
        }
        // 外出（要按照天维度分组，因为一天会存在多次外出）
        List<OutOrBusApproveVo> outApproveVoList = busOrOutApproveVoList.stream().filter(item -> item.getType().equals(1)).collect(Collectors.toList());
        // 将外出申请按照日期范围展开，每一天都生成一条记录
        Map<Date, List<OutOrBusApproveVo>> outApproveByDayMap = new HashMap<>();
        for (OutOrBusApproveVo approveVo : outApproveVoList) {
            Date startDate = DateUtil.beginOfDay(approveVo.getStartTime());
            Date endDate = DateUtil.endOfDay(approveVo.getEndTime());
            // 遍历申请时间范围内的每一天
            Date currentDate = startDate;
            while (!currentDate.after(endDate)) {
                Date dayKey = DateUtil.beginOfDay(currentDate);
                outApproveByDayMap.computeIfAbsent(dayKey, k -> new ArrayList<>()).add(approveVo);
                currentDate = DateUtil.offsetDay(currentDate, 1);
            }
        }
        // 获取当前统计日期对应的外出申请
        List<OutOrBusApproveVo> todayOutApproveList = outApproveByDayMap.getOrDefault(day, new ArrayList<>());
        if (CollUtil.isNotEmpty(todayOutApproveList)) {
            List<BatchNumberResult> batchNumberResultList = todayOutApproveList.stream().map(item -> {
                BigDecimal hoursForDate = item.getDurationForDate(day, Boolean.FALSE);
                return BatchNumberResult.builder().day(day).batchNumberId(item.getId()).days(hoursForDate.compareTo(attendanceRatio) >= 0 ? BigDecimal.ONE : hoursForDate.divide(attendanceRatio, 2, RoundingMode.HALF_UP)).hours(hoursForDate.compareTo(attendanceRatio) >= 0 ? attendanceRatio : hoursForDate).build();
            }).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(batchNumberResultList)) {
                dayStaVo.setOutBatchNumber(JSONObject.toJSONString(batchNumberResultList));
            }
            // 计算当天的外出时长
            BigDecimal hoursForDate = todayOutApproveList.stream().map(item -> item.getDurationForDate(day, Boolean.FALSE)).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
            dayStaVo.setOutDays(dayStaVo.getOutDays().add(hoursForDate.compareTo(attendanceRatio) >= 0 ? BigDecimal.ONE : hoursForDate.divide(attendanceRatio, 2, RoundingMode.HALF_UP)));
            dayStaVo.setOutHours(dayStaVo.getOutHours().add(hoursForDate.compareTo(attendanceRatio) >= 0 ? attendanceRatio : hoursForDate));
        }
    }

    /**
     * 通用数据汇总方法 - 对指定字段求和
     *
     * @param dataList 数据列表
     * @param valueExtractor 值提取器
     * @param <T> 数据类型
     * @param <V> 值类型（需为Number类型）
     * @return 所有数据的值求和结果
     */
    public static <T, V extends Number> BigDecimal calculateSum(List<T> dataList, Function<T, V> valueExtractor) {
        if (CollUtil.isEmpty(dataList)) {
            return BigDecimal.ZERO;
        }
        return dataList.stream().filter(Objects::nonNull).map(item -> {
            V value = valueExtractor.apply(item);
            if (value == null) {
                return BigDecimal.ZERO;
            }
            if (value instanceof BigDecimal) {
                return (BigDecimal) value;
            } else {
                return new BigDecimal(value.toString());
            }
        }).reduce(BigDecimal.ZERO, BigDecimal::add).stripTrailingZeros();
    }

    /**
     * 计算两数相减，保留两位小数
     *
     * @param minuend    被减数
     * @param subtrahend 减数
     * @return 结果，保留两位小数
     */
    public static BigDecimal calculateSubtract(BigDecimal minuend, BigDecimal subtrahend) {
        if (minuend == null || subtrahend == null) {
            return null;
        }
        BigDecimal subtract = minuend.subtract(subtrahend);
        if (subtract.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        return subtract.setScale(4, RoundingMode.HALF_UP).stripTrailingZeros();
    }

    /**
     * 通用数据求和方法 - 对Integer类型字段求和
     *
     * @param dataList 数据列表
     * @param valueExtractor 值提取器
     * @param <T> 数据类型
     * @return 所有数据的值求和结果
     */
    public static <T> Integer calculateSumToInt(List<T> dataList, Function<T, Integer> valueExtractor) {
        if (CollUtil.isEmpty(dataList)) {
            return 0;
        }
        return dataList.stream().filter(Objects::nonNull).map(valueExtractor).filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
    }

    /**
     * 获取指定日期的月份
     *
     * @param date 日期
     * @return 月份
     */
    public static String getMonthFromDate(Date date) {
        YearMonth yearMonth = YearMonth.from(date.toInstant().atZone(java.time.ZoneId.systemDefault()));
        return yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"));
    }

    /**
     *
     * @param dto 请求参数
     * @param userStaMap 用户统计数据
     * @return 用户统计数据
     */
    public static Map<String, SalaryAttendanceSupportVo> initSalaryUserData(SalaryAttendanceSupportDto dto, Map<String, SalaryAttendanceSupportVo> userStaMap) {
        return dto.getUserIds().stream().map(item -> {
            if (CollUtil.isEmpty(userStaMap)) {
                SalaryAttendanceSupportVo supportVo = new SalaryAttendanceSupportVo();
                supportVo.setUserId(item);
                return supportVo;
            }
            if (userStaMap.containsKey(item)) {
                return userStaMap.get(item);
            }
            SalaryAttendanceSupportVo supportVo = new SalaryAttendanceSupportVo();
            supportVo.setUserId(item);
            return supportVo;
        }).collect(Collectors.toMap(SalaryAttendanceSupportVo::getUserId, item -> item));
    }

    /**
     * 获取指定日期范围内的所有日期集合
     *
     * @param startDate 开始日期
     * @param endDate   结束日期
     * @return 日期集合
     */
    public static List<Date> getDatesBetween(Date startDateFilter, Date endDateFilter, Date startDate, Date endDate) {
        List<Date> dates = new ArrayList<>();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(startDate);
        while (!calendar.getTime().after(endDate)) {
            // 节假日日期要存在于我周期范围内才算
            if (calendar.getTime().compareTo(startDateFilter) >= 0 && calendar.getTime().compareTo(endDateFilter) <= 0) {
                dates.add(calendar.getTime());
            }
            calendar.add(Calendar.DAY_OF_MONTH, 1);
        }
        return dates;
    }

    /**
     * 获取用户节假日天数
     * @param startDate 开始时间
     * @param endDate   结束时间
     * @param autoSchedulingDaysMap 用户的所有节假日
     * @return 用户自动排班天数
     */
    public static Map<String, Set<Date>> getUserHolidayDaysStaMapMap(Date startDate, Date endDate, Map<String, List<AttendanceFestivalRules>> autoSchedulingDaysMap) {
        Map<String, Set<Date>> daysStaMap = new HashMap<>();
        autoSchedulingDaysMap.forEach((userId, autoSchedulingDaysList) -> {
            Set<Date> dateSet = new HashSet<>();
            // 排除掉不是自动排版的数据
            List<AttendanceFestivalRules> collect = autoSchedulingDaysList.stream().filter(item -> item.getAutoScheduling() != 1).collect(Collectors.toList());
            // 获取每个节假日Day天数集合,每个节假日是有范围的，要匹配我的
            if (CollUtil.isNotEmpty(collect)) {
                collect.forEach(festival -> {
                    dateSet.addAll(parseFestivalDates(festival.getFestivalDate(), startDate, endDate));
                });
            }
            if (CollUtil.isNotEmpty(dateSet)) {
                daysStaMap.put(userId, dateSet);
            }
        });
        return daysStaMap;
    }

    /**
     * 获取用户自动排班天数
     * @param startDate 开始时间
     * @param endDate   结束时间
     * @param autoSchedulingDaysMap 用户的所有节假日
     * @return 用户自动排班天数
     */
    public static Map<String, Integer> getUserAutoSchedulingDaysStaMap(Date startDate, Date endDate, Map<String, List<AttendanceFestivalRules>> autoSchedulingDaysMap) {
        Map<String, Integer> autoSchedulingDaysSta = new HashMap<>();
        autoSchedulingDaysMap.forEach((userId, autoSchedulingDaysList) -> {
            Set<Date> dateSet = new HashSet<>();
            // 排除掉不是自动排版的数据
            List<AttendanceFestivalRules> collect = autoSchedulingDaysList.stream().filter(item -> item.getAutoScheduling() != 1).collect(Collectors.toList());
            // 获取每个节假日Day天数集合,每个节假日是有范围的，要匹配我的
            if (CollUtil.isNotEmpty(collect)) {
                collect.forEach(festival -> {
                    dateSet.addAll(parseFestivalDates(festival.getFestivalDate(), startDate, endDate));
                });
            }
            if (CollUtil.isNotEmpty(dateSet)) {
                autoSchedulingDaysSta.put(userId, dateSet.size());
            }
        });
        return autoSchedulingDaysSta;
    }

    /**
     * 从 festivalDate 字段解析节假日日期集合
     * 字段值格式："2026-03-25,2026-03-26"
     *
     * @param festivalDate 节日日期字符串，格式："yyyy-MM-dd,yyyy-MM-dd"
     * @param startDate    查询开始日期
     * @param endDate      查询结束日期
     * @return 符合条件的日期集合
     */
    public static Set<Date> parseFestivalDates(String festivalDate, Date startDate, Date endDate) {
        Set<Date> dateSet = new HashSet<>();
        if (StrUtil.isNotBlank(festivalDate)) {
            String[] dateStrings = festivalDate.split(",");
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            for (String dateStr : dateStrings) {
                try {
                    Date holidayDate = sdf.parse(dateStr.trim());
                    // 判断日期是否在查询范围内
                    if (holidayDate.compareTo(startDate) >= 0 && holidayDate.compareTo(endDate) <= 0) {
                        dateSet.add(holidayDate);
                    }
                } catch (Exception e) {
                    log.error("解析节日日期失败：{}", dateStr, e);
                }
            }
        }
        return dateSet;
    }

    /**
     * 获取用户法定节假日天数
     * @param startDate 开始时间
     * @param endDate   结束时间
     * @param autoSchedulingDaysMap 用户的所有节假日
     * @return 用户自动排班天数
     */
    public static Map<String, Map<String, HolidaysTypeJsonData1>> getLegalHolidaysStaMap(Date startDate, Date endDate, Map<String, List<AttendanceFestivalRules>> autoSchedulingDaysMap) {
        Map<String, Map<String, HolidaysTypeJsonData1>> legalHolidaysStaMap = new HashMap<>();
        autoSchedulingDaysMap.forEach((userId, autoSchedulingDaysList) -> {
            // 获取法定节假日名称
            List<String> legalHolidayNames = Arrays.asList("元旦节", "春节", "清明节", "劳动节", "端午节", "中秋节", "国庆节");
            autoSchedulingDaysList = autoSchedulingDaysList.stream().filter(item -> legalHolidayNames.contains(item.getName())).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(autoSchedulingDaysList)) {
                return;
            }
            Map<String, AttendanceFestivalRules> festivalRulesMap = autoSchedulingDaysList.stream().collect(Collectors.toMap(AttendanceFestivalRules::getId, item -> item));
            Map<String, HolidaysTypeJsonData1> map = new HashMap<>();
            festivalRulesMap.forEach((field, item) -> {
                HolidaysTypeJsonData1 jsonData = new HolidaysTypeJsonData1();
                jsonData.setField(field);
                jsonData.setFieldName(item.getName());
                Set<Date> dateList = parseFestivalDates(item.getFestivalDate(), startDate, endDate);
                if(CollUtil.isEmpty(dateList)){
                    return;
                }
                jsonData.setLeaveDays(BigDecimal.valueOf(dateList.size()));
                map.put(field, jsonData);
            });
            if (CollUtil.isEmpty(map)) {
                return;
            }
            legalHolidaysStaMap.put(userId, map);
        });
        return legalHolidaysStaMap;
    }


    /**
     * 构建请假天数数据
     *
     * @param leaveVos      请假数据
     * @param leaveTypeMap  请假类型数据
     * @param supportVoList 日统计数据
     * @return 请假数据
     */
    public static Map<String, Map<String, LeaveTypeJsonData>> buildLeaveMap(List<LeaveApprovalModel> leaveVos, Map<String, AttendanceLeaveType> leaveTypeMap, List<SalaryAttendanceSupportQuery> supportVoList) {
        if (CollUtil.isEmpty(leaveVos)) {
            return new HashMap<>();
        }
        Map<String, List<SalaryAttendanceSupportQuery>> leaveMap = supportVoList.stream().collect(Collectors.groupingBy(SalaryAttendanceSupportQuery::getUserId));
        return leaveVos.stream().collect(Collectors.groupingBy(LeaveApprovalModel::getUserId, Collectors.groupingBy(LeaveApprovalModel::getLeaveTypeId, Collectors.collectingAndThen(Collectors.toList(), list -> {
            if (CollUtil.isEmpty(list)) {
                return null;
            }
            LeaveApprovalModel infoModel = list.get(0);
            String leaveTypeId = infoModel.getLeaveTypeId();
            String userId = infoModel.getUserId();
            List<LinkedHashMap<String, Object>> customLeaveListFlat = leaveMap.get(userId).stream().filter(item -> CollUtil.isNotEmpty(item.getCustomLeaveList())).flatMap(item -> item.getCustomLeaveList().stream()).collect(Collectors.toList());
            List<LeaveTypeJsonData> dayCustomLeaveList = DayStatisticsUtils.getDayCustomLeaveList(customLeaveListFlat).stream().filter(item -> item.getField().equals(leaveTypeId)).collect(Collectors.toList());
            Map<String, LeaveTypeJsonData> leaveTypeJsonDataMap = CollUtil.isNotEmpty(dayCustomLeaveList) ? dayCustomLeaveList.stream().collect(Collectors.toMap(LeaveTypeJsonData::getField, item -> item, (existing, replacement) -> {
                existing.setLeaveDays(existing.getLeaveDays().add(replacement.getLeaveDays()));
                existing.setLeaveHours(existing.getLeaveHours().add(replacement.getLeaveHours()));
                existing.setLeavePayrollHours(existing.getLeavePayrollHours().add(replacement.getLeavePayrollHours()));
                if (replacement.getApplyList() != null && !replacement.getApplyList().isEmpty()) {
                    existing.getApplyList().addAll(replacement.getApplyList());
                }
                return existing;
            })) : new HashMap<>();
            LeaveTypeJsonData jsonData = new LeaveTypeJsonData();
            jsonData.setField(leaveTypeId);
            jsonData.setFieldName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : "");
            jsonData.setHeadName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : "");
            LeaveTypeJsonData leaveTypeJsonData = leaveTypeJsonDataMap.getOrDefault(leaveTypeId, new LeaveTypeJsonData());
            jsonData.setLeaveDays(leaveTypeJsonData.getLeaveDays());
            jsonData.setLeaveHours(leaveTypeJsonData.getLeaveHours());
            jsonData.setLeavePayrollHours(leaveTypeJsonData.getLeavePayrollHours());
            jsonData.setApplyList(leaveTypeJsonData.getApplyList());
            return jsonData;
        })))).entrySet().stream().filter(e -> CollUtil.isNotEmpty(e.getValue())).collect(Collectors.toMap(Map.Entry::getKey, entry ->
                entry.getValue().entrySet().stream().filter(inner -> inner.getValue() != null).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
    }

    /**
     * 构建请假余额数据
     *
     * @param balanceInfoModels 请假余额数据
     * @param leaveTypeMap     请假类型数据
     * @param ratioMap          比例数据
     * @return 节假日数据
     */
    public static Map<String, Map<String, LeaveTypeJsonData>> buildLeaveBalanceMap(List<LeaveBalanceInfoModel> balanceInfoModels, Map<String, AttendanceLeaveType> leaveTypeMap, Map<String, BigDecimal> ratioMap) {
        if (CollUtil.isEmpty(balanceInfoModels)) {
            return new HashMap<>();
        }
        return balanceInfoModels.stream().collect(Collectors.groupingBy(LeaveBalanceInfoModel::getUserId, Collectors.groupingBy(LeaveBalanceInfoModel::getLeaveTypeId, Collectors.collectingAndThen(Collectors.toList(), list -> {
            if (CollUtil.isEmpty(list)) {
                return null;
            }
            LeaveBalanceInfoModel infoModel = list.get(0);
            if (!leaveTypeMap.containsKey(infoModel.getLeaveTypeId())) {
                return null;
            }
            String userId = infoModel.getUserId();
            String leaveTypeId = infoModel.getLeaveTypeId();
            LeaveTypeJsonData jsonData = new LeaveTypeJsonData();
            jsonData.setField(leaveTypeId);
            jsonData.setFieldName(leaveTypeMap.get(infoModel.getLeaveTypeId()).getName());
            jsonData.setHeadName(leaveTypeMap.get(infoModel.getLeaveTypeId()).getName());
            BigDecimal days = list.stream().map(LeaveBalanceInfoModel::getValue).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
            BigDecimal ratio = ratioMap.getOrDefault(userId, BigDecimal.ZERO);
            jsonData.setLeaveDays(days.stripTrailingZeros());
            jsonData.setLeaveHours(days.multiply(ratio).stripTrailingZeros());
            return jsonData;
        })))).entrySet().stream().filter(e -> CollUtil.isNotEmpty(e.getValue())).collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().entrySet().stream().filter(inner -> inner.getValue() != null).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
    }

    /**
     * 构建请假抵扣天数数据
     *
     * @param leaveVos     请假数据
     * @param leaveTypeMap 请假类型数据
     * @param startDate    开始时间
     * @param endDate      结束时间
     * @return 请假数据
     */
    public static Map<String, Map<String, LeaveTypeJsonData>> buildLeaveDeductMap(List<LeaveApprovalModel> leaveVos, Map<String, AttendanceLeaveType> leaveTypeMap, Date startDate, Date endDate) {
        if (CollUtil.isEmpty(leaveVos)) {
            return new HashMap<>();
        }
        return leaveVos.stream().collect(Collectors.groupingBy(LeaveApprovalModel::getUserId, Collectors.groupingBy(LeaveApprovalModel::getLeaveTypeId, Collectors.collectingAndThen(Collectors.toList(), list -> {
            if (CollUtil.isEmpty(list)) {
                return null;
            }
            List<LeaveDeductDetailVo> jsonDetailVos = list.stream().filter(Objects::nonNull).filter(item -> StringUtil.isNotEmpty(item.getBalanceJson())).map(item -> {
                LeaveConsumptionDetailVo jsonToBean = JsonUtil.getJsonToBean(item.getBalanceJson(), LeaveConsumptionDetailVo.class);
                LeaveDeductDetailVo jsonDetailVo = new LeaveDeductDetailVo();
                jsonDetailVo.setStartTime(item.getStartTime());
                jsonDetailVo.setEndTime(item.getEndTime());
                jsonDetailVo.setDeduction(jsonToBean.getRetirementLeave().add(jsonToBean.getInputTypeBalance()));
                jsonDetailVo.setAttendanceRatio(jsonToBean.getAttendanceRatio());
                return jsonDetailVo;
            }).collect(Collectors.toList());
            LeaveApprovalModel infoModel = list.get(0);
            String leaveTypeId = infoModel.getLeaveTypeId();
            LeaveTypeJsonData jsonData = new LeaveTypeJsonData();
            jsonData.setField(leaveTypeId);
            jsonData.setFieldName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : "");
            jsonData.setHeadName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : "");
            final BigDecimal[] days = {BigDecimal.ZERO};
            final BigDecimal[] hours = {BigDecimal.ZERO};
            jsonDetailVos.stream().filter(Objects::nonNull).forEach(vo -> {
                Date leaveStart = vo.getStartTime();
                Date leaveEnd = vo.getEndTime();
                // 计算交集时间段
                Date intersectionStart = leaveStart.after(startDate) ? leaveStart : startDate;
                Date intersectionEnd = leaveEnd.before(endDate) ? leaveEnd : endDate;
                // 如果有交集，则返回未抵扣天数，否则返回0
                if (intersectionStart.before(intersectionEnd)) {
                    days[0] = days[0].add(vo.getDeduction() != null ? vo.getDeduction() : BigDecimal.ZERO);
                    hours[0] = hours[0].add(vo.getDeduction() != null ? vo.getDeduction().multiply(vo.getAttendanceRatio()) : BigDecimal.ZERO);
                }
            });
            if (days[0].compareTo(BigDecimal.ZERO) == 0) {
                return null;
            }
            jsonData.setLeaveDays(days[0]);
            jsonData.setLeaveHours(hours[0]);
            return jsonData;
        })))).entrySet().stream().filter(e -> CollUtil.isNotEmpty(e.getValue())).collect(Collectors.toMap(Map.Entry::getKey,
                entry -> entry.getValue().entrySet().stream().filter(inner -> inner.getValue() != null)
                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
    }

    /**
     * 构建请假扣薪天数数据
     *
     * @param leaveVos     请假数据
     * @param leaveTypeMap 请假类型数据
     * @param startDate    开始时间
     * @param endDate      结束时间
     * @return 请假数据
     */
    public static Map<String, Map<String, LeaveTypeJsonData>> buildLeaveDeductWagesMap(List<LeaveApprovalModel> leaveVos, Map<String, AttendanceLeaveType> leaveTypeMap, Date startDate, Date endDate) {
        if (CollUtil.isEmpty(leaveVos)) {
            return new HashMap<>();
        }
        return leaveVos.stream().collect(Collectors.groupingBy(LeaveApprovalModel::getUserId, Collectors.groupingBy(LeaveApprovalModel::getLeaveTypeId, Collectors.collectingAndThen(Collectors.toList(), list -> {
            if (CollUtil.isEmpty(list)) {
                return null;
            }
            List<LeaveDeductWagesJsonDetailVo> jsonDetailVos = list.stream().filter(Objects::nonNull).filter(item -> StringUtil.isNotEmpty(item.getBalanceJson())).map(item -> {
                LeaveConsumptionDetailVo jsonToBean = JsonUtil.getJsonToBean(item.getBalanceJson(), LeaveConsumptionDetailVo.class);
                LeaveDeductWagesJsonDetailVo jsonDetailVo = new LeaveDeductWagesJsonDetailVo();
                jsonDetailVo.setStartTime(item.getStartTime());
                jsonDetailVo.setEndTime(item.getEndTime());
                jsonDetailVo.setUnconsumedBalance(jsonToBean.getUnconsumedBalance());
                jsonDetailVo.setAttendanceRatio(jsonToBean.getAttendanceRatio());
                return jsonDetailVo;
            }).collect(Collectors.toList());
            LeaveApprovalModel infoModel = list.get(0);
            String leaveTypeId = infoModel.getLeaveTypeId();
            LeaveTypeJsonData jsonData = new LeaveTypeJsonData();
            jsonData.setField(leaveTypeId);
            jsonData.setFieldName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : "");
            jsonData.setHeadName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : "");
            final BigDecimal[] days = {BigDecimal.ZERO};
            final BigDecimal[] hours = {BigDecimal.ZERO};
            jsonDetailVos.stream().filter(Objects::nonNull).forEach(vo -> {
                Date leaveStart = vo.getStartTime();
                Date leaveEnd = vo.getEndTime();
                // 计算交集时间段
                Date intersectionStart = leaveStart.after(startDate) ? leaveStart : startDate;
                Date intersectionEnd = leaveEnd.before(endDate) ? leaveEnd : endDate;
                // 如果有交集，则返回未抵扣天数，否则返回0
                if (intersectionStart.before(intersectionEnd)) {
                    days[0] = days[0].add(vo.getUnconsumedBalance() != null ? vo.getUnconsumedBalance() : BigDecimal.ZERO);
                    hours[0] = hours[0].add(vo.getUnconsumedBalance() != null ? vo.getUnconsumedBalance().multiply(vo.getAttendanceRatio()) : BigDecimal.ZERO);
                }
            });
            if (days[0].compareTo(BigDecimal.ZERO) == 0) {
                return null;
            }
            jsonData.setLeaveDays(days[0]);
            jsonData.setLeaveHours(hours[0]);
            return jsonData;
        })))).entrySet().stream().filter(e -> CollUtil.isNotEmpty(e.getValue())).collect(Collectors.toMap(Map.Entry::getKey,
                entry -> entry.getValue().entrySet().stream().filter(inner -> inner.getValue() != null)
                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
    }

    /**
     * 构建节假日数据
     *
     * @param startDate 开始时间
     * @param endDate   结束时间
     * @param autoSchedulingDaysMap 节假日数据
     * @param ratioMap              比例数据
     * @return 节假日数据
     */
    public static Map<String, Map<String, HolidaysTypeJsonData1>> buildHolidayTypeMap(Date startDate, Date endDate, Map<String, List<AttendanceFestivalRules>> autoSchedulingDaysMap, Map<String, BigDecimal> ratioMap) {
        if (CollUtil.isEmpty(autoSchedulingDaysMap)) {
            return new HashMap<>();
        }
        Map<String, Map<String, HolidaysTypeJsonData1>> result = new HashMap<>();
        autoSchedulingDaysMap.forEach((userId, list) -> {
            Map<String, HolidaysTypeJsonData1> map = new HashMap<>();
            if (CollUtil.isEmpty(list)) {
                return;
            }
            Map<String, AttendanceFestivalRules> festivalRulesMap = list.stream().collect(Collectors.toMap(AttendanceFestivalRules::getId, item -> item));
            festivalRulesMap.forEach((field, item) -> {
                HolidaysTypeJsonData1 jsonData = new HolidaysTypeJsonData1();
                jsonData.setField(field);
                jsonData.setFieldName(item.getName());
                Set<Date> dateList = parseFestivalDates(item.getFestivalDate(), startDate, endDate);
                if(CollUtil.isEmpty(dateList)){
                    return;
                }
                jsonData.setLeaveDays(BigDecimal.valueOf(dateList.size()));
                jsonData.setLeaveHours(BigDecimal.valueOf(dateList.size()).multiply(ratioMap.getOrDefault(userId, BigDecimal.ZERO)).stripTrailingZeros());
                map.put(field, jsonData);
            });
            if (CollUtil.isEmpty(map)) {
                return;
            }
            result.put(userId, map);
        });
        return result;
    }

    /**
     * 构建加班节假日数据
     *
     * @param overtimeHolidaysInfoList 加班节假日换休数据
     * @param overtimeSalaryJson       加班节假日换薪数据
     * @param ratioMap                 比例数据
     * @return 加班节假日数据
     */
    public static Map<String, Map<String, HolidaysTypeJsonData1>> buildOvertimeHolidaysMap(List<OvertimeHolidaysInfoModel> overtimeHolidaysInfoList, List<OvertimeSalaryHoursJsonVo> overtimeSalaryJson, Map<String, BigDecimal> ratioMap) {
        if (CollUtil.isEmpty(overtimeHolidaysInfoList) && CollUtil.isEmpty(overtimeSalaryJson)) {
            return new HashMap<>();
        }
        // 同一天会存在多个节假日 需要拆分出来
        List<OvertimeHolidaysInfoModel> splitHolidayList = CollUtil.isNotEmpty(overtimeHolidaysInfoList) ? overtimeHolidaysInfoList.stream().flatMap(info -> {
            if (StringUtil.isNotEmpty(info.getHolidayNameList())) {
                return Arrays.stream(info.getHolidayNameList().split("@")).map(holidayName -> {
                    OvertimeHolidaysInfoModel newInfo = new OvertimeHolidaysInfoModel();
                    BeanUtils.copyProperties(info, newInfo);
                    newInfo.setHolidayNameList(holidayName);
                    return newInfo;
                });
            } else {
                return Stream.of(info);
            }
        }).collect(Collectors.toList()) : new ArrayList<>();
        if (CollUtil.isNotEmpty(overtimeSalaryJson)) {
            splitHolidayList.addAll(overtimeSalaryJson.stream().flatMap(info -> {
                OvertimeHolidaysInfoModel newInfo = new OvertimeHolidaysInfoModel();
                newInfo.setUserId(info.getUserId());
                newInfo.setHolidaysOvertimeDays(info.getHolidaysOvertimeSalaryDays());
                newInfo.setHolidayNameList(info.getFestivalStr());
                if (StringUtil.isNotEmpty(info.getFestivalStr())) {
                    return Arrays.stream(info.getFestivalStr().split("@")).map(holidayName -> {
                        OvertimeHolidaysInfoModel newInfo1 = new OvertimeHolidaysInfoModel();
                        newInfo1.setUserId(info.getUserId());
                        newInfo1.setHolidayNameList(holidayName);
                        newInfo1.setHolidaysOvertimeDays(info.getHolidaysOvertimeSalaryDays());
                        return newInfo1;
                    });
                } else {
                    return Stream.of(newInfo);
                }
            }).collect(Collectors.toList()));
        }
        if (CollUtil.isEmpty(splitHolidayList)) {
            return new HashMap<>();
        }
        return splitHolidayList.stream().collect(Collectors.groupingBy(OvertimeHolidaysInfoModel::getUserId, Collectors.groupingBy(OvertimeHolidaysInfoModel::getHolidayNameList, Collectors.collectingAndThen(Collectors.toList(), list -> {
            if (CollUtil.isEmpty(list)) {
                return null;
            }
            OvertimeHolidaysInfoModel infoModel = list.get(0);
            String userId = infoModel.getUserId();
            HolidaysTypeJsonData1 jsonData = new HolidaysTypeJsonData1();
            jsonData.setField(infoModel.getHolidayNameList());
            jsonData.setFieldName(infoModel.getHolidayNameList());
            BigDecimal days = list.stream().map(OvertimeHolidaysInfoModel::getHolidaysOvertimeDays).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
            BigDecimal ratio = ratioMap.getOrDefault(userId, BigDecimal.ZERO);
            jsonData.setLeaveDays(days);
            jsonData.setLeaveHours(days.multiply(ratio).stripTrailingZeros());
            return jsonData;
        })))).entrySet().stream().filter(e -> CollUtil.isNotEmpty(e.getValue())).collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().entrySet().stream().filter(inner -> inner.getValue() != null).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
    }

    /**
     * 将 ConfirmDetailsInfo 对象转换为 Map
     * @param info ConfirmDetailsInfo 实例
     * @return Map<ExcelProperty值, 字段值>
     */
    @SneakyThrows
    public static <T> Map<String, Object> convertToMap(T info) {
        Map<String, Object> resultMap = new HashMap<>();
        Class<?> clazz = info.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            if ("customLeaveList".equals(field.getName()) && Objects.nonNull(field.get(info))) {
                Object fieldValue = field.get(info);
                List<LeaveTypeJsonData> customLeaveList = new ArrayList<>();
                if (fieldValue instanceof List<?>) {
                    ObjectMapper mapper = new ObjectMapper();
                    List<?> rawList = (List<?>) fieldValue;
                    for (Object item : rawList) {
                        if (item instanceof LeaveTypeJsonData) {
                            customLeaveList.add((LeaveTypeJsonData) item);
                        } else {
                            // 如果是Map类型，可以转换
                            LeaveTypeJsonData data = mapper.convertValue(item, LeaveTypeJsonData.class);
                            customLeaveList.add(data);
                        }
                    }
                }
                if (CollUtil.isNotEmpty(customLeaveList)) {
                    customLeaveList.forEach(item -> {
                        resultMap.put(item.getField() + "&" + item.getFieldName(), item.getLeaveDays());
                    });
                }
                continue;
            }
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
            if (excelProperty != null && excelProperty.value().length > 0) {
                String key = excelProperty.value()[0];
                try {
                    Object value = field.get(info);
                    resultMap.put(field.getName() + "&" + key, value);
                } catch (IllegalAccessException e) {
                    log.error("获取字段值失败: {}", field.getName(), e);
                }
            }
        }
        return resultMap;
    }

    /**
     * 判断当前时间是否等于指定的时间
     * @param sealTime 指定的时间
     * @return true 表示当前时间等于指定的时间，false 表示当前时间不等于指定的时间
     */
    public static boolean isCurrentTimeEqualsSealTime(String sealTime) {
        if (sealTime == null || sealTime.isEmpty()) {
            return false;
        }
        try {
            // 获取当前时间
            Calendar now = Calendar.getInstance();
            // 解析 sealTime
            String[] timeParts = sealTime.split(":");
            int sealHour = Integer.parseInt(timeParts[0]);
            int sealMinute = Integer.parseInt(timeParts[1]);
            // 比较小时和分钟
            return now.get(Calendar.HOUR_OF_DAY) == sealHour && now.get(Calendar.MINUTE) == sealMinute;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 计算班次占比
     *
     * @param dataList          日常班次
     * @param attendanceRatio   出勤换算比
     * @param payrollProportion 当天划线排班计薪工时占比MutablePair<计薪工时小时数, 计薪工时小时数/分钟>
     * @return 班次占比
     */
    public static Map<Integer, MutablePair<BigDecimal, MutablePair<BigDecimal, BigDecimal>>> processClassTypeData(List<UserDaySituationData> dataList, BigDecimal attendanceRatio, MutablePair<BigDecimal, BigDecimal> payrollProportion) {
        if (CollUtil.isEmpty(dataList)) {
            return new HashMap<>();
        }
        Map<Integer, MutablePair<BigDecimal, MutablePair<BigDecimal, BigDecimal>>> result = new HashMap<>();
        // 排除掉加班班次（因为只有普通班和请假才会算班次占比）
        dataList = dataList.stream().filter(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList());
        // 判断当天是否为划线排班
        boolean isUnderlineScheduling = dataList.stream().anyMatch(item -> item.getFixedMark().equals(2));
        if (isUnderlineScheduling) {
            result.put(-1, MutablePair.of(attendanceRatio, payrollProportion));
            return result;
        }
        Map<Integer, List<UserDaySituationData>> classTypeMap = CollUtil.isNotEmpty(dataList) ? dataList.stream().filter(dailyRule -> Objects.nonNull(dailyRule.getSchedulesType())).collect(Collectors.groupingBy(UserDaySituationData::getSchedulesType)) : new HashMap<>();
        for (Map.Entry<Integer, List<UserDaySituationData>> entry : classTypeMap.entrySet()) {
            // 请假是否全覆盖 全天班、上午班、下午班
            boolean isHasAllLeave = entry.getValue().stream().allMatch(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode()));
            int payrollMinute = entry.getValue().stream().filter(dailyRule -> !dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).map(UserDaySituationData::getValidDuration).mapToInt(Integer::intValue).sum();
            BigDecimal payrollHours;
            BigDecimal periodWorkDay;
            if (isHasAllLeave) {
                periodWorkDay = entry.getValue().stream().map(UserDaySituationData::getLeaveDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
                payrollHours = entry.getValue().stream().filter(dailyRule -> !dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).map(UserDaySituationData::getPayrollHours).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
            } else {
                // 全天班、上午班、下午班班次内是否有请假，有的话，普通班的天数只会会统计一次
                boolean isHasLeave = entry.getValue().stream().anyMatch(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode()));
                if (isHasLeave) {
                    periodWorkDay = entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).findFirst().orElse(BigDecimal.ZERO);
                    payrollHours = entry.getValue().stream().filter(dailyRule -> !dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).map(UserDaySituationData::getPayrollHours).findFirst().orElse(BigDecimal.ZERO);
                    entry.getValue().removeIf(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()));
                    if (CollUtil.isNotEmpty(entry.getValue())) {
                        periodWorkDay = periodWorkDay.add(entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
                    }
                } else {
                    // 公休天数+普班天数
                    periodWorkDay = entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
                    payrollHours = entry.getValue().stream().filter(dailyRule -> !dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).map(UserDaySituationData::getPayrollHours).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
                }
            }
            if (periodWorkDay.compareTo(BigDecimal.ZERO) > 0) {
                MutablePair<BigDecimal, BigDecimal> mutablePair = MutablePair.of(payrollHours, BigDecimal.ZERO);
                if (payrollHours.compareTo(BigDecimal.ZERO) > 0 && payrollMinute > 0) {
                    mutablePair = MutablePair.of(payrollHours, payrollHours.divide(BigDecimal.valueOf(payrollMinute), 6, RoundingMode.HALF_UP));
                }
                result.put(entry.getKey(), MutablePair.of(BigDecimal.valueOf(payrollMinute).divide(periodWorkDay, 2, RoundingMode.HALF_UP), mutablePair));
            }
        }
        return result;
    }

    /**
     * 处理请假、调休数据
     * @param statisticsListVo 日常统计数据
     * @param leaveTypeMap 请假数据
     */
    public static void processLeaveOrCompensationData(AttendanceDayStatistics statisticsListVo, Map<String, LeaveTypeStaModel> leaveTypeMap) {
        if (CollUtil.isEmpty(leaveTypeMap)) {
            return;
        }
        statisticsListVo.setLeaveDays(leaveTypeMap.values().stream().map(LeaveTypeStaModel::getLeaveDays).reduce(BigDecimal::add).orElse(BigDecimal.ZERO));
        statisticsListVo.setLeaveHours(leaveTypeMap.values().stream().map(LeaveTypeStaModel::getLeaveHours).reduce(BigDecimal::add).orElse(BigDecimal.ZERO));
        statisticsListVo.setLeavePayrollHours(leaveTypeMap.values().stream().map(LeaveTypeStaModel::getLeavePayrollHours).reduce(BigDecimal::add).orElse(BigDecimal.ZERO));
        statisticsListVo.setLeaveBatchNumber(leaveTypeMap.values().stream().map(LeaveTypeStaModel::getApplyList).flatMap(Collection::stream).map(LeaveTypeStaDetailsModel::getApplyId).collect(Collectors.joining(",")));
        // 获取当天的调休数据
        LeaveTypeStaModel compensationInfo = CollUtil.isNotEmpty(leaveTypeMap) ? leaveTypeMap.values().stream().filter(item -> Objects.nonNull(item.getIsRest()) && item.getIsRest() == 1).findFirst().orElse(null) : null;
        if (Objects.nonNull(compensationInfo)) {
            statisticsListVo.setCompensationHours(compensationInfo.getLeaveHours());
            statisticsListVo.setCompensationPayrollHours(compensationInfo.getLeavePayrollHours());
            statisticsListVo.setCompensationDays(compensationInfo.getLeaveDays());
            statisticsListVo.setCompensationTimes(compensationInfo.getApplyList().size());
        }
    }

    /**
     * 获取自定义请假数据
     * @param leaveTypeNameMap 请假数据
     * @param leaveTypeMap 请假数据
     * @return 自定义请假数据
     */
    public static String getCustomLeaveJson(Map<String, String> leaveTypeNameMap, Map<String, LeaveTypeStaModel> leaveTypeMap) {
        List<LeaveTypeJsonData> leaveTypeJsonDataList = CollUtil.newArrayList();
        for (Map.Entry<String, LeaveTypeStaModel> entry : leaveTypeMap.entrySet()) {
            if (!leaveTypeMap.containsKey(entry.getKey())) {
                continue;
            }
            LeaveTypeJsonData typeJsonData = LeaveTypeJsonData.builder().field(entry.getKey())
                    .fieldName(leaveTypeNameMap.getOrDefault(entry.getKey(), "未知请假类型"))
                    .headName(leaveTypeNameMap.getOrDefault(entry.getKey(), "未知请假类型"))
                    .leaveDays(entry.getValue().getLeaveDays()).leaveHours(entry.getValue()
                            .getLeaveHours()).leavePayrollHours(entry.getValue().getLeavePayrollHours())
                    .applyList(entry.getValue().getApplyList()).build();
            leaveTypeJsonDataList.add(typeJsonData);
        }
        return JSON.toJSONString(leaveTypeJsonDataList);
    }

    /**
     * 解析请假批次号字符串，支持多种格式
     * 格式1: [111,222] 或 ["111","222"]
     * 格式2: 11111,222
     *
     * @param batchNumber 批次号字符串
     * @return 解析后的批次号列表
     */
    public static List<String> parseBatchNumbers(String batchNumber) {
        if (batchNumber == null || batchNumber.isEmpty()) {
            return Collections.emptyList();
        }

        if (batchNumber.startsWith("[") && batchNumber.endsWith("]")) {
            // 处理数组格式: [111,222] 或 ["111","222"]
            String content = batchNumber.substring(1, batchNumber.length() - 1);
            return Arrays.stream(content.split(",")).map(String::trim).map(s -> s.replaceAll("^\"|\"$", "")) // 去除引号
                    .filter(s -> !s.isEmpty()).collect(Collectors.toList());
        } else {
            // 处理逗号分隔格式: 11111,222
            return Arrays.stream(batchNumber.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toList());
        }
    }

    /**
     * 解析外出批次号字符串，支持多种格式
     * 格式1: [111,222] 或 ["111","222"]
     * 格式2: 11111,222
     *
     * @param batchNumber 批次号字符串
     * @return 解析后的批次号列表
     */
    public static List<String> parseBatchNumber(String batchNumber) {
        if (batchNumber == null || batchNumber.isEmpty()) {
            return Collections.emptyList();
        }
        if (batchNumber.startsWith("[") && batchNumber.endsWith("]")) {
            List<BatchNumberResult> shiftsJsonVoList = parseArray(batchNumber, BatchNumberResult.class);
            return shiftsJsonVoList.stream().map(BatchNumberResult::getBatchNumberId).distinct().collect(Collectors.toList());
        } else {
            return Arrays.stream(batchNumber.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toList());
        }
    }

    /**
     * 获取空数据列表
     *
     * @param pageListDto 分页参数
     * @return 空数据列表
     */
    public static <R> PageListVO<R> returnEmptyList(CultivatePage pageListDto) {
        PageListVO<R> pageInfo = new PageListVO<>();
        PaginationVO pagination = new PaginationVO();
        pagination.setCurrentPage(pageListDto.getCurrentPage());
        pagination.setPageSize(pageListDto.getPageSize());
        pagination.setTotal(0);
        pageInfo.setList(new ArrayList<>());
        pageInfo.setPagination(pagination);
        return pageInfo;
    }

    /**
     * 获取数据列表
     *
     * @param listVoPage   分页信息
     * @param pageListDto 分页参数
     * @return 数据列表
     */
    public static <R> PageListVO<R> returnList(Page<R> listVoPage, CultivatePage pageListDto) {
        PageListVO<R> pageListVO = new PageListVO<>();
        PaginationVO pagination = new PaginationVO();
        pagination.setCurrentPage(pageListDto.getCurrentPage());
        pagination.setPageSize(pageListDto.getPageSize());
        pagination.setTotal(Math.toIntExact(listVoPage.getTotal()));
        pageListVO.setList(listVoPage.getRecords());
        pageListVO.setPagination(pagination);
        return pageListVO;
    }

    /**
     * 获取指定月份的开始时间和结束时间
     *
     * @param startMonth 开始月份
     * @param endMonth   结束月份
     * @return 包含开始时间和结束时间的MutablePair对象
     */
    public static MutablePair<LocalDate, LocalDate> getDateBetweenByMonth(String startMonth, String endMonth) {
        DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM", Locale.ENGLISH);
        YearMonth yearStartMonth = YearMonth.parse(startMonth, monthFormatter);
        LocalDate startDate = yearStartMonth.atDay(1);
        YearMonth yearEndMonth = YearMonth.parse(endMonth, monthFormatter);
        LocalDate endDate = yearEndMonth.atEndOfMonth().isAfter(LocalDate.now()) ? LocalDate.now() : yearEndMonth.atEndOfMonth();
        return MutablePair.of(startDate, endDate);
    }

    /**
     * 计算增长率
     * 公式：(本期数 - 同期数) / 同期数 * 100
     *
     * @param currentPeriod 本期数
     * @param previousPeriod 同期数
     * @return 增长率百分比
     */
    public static BigDecimal calculateGrowthRate(BigDecimal currentPeriod, BigDecimal previousPeriod) {
        if (currentPeriod == null) {
            currentPeriod = BigDecimal.ZERO;
        }
        if (previousPeriod == null || previousPeriod.compareTo(BigDecimal.ZERO) == 0) {
            // 如果同期数为 0，则无法计算增长率
            return currentPeriod.compareTo(BigDecimal.ZERO) > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO;
        }
        // 增长率 = (本期数 - 同期数) / 同期数 * 100
        return currentPeriod.subtract(previousPeriod).divide(previousPeriod, 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100));
    }

    /**
     * 获取两个月份之间的所有月份列表
     *
     * @param startMonth 开始月份（yyyy-MM）
     * @param endMonth   结束月份（yyyy-MM）
     * @return 月份列表
     */
    public static List<String> getAllMonthsBetween(String startMonth, String endMonth) {
        List<String> months = new ArrayList<>();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
        YearMonth start = YearMonth.parse(startMonth, formatter);
        YearMonth end = YearMonth.parse(endMonth, formatter);
        while (!start.isAfter(end)) {
            months.add(start.format(formatter));
            start = start.plusMonths(1);
        }
        return months;
    }
}
