package jnpf.attendance.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.lang.Assert;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import jnpf.AnalysisExternalInterfaceApi;
import jnpf.EstimateTurnoverTaskApi;
import jnpf.attendance.antifreeze.UserAntifreeze;
import jnpf.attendance.entity.FixedHandleDTO;
import jnpf.attendance.excel.HeadStyleHandler;
import jnpf.attendance.excel.listener.CustomStringStringConverter;
import jnpf.attendance.excel.listener.LineSchedulesDataListener;
import jnpf.attendance.excel.listener.SchedulesDataListener;
import jnpf.attendance.mapper.AttendanceClockInMapper;
import jnpf.attendance.mapper.AttendanceClockInResultMapper;
import jnpf.attendance.mapper.AttendanceDailyRuleMapper;
import jnpf.attendance.service.*;
import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedEmployeeView;
import jnpf.attendance.service.impl.preschedule.ByEmployeeSchedulesV2Converter;
import jnpf.attendance.service.impl.preschedule.PreScheduleRedisSupport;
import jnpf.attendance.service.handle.notification.AttendanceRuleNotificationHandle;
import jnpf.attendance.service.handle.rule.*;
import jnpf.base.service.SuperServiceImpl;
import jnpf.constants.AttendanceConstant;
import jnpf.constants.RedisConstant;
import jnpf.controller.util.ExcelExportTemplate;
import jnpf.entity.AttendanceGroup;
import jnpf.entity.AttendanceGroupUser;
import jnpf.entity.attendance.*;
import jnpf.entity.workflow.AttendanceBusinessTripApprove;
import jnpf.entity.workflow.AttendanceGoOutApprove;
import jnpf.enums.attendance.*;
import jnpf.enums.attendance.v2.ClockOutHandleParam;
import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum;
import jnpf.exception.HandleException;
import jnpf.model.attendance.dto.*;
import jnpf.model.attendance.model.DayClockRange;
import jnpf.model.attendance.model.DayInfo;
import jnpf.model.attendance.model.ScheduleImportFailModel;
import jnpf.model.attendance.vo.*;
import jnpf.model.attendance.vo.attendance.*;
import jnpf.permission.V2OrganizeApi;
import jnpf.permission.entity.UserEntity;
import jnpf.permission.eum.v2.OrganizeCategoryEnums;
import jnpf.permission.eum.v2.TargetAuthEnums;
import jnpf.permission.model.user.PartUserInfoVo;
import jnpf.permission.vo.v2.TargetAuthIdsVO;
import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO;
import jnpf.permission.vo.v2.user.UserBoundVO;
import jnpf.util.*;
import jnpf.util.attendance.AttendanceGroupUserStatusUtil;
import jnpf.util.attendance.DailyRuleUtil;
import jnpf.util.attendance.SecondmentTypeUtil;
import jnpf.util.attendance.RuleExcelImportUtil;
import jnpf.util.auth.V2AuthPermissionUtils;
import jnpf.util.context.ThreadContext;
import jnpf.util.data.DataSourceContextHolder;
import jnpf.vo.EstimateTurnoverStoredRowApiVo;
import jnpf.vo.StoreDayReceivableRevenueVo;
import jnpf.workflow.service.BusinessTripApproveService;
import jnpf.workflow.service.GoOutApproveService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URL;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static jnpf.constants.AttendanceConstant.HOLIDAYS_COLOR;
import static jnpf.constants.RedisConstant.ATTENDANCE_SET_SCHEDULES;
import static jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE;

/**
 * <p>
 * 考勤组-每日出勤规则 服务实现类
 * </p>
 *
 * @author ahua
 * @since 2023-11-22
 */
@Service
@Slf4j
public class AttendanceDailyRuleServiceImpl extends SuperServiceImpl<AttendanceDailyRuleMapper, FtbAttendanceDailyRule> implements AttendanceDailyRuleService {

    private final static String[] weekDays = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
    @Autowired
    private AttendanceGroupService attendanceGroupService;
    @Autowired
    private AttendanceUserService attendanceGroupUserService;
    @Autowired
    private AttendanceShiftSettingService attendanceShiftSettingService;
    @Autowired
    private AttendanceShiftNameSettingService attendanceShiftNameSettingService;
    @Autowired
    private AttendanceFestivalRulesService attendanceFestivalRulesService;
    @Autowired
    private AttendanceBaseSettingService attendanceBaseSettingService;
    @Resource
    private AttendanceClockInResultMapper attendanceClockInResultMapper;
    @Resource
    private AttendanceClockInMapper attendanceClockInMapper;
    @Autowired
    private WorkOvertimeRuleProcessor workOvertimeRuleProcessor;
    @Autowired
    private StepOutRuleProcessor stepOutRuleProcessor;
    @Autowired
    private LeaveRuleProcessor leaveRuleProcessor;
    @Autowired
    private OrdinaryRuleProcessor ordinaryRuleProcessor;
    @Autowired
    private RestRuleProcessor restRuleProcessor;
    @Autowired
    private UserAntifreeze userApi;
    @Autowired
    private AnalysisExternalInterfaceApi analysisExternalInterfaceApi;
    @Autowired
    private EstimateTurnoverTaskApi estimateTurnoverTaskApi;
    @Autowired
    private AttendanceRuleNotificationHandle attendanceRuleNotificationHandle;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private GoOutApproveService goOutApproveService;
    @Autowired
    private BusinessTripApproveService businessTripApproveService;
    @Resource
    private ExcelExportTemplate excelExportTemplate;
    @Resource
    private ThreadPoolExecutor cpuIntensiveThreadPool;
    @Resource
    private CustomTenantUtil customTenantUtil;
    @Autowired
    private StringRedisTemplate redisUtil;
    @Resource
    private OvertimeRuleService overtimeRuleService;
    @Autowired
    private PublicHolidayRulesService publicHolidayRulesService;
    @Resource
    private AttendanceDailyRuleMapper attendanceDailyRuleMapper;
    @Autowired
    private V2OrganizeApi organizeApi;
    @Autowired
    private AttendanceDayStatisticsService attendanceDayStatisticsService;
    @Resource
    private V2AuthPermissionUtils authPermissionUtils;
    @Autowired
    private AttendanceLineSchedulingConfigService lineSchedulingConfigService;
    @Autowired
    private AttendanceLineSchedulingPayrollHoursService lineSchedulingPayrollHoursService;
    @Resource
    private PreScheduleRedisSupport preScheduleRedisSupport;


    /**
     * 获取目标日期涉及的借调时段
     *
     * @param secondmentDateVos
     * @param inPoint
     * @param outPoint
     * @return
     */
    /**
     * 获取目标日期涉及的借调时段
     *
     * @param secondmentDateVos
     * @param inPoint
     * @param outPoint
     * @return
     */
    private static List<SecondmentDateVo> getSecondmentTypeList(List<SecondmentDateVo> secondmentDateVos, Date inPoint, Date outPoint) {
        if (CollUtil.isEmpty(secondmentDateVos)) {
            return null;
        }
        return secondmentDateVos.stream().filter(dateVo -> DateDetail.checkTimeBetween(inPoint, dateVo.getStartTime(), dateVo.getEndTime()) || DateDetail.checkTimeBetween(outPoint, dateVo.getStartTime(), dateVo.getEndTime()) || DateDetail.checkTimeBetween(dateVo.getStartTime(), inPoint, outPoint) || DateDetail.checkTimeBetween(dateVo.getEndTime(), inPoint, outPoint)).sorted(Comparator.comparing(SecondmentDateVo::getStartTime)).collect(Collectors.toList());
    }

    private OvertimeRuleDetailVo getOvertimeRuleDetailVo(List<FtbAttendanceDailyRule> rules, Date start, Date end, String applyId) {
        if (CollUtil.isEmpty(rules)) {
            return null;
        }
        String userId = rules.get(0).getUserId();
        rules = rules.stream().filter(vo -> Objects.equals(vo.getApplyViewEnable(), 1) || Objects.equals(vo.getApplyViewEnable(), 3) || Objects.equals(vo.getApplyViewEnable(), 9) || Objects.equals(vo.getApplyViewEnable(), 10))
                //去除本身这条
                .filter(vo -> !Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode()) || !StringUtil.equals(vo.getApplyId(), applyId)).collect(Collectors.toList());
        if (CollUtil.isEmpty(rules)) {
            OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.ORDINARY.getCode(), start);
            if (Objects.isNull(effectDetail)) {
                return null;
            }
            return effectDetail.getEffectRuleDetail();
        }
        //规则是否包含公休 attendanceType = 2
        FtbAttendanceDailyRule restRule = rules.stream().filter(v -> v.getAttendanceType() == 2).findFirst().orElse(null);
        //如果不包含公休，则获取工作日的加班规则
        if (Objects.isNull(restRule)) {
            OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.ORDINARY.getCode(), start);
            if (Objects.isNull(effectDetail)) {
                return null;
            }
            return effectDetail.getEffectRuleDetail();
        }
        //如果包含公休，且公休为全天班,则获取公休的加班规则
        if (Objects.equals(restRule.getSchedulesType(), 0)) {
            OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.REST.getCode(), start);
            if (Objects.isNull(effectDetail)) {
                return null;
            }
            return effectDetail.getEffectRuleDetail();
        }
        //过滤加班类型为公休日的加班
        List<FtbAttendanceDailyRule> collect = rules.stream().filter(vo -> {
            if (!Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                return true;
            }
            OvertimeRuleDetailVo overtimeRuleDetailVo = JSONObject.parseObject(vo.getPeriodInfo(), OvertimeRuleDetailVo.class);
            return !Objects.equals(overtimeRuleDetailVo.getOvertimeType(), 2);
        }).collect(Collectors.toList());
        //拿取rules中的去除休的最早普班、请假、加班
        FtbAttendanceDailyRule minRule = collect.stream().filter(v -> v.getAttendanceType() != 2).min(Comparator.comparing(FtbAttendanceDailyRule::getInPoint)).orElse(null);
        //拿取rules中的去除休的最晚普班、请假、加班
        FtbAttendanceDailyRule maxRule = collect.stream().filter(v -> v.getAttendanceType() != 2).max(Comparator.comparing(FtbAttendanceDailyRule::getOutPoint)).orElse(null);
        //如果公休为上半天班
        if (Objects.equals(restRule.getSchedulesType(), 1)) {
            //开始时间小于最早班次的开始时间,则获取公休的加班规则
            if (start.compareTo(minRule.getInPoint()) < 0) {
                OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.REST.getCode(), start);
                if (Objects.isNull(effectDetail)) {
                    return null;
                }
                return effectDetail.getEffectRuleDetail();
            }
            //开始时间大于最早班次的开始时间,则获取工作日的加班规则
            OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.ORDINARY.getCode(), start);
            if (Objects.isNull(effectDetail)) {
                return null;
            }
            return effectDetail.getEffectRuleDetail();
        }
        //如果公休为下半天班
        //开始时间小于最晚班次的结束时间,则获取工作日的加班规则
        if (start.compareTo(maxRule.getInPoint()) < 0) {
            OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.ORDINARY.getCode(), start);
            if (Objects.isNull(effectDetail)) {
                return null;
            }
            return effectDetail.getEffectRuleDetail();
        }
        //开始时间大于最晚班次的结束时间,则获取公休的加班规则
        OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.REST.getCode(), start);
        if (Objects.isNull(effectDetail)) {
            return null;
        }
        return effectDetail.getEffectRuleDetail();

    }

    /**
     * 排班导出
     *
     * @param groupId
     * @param realName
     * @param month
     * @return
     */
    @Override
    public void schedulesExport(String groupId, String workGroupId, String realName, String month, List<String> userIdList, Integer isSchedules) {
        List<SchedulesV2Vo> schedulesList = getSchedulesListV2(groupId, workGroupId, realName, month, userIdList, isSchedules);
        Date date = DateUtil.stringToDates(month + "-01");
        List<DayInfo> monthDayInfos = getMonthDayInfos(date);
        AttendanceShiftSettingVo byGroupId = attendanceShiftSettingService.findByGroupId(groupId);
        boolean isFixed = Objects.equals(byGroupId.getSystemType(), 1);
        List<ShiftPeriodVo> shiftPeriodVos = attendanceShiftSettingService.periodList(groupId);
        AttendanceGroup byId = attendanceGroupService.getById(groupId);
        List<String> heads = monthDayInfos.stream().map(dayInfo -> dayInfo.getWeekStr() + (char) 10 + dayInfo.getDayNum()).collect(Collectors.toList());
        OrganizeGeneralDetailVO infoById = organizeApi.organizeInfoById(StringUtil.isNotEmpty(workGroupId) ? OrganizeCategoryEnums.TEAM : null, StringUtil.isNotEmpty(workGroupId) ? workGroupId : byId.getOrgId()).getData();
        String titleHead = DateUtil.dateToString(date, "yyyy年MM月【") + infoById.getName() + "】排班表";
        List<String> collect = shiftPeriodVos.stream().map(vo -> vo.getName() + "：" + vo.getSchedulingPeriod().getPeriodNames().stream().map(PeriodNameVO::getPeriodName).reduce((r1, r2) -> r1 + " | " + r2).orElse("")).filter(StringUtil::isNotEmpty).collect(Collectors.toList());
        List<List<String>> partition = Lists.partition(collect, 8);
        String shiftHead = "[RED]成员班次请输入班次全称！" + (char) 10 + "班次信息：     公休     " + partition.stream().map(list -> StringUtil.join(list, "     ")).reduce((r1, r2) -> r1 + (char) 10 + r2).orElse("");
        heads.add(0, "已休|应休");
        heads.add(0, "手机号码");
        heads.add(0, "姓名");
        excelExportTemplate.exportModule(UserProvider.getLoginUserId(), titleHead, MonthStatisticsPageListExportVo.class, heads.stream().map(head -> CollUtil.newArrayList(titleHead, shiftHead, head)).collect(Collectors.toList()), null, null, 2000, HeadStyleHandler::new, (page, size) -> {
            List<Map<Integer, Object>> data = CollUtil.newArrayList();
            schedulesList.forEach(schedulesVo -> {
                Map<Integer, Object> objectObjectHashMap = Maps.newHashMap();
                Map<String, SchedulesDayVo> val = schedulesVo.getVal();
                objectObjectHashMap.put(0, schedulesVo.getRealName());
                objectObjectHashMap.put(1, schedulesVo.getMobilePhone());
                objectObjectHashMap.put(2, schedulesVo.getUsed() + "|" + (isFixed ? "-" : schedulesVo.getTotal()));
                for (int i = 0; i < monthDayInfos.size(); i++) {
                    DayInfo dayInfo = monthDayInfos.get(i);
                    SchedulesDayVo orDefault = val.get(dayInfo.getDay());
                    String context = CollUtil.isEmpty(orDefault.getItemVos()) ? "" : orDefault.getItemVos().stream().filter(item -> Objects.nonNull(item.getType())).map(item -> !Objects.equals(SchedulesTypeEnum.REST.getCode(), item.getType()) && !Objects.equals(SchedulesTypeEnum.HOLIDAYS.getCode(), item.getType()) && !Objects.equals(SchedulesTypeEnum.PAID_HOLIDAYS.getCode(), item.getType()) ? item.getName() : SchedulesTypeEnum.getMsg(item.getType())).filter(StringUtil::isNotEmpty).reduce((r1, r2) -> r1 + "|" + r2).orElse("");
                    objectObjectHashMap.put(i + 3, context);
                }
                data.add(objectObjectHashMap);
            });
            return data;
        });
    }

    @Override
    public List<SchedulesV2Vo> schedulesImport(SchedulesImportDto schedulesImportDto) throws IOException {
        Date date = DateUtil.stringToDates(schedulesImportDto.getMonth() + "-01");
        List<DayInfo> monthDayInfos = getMonthDayInfos(date);
        AttendanceShiftSettingVo shiftPeriodVos = attendanceShiftSettingService.findByGroupId(schedulesImportDto.getGroupId());
        Map<String, ShiftNameVo> shiftMap = shiftPeriodVos.getSchedulingPeriods().stream().collect(Collectors.toMap(ShiftNameVo::getName, Function.identity(), (r1, r2) -> r1));
        //获取当前考勤组成员
        List<AttendanceGroupUser> attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(date, cn.hutool.core.date.DateUtil.endOfMonth(date), List.of(), CollUtil.newArrayList(schedulesImportDto.getGroupId()), Boolean.TRUE);
        if (CollUtil.isEmpty(attendanceGroupUserVos)) {
            return CollUtil.newArrayList();
        }
        Map<String, List<AttendanceGroupUser>> groupUserMap = attendanceGroupUserVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));
        List<PartUserInfoVo> infoByIds = userApi.getStaffRosterListInfoByIds(attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()));
        Map<String, PartUserInfoVo> userMap = infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getMobilePhone, Function.identity(), (r1, r2) -> r1));
        List<String> heads = monthDayInfos.stream().map(dayInfo -> dayInfo.getWeekStr() + (char) 10 + dayInfo.getDayNum()).collect(Collectors.toList());
        heads.add(0, "已休|应休");
        heads.add(0, "手机号码");
        heads.add(0, "姓名");
        List<SchedulesV2Vo> importData = CollUtil.newArrayList();
        List<ScheduleImportFailModel> failList = CollUtil.newArrayList();
        EasyExcel.read(new URL(schedulesImportDto.getFileUrl()).openStream(), new SchedulesDataListener(importData, failList, heads, date, shiftMap, userMap)).registerConverter(new CustomStringStringConverter()).sheet().headRowNumber(3).doRead();
        importData.forEach(v -> {
            if (groupUserMap.containsKey(v.getUserId())) {
                List<AttendanceGroupUser> userList = groupUserMap.getOrDefault(v.getUserId(), List.of());
                v.getVal().forEach((day, value) -> {
                    DateTime parse = cn.hutool.core.date.DateUtil.parse(day, "yyyy-MM-dd");
                    Integer existStatus = isExistStatus(userList, parse);
                    if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) {
                        existStatus = 1;
                    }
                    Integer finalExistStatus = existStatus;
                    value.getItemVos().forEach(vo -> {
                        vo.setIsExist(finalExistStatus);
                        if (Objects.equals(finalExistStatus, 0) || Objects.equals(finalExistStatus, -1)) {
                            vo.setName(null);
                            vo.setShortName(null);
                            vo.setType(null);
                        }
                    });
                });
            }
        });

        List<SchedulesV2Vo> schedulesList = getSchedulesListV2(schedulesImportDto.getGroupId(), schedulesImportDto.getWorkGroupId(), null, schedulesImportDto.getMonth(), null, 1);
        Map<String, Map<String, SchedulesDayVo>> collect = importData.stream().collect(Collectors.toMap(SchedulesV2Vo::getUserId, SchedulesV2Vo::getVal));
        schedulesList.forEach(v -> {
            if (collect.containsKey(v.getUserId())) {
                Map<String, SchedulesDayVo> stringSchedulesDayVoMap = collect.get(v.getUserId());
                BigDecimal balance = v.getTotal();
                BigDecimal used = BigDecimal.ZERO;
                for (Map.Entry<String, SchedulesDayVo> entry : v.getVal().entrySet()) {
                    SchedulesDayVo value = entry.getValue();
                    if (stringSchedulesDayVoMap.containsKey(entry.getKey())) {
                        value.setItemVos(stringSchedulesDayVoMap.get(entry.getKey()).getItemVos());
                    }
                    List<SchedulesItemVo> itemVos = value.getItemVos();
                    if (CollUtil.isEmpty(itemVos)) {
                        continue;
                    }
                    if (itemVos.stream().noneMatch(vo -> Objects.equals(vo.getType(), 1))) {
                        continue;
                    }
                    String shiftName = itemVos.stream().filter(vo -> StringUtil.isNotEmpty(vo.getName())).map(SchedulesItemVo::getName).distinct().findFirst().orElse("");
                    ShiftNameVo shiftPeriodVo = StringUtil.isEmpty(shiftName) ? null : shiftMap.get(shiftName);
                    List<AttendanceShiftSettingPeriodVo> periods = Objects.isNull(shiftPeriodVo) ? null : shiftPeriodVo.getPeriods();
                    //扣除公休余额逻辑
                    List<SchedulesItemVo> collect1 = itemVos.stream().filter(vo -> Objects.equals(vo.getType(), 1)).collect(Collectors.toList());
                    for (SchedulesItemVo vo : collect1) {
                        BigDecimal calPeriodWorkDay = calPeriodWorkDay(vo.getSchedulesType(), periods);
                        balance = balance.subtract(calPeriodWorkDay);
                        used = used.add(calPeriodWorkDay);
                        if (RuleExcelImportUtil.addFail(balance.compareTo(BigDecimal.ZERO) < 0, ScheduleImportEnum.IMPORT_ERROR_PUBLIC_HOLIDAY_NOT_ENOUGH, v.getRealName(), failList)) {
                            return;
                        }

                    }
                }
                v.setUsed(used);
            }
        });
        String fail = failList.stream().sorted(Comparator.comparingInt(model -> model.getError().getIndex())).map(model -> String.format(model.getError().getMessage(), StringUtil.join(model.getUserNames().stream().distinct().collect(Collectors.toList()), "】、【"))).findFirst().orElse(null);
        Assert.isFalse(StringUtil.isNotEmpty(fail), fail);
        return schedulesList;
    }

    @Override
    public Map<String, BigDecimal> getUserPublicHoliday(String yearMonth, List<String> userIds) {
        List<KeyValueVo> publicHoliday = attendanceDailyRuleMapper.getUserPublicHoliday(yearMonth, userIds);
        return CollUtil.isNotEmpty(publicHoliday) ? publicHoliday.stream().collect(Collectors.toMap(KeyValueVo::getUserId, KeyValueVo::getValue)) : new HashMap<>();
    }

    @Override
    public Date getEarliestSchedulingDate(Date start) {
        FtbAttendanceDailyRule one = lambdaQuery()
                .select(FtbAttendanceDailyRule::getDay)
                .ge(Objects.nonNull(start), FtbAttendanceDailyRule::getDay, start)
                .orderByAsc(FtbAttendanceDailyRule::getDay)
                .last("limit 1")
                .one();
        return Objects.nonNull(one) ? one.getDay() : new Date();
    }

    @Override
    public void hisDailyRule(Map<String, String> group2orgMap, Map<String, String> org2groupMap, Map<String, List<AttendanceGroupUser>> usersMap) {
        //排班日规则历史数据处理
        List<FtbAttendanceDailyRule> list = lambdaQuery().list();
        list.stream().collect(Collectors.groupingBy(dailyRule -> dailyRule.getGroupId() + dailyRule.getUserId() + dailyRule.getDay())).forEach((k, list1) -> {
            boolean isOrdinary = list1.stream().anyMatch(v -> Objects.equals(v.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) || Objects.equals(v.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()));
            list1.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.REST.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.HOLIDAYS.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())).forEach(vo -> {
                vo.setAttendanceType(AttendanceTypeEnum.REST.getCode());
                if (isOrdinary) {
                    vo.setPeriodWorkDay(BigDecimal.valueOf(0.5));
                    vo.setPayrollHours(BigDecimal.valueOf(0.5).multiply(vo.getPayrollHours()).setScale(4, RoundingMode.HALF_UP));
                    return;
                }
                vo.setPeriodWorkDay(BigDecimal.ONE);
                vo.setPayrollHours(vo.getPayrollHours());
            });
            List<FtbAttendanceDailyRule> ordinaryRules = list1.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode())).collect(Collectors.toList());
            ordinaryRules.forEach(vo -> {
                if (Objects.equals(vo.getSchedulesType(), 1) || Objects.equals(vo.getSchedulesType(), 2)) {
                    vo.setPeriodWorkDay(BigDecimal.valueOf(0.5));
                    vo.setPayrollHours(BigDecimal.valueOf(0.5).multiply(vo.getPayrollHours()).setScale(4, RoundingMode.HALF_UP));
                    return;
                }
                vo.setPeriodWorkDay(BigDecimal.ONE);
                vo.setPayrollHours(vo.getPayrollHours());
            });
            Map<Integer, FtbAttendanceDailyRule> collect = ordinaryRules.stream().collect(Collectors.toMap(FtbAttendanceDailyRule::getSchedulesType, Function.identity(), (r1, r2) -> r1));
            List<FtbAttendanceDailyRule> collect1 = list1.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) && !Objects.equals(vo.getApplyViewEnable(), 0) && !Objects.equals(vo.getApplyViewEnable(), 2)).collect(Collectors.toList());
            Map<Integer, List<FtbAttendanceDailyRule>> leaveForSchedulesType = collect1.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getSchedulesType));
            collect1.forEach(vo -> {
                //如果普班为空，则是请假覆盖完普班
                BigDecimal leaveDay = Objects.equals(vo.getSchedulesType(), 1) || Objects.equals(vo.getSchedulesType(), 2) ? BigDecimal.valueOf(0.5) : BigDecimal.ONE;
                if (CollUtil.isEmpty(collect)) {
                    //如果单个请假覆盖完全天班
                    if (collect1.size() < 2) {
                        vo.setLeaveDay(BigDecimal.ONE);
                        vo.setPayrollHours(BigDecimal.valueOf(vo.getValidDuration()));
                        return;
                    }
                    //如果多个请假覆盖完全天班
                    List<FtbAttendanceDailyRule> leaveRules = leaveForSchedulesType.get(vo.getSchedulesType());
                    //如果半天只有一条数据
                    if (leaveRules.size() < 2) {
                        vo.setLeaveDay(BigDecimal.valueOf(0.5));
                        vo.setPayrollHours(BigDecimal.valueOf(0.5).multiply(vo.getPayrollHours()).setScale(4, RoundingMode.HALF_UP));
                        return;
                    }
                    //如果为全天班,则为0.5天,否则为1天
                    Integer totalValidDuration = leaveRules.stream().map(FtbAttendanceDailyRule::getValidDuration).reduce(Integer::sum).orElse(0);
                    vo.setLeaveDay(BigDecimal.valueOf(vo.getValidDuration()).divide(BigDecimal.valueOf(totalValidDuration), 2, RoundingMode.HALF_UP).multiply(leaveDay).setScale(2, RoundingMode.HALF_UP));
                    return;
                }
                //如果普班未覆盖完，则通过请假有效时长占普班原时长来计算，
                FtbAttendanceDailyRule ordinaryRule = collect.get(vo.getSchedulesType());
                if (Objects.isNull(ordinaryRule)) {
                    //如果多个请假覆盖完半天班
                    List<FtbAttendanceDailyRule> leaveRules = leaveForSchedulesType.get(vo.getSchedulesType());
                    //如果半天只有一条数据
                    if (leaveRules.size() < 2) {
                        vo.setLeaveDay(BigDecimal.valueOf(0.5));
                        vo.setPayrollHours(BigDecimal.valueOf(0.5).multiply(ordinaryRule.getPayrollHours()).setScale(4, RoundingMode.HALF_UP));
                        return;
                    }
                    //如果为全天班,则为0.5天,否则为1天
                    Integer totalValidDuration = leaveRules.stream().map(FtbAttendanceDailyRule::getValidDuration).reduce(Integer::sum).orElse(0);
                    vo.setLeaveDay(BigDecimal.valueOf(vo.getValidDuration()).divide(BigDecimal.valueOf(totalValidDuration), 2, RoundingMode.HALF_UP).multiply(leaveDay).setScale(2, RoundingMode.HALF_UP));
                    vo.setPayrollHours(BigDecimal.valueOf(0.5).multiply(ordinaryRule.getPayrollHours()).setScale(4, RoundingMode.HALF_UP));
                    return;
                }
                //计算ordinaryRule的originInPoint与originOutPoint的差值
                int originMinute = DateDetail.calculateMinuteDiff(ordinaryRule.getOriginInPoint(), ordinaryRule.getOriginOutPoint());
                // 计算originBreakStartPoint与originBreakEndPoint的差值来算出，如果任意一个为null,则默认为0
                originMinute -= (Objects.isNull(ordinaryRule.getBreakStartPoint()) ? 0 : DateDetail.calculateMinuteDiff(ordinaryRule.getOriginBreakStartPoint(), ordinaryRule.getOriginBreakEndPoint()));
                vo.setLeaveDay(BigDecimal.valueOf(vo.getValidDuration()).divide(BigDecimal.valueOf(originMinute), 2, RoundingMode.HALF_UP).multiply(leaveDay).setScale(2, RoundingMode.HALF_UP));
                vo.setPayrollHours(BigDecimal.valueOf(vo.getValidDuration()).divide(BigDecimal.valueOf(originMinute), 4, RoundingMode.HALF_UP).multiply(ordinaryRule.getPayrollHours()).setScale(4, RoundingMode.HALF_UP));
            });
        });
        List<FtbAttendanceDailyRule> collect = list.stream().filter(dailyRule -> !StringUtil.equals(org2groupMap.get(group2orgMap.getOrDefault(dailyRule.getGroupId(), "")), dailyRule.getGroupId())).collect(Collectors.toList());
        collect.forEach(dailyRule -> dailyRule.setGroupId(org2groupMap.get(group2orgMap.getOrDefault(dailyRule.getGroupId(), ""))));
        collect.forEach(dailyRule -> {
            List<AttendanceGroupUser> userList = usersMap.getOrDefault(dailyRule.getUserId(), List.of());
            if (userList.stream().anyMatch(vo -> Objects.equals(vo.getGroupId(), dailyRule.getGroupId()))) {
                return;
            }
            AttendanceGroupUser attendanceGroupUser = userList.stream().filter(vo -> vo.getCreatorTime().compareTo(dailyRule.getDay()) <= 0 && (Objects.isNull(vo.getRemoveTime()) || vo.getRemoveTime().compareTo(dailyRule.getDay()) >= 0)).findFirst().orElse(null);
            if (Objects.isNull(attendanceGroupUser)) {
                return;
            }
            dailyRule.setGroupId(attendanceGroupUser.getGroupId());
        });
        updateBatchById(list);
    }

    private List<DayInfo> getMonthDayInfos(Date date) {

        DateTime beginOfMonth = cn.hutool.core.date.DateUtil.beginOfMonth(date);
        DateTime endOfMonth = cn.hutool.core.date.DateUtil.endOfMonth(date);
        List<DateTime> days = cn.hutool.core.date.DateUtil.rangeToList(beginOfMonth, endOfMonth, DateField.DAY_OF_MONTH);
        //遍历获取每天属于周几
        return days.stream().map(day -> DayInfo.builder().day(DateUtil.dateToString(day, "yyyy-MM-dd")).weekStr(weekDays[cn.hutool.core.date.DateUtil.dayOfWeek(day) - 1]).dayNum(cn.hutool.core.date.DateUtil.dayOfMonth(day)).build()).collect(Collectors.toList());
    }

    /**
     * 晚走晚到与加班无需审批
     *
     * @param param
     */
    @Override
    public void clockOutHandle(ClockOutHandleParam param) {
        if (param.getEnd().compareTo(cn.hutool.core.date.DateUtil.offsetMinute(param.getStart(), 1)) <= 0) {
            return;
        }
        CompletableFuture.runAsync(() -> {
            customTenantUtil.checkOutTenant(param.getTenantId());
            lateOutLateIn(param.getUserId(), param.getTenantId(), param.getStart(), param.getEnd());
        });
    }

    private void lateOutLateIn(String userId, String tenantId, Date start, Date end) {
        DateTime endTime = cn.hutool.core.date.DateUtil.endOfDay(cn.hutool.core.date.DateUtil.offsetDay(end, 1));
        //检测是否已存在晚走晚到
        String key = String.format(RedisConstant.ATTENDANCE_LATE_IN_LATE_OUT, tenantId, userId);
        if (redisUtil.hasKey(key)) {
            String hashValues = redisUtil.opsForValue().get(key);
            AttendanceLateInLateOut attendanceLateInLateOut = JSON.parseObject(hashValues, AttendanceLateInLateOut.class);
            //是否为同一日期晚走晚到
            if (attendanceLateInLateOut.getEndTime().compareTo(endTime) == 0) {
                return;
            }
        }
        List<AttendanceGroupUser> attendanceGroupUsersOfSecondment = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(end, end, List.of(userId), null, Boolean.FALSE);
        if (CollUtil.isEmpty(attendanceGroupUsersOfSecondment)) {
            return;
        }
        AttendanceGroupUser attendanceGroupUser = attendanceGroupUsersOfSecondment.stream().findFirst().orElse(null);
        //查询晚走晚到规则
        AttendanceBaseSettingVo baseSettingVo = attendanceBaseSettingService.getOne(attendanceGroupUser.getGroupId());
        if (Objects.equals(baseSettingVo.getLateEarly(), 0)) {
            return;
        }
        //时间未达标数据过滤
        if (cn.hutool.core.date.DateUtil.offsetMinute(start, baseSettingVo.getLateMinis()).after(end)) {
            return;
        }

        //缓存起来，防止第二天重新排班
        redisUtil.opsForValue().set(key, JSON.toJSONString(AttendanceLateInLateOut.builder().startTime(end).endTime(endTime).lateMinis(baseSettingVo.getEarlyMinis()).build()));
        redisUtil.expire(key, 2, TimeUnit.DAYS);
        //查询daily rule,clockInTime后的最早一个班次
        List<FtbAttendanceDailyRule> list = lambdaQuery().eq(FtbAttendanceDailyRule::getUserId, userId).and(x -> x.or(x1 -> x1.gt(FtbAttendanceDailyRule::getInPoint, end).isNotNull(FtbAttendanceDailyRule::getInPoint)).or(x1 -> x1.between(FtbAttendanceDailyRule::getDay, end, endTime).isNull(FtbAttendanceDailyRule::getInPoint))).orderByAsc(FtbAttendanceDailyRule::getInPoint).last("limit 10").list();
        setRuleForLateOutLateIn(userId, tenantId, list);
    }

    private void setRuleForLateOutLateIn(String userId, String tenantId, List<FtbAttendanceDailyRule> list) {
        //划线排班不做晚走晚到
        if (list.stream().anyMatch(rule -> Objects.equals(rule.getFixedMark(), 2))) {
            return;
        }
        String key = String.format(RedisConstant.ATTENDANCE_LATE_IN_LATE_OUT, tenantId, userId);
        if (!redisUtil.hasKey(key)) {
            return;
        }
        list = list.stream().filter(x -> Objects.equals(x.getUserId(), userId)).collect(Collectors.toList());
        list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getDay)).forEach((day, dayRules) -> {
            Date amMaxDate = dayRules.stream().filter(dayRule -> Objects.nonNull(dayRule.getInPoint()) && Objects.equals(dayRule.getSchedulesType(), 1)).map(FtbAttendanceDailyRule::getOutPoint).min(Comparator.comparing(Date::getTime)).orElse(null);
            Date pmMinDate = dayRules.stream().filter(dayRule -> Objects.nonNull(dayRule.getInPoint()) && Objects.equals(dayRule.getSchedulesType(), 2)).map(FtbAttendanceDailyRule::getInPoint).min(Comparator.comparing(Date::getTime)).orElse(null);
            dayRules.forEach(dayRule -> {
                if (Objects.nonNull(dayRule.getInPoint())) {
                    return;
                }
                //如果是首次数据，则设置inPoint为day
                if (Objects.equals(dayRule.getSchedulesType(), 1)) {
                    dayRule.setInPoint(DateUtil.dateAddMinutes(pmMinDate, -1));
                    return;
                }
                if (Objects.equals(dayRule.getSchedulesType(), 2)) {
                    dayRule.setInPoint(DateUtil.dateAddMinutes(amMaxDate, 1));
                    return;
                }
                dayRule.setInPoint(dayRule.getDay());
            });
        });

        String hashValues = redisUtil.opsForValue().get(key);
        AttendanceLateInLateOut attendanceLateInLateOut = JSON.parseObject(hashValues, AttendanceLateInLateOut.class);
        //第一个班次
        //如果第二天未查询到班
        if (CollUtil.isEmpty(list)) {
            return;
        }
        CollUtil.sort(list, Comparator.comparing(FtbAttendanceDailyRule::getInPoint));
        FtbAttendanceDailyRule rule1 = list.stream().findFirst().orElse(null);
        //最近班次为加班/请假/超过第二天的24点不做处理
        DateTime dateTime = Objects.isNull(rule1.getLatePoint()) ? null : cn.hutool.core.date.DateUtil.offsetMinute(rule1.getLatePoint(), attendanceLateInLateOut.getLateMinis());
        if (!Objects.equals(rule1.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) || rule1.getInPoint().before(attendanceLateInLateOut.getStartTime()) || rule1.getInPoint().after(attendanceLateInLateOut.getEndTime())) {
            return;
        }
        list.forEach(vo -> {
            if (Objects.nonNull(vo.getOriginalLatePoint())) {
                vo.setInLackPoint(DateUtil.min(DateDetail.getNewDateByPeriod(vo.getLatePoint(), vo.getInLackPoint(), vo.getOriginalLatePoint()), DateUtil.dateAddMinutes(vo.getOutPoint(), -1)));
                vo.setLatePoint(vo.getOriginalLatePoint());
            }
        });
        list = list.stream().filter(rule -> DateDetail.checkTimeBetween(rule.getInPoint(), attendanceLateInLateOut.getStartTime(), attendanceLateInLateOut.getEndTime())).collect(Collectors.toList());
        if (list.size() > 1) {
            FtbAttendanceDailyRule rule2 = list.get(1);
            //第二班次在晚到范围内
            if (rule2.getInPoint().compareTo(dateTime) <= 0) {
                return;
            }
        }
        if (Objects.isNull(rule1.getOriginalLatePoint())) {
            rule1.setOriginalLatePoint(rule1.getLatePoint());
        }
        rule1.setInLackPoint(DateUtil.min(DateDetail.getNewDateByPeriod(rule1.getLatePoint(), rule1.getInLackPoint(), dateTime), DateUtil.dateAddMinutes(rule1.getOutPoint(), -1)));
        rule1.setLatePoint(DateUtil.min(dateTime, DateUtil.dateAddMinutes(rule1.getInLackPoint(), -1)));
        rule1.setIsLateOutLateIn(1);
        if (rule1.getOutPoint().compareTo(dateTime) <= 0) {
            rule1.setInUnbounded(Objects.equals(rule1.getInUnbounded(), 0) ? LATE_LEAVE_LATE_ARRIVE.getCode() : rule1.getInUnbounded());
            rule1.setOutUnbounded(Objects.equals(rule1.getOutUnbounded(), 0) ? LATE_LEAVE_LATE_ARRIVE.getCode() : rule1.getOutUnbounded());
        }
        updateById(rule1);
    }

    /**
     * 加班不审批
     */
    @Override
    @Transactional
    public FtbAttendanceDailyRule workOvertimeNotApprove(ClockOutHandleParam param) {
        if (StringUtil.isEmpty(param.getApplyId()) && Objects.equals(param.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())) {
            lambdaUpdate().set(FtbAttendanceDailyRule::getOutPoint, param.getEnd()).set(FtbAttendanceDailyRule::getApplyEnd, param.getEnd()).eq(FtbAttendanceDailyRule::getId, param.getRuleId()).update();
            return null;
        }
        OvertimeRuleDetailVo overtimeRuleDetailVo = param.getRuleDetail();
        //查询打卡班次为公休、普班
        if (Objects.isNull(overtimeRuleDetailVo)) {
            return null;
        }
        if (!Objects.equals(overtimeRuleDetailVo.getCalcMethod(), 3)) {
            return null;
        }
        if (param.getEnd().compareTo(cn.hutool.core.date.DateUtil.offsetMinute(param.getStart(), overtimeRuleDetailVo.getMinOvertimeMinute())) <= 0) {
            return null;
        }
        // 获取当前考勤组的排班锁
        RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), param.getUserId()));
        // 确保锁未被其他操作持有，避免并发执行
        try {
            Assert.isFalse(lock.isLocked() || !lock.tryLock(5, TimeUnit.SECONDS), "存在其他排班或申请操作正在执行，请稍后再试");
            param.setApplyId("notApprove" + System.currentTimeMillis());
            List<FtbAttendanceDailyRule> allRules = CollUtil.newArrayList();
            List<String> allRuleIds = CollUtil.newArrayList();
            List<DailyRuleResultVo> dailyRuleResultVos = applyDailyRuleHandle(ApplyParam.builder().groupId(param.getGroupId()).userId(param.getUserId()).applyId(param.getApplyId()).validDuration(BigDecimal.ZERO).start(param.getStart()).end(param.getEnd()).attendanceType(AttendanceTypeEnum.WORKOVERTIME).build(), allRules, allRuleIds);
            List<DailyRuleResultVo> collect = dailyRuleResultVos.stream().filter(result -> result.getType() == 1).collect(Collectors.toList());
            if (CollUtil.isEmpty(collect)) {
                allRules.stream().filter(vo -> StringUtil.equals(vo.getApplyId(), param.getApplyId())).forEach(vo -> {
                    vo.setPeriodInfo(JSON.toJSONString(overtimeRuleDetailVo));
                });
                saveOrUpdateBatch(allRules, allRuleIds);
                attendanceRuleNotificationHandle.notificationHandle(dailyRuleResultVos, param.getTenantId(), false, false);
            }
            return allRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).max(Comparator.comparing(FtbAttendanceDailyRule::getInPoint)).orElse(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private Map<String, BigDecimal> buildPublicHolidayBalanceMap(List<String> userIds, Date periodStart, Date periodEnd) {
        Map<String, BigDecimal> balanceMap = new HashMap<>();
        if (CollUtil.isEmpty(userIds)) {
            return balanceMap;
        }
        LocalDate s = periodStart.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate e = periodEnd.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        YearMonth ymStart = YearMonth.from(s);
        YearMonth ymEnd = YearMonth.from(e);
        YearMonth ym = ymStart;
        while (!ym.isAfter(ymEnd)) {
            String ymStr = ym.toString();
            List<AttendancePublicHolidayBalance> list = publicHolidayRulesService.getBalanceList(ymStr, userIds);
            if (CollUtil.isNotEmpty(list)) {
                for (AttendancePublicHolidayBalance b : list) {
                    BigDecimal t = b.getTotal();
                    if (t == null) {
                        t = BigDecimal.ZERO;
                    }
                    balanceMap.merge(b.getUserId(), t, BigDecimal::add);
                }
            }
            ym = ym.plusMonths(1);
        }
        return balanceMap;
    }

    /**
     * 获取排班数据（按自然月）
     */
    @Override
    public List<SchedulesV2Vo> getSchedulesListV2(String groupId, String workGroupId, String realName, String month, List<String> finalUserIdList, Integer isSchedules) {
        List<String> userIds = Optional.ofNullable(finalUserIdList).orElseGet(ArrayList::new);
        Date start = DateDetail.getMonthBeginDate(month);
        Date end = DateDetail.getMonthEndDate(month);
        return getSchedulesListV2Core(groupId, workGroupId, realName, start, end, month, userIds, isSchedules);
    }

    /**
     * 获取排班数据（按开始日期、结束日期）
     */
    @Override
    public List<SchedulesV2Vo> getSchedulesListV2ByDateRange(String groupId, String workGroupId, String realName, String startDate, String endDate, List<String> finalUserIdList, Integer isSchedules) {
        List<String> userIds = Optional.ofNullable(finalUserIdList).orElseGet(ArrayList::new);
        Date start = cn.hutool.core.date.DateUtil.beginOfDay(DateUtil.stringToDates(startDate));
        Date end = cn.hutool.core.date.DateUtil.endOfDay(DateUtil.stringToDates(endDate));
        return getSchedulesListV2Core(groupId, workGroupId, realName, start, end, null, userIds, isSchedules, true);
    }

    @Override
    public List<SchedulesV2Vo> getSchedulesListV2ByPreScheduleDraft(
            String groupId, String startDate, String endDate, String draftId) {
        List<AssignedEmployeeView> byEmployee =
                preScheduleRedisSupport.getByEmployeeByDraftId(draftId);
        if (CollUtil.isEmpty(byEmployee)) {
            return CollUtil.newArrayList();
        }
        Date start = cn.hutool.core.date.DateUtil.beginOfDay(DateUtil.stringToDates(startDate));
        Date end = cn.hutool.core.date.DateUtil.endOfDay(DateUtil.stringToDates(endDate));
        List<AttendanceGroupUser> attendanceGroupUserVos =
                attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(
                        start, end, null, CollUtil.newArrayList(groupId), Boolean.TRUE);
        if (CollUtil.isEmpty(attendanceGroupUserVos)) {
            return CollUtil.newArrayList();
        }
        Map<String, List<AttendanceGroupUser>> groupUserMap =
                attendanceGroupUserVos.stream()
                        .filter(u -> StringUtil.isNotEmpty(u.getUserId()))
                        .collect(
                                Collectors.groupingBy(
                                        u -> u.getUserId().trim()));
        List<String> userIds =
                attendanceGroupUserVos.stream()
                        .map(AttendanceGroupUser::getUserId)
                        .filter(StringUtil::isNotEmpty)
                        .distinct()
                        .collect(Collectors.toList());
        List<PartUserInfoVo> infoByIds =
                CollUtil.isNotEmpty(userIds) ? userApi.getStaffRosterListInfoByIds(userIds) : CollUtil.newArrayList();
        if (CollUtil.isEmpty(infoByIds)) {
            return CollUtil.newArrayList();
        }
        Map<String, String> realNameByUserId =
                infoByIds.stream()
                        .filter(vo -> StringUtil.isNotEmpty(vo.getUserId()))
                        .collect(
                                Collectors.toMap(
                                        PartUserInfoVo::getUserId,
                                        vo ->
                                                vo.getRealName() == null
                                                        ? ""
                                                        : vo.getRealName(),
                                        (a, b) -> a,
                                        LinkedHashMap::new));
        List<String> userIdsInOrder =
                infoByIds.stream()
                        .map(PartUserInfoVo::getUserId)
                        .filter(StringUtil::isNotEmpty)
                        .collect(Collectors.toList());
        Set<String> shiftIdSet = ByEmployeeSchedulesV2Converter.collectShiftIds(byEmployee);
        Map<String, AttendanceShiftNameEntity> shiftMap =
                CollUtil.isEmpty(shiftIdSet)
                        ? Collections.emptyMap()
                        : getShiftByShiftIds(new ArrayList<>(shiftIdSet));
        List<Date> datesByPeriod = DateDetail.getDatesByPeriod(start, end);
        List<SchedulesV2Vo> schedulesV2Vos =
                ByEmployeeSchedulesV2Converter.toSchedulesV2List(
                        byEmployee,
                        userIdsInOrder,
                        realNameByUserId,
                        shiftMap,
                        datesByPeriod,
                        (userId, day) ->
                                buildSchedulesDayVoForPreScheduleDraft(
                                        groupUserMap.getOrDefault(
                                                userId, Collections.emptyList()),
                                        day));
        fillPreScheduleDraftSchedulesV2UserFields(start, end, groupUserMap, schedulesV2Vos);
        fillCanLineScheduleByGroupConfig(groupId, schedulesV2Vos, infoByIds);
        return schedulesV2Vos.stream()
                .sorted(
                        Comparator.comparing(
                                vo ->
                                        Objects.equals(vo.getDelMark(), 1)
                                                ? 20000
                                                : Objects.isNull(vo.getSort()) ? 0 : vo.getSort()))
                .collect(Collectors.toList());
    }

    /**
     * 预排班草稿：填充 {@link SchedulesV2Vo} 人员头字段（delMark、selfGroup），与
     * {@link #getSchedulesListV2Core} 一致；canLineSchedule 由 {@link #fillCanLineScheduleByGroupConfig} 填充。
     */
    private void fillPreScheduleDraftSchedulesV2UserFields(
            Date start,
            Date end,
            Map<String, List<AttendanceGroupUser>> groupUserMap,
            List<SchedulesV2Vo> schedulesV2Vos) {
        if (CollUtil.isEmpty(schedulesV2Vos)) {
            return;
        }
        for (SchedulesV2Vo vo : schedulesV2Vos) {
            if (StringUtil.isEmpty(vo.getUserId())) {
                continue;
            }
            List<AttendanceGroupUser> groupUserVos =
                    groupUserMap.getOrDefault(vo.getUserId().trim(), Collections.emptyList());
            if (CollUtil.isEmpty(groupUserVos)) {
                vo.setSelfGroup(0);
                continue;
            }
            AttendanceGroupUser attendanceGroupUserVo =
                    groupUserVos.stream()
                            .max(Comparator.comparing(AttendanceGroupUser::getCreatorTime))
                            .orElse(null);
            if (attendanceGroupUserVo == null) {
                continue;
            }
            vo.setDelMark(attendanceGroupUserVo.getDeleteMark());
            Integer existStatus1 = isExistStatus(groupUserVos, start, end);
            existStatus1 =
                    Objects.equals(existStatus1, 4)
                            ? 2
                            : Objects.equals(existStatus1, 2) || Objects.equals(existStatus1, 3) ? 1 : 0;
            vo.setSelfGroup(existStatus1);
        }
    }

    private SchedulesDayVo buildSchedulesDayVoForPreScheduleDraft(
            List<AttendanceGroupUser> groupUserVos, Date day) {
        Integer existStatus = isExistStatus(groupUserVos, day);
        existStatus = ByEmployeeSchedulesV2Converter.normalizeExistStatus(existStatus);
        return SchedulesDayVo.builder()
                .isExist(existStatus)
                .isLineSchedule(Boolean.FALSE)
                .itemVos(new ArrayList<>())
                .build();
    }

    /**
     * @param staffRosterList 已加载的花名册，非空时复用，避免重复调用 {@code userApi.getStaffRosterListInfoByIds}
     */
    private void fillCanLineScheduleByGroupConfig(String groupId, List<SchedulesV2Vo> schedulesV2Vos, List<PartUserInfoVo> staffRosterList) {
        if (CollUtil.isEmpty(schedulesV2Vos)) {
            return;
        }
        FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulingConfigService.getByGroupId(groupId);
        if (Objects.isNull(lineSchedulingConfig)) {
            schedulesV2Vos.forEach(vo -> vo.setCanLineSchedule(Boolean.FALSE));
            return;
        }
        List<String> allUserIds = schedulesV2Vos.stream()
                .map(SchedulesV2Vo::getUserId)
                .filter(StringUtil::isNotEmpty)
                .distinct()
                .collect(Collectors.toList());
        Set<String> eligibleUserIds;
        if (CollUtil.isNotEmpty(staffRosterList)) {
            List<String> filteredUserIds = CollUtil.newArrayList(allUserIds);
            filterUserIdsByLineSchedulingConfig(filteredUserIds, lineSchedulingConfig, staffRosterList);
            eligibleUserIds = new HashSet<>(filteredUserIds);
        } else {
            getFtbAttendanceLineSchedulingConfig(allUserIds, lineSchedulingConfig);
            eligibleUserIds = new HashSet<>(allUserIds);
        }
        for (SchedulesV2Vo vo : schedulesV2Vos) {
            vo.setCanLineSchedule(StringUtil.isNotEmpty(vo.getUserId()) && eligibleUserIds.contains(vo.getUserId()));
        }
    }

    /**
     * 获取排班数据内部实现；month 非空时与按自然月查询一致，为空时按 start、end 自然日区间查询非请假规则
     */
    private List<SchedulesV2Vo> getSchedulesListV2Core(String groupId, String workGroupId, String realName, Date start, Date end, String month, List<String> finalUserIdList, Integer isSchedules) {
        return getSchedulesListV2Core(groupId, workGroupId, realName, start, end, month, finalUserIdList, isSchedules, false);
    }

    private List<SchedulesV2Vo> getSchedulesListV2Core(String groupId, String workGroupId, String realName, Date start, Date end, String month, List<String> finalUserIdList, Integer isSchedules, boolean fillCanLineSchedule) {
        //班组查询
        TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds();
        List<UserBoundVO> userBoundVO = null;
        if (Objects.nonNull(targetAuthIdsVO) && Objects.equals(targetAuthIdsVO.getTargetAuthEnums(), TargetAuthEnums.USER) || StringUtil.isNotEmpty(workGroupId)) {
            userBoundVO = attendanceGroupUserService.getUserBoundVO(groupId, workGroupId);
            if (CollUtil.isEmpty(userBoundVO)) {
                return List.of();
            }
        }
        List<UserEntity> byRealName = StringUtil.isBlank(realName) ? null : userApi.getInfoByLikeName(realName);
        if (StringUtil.isNotEmpty(realName) && CollUtil.isEmpty(byRealName)) {
            return CollUtil.newArrayList();
        }
        if (CollUtil.isNotEmpty(byRealName)) {
            finalUserIdList.addAll(byRealName.stream().map(UserEntity::getId).collect(Collectors.toList()));
        }
        List<String> userIdList;
        if (CollUtil.isEmpty(userBoundVO)) {
            userIdList = finalUserIdList;
        } else {
            userIdList = userBoundVO.stream().map(UserBoundVO::getId).filter(vo -> CollUtil.isEmpty(finalUserIdList) || finalUserIdList.contains(vo)).collect(Collectors.toList());
            if (CollUtil.isEmpty(userIdList)) {
                return List.of();
            }
        }

        //获取当前考勤组成员
        List<AttendanceGroupUser> attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, Objects.isNull(userIdList) ? null : userIdList.stream().distinct().collect(Collectors.toList()), CollUtil.newArrayList(groupId), Boolean.TRUE);
        if (CollUtil.isEmpty(attendanceGroupUserVos)) {
            return CollUtil.newArrayList();
        }
        Map<String, List<AttendanceGroupUser>> groupUserMap = attendanceGroupUserVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));
        //获取用户名
        List<String> collect2 = attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList());
        Map<String, Boolean> stringBooleanMap = StringUtil.isNotEmpty(month)
                ? attendanceDayStatisticsService.selectUserIsSeal(collect2, month)
                : attendanceDayStatisticsService.selectUserIsSealByDateRange(collect2, start, end);
        // 按天查询封存状态，跨月场景下用于精确判定每个用户每一天是否被封存
        Map<String, Set<String>> userDailySealMap = attendanceDayStatisticsService.selectUserDailySealByDateRange(collect2, start, end);
        List<PartUserInfoVo> infoByIds = CollUtil.isNotEmpty(collect2) ? userApi.getStaffRosterListInfoByIds(collect2) : CollUtil.newArrayList();
        if (CollUtil.isEmpty(infoByIds)) {
            return CollUtil.newArrayList();
        }
        List<FtbAttendanceDailyRule> list = lambdaQuery()
                .eq(FtbAttendanceDailyRule::getGroupId, groupId)
                .in(CollUtil.isNotEmpty(userIdList), FtbAttendanceDailyRule::getUserId, userIdList)
                .notIn(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode())
                .notIn(FtbAttendanceDailyRule::getApplyViewEnable, 0)
                .and(x -> {
                    if (StringUtil.isNotEmpty(month)) {
                        x.eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode())
                                .le(FtbAttendanceDailyRule::getApplyStart, end)
                                .ge(FtbAttendanceDailyRule::getApplyEnd, start)
                                .or()
                                .likeRight(FtbAttendanceDailyRule::getDay, month);
                    } else {
                        Date ds = cn.hutool.core.date.DateUtil.beginOfDay(start);
                        Date de = cn.hutool.core.date.DateUtil.endOfDay(end);
                        x.eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode())
                                .le(FtbAttendanceDailyRule::getApplyStart, end)
                                .ge(FtbAttendanceDailyRule::getApplyEnd, start)
                                .or(w -> w.ge(FtbAttendanceDailyRule::getDay, ds).le(FtbAttendanceDailyRule::getDay, de));
                    }
                })
                .orderByAsc(FtbAttendanceDailyRule::getDay, FtbAttendanceDailyRule::getInPoint)
                .list();
        if (Objects.isNull(list)) {
            list = CollUtil.newArrayList();
        }
        Map<String, List<Date>> userLeaveDayMap = list.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toMap(FtbAttendanceDailyRule::getUserId, vo -> DateDetail.getDatesByPeriod(vo.getApplyStart(), vo.getApplyEnd()), (k1, k2) -> {
            k1.addAll(k2);
            return k1.stream().distinct().collect(Collectors.toList());
        }));
        Date now = new Date();
        list.removeIf(vo -> vo.getDay() != null && vo.getDay().before(cn.hutool.core.date.DateUtil.beginOfDay(start)));
        list.removeIf(vo -> vo.getDay() != null && vo.getDay().after(cn.hutool.core.date.DateUtil.endOfDay(end)));
        Map<String, List<FtbAttendanceDailyRule>> collect = list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId));
        Map<String, PartUserInfoVo> userMap = infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity()));
        //查询打卡结果
        List<String> ruleIds = list.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList());
        List<AttendanceClockInResult> clockInResults = getClockInResult(ruleIds);
        Map<String, List<AttendanceClockInResult>> clockInResult = clockInResults.stream().collect(Collectors.groupingBy(AttendanceClockInResult::getRuleId));
        //查询打卡记录获取打卡时间
        List<SchedulesV2Vo> schedulesVos = CollUtil.newArrayList();
        //查询班次数据
        List<String> collect5 = list.stream().map(FtbAttendanceDailyRule::getShiftId).collect(Collectors.toList());
        Map<String, AttendanceShiftNameEntity> shiftMap = getShiftByShiftIds(collect5);
        List<Date> datesByPeriod = DateDetail.getDatesByPeriod(start, end);
        //公休余额查询
        Map<String, BigDecimal> balanceMap = StringUtil.isNotEmpty(month)
                ? publicHolidayRulesService.getBalanceList(month, collect2).stream().collect(Collectors.toMap(AttendancePublicHolidayBalance::getUserId, AttendancePublicHolidayBalance::getTotal, (r1, r2) -> r2))
                : Maps.newHashMap();
        List<Integer> attendanceTypeOfApply = CollUtil.newArrayList();
        AtomicReference<Boolean> isValidClock = new AtomicReference<>(Boolean.FALSE);
        AtomicReference<BigDecimal> usedAll = new AtomicReference<>(BigDecimal.ZERO);
        groupUserMap.forEach((ruleUserId, groupUserVos) -> {
            Map<String, SchedulesDayVo> val = Maps.newTreeMap();
            isValidClock.set(false);
            usedAll.set(BigDecimal.ZERO);
            AttendanceGroupUser attendanceGroupUserVo = groupUserVos.stream().max(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).orElse(null);
            List<FtbAttendanceDailyRule> rules = collect.getOrDefault(ruleUserId, CollUtil.newArrayList());
            Map<Date, List<FtbAttendanceDailyRule>> collect1 = rules.stream()
                    //过滤原离组考勤组当日数据
                    .filter(rule -> !Objects.equals(rule.getApplyViewEnable(), 3) || StringUtil.equals(rule.getGroupId(), groupId)).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getDay));
            datesByPeriod.forEach(date -> {
                collect1.putIfAbsent(date, CollUtil.newArrayList());
            });
            Map<Date, List<FtbAttendanceDailyRule>> collect3 = collect1.entrySet()
                    .stream()
                    .sorted(Map.Entry.comparingByKey())  // 按 key 排序
                    .collect(Collectors.toMap(
                            Map.Entry::getKey,
                            Map.Entry::getValue,
                            (e1, e2) -> e1,
                            LinkedHashMap::new  // 使用 LinkedHashMap 保持排序后的顺序
                    ));
            List<Date> userLeaveDayList = userLeaveDayMap.getOrDefault(ruleUserId, CollUtil.newArrayList());
            collect3.forEach((day, dayRules) -> {
                attendanceTypeOfApply.clear();
                if (userLeaveDayList.stream().anyMatch(leaveDay -> day.compareTo(leaveDay) == 0)) {
                    attendanceTypeOfApply.add(SchedulesTypeEnum.LEAVE.getCode());
                }
                String date = DateUtil.daFormat(day);
                // 当日是否封存：按用户+日期精确判断，避免跨月查询时整体聚合导致误锁定
                boolean dayIsSeal = userDailySealMap.getOrDefault(ruleUserId, Collections.emptySet()).contains(date);
                if (CollUtil.isEmpty(dayRules)) {
                    Integer existStatus = isExistStatus(groupUserVos, day);
                    if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3) || Objects.equals(existStatus, 2)) {
                        attendanceTypeOfApply.add(SchedulesTypeEnum.SECONDMENT_TO.getCode());
                    }
                    if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) {
                        existStatus = 1;
                    }
                    val.put(date, SchedulesDayVo.builder().isExist(existStatus).attendanceTypeOfApply(attendanceTypeOfApply.stream().distinct().collect(Collectors.toList())).isLineSchedule(Boolean.FALSE).totalDuration(0).isSeal(dayIsSeal).build());
                    return;
                }
                Integer existStatus = isExistStatus(groupUserVos, day);
                if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3) || Objects.equals(existStatus, 2)) {
                    attendanceTypeOfApply.add(SchedulesTypeEnum.SECONDMENT_TO.getCode());
                }
                if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) {
                    existStatus = 1;
                }
                boolean isLineScheduleDay = dayRules.stream().anyMatch(rule -> Objects.equals(rule.getFixedMark(), 2));
                if (Objects.equals(existStatus, -1) || Objects.equals(existStatus, 2) || Objects.equals(existStatus, 0)) {
                    val.put(date, SchedulesDayVo.builder().isExist(existStatus).attendanceTypeOfApply(attendanceTypeOfApply.stream().distinct().collect(Collectors.toList())).isLineSchedule(isLineScheduleDay).totalDuration(0).isSeal(dayIsSeal).build());
                    return;
                }
                if (Objects.equals(existStatus, 1)) {
                    BigDecimal used = dayRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.REST.getCode()) && Objects.nonNull(vo.getPeriodWorkDay())).map(FtbAttendanceDailyRule::getPeriodWorkDay).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
                    usedAll.set(usedAll.get().add(used));
                }
                boolean isContainsOrdinary = dayRules.stream().anyMatch(dayRule -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), dayRule.getAttendanceType()));
                Date amMaxDate = dayRules.stream().filter(dayRule -> Objects.nonNull(dayRule.getOutPoint()) && Objects.equals(dayRule.getSchedulesType(), 1)).map(FtbAttendanceDailyRule::getOutPoint).min(Comparator.comparing(Date::getTime)).orElse(null);
                Date pmMinDate = dayRules.stream().filter(dayRule -> Objects.nonNull(dayRule.getInPoint()) && Objects.equals(dayRule.getSchedulesType(), 2)).map(FtbAttendanceDailyRule::getInPoint).min(Comparator.comparing(Date::getTime)).orElse(null);
                List<SchedulesItemVo> schedulesItemVos = dayRules.stream().map(dayRule -> {
                            if (Objects.equals(attendanceGroupUserVo.getType(), 2) && StringUtil.equals(attendanceGroupUserVo.getGroupId(), groupId) && !StringUtil.equals(dayRule.getGroupId(), groupId) && Objects.equals(dayRule.getSelfGroup(), 2)) {
                                return null;
                            }
                            List<AttendanceClockInResult> attendanceClockInResults = clockInResult.get(dayRule.getId());
                            ShiftNameVo shiftNameVo = JSONUtil.toBean(dayRule.getPeriodInfo(), ShiftNameVo.class);
                            AttendanceShiftNameEntity attendanceShiftNameEntity = shiftMap.getOrDefault(dayRule.getShiftId(), new AttendanceShiftNameEntity());
                            Integer type = getType2(dayRule, attendanceTypeOfApply, attendanceClockInResults);
                            if (Objects.isNull(type) || AttendanceTypeEnum.LEAVE.getCode().equals(dayRule.getAttendanceType()) && (isContainsOrdinary || Objects.equals(dayRule.getApplyViewEnable(), 2))) {
                                return null;
                            }
                            if (Objects.equals(dayRule.getFixedMark(), 2)) {
                                return SchedulesItemVo.builder().isExist(1).id(dayRule.getId()).inPoint(dayRule.getInPoint()).start(getDate2Str(dayRule.getOriginInPoint())).isStartTomorrow(isTomorrow(dayRule.getDay(), dayRule.getOriginInPoint())).end(getDate2Str(dayRule.getOriginOutPoint())).isEndTomorrow(isTomorrow(dayRule.getDay(), dayRule.getOriginOutPoint())).validDuration(dayRule.getOriginValidDuration()).type(SchedulesTypeEnum.NORMAL.getCode()).name("划线排班").sort(dayRule.getSort()).selfGroup(dayRule.getSelfGroup()).build();
                            }
                            if (CollUtil.isNotEmpty(attendanceClockInResults) || Objects.equals(AttendanceTypeEnum.LEAVE.getCode(), dayRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), dayRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.STEP_OUT.getCode(), dayRule.getAttendanceType())) {
                                isValidClock.set(Boolean.TRUE);
                            }
                            SchedulesItemVo build1 = SchedulesItemVo.builder().isExist(1).id(dayRule.getId()).schedulesType(dayRule.getSchedulesType()).sort(dayRule.getSort()).shiftId(dayRule.getShiftId()).selfGroup(attendanceGroupUserVo.getType()).type(type).isShow(1).start(getDate2Str(dayRule.getOriginInPoint())).isStartTomorrow(isTomorrow(dayRule.getDay(), dayRule.getOriginInPoint())).end(getDate2Str(dayRule.getOriginOutPoint())).isEndTomorrow(isTomorrow(dayRule.getDay(), dayRule.getOriginOutPoint())).validDuration(dayRule.getValidDuration()).name(!Objects.equals(dayRule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) && !Objects.equals(dayRule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) ? "" : StringUtil.isNotEmpty(shiftNameVo.getName()) ? shiftNameVo.getName() : attendanceShiftNameEntity.getName()).shortName(!Objects.equals(dayRule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) && !Objects.equals(dayRule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) ? "" : StringUtil.isNotEmpty(shiftNameVo.getShortName()) ? shiftNameVo.getShortName() : attendanceShiftNameEntity.getShortName()).colour(Objects.equals(SchedulesTypeEnum.HOLIDAYS.getCode(), type) || Objects.equals(SchedulesTypeEnum.PAID_HOLIDAYS.getCode(), type) ? HOLIDAYS_COLOR : StringUtil.isNotEmpty(shiftNameVo.getColour()) ? shiftNameVo.getColour() : attendanceShiftNameEntity.getColour()).build();
                            setItemSortInput(dayRule, build1, pmMinDate, amMaxDate);
                            return build1;
                        }).filter(Objects::nonNull).filter(rule -> !Objects.equals(rule.getType(), 0))
                        //inpoint为空的数据按照schedulesType排序
                        .sorted(Comparator.comparing(SchedulesItemVo::getInPoint)).collect(Collectors.toList());
                schedulesItemVos.stream().filter(item -> Objects.equals(SchedulesTypeEnum.REST.getCode(), item.getType())).skip(1).forEach(vo -> vo.setType(null));
                int size = schedulesItemVos.size();
                List<SchedulesItemVo> collect6 = schedulesItemVos.stream().filter(vo -> !Objects.equals(SchedulesTypeEnum.LEAVE.getCode(), vo.getType())).collect(Collectors.toList());
                //存在请假且班次数量大于2，隐藏请假
                schedulesItemVos.stream().skip(collect6.isEmpty() ? 1 : 0).forEach(item -> {
                    if (size > 1 && Objects.equals(SchedulesTypeEnum.LEAVE.getCode(), item.getType())) {
                        item.setType(null);
                    }
                });
                int isShow = Objects.nonNull(attendanceGroupUserVo.getRemoveTime()) && attendanceGroupUserVo.getRemoveTime().before(day) ? -1 : 0;
                schedulesItemVos = schedulesItemVos.stream().filter(Objects::nonNull).filter(item -> Objects.nonNull(item.getType())).collect(Collectors.toList());
                //隐藏同班次多时段数据
                schedulesItemVos.stream().filter(vo -> StringUtil.isNotEmpty(vo.getName())).collect(Collectors.groupingBy(SchedulesItemVo::getName)).forEach((name, items) -> {
                    if (StringUtil.isEmpty(name) || CollUtil.isEmpty(items) || items.size() < 2) {
                        return;
                    }
                    items.stream().skip(1).forEach(lastItemVo -> {
                        lastItemVo.setName("");
                        lastItemVo.setShortName("");
                        lastItemVo.setIsShow(isShow);
                    });
                    items.stream().collect(Collectors.groupingBy(SchedulesItemVo::getStart)).forEach((start1, items1) -> {
                        items1.stream().skip(1).forEach(lastItemVo -> {
                            lastItemVo.setStart("");
                            lastItemVo.setEnd("");
                        });
                    });

                });
                int totalDuration = schedulesItemVos.stream().map(SchedulesItemVo::getValidDuration).filter(Objects::nonNull).reduce(0, Integer::sum);
                val.put(date, SchedulesDayVo.builder().isExist(existStatus).attendanceTypeOfApply(attendanceTypeOfApply.stream().distinct().collect(Collectors.toList())).itemVos(schedulesItemVos).isLineSchedule(isLineScheduleDay).totalDuration(totalDuration).isSeal(dayIsSeal).build());
            });
            AttendanceGroupUser attendanceGroupUser = groupUserVos.stream().sorted(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
            if (Objects.isNull(attendanceGroupUser)) {
                return;
            }
            PartUserInfoVo partUserInfoVo = userMap.get(ruleUserId);
            if (Objects.isNull(partUserInfoVo)) {
                return;
            }
            int existStatus1 = SecondmentTypeUtil.toSecondmentType(isExistStatus(groupUserVos, start, end));
            SchedulesV2Vo build = SchedulesV2Vo.builder().userId(ruleUserId).IsSeal(stringBooleanMap.getOrDefault(ruleUserId, Boolean.FALSE)).sort(attendanceGroupUser.getSort()).total(balanceMap.getOrDefault(ruleUserId, BigDecimal.ZERO)).used(usedAll.get()).mobilePhone(partUserInfoVo.getMobilePhone()).realName(partUserInfoVo.getRealName()).delMark(attendanceGroupUserVo.getDeleteMark()).selfGroup(existStatus1).build();
            if (build != null && Objects.equals(build.getDelMark(), 1) && !isValidClock.get() && attendanceGroupUserVo.getRemoveTime().before(now)) {
                return;
            }
            build.setVal(val);
            schedulesVos.add(build);
        });
        List<SchedulesV2Vo> result = schedulesVos.stream().sorted(Comparator.comparing(vo -> Objects.equals(vo.getDelMark(), 1) ? 20000 : Objects.isNull(vo.getSort()) ? 0 : vo.getSort())).collect(Collectors.toList());
        if (fillCanLineSchedule) {
            fillCanLineScheduleByGroupConfig(groupId, result, infoByIds);
        }
        return result;
    }

    private void setItemSortInput(FtbAttendanceDailyRule dayRule, SchedulesItemVo build1, Date pmMinDate, Date amMaxDate) {
        if (Objects.nonNull(dayRule.getInPoint())) {
            build1.setInPoint(dayRule.getInPoint());
            return;
        }
        //如果是首次数据，则设置inPoint为day
        if (Objects.equals(dayRule.getSchedulesType(), 1)) {
            build1.setInPoint(DateUtil.dateAddMinutes(pmMinDate, -1));
            return;
        }
        if (Objects.equals(dayRule.getSchedulesType(), 2)) {
            build1.setInPoint(DateUtil.dateAddMinutes(amMaxDate, 1));
            return;
        }
        build1.setInPoint(dayRule.getDay());
    }

    @Override
    public Map<String, AttendanceShiftNameEntity> getShiftByShiftIds(List<String> shiftIds) {
        List<AttendanceShiftNameEntity> attendanceShiftNameEntities = CollUtil.isEmpty(shiftIds) ? CollUtil.newArrayList() : attendanceShiftNameSettingService.listByIds(shiftIds);
        return attendanceShiftNameEntities.stream().collect(Collectors.toMap(AttendanceShiftNameEntity::getId, entity -> entity));
    }

    /**
     * 查询当日该用户是否存在于当前考勤组(-1离组 0未加入 1存在 2借调)
     *
     * @param users 该用户存在于当前考勤组的记录集合
     * @param date  当前日期
     * @return
     */
    @Override
    public Integer findUserIsExistsStatusByDay(List<AttendanceGroupUser> users, Date date) {
        return AttendanceGroupUserStatusUtil.findUserIsExistsStatusByDay(users, date);
    }

    @Override
    public Integer findUserIsExistsByDay(List<AttendanceGroupUser> users, Date date) {
        return AttendanceGroupUserStatusUtil.findUserIsExistsByDay(users, date);
    }

    @Override
    public Integer findUserIsExistsByDay(List<AttendanceGroupUser> users, Date start, Date end) {
        return AttendanceGroupUserStatusUtil.findUserIsExistsByDay(users, start, end);
    }

    @Override
    public Map<String, Integer> findUserIsExistsByUserList(List<AttendanceGroupUser> users, Date start, Date end) {
        return AttendanceGroupUserStatusUtil.findUserIsExistsByUserList(users, start, end);
    }

    @Override
    public Integer isExistStatus(List<AttendanceGroupUser> users, Date start) {
        return AttendanceGroupUserStatusUtil.isExistStatus(users, start);
    }

    @Override
    public Integer isExistStatus(List<AttendanceGroupUser> users, Date start, Date end) {
        return AttendanceGroupUserStatusUtil.isExistStatus(users, start, end);
    }

    @Override
    public Boolean isInSecondment(List<AttendanceGroupUser> users, Date start, Date end) {
        return AttendanceGroupUserStatusUtil.isInSecondment(users, start, end);
    }

    @Override
    public ScheduleRuleDetailVo getDetail(String id) throws HandleException {
        FtbAttendanceDailyRule byId = getById(id);
        if (Objects.isNull(byId)) {
            throw new HandleException("未查询到当前排班的排班信息");
        }
        List<AttendanceGroup> attendanceGroups = attendanceGroupService.queryListByIds(CollUtil.newArrayList(byId.getGroupId()));
        if (CollUtil.isEmpty(attendanceGroups)) {
            throw new HandleException("未查询到当前排班的考勤组信息");
        }
        AttendanceGroup attendanceGroup = attendanceGroups.get(0);
        if (Objects.isNull(attendanceGroup)) {
            throw new HandleException("未查询到当前排班的考勤组信息");
        }
        UserEntity infoById = userApi.getInfo(byId.getCreatorUserId());
        return ScheduleRuleDetailVo.builder().groupId(byId.getGroupId()).groupName(attendanceGroup.getDetailName()).attendanceType(byId.getAttendanceType()).createId(byId.getUserId()).createName(infoById.getRealName()).createTime(DateDetail.getDateTime2Str(byId.getCreatorTime())).day(DateDetail.getDate2Str(byId.getDay(), DateDetail.DF)).build();
    }

    @Override
    @Transactional
    public void fixedPeriodChange(List<String> groupIds, AttendanceShiftSettingVo periodList, Date enableTime) {
        List<AttendanceGroup> attendanceGroups = attendanceGroupService.queryDropList();
        Date end = cn.hutool.core.date.DateUtil.endOfMonth(enableTime);
        List<AttendanceGroupUser> attendanceGroupUserAllVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(enableTime, end, null, groupIds);
        Map<String, AttendanceShiftSettingVo> shiftSettingMap = Maps.newHashMap();
        groupIds.forEach(groupId -> shiftSettingMap.put(groupId, periodList));
        int day = cn.hutool.core.date.DateUtil.dayOfMonth(new Date());
        if (day >= 20) {
            end = cn.hutool.core.date.DateUtil.endOfMonth(DateUtil.dateAddMonths(enableTime, 1));
        }
        FixedHandleDTO build = FixedHandleDTO.builder().groupIds(groupIds).attendanceGroupVos(attendanceGroups).users(attendanceGroupUserAllVos).shiftSettingMap(shiftSettingMap).tenantId(UserProvider.getUser().getTenantId()).start(enableTime).end(end).build();
        fixedHandle(build);
        //处理节日自动排休
        addHolidayDailyRule(build);
    }

    @Override
    public void addUserFixedHandle(String groupId, List<String> userIds) {
        addUserFixedHandle(groupId, null, userIds);
    }

    @Override
    @Transactional
    public void addUserFixedHandle(String groupId, String tenantId, List<String> userIds) {
        List<String> groupIds = CollUtil.newArrayList(groupId);
        List<AttendanceGroup> attendanceGroups = attendanceGroupService.queryDropList();
        Map<String, AttendanceShiftSettingVo> shiftSettingMap = attendanceShiftSettingService.getEnableShiftSetting(groupIds, attendanceGroups);
        AttendanceShiftSettingVo attendanceShiftSettingVo = shiftSettingMap.get(groupId);
        if (Objects.isNull(attendanceShiftSettingVo)) {

            log.error("未查询到当前考勤组可用班制配置{}", groupId);
            statisticsNotificationNail(groupId, userIds, tenantId);
            return;
        }
        if (Objects.equals(attendanceShiftSettingVo.getSystemType(), 2)) {
            log.error("非当前考勤组非固定班，不生成固定班规则{}", groupId);
            statisticsNotificationNail(groupId, userIds, tenantId);
            return;
        }
        Date end = DateUtil.getEndDayOfMonth();
        Date start = attendanceShiftSettingVo.getEnableTime();
        int day = cn.hutool.core.date.DateUtil.dayOfMonth(new Date());
        if (day >= 20) {
            end = cn.hutool.core.date.DateUtil.endOfMonth(DateUtil.dateAddMonths(new Date(), 1)).toJdkDate();
        }
        List<AttendanceGroupUser> attendanceGroupUserAllVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, userIds, groupIds);
        //处理节日自动排休
        FixedHandleDTO build = FixedHandleDTO.builder().groupIds(groupIds).attendanceGroupVos(attendanceGroups).users(attendanceGroupUserAllVos).shiftSettingMap(shiftSettingMap).tenantId(tenantId).start(start).end(end).build();
        fixedHandle(build);
        addHolidayDailyRule(build);
    }

    private void statisticsNotificationNail(String groupId, List<String> userIds, String tenantId) {
        attendanceRuleNotificationHandle.notificationStatistics(tenantId, groupId, new Date(), userIds);
    }

    private Integer isTomorrow(Date day, Date date) {
        if (Objects.isNull(date)) {
            return 1;
        }
        Date date1 = DateUtil.dateAddDays(day, 1);
        if (date1.before(date)) {
            return 2;
        }
        return 1;
    }

    /**
     * 自动排休处理
     *
     * @param fixedHandleDTO fixedHandleDTO
     */
    private void addHolidayDailyRule(FixedHandleDTO fixedHandleDTO) {
        Map<String, AttendanceBaseSetting> enableBaseSetting = CollUtil.isNotEmpty(fixedHandleDTO.getEnableBaseSetting()) ? fixedHandleDTO.getEnableBaseSetting() : attendanceBaseSettingService.getEnableBaseSetting(fixedHandleDTO.getGroupIds(), fixedHandleDTO.getAttendanceGroupVos());
        List<AttendanceGroupUser> attendanceGroupUserAllVos = attendanceGroupUserService.queryByUsersGroupIds(fixedHandleDTO.getGroupIds());
        //查询节日规则
        Map<String, List<AttendanceFestivalRules>> festivalMap = CollUtil.isNotEmpty(fixedHandleDTO.getFestivalMap()) ? fixedHandleDTO.getFestivalMap() : attendanceFestivalRulesService.batchGetFestivalRulesByUserIds(fixedHandleDTO.getStart(), fixedHandleDTO.getEnd(), attendanceGroupUserAllVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()));
        Map<String, List<AttendanceGroupUser>> userMap = attendanceGroupUserAllVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId));
        Map<String, AttendanceShiftSettingVo> shiftMap = CollUtil.isNotEmpty(fixedHandleDTO.getShiftSettingMap()) ? fixedHandleDTO.getShiftSettingMap() : attendanceShiftSettingService.getEnableShiftSetting(fixedHandleDTO.getGroupIds(), fixedHandleDTO.getAttendanceGroupVos());
        List<String> userIds = attendanceGroupUserAllVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList());
        List<FtbAttendanceDailyRule> hisAllRules = getDailyRulesByPeriod(fixedHandleDTO.getStart(), fixedHandleDTO.getEnd(), userIds, fixedHandleDTO.getGroupIds());
        Map<String, List<FtbAttendanceDailyRule>> hisRulesMap = hisAllRules.stream().collect(Collectors.groupingBy(rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId()));
        if (CollUtil.isEmpty(shiftMap)) {
            log.error("自动排休未查询到所有考勤组班制配置");
            return;
        }
        addHolidayDailyRule(fixedHandleDTO.getGroupIds(), fixedHandleDTO.getTenantId(), enableBaseSetting, CollUtil.newArrayList(), festivalMap, userMap, shiftMap, hisRulesMap, CollUtil.newArrayList(), CollUtil.newArrayList());
    }

    /**
     * 自动排休处理
     *
     * @param start              开始时间
     * @param end                结束时间
     * @param attendanceGroupVos 考勤组集合
     * @param festivalSetting    调整后节日配置
     * @param hisFestivalSetting 调整前节日配置
     */
    /**
     * 处理节日规则更新的日常考勤规则
     *
     * @param start              开始时间
     * @param end                结束时间
     * @param attendanceGroupVos 考勤组集合
     * @param festivalSetting    调整后节日配置
     * @param hisFestivalSetting 调整前节日配置
     * @param newUserIds         新的用户ID列表
     * @param oldUserIds         旧的用户ID列表
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addHolidayDailyRule(Date start, Date end, List<AttendanceGroup> attendanceGroupVos, AttendanceFestivalRules festivalSetting, AttendanceFestivalRules hisFestivalSetting, List<String> newUserIds, List<String> oldUserIds) {
        // 1. 获取所有考勤组的班制配置
        Map<String, AttendanceShiftSettingVo> shiftMap = attendanceShiftSettingService.getEnableShiftSetting(attendanceGroupVos.stream().map(AttendanceGroup::getId).distinct().collect(Collectors.toList()), attendanceGroupVos);

        // 2. 检查是否获取到班制配置
        if (CollUtil.isEmpty(shiftMap)) {
            log.error("自动排休未查询到所有考勤组班制配置");
            return;
        }

        // 3. 过滤出固定班的班制配置（systemType=1）
        shiftMap.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()) || Objects.equals(entry.getValue().getSystemType(), 2));

        // 4. 检查是否有固定班的班制配置
        if (CollUtil.isEmpty(shiftMap)) {
            log.error("自动排休未查询到所有固定班考勤组配置");
            return;
        }

        // 5. 获取新用户的考勤组信息（包括借调情况）
        List<AttendanceGroupUser> attendanceGroupUserAllVos = CollUtil.isEmpty(newUserIds) ? Lists.newArrayList() : attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, newUserIds, new ArrayList<>(shiftMap.keySet()));

        // 6. 获取新用户所在的考勤组ID列表
        List<String> groupIds = attendanceGroupUserAllVos.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList());

        // 7. 构建用户ID到节日配置的映射
        Map<String, List<AttendanceFestivalRules>> festivalMap = attendanceGroupUserAllVos.stream().collect(Collectors.toMap(AttendanceGroupUser::getUserId, vo -> Objects.isNull(festivalSetting) ? List.of() : List.of(festivalSetting), (r1, r2) -> r1));

        // 8. 过滤出固定班的用户
        attendanceGroupUserAllVos.removeIf(vo -> !shiftMap.containsKey(vo.getGroupId()) || Objects.equals(shiftMap.get(vo.getGroupId()).getSystemType(), 2));

        // 9. 获取旧用户的考勤组信息（包括借调情况）
        List<AttendanceGroupUser> oldGroupUserAll = CollUtil.isEmpty(oldUserIds) ? Lists.newArrayList() : attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, oldUserIds, new ArrayList<>(shiftMap.keySet()));

        // 10. 过滤出固定班的旧用户
        oldGroupUserAll.removeIf(vo -> !shiftMap.containsKey(vo.getGroupId()) || Objects.equals(shiftMap.get(vo.getGroupId()).getSystemType(), 2));

        // 11. 获取旧用户所在的考勤组ID列表
        List<String> oldGroupIds = oldGroupUserAll.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList());

        // 12. 构建旧用户的考勤组映射
        Map<String, List<AttendanceGroupUser>> oldUserMap = oldGroupUserAll.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId));

        // 13. 合并新用户和旧用户的考勤组ID
        List<String> allGroupIds = new ArrayList<>(groupIds);
        allGroupIds.addAll(oldGroupIds);
        allGroupIds = allGroupIds.stream().distinct().collect(Collectors.toList());

        // 14. 获取基础配置（包含新用户和旧用户的考勤组）
        Map<String, AttendanceBaseSetting> enableBaseSetting = attendanceBaseSettingService.getEnableBaseSetting(allGroupIds, attendanceGroupVos);

        // 15. 准备变量
        List<FtbAttendanceDailyRule> allRules = CollUtil.newArrayList();
        List<String> allRuleIds = CollUtil.newArrayList();
        List<DailyRuleResultVo> resultList = CollUtil.newArrayList();

        // 16. 合并新用户和旧用户的ID
        List<String> allUserIds = new ArrayList<>();
        if (CollUtil.isNotEmpty(newUserIds)) {
            allUserIds.addAll(newUserIds);
        }
        if (CollUtil.isNotEmpty(oldUserIds)) {
            allUserIds.addAll(oldUserIds);
        }
        allUserIds = allUserIds.stream().distinct().collect(Collectors.toList());

        // 17. 获取历史规则（查询所有用户的，修复逻辑缺陷）
        List<FtbAttendanceDailyRule> hisAllRules = getDailyRulesByPeriod(start, end, allUserIds, allGroupIds);

        // 18. 构建历史规则映射
        Map<String, List<FtbAttendanceDailyRule>> hisRulesMap = hisAllRules.stream().collect(Collectors.groupingBy(rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId()));

        // 19. 清除历史规则
        clearHisRuleByUpdateHoliday(oldGroupIds, hisFestivalSetting, enableBaseSetting, resultList, oldUserMap, shiftMap, hisRulesMap, allRules, allRuleIds);

        // 20. 构建新用户的考勤组映射
        Map<String, List<AttendanceGroupUser>> userMap = attendanceGroupUserAllVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId));

        // 21. 添加新的节日规则
        addHolidayDailyRule(groupIds, UserProvider.getUser().getTenantId(), enableBaseSetting, resultList, festivalMap, userMap, shiftMap, hisRulesMap, allRules, allRuleIds);
    }

    /**
     * 清除修改的节日历史规则
     *
     * @param groupIds                   考勤组id
     * @param hisFestivalSetting        历史节日配置
     * @param enableBaseSetting         当前组使用的基础配置
     * @param resultList                返回结果
     * @param userMap                   考勤组用户集合
     * @param shiftMap                  班制配置集合
     * @param hisRulesMap               历史规则集合
     */
    private void clearHisRuleByUpdateHoliday(List<String> groupIds, AttendanceFestivalRules hisFestivalSetting, Map<String, AttendanceBaseSetting> enableBaseSetting, List<DailyRuleResultVo> resultList, Map<String, List<AttendanceGroupUser>> userMap, Map<String, AttendanceShiftSettingVo> shiftMap, Map<String, List<FtbAttendanceDailyRule>> hisRulesMap, List<FtbAttendanceDailyRule> allRules, List<String> allRuleIds) {
        //清除历史该节日规则
        if (Objects.isNull(hisFestivalSetting)) {
            return;
        }
        if (StringUtil.isEmpty(hisFestivalSetting.getFestivalDate())) {
            return;
        }
        List<FtbAttendanceDailyRule> rules = CollUtil.newArrayList();
        groupIds.forEach(groupId -> {
            AttendanceShiftSettingVo nextShiftSetting = shiftMap.get(groupId);
            if (Objects.isNull(nextShiftSetting) || !Objects.equals(nextShiftSetting.getSystemType(), 1)) {
                log.error("自动排休当请求为固定班规则生成，过滤非固定班配置{}", groupId);
                return;
            }
            AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(groupId);
            BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.valueOf(8) : attendanceBaseSetting.getAttendanceRatio();
            List<Date> dateList = Arrays.stream(hisFestivalSetting.getFestivalDate().split(",")).map(DateDetail::getStr2Date10).collect(Collectors.toList());
            List<Date> compensateInDate = StringUtil.isEmpty(hisFestivalSetting.getCompensateInDate()) ? CollUtil.newArrayList() : Arrays.stream(hisFestivalSetting.getCompensateInDate().split(",")).map(date -> DateDetail.getStr2Date10(date)).collect(Collectors.toList());
            dateList.addAll(compensateInDate);
            List<FixedClassShiftVo> fixedClassShiftVos = nextShiftSetting.getFixedPeriods();
            Map<Integer, FixedClassShiftVo> collect = fixedClassShiftVos.stream().collect(Collectors.toMap(FixedClassShiftVo::getNum, fixedClassShift -> fixedClassShift));
            Map<String, List<FtbAttendanceDailyRule>> applyCrossRules = Maps.newHashMap();
            List<AttendanceGroupUser> attendanceGroupUserAllVos = userMap.getOrDefault(groupId, List.of());
            attendanceGroupUserAllVos.forEach(user -> {
                applyCrossRules.clear();
                dateList.forEach(date -> {
                    DateTime end = cn.hutool.core.date.DateUtil.endOfDay(date);
                    String userGroupDay = DateDetail.getDateTime2Str(date) + user.getGroupId() + user.getUserId();
                    List<FtbAttendanceDailyRule> hisRules = hisRulesMap.getOrDefault(userGroupDay, CollUtil.newArrayList());
                    //将夸日的申请规则添加到当前日的规则中方便计算
                    applyCrossRulesAdd(hisRules, applyCrossRules, end, date, allRules);
                    List<String> ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList());
                    rules.clear();
                    int currentWeekNumber = DateDetail.getCurrentWeekNumber(date);
                    FixedClassShiftVo fixedClassShiftVo = collect.get(currentWeekNumber);
                    if (Objects.isNull(fixedClassShiftVo)) {
                        return;
                    }
                    List<ShiftNameVo> shiftNameVos = fixedClassShiftVo.getShiftNameVos();
                    if (CollUtil.isEmpty(shiftNameVos)) {
                        return;
                    }
                    ShiftNameVo ordShift = shiftNameVos.stream().filter(vo -> Objects.equals(vo.getType(), 2)).findFirst().orElse(null);
                    shiftNameVos.forEach(shiftName -> {
                        if (Objects.isNull(shiftName.getType())) {
                            return;
                        }
                        if (Objects.equals(shiftName.getType(), 1)) {
                            buildDailyRule(user, date, rules, shiftName.getFixedSort(), Objects.nonNull(ordShift) ? ordShift.getPeriods() : null, nextShiftSetting, AttendanceTypeEnum.REST, attendanceRatio, 4);
                        } else {
                            buildDailyRule(user, date, rules, shiftName.getFixedSort(), shiftName.getPeriods(), nextShiftSetting, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 4);
                        }
                    });

                    dailyRuleArrangementHandle(resultList, hisRules, rules);
                    secondmentDailyRulesHandle(resultList, user, hisRules);
                    //将夸日的申请规则添加到当前日的规则中方便计算
                    applyCrossRulesAddNotCurrent(hisRules, applyCrossRules, end, date);
                    hisRulesMap.put(userGroupDay, hisRules);
                    allRuleIds.addAll(ruleIds);
                });
            });
        });
    }

    private void addHolidayDailyRule(List<String> groupIds, String tenantId, Map<String, AttendanceBaseSetting> enableBaseSetting, List<DailyRuleResultVo> resultList, Map<String, List<AttendanceFestivalRules>> festivalMap, Map<String, List<AttendanceGroupUser>> userMap, Map<String, AttendanceShiftSettingVo> shiftMap, Map<String, List<FtbAttendanceDailyRule>> hisRulesMap, List<FtbAttendanceDailyRule> allRules, List<String> allRuleIds) {
        Map<String, List<FtbAttendanceDailyRule>> applyCrossRules = Maps.newHashMap();
        List<FtbAttendanceDailyRule> rules = CollUtil.newArrayList();
        for (String groupId : groupIds) {
            AttendanceShiftSettingVo nextShiftSetting = shiftMap.get(groupId);
            if (Objects.isNull(nextShiftSetting)) {
                log.error("自动排休未查询到当前考勤组[{}]班制配置", groupId);
                return;
            }
            if (!Objects.equals(nextShiftSetting.getSystemType(), 1)) {
                log.error("自动排休当请求为固定班规则生成时，过滤非固定班配置{}", groupId);
                return;
            }
            AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(groupId);
            BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.valueOf(8) : attendanceBaseSetting.getAttendanceRatio();
            List<AttendanceGroupUser> attendanceGroupUserVos = userMap.getOrDefault(groupId, CollUtil.newArrayList());
            attendanceGroupUserVos.forEach(user -> {
                try {
                    List<AttendanceFestivalRules> festivals = festivalMap.get(user.getUserId());
                    if (CollUtil.isEmpty(festivals)) {
                        log.error("自动排休未查询到当前人员[{}]节日配置", user.getUserId());
                        return;
                    }
                    festivals.forEach(festival -> {
                        applyCrossRules.clear();
                        if (StringUtil.isNotEmpty(festival.getFestivalDate())) {
                            List<Date> dateList = Arrays.stream(festival.getFestivalDate().split(",")).map(DateDetail::getStr2Date10).collect(Collectors.toList());
                            dateList.forEach(date -> {
                                Date start = cn.hutool.core.date.DateUtil.beginOfDay(date);
                                Date end = cn.hutool.core.date.DateUtil.endOfDay(date);
                                if (!Objects.equals(user.getUserGroupType(), 2) && (Objects.isNull(user.getCreatorTime()) ? Boolean.FALSE : end.compareTo(user.getCreatorTime()) < 0 || (Objects.isNull(user.getRemoveTime()) ? Boolean.FALSE : user.getRemoveTime().compareTo(start) < 0))) {
                                    return;
                                }
                                String key = DateDetail.getDateTime2Str(date) + user.getGroupId() + user.getUserId();
                                List<FtbAttendanceDailyRule> hisRules = hisRulesMap.getOrDefault(key, CollUtil.newArrayList());
                                hisRulesMap.remove(key);
                                //将夸日的申请规则添加到当前日的规则中方便计算
                                applyCrossRulesAdd(hisRules, applyCrossRules, end, date, allRules);
                                rules.clear();
                                buildDailyRule(user, date, rules, 0, null, null, AttendanceTypeEnum.REST, attendanceRatio, 3);
                                List<String> ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList());
                                dailyRuleArrangementHandle(resultList, hisRules, rules);
                                secondmentDailyRulesHandle(resultList, user, hisRules);
                                fillInNailRuleForOnlyStepOut(hisRules);
                                hisRules.stream().filter(ru -> Objects.equals(ru.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(ru -> {
                                    OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(hisRules, ru.getInPoint(), ru.getOutPoint(), ru.getApplyId());
                                    ru.setPeriodInfo(Objects.isNull(overtimeRuleDetailVo) ? null : JSON.toJSONString(overtimeRuleDetailVo));
                                });
                                //将夸日的申请规则添加到当前日的规则中方便计算
                                applyCrossRulesAddNotCurrent(hisRules, applyCrossRules, end, date);
                                allRules.addAll(hisRules);
                                allRuleIds.addAll(ruleIds);
                            });
                        }
                    });
                } catch (Exception e) {
                    log.error("自动排休发生异常{}{}", user, e);
                }
            });

        }
        saveOrUpdateBatch(allRules.stream().distinct().collect(Collectors.toList()), allRuleIds);
        attendanceRuleNotificationHandle.sendSystemNotice(resultList, tenantId);
        attendanceRuleNotificationHandle.notificationHandle(resultList, tenantId);
    }

    /**
     * 查询打卡结果数据
     *
     * @param ruleIds
     * @return
     */
    private List<AttendanceClockInResult> getClockInResult(List<String> ruleIds) {
        if (CollUtil.isEmpty(ruleIds)) {
            return CollUtil.newArrayList();
        }
        return attendanceClockInResultMapper.selectList(new LambdaQueryWrapper<AttendanceClockInResult>().in(AttendanceClockInResult::getRuleId, ruleIds).isNull(AttendanceClockInResult::getApplyId).eq(AttendanceClockInResult::getDeleteMark, Boolean.FALSE));

    }

    /**
     * 自己排班调整排班
     *
     * @param shiftId 班次id
     * @return
     */
    @Transactional
    @Override
    public String setSchedulesForSelfSchedules(String shiftId) throws HandleException {
        //查询当前用户当前时间所在考勤组
        String loginUserId = UserProvider.getLoginUserId();
        Date date = new Date();
        List<AttendanceGroupUser> attendanceGroupUsers = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(date, date, List.of(loginUserId), null);
        Assert.isFalse(CollUtil.isEmpty(attendanceGroupUsers), "未查询到当前时间所在考勤组");
        AttendanceGroupUser groupUser = attendanceGroupUsers.stream().findFirst().orElse(null);
        //查询班次涉及考勤组
        AttendanceShiftSettingVo byGroupId = attendanceShiftSettingService.findByGroupId(groupUser.getGroupId());
        Assert.isFalse(byGroupId.getSchedulingPeriods().stream().allMatch(vo -> !StringUtil.equals(vo.getId(), shiftId)), "所选班次非当前所在考勤组");
        List<FtbAttendanceDailyRule> list = lambdaQuery().eq(FtbAttendanceDailyRule::getDay, cn.hutool.core.date.DateUtil.beginOfDay(date)).eq(FtbAttendanceDailyRule::getGroupId, groupUser.getGroupId()).eq(FtbAttendanceDailyRule::getUserId, loginUserId).in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 9, 10).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.HOLIDAYS.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode()).list();
        Assert.isFalse(CollUtil.isNotEmpty(list), "当前已存在排班，请重新进入打卡页面");
        SchedulesSetDto schedulesSetDto = new SchedulesSetDto();
        schedulesSetDto.setUserId(loginUserId);
        schedulesSetDto.setGroupId(groupUser.getGroupId());
        Map<String, SchedulesDayVo> val = Maps.newHashMap();
        val.put(DateDetail.getDate2Str(date, DateDetail.DF), SchedulesDayVo.builder().itemVos(List.of(new SchedulesItemVo() {{
            setShiftId(shiftId);
            setSchedulesType(0);
            setType(2);
        }})).build());
        List<SchedulesItemVo> schedulesDaySetDtos = List.of(new SchedulesItemVo() {{
            setId("1");
            setShiftId(shiftId);
            setSchedulesType(0);
            setType(2);
        }});
        val.put(DateDetail.getDate2Str(cn.hutool.core.date.DateUtil.offsetDay(date, 1), DateDetail.DF), SchedulesDayVo.builder().itemVos(schedulesDaySetDtos).build());
        val.put(DateDetail.getDate2Str(cn.hutool.core.date.DateUtil.offsetDay(date, -1), DateDetail.DF), SchedulesDayVo.builder().itemVos(schedulesDaySetDtos).build());
        schedulesSetDto.setVal(val);
        return setSchedulesHandle(CollUtil.newArrayList(schedulesSetDto), null, Boolean.TRUE, Boolean.FALSE, null);
    }

    /**
     * 根据班次配置更新排班日程信息
     *
     * @param groupId       组织/集团ID
     * @param mark          时间标记（1:当日，其他值:次日）
     * @param periodConfigs 时段配置列表
     * @return 排班设置操作结果
     * @throws HandleException 业务处理异常
     */
    @Transactional
    @Override
    public Integer setSchedulesForShiftConfigUpdate(String groupId, Integer mark, List<PeriodConfig> periodConfigs, List<AttendanceShiftSettingPeriodEntity> periodEntities) throws HandleException {
        // 获取当日零点时间基准
        Date date = cn.hutool.core.date.DateUtil.beginOfDay(new Date());
        // 提取所有不重复的班次ID集合
        List<String> shiftIds = periodConfigs.stream().map(PeriodConfig::getShiftId).distinct().collect(Collectors.toList());
        if (shiftIds.isEmpty()) {
            return null;
        }

        // 构建初始查询条件：根据时间标记确定查询起始日
        Date queryDate = Objects.equals(1, mark) ? date : cn.hutool.core.date.DateUtil.offsetDay(date, 1);
        List<FtbAttendanceDailyRule> list = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, groupId).in(FtbAttendanceDailyRule::getShiftId, shiftIds).eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode()).ge(FtbAttendanceDailyRule::getDay, queryDate).list();

        // 提取用户ID集合和日期集合进行二次精确查询
        List<String> userIds = list.stream().map(FtbAttendanceDailyRule::getUserId).distinct().collect(Collectors.toList());
        List<Date> days = list.stream().map(FtbAttendanceDailyRule::getDay).distinct().collect(Collectors.toList());

        if (userIds.isEmpty() || days.isEmpty()) {
            return null;
        }
        list = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, groupId).in(FtbAttendanceDailyRule::getUserId, userIds).in(FtbAttendanceDailyRule::getAttendanceType, List.of(AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())).in(FtbAttendanceDailyRule::getDay, days).list();

        // 按用户ID分组规则数据
        Map<String, List<FtbAttendanceDailyRule>> collect = list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId));

        // 构建排班配置传输对象
        UpdateShiftConfigDto updateShiftConfigDto = new UpdateShiftConfigDto();
        updateShiftConfigDto.setPeriods(periodConfigs);
        List<SchedulesSetDto> schedulesSets = CollUtil.newArrayList();

        // 遍历用户构建排班设置结构
        collect.forEach((userId, rules) -> {
            SchedulesSetDto schedulesSetDto = new SchedulesSetDto();
            schedulesSetDto.setUserId(userId);
            schedulesSetDto.setGroupId(groupId);
            Map<String, SchedulesDayVo> val = Maps.newTreeMap();
            // 按日期分组处理每日排班规则
            rules.stream().sorted(Comparator.comparing(FtbAttendanceDailyRule::getDay)).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getDay, TreeMap::new, Collectors.toList())).forEach((day, rules1) -> {
                List<SchedulesItemVo> schedulesDaySetDtos = CollUtil.newArrayList();
                rules1.stream().collect(Collectors.toMap(vo -> Objects.isNull(vo.getSchedulesType()) ? 0 : vo.getSchedulesType(), Function.identity(), (r1, r2) -> r1, TreeMap::new)).forEach((schedulesType, rule) -> {
                    SchedulesItemVo dto = new SchedulesItemVo();
                    dto.setShiftId(rule.getShiftId());
                    dto.setSchedulesType(schedulesType);
                    dto.setType(getAttendanceType2type(rule.getAttendanceType()));
                    schedulesDaySetDtos.add(dto);
                });
                val.put(DateDetail.getDate2Str(day, DateDetail.DF), SchedulesDayVo.builder().itemVos(schedulesDaySetDtos).build());
            });
            List<String> strings = CollUtil.newArrayList(val.keySet());
            strings.forEach(day -> {
                DateTime date1 = cn.hutool.core.date.DateUtil.parse(day, DateDetail.DF);
                String yesterday = DateDetail.getDate2Str(cn.hutool.core.date.DateUtil.offsetDay(date1, -1), DateDetail.DF);
                String nextDay = DateDetail.getDate2Str(cn.hutool.core.date.DateUtil.offsetDay(date1, 1), DateDetail.DF);
                if (!val.containsKey(yesterday)) {
                    val.put(yesterday, SchedulesDayVo.builder().itemVos(List.of(new SchedulesItemVo() {{
                        setId("1");
                        setShiftId("1");
                        setSchedulesType(0);
                        setType(2);
                    }})).isExist(1).build());
                }
                if (!val.containsKey(nextDay)) {
                    val.put(nextDay, SchedulesDayVo.builder().itemVos(List.of(new SchedulesItemVo() {{
                        setId("1");
                        setShiftId("1");
                        setSchedulesType(0);
                        setType(2);
                    }})).isExist(1).build());
                }
            });
            schedulesSetDto.setVal(val);
            schedulesSets.add(schedulesSetDto);
        });
        setSchedulesHandle(schedulesSets, updateShiftConfigDto, Boolean.FALSE, Boolean.TRUE, periodEntities);
        // 调用排班设置核心处理方法
        return updateShiftConfigDto.getCode();
    }

    /**
     * 调整排班
     *
     * @param schedulesSets
     * @return
     */
    @Override
    @Transactional
    public String setSchedules(List<SchedulesSetDto> schedulesSets) throws HandleException {
        return setSchedulesHandle(schedulesSets, null, Boolean.FALSE, Boolean.FALSE, null);
    }

    /**
     * 统一排班接口（支持固定排班和划线排班）
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String setUnifiedSchedules(UnifiedSchedulesDto dto) throws HandleException {
        // 1. 参数验证
        Assert.isFalse(Objects.isNull(dto), "未查询到排班配置数据！");
        Assert.isFalse(StringUtil.isEmpty(dto.getGroupId()), "考勤组ID不能为空！");
        Assert.isFalse(CollUtil.isEmpty(dto.getUserDaySchedules()), "未找到需要排班的用户数据！");
        // 3. 调用高性能内部方法
        return setUnifiedSchedulesInternal(dto);
    }

    private Boolean check24H(List<FtbAttendanceDailyRule> rules) {
        // 一个时段的时候不可能超过24小时不做校验 校验上班时间不超过24小时 规则:第二个时段的下班时间有没有超过第一个上班时间的24小时    并校验  下班时间不可超过上班时间的24小时！
        // 9:00 -- 18:00    19:00 -- 次日 9:59
        Date inPoint = rules.stream().map(FtbAttendanceDailyRule::getInPoint).filter(Objects::nonNull).reduce((r1, r2) -> r1.before(r2) ? r1 : r2).orElse(null);
        Date outPoint = rules.stream().map(FtbAttendanceDailyRule::getOutPoint).filter(Objects::nonNull).reduce((r1, r2) -> r1.after(r2) ? r1 : r2).orElse(null);
        if (Objects.isNull(inPoint) || Objects.isNull(outPoint)) {
            return Boolean.FALSE;
        }
        if (outPoint.getTime() - inPoint.getTime() > 86400000L) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    /**
     * 调整排班
     *
     * @param schedulesSets
     * @return
     */
    private String setSchedulesHandle(List<SchedulesSetDto> schedulesSets, UpdateShiftConfigDto updateShiftConfigDto, Boolean isSync, Boolean isConfigUpdate, List<AttendanceShiftSettingPeriodEntity> list1) throws HandleException {
        if (CollUtil.isEmpty(schedulesSets)) {
            return "";
        }
        SchedulesSetDto schedulesSetDto = schedulesSets.stream().findFirst().orElse(null);
        // 确保schedulesSetDto不为空，否则抛出异常
        Assert.isFalse(Objects.isNull(schedulesSetDto), "未查询到排班数据！");
        // 获取当前考勤组的排班锁
        RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), schedulesSetDto.getGroupId()));
        // 确保锁未被其他操作持有，避免并发执行
        Assert.isFalse(lock.isLocked(), "当前考勤组排班操作正在执行中，请稍后再试");
        // 获取用户级别的排班锁列表
        List<RLock> rLocks = setSchedulesLockByUsers(schedulesSets);
        // 尝试获取排班锁
        try {
            Assert.isFalse(!lock.tryLock(5, 100, TimeUnit.SECONDS), "当前考勤组排班操作正在执行中，请稍后再试");
            // 初始化排班日期列表和班次ID列表
            List<String> dayList = CollUtil.newArrayList();
            List<String> shiftIds = CollUtil.newArrayList();
            // 遍历排班设置，收集所有排班日期和班次ID
            schedulesSets.forEach(dto -> {
                dayList.addAll(dto.getVal().keySet());
                dto.getVal().forEach((k, v) -> {
                    shiftIds.addAll(v.getItemVos().stream().map(SchedulesItemVo::getShiftId).collect(Collectors.toList()));
                });
            });

            // 将排班日期去重并排序
            List<Date> collect = dayList.stream().distinct().map(this::getStr2Date).sorted(Comparator.comparing(v -> v)).collect(Collectors.toList());
            // 确保有排班日期，否则抛出异常
            Assert.isFalse(CollUtil.isEmpty(collect), "无排班数据提交！");
            // 获取排班日期范围的起始和结束日期
            Date start = collect.stream().findFirst().orElse(null);
            Date end = collect.stream().max(Comparator.comparing(v -> v)).orElse(null);
            // 收集所有用户的ID和考勤组ID
            // 收集所有存在空ID排班项的用户ID
            List<String> userIds = schedulesSets.stream().filter(this::hasBlankIdItem).map(SchedulesSetDto::getUserId).distinct().collect(Collectors.toList());
            if (CollUtil.isEmpty(userIds)) {
                return "";
            }
            Map<String, SchedulesSetDto> collect4 = schedulesSets.stream().collect(Collectors.toMap(SchedulesSetDto::getUserId, Function.identity(), (r1, r2) -> r2));
            List<String> groupIds = schedulesSets.stream().map(SchedulesSetDto::getGroupId).distinct().collect(Collectors.toList());
            // 查询所有用户的考勤组信息
            List<AttendanceGroupUser> users = attendanceGroupUserService.queryAllByUsersIds(userIds);
            // 确保查询到用户的考勤组信息，否则抛出异常
            Assert.isFalse(CollUtil.isEmpty(users), "未查询到当前选择用户所属考勤组！");
            // 按用户和考勤组分组
            Map<String, List<AttendanceGroupUser>> userMap = users.stream().collect(Collectors.groupingBy(user -> user.getGroupId() + user.getUserId()));
            // 查询所有考勤组信息
            List<AttendanceGroup> attendanceGroupVos = attendanceGroupService.queryDropList();
            // 确保查询到考勤组信息，否则抛出异常
            Assert.isFalse(CollUtil.isEmpty(attendanceGroupVos), "未查询到考勤组信息！");
            // 查询指定日期范围内所有用户的排班规则
            List<FtbAttendanceDailyRule> hisAllRules = getDailyRulesByPeriod(start, cn.hutool.core.date.DateUtil.endOfDay(end), userIds, groupIds);
            Map<String, List<FtbAttendanceDailyRule>> acrossTheMoonUserLeaveRuleMap = hisAllRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId));
            // 按日期、考勤组和用户分组
            Map<String, List<FtbAttendanceDailyRule>> collect1 = hisAllRules.stream().collect(Collectors.groupingBy(rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId()));
            // 查询所有考勤组的有效班制设置
            Map<String, AttendanceShiftSettingVo> allShiftMap = attendanceShiftSettingService.getEnableShiftSetting(groupIds, attendanceGroupVos);
            // 确保查询到班制设置，否则抛出异常
            Assert.isFalse(CollUtil.isEmpty(allShiftMap), "未查询到所有考勤组班制配置！");
            // 获取当前考勤组的班制设置
            AttendanceShiftSettingVo attendanceShiftSettingVo = allShiftMap.get(groupIds.get(0));
            // 使用传入的list1或执行数据库查询
            List<AttendanceShiftSettingPeriodVo> attendanceShiftSettingPeriodEntities = Objects.isNull(attendanceShiftSettingVo) ? CollUtil.newArrayList() : attendanceShiftSettingVo.getSchedulingPeriods().stream().filter(vo -> shiftIds.contains(vo.getId())).flatMap(shiftNameVo -> shiftNameVo.getPeriods().stream()).collect(Collectors.toList());
            // 按班次ID分组
            Map<String, List<AttendanceShiftSettingPeriodVo>> periodMap = attendanceShiftSettingPeriodEntities.stream().collect(Collectors.groupingBy(AttendanceShiftSettingPeriodVo::getShiftId));
            // 确保当前考勤组的班制设置存在，否则抛出异常
            Assert.isFalse(Objects.isNull(attendanceShiftSettingVo), "未查询到当前考勤组班制配置！");
            // 初始化排班结果列表
            List<DailyRuleResultVo> resultList = new CopyOnWriteArrayList<>();
            // 查询所有考勤组的有效基础设置
            Map<String, AttendanceBaseSetting> enableBaseSetting = attendanceBaseSettingService.getEnableBaseSetting(groupIds);
            // 初始化排班规则列表和规则ID列表
            List<FtbAttendanceDailyRule> allRules = new CopyOnWriteArrayList<>();
            List<String> allRuleIds = new CopyOnWriteArrayList<>();
            // 查询所有适用的排班规则
            List<FtbAttendanceDailyRule> list = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, schedulesSetDto.getGroupId()).between(FtbAttendanceDailyRule::getDay, start, cn.hutool.core.date.DateUtil.endOfDay(end)).eq(FtbAttendanceDailyRule::getApplyViewEnable, 3).list();
            // 按用户和日期分组
            Map<String, List<FtbAttendanceDailyRule>> collect3 = list.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF)));
            // 查询所有用户的余额信息
            List<AttendancePublicHolidayBalance> usersBalance = publicHolidayRulesService.getBalanceList(cn.hutool.core.date.DateUtil.format(start, "yyyy-MM"), userIds);
            Map<String, Boolean> stringBooleanMap = attendanceDayStatisticsService.selectUserIsSeal(userIds, DateUtil.dateToString(start, "yyyy-MM"));
            // 将用户余额信息转换为Map，便于后续查询
            Map<String, BigDecimal> totalMap = usersBalance.stream().collect(Collectors.toMap(AttendancePublicHolidayBalance::getUserId, AttendancePublicHolidayBalance::getTotal, (r1, r2) -> r2));
            AtomicReference<BigDecimal> balance = new AtomicReference<>(BigDecimal.ZERO);
            List<CompletableFuture<Void>> futures = CollUtil.newArrayList();
            // 遍历每个排班设置，进行排班逻辑处理
            for (String userId : userIds) {
                SchedulesSetDto dto = collect4.get(userId);
                futures.add(CompletableFuture.runAsync(ThreadContext.wrap(() -> {
                    // 初始化排班冲突规则列表
                    // 初始化一个列表，用于存储时段冲突的考勤规则
                    List<FtbAttendanceDailyRule> periodConflictRules = CollUtil.newArrayList();
                    List<FtbAttendanceDailyRule> rules = CollUtil.newArrayList();
                    String groupId = dto.getGroupId();
                    // 获取用户余额信息
                    BigDecimal total = totalMap.getOrDefault(userId, BigDecimal.ZERO);
                    // 初始化用户的公休余额
                    balance.set(total);
                    // 获取考勤组的基础设置
                    AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(groupId);
                    // 获取考勤比例，默认为8小时
                    BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.valueOf(8) : attendanceBaseSetting.getAttendanceRatio();
                    // 获取用户考勤组信息列表
                    List<AttendanceGroupUser> userList = userMap.getOrDefault(groupId + userId, CollUtil.newArrayList());
                    // 获取调度任务的值，类型为Map，键为字符串，值为SchedulesDaySetDto对象的列表
                    Map<String, SchedulesDayVo> val = dto.getVal();
                    List<FtbAttendanceDailyRule> acrossTheMoonLeaveRuleList = acrossTheMoonUserLeaveRuleMap.getOrDefault(userId, CollUtil.newArrayList());
                    // 遍历Map中的每天排班数据
                    for (Map.Entry<String, SchedulesDayVo> stringListEntry : val.entrySet()) {
                        // 获取日期字符串
                        String day = stringListEntry.getKey();
                        // 获取该日期对应的考勤规则对象列表
                        List<SchedulesItemVo> items = stringListEntry.getValue().getItemVos();
                        // 将日期字符串转换为Date对象
                        Date date = getStr2Date(day);
                        Date yesterday = cn.hutool.core.date.DateUtil.offsetDay(date, -1);
                        // 获取当天结束时间的Date对象
                        Date endByDay = cn.hutool.core.date.DateUtil.endOfDay(date);
                        // 清空rules列表，减少重复创建集合
                        rules.clear();
                        // 根据日期和用户信息获取历史考勤规则
                        List<FtbAttendanceDailyRule> hisRules = collect1.getOrDefault(DateDetail.getDateTime2Str(date) + groupId + userId, CollUtil.newArrayList());
                        //将夸日的申请规则添加到当前日的规则中方便计算
                        // 获取所有非空的班次ID列表
                        List<String> collect2 = items.stream().map(SchedulesItemVo::getShiftId).filter(StringUtil::isNotEmpty).collect(Collectors.toList());
                        // 根据班次ID获取对应的考勤时段设置
                        List<AttendanceShiftSettingPeriodVo> periods = CollUtil.isNotEmpty(collect2) ? periodMap.get(collect2.get(0)) : CollUtil.newArrayList();
                        // 获取每天排班规则数量
                        int size = items.size();
                        // 获取每天排班规则中的第一个规则
                        Integer fixedMark = attendanceShiftSettingVo.getSystemType();
                        // 根据用户信息和日期获取最新的用户组信息
                        AttendanceGroupUser user = userList.stream().filter(user1 -> user1.getUserGroupType() == 2 ? Boolean.TRUE : endByDay.compareTo(user1.getCreatorTime()) >= 0 && (Objects.isNull(user1.getRemoveTime()) ? Boolean.TRUE : date.before(cn.hutool.core.date.DateUtil.endOfDay(user1.getRemoveTime())))).reduce((r1, r2) -> r2).orElse(null);
                        // 如果用户组信息为空，则跳过当前循环
                        if (Objects.isNull(user) || Objects.nonNull(user.getRemoveTime()) && date.after(user.getRemoveTime()) || date.before(cn.hutool.core.date.DateUtil.beginOfDay(user.getCreatorTime()))) {
                            continue;
                        }
                        batchBuildDailyRule(items, periodMap, balance, stringBooleanMap, userId, hisRules, size, user, date, rules, attendanceShiftSettingVo, groupId, periods, attendanceRatio);
                        // 如果rules列表为空，则将历史考勤规则添加到periodConflictRules列表中
                        if (CollUtil.isEmpty(rules)) {
                            // 判断时段是否冲突
                            periodConflictRules.addAll(hisRules);
                            boolean isPeriodConflict = !fixedPeriodConflict(periodConflictRules);
                            if (!isConfigUpdate) {
                                Assert.isFalse(isPeriodConflict, "选择的时段有冲突，请重新选择");
                                continue;
                            }
                            //修改班次配置给定前端返回值
                            if (isPeriodConflict) {
                                if (allRules.stream().anyMatch(rule -> date.compareTo(rule.getDay()) == 0 && updateShiftConfigDto.getPeriods().stream().anyMatch(config -> StringUtil.equals(config.getShiftId(), rule.getShiftId()) && Objects.equals(rule.getSchedulesType(), config.getType())))) {
                                    allRules.removeAll(allRules.stream().filter(rule -> date.compareTo(rule.getDay()) == 0).collect(Collectors.toList()));
                                }
                                if (allRules.stream().anyMatch(rule -> yesterday.compareTo(rule.getDay()) == 0 && updateShiftConfigDto.getPeriods().stream().anyMatch(config -> StringUtil.equals(config.getShiftId(), rule.getShiftId()) && Objects.equals(rule.getSchedulesType(), config.getType())))) {
                                    allRules.removeAll(allRules.stream().filter(rule -> yesterday.compareTo(rule.getDay()) == 0).collect(Collectors.toList()));
                                }
                                updateShiftConfigDto.setCode(CautionEnum.Caution_201.getCode());
                                if (Objects.equals(fixedMark, 1)) {
                                    updateShiftConfigDto.setCode(CautionEnum.Caution_202.getCode());
                                }
                                periodConflictRules.clear();
                                periodConflictRules.addAll(hisRules);
                            }
                            continue;
                        }

                        // 获取历史考勤规则的ID列表
                        List<String> ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList());
                        hisRules.addAll(BeanUtil.copyToList(acrossTheMoonLeaveRuleList.stream().filter(rule -> rule.getApplyStart().compareTo(endByDay) <= 0 && rule.getApplyEnd().compareTo(date) >= 0).collect(Collectors.toList()), FtbAttendanceDailyRule.class).stream().peek(vo -> {
                            vo.setDay(date);
                            vo.setId(RandomUtil.uuId());
                        }).collect(Collectors.toList()));
                        // 处理日常规则安排
                        dailyRuleArrangementHandle(resultList, hisRules, rules);
                        // 处理借调日常规则
                        secondmentDailyRulesHandle(resultList, user, hisRules);
                        // 填充仅外出的规则
                        fillInNailRuleForOnlyStepOut(hisRules);
                        hisRules.stream().filter(ru -> Objects.equals(ru.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(ru -> {
                            OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(hisRules, ru.getInPoint(), ru.getOutPoint(), ru.getApplyId());
                            ru.setPeriodInfo(Objects.isNull(overtimeRuleDetailVo) ? null : JSON.toJSONString(overtimeRuleDetailVo));
                        });
                        // 将历史考勤规则添加到periodConflictRules列表中
                        periodConflictRules.addAll(hisRules);

                        // 将历史考勤规则添加到allRules列表中
                        allRules.addAll(hisRules);
                        // 将历史考勤规则的ID添加到allRuleIds列表中
                        allRuleIds.addAll(ruleIds);
                        // 清理撤回规则
                        clearWithdrawRule(collect3.getOrDefault(user.getUserId() + day, CollUtil.newArrayList()), hisRules);
                        // 判断时段是否冲突
                        boolean isPeriodConflict = !fixedPeriodConflict(periodConflictRules);
                        Boolean is24H = check24H(rules);
                        if (!isConfigUpdate) {
                            Assert.isFalse(isPeriodConflict, "选择的时段有冲突，请重新选择");
                            Assert.isFalse(is24H, "选择的时段已超过24小时，请重新选择");
                            continue;
                        }
                        //修改班次配置给定前端返回值
                        if (isPeriodConflict) {
                            if (allRules.stream().anyMatch(rule -> date.compareTo(rule.getDay()) == 0 && updateShiftConfigDto.getPeriods().stream().anyMatch(config -> StringUtil.equals(config.getShiftId(), rule.getShiftId()) && Objects.equals(rule.getSchedulesType(), config.getType())))) {
                                allRules.removeAll(allRules.stream().filter(rule -> date.compareTo(rule.getDay()) == 0).collect(Collectors.toList()));
                            }
                            if (allRules.stream().anyMatch(rule -> yesterday.compareTo(rule.getDay()) == 0 && updateShiftConfigDto.getPeriods().stream().anyMatch(config -> StringUtil.equals(config.getShiftId(), rule.getShiftId()) && Objects.equals(rule.getSchedulesType(), config.getType())))) {
                                allRules.removeAll(allRules.stream().filter(rule -> yesterday.compareTo(rule.getDay()) == 0).collect(Collectors.toList()));
                            }
                            updateShiftConfigDto.setCode(CautionEnum.Caution_201.getCode());
                            if (Objects.equals(fixedMark, 1)) {
                                updateShiftConfigDto.setCode(CautionEnum.Caution_202.getCode());
                            }
                            periodConflictRules.clear();
                            periodConflictRules.addAll(hisRules);
                        }
                        //修改班次配置给定前端返回值
                        if (is24H) {
                            //根据时段id清空班次
                            if (allRules.stream().anyMatch(rule -> date.compareTo(rule.getDay()) == 0 && updateShiftConfigDto.getPeriods().stream().anyMatch(config -> StringUtil.equals(config.getShiftId(), rule.getShiftId()) && Objects.equals(rule.getSchedulesType(), config.getType())))) {
                                allRules.removeAll(allRules.stream().filter(rule -> date.compareTo(rule.getDay()) == 0).collect(Collectors.toList()));
                            }
                            updateShiftConfigDto.setCode(CautionEnum.Caution_203.getCode());
                        }

                    }
                    setRuleForLateOutLateIn(userId, UserProvider.getUser().getTenantId(), allRules);
                }), cpuIntensiveThreadPool));
            }
            // 等待所有任务完成
            CompletableFuture<Void> allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
            // 阻塞主线程直到所有异步任务完成
            allFutures.join(); // 这里会抛出 CompletionException 包装的原始异常
            // 保存或更新考勤规则
            saveOrUpdateBatch(allRules, allRuleIds);
            String result = resultSchedulingHandle(resultList).stream().map(DailyRuleResultVo::getFailMsg).filter(StringUtil::isNotEmpty).reduce((msg1, msg2) -> msg1 + "\\br" + msg2).orElse("");
            // 发送系统通知
            attendanceRuleNotificationHandle.sendSystemNotice(resultList, null);
            // 处理考勤规则通知
            attendanceRuleNotificationHandle.notificationHandle(resultList, isSync);
            // 处理结果列表，返回失败消息
            return result;
        } catch (InterruptedException e) {
            throw new RuntimeException(e.getMessage());
        } finally {
            // 解锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
            rLocks.forEach(rLock -> {
                if (rLock.isHeldByCurrentThread()) {
                    rLock.unlock();
                }
            });
        }
    }

    private void batchBuildDailyRule(List<SchedulesItemVo> items, Map<String, List<AttendanceShiftSettingPeriodVo>> periodMap, AtomicReference<BigDecimal> balance, Map<String, Boolean> stringBooleanMap, String userId, List<FtbAttendanceDailyRule> hisRules, int size, AttendanceGroupUser user, Date date, List<FtbAttendanceDailyRule> rules, AttendanceShiftSettingVo attendanceShiftSettingVo, String groupId, List<AttendanceShiftSettingPeriodVo> periods, BigDecimal attendanceRatio) {
        // 遍历items列表，处理每个排班
        items.forEach(item -> {
            List<AttendanceShiftSettingPeriodVo> attendanceShiftSettingPeriodVos1 = periodMap.get(item.getShiftId());
            if (Objects.equals(item.getType(), 1)) {
                balance.set(balance.get().subtract(Objects.equals(item.getSchedulesType(), 0) || Objects.isNull(item.getSchedulesType()) || Objects.isNull(attendanceShiftSettingPeriodVos1) || attendanceShiftSettingPeriodVos1.size() < 2 ? BigDecimal.ONE : attendanceShiftSettingPeriodVos1.get(item.getSchedulesType() - 1).getTimeSlotDay()));
            }
            // 如果调度任务的ID非空或者不存在，则跳过当前循环
            if (StringUtil.isNotEmpty(item.getId()) || Objects.equals(item.getIsExist(), 0) || Objects.isNull(item.getType())) {
                return;
            }
            Assert.isFalse(stringBooleanMap.getOrDefault(userId, Boolean.FALSE), "本月存在部分用户被封账！");
            // 根据班次ID获取考勤时段设置
            // 根据调度任务的类型处理考勤规则
            if (Objects.equals(item.getType(), 2)) {
                boolean hasWork = hisRules.stream().anyMatch(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode()));
                boolean hasLeave = hisRules.stream().anyMatch(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()));
                if (hasWork && hasLeave) {
                    return;
                }
                if (size < 2 && CollUtil.isNotEmpty(attendanceShiftSettingPeriodVos1)) {
                    attendanceShiftSettingPeriodVos1.forEach(period -> buildDailyRule(user, date, rules, item.getSchedulesType(), CollUtil.newArrayList(period), attendanceShiftSettingVo, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 0));
                    return;
                }
                buildDailyRule(groupId, userId, date, rules, item.getSchedulesType(), attendanceShiftSettingPeriodVos1, attendanceShiftSettingVo, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 0);
                return;
            }
            buildDailyRule(groupId, userId, date, rules, item.getSchedulesType(), periods, attendanceShiftSettingVo, getType2AttendanceType(item.getType()), attendanceRatio, 0, balance.get());

        });
    }

    private void applyCrossRulesAdd(List<FtbAttendanceDailyRule> hisRules, Map<String, List<FtbAttendanceDailyRule>> applyCrossRules, Date endByDay, Date date, List<FtbAttendanceDailyRule> allRules) {
        //如果有从上一日夸日到今天的申请数据，则直接添加到今天
        hisRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getApplyId)).forEach((applyId, applyRules) -> {
            FtbAttendanceDailyRule ftbAttendanceDailyRule = applyRules.get(0);
            if (isApplyCrossDay(ftbAttendanceDailyRule.getApplyStart(), ftbAttendanceDailyRule.getApplyEnd())) {
                List<FtbAttendanceDailyRule> collect2 = applyCrossRules.containsKey(applyId) ? applyCrossRules.get(applyId) : applyRules.stream().filter(vo -> vo.getInPoint().before(endByDay) && vo.getOutPoint().after(date)).collect(Collectors.toList());
                hisRules.addAll(collect2);
                allRules.removeAll(collect2);
            }
        });
    }

    private void applyCrossRulesAddNotCurrent(List<FtbAttendanceDailyRule> hisRules, Map<String, List<FtbAttendanceDailyRule>> applyCrossRules, Date endByDay, Date date) {
        //如果有从上一日夸日到当前天的申请数据，匹配完后迁移到后一天
        hisRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getApplyId)).forEach((applyId, applyRules) -> {
            FtbAttendanceDailyRule ftbAttendanceDailyRule = applyRules.get(0);
            applyCrossRules.remove(applyId);
            if (isApplyCrossDay(ftbAttendanceDailyRule.getApplyStart(), ftbAttendanceDailyRule.getApplyEnd()) && endByDay.before(ftbAttendanceDailyRule.getApplyEnd())) {
                applyCrossRules.put(applyId, applyRules.stream().filter(vo -> vo.getInPoint().before(endByDay) && vo.getOutPoint().after(date)).collect(Collectors.toList()));
            }
        });
    }


    private void applyCrossRulesAdd(List<FtbAttendanceDailyRule> hisRules, List<FtbAttendanceDailyRule> applyCrossRules, Date endByDay, Date date) {
        //如果有从上一日夸日到当前天的申请数据，匹配完后迁移到后一天
        hisRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getApplyId)).forEach((applyId, applyRules) -> {
            applyRules.forEach(ftbAttendanceDailyRule -> {
                if (isApplyCrossDay(ftbAttendanceDailyRule.getApplyStart(), ftbAttendanceDailyRule.getApplyEnd()) && endByDay.before(ftbAttendanceDailyRule.getApplyEnd())) {
                    applyCrossRules.add(ftbAttendanceDailyRule);
                }
            });
        });
        applyCrossRules.removeIf(vo -> vo.getApplyStart().after(endByDay) || vo.getApplyEnd().before(date));
    }

    /**
     * 填充外出在存在无覆盖排班时，外出未标记
     *
     * @param hisRules
     */
    private void fillInNailRuleForOnlyStepOut(List<FtbAttendanceDailyRule> hisRules) {
        List<FtbAttendanceDailyRule> collect = hisRules.stream().filter(hisRule -> (Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.STEP_OUT.getCode())) && Objects.equals(hisRule.getApplyViewEnable(), hisRule.getAttendanceType())).collect(Collectors.toList());
        if (CollUtil.isNotEmpty(collect)) {
            List<FtbAttendanceDailyRule> collect2 = hisRules.stream().filter(hisRule -> !collect.contains(hisRule) && !Objects.equals(hisRule.getApplyViewEnable(), AttendanceTypeEnum.STEP_OUT.getCode())).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(collect2)) {
                FtbAttendanceDailyRule ftbAttendanceDailyRule = collect.stream().findFirst().orElse(null);
                collect.forEach(rule -> rule.setApplyViewEnable(0));
                hisRules.add(buildRuleForNail(ftbAttendanceDailyRule.getGroupId(), ftbAttendanceDailyRule.getUserId(), ftbAttendanceDailyRule.getDay(), ftbAttendanceDailyRule.getAttendanceType(), AttendanceTypeEnum.CLEAR));
            }
        }
    }

    private List<RLock> setSchedulesLockByUsers(List<SchedulesSetDto> schedulesSets) {
        List<RLock> locks = new ArrayList<>();
        String day = DateUtil.dateToString(new Date(), "yyyy-MM-dd");
        for (SchedulesSetDto set : schedulesSets) {
            Map<String, SchedulesDayVo> val = set.getVal();
            if (Objects.isNull(val) || !val.containsKey(day)) {
                continue;
            }
            List<SchedulesItemVo> schedulesDaySetDtos = val.get(day).getItemVos();
            if (CollUtil.isEmpty(schedulesDaySetDtos)) {
                continue;
            }
            List<SchedulesItemVo> collect = schedulesDaySetDtos.stream().filter(dto -> Objects.isNull(dto.getId())).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(collect)) {
                RLock lock = redissonClient.getLock(String.format(RedisConstant.ATTENDANCE_USER_SET_SCHEDULES, UserProvider.getUser().getTenantId(), set.getGroupId(), set.getUserId()));
                try {
                    Assert.isFalse(!lock.tryLock(5, 100, TimeUnit.SECONDS), "当前用户正在排班中，请稍后再试");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                locks.add(lock);

            }
        }
        return locks;
    }

    private Boolean fixedPeriodConflict(List<FtbAttendanceDailyRule> periodEntities) {
        if (CollUtil.isEmpty(periodEntities)) {
            return Boolean.TRUE;
        }
        periodEntities = periodEntities.stream().filter(rule -> Objects.equals(rule.getApplyViewEnable(), 1) && !Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) && !Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.REST.getCode()) && !Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).collect(Collectors.toMap(FtbAttendanceDailyRule::getId, rule -> rule, (r1, r2) -> Objects.isNull(r1.getInPoint()) ? r2 : r1)).values().stream().collect(Collectors.toList());
        List<SecondmentDateVo> list = CollUtil.newArrayList();
        for (FtbAttendanceDailyRule period : periodEntities) {
            if (!fixedPeriodSingleConflict(list, period)) {
                return Boolean.FALSE;
            }
        }
        return Boolean.TRUE;
    }

    private Boolean fixedPeriodSingleConflict(List<SecondmentDateVo> list, FtbAttendanceDailyRule rule) {
        Date inPoint = rule.getInPoint();
        Date outPoint = rule.getOutPoint();
        if (Objects.isNull(inPoint)) {
            return Boolean.TRUE;
        }
        if (CollUtil.isEmpty(list)) {
            list.add(SecondmentDateVo.builder().startTime(inPoint).endTime(outPoint).build());
            return Boolean.TRUE;
        }
        for (SecondmentDateVo vo : list) {
            if (DateDetail.checkTimeBetween(inPoint, vo.getStartTime(), vo.getEndTime()) || DateDetail.checkTimeBetween(outPoint, vo.getStartTime(), vo.getEndTime()) || DateDetail.checkTimeBetween(vo.getStartTime(), inPoint, outPoint) || DateDetail.checkTimeBetween(vo.getEndTime(), inPoint, outPoint)) {
                if (vo.getStartTime().compareTo(outPoint) != 0 && vo.getEndTime().compareTo(inPoint) != 0) {
                    return Boolean.FALSE;
                }
            }
        }
        list.add(SecondmentDateVo.builder().startTime(inPoint).endTime(outPoint).build());
        return Boolean.TRUE;
    }

    private void saveOrUpdateBatch(List<FtbAttendanceDailyRule> allRules, List<String> allRuleIds) {
        if (CollUtil.isNotEmpty(allRuleIds)) {
            List<FtbAttendanceDailyRule> collect = allRules.stream().filter(rule -> allRuleIds.contains(rule.getId())).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(collect)) {
                updateBatchById(collect);
            }
            allRules = new ArrayList<>(allRules.stream().filter(rule -> !allRuleIds.contains(rule.getId())).distinct().collect(Collectors.toMap(FtbAttendanceDailyRule::getId, rule -> rule, (r1, r2) -> Objects.isNull(r1.getInPoint()) ? r2 : r1)).values());
            allRuleIds.removeAll(collect.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()));
            allRuleIds.removeAll(allRules.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()));
            removeBatchByIds(allRuleIds);
            saveBatch(allRules);
            crossDayRuleByWorkOvertime(collect);
            return;
        }
        if (CollUtil.isNotEmpty(allRules)) {
            saveBatch(allRules.stream().distinct().collect(Collectors.toList()));
        }
    }

    /**
     * 跨日的加班规则处理
     *
     * @param collect
     */
    private void crossDayRuleByWorkOvertime(List<FtbAttendanceDailyRule> collect) {
        //过滤出加班且被跨日排班的规则
        Map<String, FtbAttendanceDailyRule> ruleMap = collect.stream().filter(rule -> Objects.equals(rule.getApplyViewEnable(), 0) && Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode()) && !rule.getIsInsert()).collect(Collectors.toMap(FtbAttendanceDailyRule::getApplyId, rule -> rule, (v1, v2) -> v1));
        if (CollUtil.isEmpty(ruleMap)) {
            return;
        }
        //查询所有隐藏的跨日加班申请涉及的所有加班申请规则
        List<FtbAttendanceDailyRule> list = lambdaQuery().in(FtbAttendanceDailyRule::getApplyId, ruleMap.keySet()).eq(FtbAttendanceDailyRule::getApplyViewEnable, 1).eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.WORKOVERTIME.getCode()).orderByAsc(FtbAttendanceDailyRule::getInPoint).list();
        if (CollUtil.isEmpty(list)) {
            return;
        }
        Map<String, List<FtbAttendanceDailyRule>> collect1 = list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getApplyId));
        List<FtbAttendanceDailyRule> objects = CollUtil.newArrayList();
        collect1.forEach((applyId, rules) -> {
            FtbAttendanceDailyRule currRule = ruleMap.get(applyId);
            if (Objects.isNull(currRule)) {
                return;
            }
            //找到当前隐藏申请之前的最后数据
            FtbAttendanceDailyRule ftbAttendanceDailyRule1 = rules.stream().filter(rule -> rule.getInPoint().before(currRule.getDay())).reduce((r1, r2) -> r2).orElse(null);
            if (Objects.nonNull(ftbAttendanceDailyRule1)) {
                //修改最后下班打卡时间为隐藏规则的下班打卡时间
                ftbAttendanceDailyRule1.setOutPoint(currRule.getOutPoint());
                objects.add(ftbAttendanceDailyRule1);
            }
        });
        updateBatchById(objects);
    }

    /**
     * 定时执行初始化下个月固定排班
     */
    @Override
    @Transactional
    public void initFixedScheduleRule(String tenantId) {
        Date start = DateUtil.dateAddMonths(DateUtil.getBeginDayOfMonth(), 1);
        Date end = DateUtil.dateAddSeconds(DateUtil.dateAddMonths(start, 1), -1);
        Date date = new Date();
        int day = cn.hutool.core.date.DateUtil.dayOfMonth(date);
        if (day < 20) {
            start = date;
            end = cn.hutool.core.date.DateUtil.endOfMonth(date).toJdkDate();
        } else if (day > 20) {
            start = date;
        }
        List<FtbAttendanceDailyRule> list = lambdaQuery().eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode()).between(FtbAttendanceDailyRule::getDay, start, end).list();
        List<String> enableGroupIds = CollUtil.isNotEmpty(list) ? list.stream().map(FtbAttendanceDailyRule::getGroupId).collect(Collectors.toList()) : CollUtil.newArrayList();
        //读取考勤组数据
        List<AttendanceGroup> attendanceGroupVos = attendanceGroupService.queryDropList();
        if (CollUtil.isEmpty(attendanceGroupVos)) {
            log.error("未查询到考勤组信息");
            return;
        }

        List<String> groupIds = attendanceGroupVos.stream().filter(vo -> !enableGroupIds.contains(vo.getId())).map(AttendanceGroup::getId).collect(Collectors.toList());
        //获取考勤组人员数据
        List<AttendanceGroupUser> users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, null, groupIds);
        if (CollUtil.isEmpty(users)) {
            log.error("未查询到所有考勤组人员信息");
            return;
        }
        FixedHandleDTO build = FixedHandleDTO.builder().groupIds(groupIds).attendanceGroupVos(attendanceGroupVos).users(users).tenantId(tenantId).start(start).end(end).build();
        //规则处理
        fixedHandle(build);
        //处理节日自动排休
        addHolidayDailyRule(build);
    }

    @Override
    public void clearGroupRule(String groupId, List<String> userIds) {
        clearGroupRule(groupId, userIds, new Date(), null);
    }

    @Override
    public void clearGroupRule(String groupId, List<String> userIds, Date departTime, String tenantId) {
        if (StringUtil.isEmpty(groupId) && CollUtil.isEmpty(userIds)) {
            return;
        }
        DateTime end = cn.hutool.core.date.DateUtil.endOfDay(departTime);
        List<FtbAttendanceDailyRule> list = lambdaQuery()
                .and(x -> x.ge(FtbAttendanceDailyRule::getInPoint, end).
                        or(x1 -> x1.gt(FtbAttendanceDailyRule::getDay, end)
                                .in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.HOLIDAYS.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())))
                .notIn(StringUtil.isEmpty(userIds), FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode())
                .eq(StringUtil.isNotEmpty(groupId), FtbAttendanceDailyRule::getGroupId, groupId)
                .in(CollUtil.isNotEmpty(userIds), FtbAttendanceDailyRule::getUserId, userIds)
                .list();
        List<String> ruleIds = list.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList());
        if (CollUtil.isNotEmpty(ruleIds)) {
            removeBatchByIds(ruleIds);
        }
        attendanceRuleNotificationHandle.notificationClearHandle(tenantId, groupId, userIds, end);
        lambdaUpdate().eq(FtbAttendanceDailyRule::getDay, cn.hutool.core.date.DateUtil.beginOfDay(departTime)).notIn(StringUtil.isEmpty(userIds), FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()).eq(StringUtil.isNotEmpty(groupId), FtbAttendanceDailyRule::getGroupId, groupId).in(CollUtil.isNotEmpty(userIds), FtbAttendanceDailyRule::getUserId, userIds).set(FtbAttendanceDailyRule::getApplyViewEnable, 3).update();

    }

    private void clearWithdrawRule(List<FtbAttendanceDailyRule> list, List<FtbAttendanceDailyRule> hisRules) {
        if (CollUtil.isEmpty(list)) {
            return;
        }
        List<DailyRuleResultVo> resultList = CollUtil.newArrayList();
        List<String> ruleIds = CollUtil.newArrayList();
        list.forEach(rule -> {
            boolean isCover = hisRules.stream().anyMatch(hisRule -> Objects.equals(rule.getAttendanceType(), -1) || Objects.equals(rule.getAttendanceType(), 2) || Objects.isNull(hisRule.getInPoint()) || hisRule.getInPoint().compareTo(rule.getOutPoint()) < 0 && hisRule.getOutPoint().compareTo(rule.getInPoint()) > 0);
            if (isCover) {
                ruleIds.add(rule.getId());
            }
            resultList.add(DailyRuleResultVo.successBuild(rule));
        });
        if (CollUtil.isNotEmpty(ruleIds)) {
            removeBatchByIds(ruleIds);
        }
    }

    /**
     * 申请验证的日规则处理
     *
     * @param applyParam 申请参数类
     */
    @Override
    public List<DailyRuleResultVo> applyVerifyHandle(ApplyParam applyParam) throws HandleException {
        List<FtbAttendanceDailyRule> allRules = CollUtil.newArrayList();
        List<String> allRuleIds = CollUtil.newArrayList();
        if (Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP)) {
            applyParam.setLeaveUnit(2);
        }
        List<DailyRuleResultVo> dailyRuleResultVos = applyDailyRuleHandle(applyParam, allRules, allRuleIds);

        allRules.stream().filter(vo -> (StringUtil.equals(vo.getApplyId(), applyParam.getApplyId()) || StringUtil.isEmpty(vo.getApplyId()) && StringUtil.isEmpty(applyParam.getApplyId())) && Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(vo -> {
            OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(allRules, vo.getInPoint(), vo.getOutPoint(), applyParam.getApplyId());
            Assert.notNull(overtimeRuleDetailVo, "未匹配到加班规则");
        });
        return dailyRuleResultVos.stream().filter(vo -> !Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.LEAVE) || StringUtil.isNotEmpty(vo.getFromId())).peek(vo -> vo.setDuration(Objects.requireNonNullElse(vo.getDuration(), BigDecimal.ZERO).divide(BigDecimal.valueOf(60), 1, RoundingMode.HALF_UP))).sorted(Comparator.comparing(DailyRuleResultVo::getInPoint)).collect(Collectors.toList());
    }

    /**
     * 申请的日规则处理
     *
     * @param applyParam 申请参数类
     */
    @Override
    @Transactional
    public String applyDailyRuleHandle(ApplyParam applyParam) throws HandleException {
        List<FtbAttendanceDailyRule> allRules = CollUtil.newArrayList();
        List<String> allRuleIds = CollUtil.newArrayList();
        // 获取当前考勤组的排班锁
        RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), applyParam.getUserId()));
        // 确保锁未被其他操作持有，避免并发执行
        try {
            Assert.isFalse(lock.isLocked() || !lock.tryLock(3, TimeUnit.SECONDS), "存在其他排班或申请操作正在执行，请稍后再试");
        } catch (Exception e) {
            throw new HandleException("存在其他排班或申请操作正在执行，请稍后再试");
        }
        try {
            if (Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP)) {
                applyParam.setLeaveUnit(2);
            }
            List<DailyRuleResultVo> dailyRuleResultVos = applyDailyRuleHandle(applyParam, allRules, allRuleIds);
            List<DailyRuleResultVo> collect = dailyRuleResultVos.stream().filter(result -> result.getType() == 1).collect(Collectors.toList());
            if (CollUtil.isEmpty(collect)) {
                allRules.stream().filter(ru -> Objects.equals(ru.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(ru -> {
                    OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(allRules, ru.getInPoint(), ru.getOutPoint(), ru.getApplyId());
                    ru.setPeriodInfo(Objects.isNull(overtimeRuleDetailVo) ? null : JSON.toJSONString(overtimeRuleDetailVo));
                });
                saveOrUpdateBatch(allRules, allRuleIds);
                List<Date> datesByPeriod = DateDetail.getDatesByPeriod(applyParam.getStart(), cn.hutool.core.date.DateUtil.endOfDay(applyParam.getEnd()));
                attendanceRuleNotificationHandle.notificationHandle(datesByPeriod.stream().map(day -> {
                    DailyRuleResultVo dailyRuleResultVo = new DailyRuleResultVo();
                    dailyRuleResultVo.setType(0);
                    dailyRuleResultVo.setGroupId(applyParam.getGroupId());
                    dailyRuleResultVo.setDate(day);
                    dailyRuleResultVo.setUserId(applyParam.getUserId());
                    return dailyRuleResultVo;
                }).collect(Collectors.toList()), DataSourceContextHolder.getDatasourceId(), Boolean.FALSE, Boolean.TRUE);
            }
            return resultHandle(dailyRuleResultVos).stream().map(DailyRuleResultVo::getFailMsg).filter(StringUtil::isNotEmpty).reduce((msg1, msg2) -> msg1).orElse("");
        } catch (Exception e) {
            throw new HandleException(e.getMessage());
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 申请的日规则处理
     *
     * @param applyParam
     * @param allRules
     * @param allRuleIds
     * @return
     * @throws HandleException
     */
    private List<DailyRuleResultVo> applyDailyRuleHandle(ApplyParam applyParam, List<FtbAttendanceDailyRule> allRules, List<String> allRuleIds) throws HandleException {
        List<String> userIds = CollUtil.newArrayList(applyParam.getUserId());
        List<AttendanceGroupUser> users;
        Date start = Objects.isNull(applyParam.getStart()) ? DateUtil.getDayBegin() : applyParam.getStart();
        Date end = Objects.isNull(applyParam.getEnd()) ? DateUtil.getDayEnd() : applyParam.getEnd();
        LeaveParam leaveParam = BeanUtil.toBean(applyParam, LeaveParam.class);
        if (isLeaveUnitOfDayOrHalfDay(applyParam)) {
            //查询请假时间内所有考勤组
            users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(applyParam.getStart(), Objects.equals(applyParam.getLeaveUnit(), LeaveUnitEnum.HOUR.getCode()) || Objects.isNull(applyParam.getLeaveUnit()) ? applyParam.getEnd() : cn.hutool.core.date.DateUtil.endOfDay(applyParam.getEnd()), userIds, StringUtil.isEmpty(applyParam.getGroupId()) ? null : List.of(applyParam.getGroupId()));
            //如果考勤组数量大于1，判断是否包含当前时间所属考勤组，包含则取当前所属考勤组，不包含直接报错
            if (users.size() > 1) {
                //查询当前时间所属考勤组
                Date date = new Date();
                users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(date, date, userIds, null);
                Assert.isFalse(users.stream().anyMatch(vo -> Objects.equals(vo.getType(), GroupUserTypeEnum.BORROW.getCode())), "申请时间与借调时间冲突！请重新选择");
            }
            end = cn.hutool.core.date.DateUtil.endOfDay(applyParam.getEnd());
        } else {
            users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(applyParam.getStart(), applyParam.getEnd(), userIds, StringUtil.isEmpty(applyParam.getGroupId()) ? null : List.of(applyParam.getGroupId()));
        }
        Assert.isFalse(CollUtil.isEmpty(users), "未查询到所属考勤组");
        Assert.isFalse(users.stream().map(AttendanceGroupUser::getGroupId).distinct().count() > 1, "当前申请时段存在多个考勤组");
        List<String> groupIds = users.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList());
        applyParam.setGroupId(groupIds.get(0));
        List<DailyRuleResultVo> resultList = CollUtil.newArrayList();
        Map<String, List<AttendanceGroupUser>> userMap = users.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId));
        //查询基础配置
        Map<String, AttendanceBaseSetting> enableBaseSetting = attendanceBaseSettingService.getEnableBaseSetting(groupIds);
        //查询原排班
        List<FtbAttendanceDailyRule> hisAllRules = isFullDayQuery(applyParam) ? getDailyRulesByPeriodForStepOut(applyParam.getStart(), applyParam.getEnd(), userIds, groupIds) : getDailyRulesByPeriod(cn.hutool.core.date.DateUtil.beginOfDay(start), cn.hutool.core.date.DateUtil.endOfDay(applyParam.getEnd()), userIds, groupIds);
        //计算半天、全天请假申请开始时间及结束时间
        FtbAttendanceDailyRule min = hisAllRules.stream().min(Comparator.comparing(FtbAttendanceDailyRule::getDay)).orElse(null);
        //非外出、出差无考勤只保存第一天申请数据
        List<Date> dates = DateDetail.getDatesByPeriod(Objects.isNull(min) ? start : min.getDay().after(start) ? start : min.getDay(), !Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME) && !Objects.equals(applyParam.getLeaveUnit(), 1) ? cn.hutool.core.date.DateUtil.endOfDay(end) : end);
        Map<String, List<FtbAttendanceDailyRule>> collect1 = hisAllRules.stream().collect(Collectors.groupingBy((rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId())));
        //遍历组员
        List<FtbAttendanceDailyRule> rules = CollUtil.newArrayList();
        List<FtbAttendanceDailyRule> applyCrossRules = CollUtil.newArrayList();
        Date applyBeginOfDay = cn.hutool.core.date.DateUtil.beginOfDay(applyParam.getStart());
        Date applyEndOfDay = cn.hutool.core.date.DateUtil.endOfDay(applyParam.getEnd());
        groupIds.forEach(id -> {
            List<AttendanceGroupUser> usersGroup = userMap.get(id);
            AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(id);
            BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.valueOf(8) : attendanceBaseSetting.getAttendanceRatio();
            Map<String, List<AttendanceGroupUser>> collect = usersGroup.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));
            collect.forEach((groupUserId, userList) -> {
                applyCrossRules.clear();
                //遍历每天
                for (Date date : dates) {
                    Date endByDay = cn.hutool.core.date.DateUtil.endOfDay(date);
                    AttendanceGroupUser user = userList.stream().filter(user1 -> user1.getUserGroupType() == 2 ? Boolean.TRUE : endByDay.compareTo(user1.getCreatorTime()) >= 0 && (Objects.isNull(user1.getRemoveTime()) ? Boolean.TRUE : date.compareTo(user1.getRemoveTime()) < 0)).max(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).orElse(null);
                    if (Objects.isNull(user)) {
                        continue;
                    }
                    List<FtbAttendanceDailyRule> hisRules = collect1.getOrDefault(DateDetail.getDateTime2Str(date) + id + user.getUserId(), CollUtil.newArrayList());
                    List<String> ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList());
                    //不在申请时间内数据不做下面处理
                    if (!DateDetail.checkTimeBetween(date, applyBeginOfDay, applyEndOfDay)) {
                        allRules.addAll(hisRules);
                        allRuleIds.addAll(ruleIds);
                        continue;
                    }
                    rules.clear();
                    buildDailyRuleForApply(applyParam, date, user, rules, attendanceRatio, leaveParam, applyCrossRules);
                    dailyRuleArrangementHandle(resultList, hisRules, rules);
                    secondmentDailyRulesHandle(resultList, user, hisRules);
                    fillInNailRuleForOnlyStepOut(hisRules);
                    //清除原跨日规则，判断申请日期在今天后，继续将匹配后的规则继续传递到后一天
                    applyCrossRules.clear();
                    if (isApplyCrossDay(applyParam) && endByDay.before(applyParam.getEnd()) && !Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP) && !Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.STEP_OUT)) {
                        applyCrossRules.addAll(hisRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).filter(vo -> StringUtil.equals(vo.getApplyId(), applyParam.getApplyId()) || (StringUtil.isEmpty(vo.getApplyId()) && StringUtil.isEmpty(applyParam.getApplyId()))).collect(Collectors.toList()));
                    }
                    allRules.addAll(hisRules);
                    allRuleIds.addAll(ruleIds);
                }
                setRuleForLateOutLateIn(groupUserId, UserProvider.getUser().getTenantId(), allRules);
            });
        });
        hisAllRules.clear();
        return resultHandle(resultList);
    }

    private void buildDailyRuleForApply(ApplyParam applyParam, Date date, AttendanceGroupUser user, List<FtbAttendanceDailyRule> rules, BigDecimal attendanceRatio, LeaveParam leaveParam, List<FtbAttendanceDailyRule> applyCrossRules) {
        //出差，外出跨日拆分为多日
        if (Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP) || Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.STEP_OUT) && !Objects.equals(applyParam.getLeaveUnit(), 1)) {
            Date startTime = cn.hutool.core.date.DateUtil.beginOfDay(date);
            Date endTime = DateUtil.dateAddSeconds(DateUtil.dateAddDays(startTime, 1), -1);
            buildDailyRule(user, date, rules, 0, null, null, applyParam.getAttendanceType(), applyParam.getApplyId(), startTime, endTime, attendanceRatio, 0, leaveParam);
        } else {
            //是否需要次日不展示
            boolean isTomorrow = CollUtil.isNotEmpty(applyCrossRules) && isApplyCrossDay(applyParam);
            //请假申请,次日使用新day
            if (isTomorrow && Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.LEAVE)) {
                isTomorrow = false;
            }
            if (isTomorrow && Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME)) {
                rules.addAll(applyCrossRules);
                return;
            }
            buildDailyRule(user, isTomorrow ? cn.hutool.core.date.DateUtil.beginOfDay(applyParam.getStart()) : date, rules, 0, null, null, applyParam.getAttendanceType(), applyParam.getApplyId(), applyParam.getStart(), applyParam.getEnd(), applyParam.getValidDuration(), 0, leaveParam);
        }
    }

    private boolean isApplyCrossDay(Date start, Date end) {
        return cn.hutool.core.date.DateUtil.beginOfDay(start).compareTo(cn.hutool.core.date.DateUtil.beginOfDay(end)) != 0;
    }

    private boolean isApplyCrossDay(ApplyParam applyParam) {
        return isApplyCrossDay(applyParam.getStart(), applyParam.getEnd());
    }

    /**
     * 半天假全天班中间分界时间点
     *
     * @param ftbAttendanceDailyRule
     * @return
     */
    private Date getLeaveDate(FtbAttendanceDailyRule ftbAttendanceDailyRule) {
        int ruleMinute = DateDetail.calculateMinuteDiff(ftbAttendanceDailyRule.getInPoint(), ftbAttendanceDailyRule.getOutPoint());
        return cn.hutool.core.date.DateUtil.offsetMinute(ftbAttendanceDailyRule.getInPoint(), ruleMinute / 2);
    }

    /**
     * 判断是否是全天查询
     *
     * @param applyParam 申请参数，包含出勤类型等信息
     * @return 如果是全天查询则返回true，否则返回false
     */
    private boolean isFullDayQuery(ApplyParam applyParam) {
        //外出申请非全天外出
        if (Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.STEP_OUT) && Objects.equals(applyParam.getLeaveUnit(), 1)) {
            return Boolean.FALSE;
        }
        // 如果出勤类型是商务出差或外出，则认为是全天查询
        if (Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP) || Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.STEP_OUT)) {
            return Boolean.TRUE;
        }
        // 如果是按天或半天请假单位，则认为是全天查询
        if (isLeaveUnitOfDayOrHalfDay(applyParam)) {
            return Boolean.TRUE;
        }
        // 默认不是全天查询
        return Boolean.FALSE;
    }

    /**
     * 判断申请的请假类型是否为全天或半天
     *
     * @param applyParam 请假申请参数，包含考勤类型和请假单位等信息
     * @return 如果请假类型为全天或半天则返回true，否则返回false
     */
    private boolean isLeaveUnitOfDayOrHalfDay(ApplyParam applyParam) {
        // 检查考勤类型是否为请假，并且请假单位是否为全天或半天
        return Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.LEAVE) && (Objects.equals(applyParam.getLeaveUnit(), LeaveUnitEnum.DAY.getCode()) || Objects.equals(applyParam.getLeaveUnit(), LeaveUnitEnum.HALF_DAY.getCode()));
    }

    /**
     * 借调申请日规则处理
     *
     * @param userIds       借调用户id集合
     * @param fromGroupId   原考勤组id
     * @param toGroupId     借调考勤组id
     * @param start         开始时间
     * @param end           结束时间
     * @param departureTime 离岗时间
     * @param backTime      回岗时间
     * @param tenantId
     */
    @Override
    public List<DailyRuleResultVo> secondmentDailyRuleHandle(List<String> userIds, String fromGroupId, String toGroupId, Date start, Date end, Date departureTime, Date backTime, String tenantId) throws HandleException {
        List<String> groupIds = CollUtil.newArrayList(fromGroupId, toGroupId);
        List<AttendanceGroup> attendanceGroupVos = attendanceGroupService.queryListByIds(groupIds);
        if (CollUtil.isEmpty(attendanceGroupVos)) {
            log.error("未查询到考勤组信息,{}", JSON.toJSONString(groupIds));
            throw new HandleException("未查询到考勤组信息");
        }
        List<AttendanceGroupUser> users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(null, null, userIds, CollUtil.newArrayList(fromGroupId));
        users.forEach(user -> {
            List<SecondmentDateVo> secondmentDateVos = StringUtil.isEmpty(user.getTimeJson()) ? CollUtil.newArrayList() : JsonUtil.getJsonToList(user.getTimeJson(), SecondmentDateVo.class);
            SecondmentDateVo secondmentDateVo = new SecondmentDateVo();
            secondmentDateVo.setStartTime(departureTime);
            secondmentDateVo.setEndTime(backTime);
            secondmentDateVos.add(secondmentDateVo);
            user.setTimeJson(JSON.toJSONString(secondmentDateVos));
        });
        for (String userId : userIds) {
            AttendanceGroupUser attendanceGroupUser = new AttendanceGroupUser();
            attendanceGroupUser.setGroupId(toGroupId);
            attendanceGroupUser.setUserId(userId);
            attendanceGroupUser.setType(2);
            attendanceGroupUser.setUserGroupType(2);
            List<SecondmentDateVo> secondmentDateVos = CollUtil.newArrayList();
            SecondmentDateVo secondmentDateVo = new SecondmentDateVo();
            secondmentDateVo.setStartTime(start);
            secondmentDateVo.setEndTime(end);
            secondmentDateVos.add(secondmentDateVo);
            attendanceGroupUser.setTimeJson(JSON.toJSONString(secondmentDateVos));
            users.add(attendanceGroupUser);
        }
        if (CollUtil.isEmpty(users)) {
            log.error("未查询到所有考勤组人员信息,{}", JSON.toJSONString(userIds));
            throw new HandleException("未查询到所有考勤组人员信息");
        }
        List<DailyRuleResultVo> resultList = CollUtil.newArrayList();
        //根据考勤组id查询考勤组班制配置
        Map<String, AttendanceShiftSettingVo> enableShiftSetting = attendanceShiftSettingService.getEnableShiftSetting(groupIds, attendanceGroupService.queryDropList());
        if (CollUtil.isEmpty(enableShiftSetting)) {
            log.error("未查询到所有考勤组班制配置");
            return resultList;
        }
        List<Date> dates = DateDetail.getDatesByPeriod(start, end);
        Map<String, List<AttendanceGroupUser>> collect1 = users.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId));
        //获取每日出勤规则
        List<FtbAttendanceDailyRule> dailyRulesByPeriod = getDailyRulesByPeriod(departureTime, backTime, userIds, groupIds);
        Map<String, List<FtbAttendanceDailyRule>> dailyRuleMap = CollUtil.isEmpty(dailyRulesByPeriod) ? Maps.newHashMap() : dailyRulesByPeriod.stream().collect(Collectors.groupingBy(rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId()));
        Map<String, AttendanceBaseSetting> enableBaseSetting = attendanceBaseSettingService.getEnableBaseSetting(groupIds, attendanceGroupVos);
        List<FtbAttendanceDailyRule> allRules = CollUtil.newArrayList();
        List<String> allRuleIds = CollUtil.newArrayList();
        attendanceGroupVos.forEach(vo -> {
            AttendanceShiftSettingVo attendanceShiftSettingVo = enableShiftSetting.get(vo.getId());
            if (Objects.isNull(attendanceShiftSettingVo)) {
                log.error("未查询到考勤组[{}]班制时段数据", vo.getId());
                return;
            }
            List<AttendanceGroupUser> attendanceGroupUsers = collect1.get(vo.getId());
            if (CollUtil.isEmpty(attendanceGroupUsers)) {
                log.error("未查询到当前考勤组人员信息,{}", vo.getId());
                return;
            }
            AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(vo.getId());
            Map<String, List<AttendanceGroupUser>> userMap = attendanceGroupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));
            //遍历组员
            userMap.forEach((userId, userList) -> {
                //遍历每天
                for (Date date : dates) {
                    Date endByDay = cn.hutool.core.date.DateUtil.endOfDay(date);
                    AttendanceGroupUser user = userList.stream().filter(user1 -> user1.getUserGroupType() == 2 ? Boolean.TRUE : Objects.isNull(user1.getCreatorTime()) ? Boolean.TRUE : endByDay.compareTo(user1.getCreatorTime()) >= 0 && (Objects.isNull(user1.getRemoveTime()) ? Boolean.TRUE : date.compareTo(user1.getRemoveTime()) < 0)).reduce((r1, r2) -> r2).orElse(null);
                    if (Objects.isNull(user)) {
                        continue;
                    }
                    List<FtbAttendanceDailyRule> hisRules = dailyRuleMap.getOrDefault(DateDetail.getDateTime2Str(date) + user.getGroupId() + user.getUserId(), CollUtil.newArrayList());
                    List<String> ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList());
                    //新增借调借调组固定班日规则生成
                    afterSecondmentRulesHandle(attendanceShiftSettingVo, date, user, hisRules, attendanceBaseSetting);
                    secondmentDailyRulesHandle(resultList, user, hisRules);
                    hisRules.stream().filter(ru -> Objects.equals(ru.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(ru -> {
                        OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(hisRules, ru.getInPoint(), ru.getOutPoint(), ru.getApplyId());
                        ru.setPeriodInfo(Objects.isNull(overtimeRuleDetailVo) ? null : JSON.toJSONString(overtimeRuleDetailVo));
                    });
                    allRules.addAll(hisRules);
                    allRuleIds.addAll(ruleIds);
                }
                setRuleForLateOutLateIn(userId, tenantId, allRules);
            });
        });
        saveOrUpdateBatch(allRules, allRuleIds);
        attendanceRuleNotificationHandle.notificationHandle(resultList, tenantId, Boolean.TRUE, Boolean.TRUE);
        return resultHandle(resultList);
    }

    @Override
    public List<FtbAttendanceDailyRule> getAttendanceDayRulesForStatistic(String groupId, String userId, Date day) {
        return lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(StringUtil.isNotEmpty(userId), FtbAttendanceDailyRule::getUserId, userId).eq(StringUtil.isNotEmpty(groupId), FtbAttendanceDailyRule::getGroupId, groupId).notIn(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode()).eq(FtbAttendanceDailyRule::getDay, day).orderByAsc(FtbAttendanceDailyRule::getApplyStart).list();
    }


    @Override
    public DayClockRange getDayClockRange(String userId, Date day) {
        //查询开始时间
        DayClockRange clockRange = DayClockRange.builder().endTimeIsOpenClose(Boolean.TRUE).startTimeIsOpenClose(Boolean.TRUE).build();
        //查询昨天最后一个班次
        Date yesterday = DateUtil.dateAddDays(day, -1);
        FtbAttendanceDailyRule yesterdayDailyRule = lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getUserId, userId).notIn(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDay, yesterday).gt(FtbAttendanceDailyRule::getOutPoint, DateUtil.getDayEndTime(yesterday)).orderByDesc(FtbAttendanceDailyRule::getOutPoint).last("limit 1").one();
        if (Objects.nonNull(yesterdayDailyRule)) {
            //查询当天班次的打卡结果
            AttendanceClockInResult attendanceClockInResult = attendanceClockInResultMapper.selectOne(new LambdaQueryWrapper<AttendanceClockInResult>().eq(AttendanceClockInResult::getRuleId, yesterdayDailyRule.getId()).eq(AttendanceClockInResult::getClockInType, ConstantUtil.OFF_WORK).eq(AttendanceClockInResult::getDeleteMark, Boolean.FALSE).last("limit 1"));
            if (Objects.nonNull(attendanceClockInResult) && StringUtil.isNotEmpty(attendanceClockInResult.getClockInId())) {
                FtbAttendanceClockIn clockIn = attendanceClockInMapper.selectById(attendanceClockInResult.getClockInId());
                clockRange.setStartTime(Objects.nonNull(clockIn) ? clockIn.getClockInTime() : attendanceClockInResult.getEffectiveTime());
                clockRange.setEndTimeIsOpenClose(Boolean.FALSE);
            } else {
                clockRange.setStartTime(DateUtil.getDayStartTime(day));
            }
        } else {
            clockRange.setStartTime(DateUtil.getDayStartTime(day));
        }
        //查询结束时间
        FtbAttendanceDailyRule dayDailyRule = lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getUserId, userId).notIn(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDay, day).orderByDesc(FtbAttendanceDailyRule::getOutPoint).last("limit 1").one();
        if (Objects.nonNull(dayDailyRule)) {
            //查询当天班次的打卡结果
            AttendanceClockInResult attendanceClockInResult = attendanceClockInResultMapper.selectOne(new LambdaQueryWrapper<AttendanceClockInResult>().eq(AttendanceClockInResult::getRuleId, dayDailyRule.getId()).eq(AttendanceClockInResult::getClockInType, ConstantUtil.OFF_WORK).eq(AttendanceClockInResult::getDeleteMark, Boolean.FALSE).last("limit 1"));
            if (Objects.nonNull(attendanceClockInResult) && StringUtil.isNotEmpty(attendanceClockInResult.getClockInId())) {
                FtbAttendanceClockIn clockIn = attendanceClockInMapper.selectById(attendanceClockInResult.getClockInId());
                clockRange.setEndTime(Objects.nonNull(clockIn) ? clockIn.getClockInTime() : attendanceClockInResult.getEffectiveTime());
                clockRange.setEndTimeIsOpenClose(Boolean.FALSE);
            } else {
                clockRange.setEndTime(DateUtil.getDayEndTime(day));
            }
        } else {
            clockRange.setEndTime(DateUtil.getDayEndTime(day));
        }
        return clockRange;
    }

    @Override
    public Date getDayEndRuleDeletionDate(String userId, Date day) {
        //查询当天最后一个班次
        FtbAttendanceDailyRule dailyEndRule = lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getUserId, userId).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()).eq(FtbAttendanceDailyRule::getDay, day).orderByDesc(FtbAttendanceDailyRule::getOutPoint).last("limit 1").one();
        //判断最后一个班次是不是加班
        return Objects.nonNull(dailyEndRule) ? dailyEndRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode()) ? dailyEndRule.getOutPoint() : dailyEndRule.getOutLackPoint() : null;
    }

    @Override
    public boolean userIsScheduling(String userId) {
        return lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getUserId, userId).notIn(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode()).count() > 0;
    }

    @Override
    public List<String> userIsSchedulingOrdinary(List<String> organizeIds) {
        List<AttendanceGroup> byOrgId = attendanceGroupService.getByOrgIds(organizeIds);
        if (CollUtil.isEmpty(byOrgId)) {
            return List.of();
        }
        List<String> collect = byOrgId.stream().map(AttendanceGroup::getId).collect(Collectors.toList());
        List<FtbAttendanceDailyRule> list = lambdaQuery().select(FtbAttendanceDailyRule::getUserId).in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getDay, cn.hutool.core.date.DateUtil.beginOfDay(new Date())).in(FtbAttendanceDailyRule::getGroupId, collect).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()).list();
        return list.stream().map(FtbAttendanceDailyRule::getUserId).collect(Collectors.toList());
    }

    @Override
    public boolean hasRuleByUserIdAndTime(String userId, Date start, Date end) {
        return lambdaQuery().eq(FtbAttendanceDailyRule::getUserId, userId).isNotNull(FtbAttendanceDailyRule::getInPoint).le(FtbAttendanceDailyRule::getInPoint, end).ge(FtbAttendanceDailyRule::getOutPoint, start).count() > 0;
    }

    @Override
    public List<OutOrBusApproveVo> getUserDayBusAndOutInfo(String userId, String groupId, Date day, List<AttendanceTypeEnum> typeEnumList) {
        List<Integer> typeEnumIdList = typeEnumList.stream().map(AttendanceTypeEnum::getCode).collect(Collectors.toList());
        List<FtbAttendanceDailyRule> dailyEndRuleList = lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, CollUtil.addAll(typeEnumIdList, 0)).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getUserId, userId).in(FtbAttendanceDailyRule::getAttendanceType, typeEnumIdList).eq(FtbAttendanceDailyRule::getDay, day).list();
        List<OutOrBusApproveVo> outOrBusApproveVoList = CollUtil.newArrayList();
        if (CollUtil.isEmpty(dailyEndRuleList)) {
            return outOrBusApproveVoList;
        }
        if (dailyEndRuleList.stream().anyMatch(dailyEndRule -> dailyEndRule.getAttendanceType().equals(AttendanceTypeEnum.STEP_OUT.getCode()))) {
            List<AttendanceGoOutApprove> outApproveList = this.goOutApproveService.getBatchByIds(List.of(groupId), dailyEndRuleList.stream().map(FtbAttendanceDailyRule::getApplyId).distinct().collect(Collectors.toList()));
            for (AttendanceGoOutApprove approve : outApproveList) {
                OutOrBusApproveVo outOrBusApproveVo = OutOrBusApproveVo.builder().id(approve.getId()).type(1).startTime(approve.getStartTime()).endTime(approve.getEndTime()).unit(approve.getUnit()).build();
                if (approve.getUnit().equals(LeaveUnitEnum.DAY.getCode()) && approve.getDayNum().compareTo(BigDecimal.ONE) > 0) {
                    outOrBusApproveVo.setDayNum(BigDecimal.ONE);
                } else {
                    outOrBusApproveVo.setDayNum(approve.getDayNum());
                }
                outOrBusApproveVoList.add(outOrBusApproveVo);
            }
        }
        if (dailyEndRuleList.stream().anyMatch(dailyEndRule -> dailyEndRule.getAttendanceType().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()))) {
            List<AttendanceBusinessTripApprove> busApproveList = this.businessTripApproveService.getBatchByIds(List.of(groupId), dailyEndRuleList.stream().map(FtbAttendanceDailyRule::getApplyId).distinct().collect(Collectors.toList()));
            for (AttendanceBusinessTripApprove entry : busApproveList) {
                OutOrBusApproveVo outOrBusApproveVo = OutOrBusApproveVo.builder().id(entry.getId()).type(2).startTime(entry.getStartTime()).endTime(entry.getEndTime()).build();
                if (entry.getDayNum().compareTo(BigDecimal.ONE) > 0) {
                    outOrBusApproveVo.setDayNum(BigDecimal.ONE);
                } else {
                    outOrBusApproveVo.setDayNum(entry.getDayNum());
                }
                outOrBusApproveVoList.add(outOrBusApproveVo);
            }
        }
        return outOrBusApproveVoList;
    }

    @Override
    public Map<String, Integer> getUserBusAndOutCount(List<String> userIds, List<String> groupIdList, Date startDate, Date endDate, AttendanceTypeEnum businessTrip) {
        List<FtbAttendanceDailyRule> dailyEndRuleList = lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, List.of(businessTrip.getCode(), 0)).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).in(FtbAttendanceDailyRule::getUserId, userIds).in(StringUtil.isNotEmpty(groupIdList), FtbAttendanceDailyRule::getGroupId, groupIdList).eq(FtbAttendanceDailyRule::getAttendanceType, businessTrip.getCode()).ge(FtbAttendanceDailyRule::getDay, startDate).le(FtbAttendanceDailyRule::getDay, endDate).list();
        Map<String, List<FtbAttendanceDailyRule>> busApproveMap = CollUtil.isNotEmpty(dailyEndRuleList) ? dailyEndRuleList.stream().collect(Collectors.groupingBy(record -> record.getUserId() + "&" + record.getGroupId())) : Maps.newHashMap();
        Map<String, Integer> resultMap = Maps.newHashMap();
        busApproveMap.forEach((key, value) -> resultMap.put(key, Math.toIntExact(value.stream().map(FtbAttendanceDailyRule::getApplyId).distinct().count())));
        return resultMap;
    }

    /**
     * 申请规则的异常结果处理
     *
     * @param resultList
     * @return
     */
    private List<DailyRuleResultVo> resultHandle(List<DailyRuleResultVo> resultList) {
        if (CollUtil.isEmpty(resultList)) {
            return resultList;
        }
        List<String> userIds = resultList.stream().map(DailyRuleResultVo::getUserId).collect(Collectors.toList());
        List<UserEntity> infoByIds = userApi.getInfoByIds(userIds);
        Map<String, String> userMap = CollUtil.isEmpty(infoByIds) ? Maps.newHashMap() : infoByIds.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getRealName));
        resultList.forEach(result -> {
            if (result.getType() == 1) {
                result.setFailMsg(AttendanceConstant.getApplyResult(userMap.get(result.getUserId()), AttendanceTypeEnum.getMsg(result.getFromType())));
            }
        });
        return resultList;
    }

    /**
     * 排班规则的异常结果处理
     *
     * @param resultList
     * @return
     */
    private List<DailyRuleResultVo> resultSchedulingHandle(List<DailyRuleResultVo> resultList) {
        List<String> userIds = resultList.stream().filter(result -> Objects.equals(result.getType(), 1)).map(DailyRuleResultVo::getUserId).collect(Collectors.toList());
        if (CollUtil.isEmpty(userIds)) {
            return resultList;
        }
        List<PartUserInfoVo> infoByIds = userApi.getStaffRosterListInfoByIds(userIds);
        Map<String, String> userMap = infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, PartUserInfoVo::getRealName));
        resultList.forEach(result -> {
            if (result.getType() == 1) {
                result.setFailMsg(AttendanceConstant.getSchedulingResult(userMap.get(result.getUserId()), result.getDate(), AttendanceTypeEnum.getMsg(result.getFromType()), AttendanceTypeEnum.getMsg(result.getToType())));
            }
        });
        return resultList;
    }

    /**
     * 固定班规则处理
     *
     * @param fixedHandleDTO
     */
    private void fixedHandle(FixedHandleDTO fixedHandleDTO) {
        List<DailyRuleResultVo> resultList = CollUtil.newArrayList();
        //根据考勤组id查询考勤组班制配置
        List<String> groupIds = fixedHandleDTO.getUsers().stream().map(AttendanceGroupUser::getGroupId).collect(Collectors.toList());
        if (CollUtil.isEmpty(fixedHandleDTO.getShiftSettingMap())) {
            Map<String, AttendanceShiftSettingVo> enableShiftSetting = attendanceShiftSettingService.getEnableShiftSetting(groupIds, fixedHandleDTO.getAttendanceGroupVos());
            if (CollUtil.isEmpty(enableShiftSetting)) {
                log.error("未查询到所有考勤组班制配置");
                return;
            }
            fixedHandleDTO.setShiftSettingMap(enableShiftSetting);
        }
        List<Date> dates = DateDetail.getDatesByPeriod(fixedHandleDTO.getStart(), fixedHandleDTO.getEnd());
        Map<String, List<AttendanceGroupUser>> collect1 = fixedHandleDTO.getUsers().stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId));
        List<String> collect = fixedHandleDTO.getUsers().stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList());
        //获取每日出勤规则
        List<FtbAttendanceDailyRule> dailyRulesByPeriod = getDailyRulesByPeriod(fixedHandleDTO.getStart(), fixedHandleDTO.getEnd(), collect, groupIds);
        //过滤昨日排班数据，次日下班
        Date date1 = new Date();
        dailyRulesByPeriod = dailyRulesByPeriod.stream().filter(rule -> !Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) || rule.getInPoint().after(fixedHandleDTO.getStart())).collect(Collectors.toList());
        List<FtbAttendanceDailyRule> collect4 = dailyRulesByPeriod.stream().filter(rule -> Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) && rule.getInPoint().before(fixedHandleDTO.getStart()) && rule.getOutPoint().after(date1)).collect(Collectors.toList());
        Assert.isFalse(CollUtil.isNotEmpty(collect4), "该班次上班时间与前一天下班时间有冲突！");
        Map<String, List<FtbAttendanceDailyRule>> dailyRuleMap = CollUtil.isEmpty(dailyRulesByPeriod) ? Maps.newHashMap() : dailyRulesByPeriod.stream().collect(Collectors.groupingBy(rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId()));
        Map<String, AttendanceBaseSetting> enableBaseSetting = CollUtil.isNotEmpty(fixedHandleDTO.getEnableBaseSetting()) ? fixedHandleDTO.getEnableBaseSetting() : attendanceBaseSettingService.getEnableBaseSetting(groupIds, fixedHandleDTO.getAttendanceGroupVos());
        if (CollUtil.isEmpty(fixedHandleDTO.getEnableBaseSetting())) {
            fixedHandleDTO.setEnableBaseSetting(enableBaseSetting);
        }
        List<AttendanceGroup> collect5 = fixedHandleDTO.getAttendanceGroupVos().stream().filter(group -> groupIds.contains(group.getId())).collect(Collectors.toList());
        List<FtbAttendanceDailyRule> allRules = CollUtil.newArrayList();
        List<String> allRuleIds = CollUtil.newArrayList();
        List<FtbAttendanceDailyRule> list = lambdaQuery().eq(FtbAttendanceDailyRule::getApplyViewEnable, 3).list();
        Map<String, List<FtbAttendanceDailyRule>> collect3 = list.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF)));
        Map<String, List<FtbAttendanceDailyRule>> applyCrossRules = Maps.newHashMap();
        List<FtbAttendanceDailyRule> periodConflictRules = CollUtil.newArrayList();
        collect5.forEach(vo -> {
            AttendanceShiftSettingVo attendanceShiftSettingVo = fixedHandleDTO.getShiftSettingMap().get(vo.getId());
            if (Objects.isNull(attendanceShiftSettingVo)) {
                log.error("未查询到考勤组[{}]班制时段数据", vo.getId());
                return;
            }
            if (!Objects.equals(1, attendanceShiftSettingVo.getSystemType())) {
                log.error("当请求为固定班规则生成时，过滤非固定班配置{}", vo.getId());
                return;
            }
            List<AttendanceGroupUser> attendanceGroupUsers = collect1.get(vo.getId());
            if (CollUtil.isEmpty(attendanceGroupUsers)) {
                log.error("未查询到当前考勤组人员信息,{}", vo.getId());
                return;
            }
            AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(vo.getId());
            BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.ZERO : attendanceBaseSetting.getAttendanceRatio();
            List<FixedClassShiftVo> fixedClassShiftVos = attendanceShiftSettingVo.getFixedPeriods();
            if (CollUtil.isEmpty(fixedClassShiftVos)) {
                return;
            }
            Map<Integer, FixedClassShiftVo> collect2 = fixedClassShiftVos.stream().collect(Collectors.toMap(FixedClassShiftVo::getNum, fixedClassShift -> fixedClassShift));
            Map<String, List<AttendanceGroupUser>> userMap = attendanceGroupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));
            //遍历组员
            userMap.forEach((userId, userList) -> {
                periodConflictRules.clear();
                applyCrossRules.clear();
                //遍历每天
                for (Date date : dates) {
                    Date endByDay = cn.hutool.core.date.DateUtil.endOfDay(date);
                    AttendanceGroupUser user = userList.stream().filter(user1 -> Objects.equals(user1.getUserGroupType(), 2) || (endByDay.compareTo(user1.getCreatorTime()) >= 0 && (Objects.isNull(user1.getRemoveTime()) ? Boolean.TRUE : date.compareTo(user1.getRemoveTime()) < 0))).reduce((r1, r2) -> r2).orElse(null);
                    if (Objects.isNull(user)) {
                        continue;
                    }
                    List<FtbAttendanceDailyRule> hisRules = dailyRuleMap.getOrDefault(DateDetail.getDateTime2Str(date) + user.getGroupId() + user.getUserId(), CollUtil.newArrayList());
                    //历史规则如果存在划线排班，则过滤
                    if (hisRules.stream().anyMatch(rule -> Objects.equals(rule.getFixedMark(), 2))) {
                        return;
                    }
                    //将夸日的申请规则添加到当前日的规则中方便计算
                    applyCrossRulesAdd(hisRules, applyCrossRules, endByDay, date, allRules);
                    List<String> ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList());
                    List<FtbAttendanceDailyRule> rules = CollUtil.newArrayList();
                    int currentWeekNumber = DateDetail.getCurrentWeekNumber(date);
                    FixedClassShiftVo fixedClassShiftVo = collect2.get(currentWeekNumber);
                    //未设置固定班次过滤
                    if (Objects.isNull(fixedClassShiftVo)) {
                        fixedClassShiftVo = FixedClassShiftVo.builder().shiftNameVos(CollUtil.newArrayList(new ShiftNameVo())).build();
                    }
                    List<ShiftNameVo> shiftNameVos = fixedClassShiftVo.getShiftNameVos();
                    ShiftNameVo ordShift = shiftNameVos.stream().filter(shiftNameVo -> Objects.equals(shiftNameVo.getAttendanceType(), 2)).findFirst().orElse(null);
                    int size = shiftNameVos.size();
                    shiftNameVos.forEach(shiftName -> {
                        if (Objects.equals(shiftName.getType(), 1)) {
                            buildDailyRule(user, date, rules, 0, Objects.nonNull(ordShift) ? ordShift.getPeriods() : null, attendanceShiftSettingVo, AttendanceTypeEnum.REST, attendanceRatio, 1);
                            return;
                        }
                        if (Objects.equals(shiftName.getType(), 2)) {
                            if (size < 2) {
                                shiftName.getPeriods().forEach(period -> buildDailyRule(user, date, rules, 0, CollUtil.newArrayList(period), attendanceShiftSettingVo, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 1));
                                return;
                            }
                            buildDailyRule(user, date, rules, 0, shiftName.getPeriods(), attendanceShiftSettingVo, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 1);
                            return;
                        }
                        buildDailyRule(user, date, rules, 0, shiftName.getPeriods(), attendanceShiftSettingVo, AttendanceTypeEnum.CLEAR, BigDecimal.ZERO, 1);
                    });
                    if (CollUtil.isEmpty(rules)) {
                        periodConflictRules.addAll(hisRules);
                        continue;
                    }
                    dailyRuleArrangementHandle(resultList, hisRules, rules);
                    secondmentDailyRulesHandle(resultList, user, hisRules);
                    periodConflictRules.addAll(hisRules);
                    hisRules.stream().filter(ru -> Objects.equals(ru.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(ru -> {
                        OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(hisRules, ru.getInPoint(), ru.getOutPoint(), ru.getApplyId());
                        ru.setPeriodInfo(Objects.isNull(overtimeRuleDetailVo) ? null : JSON.toJSONString(overtimeRuleDetailVo));
                    });
                    //将夸日的申请规则添加到当前日的规则中方便计算
                    applyCrossRulesAddNotCurrent(hisRules, applyCrossRules, endByDay, date);
                    allRules.addAll(hisRules);
                    allRuleIds.addAll(ruleIds);
                    clearWithdrawRule(collect3.getOrDefault(user.getUserId() + DateDetail.getDate2Str(date, DateDetail.DF), CollUtil.newArrayList()), hisRules);
                    //判断时段是否冲突
                    Assert.isTrue(fixedPeriodConflict(periodConflictRules), "所选班次时段存在冲突！");
                }
                setRuleForLateOutLateIn(userId, fixedHandleDTO.getTenantId(), allRules);
            });
        });
        saveOrUpdateBatch(allRules, allRuleIds);
        attendanceRuleNotificationHandle.sendSystemNotice(resultList, fixedHandleDTO.getTenantId());
        attendanceRuleNotificationHandle.notificationHandle(resultList, fixedHandleDTO.getTenantId());
    }

    /**
     * 借调的日规则处理
     *
     * @param resultList              处理结果集合
     * @param user                    考勤组用户信息
     * @param ftbAttendanceDailyRules 历史考勤规则
     */
    private void secondmentDailyRulesHandle(List<DailyRuleResultVo> resultList, AttendanceGroupUser user, List<FtbAttendanceDailyRule> ftbAttendanceDailyRules) {
        List<SecondmentDateVo> secondmentDateVos = StringUtil.isEmpty(user.getTimeJson()) ? null : JsonUtil.getJsonToList(user.getTimeJson(), SecondmentDateVo.class);
        //处理借调类型
        if (CollUtil.isEmpty(secondmentDateVos)) {
            ftbAttendanceDailyRules.forEach(rule -> {
                rule.setSelfGroup(SecondmentTypeEnum.NOT.getCode());
            });
            return;
        }
        //历史借调
        hisSecondmentDailyRulesHandle(resultList, user, ftbAttendanceDailyRules, secondmentDateVos);
    }

    /**
     * 借调的规则生成处理
     *
     * @param resultList
     * @param user
     * @param ftbAttendanceDailyRules
     * @param secondmentDateVos
     */
    private void hisSecondmentDailyRulesHandle(List<DailyRuleResultVo> resultList, AttendanceGroupUser user, List<FtbAttendanceDailyRule> ftbAttendanceDailyRules, List<SecondmentDateVo> secondmentDateVos) {
        Iterator<FtbAttendanceDailyRule> iterator = ftbAttendanceDailyRules.iterator();
        Map<Integer, FtbAttendanceDailyRule> collect1 = ftbAttendanceDailyRules.stream().filter(rule -> Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) && Objects.equals(rule.getApplyUnit(), LeaveUnitEnum.DAY.getCode())).collect(Collectors.toMap(FtbAttendanceDailyRule::getSchedulesType, Function.identity(), (e1, e2) -> e1));
        List<FtbAttendanceDailyRule> rules = CollUtil.newArrayList();
        while (iterator.hasNext()) {
            FtbAttendanceDailyRule hisRule = iterator.next();
            Date start = Objects.nonNull(hisRule.getInPoint()) ? hisRule.getInPoint() : hisRule.getDay();
            Date end = Objects.nonNull(hisRule.getOutPoint()) ? hisRule.getOutPoint() : cn.hutool.core.date.DateUtil.endOfDay(hisRule.getDay());
            if (!Objects.equals(user.getUserGroupType(), 2) && (Objects.isNull(user.getCreatorTime()) ? Boolean.FALSE : end.compareTo(user.getCreatorTime()) < 0 || (Objects.isNull(user.getRemoveTime()) ? Boolean.FALSE : user.getRemoveTime().compareTo(start) < 0))) {
                continue;
            }
            if (Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.STEP_OUT.getCode())) {
                continue;
            }
            secondmentRuleHandle(resultList, user, secondmentDateVos, hisRule.getDay(), cn.hutool.core.date.DateUtil.endOfDay(hisRule.getDay()), hisRule, iterator, rules, collect1);
        }
        ftbAttendanceDailyRules.addAll(rules);
        //当外出、出差标识的排次数据都被借调覆盖时，恢复外出、出差申请记录展示标识
        List<FtbAttendanceDailyRule> collect = ftbAttendanceDailyRules.stream().filter(hisRule -> !Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) && !Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.STEP_OUT.getCode()) || !Objects.equals(hisRule.getApplyViewEnable(), 0)).collect(Collectors.toList());
        List<FtbAttendanceDailyRule> collect2 = ftbAttendanceDailyRules.stream().filter(hisRule -> Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.STEP_OUT.getCode()) && Objects.equals(hisRule.getApplyViewEnable(), AttendanceTypeEnum.STEP_OUT.getCode())).collect(Collectors.toList());
        if (CollUtil.isNotEmpty(collect2)) {
            List<FtbAttendanceDailyRule> collect3 = ftbAttendanceDailyRules.stream().filter(hisRule -> !collect2.contains(hisRule) && !Objects.equals(hisRule.getApplyViewEnable(), AttendanceTypeEnum.STEP_OUT.getCode())).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(collect3)) {
                FtbAttendanceDailyRule ftbAttendanceDailyRule = collect2.stream().findFirst().orElse(null);
                collect2.forEach(rule -> rule.setApplyViewEnable(0));
                ftbAttendanceDailyRules.add(buildRuleForNail(ftbAttendanceDailyRule.getGroupId(), ftbAttendanceDailyRule.getUserId(), ftbAttendanceDailyRule.getDay(), ftbAttendanceDailyRule.getAttendanceType(), AttendanceTypeEnum.CLEAR));
            }
        }
        if (CollUtil.isEmpty(collect) && CollUtil.isNotEmpty(ftbAttendanceDailyRules)) {
            FtbAttendanceDailyRule ftbAttendanceDailyRule = ftbAttendanceDailyRules.stream().findFirst().orElse(null);
            ftbAttendanceDailyRules.add(buildRuleForNail(ftbAttendanceDailyRule.getGroupId(), ftbAttendanceDailyRule.getUserId(), ftbAttendanceDailyRule.getDay(), ftbAttendanceDailyRule.getAttendanceType(), AttendanceTypeEnum.CLEAR));
        }
    }

    private void secondmentRuleHandle(List<DailyRuleResultVo> resultList, AttendanceGroupUser user, List<SecondmentDateVo> secondmentDateVos, Date start, Date end, FtbAttendanceDailyRule hisRule, Iterator<FtbAttendanceDailyRule> iterator, List<FtbAttendanceDailyRule> rules, Map<Integer, FtbAttendanceDailyRule> collect1) {
        SecondmentDateVo secondmentDateVo;
        secondmentDateVos = BeanUtil.copyToList(secondmentDateVos, SecondmentDateVo.class);
        while (Objects.nonNull(secondmentDateVo = AttendanceGroupUserStatusUtil.getSecondmentType(secondmentDateVos, start, end)) && !hisRule.getIsDelete()) {
            secondmentDateVos.remove(secondmentDateVo);
            //被借调组
            if (SecondmentTypeEnum.FROM.getCode().equals(user.getUserGroupType())) {
                hisRule.setSelfGroup(SecondmentTypeEnum.FROM.getCode());
                //调休
                if (Objects.isNull(hisRule.getInPoint()) && Objects.equals(AttendanceTypeEnum.REST.getCode(), hisRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.DEFAULT.getCode(), hisRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), hisRule.getAttendanceType())) {
                    DateTime endOfDay = cn.hutool.core.date.DateUtil.endOfDay(hisRule.getDay());
                    if (endOfDay.compareTo(secondmentDateVo.getEndTime()) <= 0 && secondmentDateVo.getStartTime().compareTo(hisRule.getDay()) <= 0) {
                        log.error("被借调组，全天调休规则被借调完全覆盖，直接删除{}", JSON.toJSONString(hisRule));
                        iterator.remove();
                        hisRule.delete();
                        return;
                    }
                    log.error("被借调组，全天调休规则直接保存{}", JSON.toJSONString(hisRule));
                    continue;
                }
                if (secondmentDateVo.getStartTime().compareTo(hisRule.getOutPoint()) == 0 || secondmentDateVo.getEndTime().compareTo(hisRule.getInPoint()) == 0) {
                    if (secondmentDateVo.getStartTime().compareTo(hisRule.getOutPoint()) == 0) {
                        hisRule.setOutUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode());
                    }
                    if (secondmentDateVo.getEndTime().compareTo(hisRule.getInPoint()) == 0) {
                        hisRule.setInUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode());
                    }
                    continue;
                }
                //借调处在班次当中
                if (DateDetail.checkTimeBetween(secondmentDateVo.getStartTime(), hisRule.getInPoint(), hisRule.getOutPoint()) && DateDetail.checkTimeBetween(secondmentDateVo.getEndTime(), hisRule.getInPoint(), hisRule.getOutPoint()) && secondmentDateVo.getStartTime().compareTo(hisRule.getInPoint()) != 0 && secondmentDateVo.getEndTime().compareTo(hisRule.getOutPoint()) != 0) {
                    FtbAttendanceDailyRule hisRule2 = BeanUtil.toBean(hisRule, FtbAttendanceDailyRule.class);
                    hisRule.setEarlyPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getEarlyPoint(), secondmentDateVo.getStartTime()));
                    hisRule.setOutLackPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getOutLackPoint(), secondmentDateVo.getStartTime()));
                    hisRule.setOutPoint(secondmentDateVo.getStartTime());
                    hisRule.setOutUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode());
                    hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), hisRule.getInPoint()));
                    hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), hisRule.getInPoint()));
                    getBreakTimeByOrdinary(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule);
                    calcPeriodWorkDayForSecondment(hisRule);
                    hisRule.setOriginOutPoint(secondmentDateVo.getStartTime());
                    hisRule.calOriginValidDuration();
                    hisRule2.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule2.getInPoint(), hisRule2.getInLackPoint(), secondmentDateVo.getEndTime()));
                    hisRule2.calLatePoint(DateDetail.getNewDateByPeriod(hisRule2.getInPoint(), hisRule2.getLatePoint(), secondmentDateVo.getEndTime()));
                    hisRule2.setClockStartPoint(DateDetail.getNewDateByPeriod(hisRule2.getInPoint(), hisRule2.getClockStartPoint(), secondmentDateVo.getEndTime()));
                    hisRule2.setInPoint(secondmentDateVo.getEndTime());
                    hisRule2.setInUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode());
                    hisRule2.setId(RandomUtil.uuId());
                    getBreakTimeByOrdinary(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule2);
                    calcPeriodWorkDayForSecondment(hisRule2);
                    hisRule2.setOriginInPoint(secondmentDateVo.getEndTime());
                    hisRule2.calOriginValidDuration();
                    secondmentRuleHandle(resultList, user, secondmentDateVos, hisRule2.getInPoint(), hisRule2.getOutPoint(), hisRule2, iterator, rules, collect1);
                    rules.add(hisRule2);
                    log.error("被借调组，借调在在时段中，调整配置{}", JSON.toJSONString(hisRule));
                    resultList.add(DailyRuleResultVo.secondmentBuild(hisRule));
                    continue;
                }
                //借调开始或者结束是否在班次当中
                //借调开始在班次中
                if (DateDetail.checkTimeBetween(secondmentDateVo.getStartTime(), hisRule.getInPoint(), hisRule.getOutPoint()) && secondmentDateVo.getStartTime().compareTo(hisRule.getOutPoint()) != 0 && secondmentDateVo.getStartTime().compareTo(hisRule.getInPoint()) != 0) {
                    //非调休
                    hisRule.setEarlyPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getEarlyPoint(), secondmentDateVo.getStartTime()));
                    hisRule.setOutLackPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getOutLackPoint(), secondmentDateVo.getStartTime()));
                    hisRule.setOutPoint(secondmentDateVo.getStartTime());
                    hisRule.setOutUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode());
                    hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), hisRule.getInPoint()));
                    hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), hisRule.getInPoint()));
                    getBreakTimeByOrdinary(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule);
                    calcPeriodWorkDayForSecondment(hisRule);
                    hisRule.setOriginOutPoint(secondmentDateVo.getStartTime());
                    hisRule.calOriginValidDuration();
                    secondmentRuleHandle(resultList, user, secondmentDateVos, hisRule.getInPoint(), hisRule.getOutPoint(), hisRule, iterator, rules, collect1);
                    log.error("被借调组，借调开始在时段中，调整下班点配置{}", JSON.toJSONString(hisRule));
                    resultList.add(DailyRuleResultVo.secondmentBuild(hisRule));
                    //如果为半天假全天班，且借调时间完全覆盖下半天假,剩下请假时段请假天数为0.5
                    if (Objects.equals(hisRule.getApplyUnit(), 3) && Objects.equals(hisRule.getSchedulesType(), 0)) {
                        Date leaveDate = getLeaveDate(hisRule);
                        if (secondmentDateVo.getStartTime().compareTo(leaveDate) <= 0) {
                            hisRule.setLeaveDay(hisRule.getPeriodWorkDay());
                            hisRule.setPayrollHours(hisRule.getPayrollHours());
                        }
                    }
                    continue;
                }
                //借调结束在班次中
                if (DateDetail.checkTimeBetween(secondmentDateVo.getEndTime(), hisRule.getInPoint(), hisRule.getOutPoint()) && secondmentDateVo.getEndTime().compareTo(hisRule.getInPoint()) != 0 && secondmentDateVo.getEndTime().compareTo(hisRule.getOutPoint()) != 0) {
                    //非调休
                    hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), secondmentDateVo.getEndTime()));
                    hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), secondmentDateVo.getEndTime()));
                    hisRule.setClockStartPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getClockStartPoint(), secondmentDateVo.getEndTime()));
                    hisRule.setInPoint(secondmentDateVo.getEndTime());
                    hisRule.setInUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode());
                    getBreakTimeByOrdinary(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule);
                    calcPeriodWorkDayForSecondment(hisRule);
                    hisRule.setOriginInPoint(secondmentDateVo.getEndTime());
                    hisRule.calOriginValidDuration();
                    secondmentRuleHandle(resultList, user, secondmentDateVos, hisRule.getInPoint(), hisRule.getOutPoint(), hisRule, iterator, rules, collect1);
                    log.error("被借调组，借调结束在时段中，调整上班点配置{}", JSON.toJSONString(hisRule));
                    resultList.add(DailyRuleResultVo.secondmentBuild(hisRule));
                    //如果为半天假全天班，且借调时间完全覆盖下半天假,剩下请假时段请假天数为0.5
                    if (Objects.equals(hisRule.getApplyUnit(), 3) && Objects.equals(hisRule.getSchedulesType(), 0)) {
                        Date leaveDate = getLeaveDate(hisRule);
                        if (secondmentDateVo.getEndTime().compareTo(leaveDate) >= 0) {
                            hisRule.setLeaveDay(hisRule.getPeriodWorkDay());
                        }
                    }
                    continue;
                }
                if (DateDetail.checkTimeBetween(hisRule.getInPoint(), secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime()) && DateDetail.checkTimeBetween(hisRule.getOutPoint(), secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime())) {
                    hisRule.setApplyViewEnable(0);
                    iterator.remove();
                    hisRule.delete();
                    log.error("被借调组，借调覆盖当前班次隐藏当前规则{}", JSON.toJSONString(hisRule));
                    resultList.add(DailyRuleResultVo.secondmentBuild(hisRule));
                    //如果全天假半天班，剩余班次请假天数调整为 1
                    if (Objects.equals(hisRule.getApplyUnit(), 2) && !Objects.equals(hisRule.getSchedulesType(), 0)) {
                        FtbAttendanceDailyRule ftbAttendanceDailyRule = collect1.get(Objects.equals(hisRule.getSchedulesType(), 1) ? 2 : 1);
                        if (Objects.nonNull(ftbAttendanceDailyRule)) {
                            ftbAttendanceDailyRule.setLeaveDay(BigDecimal.ONE);
                            ftbAttendanceDailyRule.setPayrollHours(hisRule.getPayrollHours());
                        }
                    }
                    return;
                }
                hisRule.setSelfGroup(SecondmentTypeEnum.NOT.getCode());
                continue;
            }
            if (SecondmentTypeEnum.TO.getCode().equals(user.getUserGroupType())) {
                hisRule.setSelfGroup(SecondmentTypeEnum.TO.getCode());
                //借调开始或者结束是否在班次当中
                //调休
                if (Objects.isNull(hisRule.getInPoint()) && Objects.equals(AttendanceTypeEnum.REST.getCode(), hisRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.DEFAULT.getCode(), hisRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), hisRule.getAttendanceType())) {
                    log.error("借调组，全天调休规则直接保存{}", JSON.toJSONString(hisRule));
                    continue;
                }
                //借调处在班次当中
                if (DateDetail.checkTimeBetween(secondmentDateVo.getStartTime(), hisRule.getInPoint(), hisRule.getOutPoint()) && DateDetail.checkTimeBetween(secondmentDateVo.getEndTime(), hisRule.getInPoint(), hisRule.getOutPoint())) {
                    hisRule.setEarlyPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getEarlyPoint(), secondmentDateVo.getEndTime()));
                    hisRule.setOutLackPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getOutLackPoint(), secondmentDateVo.getEndTime()));
                    hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), secondmentDateVo.getStartTime()));
                    hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), secondmentDateVo.getStartTime()));
                    hisRule.setClockStartPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getClockStartPoint(), secondmentDateVo.getStartTime()));
                    hisRule.setInPoint(secondmentDateVo.getStartTime());
                    hisRule.setOriginInPoint(secondmentDateVo.getStartTime());
                    hisRule.setOutPoint(secondmentDateVo.getEndTime());
                    getBreakTimeByOrdinaryOfSecondment(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule);
                    calcPeriodWorkDayForSecondment(hisRule);
                    hisRule.setOriginOutPoint(secondmentDateVo.getEndTime());
                    hisRule.calOriginValidDuration();
                    log.error("借调组，借调在时段中，调整配置{}", JSON.toJSONString(hisRule));
                    resultList.add(DailyRuleResultVo.secondmentBuild(hisRule));
                    continue;
                }
                //借调开始在班次中
                if (DateDetail.checkTimeBetweenL(secondmentDateVo.getStartTime(), hisRule.getInPoint(), hisRule.getOutPoint())) {
                    //非调休
                    hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), secondmentDateVo.getStartTime()));
                    hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), secondmentDateVo.getStartTime()));
                    hisRule.setClockStartPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getClockStartPoint(), secondmentDateVo.getStartTime()));
                    hisRule.setInPoint(secondmentDateVo.getStartTime());
                    getBreakTimeByOrdinaryOfSecondment(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule);
                    calcPeriodWorkDayForSecondment(hisRule);
                    hisRule.setOriginInPoint(secondmentDateVo.getStartTime());
                    hisRule.calOriginValidDuration();
                    log.error("借调组，借调开始在时段中，调整上班点配置{}", JSON.toJSONString(hisRule));
                    resultList.add(DailyRuleResultVo.secondmentBuild(hisRule));
                    continue;
                }
                //借调结束在班次中
                if (DateDetail.checkTimeBetweenR(secondmentDateVo.getEndTime(), hisRule.getInPoint(), hisRule.getOutPoint())) {
                    //调休
                    //非调休
                    hisRule.setEarlyPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getEarlyPoint(), secondmentDateVo.getEndTime()));
                    hisRule.setOutLackPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getOutLackPoint(), secondmentDateVo.getEndTime()));
                    hisRule.setOutPoint(secondmentDateVo.getEndTime());
                    hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), hisRule.getInPoint()));
                    hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), hisRule.getInPoint()));
                    getBreakTimeByOrdinaryOfSecondment(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule);
                    calcPeriodWorkDayForSecondment(hisRule);
                    hisRule.setOriginOutPoint(secondmentDateVo.getEndTime());
                    hisRule.calOriginValidDuration();
                    if (!rules.contains(hisRule)) {
                        rules.add(hisRule);
                    }
                    log.error("借调组，借调结束在时段中，调整下班点配置{}", JSON.toJSONString(hisRule));
                    resultList.add(DailyRuleResultVo.secondmentBuild(hisRule));
                    continue;
                }
                if (DateDetail.checkTimeBetween(hisRule.getInPoint(), secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime()) && DateDetail.checkTimeBetween(hisRule.getOutPoint(), secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime())) {
                    log.error("借调组，借调覆盖当前班次直接保存{}", JSON.toJSONString(hisRule));
                    continue;
                }
                //是否没有下个借调请求
                if (!Objects.nonNull(AttendanceGroupUserStatusUtil.getSecondmentType(secondmentDateVos, start, end))) {
                    iterator.remove();
                }
                log.error("借调组，{}班次未命中借调时间范围，过滤{}", AttendanceTypeEnum.getMsg(hisRule.getAttendanceType()), JSON.toJSONString(hisRule));
                resultList.add(DailyRuleResultVo.secondmentBuild(hisRule));
            }
        }
    }

    private void calcPeriodWorkDayForSecondment(FtbAttendanceDailyRule rule) {
        if (Objects.isNull(rule.getPeriodWorkDay()) || Objects.isNull(rule.getOriginInPoint()) || Objects.isNull(rule.getOriginOutPoint())) {
            return;
        }
        int validDuration = rule.getOriginValidDuration();
        rule.setPeriodWorkDay(BigDecimal.valueOf(rule.getValidDuration()).divide(BigDecimal.valueOf(validDuration), 4, RoundingMode.HALF_UP).multiply(rule.getPeriodWorkDay()).setScale(2, RoundingMode.HALF_UP));
        if (Objects.nonNull(rule.getPayrollHours())) {
            rule.setPayrollHours(BigDecimal.valueOf(rule.getValidDuration()).divide(BigDecimal.valueOf(validDuration), 4, RoundingMode.HALF_UP).multiply(rule.getPayrollHours()).setScale(2, RoundingMode.HALF_UP));
        }
    }

    /**
     * 新增排班的休息时间处理
     *
     * @param appleStart 原请假规则
     * @param rule       新增正常排班规则
     */
    private void getBreakTimeByOrdinary(Date appleStart, Date appleEnd, FtbAttendanceDailyRule rule) {
        if (!Objects.equals(rule.getBreakEnable(), 1)) {
            return;
        }
        if (Objects.isNull(rule.getBreakStartPoint()) || Objects.isNull(rule.getBreakEndPoint())) {
            return;
        }
        //休息结束时间小于排班上班时间，或者休息开始时间大于排班下班时间
        if (rule.getBreakEndPoint().compareTo(rule.getInPoint()) <= 0 || rule.getBreakStartPoint().compareTo(rule.getOutPoint()) >= 0) {
            rule.setBreakStartPoint(null);
            rule.setBreakEndPoint(null);
            rule.setBreakEnable(0);
            return;
        }
        //借调时间完全覆盖原来休息时段
        if (DateDetail.checkTimeBetween(rule.getBreakStartPoint(), appleStart, appleEnd) && DateDetail.checkTimeBetween(rule.getBreakEndPoint(), appleStart, appleEnd)) {
            rule.setBreakStartPoint(null);
            rule.setBreakEndPoint(null);
            rule.setBreakEnable(0);
            return;
        }
        //休息时间完全覆盖借调时间
        if (DateDetail.checkTimeBetween(appleStart, rule.getBreakStartPoint(), rule.getBreakEndPoint()) && DateDetail.checkTimeBetween(appleEnd, rule.getBreakStartPoint(), rule.getBreakEndPoint())) {
            if (appleStart.compareTo(rule.getBreakStartPoint()) >= 0) {
                rule.setBreakEndPoint(appleStart);
                rule.setOriginBreakEndPoint(appleStart);
            }
            if (appleEnd.compareTo(rule.getBreakEndPoint()) <= 0) {
                rule.setBreakStartPoint(appleEnd);
                rule.setOriginBreakStartPoint(appleEnd);
            }
            return;
        }
        //部分覆盖
        //开始部分覆盖
        if (DateDetail.checkTimeBetween(appleEnd, rule.getBreakStartPoint(), rule.getBreakEndPoint())) {
            rule.setBreakStartPoint(appleEnd);
            rule.setOriginBreakStartPoint(appleEnd);
            return;
        }
        //结束部分覆盖
        if (DateDetail.checkTimeBetween(appleStart, rule.getBreakStartPoint(), rule.getBreakEndPoint())) {
            rule.setBreakEndPoint(appleStart);
            rule.setOriginBreakEndPoint(appleStart);

        }
    }

    /**
     * 新增排班的休息时间处理
     *
     * @param appleStart 原请假规则
     * @param rule       新增正常排班规则
     */
    private void getBreakTimeByOrdinaryOfSecondment(Date appleStart, Date appleEnd, FtbAttendanceDailyRule rule) {
        if (!Objects.equals(rule.getBreakEnable(), 1)) {
            return;
        }
        if (Objects.isNull(rule.getBreakStartPoint()) || Objects.isNull(rule.getBreakEndPoint())) {
            return;
        }
        //休息结束时间小于排班上班时间，或者休息开始时间大于排班下班时间
        if (rule.getBreakEndPoint().compareTo(rule.getInPoint()) <= 0 || rule.getBreakStartPoint().compareTo(rule.getOutPoint()) >= 0) {
            rule.setBreakStartPoint(null);
            rule.setBreakEndPoint(null);
            rule.setBreakEnable(0);
            return;
        }
        //借调时间完全覆盖原来休息时段
        if (DateDetail.checkTimeBetween(rule.getBreakStartPoint(), appleStart, appleEnd) && DateDetail.checkTimeBetween(rule.getBreakEndPoint(), appleStart, appleEnd)) {
            return;
        }
        //休息时间完全覆盖借调时间
        if (DateDetail.checkTimeBetween(appleStart, rule.getBreakStartPoint(), rule.getBreakEndPoint()) && DateDetail.checkTimeBetween(appleEnd, rule.getBreakStartPoint(), rule.getBreakEndPoint())) {
            if (appleStart.compareTo(rule.getBreakStartPoint()) >= 0) {
                rule.setBreakStartPoint(appleStart);
            }
            if (appleEnd.compareTo(rule.getBreakEndPoint()) <= 0) {
                rule.setBreakEndPoint(appleEnd);
            }
            return;
        }
        //部分覆盖
        //开始部分覆盖
        if (DateDetail.checkTimeBetween(appleEnd, rule.getBreakStartPoint(), rule.getBreakEndPoint())) {
            rule.setBreakEndPoint(appleEnd);
            return;
        }
        //结束部分覆盖
        if (DateDetail.checkTimeBetween(appleStart, rule.getBreakStartPoint(), rule.getBreakEndPoint())) {
            rule.setBreakStartPoint(appleStart);
        }
    }

    /**
     * 新增借调借调组的规则处理
     *
     * @param nextShiftSetting
     * @param date
     * @param user
     * @param ftbAttendanceDailyRules
     */
    private void afterSecondmentRulesHandle(AttendanceShiftSettingVo nextShiftSetting, Date date, AttendanceGroupUser user, List<FtbAttendanceDailyRule> ftbAttendanceDailyRules, AttendanceBaseSetting attendanceBaseSetting) {
        if (CollUtil.isEmpty(ftbAttendanceDailyRules)) {
            //是否为固定班
            if (!Objects.equals(1, nextShiftSetting.getSystemType())) {
                log.error("新增借调无历史规则且非固定班，不生成固定班规则{}", user.getUserId());
                return;
            }
            if (!SecondmentTypeEnum.TO.getCode().equals(user.getUserGroupType())) {
                log.error("新增借调当前非借调组，不生成固定班规则{}", user.getUserId());
                return;
            }
            BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.ZERO : attendanceBaseSetting.getAttendanceRatio();
            List<FixedClassShiftVo> fixedClassShiftVos = nextShiftSetting.getFixedPeriods();
            Map<Integer, FixedClassShiftVo> collect = fixedClassShiftVos.stream().collect(Collectors.toMap(FixedClassShiftVo::getNum, fixedClassShift -> fixedClassShift));
            int currentWeekNumber = DateDetail.getCurrentWeekNumber(date);
            FixedClassShiftVo fixedClassShiftVo = collect.get(currentWeekNumber);
            List<ShiftNameVo> shiftNameVos = fixedClassShiftVo.getShiftNameVos();
            ShiftNameVo ordShift = shiftNameVos.stream().filter(shiftNameVo -> Objects.equals(shiftNameVo.getAttendanceType(), 2)).findFirst().orElse(null);
            shiftNameVos.forEach(shiftName -> {
                if (Objects.equals(shiftName.getType(), 1)) {
                    buildDailyRule(user, date, ftbAttendanceDailyRules, shiftName.getFixedSort(), Objects.nonNull(ordShift) ? ordShift.getPeriods() : null, nextShiftSetting, AttendanceTypeEnum.REST, attendanceRatio, 1);
                } else {
                    if (StringUtil.isEmpty(shiftName.getId())) {
                        return;
                    }
                    buildDailyRule(user, date, ftbAttendanceDailyRules, shiftName.getFixedSort(), shiftName.getPeriods(), nextShiftSetting, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 1);
                }
            });
            ftbAttendanceDailyRules.forEach(rule -> rule.setSelfGroup(2));
            log.error("新增借调当前借调组用户[{}],生成固定班规则为[{}]", user.getUserId(), JsonUtil.getObjectToString(ftbAttendanceDailyRules));
        }
    }

    /**
     * 页面类型转排班表类型
     *
     * @param type 页面传入类型 （0清除 1休 2时段 3假期(抵扣卷) 4假期(兑换卷)）
     * @return
     */
    private AttendanceTypeEnum getType2AttendanceType(Integer type) {
        if (0 == type) {
            return AttendanceTypeEnum.CLEAR;
        }
        if (1 == type) {
            return AttendanceTypeEnum.REST;
        }
        if (3 == type) {
            return AttendanceTypeEnum.DEDUCTION_HOLIDAYS;
        }
        if (4 == type) {
            return AttendanceTypeEnum.EXCHANGE_HOLIDAYS;
        }
        return AttendanceTypeEnum.ORDINARY;
    }


    /**
     * 页面类型转排班表类型
     *
     * @param attendanceType 页面传入类型 （0清除 1休 2时段 3假期(抵扣卷) 4假期(兑换卷)）
     * @return
     */
    private Integer getAttendanceType2type(Integer attendanceType) {
        if (Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), attendanceType)) {
            return 0;
        }
        if (Objects.equals(AttendanceTypeEnum.REST.getCode(), attendanceType)) {
            return 1;
        }
        if (Objects.equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), attendanceType)) {
            return 3;
        }
        if (Objects.equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode(), attendanceType)) {
            return 4;
        }
        return 2;
    }

    /**
     * 获取排班展示类型
     *
     * @param rule
     * @return
     */
    private Integer getType(FtbAttendanceDailyRule rule) {
        //1休息日 2正常出勤 3假期 4带薪假期 5未来的时间不在本组 6迟到 7早退 8缺卡 9旷工 10外勤 11请假 12加班
//        13借调 14被借调
//                * 15当日有早退且借调情况 16当日有迟到且借调情况 17当日有缺卡且借调情况 18当日有旷工且借调情况 19当日有打外勤且借调情况）
        if (AttendanceTypeEnum.ORDINARY.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.NORMAL.getCode();
        }
        if (AttendanceTypeEnum.REST.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.REST.getCode();
        }
        if (AttendanceTypeEnum.HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.PAID_HOLIDAYS.getCode();

        }
        return SchedulesTypeEnum.NONE.getCode();
    }

    /**
     * 获取排班展示类型
     *
     * @param rule
     * @param groupUser
     * @param day
     * @return
     */
    private Integer getType(FtbAttendanceDailyRule rule, AttendanceGroupUser groupUser, Date day, List<AttendanceClockInResult> attendanceClockInResults, Map<String, FtbAttendanceClockIn> clockInMap, Integer isSchedules) {
        //1休息日 2正常出勤 3假期 4带薪假期 5未来的时间不在本组 6迟到 7早退 8缺卡 9旷工 10外勤 11请假 12加班
//        13借调 14被借调
//                * 15当日有早退且借调情况 16当日有迟到且借调情况 17当日有缺卡且借调情况 18当日有旷工且借调情况 19当日有打外勤且借调情况）
        if (AttendanceTypeEnum.LEAVE.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.LEAVE.getCode();
        }
        if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.OVERTIME.getCode();
        }
        if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.STEP_OUT.getCode();
        }
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.BUSINESS_TRIP.getCode();
        }
        Date date = new Date();
        if (AttendanceTypeEnum.DEFAULT.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.NONE.getCode();
        }
        if (AttendanceTypeEnum.REST.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.REST.getCode();
        }
        if (AttendanceTypeEnum.HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.PAID_HOLIDAYS.getCode();
        }
        if (DateDetail.checkTimeBetween(date, rule.getClockStartPoint(), rule.getOutPoint())) {
            if (CollUtil.isEmpty(attendanceClockInResults)) {
                return SchedulesTypeEnum.NORMAL.getCode();
            }
            //上班打卡
            AttendanceClockInResult attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
            if (Objects.isNull(attendanceClockInResult)) {
                return SchedulesTypeEnum.NORMAL.getCode();
            }
            FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId());
            if (Objects.nonNull(ftbAttendanceClockIn)) {
                rule.setInPoint(ftbAttendanceClockIn.getClockInTime());
            }
            //查询最后异常打卡结果
            //是否有旷工
            List<AttendanceClockInResult> clockInResults = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getAbsence(), 1) && (Objects.equals(result.getClockInType(), 1) || Objects.equals(result.getClockInType(), 2))).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(clockInResults) && clockInResults.size() > 1) {
                return SchedulesTypeEnum.ABSENCE.getCode();
            }
            attendanceClockInResult = attendanceClockInResults.stream().filter(result -> !Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
            if (Objects.nonNull(attendanceClockInResult)) {
                Integer clockInStatus = attendanceClockInResult.getClockInStatus();
                //缺卡
                if (Objects.equals(clockInStatus, -1)) {
                    return SchedulesTypeEnum.MISSING.getCode();
                }
                //迟到
                if (Objects.equals(clockInStatus, 2)) {
                    return SchedulesTypeEnum.LATE.getCode();
                }
                //早退
                if (Objects.equals(clockInStatus, 3)) {
                    return SchedulesTypeEnum.EARLY.getCode();
                }
            }
            //外勤打卡
            attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInKind(), 2)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).findFirst().orElse(null);
            if (Objects.nonNull(attendanceClockInResult)) {
                return SchedulesTypeEnum.FIELD_WORK.getCode();
            }
            return SchedulesTypeEnum.IN_CLOCK.getCode();
        }
        //处理历史数据
        if (rule.getOutPoint().before(date)) {
            if (CollUtil.isEmpty(attendanceClockInResults)) {
                return SchedulesTypeEnum.NORMAL.getCode();
            }
            //查询最后异常打卡结果
            //是否有旷工
            List<AttendanceClockInResult> clockInResults = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getAbsence(), 1) && (Objects.equals(result.getClockInType(), 1) || Objects.equals(result.getClockInType(), 2))).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(clockInResults) && clockInResults.size() > 1) {
                return SchedulesTypeEnum.ABSENCE.getCode();
            }
            //异常打卡
            AttendanceClockInResult attendanceClockInResult = attendanceClockInResults.stream().filter(result -> !Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
            if (Objects.nonNull(attendanceClockInResult)) {
                Integer clockInStatus = attendanceClockInResult.getClockInStatus();
                //缺卡
                if (Objects.equals(clockInStatus, -1)) {
                    return SchedulesTypeEnum.MISSING.getCode();
                }
                //迟到
                if (Objects.equals(clockInStatus, 2)) {
                    return SchedulesTypeEnum.LATE.getCode();
                }
                //早退
                if (Objects.equals(clockInStatus, 3)) {
                    return SchedulesTypeEnum.EARLY.getCode();
                }
            }
            //外勤打卡
            attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInKind(), 2)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).findFirst().orElse(null);
            if (Objects.nonNull(attendanceClockInResult)) {
                return SchedulesTypeEnum.FIELD_WORK.getCode();
            }
            //上班打卡
            attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
            if (Objects.isNull(attendanceClockInResult)) {
                return SchedulesTypeEnum.NORMAL.getCode();
            }
            FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId());
            if (Objects.nonNull(ftbAttendanceClockIn) && Objects.equals(isSchedules, 0)) {
                rule.setInPoint(ftbAttendanceClockIn.getClockInTime());
            }
            //下班打卡
            attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 2)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
            if (Objects.isNull(attendanceClockInResult)) {
                return SchedulesTypeEnum.NORMAL.getCode();
            }
            ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId());
            if (Objects.nonNull(ftbAttendanceClockIn) && Objects.equals(isSchedules, 0)) {
                rule.setOutPoint(ftbAttendanceClockIn.getClockInTime());
            }
            return SchedulesTypeEnum.NORMAL.getCode();
        }

        List<SecondmentDateVo> secondmentDateVos = StringUtil.isEmpty(groupUser.getTimeJson()) ? null : JsonUtil.getJsonToList(groupUser.getTimeJson(), SecondmentDateVo.class);
        if (CollUtil.isNotEmpty(secondmentDateVos)) {
            for (SecondmentDateVo secondmentDate : secondmentDateVos) {
                if (DateDetail.checkTimeBetween(day, secondmentDate.getStartTime(), secondmentDate.getEndTime())) {
                    return SchedulesTypeEnum.NOT_IN_GROUP.getCode();
                }
            }
        }
        if (AttendanceTypeEnum.ORDINARY.getCode().equals(rule.getAttendanceType())) {
            return SchedulesTypeEnum.NORMAL.getCode();
        }
        return SchedulesTypeEnum.NONE.getCode();
    }

    private Integer getType2(FtbAttendanceDailyRule rule, List<Integer> attendanceTypeOfApply, List<AttendanceClockInResult> attendanceClockInResults) {
        //1休息日 2正常出勤 3假期 4带薪假期 5未来的时间不在本组 6迟到 7早退 8缺卡 9旷工 10外勤 11请假 12加班
//        13借调 14被借调
//                * 15当日有早退且借调情况 16当日有迟到且借调情况 17当日有缺卡且借调情况 18当日有旷工且借调情况 19当日有打外勤且借调情况）
        Integer schedulesType = SchedulesTypeEnum.NORMAL.getCode();
        if (AttendanceTypeEnum.LEAVE.getCode().equals(rule.getAttendanceType())) {
            attendanceTypeOfApply.add(SchedulesTypeEnum.LEAVE.getCode());
        }
        if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(rule.getAttendanceType())) {
            attendanceTypeOfApply.add(SchedulesTypeEnum.OVERTIME.getCode());
            return null;
        }
        if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getAttendanceType())) {
            attendanceTypeOfApply.add(SchedulesTypeEnum.STEP_OUT.getCode());
            schedulesType = SchedulesTypeEnum.STEP_OUT.getCode();
        }
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getAttendanceType())) {
            attendanceTypeOfApply.add(SchedulesTypeEnum.BUSINESS_TRIP.getCode());
            schedulesType = SchedulesTypeEnum.BUSINESS_TRIP.getCode();
        }
        if (AttendanceTypeEnum.REST.getCode().equals(rule.getAttendanceType())) {
            schedulesType = SchedulesTypeEnum.REST.getCode();
        }
        if (AttendanceTypeEnum.HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            schedulesType = SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            schedulesType = SchedulesTypeEnum.HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            schedulesType = SchedulesTypeEnum.PAID_HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getApplyViewEnable())) {
            attendanceTypeOfApply.add(SchedulesTypeEnum.STEP_OUT.getCode());
        }
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable())) {
            attendanceTypeOfApply.add(SchedulesTypeEnum.BUSINESS_TRIP.getCode());
        }
        if (AttendanceTypeEnum.DEFAULT.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.CLEAR.getCode().equals(rule.getAttendanceType())) {
            schedulesType = SchedulesTypeEnum.NONE.getCode();
            return schedulesType;
        }
        if (CollUtil.isEmpty(attendanceClockInResults)) {
            return schedulesType;
        }
        attendanceClockInResults.forEach(attendanceClockInResult -> {
            if (Objects.nonNull(attendanceClockInResult) && Objects.equals(attendanceClockInResult.getRepaired(), 1)) {
                //补卡
                attendanceTypeOfApply.add(SchedulesTypeEnum.REISSUE.getCode());
            }
            if (StringUtil.isNotEmpty(attendanceClockInResult.getAbsenceLeader())) {
                //出勤变更
                attendanceTypeOfApply.add(SchedulesTypeEnum.ATTENDANCE_CHANGE_NORMAL.getCode());
            }
            //外勤打卡
            if (Objects.equals(attendanceClockInResult.getClockInKind(), 2)) {
                attendanceTypeOfApply.add(SchedulesTypeEnum.FIELD_WORK.getCode());
            }
        });
        return schedulesType;
    }

    /**
     * 获取排班展示类型
     *
     * @param rule
     * @param groupUser
     * @param day
     * @param attendanceTypeOfApply
     * @return
     */
    private Integer getType(FtbAttendanceDailyRule rule, SchedulesItemVo build, AttendanceGroupUser groupUser, Date day, List<AttendanceClockInResult> attendanceClockInResults, Map<String, FtbAttendanceClockIn> clockInMap, Integer isSchedules, List<Integer> attendanceTypeOfApply) {
        //1休息日 2正常出勤 3假期 4带薪假期 5未来的时间不在本组 6迟到 7早退 8缺卡 9旷工 10外勤 11请假 12加班
//        13借调 14被借调
//                * 15当日有早退且借调情况 16当日有迟到且借调情况 17当日有缺卡且借调情况 18当日有旷工且借调情况 19当日有打外勤且借调情况）
        Integer schedulesType = SchedulesTypeEnum.NORMAL.getCode();
        if (AttendanceTypeEnum.LEAVE.getCode().equals(rule.getAttendanceType())) {
            build.setStartType(SchedulesTypeEnum.LEAVE.getCode());
            build.setEndType(SchedulesTypeEnum.LEAVE.getCode());
            attendanceTypeOfApply.add(SchedulesTypeEnum.LEAVE.getCode());
            schedulesType = SchedulesTypeEnum.LEAVE.getCode();
        }
        if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(rule.getAttendanceType())) {
            attendanceTypeOfApply.add(SchedulesTypeEnum.OVERTIME.getCode());
        }
        if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getAttendanceType())) {
            rule.setInPoint(null);
            rule.setOutPoint(null);
            build.setStartType(SchedulesTypeEnum.STEP_OUT.getCode());
            build.setEndType(SchedulesTypeEnum.STEP_OUT.getCode());
            attendanceTypeOfApply.add(SchedulesTypeEnum.STEP_OUT.getCode());
            schedulesType = SchedulesTypeEnum.STEP_OUT.getCode();
        }
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getAttendanceType())) {
            rule.setInPoint(null);
            rule.setOutPoint(null);
            build.setStartType(SchedulesTypeEnum.BUSINESS_TRIP.getCode());
            build.setEndType(SchedulesTypeEnum.BUSINESS_TRIP.getCode());
            attendanceTypeOfApply.add(SchedulesTypeEnum.BUSINESS_TRIP.getCode());
            schedulesType = SchedulesTypeEnum.BUSINESS_TRIP.getCode();
        }
        if (AttendanceTypeEnum.REST.getCode().equals(rule.getAttendanceType())) {
            build.setStartType(SchedulesTypeEnum.REST.getCode());
            build.setEndType(SchedulesTypeEnum.REST.getCode());
            schedulesType = SchedulesTypeEnum.REST.getCode();
        }
        if (AttendanceTypeEnum.HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            build.setStartType(SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode());
            build.setEndType(SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode());
            schedulesType = SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            build.setStartType(SchedulesTypeEnum.HOLIDAYS.getCode());
            build.setEndType(SchedulesTypeEnum.HOLIDAYS.getCode());
            schedulesType = SchedulesTypeEnum.HOLIDAYS.getCode();
        }
        if (AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            build.setStartType(SchedulesTypeEnum.PAID_HOLIDAYS.getCode());
            build.setEndType(SchedulesTypeEnum.PAID_HOLIDAYS.getCode());
            schedulesType = SchedulesTypeEnum.PAID_HOLIDAYS.getCode();
        }
        Integer inStepOutType = null;
        Integer outStepOutType = null;
        if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getApplyViewEnable())) {
            inStepOutType = Objects.equals(rule.getInStepOutType(), 0) ? null : SchedulesTypeEnum.STEP_OUT.getCode();
            outStepOutType = Objects.equals(rule.getOutStepOutType(), 0) ? null : SchedulesTypeEnum.STEP_OUT.getCode();
            attendanceTypeOfApply.add(SchedulesTypeEnum.STEP_OUT.getCode());
        }
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable())) {
            inStepOutType = SchedulesTypeEnum.BUSINESS_TRIP.getCode();
            outStepOutType = SchedulesTypeEnum.BUSINESS_TRIP.getCode();
            attendanceTypeOfApply.add(SchedulesTypeEnum.BUSINESS_TRIP.getCode());
        }
        if (AttendanceTypeEnum.DEFAULT.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.CLEAR.getCode().equals(rule.getAttendanceType())) {
            build.setStartType(SchedulesTypeEnum.NONE.getCode());
            build.setEndType(SchedulesTypeEnum.NONE.getCode());
            schedulesType = SchedulesTypeEnum.NONE.getCode();
            return schedulesType;
        }
        if (Objects.nonNull(inStepOutType)) {
            build.setStartType(SchedulesTypeEnum.NONE.getCode());
            build.setEndType(SchedulesTypeEnum.NONE.getCode());
            if (Objects.isNull(rule.getInPoint())) {
                build.setStartType(inStepOutType);
                build.setEndType(inStepOutType);
            }
        }
        //处理打卡
        if (CollUtil.isEmpty(attendanceClockInResults)) {
            return schedulesType;
        }
        //是否有操作人
        boolean absenceLeader = attendanceClockInResults.stream().anyMatch(result -> StringUtil.isNotEmpty(result.getAbsenceLeader()));
        if (absenceLeader) {
            attendanceTypeOfApply.add(SchedulesTypeEnum.ATTENDANCE_CHANGE_NORMAL.getCode());
        }
        if (AttendanceTypeEnum.REST.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.HOLIDAYS.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(rule.getAttendanceType())) {
            return schedulesType;
        }
        //异常打卡
        List<AttendanceClockInResult> attendanceClockInResultList = attendanceClockInResults.stream().filter(result -> !Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).collect(Collectors.toList());
        if (CollUtil.isNotEmpty(attendanceClockInResultList)) {
            for (AttendanceClockInResult attendanceClockInResult : attendanceClockInResultList) {
                Integer clockInStatus = attendanceClockInResult.getClockInStatus();
                //旷工
                if (Objects.equals(attendanceClockInResult.getAbsence(), 1) && StringUtil.isEmpty(attendanceClockInResult.getAbsenceLeader()) && (Objects.equals(attendanceClockInResult.getClockInType(), 1) || Objects.equals(attendanceClockInResult.getClockInType(), 2)) && StringUtil.isEmpty(attendanceClockInResult.getAbsenceLeader())) {
                    build.setStartType(SchedulesTypeEnum.ABSENCE.getCode());
                    build.setEndType(SchedulesTypeEnum.ABSENCE.getCode());
                    schedulesType = SchedulesTypeEnum.ABSENCE.getCode();
                    continue;
                }
                FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId());
                //早退
                if (Objects.equals(clockInStatus, 3)) {
                    rule.setOutPoint(ftbAttendanceClockIn.getClockInTime());
                    build.setEndType(SchedulesTypeEnum.EARLY.getCode());
                    schedulesType = SchedulesTypeEnum.EARLY.getCode();
                }
                //迟到
                if (Objects.equals(clockInStatus, 2)) {
                    rule.setInPoint(ftbAttendanceClockIn.getClockInTime());
                    build.setStartType(SchedulesTypeEnum.LATE.getCode());
                    schedulesType = SchedulesTypeEnum.LATE.getCode();
                }
                //缺卡
                if (Objects.equals(clockInStatus, -1)) {
                    if (Objects.equals(attendanceClockInResult.getClockInType(), 1)) {
                        build.setStartType(SchedulesTypeEnum.MISSING.getCode());
                    } else {
                        build.setEndType(SchedulesTypeEnum.MISSING.getCode());
                    }
                    schedulesType = SchedulesTypeEnum.MISSING.getCode();
                }
            }
        }
        //上班打卡
        AttendanceClockInResult attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 1) && Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
        if (Objects.nonNull(attendanceClockInResult)) {
            //补卡
            if (Objects.equals(attendanceClockInResult.getRepaired(), 1)) {
                build.setStartType(SchedulesTypeEnum.REISSUE.getCode());
                attendanceTypeOfApply.add(SchedulesTypeEnum.REISSUE.getCode());
            } else {
                FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId());
                if (Objects.nonNull(ftbAttendanceClockIn) && Objects.equals(isSchedules, 0)) {
                    rule.setInPoint(ftbAttendanceClockIn.getClockInTime());
                    build.setStartType(SchedulesTypeEnum.NORMAL.getCode());
                    if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(rule.getAttendanceType())) {
                        build.setStartType(Objects.nonNull(inStepOutType) ? inStepOutType : SchedulesTypeEnum.OVERTIME.getCode());
                        schedulesType = Objects.nonNull(inStepOutType) ? inStepOutType : SchedulesTypeEnum.OVERTIME.getCode();
                    }
                }
            }
        }

        //下班打卡
        attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 2) && Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
        if (Objects.nonNull(attendanceClockInResult)) {
            //补卡
            if (Objects.equals(attendanceClockInResult.getRepaired(), 1)) {
                build.setEndType(SchedulesTypeEnum.REISSUE.getCode());
                attendanceTypeOfApply.add(SchedulesTypeEnum.REISSUE.getCode());
            } else {
                FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId());
                if (Objects.nonNull(ftbAttendanceClockIn) && Objects.equals(isSchedules, 0)) {
                    build.setEndType(SchedulesTypeEnum.NORMAL.getCode());
                    rule.setOutPoint(ftbAttendanceClockIn.getClockInTime());
                    if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(rule.getAttendanceType())) {
                        build.setEndType(Objects.nonNull(outStepOutType) ? outStepOutType : SchedulesTypeEnum.OVERTIME.getCode());
                        schedulesType = Objects.nonNull(outStepOutType) ? outStepOutType : SchedulesTypeEnum.OVERTIME.getCode();
                    }

                }
            }
        }

        //外勤打卡
        attendanceClockInResultList = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInKind(), 2) && StringUtil.isEmpty(result.getApplyId())).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).collect(Collectors.toList());
        if (CollUtil.isNotEmpty(attendanceClockInResultList)) {
            for (AttendanceClockInResult clockInResult : attendanceClockInResultList) {
                FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(clockInResult.getClockInId());
                if (Objects.equals(clockInResult.getClockInType(), 1)) {
                    rule.setInPoint(ftbAttendanceClockIn.getClockInTime());
                    build.setStartType(SchedulesTypeEnum.FIELD_WORK.getCode());
                    attendanceTypeOfApply.add(SchedulesTypeEnum.FIELD_WORK.getCode());
                } else {
                    rule.setOutPoint(ftbAttendanceClockIn.getClockInTime());
                    build.setEndType(SchedulesTypeEnum.FIELD_WORK.getCode());
                    attendanceTypeOfApply.add(SchedulesTypeEnum.FIELD_WORK.getCode());
                }
            }
            schedulesType = SchedulesTypeEnum.FIELD_WORK.getCode();
        }

        //考勤变更
        attendanceClockInResultList = attendanceClockInResults.stream().filter(result -> StringUtil.isNotEmpty(result.getAbsenceLeader())).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).collect(Collectors.toList());
        if (CollUtil.isNotEmpty(attendanceClockInResultList)) {
            for (AttendanceClockInResult attendanceClockInResult1 : attendanceClockInResultList) {
                FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult1.getClockInId());
                Integer clockInStatus = attendanceClockInResult1.getClockInStatus();
                if (Objects.equals(attendanceClockInResult1.getAbsence(), 1)) {
                    schedulesType = SchedulesTypeEnum.ATTENDANCE_CHANGE_ABSENCE.getCode();
                    build.setStartType(SchedulesTypeEnum.ATTENDANCE_CHANGE_ABSENCE.getCode());
                    build.setEndType(SchedulesTypeEnum.ATTENDANCE_CHANGE_ABSENCE.getCode());
                }
                if (Objects.equals(clockInStatus, 1)) {
                    if (Objects.equals(attendanceClockInResult1.getClockInType(), 1)) {
                        rule.setInPoint(ftbAttendanceClockIn.getClockInTime());
                        build.setStartType(SchedulesTypeEnum.ATTENDANCE_CHANGE_NORMAL.getCode());
                    } else {
                        rule.setOutPoint(ftbAttendanceClockIn.getClockInTime());
                        build.setEndType(SchedulesTypeEnum.ATTENDANCE_CHANGE_NORMAL.getCode());
                    }
                    schedulesType = SchedulesTypeEnum.ATTENDANCE_CHANGE_NORMAL.getCode();
                }
            }
        }
/**
 * applyViewEnable为外出、出差设置build的startType和EndType(上和下班类型)字段在分别存在打卡记录是为正常出勤，不存在时根据rule的applyViewEnable设置为外出或者出差，
 * 存在上下班打卡时设置build的InPoint和OutPoint(上下班时间)为上下班打卡时间，出存在时设置为原InPoint和OutPoint值
 */
        if (Objects.nonNull(inStepOutType)) {
            build.setStartType(SchedulesTypeEnum.NONE.getCode());
            build.setEndType(SchedulesTypeEnum.NONE.getCode());
            attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 1) && Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
            if (Objects.nonNull(attendanceClockInResult)) {
                FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId());
                if (Objects.nonNull(ftbAttendanceClockIn)) {
                    rule.setInPoint(ftbAttendanceClockIn.getClockInTime());
                    build.setStartType(inStepOutType);
                }
            }
            schedulesType = inStepOutType;
        }
        if (Objects.nonNull(outStepOutType)) {
            attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 2) && Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
            if (Objects.nonNull(attendanceClockInResult)) {
                FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId());
                if (Objects.nonNull(ftbAttendanceClockIn)) {
                    rule.setOutPoint(ftbAttendanceClockIn.getClockInTime());
                    build.setEndType(outStepOutType);
                }
            }

            schedulesType = outStepOutType;
        }
        if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getAttendanceType())) {
            build.setStartType(inStepOutType);
            build.setEndType(outStepOutType);
            rule.setInPoint(null);
            rule.setOutPoint(null);
        }
        List<SecondmentDateVo> secondmentDateVos = StringUtil.isEmpty(groupUser.getTimeJson()) ? null : JsonUtil.getJsonToList(groupUser.getTimeJson(), SecondmentDateVo.class);
        if (CollUtil.isNotEmpty(secondmentDateVos)) {
            for (SecondmentDateVo secondmentDate : secondmentDateVos) {
                if (DateDetail.checkTimeBetween(day, secondmentDate.getStartTime(), secondmentDate.getEndTime())) {
                    return SchedulesTypeEnum.NOT_IN_GROUP.getCode();
                }
            }
        }
        return schedulesType;
    }

    private String getDate2Str(Date date) {
        return DateDetail.getDate2Str(date, DateDetail.DF10);
    }

    private Date getStr2Date(String date) {
        return DateDetail.getStr2Date10(date);
    }

    /**
     * 执行借调规则生成
     *
     * @param resultList    异常结果响应
     * @param hisDailyRules 历史规则
     * @param currDailyRule 当前新增规则
     */
    private void dailyRuleArrangementHandle(List<DailyRuleResultVo> resultList, List<FtbAttendanceDailyRule> hisDailyRules, List<FtbAttendanceDailyRule> currDailyRule) {
        hisDailyRules.removeIf(rule -> Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.DEFAULT.getCode()));
        if (CollUtil.isEmpty(hisDailyRules)) {
            currDailyRule = currDailyRule.stream().filter(rule -> !Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.CLEAR.getCode())).collect(Collectors.toList());
            hisDailyRules.addAll(currDailyRule);
            currDailyRule.forEach(rule -> {
                rule.insertTrue();
                resultList.add(DailyRuleResultVo.successNailBuild(rule));
            });
            return;
        }
        workOvertimeRuleProcessor.setNext(ordinaryRuleProcessor).setNext(restRuleProcessor).setNext(leaveRuleProcessor).setNext(stepOutRuleProcessor);
        workOvertimeRuleProcessor.ruleArrangementHandle(hisDailyRules, currDailyRule, resultList);
        if (CollUtil.isEmpty(resultList)) {
            resultList.addAll(currDailyRule.stream().map(DailyRuleResultVo::successNailBuild).collect(Collectors.toList()));
        }
    }


    private void buildDailyRule(AttendanceGroupUser user, Date date, List<FtbAttendanceDailyRule> rules, Integer schedulesType, List<AttendanceShiftSettingPeriodVo> periods, AttendanceShiftSettingVo attendanceShiftSettingVo, AttendanceTypeEnum attendanceTypeEnum, BigDecimal validDuration, Integer fixedMark) {
        buildDailyRule(user, date, rules, schedulesType, periods, attendanceShiftSettingVo, attendanceTypeEnum, null, null, null, validDuration, fixedMark, null);
    }

    private void buildDailyRule(String groupId, String userId, Date date, List<FtbAttendanceDailyRule> rules, Integer schedulesType, List<AttendanceShiftSettingPeriodVo> periods, AttendanceShiftSettingVo attendanceShiftSettingVo, AttendanceTypeEnum attendanceTypeEnum, BigDecimal validDuration, Integer fixedMark) {
        buildDailyRule(groupId, userId, date, rules, schedulesType, periods, attendanceShiftSettingVo, attendanceTypeEnum, null, null, null, validDuration, fixedMark, null, null);
    }

    private void buildDailyRule(String groupId, String userId, Date date, List<FtbAttendanceDailyRule> rules, Integer schedulesType, List<AttendanceShiftSettingPeriodVo> periods, AttendanceShiftSettingVo attendanceShiftSettingVo, AttendanceTypeEnum attendanceTypeEnum, BigDecimal validDuration, Integer fixedMark, BigDecimal paidBalance) {
        buildDailyRule(groupId, userId, date, rules, schedulesType, periods, attendanceShiftSettingVo, attendanceTypeEnum, null, null, null, validDuration, fixedMark, paidBalance, null);
    }

    private FtbAttendanceDailyRule buildDailyRule(AttendanceGroupUser user, Date date, List<FtbAttendanceDailyRule> rules, Integer schedulesType, List<AttendanceShiftSettingPeriodVo> periods, AttendanceShiftSettingVo attendanceShiftSettingVo, AttendanceTypeEnum attendanceTypeEnum, String applyId, Date applyStart, Date applyEnd, BigDecimal validDuration, Integer fixedMark, LeaveParam leaveParam) {
        return buildDailyRule(user.getGroupId(), user.getUserId(), date, rules, schedulesType, periods, attendanceShiftSettingVo, attendanceTypeEnum, applyId, applyStart, applyEnd, validDuration, fixedMark, null, leaveParam);
    }

    private FtbAttendanceDailyRule buildDailyRule(String groupId, String userId, Date date, List<FtbAttendanceDailyRule> rules, Integer schedulesType, List<AttendanceShiftSettingPeriodVo> periods, AttendanceShiftSettingVo attendanceShiftSettingVo, AttendanceTypeEnum attendanceTypeEnum, String applyId, Date applyStart, Date applyEnd, BigDecimal validDuration, Integer fixedMark, BigDecimal balance, LeaveParam leaveParam) {
        FtbAttendanceDailyRule rule = new FtbAttendanceDailyRule();
        rule.setGroupId(groupId);
        rule.setSchedulesType(schedulesType);
        rule.setUserId(userId);
        rule.setDeleteMark(0);
        rule.setSelfGroup(0);
        rule.setId(RandomUtil.uuId());
        rule.setDay(date);
        rule.setFixedMark(Objects.isNull(fixedMark) ? 0 : fixedMark);
        rule.setApplyViewEnable(1);
        if (Objects.nonNull(leaveParam)) {
            rule.setLeaveParam(JSON.toJSONString(leaveParam));
            rule.setApplyUnit(leaveParam.getLeaveUnit());
            if (Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.LEAVE.getCode())) {
                if (leaveParam.getStart().compareTo(date) == 0) {
                    if (Objects.equals(leaveParam.getStartTimeType(), 2)) {
                        rule.setLeaveDay(BigDecimal.valueOf(0.5));
                    }
                }
                if (leaveParam.getEnd().compareTo(date) == 0) {
                    if (Objects.equals(leaveParam.getEndTimeType(), 1)) {
                        rule.setLeaveDay(BigDecimal.valueOf(0.5));
                    }
                }
            }
        }
        if (Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.LEAVE.getCode())) {
            rule.setApplyViewEnable(2);
        }
        if (StringUtil.isNotEmpty(applyId)) {
            rule.setApplyId(applyId);
        }
        if (Objects.nonNull(applyStart)) {
            rule.setApplyStart(applyStart);
            rule.setInPoint(applyStart);
        }
        if (Objects.nonNull(applyEnd)) {
            rule.setApplyEnd(applyEnd);
            rule.setOutPoint(applyEnd);
        }
        rule.setAttendanceType(attendanceTypeEnum.getCode());
        rule.setValidDuration(Objects.isNull(validDuration) ? 0 : validDuration.multiply(BigDecimal.valueOf(60)).setScale(0, RoundingMode.HALF_UP).intValue());
        if (Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.STEP_OUT.getCode())) {
            rule.setApplyViewEnable(attendanceTypeEnum.getCode());
            rule.setValidDuration(0);
        }
        if (Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()) || Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.REST.getCode())) {
            rule.setValidDuration(0);
        }
        buildHalfRestDailyRule(rule, periods);
        if (Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.ORDINARY.getCode())) {
            if (CollUtil.isEmpty(periods)) {
                return rule;
            }
            List<ShiftNameVo> schedulingPeriods = attendanceShiftSettingVo.getSchedulingPeriods();
            if (!Objects.equals(schedulesType, 0) && periods.size() < 2) {
                AttendanceShiftSettingPeriodVo period = periods.get(0);
                buildDay2HalfDailyRule(rule, period);
                if (CollUtil.isNotEmpty(schedulingPeriods)) {
                    ShiftNameVo shiftNameVo = schedulingPeriods.stream().filter(r -> Objects.equals(r.getId(), period.getShiftId())).findFirst().orElse(null);
                    if (Objects.nonNull(shiftNameVo)) {
                        rule.setPeriodInfo(JSON.toJSONString(shiftNameVo));
                    }
                }
                rules.add(rule);
                return rule;
            }
            AttendanceShiftSettingPeriodVo period = Objects.equals(schedulesType, 0) || Objects.equals(schedulesType, 1) ? periods.stream().findFirst().orElse(null) : periods.stream().reduce((r1, r2) -> r2).orElse(null);
            rule.setSort(period.getSort());
            rule.setShiftId(period.getShiftId());
            rule.setPeriodId(period.getId());
            if (CollUtil.isNotEmpty(schedulingPeriods)) {
                ShiftNameVo shiftNameVo = schedulingPeriods.stream().filter(r -> Objects.equals(r.getId(), period.getShiftId())).findFirst().orElse(null);
                if (Objects.nonNull(shiftNameVo)) {
                    rule.setPeriodInfo(JSON.toJSONString(shiftNameVo));
                }
            }
            rule.setLateEnable(period.getLateEnable());
            rule.setEarlyEnable(period.getEarlyEnable());
            rule.setBreakEnable(period.getBreakEnable());
            rule.setInUnbounded(0);
            rule.setOutUnbounded(0);
            rule.setNextDayEnable(period.getNextDayEnable());
            rule.setSchedulesType(Objects.isNull(period.getType()) ? 0 : period.getType());
            rule.setClockStartPoint(DateDetail.getDateByPoint(date, period.getClockStartPoint(), period.getClockStartPointType()));
            rule.setInPoint(DateDetail.getDateByPoint(date, period.getInPoint(), period.getInType()));
            rule.setOutPoint(DateDetail.getDateByPoint(date, period.getOutPoint(), period.getOutType()));
            rule.setOriginInPoint(rule.getInPoint());
            rule.setOriginOutPoint(rule.getOutPoint());
            rule.calLatePoint(DateDetail.getDateByPoint(date, period.getLatePoint(), period.getLateType()));
            rule.setOriginalLatePoint(rule.getLatePoint());
            rule.setIsLateOutLateIn(0);
            rule.calInLackPoint(DateDetail.getDateByPoint(date, period.getInLackPoint(), period.getInLackPointType()));
            rule.setBreakStartPoint(DateDetail.getDateByPoint(date, period.getBreakStartPoint(), period.getBreakStartType()));
            rule.setBreakEndPoint(DateDetail.getDateByPoint(date, period.getBreakEndPoint(), period.getBreakEndType()));
            rule.setOriginBreakStartPoint(rule.getBreakStartPoint());
            rule.setOriginBreakEndPoint(rule.getBreakEndPoint());
            rule.setEarlyPoint(DateDetail.getDateByPoint(date, period.getEarlyPoint(), period.getEarlyType()));
            rule.setOutLackPoint(DateDetail.getDateByPoint(date, period.getOutLackPoint(), period.getOutLackType()));
            rule.setTenantId(period.getTenantId());
            rule.setPeriodWorkDay(period.getTimeSlotDay());
            rule.setPayrollHours(period.getPayrollHours());
            rule.setValidDuration(DateDetail.calculateMinuteDiff(rule.getInPoint(), rule.getOutPoint()));
            if (rule.getBreakEnable() == 1 && Objects.nonNull(rule.getBreakStartPoint()) && Objects.nonNull(rule.getBreakEndPoint())) {
                rule.setValidDuration(rule.getValidDuration() - DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()));
            }
            rule.setOriginValidDuration(rule.getValidDuration());

        }
        rules.add(rule);
        return rule;
    }

    /**
     * 构建公休REST的DailyRule实体
     */
    private void buildHalfRestDailyRule(FtbAttendanceDailyRule restRule, List<AttendanceShiftSettingPeriodVo> periods) {
        if (Objects.equals(restRule.getAttendanceType(), AttendanceTypeEnum.REST.getCode())) {
            if (CollUtil.isEmpty(periods)) {
                restRule.setPeriodWorkDay(BigDecimal.ONE);
                restRule.setPayrollHours(restRule.getPayrollHours());
                return;
            }
            if (periods.size() > 1) {
                AttendanceShiftSettingPeriodVo periodVo = periods.get(Objects.equals(restRule.getSchedulesType(), 0) ? 0 : restRule.getSchedulesType() - 1);
                restRule.setPeriodWorkDay(periodVo.getTimeSlotDay());
                restRule.setPayrollHours(periodVo.getPayrollHours());
                restRule.setValidDuration(periodVo.calValidDuration());
                restRule.setOriginValidDuration(periodVo.getValidDuration());
                restRule.setLateEnable(periodVo.getLateEnable());
                restRule.setEarlyEnable(periodVo.getEarlyEnable());
                restRule.setBreakEnable(periodVo.getBreakEnable());
                restRule.setInUnbounded(0);
                restRule.setOutUnbounded(0);
                restRule.setNextDayEnable(periodVo.getNextDayEnable());
                restRule.setSchedulesType(Objects.isNull(periodVo.getType()) ? 0 : periodVo.getType());
                restRule.setClockStartPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getClockStartPoint(), periodVo.getClockStartPointType()));
                restRule.setInPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getInPoint(), periodVo.getInType()));
                restRule.setOutPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getOutPoint(), periodVo.getOutType()));
                restRule.setOriginInPoint(restRule.getInPoint());
                restRule.setOriginOutPoint(restRule.getOutPoint());
                restRule.calLatePoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getLatePoint(), periodVo.getLateType()));
                restRule.setOriginalLatePoint(restRule.getLatePoint());
                restRule.setIsLateOutLateIn(0);
                restRule.calInLackPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getInLackPoint(), periodVo.getInLackPointType()));
                restRule.setBreakStartPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getBreakStartPoint(), periodVo.getBreakStartType()));
                restRule.setBreakEndPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getBreakEndPoint(), periodVo.getBreakEndType()));
                restRule.setOriginBreakStartPoint(restRule.getBreakStartPoint());
                restRule.setOriginBreakEndPoint(restRule.getBreakEndPoint());
                restRule.setEarlyPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getEarlyPoint(), periodVo.getEarlyType()));
                restRule.setOutLackPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getOutLackPoint(), periodVo.getOutLackType()));
                restRule.setTenantId(periodVo.getTenantId());
                return;
            }
            buildDay2HalfDailyRule(restRule, periods.get(0));
        }
    }

    private void buildDay2HalfDailyRule(FtbAttendanceDailyRule rule, AttendanceShiftSettingPeriodVo period) {
        rule.setSort(period.getSort());
        rule.setShiftId(period.getShiftId());
        rule.setPeriodId(period.getId());
        rule.setLateEnable(period.getLateEnable());
        rule.setEarlyEnable(period.getEarlyEnable());
        rule.setBreakEnable(period.getBreakEnable());
        rule.setNextDayEnable(period.getNextDayEnable());
        rule.setClockStartPoint(DateDetail.getDateByPoint(rule.getDay(), period.getClockStartPoint(), period.getClockStartPointType()));
        rule.setInPoint(DateDetail.getDateByPoint(rule.getDay(), period.getInPoint(), period.getInType()));
        rule.setOutPoint(DateDetail.getDateByPoint(rule.getDay(), period.getOutPoint(), period.getOutType()));
        rule.calInLackPoint(DateDetail.getDateByPoint(rule.getDay(), period.getInLackPoint(), period.getInLackPointType()));
        rule.calLatePoint(DateDetail.getDateByPoint(rule.getDay(), period.getLatePoint(), period.getLateType()));
        rule.setEarlyPoint(DateDetail.getDateByPoint(rule.getDay(), period.getEarlyPoint(), period.getEarlyType()));
        rule.setOutLackPoint(DateDetail.getDateByPoint(rule.getDay(), period.getOutLackPoint(), period.getOutLackType()));

        boolean isRest = Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.REST.getCode());
        rule.setOriginInPoint(rule.getInPoint());
        rule.setOriginOutPoint(rule.getOutPoint());
        rule.setBreakStartPoint(DateDetail.getDateByPoint(rule.getDay(), period.getBreakStartPoint(), period.getBreakStartType()));
        rule.setBreakEndPoint(DateDetail.getDateByPoint(rule.getDay(), period.getBreakEndPoint(), period.getBreakEndType()));
        //上半天休
        int periodMinuteAll = DateDetail.calculateMinuteDiff(rule.getInPoint(), rule.getOutPoint());
        DateTime point = cn.hutool.core.date.DateUtil.offsetMinute(rule.getInPoint(), periodMinuteAll / 2);
        if (Objects.equals(rule.getSchedulesType(), 1)) {
            if (Objects.nonNull(rule.getBreakStartPoint())) {
                Date outPoint = isRest ? rule.getBreakEndPoint() : rule.getBreakStartPoint();
                rule.setEarlyPoint(DateDetail.getNewDateByPeriod(rule.getOutPoint(), rule.getEarlyPoint(), outPoint));
                rule.setOutLackPoint(DateDetail.getNewDateByPeriod(rule.getOutPoint(), rule.getOutLackPoint(), outPoint));
                int breakMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint());
                int newMinute = DateDetail.calculateMinuteDiff(rule.getInPoint(), outPoint) - (isRest ? breakMinute : 0);
                int periodMinute = periodMinuteAll - breakMinute;
                rule.setOutPoint(outPoint);
                rule.setOriginOutPoint(outPoint);
                if (!isRest) {
                    rule.setBreakStartPoint(null);
                    rule.setBreakEndPoint(null);
                }
                rule.setPeriodWorkDay(period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinute), 1, RoundingMode.HALF_UP)));
                rule.setOriginValidDuration(rule.getValidDuration());
                return;
            }
            rule.setEarlyPoint(DateDetail.getNewDateByPeriod(rule.getOutPoint(), rule.getEarlyPoint(), point));
            rule.setOutLackPoint(DateDetail.getNewDateByPeriod(rule.getOutPoint(), rule.getOutLackPoint(), point));
            rule.setOutPoint(point);
            rule.setOriginOutPoint(point);
            int newMinute = DateDetail.calculateMinuteDiff(rule.getInPoint(), rule.getOutPoint());
            rule.setPeriodWorkDay(period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 1, RoundingMode.HALF_UP)));
            rule.setPayrollHours(period.getPayrollHours().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 1, RoundingMode.HALF_UP)));
            rule.setOriginValidDuration(rule.getValidDuration());
            return;
        }
        if (Objects.equals(rule.getSchedulesType(), 2)) {
            if (Objects.nonNull(rule.getBreakStartPoint())) {
                Date inPoint = isRest ? rule.getBreakStartPoint() : rule.getBreakEndPoint();
                rule.calInLackPoint(DateDetail.getNewDateByPeriod(rule.getInPoint(), rule.getInLackPoint(), inPoint));
                rule.calLatePoint(DateDetail.getNewDateByPeriod(rule.getInPoint(), rule.getLatePoint(), inPoint));
                int breakMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint());
                int newMinute = DateDetail.calculateMinuteDiff(inPoint, rule.getOutPoint()) - (isRest ? breakMinute : 0);
                int periodMinute = periodMinuteAll - breakMinute;
                rule.setInPoint(inPoint);
                rule.setOriginInPoint(inPoint);
                if (!isRest) {
                    rule.setBreakStartPoint(null);
                    rule.setBreakEndPoint(null);
                }
                rule.setPeriodWorkDay(period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinute), 1, RoundingMode.HALF_UP)));
                rule.setPayrollHours(period.getPayrollHours().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinute), 1, RoundingMode.HALF_UP)));
                rule.setOriginValidDuration(rule.getValidDuration());
                return;
            }
            rule.calInLackPoint(DateDetail.getNewDateByPeriod(rule.getInPoint(), rule.getInLackPoint(), point));
            rule.calLatePoint(DateDetail.getNewDateByPeriod(rule.getInPoint(), rule.getLatePoint(), point));
            rule.setOriginalLatePoint(rule.getLatePoint());
            rule.setIsLateOutLateIn(0);
            rule.setInPoint(point);
            rule.setOriginInPoint(point);
            int newMinute = DateDetail.calculateMinuteDiff(rule.getInPoint(), rule.getOutPoint());
            rule.setPeriodWorkDay(period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 1, RoundingMode.HALF_UP)));
            rule.setPayrollHours(period.getPayrollHours().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 1, RoundingMode.HALF_UP)));
            rule.setOriginValidDuration(rule.getValidDuration());
        }
    }

    private BigDecimal calPeriodWorkDay(Integer schedulesType, List<AttendanceShiftSettingPeriodVo> periods) {
        if (CollUtil.isEmpty(periods) || Objects.equals(schedulesType, 0)) {
            return BigDecimal.ONE;
        }
        if (periods.size() > 1) {
            return periods.get(schedulesType - 1).getTimeSlotDay();
        }
        AttendanceShiftSettingPeriodVo period = periods.get(0);
        Date date = new Date();
        Date periodInPoint = DateDetail.getDateByPoint(date, period.getInPoint(), period.getInType());
        Date periodOutPoint = DateDetail.getDateByPoint(date, period.getOutPoint(), period.getOutType());
        Date periodBreakStart = DateDetail.getDateByPoint(date, period.getBreakStartPoint(), period.getBreakStartType());
        Date periodBreakEnd = DateDetail.getDateByPoint(date, period.getBreakEndPoint(), period.getBreakEndType());
        int periodMinuteAll = DateDetail.calculateMinuteDiff(periodInPoint, periodOutPoint);
        int breakMinute = Objects.isNull(periodBreakStart) ? 0 : DateDetail.calculateMinuteDiff(periodBreakStart, periodBreakEnd);
        DateTime point = cn.hutool.core.date.DateUtil.offsetMinute(periodInPoint, periodMinuteAll / 2);
        if (Objects.equals(schedulesType, 1)) {
            if (Objects.nonNull(periodBreakEnd)) {
                int newMinute = DateDetail.calculateMinuteDiff(periodInPoint, periodBreakEnd) - breakMinute;
                int periodMinute = periodMinuteAll - breakMinute;
                return period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinute), 2, RoundingMode.HALF_UP));
            }
            int newMinute = DateDetail.calculateMinuteDiff(periodInPoint, point);
            return period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 2, RoundingMode.HALF_UP));
        }
        if (Objects.equals(schedulesType, 2)) {
            if (Objects.nonNull(periodBreakStart)) {
                int newMinute = DateDetail.calculateMinuteDiff(periodBreakStart, periodOutPoint) - breakMinute;
                int periodMinute = periodMinuteAll - breakMinute;
                return period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinute), 2, RoundingMode.HALF_UP));
            }
            int newMinute = DateDetail.calculateMinuteDiff(point, periodOutPoint);
            return period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 2, RoundingMode.HALF_UP));
        }
        return period.getTimeSlotDay();
    }

    /**
     * 构建针对填充数据的考勤规则
     *
     * @param groupId            部门ID，标识考勤规则适用的部门
     * @param userId             用户ID，标识考勤规则适用的用户
     * @param date               日期，标识考勤规则适用的日期
     * @param applyViewEnable    考勤类型，如周休日、节假日等
     * @param attendanceTypeEnum 考勤类型枚举，用于详细指定考勤规则的类型
     * @return 返回构建的考勤规则对象
     */
    public FtbAttendanceDailyRule buildRuleForNail(String groupId, String userId, Date date, Integer applyViewEnable, AttendanceTypeEnum attendanceTypeEnum) {
        // 创建一个新的考勤规则对象
        FtbAttendanceDailyRule rule = new FtbAttendanceDailyRule();
        // 设置部门ID
        rule.setGroupId(groupId);
        // 设置考勤类型
        rule.setSchedulesType(0);
        // 设置用户ID
        rule.setUserId(userId);
        // 设置未删除标记
        rule.setDeleteMark(0);
        // 设置非自建群组
        rule.setSelfGroup(0);
        // 生成并设置唯一ID
        rule.setId(RandomUtil.uuId());
        // 设置适用日期
        rule.setDay(date);
        // 设置考勤类型代码
        rule.setAttendanceType(attendanceTypeEnum.getCode());
        rule.setApplyViewEnable(applyViewEnable);
        // 设置有效时长为0，表示全天有效
        rule.setValidDuration(0);
        // 返回构建的考勤规则对象
        return rule;
    }

    /**
     * 根据时段及用户集合、考勤组集合查询规则列表
     *
     * @param start   开始时间
     * @param end     结束时间
     * @param userIds 用户id集合
     * @param groupId 考勤id集合
     * @return
     */
    private List<FtbAttendanceDailyRule> getDailyRulesByPeriod(Date start, Date end, List<String> userIds, List<String> groupId) {
        List<FtbAttendanceDailyRule> list = lambdaQuery().in(CollUtil.isNotEmpty(groupId), FtbAttendanceDailyRule::getGroupId, groupId).and(x -> x.or(x1 -> x1.eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode()).between(FtbAttendanceDailyRule::getDay, start, end)).or(x1 -> x1.ge(FtbAttendanceDailyRule::getOutPoint, start).le(FtbAttendanceDailyRule::getInPoint, end)).or(x1 -> x1.eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode()).lt(FtbAttendanceDailyRule::getDay, start).le(FtbAttendanceDailyRule::getApplyStart, end).ge(FtbAttendanceDailyRule::getApplyEnd, start)).or(x1 -> x1.between(FtbAttendanceDailyRule::getDay, cn.hutool.core.date.DateUtil.beginOfDay(start), end).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode(), AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.HOLIDAYS.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode()))).in(CollUtil.isNotEmpty(userIds), FtbAttendanceDailyRule::getUserId, userIds).list();
        return BeanUtil.copyToList(list, FtbAttendanceDailyRule.class);
    }

    /**
     * 是否存在划线排班
     *@param userId 用户id
     * @param start   开始时间
     * @param end     结束时间
     * @return
     */
    @Override
    public boolean hasLinearRulesByPeriod(String userId, Date start, Date end) {
        Long count = lambdaQuery().eq(FtbAttendanceDailyRule::getFixedMark, 2).and(x -> x.or(x1 -> x1.eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode()).between(FtbAttendanceDailyRule::getDay, start, end)).or(x1 -> x1.ge(FtbAttendanceDailyRule::getOutPoint, start).le(FtbAttendanceDailyRule::getInPoint, end)).or(x1 -> x1.between(FtbAttendanceDailyRule::getDay, cn.hutool.core.date.DateUtil.beginOfDay(start), end).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode(), AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.HOLIDAYS.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode()))).eq(FtbAttendanceDailyRule::getUserId, userId).count();
        return count > 0;
    }

    /**
     * 根据时段及用户集合、考勤组集合查询规则列表，查询时间为日
     *
     * @param start   开始时间
     * @param end     结束时间
     * @param userIds 用户id集合
     * @param groupId 考勤id集合
     * @return
     */
    private List<FtbAttendanceDailyRule> getDailyRulesByPeriodForStepOut(Date start, Date end, List<String> userIds, List<String> groupId) {
        List<FtbAttendanceDailyRule> list = lambdaQuery().between(FtbAttendanceDailyRule::getDay, start, end).in(CollUtil.isNotEmpty(userIds), FtbAttendanceDailyRule::getUserId, userIds).in(CollUtil.isNotEmpty(groupId), FtbAttendanceDailyRule::getGroupId, groupId).list();
        return BeanUtil.copyToList(list, FtbAttendanceDailyRule.class);
    }


    /**
     * 划线排班（支持同用户不同日期不同时段）
     *
     * @param configDto 排班配置
     * @return 错误信息，空表示成功
     */
    private String setLineDrawingSchedulesWithMultiPeriods(LineDrawingSchedulesConfigDto configDto) {
        // 1. 参数验证
        Assert.isFalse(Objects.isNull(configDto), "未查询到排班配置数据！");
        Assert.isFalse(StringUtil.isEmpty(configDto.getGroupId()), "考勤组ID不能为空！");
        Assert.isFalse(CollUtil.isEmpty(configDto.getUserList()), "未找到需要划线排班的用户");

        // 2. 收集所有日期和用户ID（去重）
        Set<Date> allDatesSet = new HashSet<>();
        List<String> userIds = new ArrayList<>();

        for (LineDrawingUserDto userDto : configDto.getUserList()) {
            userIds.add(userDto.getUserId());
            if (CollUtil.isNotEmpty(userDto.getPeriods())) {
                for (LineDrawingPeriodDto period : userDto.getPeriods()) {
                    if (Objects.nonNull(period.getDay())) {
                        allDatesSet.add(period.getDay());
                    }
                }
            }
        }

        Assert.isFalse(allDatesSet.isEmpty(), "未找到排班日期");

        Date start = allDatesSet.stream().min(Date::compareTo).orElse(null);
        Date end = cn.hutool.core.date.DateUtil.endOfDay(allDatesSet.stream().max(Date::compareTo).orElse(null));

        // 扩展日期范围（前后各一天，用于冲突检测）
        List<Date> days = new ArrayList<>(allDatesSet);
        days.add(cn.hutool.core.date.DateUtil.offsetDay(start, -1));
        days.add(cn.hutool.core.date.DateUtil.offsetDay(end, 1));

        // 3. 获取分布式锁
        RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), configDto.getGroupId()));
        Assert.isFalse(lock.isLocked(), "当前考勤组排班操作正在执行中，请稍后再试");

        try {
            Assert.isFalse(!lock.tryLock(10, 60, TimeUnit.SECONDS), "当前考勤组排班操作正在执行中，请稍后再试");

            // 4. 查询基础数据
            FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulingConfigService.getByGroupId(configDto.getGroupId());
            Assert.isFalse(Objects.isNull(lineSchedulingConfig), "未查询到考勤组的划线排班配置！");

            List<AttendanceGroupUser> users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, userIds, List.of(configDto.getGroupId()));
            Assert.isFalse(CollUtil.isEmpty(users), "当前人员所选日期不在考勤组！");
            Map<String, List<AttendanceGroupUser>> usersMap = users.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));

            // 5. 查询历史规则
            List<FtbAttendanceDailyRule> hisRulesAll = lambdaQuery()
                    .eq(FtbAttendanceDailyRule::getGroupId, configDto.getGroupId())
                    .in(FtbAttendanceDailyRule::getUserId, userIds)
                    .in(FtbAttendanceDailyRule::getDay, days)
                    .list();

            Map<String, List<FtbAttendanceDailyRule>> ruleMap = hisRulesAll.stream()
                    .collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF)));
            Map<String, List<FtbAttendanceDailyRule>> ruleUserMap = hisRulesAll.stream()
                    .collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId));

            // 6. 处理每个用户的排班
            List<DailyRuleResultVo> resultList = new CopyOnWriteArrayList<>();
            List<FtbAttendanceDailyRule> allRules = new CopyOnWriteArrayList<>();
            List<String> allRuleIds = new CopyOnWriteArrayList<>();
            List<Boolean> isExists = new CopyOnWriteArrayList<>();
            List<FtbAttendanceLineSchedulingPayrollHours> payrollHoursList = new CopyOnWriteArrayList<>();

            for (LineDrawingUserDto userDto : configDto.getUserList()) {
                processUserLineDrawingSchedules(userDto, configDto.getGroupId(), lineSchedulingConfig,
                        usersMap, ruleMap, ruleUserMap, days,
                        resultList, allRules, allRuleIds, isExists, payrollHoursList);
            }

            Assert.isFalse(CollUtil.isNotEmpty(isExists) && isExists.stream().noneMatch(boo -> boo), "当前时间不能复制");

            // 7. 保存数据
            saveOrUpdateBatch(allRules, allRuleIds);
            savePayrollHoursAndHandleNotification(configDto, userIds, days, lineSchedulingConfig,
                    resultList, payrollHoursList);

            return resultSchedulingHandle(resultList).stream()
                    .map(DailyRuleResultVo::getFailMsg)
                    .filter(StringUtil::isNotEmpty)
                    .reduce((msg1, msg2) -> msg1 + "\br" + msg2)
                    .orElse("");

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 处理单个用户的划线排班
     */
    private void processUserLineDrawingSchedules(LineDrawingUserDto userDto, String groupId,
                                                 FtbAttendanceLineSchedulingConfig lineSchedulingConfig,
                                                 Map<String, List<AttendanceGroupUser>> usersMap,
                                                 Map<String, List<FtbAttendanceDailyRule>> ruleMap,
                                                 Map<String, List<FtbAttendanceDailyRule>> ruleUserMap,
                                                 List<Date> days,
                                                 List<DailyRuleResultVo> resultList,
                                                 List<FtbAttendanceDailyRule> allRules,
                                                 List<String> allRuleIds,
                                                 List<Boolean> isExists,
                                                 List<FtbAttendanceLineSchedulingPayrollHours> payrollHoursList) {
        String userId = userDto.getUserId();
        List<LineDrawingPeriodDto> allPeriods = userDto.getPeriods();

        // 按日期分组时段
        Map<String, List<LineDrawingPeriodDto>> dayPeriodsMap = new HashMap<>();
        Map<String, BigDecimal> dayPayrollHoursMap = new HashMap<>();

        if (CollUtil.isNotEmpty(allPeriods)) {
            for (LineDrawingPeriodDto period : allPeriods) {
                String dayStr = DateDetail.getDate2Str(period.getDay(), DateDetail.DF);
                dayPeriodsMap.computeIfAbsent(dayStr, k -> new ArrayList<>()).add(period);

                if (Objects.nonNull(userDto.getPayrollHours())) {
                    dayPayrollHoursMap.put(dayStr, userDto.getPayrollHours());
                }
            }
        }

        // 用于快速查找排班日期
        Set<String> schedulingDays = dayPeriodsMap.keySet();

        // 检查时段是否未变化（优化：避免重复保存）
        if (shouldSkipUpdate(allPeriods, ruleUserMap, userId)) {
            // 只更新计薪工时
            for (Map.Entry<String, BigDecimal> entry : dayPayrollHoursMap.entrySet()) {
                Date day = DateUtil.stringToDates(entry.getKey());
                payrollHoursList.add(FtbAttendanceLineSchedulingPayrollHours.builder()
                        .day(day).groupId(groupId).userId(userId)
                        .payrollHours(entry.getValue()).build());
            }
            return;
        }

        // 处理排班
        List<AttendanceGroupUser> userList = usersMap.getOrDefault(userId, CollUtil.newArrayList());
        List<FtbAttendanceDailyRule> periodConflictRules = CollUtil.newArrayList();

        for (Date day : days) {
            List<AttendanceGroupUser> attendanceGroupUsers = attendanceGroupUserService.getAttendanceGroupUsers(
                    day, cn.hutool.core.date.DateUtil.endOfDay(day), userList);
            if (CollUtil.isEmpty(attendanceGroupUsers)) {
                continue;
            }

            AttendanceGroupUser user = attendanceGroupUsers.stream().reduce((r1, r2) -> r2).orElse(null);
            String date2Str = DateDetail.getDate2Str(day, DateDetail.DF);
            List<FtbAttendanceDailyRule> hisRules = ruleMap.getOrDefault(userId + date2Str, CollUtil.newArrayList());

            // 非排班日期，只检查冲突
            if (!schedulingDays.contains(date2Str)) {
                periodConflictRules.addAll(hisRules);
                continue;
            }

            // 处理排班日期的规则
            processDrawingDay(userDto, groupId, userId, day, date2Str, dayPeriodsMap, dayPayrollHoursMap,
                    attendanceGroupUsers, user, hisRules, lineSchedulingConfig,
                    allRuleIds, isExists, resultList, allRules, periodConflictRules, payrollHoursList);
        }

        Assert.isTrue(fixedPeriodConflict(periodConflictRules), "设置班次时段与前一日/后一日班次时段存在冲突！");
    }

    /**
     * 检查是否应该跳过更新（时段未变化）
     */
    private boolean shouldSkipUpdate(List<LineDrawingPeriodDto> allPeriods,
                                     Map<String, List<FtbAttendanceDailyRule>> ruleUserMap, String userId) {
        if (CollUtil.isEmpty(allPeriods) || !allPeriods.stream().allMatch(vo -> StringUtil.isNotEmpty(vo.getId()))) {
            return false;
        }

        List<FtbAttendanceDailyRule> list = ruleUserMap.getOrDefault(userId, CollUtil.newArrayList());
        List<String> ids = allPeriods.stream().map(LineDrawingPeriodDto::getId).collect(Collectors.toList());
        List<String> ruleIds = list.stream()
                .filter(rule -> Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()))
                .map(FtbAttendanceDailyRule::getId)
                .collect(Collectors.toList());

        return CollectionUtils.isEqualCollection(ids, ruleIds);
    }

    /**
     * 处理单个排班日期的规则
     */
    private void processDrawingDay(LineDrawingUserDto userDto, String groupId, String userId,
                                   Date day, String date2Str,
                                   Map<String, List<LineDrawingPeriodDto>> dayPeriodsMap,
                                   Map<String, BigDecimal> dayPayrollHoursMap,
                                   List<AttendanceGroupUser> attendanceGroupUsers,
                                   AttendanceGroupUser user,
                                   List<FtbAttendanceDailyRule> hisRules,
                                   FtbAttendanceLineSchedulingConfig lineSchedulingConfig,
                                   List<String> allRuleIds, List<Boolean> isExists,
                                   List<DailyRuleResultVo> resultList,
                                   List<FtbAttendanceDailyRule> allRules,
                                   List<FtbAttendanceDailyRule> periodConflictRules,
                                   List<FtbAttendanceLineSchedulingPayrollHours> payrollHoursList) {
        List<LineDrawingPeriodDto> dayPeriods = dayPeriodsMap.getOrDefault(date2Str, CollUtil.newArrayList());
        BigDecimal dayPayrollHours = dayPayrollHoursMap.get(date2Str);

        // 保存计薪工时
        if (Objects.nonNull(dayPayrollHours)) {
            payrollHoursList.add(FtbAttendanceLineSchedulingPayrollHours.builder()
                    .day(day).groupId(groupId).userId(userId)
                    .payrollHours(dayPayrollHours).build());
        }

        // 提取历史规则ID
        List<String> ruleIds = hisRules.stream()
                .map(FtbAttendanceDailyRule::getId)
                .filter(StringUtil::isNotBlank)
                .collect(Collectors.toList());
        allRuleIds.addAll(ruleIds);

        List<FtbAttendanceDailyRule> newRules = new ArrayList<>();

        if (CollUtil.isNotEmpty(dayPeriods)) {
            // 遍历该日期的每个时段
            for (LineDrawingPeriodDto period : dayPeriods) {
                Date startByPoint = DateDetail.getDateByPoint(day, period.getStart(), period.getStartType());
                Date endByPoint = DateDetail.getDateByPoint(day, period.getEnd(), period.getEndType());
                Integer existStatus = isExistStatus(attendanceGroupUsers, startByPoint, endByPoint);

                if (Objects.equals(existStatus, -1) || Objects.equals(existStatus, 0) || Objects.equals(existStatus, 2)) {
                    isExists.add(Boolean.FALSE);
                    continue;
                }
                isExists.add(Boolean.TRUE);

                newRules.add(buildLineSchedulesRule(groupId, user.getType(), day, startByPoint, endByPoint, userId, lineSchedulingConfig));
            }
        } else if (hisRules.stream().anyMatch(rule -> Objects.equals(rule.getFixedMark(), 2))) {
            // 清除规则
            FtbAttendanceDailyRule clearRule = buildLineSchedulesRule(groupId, user.getType(), day, day, day, userId, lineSchedulingConfig);
            clearRule.setAttendanceType(AttendanceTypeEnum.CLEAR.getCode());
            newRules.add(clearRule);
            isExists.add(Boolean.TRUE);
        }

        // 处理规则安排
        dailyRuleArrangementHandle(resultList, hisRules, newRules);
        attendanceGroupUsers.forEach(user1 -> secondmentDailyRulesHandle(resultList, user1, hisRules));
        fillInNailRuleForOnlyStepOut(hisRules);

        allRules.addAll(hisRules);
        periodConflictRules.addAll(hisRules);
    }

    /**
     * 保存计薪工时并处理通知
     */
    private void savePayrollHoursAndHandleNotification(LineDrawingSchedulesConfigDto configDto,
                                                       List<String> userIds, List<Date> days,
                                                       FtbAttendanceLineSchedulingConfig lineSchedulingConfig,
                                                       List<DailyRuleResultVo> resultList,
                                                       List<FtbAttendanceLineSchedulingPayrollHours> payrollHoursList) {
        // 查询原有计薪工时
        List<FtbAttendanceLineSchedulingPayrollHours> oldPayrollHoursList = lineSchedulingPayrollHoursService.lambdaQuery()
                .in(FtbAttendanceLineSchedulingPayrollHours::getUserId, userIds)
                .eq(FtbAttendanceLineSchedulingPayrollHours::getGroupId, configDto.getGroupId())
                .in(FtbAttendanceLineSchedulingPayrollHours::getDay, days)
                .eq(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 0)
                .list();

        Map<String, BigDecimal> oldPayrollHoursMap = oldPayrollHoursList.stream()
                .collect(Collectors.toMap(
                        ph -> ph.getUserId() + DateDetail.getDateTime2Str(ph.getDay()) + ph.getPayrollHours(),
                        FtbAttendanceLineSchedulingPayrollHours::getPayrollHours
                ));

        // 保存新计薪工时
        lineSchedulingPayrollHoursService.savePayrollHoursList(payrollHoursList);

        // 发送通知
        if (Objects.equals(lineSchedulingConfig.getEmployeeNotify(), 1)) {
            attendanceRuleNotificationHandle.sendSystemNoticeForLine(resultList, configDto, days, null);
        }

        // 比对计薪工时变动
        Map<String, DailyRuleResultVo> resultMap = resultList.stream()
                .collect(Collectors.toMap(
                        vo -> vo.getUserId() + DateDetail.getDateTime2Str(vo.getDate()),
                        Function.identity(),
                        (r1, r2) -> r1
                ));

        payrollHoursList.stream()
                .filter(vo -> Objects.nonNull(vo.getPayrollHours()))
                .filter(vo -> {
                    String key = vo.getUserId() + DateDetail.getDateTime2Str(vo.getDay());
                    return !oldPayrollHoursMap.containsKey(key + vo.getPayrollHours()) && !resultMap.containsKey(key);
                })
                .forEach(vo -> {
                    DailyRuleResultVo dailyRuleResultVo = new DailyRuleResultVo();
                    dailyRuleResultVo.setType(0);
                    dailyRuleResultVo.setGroupId(vo.getGroupId());
                    dailyRuleResultVo.setUserId(vo.getUserId());
                    dailyRuleResultVo.setDate(vo.getDay());
                    resultList.add(dailyRuleResultVo);
                });

        // 处理考勤规则通知
        attendanceRuleNotificationHandle.notificationHandle(resultList, false);
    }

    public String setLineDrawingSchedules(LineDrawingSchedulesConfigDto configDto) {
        // 1. 参数验证
        Assert.isFalse(Objects.isNull(configDto), "未查询到排班配置数据！");
        Assert.isFalse(StringUtil.isEmpty(configDto.getGroupId()), "考勤组ID不能为空！");
        Assert.isFalse(StringUtil.isEmpty(configDto.getDays()), "结束日期不能为空！");
        List<Date> schedulesDays = Arrays.stream(StringUtil.split(configDto.getDays(), ",")).map(DateUtil::stringToDates).collect(Collectors.toList());

        Date start = schedulesDays.stream().min(Date::compareTo).orElse(null);
        Date end = cn.hutool.core.date.DateUtil.endOfDay(schedulesDays.stream().max(Date::compareTo).orElse(null));
        DateTime theDayBefore = cn.hutool.core.date.DateUtil.offsetDay(start, -1);
        DateTime theDayAfter = cn.hutool.core.date.DateUtil.offsetDay(end, 1);
        List<Date> days = new ArrayList<>(schedulesDays);
        days.add(0, theDayBefore);
        days.add(theDayAfter);
        // 2. 获取分布式锁，防止并发操作
        RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), configDto.getGroupId()));
        Assert.isFalse(lock.isLocked(), "当前考勤组排班操作正在执行中，请稍后再试");

        try {
            // 3. 尝试获取锁
            Assert.isFalse(!lock.tryLock(5, 100, TimeUnit.SECONDS), "当前考勤组排班操作正在执行中，请稍后再试");
            // 4. 根据考勤组ID查询划线排班配置
            FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulingConfigService.getByGroupId(configDto.getGroupId());
            Assert.isFalse(Objects.isNull(lineSchedulingConfig), "未查询到考勤组的划线排班配置！");
            // 6. 获取所有用户ID
            List<String> userIds = configDto.getUserList().stream().map(LineDrawingUserDto::getUserId).collect(Collectors.toList());
            Assert.isFalse(CollUtil.isEmpty(userIds), "未找到需要划线排班的用户");
            // 7. 查询用户考勤组信息
            List<AttendanceGroupUser> users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, userIds, List.of(configDto.getGroupId()));
            Assert.isFalse(CollUtil.isEmpty(users), "当前人员所选日期不在考勤组！");
            Map<String, List<AttendanceGroupUser>> usersMap = users.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));
            // 9. 初始化结果列表
            List<DailyRuleResultVo> resultList = new CopyOnWriteArrayList<>();
            List<FtbAttendanceDailyRule> allRules = new CopyOnWriteArrayList<>();
            List<String> allRuleIds = new CopyOnWriteArrayList<>();
            // 11.3.2 查询历史考勤规则
            List<FtbAttendanceDailyRule> hisRulesAll = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, configDto.getGroupId()).in(FtbAttendanceDailyRule::getUserId, userIds).in(FtbAttendanceDailyRule::getDay, days).list();
            Map<String, List<FtbAttendanceDailyRule>> ruleMap = hisRulesAll.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF)));
            Map<String, List<FtbAttendanceDailyRule>> ruleUserMap = hisRulesAll.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId));
            List<Boolean> isExists = new CopyOnWriteArrayList<>();
            List<FtbAttendanceLineSchedulingPayrollHours> payrollHoursList = new CopyOnWriteArrayList<>();
            // 11. 遍历每个用户
            configDto.getUserList().forEach(userDto -> {
                // 11.3.4 构建新的考勤规则
                List<FtbAttendanceDailyRule> newRules = new ArrayList<>();
                String userId = userDto.getUserId();
                List<LineDrawingPeriodDto> periods = userDto.getPeriods();
                if (CollUtil.isNotEmpty(periods) && periods.stream().allMatch(vo -> StringUtil.isNotEmpty(vo.getId()))) {
                    //如果划线排班时段都存在id,则判断已有的只包含普班排班是否的id是否和划线排班时段的全部id一致
                    List<FtbAttendanceDailyRule> list = ruleUserMap.getOrDefault(userId, CollUtil.newArrayList());
                    List<String> ids = periods.stream().map(LineDrawingPeriodDto::getId).collect(Collectors.toList());
                    List<String> ruleIds = list.stream().filter(rule -> Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode())).map(FtbAttendanceDailyRule::getId).collect(Collectors.toList());
                    if (CollectionUtils.isEqualCollection(ids, ruleIds)) {
                        schedulesDays.forEach(day -> payrollHoursList.add(FtbAttendanceLineSchedulingPayrollHours.builder().day(day).groupId(configDto.getGroupId()).userId(userDto.getUserId()).payrollHours(userDto.getPayrollHours()).build()));
                        return;
                    }
                }
                // 11.3.14 处理借调日常规则
                List<AttendanceGroupUser> userList = usersMap.getOrDefault(userId, CollUtil.newArrayList());
                // 初始化一个列表，用于存储时段冲突的考勤规则
                List<FtbAttendanceDailyRule> periodConflictRules = CollUtil.newArrayList();
                for (Date day : days) {
                    List<AttendanceGroupUser> attendanceGroupUsers = attendanceGroupUserService.getAttendanceGroupUsers(day, cn.hutool.core.date.DateUtil.endOfDay(day), userList);
                    if (CollUtil.isEmpty(attendanceGroupUsers)) {
                        continue;
                    }
                    AttendanceGroupUser user = attendanceGroupUsers.stream().reduce((r1, r2) -> r2).orElse(null);
                    String date2Str = DateDetail.getDate2Str(day, DateDetail.DF);
                    List<FtbAttendanceDailyRule> hisRules = ruleMap.getOrDefault(userId + date2Str, CollUtil.newArrayList());
                    if (!configDto.getDays().contains(date2Str)) {
                        periodConflictRules.addAll(hisRules);
                        continue;
                    }
                    payrollHoursList.add(FtbAttendanceLineSchedulingPayrollHours.builder().day(day).groupId(configDto.getGroupId()).userId(userDto.getUserId()).payrollHours(userDto.getPayrollHours()).build());
                    // 11.3.3 提取历史规则ID
                    List<String> ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList());
                    allRuleIds.addAll(ruleIds);
                    newRules.clear();
                    if (CollUtil.isNotEmpty(periods)) {
                        // 11.2 遍历每个时段
                        // 11.3 遍历每个日期，构建考勤规则
                        for (LineDrawingPeriodDto period : periods) {
                            Date startByPoint = DateDetail.getDateByPoint(day, period.getStart(), period.getStartType());
                            Date endByPoint = DateDetail.getDateByPoint(day, period.getEnd(), period.getEndType());
                            Integer existStatus = isExistStatus(attendanceGroupUsers, startByPoint, endByPoint);
                            if (Objects.equals(existStatus, -1) || Objects.equals(existStatus, 0) || Objects.equals(existStatus, 2)) {
                                isExists.add(Boolean.FALSE);
                                continue;
                            }
                            isExists.add(Boolean.TRUE);
                            // 11.3.12 添加到新规则列表
                            newRules.add(buildLineSchedulesRule(configDto.getGroupId(), user.getType(), day, startByPoint, endByPoint, userId, lineSchedulingConfig));
                        }
                    } else if (hisRules.stream().anyMatch(rule -> Objects.equals(rule.getFixedMark(), 2))) {
                        //清除规则
                        FtbAttendanceDailyRule ftbAttendanceDailyRule = buildLineSchedulesRule(configDto.getGroupId(), user.getType(), day, day, day, userId, lineSchedulingConfig);
                        ftbAttendanceDailyRule.setAttendanceType(AttendanceTypeEnum.CLEAR.getCode());
                        newRules.add(ftbAttendanceDailyRule);
                        isExists.add(Boolean.TRUE);
                    }
                    // 11.3.13 处理规则安排
                    dailyRuleArrangementHandle(resultList, hisRules, newRules);
                    attendanceGroupUsers.forEach(user1 -> secondmentDailyRulesHandle(resultList, user1, hisRules));
                    // 11.3.15 填充仅外出的规则
                    fillInNailRuleForOnlyStepOut(hisRules);
                    // 11.3.16 添加到所有规则列表
                    allRules.addAll(hisRules);
                    periodConflictRules.addAll(hisRules);
                }
                //判断时段是否冲突
                Assert.isTrue(fixedPeriodConflict(periodConflictRules), "设置班次时段与前一日/后一日班次时段存在冲突！");
            });
            Assert.isFalse(CollUtil.isNotEmpty(isExists) && isExists.stream().noneMatch(boo -> boo), "当前时间不能复制");
            // 12. 保存或更新考勤规则
            saveOrUpdateBatch(allRules, allRuleIds);
            List<FtbAttendanceLineSchedulingPayrollHours> oldPayrollHoursList = lineSchedulingPayrollHoursService.lambdaQuery().in(FtbAttendanceLineSchedulingPayrollHours::getUserId, userIds).eq(FtbAttendanceLineSchedulingPayrollHours::getGroupId, configDto.getGroupId()).in(FtbAttendanceLineSchedulingPayrollHours::getDay, days).eq(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 0).list();
            //根据用户id与日期聚合
            Map<String, BigDecimal> oldPayrollHoursMap = oldPayrollHoursList.stream().collect(Collectors.toMap(payrollHours -> payrollHours.getUserId() + DateDetail.getDateTime2Str(payrollHours.getDay()) + payrollHours.getPayrollHours(), FtbAttendanceLineSchedulingPayrollHours::getPayrollHours));
            //保存划线排班计薪工时设置
            lineSchedulingPayrollHoursService.savePayrollHoursList(payrollHoursList);
            if (Objects.equals(lineSchedulingConfig.getEmployeeNotify(), 1)) {
                // 13. 发送系统通知
                attendanceRuleNotificationHandle.sendSystemNoticeForLine(resultList, configDto, null);
            }
            //比对出计薪工时有变动的数据,oldPayrollHoursList为原计薪工时数据,payrollHoursList为新计薪工时数据
            Map<String, DailyRuleResultVo> collect = resultList.stream().collect(Collectors.toMap(vo -> vo.getUserId() + DateDetail.getDateTime2Str(vo.getDate()), Function.identity(), (r1, r2) -> r1));
            payrollHoursList.stream().filter(vo -> Objects.nonNull(vo.getPayrollHours())).filter(vo -> {
                String key = vo.getUserId() + DateDetail.getDateTime2Str(vo.getDay());
                return !oldPayrollHoursMap.containsKey(key + vo.getPayrollHours()) && !collect.containsKey(key);
            }).forEach(vo -> {
                DailyRuleResultVo dailyRuleResultVo = new DailyRuleResultVo();
                dailyRuleResultVo.setType(0);
                dailyRuleResultVo.setGroupId(vo.getGroupId());
                dailyRuleResultVo.setUserId(vo.getUserId());
                dailyRuleResultVo.setDate(vo.getDay());
                resultList.add(dailyRuleResultVo);
            });
            String result = resultSchedulingHandle(resultList).stream().map(DailyRuleResultVo::getFailMsg).filter(StringUtil::isNotEmpty).reduce((msg1, msg2) -> msg1 + "\br" + msg2).orElse("");
            // 14. 处理考勤规则通知
            attendanceRuleNotificationHandle.notificationHandle(resultList, false);
            // 15. 返回处理结果
            return result;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 16. 释放分布式锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 查询指定日期集合指定考勤组指定人员是否存在划线排班
     */
    @Override
    public boolean queryLineSchedulingExist(LineDrawingSchedulesConfigDto configDto) {
        // 1. 参数验证
        Assert.isFalse(Objects.isNull(configDto), "未查询到排班配置数据！");
        Assert.isFalse(StringUtil.isEmpty(configDto.getGroupId()), "考勤组ID不能为空！");
        Assert.isFalse(StringUtil.isEmpty(configDto.getDays()), "结束日期不能为空！");
        List<String> userIds = configDto.getUserList().stream().map(LineDrawingUserDto::getUserId).distinct().collect(Collectors.toList());
        List<Date> days = Arrays.stream(StringUtil.split(configDto.getDays(), ",")).map(DateUtil::stringToDates).collect(Collectors.toList());
        return lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, configDto.getGroupId()).eq(FtbAttendanceDailyRule::getFixedMark, 2).in(FtbAttendanceDailyRule::getUserId, userIds).in(FtbAttendanceDailyRule::getDay, days).count() > 0;
    }

    /**
     * 构建划线排班规则
     *
     * @param groupId 考勤组id
     * @param day       日期
     * @param start    时段开始
     * @param end    时段结束
     * @param userId    用户ID
     * @param lineSchedulingConfig 排班配置
     * @return 排班规则
     */
    @NotNull
    public static FtbAttendanceDailyRule buildLineSchedulesRule(String groupId, Integer selfGroup, Date day, Date start, Date end, String userId, FtbAttendanceLineSchedulingConfig lineSchedulingConfig) {
        FtbAttendanceDailyRule rule = new FtbAttendanceDailyRule();
        rule.setId(RandomUtil.uuId());
        rule.setGroupId(groupId);
        rule.setSelfGroup(selfGroup);
        rule.setPeriodId(rule.getId());
        rule.setShiftId(rule.getId());
        rule.setUserId(userId);
        rule.setDay(day);

        // 11.3.6 设置考勤类型为普通出勤
        rule.setAttendanceType(AttendanceTypeEnum.ORDINARY.getCode());

        // 11.3.7 设置排班类型
        rule.setSchedulesType(0);

        // 11.3.8 解析时段的上班和下班时间
        // 将BigDecimal小时数转换为分钟数进行日期偏移
        BigDecimal startCheckInLimit = lineSchedulingConfig.getStartCheckInLimit() != null ? lineSchedulingConfig.getStartCheckInLimit() : BigDecimal.ZERO;
        BigDecimal endCheckInLimit = lineSchedulingConfig.getEndCheckInLimit() != null ? lineSchedulingConfig.getEndCheckInLimit() : BigDecimal.ZERO;

        // 计算分钟数：小时数 * 60，转换为int值
        int startCheckInMinutes = startCheckInLimit.multiply(BigDecimal.valueOf(60)).intValue();
        int endCheckInMinutes = endCheckInLimit.multiply(BigDecimal.valueOf(60)).intValue();

        // 使用offsetMinute方法进行日期偏移
        Date clockStartPoint = cn.hutool.core.date.DateUtil.offsetMinute(start, -startCheckInMinutes);
        Date outLackPoint = cn.hutool.core.date.DateUtil.offsetMinute(end, endCheckInMinutes);

        // 11.3.9 设置上班开始时间和下班开始时间
        rule.setInPoint(start);
        rule.setInLackPoint(start);
        rule.setLateEnable(0);
        rule.setEarlyEnable(0);
        rule.setOriginInPoint(start);
        rule.setOutPoint(end);
        rule.setOriginOutPoint(end);
        rule.setClockStartPoint(clockStartPoint);
        rule.setOutLackPoint(outLackPoint);
        // 11.3.10 其他时间点设为空
        rule.setLatePoint(null);
        rule.setEarlyPoint(null);
        rule.setBreakStartPoint(null);
        rule.setBreakEndPoint(null);
        rule.setOriginBreakStartPoint(null);
        rule.setOriginBreakEndPoint(null);
        rule.setFixedMark(2);
        // 11.3.11 设置其他必要属性
        rule.setApplyViewEnable(1);
        rule.setDeleteMark(0);
        rule.setTenantId(UserProvider.getUser().getTenantId());
        rule.setCreatorUserId(UserProvider.getLoginUserId());
        rule.setCreatorTime(new Date());
        rule.setLastModifyUserId(UserProvider.getLoginUserId());
        rule.setLastModifyTime(new Date());
        rule.setOriginValidDuration(rule.getValidDuration());
        return rule;
    }

    /**
     * 排班导出
     *
     * @param groupId 考勤组id
     * @param workGroupId 班组id
     * @param month 月份
     * @return
     */
    @Override
    public void lineSchedulesExport(String groupId, String workGroupId, String month, List<String> userIdList) {
        // 划线排班班次查询
        Date date = DateUtil.stringToDates(month + "-01");
        List<DayInfo> monthDayInfos = getMonthDayInfos(date);
        List<LineSchedulesVo> schedulesList = getLineSchedulesList(groupId, workGroupId, monthDayInfos.stream().map(DayInfo::getDay).collect(Collectors.toList()), userIdList);
        AttendanceGroup byId = attendanceGroupService.getById(groupId);
        List<String> heads = monthDayInfos.stream().map(dayInfo -> dayInfo.getWeekStr() + (char) 10 + dayInfo.getDayNum()).collect(Collectors.toList());
        OrganizeGeneralDetailVO infoById = organizeApi.organizeInfoById(StringUtil.isNotEmpty(workGroupId) ? OrganizeCategoryEnums.TEAM : null, StringUtil.isNotEmpty(workGroupId) ? workGroupId : byId.getOrgId()).getData();
        String titleHead = DateUtil.dateToString(date, "yyyy年MM月【") + infoById.getName() + "】划线排班表";
        String shiftHead = "导入说明：\n" + "1、手机号码为必填；\n" + "2、按照日期填写班次时间，以24小时制，填写格式：在1号填写班次时间为09：00-12:00，此为一个班次，如果多个班次则以“|”隔开，如1号整天有2个班次，则填写为09：00-12:00|13：00-18:00，同理如果有三个班次则继续增加，最多运行增加24个班次，如果班次时间跨日，则为18：00-02:00，跨日的班次在导入成功后需在划线排班页面展示，班次刻度为半小时制，非（00和30）半小时的将不能导入；\n" + "3、导入的月份会根据导入时划线排班页面的月份进行判断，如果当前月份只有30天，则只能会读取30天的数据，如果有跨日的班次，则填写班次时间后会在下月生成一条跨月次日的班次；\n" + "4、未填写的日期则表示对应日期不排班；\n" + "5、导入的人员需复合划线排班范围的员工类型、用工性质、员工岗位、排班成员信息才能导入，如果不符合条件，则不能导入；\n" + "6、导入后的数据为预发布数据，需保持修改后生效。";
        heads.add(0, "手机号码");
        heads.add(0, "姓名");
        excelExportTemplate.exportModule(UserProvider.getLoginUserId(), titleHead, MonthStatisticsPageListExportVo.class, heads.stream().map(head -> CollUtil.newArrayList(titleHead, shiftHead, head)).collect(Collectors.toList()), null, null, 2000, HeadStyleHandler::new, (page, size) -> {
            List<Map<Integer, Object>> data = CollUtil.newArrayList();
            schedulesList.forEach(schedulesVo -> {
                Map<Integer, Object> objectObjectHashMap = Maps.newHashMap();
                List<LineDrawingPeriodDto> periodList = schedulesVo.getLineDrawingPeriods();
                Map<String, List<LineDrawingPeriodDto>> val = periodList.stream().collect(Collectors.groupingBy(lineDrawingPeriodDto -> DateUtil.dateToString(lineDrawingPeriodDto.getDay(), "yyyy-MM-dd")));
                objectObjectHashMap.put(0, schedulesVo.getRealName());
                objectObjectHashMap.put(1, schedulesVo.getMobilePhone());
                for (int i = 0; i < monthDayInfos.size(); i++) {
                    DayInfo dayInfo = monthDayInfos.get(i);
                    List<LineDrawingPeriodDto> periods = val.get(dayInfo.getDay());
                    String context = CollUtil.isEmpty(periods) ? "" : periods.stream().map(item -> item.getStart() + "-" + item.getEnd()).filter(StringUtil::isNotEmpty).reduce((r1, r2) -> r1 + "|" + r2).orElse("");
                    objectObjectHashMap.put(i + 2, context);
                }
                data.add(objectObjectHashMap);
            });
            return data;
        });
    }

    @Override
    @Transactional
    public void lineSchedulesImport(SchedulesImportDto schedulesImportDto) throws IOException {
        Date start = DateDetail.getMonthBeginDate(schedulesImportDto.getMonth());
        Date end = DateDetail.getMonthEndDate(schedulesImportDto.getMonth());
        List<Date> dates = DateDetail.getDatesByPeriod(start, end);
        List<DayInfo> monthDayInfos = getMonthDayInfos(start);
        // 2. 获取分布式锁，防止并发操作
        RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), schedulesImportDto.getGroupId()));
        Assert.isFalse(lock.isLocked(), "当前考勤组排班操作正在执行中，请稍后再试");
        try {
            // 3. 尝试获取锁
            Assert.isFalse(!lock.tryLock(5, 100, TimeUnit.SECONDS), "当前考勤组排班操作正在执行中，请稍后再试");
            //获取当前考勤组成员
            List<AttendanceGroupUser> attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, List.of(), CollUtil.newArrayList(schedulesImportDto.getGroupId()), Boolean.TRUE);
            if (CollUtil.isEmpty(attendanceGroupUserVos)) {
                return;
            }
            List<String> allUserIds = attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList());
            FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulesConfigFilter(schedulesImportDto.getGroupId(), allUserIds);
            List<PartUserInfoVo> infoByIds = userApi.getStaffRosterListInfoByIds(allUserIds);
            Map<String, PartUserInfoVo> userMap = infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getMobilePhone, Function.identity(), (r1, r2) -> r1));
            List<String> heads = monthDayInfos.stream().map(dayInfo -> dayInfo.getWeekStr() + (char) 10 + dayInfo.getDayNum()).collect(Collectors.toList());
            heads.add(0, "手机号码");
            heads.add(0, "姓名");
            List<FtbAttendanceDailyRule> importData = CollUtil.newArrayList();
            List<ScheduleImportFailModel> failList = CollUtil.newArrayList();
            Map<String, Integer> selfGroupMap = attendanceGroupUserVos.stream().collect(Collectors.toMap(AttendanceGroupUser::getUserId, AttendanceGroupUser::getType, (r1, r2) -> r2));
            EasyExcel.read(new URL(schedulesImportDto.getFileUrl()).openStream(), new LineSchedulesDataListener(schedulesImportDto.getGroupId(), importData, failList, heads, start, userMap, lineSchedulingConfig, selfGroupMap)).registerConverter(new CustomStringStringConverter()).sheet().headRowNumber(3).doRead();
            String fail = failList.stream().sorted(Comparator.comparingInt(model -> model.getError().getIndex())).map(model -> String.format(model.getError().getMessage(), StringUtil.join(model.getUserNames().stream().distinct().collect(Collectors.toList()), "】、【"))).findFirst().orElse(null);
            Assert.isFalse(StringUtil.isNotEmpty(fail), fail);
            List<String> userIds = importData.stream().map(FtbAttendanceDailyRule::getUserId).distinct().collect(Collectors.toList());
            dates.add(0, cn.hutool.core.date.DateUtil.offsetDay(start, -1));
            dates.add(cn.hutool.core.date.DateUtil.offsetDay(end, 1));
            List<FtbAttendanceDailyRule> hisRulesAll = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, schedulesImportDto.getGroupId()).in(FtbAttendanceDailyRule::getUserId, userIds).in(FtbAttendanceDailyRule::getDay, dates).list();
            // 11.3.13 处理规则安排
            List<DailyRuleResultVo> resultList = CollUtil.newArrayList();
            List<FtbAttendanceDailyRule> allRules = new CopyOnWriteArrayList<>();
            List<FtbAttendanceDailyRule> periodConflictRules = CollUtil.newArrayList();
            List<FtbAttendanceDailyRule> noticeRules = new CopyOnWriteArrayList<>();
            List<String> allRuleIds = new CopyOnWriteArrayList<>();
            Map<String, List<FtbAttendanceDailyRule>> ruleMap = hisRulesAll.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF)));
            //划线排班 班次保存
            Map<String, List<AttendanceGroupUser>> usersMap = attendanceGroupUserVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));
            importData.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId)).forEach((userId, list) -> {
                list.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF))).forEach((key, rules) -> {
                    // 11.3.3 提取历史规则ID
                    List<FtbAttendanceDailyRule> hisRules = ruleMap.getOrDefault(key, Lists.newArrayList());
                    List<String> ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList());
                    allRuleIds.addAll(ruleIds);
                    dailyRuleArrangementHandle(resultList, hisRules, rules);
                    FtbAttendanceDailyRule rule = rules.get(0);
                    List<AttendanceGroupUser> users = attendanceGroupUserService.getAttendanceGroupUsers(rule.getDay(), cn.hutool.core.date.DateUtil.endOfDay(rule.getDay()), usersMap.getOrDefault(rule.getUserId(), Lists.newArrayList()));
                    // 11.3.14 处理借调日常规则
                    users.forEach(user -> secondmentDailyRulesHandle(resultList, user, hisRules));
                    // 11.3.15 填充仅外出的规则
                    fillInNailRuleForOnlyStepOut(hisRules);
                    // 11.3.16 添加到所有规则列表
                    allRules.addAll(hisRules);
                    noticeRules.addAll(rules);
                    periodConflictRules.addAll(hisRules);
                });
                List<Date> collect = list.stream().map(FtbAttendanceDailyRule::getDay).distinct().collect(Collectors.toList());
                dates.stream().filter(day -> collect.stream().noneMatch(date -> date.compareTo(day) == 0)).forEach(day -> {
                    periodConflictRules.addAll(ruleMap.getOrDefault(userId + DateDetail.getDate2Str(day, DateDetail.DF), Lists.newArrayList()));
                });
            });

            periodConflictRules.addAll(hisRulesAll.stream().filter(vo -> vo.getDay().before(start) || vo.getDay().after(end)).collect(Collectors.toList()));
            //判断时段是否冲突
            periodConflictRules.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId)).forEach((key, rules) -> {
                Assert.isTrue(fixedPeriodConflict(rules), "班次时段存在冲突");
            });
            // 12. 保存或更新考勤规则
            saveOrUpdateBatch(allRules, allRuleIds);
            if (Objects.equals(lineSchedulingConfig.getEmployeeNotify(), 1)) {
                // 13. 发送系统通知
                attendanceRuleNotificationHandle.sendSystemNoticeForLine2(resultList, noticeRules, null);
            }
            // 14. 处理考勤规则通知
            attendanceRuleNotificationHandle.notificationHandle(resultList, false);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 16. 释放分布式锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 获取排班数据
     *
     * @param groupId 考勤组id
     * @param workGroupId 班组id
     * @param dayList
     * @return
     */
    @Override
    public List<LineSchedulesVo> getLineSchedulesList(String groupId, String workGroupId, List<String> dayList, List<String> finalUserIdList) {
        //班组查询
        TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds();
        List<UserBoundVO> userBoundVO = null;
        if (Objects.nonNull(targetAuthIdsVO) && Objects.equals(targetAuthIdsVO.getTargetAuthEnums(), TargetAuthEnums.USER) || StringUtil.isNotEmpty(workGroupId)) {
            userBoundVO = attendanceGroupUserService.getUserBoundVO(groupId, workGroupId);
            if (CollUtil.isEmpty(userBoundVO)) {
                return List.of();
            }
        }
        List<String> userIdList;
        if (CollUtil.isEmpty(userBoundVO)) {
            userIdList = finalUserIdList;
        } else {
            userIdList = userBoundVO.stream().map(UserBoundVO::getId).filter(vo -> CollUtil.isEmpty(finalUserIdList) || finalUserIdList.contains(vo)).collect(Collectors.toList());
            if (CollUtil.isEmpty(userIdList)) {
                return List.of();
            }
        }
        List<Date> days = dayList.stream().map(DateUtil::stringToDates).collect(Collectors.toList());
        Date start = days.stream().min(Date::compareTo).orElse(null);
        Date end = cn.hutool.core.date.DateUtil.endOfDay(days.stream().max(Date::compareTo).orElse(null));
        //获取当前考勤组成员
        List<AttendanceGroupUser> attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, Objects.isNull(userIdList) ? null : userIdList.stream().distinct().collect(Collectors.toList()), CollUtil.newArrayList(groupId), Boolean.TRUE);
        if (CollUtil.isEmpty(attendanceGroupUserVos)) {
            return CollUtil.newArrayList();
        }
        Map<String, List<AttendanceGroupUser>> groupUserMap = attendanceGroupUserVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));
        //获取用户名
        List<String> collect2 = attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList());
        FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulesConfigFilter(groupId, collect2);
        Map<String, Boolean> stringBooleanMap = attendanceDayStatisticsService.selectUserIsSeal(collect2, DateUtil.getDateString(start, "yyyy-MM"));
        List<PartUserInfoVo> infoByIds = CollUtil.isNotEmpty(collect2) ? userApi.getStaffRosterListInfoByIds(collect2) : CollUtil.newArrayList();
        if (CollUtil.isEmpty(infoByIds)) {
            return CollUtil.newArrayList();
        }
        List<FtbAttendanceLineSchedulingPayrollHours> ftbAttendanceLineSchedulingPayrollHours = lineSchedulingPayrollHoursService.listByUserIdsAndDays(collect2, groupId, days);
        Map<String, BigDecimal> userPayrollHoursMap = ftbAttendanceLineSchedulingPayrollHours.stream().collect(Collectors.toMap(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF), FtbAttendanceLineSchedulingPayrollHours::getPayrollHours));
        List<FtbAttendanceDailyRule> list = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, groupId).eq(FtbAttendanceDailyRule::getFixedMark, 2).in(CollUtil.isNotEmpty(userIdList), FtbAttendanceDailyRule::getUserId, userIdList).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode(), AttendanceTypeEnum.ORDINARY.getCode()).notIn(FtbAttendanceDailyRule::getApplyViewEnable, 0, 2).in(FtbAttendanceDailyRule::getDay, days).orderByAsc(FtbAttendanceDailyRule::getDay, FtbAttendanceDailyRule::getInPoint).list();
        Map<String, List<FtbAttendanceDailyRule>> collect = list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId));
        Map<String, PartUserInfoVo> userMap = infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity()));
        //查询打卡记录获取打卡时间
        List<LineSchedulesVo> schedulesVos = CollUtil.newArrayList();
        List<Integer> attendanceTypeOfApply = CollUtil.newArrayList();
        AtomicReference<Boolean> isValidClock = new AtomicReference<>(Boolean.FALSE);

        groupUserMap.forEach((ruleUserId, groupUserVos) -> {
            ArrayList<LineDrawingPeriodDto> val = Lists.newArrayList();
            isValidClock.set(false);
            AttendanceGroupUser attendanceGroupUserVo = groupUserVos.stream().max(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).orElse(null);
            List<FtbAttendanceDailyRule> rules = collect.getOrDefault(ruleUserId, CollUtil.newArrayList());
            Map<Date, List<FtbAttendanceDailyRule>> collect1 = rules.stream()
                    //过滤原离组考勤组当日数据
                    .filter(rule -> !Objects.equals(rule.getApplyViewEnable(), 3) || StringUtil.equals(rule.getGroupId(), groupId)).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getDay));

            collect1.forEach((day, dayRules) -> {
                Date dayStart = DateDetail.getDateByPoint(day, lineSchedulingConfig.getStartTime(), lineSchedulingConfig.getStartType());
                Date dayEnd = DateUtil.dateAddHours(DateDetail.getDateByPoint(day, lineSchedulingConfig.getEndTime(), lineSchedulingConfig.getEndType()), 1);
                attendanceTypeOfApply.clear();
                Integer existStatus = isExistStatus(groupUserVos, day);
                if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3) || Objects.equals(existStatus, 2)) {
                    attendanceTypeOfApply.add(SchedulesTypeEnum.SECONDMENT_TO.getCode());
                }
                if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) {
                    existStatus = 1;
                }
                if (Objects.equals(existStatus, -1) || Objects.equals(existStatus, 2) || Objects.equals(existStatus, 0)) {
                    return;
                }
                Date date = DateUtil.dateAddDays(day, 1);
                List<LineDrawingPeriodDto> schedulesItemVos = DailyRuleUtil.mergeRules(dayRules).stream().map(dayRule -> {
                            if (Objects.equals(attendanceGroupUserVo.getType(), 2) && StringUtil.equals(attendanceGroupUserVo.getGroupId(), groupId) && !StringUtil.equals(dayRule.getGroupId(), groupId) && Objects.equals(dayRule.getSelfGroup(), 2)) {
                                return null;
                            }
                            if (dayRule.getInPoint().after(dayEnd) || dayRule.getOutPoint().before(dayStart) || Objects.equals(dayRule.getApplyViewEnable(), 2)) {
                                return null;
                            }
                            return LineDrawingPeriodDto.builder().id(dayRule.getId()).day(day).startType(DateUtil.max(dayRule.getInPoint(), dayStart).before(date) ? 1 : 2).start(DateUtil.dateToString(DateUtil.max(dayRule.getInPoint(), dayStart), "HH:mm")).endType(DateUtil.min(dayRule.getOutPoint(), dayEnd).before(date) ? 1 : 2).end(DateUtil.dateToString(DateUtil.min(dayRule.getOutPoint(), dayEnd), "HH:mm")).duration(BigDecimal.valueOf(cn.hutool.core.date.DateUtil.between(dayRule.getInPoint(), dayRule.getOutPoint(), DateUnit.MINUTE)).divide(BigDecimal.valueOf(60), 1, RoundingMode.HALF_UP)).build();
                        }).filter(Objects::nonNull)
                        //inpoint为空的数据按照schedulesType排序
                        .sorted(Comparator.comparing(LineDrawingPeriodDto::getStart)).collect(Collectors.toList());

                val.addAll(schedulesItemVos);
            });
            AttendanceGroupUser attendanceGroupUser = groupUserVos.stream().sorted(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null);
            if (Objects.isNull(attendanceGroupUser)) {
                return;
            }
            PartUserInfoVo partUserInfoVo = userMap.get(ruleUserId);
            if (Objects.isNull(partUserInfoVo)) {
                return;
            }
            int existStatus1 = SecondmentTypeUtil.toSecondmentType(isExistStatus(groupUserVos, start, end));
            BigDecimal total = val.stream().map(LineDrawingPeriodDto::getDuration).reduce(BigDecimal.ZERO, BigDecimal::add);
            LineSchedulesVo build = LineSchedulesVo.builder().userId(ruleUserId).IsSeal(stringBooleanMap.getOrDefault(ruleUserId, Boolean.FALSE)).sort(attendanceGroupUser.getSort()).mobilePhone(partUserInfoVo.getMobilePhone()).payrollHours(userPayrollHoursMap.getOrDefault(ruleUserId + DateDetail.getDate2Str(start, DateDetail.DF), null)).realName(partUserInfoVo.getRealName()).delMark(attendanceGroupUserVo.getDeleteMark()).total(total).selfGroup(existStatus1).existPeriods(getTimeSlotVo(lineSchedulingConfig, groupUserVos, start)).build();
            if (build != null && Objects.equals(build.getDelMark(), 1) && !isValidClock.get()) {
                return;
            }
            build.setLineDrawingPeriods(val);
            schedulesVos.add(build);
        });
        return schedulesVos.stream().sorted(Comparator.comparing(vo -> Objects.equals(vo.getDelMark(), 1) ? 20000 : Objects.equals(vo.getSelfGroup(), 1) ? 19998 : Objects.equals(vo.getSelfGroup(), 2) ? 19999 : Objects.isNull(vo.getSort()) ? 0 : vo.getSort())).collect(Collectors.toList());
    }


    /**
     * 获取单个用户单天的存在时间段集合
     * @param lineSchedulingConfig
     * @param groupUserVos
     * @param day
     * @return
     */
    private List<LineDrawingPeriodDto> getTimeSlotVo(FtbAttendanceLineSchedulingConfig lineSchedulingConfig, List<AttendanceGroupUser> groupUserVos, Date day) {
        if (Objects.isNull(lineSchedulingConfig)) {
            return List.of();
        }
        Date start = DailyRuleUtil.ceilToHalfHour(DateDetail.getDateByPoint(day, lineSchedulingConfig.getStartTime(), lineSchedulingConfig.getStartType()));
        Date end = DailyRuleUtil.ceilToHalfHour(DateUtil.dateAddHours(DateDetail.getDateByPoint(day, lineSchedulingConfig.getEndTime(), lineSchedulingConfig.getEndType()), 1));
        Date date = DateUtil.dateAddDays(day, 1);
        //借调组取借调时间，需要与开始时间与结束时间比较
        //借调组timeJson转借调数据
        //本组取创建时间及离组时间
        return groupUserVos.stream().flatMap(user -> {
            List<SecondmentDateVo> secondmentDateVos = StringUtil.isEmpty(user.getTimeJson()) ? List.of() : JsonUtil.getJsonToList(user.getTimeJson(), SecondmentDateVo.class);
            List<SecondmentDateVo> secondmentDateVoList = getSecondmentTypeList(secondmentDateVos, start, end);
            if (!Objects.equals(user.getType(), 0) && CollUtil.isNotEmpty(secondmentDateVoList)) {
                //借调组取借调时间，需要与开始时间与结束时间比较
                if (Objects.equals(user.getType(), 2)) {
                    //借调组timeJson转借调数据
                    return secondmentDateVos.stream().filter(secondmentDateVo -> start.before(secondmentDateVo.getEndTime()) && end.after(secondmentDateVo.getStartTime())).map(secondmentDateVo -> {
                        Date secondmentStart = DateUtil.max(DailyRuleUtil.ceilToHalfHour(secondmentDateVo.getStartTime()), start);
                        Date secondmentEnd = DateUtil.min(DailyRuleUtil.ceilToHalfHour(secondmentDateVo.getEndTime()), end);
                        return LineDrawingPeriodDto.builder().day(secondmentStart).startType(secondmentStart.before(date) ? 1 : 2).start(DateUtil.dateToString(secondmentStart, "HH:mm")).endType(secondmentEnd.before(date) ? 1 : 2).end(DateUtil.dateToString(secondmentEnd, "HH:mm")).build();
                    });
                }
                return screenshotTimePeriod(secondmentDateVoList, start, end, date, day);
            }
            Date creatorTime = DateUtil.max(DailyRuleUtil.ceilToHalfHour(user.getCreatorTime()), start);
            Date removeTime = DateUtil.min(DailyRuleUtil.ceilToHalfHour(user.getRemoveTime()), end);
            //本组取创建时间及离组时间
            return Stream.of(LineDrawingPeriodDto.builder().day(day).startType(creatorTime.before(date) ? 1 : 2).start(DateUtil.dateToString(creatorTime, "HH:mm")).endType(removeTime.before(date) ? 1 : 2).end(DateUtil.dateToString(removeTime, "HH:mm")).build());
        }).filter(Objects::nonNull).sorted(Comparator.comparing(LineDrawingPeriodDto::getStartType).thenComparing(LineDrawingPeriodDto::getStart)).collect(Collectors.toList());
    }

    private Stream<LineDrawingPeriodDto> screenshotTimePeriod(List<SecondmentDateVo> secondmentDateVoList, Date start, Date end, Date date, Date day) {
        ArrayList<SecondmentDateVo> secondmentDateList = Lists.newArrayList();
        SecondmentDateVo previousSecondmentDateVo = null;
        for (SecondmentDateVo secondmentDateVo : secondmentDateVoList) {
            if (Objects.nonNull(secondmentDateVo)) {
                //如果开始时间大于借调开始时间，且结束时间小于借调结束时间，则返回null
                if (start.after(secondmentDateVo.getStartTime()) && end.before(secondmentDateVo.getEndTime())) {
                    break;
                }
                //如果开始时间小于借调开始时间，且结束时间小于借调结束时间，则返回开始时间至借调开始时间
                if (start.before(secondmentDateVo.getStartTime()) && end.before(secondmentDateVo.getEndTime())) {
                    secondmentDateVo.setEndTime(secondmentDateVo.getStartTime());
                    secondmentDateVo.setStartTime(start);
                    secondmentDateList.add(secondmentDateVo);
                    break;
                }
                boolean hasPrevious = Objects.nonNull(previousSecondmentDateVo);
                if (hasPrevious) {
                    start = previousSecondmentDateVo.getStartTime();
                    end = previousSecondmentDateVo.getEndTime();
                }
                //如果开始时间小于借调开始时间，且结束时间大于借调结束时间，则需要拆分为两段，开始时间至借调开始时间，结束时间至借调结束时间
                if (start.before(secondmentDateVo.getStartTime()) && end.after(secondmentDateVo.getEndTime())) {
                    SecondmentDateVo secondmentDateVo2 = BeanUtil.toBean(secondmentDateVo, SecondmentDateVo.class);
                    if (hasPrevious) {
                        previousSecondmentDateVo.setEndTime(secondmentDateVo.getStartTime());
                    } else {
                        secondmentDateVo.setEndTime(secondmentDateVo.getStartTime());
                        secondmentDateVo.setStartTime(start);
                        secondmentDateList.add(secondmentDateVo);
                    }
                    secondmentDateVo2.setStartTime(secondmentDateVo2.getEndTime());
                    secondmentDateVo2.setEndTime(end);
                    secondmentDateList.add(secondmentDateVo2);
                    previousSecondmentDateVo = secondmentDateVo2;
                    continue;
                }

                //如果开始时间大于借调开始时间，且结束时间大于借调结束时间，则返回借调结束时间至结束时间
                if (start.after(secondmentDateVo.getStartTime()) && end.after(secondmentDateVo.getEndTime())) {
                    if (hasPrevious) {
                        previousSecondmentDateVo.setStartTime(secondmentDateVo.getEndTime());
                        continue;
                    }
                    secondmentDateVo.setStartTime(secondmentDateVo.getEndTime());
                    secondmentDateVo.setEndTime(end);
                    secondmentDateList.add(secondmentDateVo);
                    previousSecondmentDateVo = secondmentDateVo;

                }
            }
        }
        return secondmentDateList.stream().map(secondmentDateVo -> LineDrawingPeriodDto.builder().day(day).startType(secondmentDateVo.getStartTime().before(date) ? 1 : 2).start(DateUtil.dateToString(secondmentDateVo.getStartTime(), "HH:mm")).endType(secondmentDateVo.getEndTime().before(date) ? 1 : 2).end(DateUtil.dateToString(secondmentDateVo.getEndTime(), "HH:mm")).build());
    }

    /**
     * 查询单个用户是否划线排班
     * @param userId
     * @param start
     * @param end
     * @return
     */
    @Override
    public boolean isLineScheduleByUserId(String userId, Date start, Date end) {
        List<String> userIds = CollUtil.newArrayList(userId);
        List<AttendanceGroupUser> attendanceGroupUsers = attendanceGroupUserService.queryByUsersAndGroupFilterSecondment(start, end, userIds, null);
        AttendanceGroupUser attendanceGroupUser = attendanceGroupUsers.stream().findFirst().orElse(null);
        Assert.notNull(attendanceGroupUser, "该用户暂无考勤组");
        lineSchedulesConfigFilter(attendanceGroupUser.getGroupId(), userIds);
        return CollUtil.isNotEmpty(userIds);
    }

    @Override
    public FtbAttendanceLineSchedulingConfig lineSchedulesConfigFilter(String groupId, List<String> userIds) {
        //划线排班配置过滤
        if (CollUtil.isNotEmpty(userIds)) {
            FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulingConfigService.getByGroupId(groupId);
            if (Objects.isNull(lineSchedulingConfig)) {
                userIds.clear();
                return null;
            }
            return getFtbAttendanceLineSchedulingConfig(userIds, lineSchedulingConfig);
        }
        return null;
    }

    @Nullable
    @Override
    public FtbAttendanceLineSchedulingConfig getFtbAttendanceLineSchedulingConfig(List<String> userIds, FtbAttendanceLineSchedulingConfig lineSchedulingConfig) {
        List<PartUserInfoVo> list = userApi.getStaffRosterListInfoByIds(userIds);
        filterUserIdsByLineSchedulingConfig(userIds, lineSchedulingConfig, list);
        return lineSchedulingConfig;
    }

    /**
     * 按划线排班配置过滤 userIds（就地修改），与 {@link #getFtbAttendanceLineSchedulingConfig} 规则一致。
     */
    private void filterUserIdsByLineSchedulingConfig(List<String> userIds, FtbAttendanceLineSchedulingConfig lineSchedulingConfig, List<PartUserInfoVo> staffRosterList) {
        List<String> workNatureArr = StringUtil.isEmpty(lineSchedulingConfig.getWorkNature()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getWorkNature(), ","));
        List<String> employeeTypeArr = StringUtil.isEmpty(lineSchedulingConfig.getEmployeeType()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getEmployeeType(), ","));
        List<String> positionArr = StringUtil.isEmpty(lineSchedulingConfig.getPosition()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getPosition(), ","));
        List<String> memberArr = StringUtil.isEmpty(lineSchedulingConfig.getMembers()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getMembers(), ","));
        if (CollUtil.isEmpty(workNatureArr) && CollUtil.isEmpty(employeeTypeArr) && CollUtil.isEmpty(positionArr) && !Objects.equals(lineSchedulingConfig.getMembersType(), 0) && CollUtil.isEmpty(memberArr)) {
            userIds.clear();
            return;
        }
        boolean hasOnboarding = workNatureArr.contains("301");
        List<String> userIdsForConfigFilter = staffRosterList.stream().filter(dto -> (CollUtil.isNotEmpty(workNatureArr) && workNatureArr.contains(dto.getWorkerStatus())) && ((hasOnboarding && StringUtil.isEmpty(dto.getWorkerType())) || employeeTypeArr.contains("-1") || CollUtil.isNotEmpty(employeeTypeArr) && employeeTypeArr.contains(dto.getWorkerType())) || (CollUtil.isNotEmpty(positionArr) && positionArr.contains(dto.getPositionId())) || Objects.equals(lineSchedulingConfig.getMembersType(), 0) || (CollUtil.isNotEmpty(memberArr) && memberArr.contains(dto.getUserId()))).map(PartUserInfoVo::getUserId).collect(Collectors.toList());
        userIds.removeIf(userId -> !userIdsForConfigFilter.contains(userId));
    }

    @Override
    public List<CreateDayStatistics> getDayRuleByMonth(LocalDate finalStartDate, LocalDate finalEndDate) {
        return this.baseMapper.getDayRuleByMonth(finalStartDate, finalEndDate);
    }

    /**
     * 判断排班设置中是否存在ID为空的排班项
     *
     * @param schedulesSetDto 排班设置DTO
     * @return 如果存在ID为空的排班项则返回true，否则返回false
     */
    private boolean hasBlankIdItem(SchedulesSetDto schedulesSetDto) {
        if (schedulesSetDto == null || schedulesSetDto.getVal() == null) {
            return false;
        }
        return schedulesSetDto.getVal().values().stream().filter(dayVo -> dayVo != null && dayVo.getItemVos() != null).anyMatch(dayVo -> dayVo.getItemVos().stream().anyMatch(item -> StringUtil.isBlank(item.getId())));
    }

    @Override
    public UserDayShiftInfoVo getUserDayShiftInfo(String userId, Date queryDate) {
        UserDayShiftInfoVo result = UserDayShiftInfoVo.builder().userId(userId).queryDate(DateUtil.dateToString(queryDate, "yyyy-MM-dd")).shiftPeriods(new ArrayList<>()).build();

        try {
            // 获取用户信息
            PartUserInfoVo userInfo = userApi.getStaffRosterListInfoByIds(Collections.singletonList(userId)).stream().findFirst().orElse(null);
            if (userInfo == null) {
                return result;
            }
            result.setUserName(userInfo.getAccount());

            // 获取当天排班规则
            List<FtbAttendanceDailyRule> dailyRules = getAttendanceDayRulesForStatistic(null, userId, queryDate);
            if (CollUtil.isEmpty(dailyRules)) {
                return result;
            }

            // 构建考勤组名称映射
            Set<String> groupIds = dailyRules.stream().map(FtbAttendanceDailyRule::getGroupId).filter(Objects::nonNull).collect(Collectors.toSet());

            Map<String, String> groupNameMap = CollUtil.isEmpty(groupIds) ? Collections.emptyMap() : attendanceGroupService.listByIds(new ArrayList<>(groupIds)).stream().collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getGroupName, (v1, v2) -> v1));

            // 设置主考勤组信息
            groupIds.stream().findFirst().ifPresent(firstGroupId -> {
                result.setGroupId(firstGroupId);
                result.setGroupName(groupNameMap.get(firstGroupId));
            });

            // 分离普班和请假规则
            List<FtbAttendanceDailyRule> ordinaryRules = dailyRules.stream().filter(rule -> rule.getAttendanceType() != null).filter(rule -> rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())).collect(Collectors.toList());

            List<FtbAttendanceDailyRule> leaveRules = dailyRules.stream().filter(rule -> rule.getAttendanceType() != null).filter(rule -> rule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList());

            // 合并普班和请假信息
            result.setShiftPeriods(mergeShiftAndLeave(ordinaryRules, leaveRules, groupNameMap));

        } catch (Exception e) {
            log.error("查询用户指定日期班次信息异常，userId=>{}", userId, e);
        }

        return result;
    }

    /**
     * 合并普班和请假信息，生成时段列表
     */
    private List<UserDayShiftInfoVo.ShiftPeriodVo> mergeShiftAndLeave(List<FtbAttendanceDailyRule> ordinaryRules, List<FtbAttendanceDailyRule> leaveRules, Map<String, String> groupNameMap) {

        if (CollUtil.isEmpty(ordinaryRules)) {
            return leaveRules.stream().map(leave -> buildLeavePeriod(leave, 0, null, groupNameMap)).collect(Collectors.toList());
        }

        List<UserDayShiftInfoVo.ShiftPeriodVo> allPeriods = ordinaryRules.stream().flatMap(ordinary -> mergeSingleShift(ordinary, leaveRules, groupNameMap).stream()).collect(Collectors.toList());

        return allPeriods.stream().sorted(Comparator.comparing(UserDayShiftInfoVo.ShiftPeriodVo::getStartTime)).collect(Collectors.toList());
    }

    /**
     * 合并单个普班与请假信息
     */
    private List<UserDayShiftInfoVo.ShiftPeriodVo> mergeSingleShift(FtbAttendanceDailyRule ordinaryRule, List<FtbAttendanceDailyRule> leaveRules, Map<String, String> groupNameMap) {

        Date ordinaryStart = ordinaryRule.getOriginInPoint();
        Date ordinaryEnd = ordinaryRule.getOriginOutPoint();

        if (ordinaryStart == null || ordinaryEnd == null) {
            return Collections.emptyList();
        }

        // 查找覆盖的请假
        List<FtbAttendanceDailyRule> coveringLeaves = leaveRules.stream().filter(l -> l.getApplyStart() != null && l.getApplyEnd() != null).filter(l -> !l.getApplyEnd().before(ordinaryStart) && !l.getApplyStart().after(ordinaryEnd)).collect(Collectors.toList());

        if (CollUtil.isEmpty(coveringLeaves)) {
            Date startTime = ordinaryRule.getInPoint();
            Date endTime = ordinaryRule.getOutPoint();
            if (startTime != null && endTime != null) {
                return Collections.singletonList(buildPeriodWithTime(ordinaryRule, 0, null, startTime, endTime, groupNameMap));
            }
            return Collections.emptyList();
        }

        // 计算未覆盖时段 + 请假时段
        List<UserDayShiftInfoVo.ShiftPeriodVo> periods = calculateUncoveredPeriods(ordinaryRule, ordinaryStart, ordinaryEnd, coveringLeaves, groupNameMap);

        coveringLeaves.stream().map(leave -> buildLeavePeriod(leave, 0, null, groupNameMap)).forEach(periods::add);

        return periods;
    }

    /**
     * 计算未被请假覆盖的时间段，直接生成时段VO
     */
    private List<UserDayShiftInfoVo.ShiftPeriodVo> calculateUncoveredPeriods(FtbAttendanceDailyRule ordinaryRule, Date ordinaryStart, Date ordinaryEnd, List<FtbAttendanceDailyRule> coveringLeaves, Map<String, String> groupNameMap) {

        List<UserDayShiftInfoVo.ShiftPeriodVo> periods = new ArrayList<>();

        if (CollUtil.isEmpty(coveringLeaves)) {
            Date startTime = ordinaryRule.getInPoint();
            Date endTime = ordinaryRule.getOutPoint();
            if (startTime != null && endTime != null) {
                periods.add(buildPeriodWithTime(ordinaryRule, 0, null, startTime, endTime, groupNameMap));
            }
            return periods;
        }

        // 按请假开始时间排序
        coveringLeaves.sort(Comparator.comparing(FtbAttendanceDailyRule::getApplyStart));

        Date currentStart = ordinaryStart;
        String relatedLeaveId = null;

        for (FtbAttendanceDailyRule leaveRule : coveringLeaves) {
            Date leaveStart = leaveRule.getInPoint();
            Date leaveEnd = leaveRule.getOutPoint();

            // 如果请假开始时间晚于当前开始时间，说明有未覆盖的时间段
            if (leaveStart.after(currentStart)) {
                Date uncoveredEnd = leaveStart.before(ordinaryEnd) ? leaveStart : ordinaryEnd;
                if (uncoveredEnd.after(currentStart)) {
                    periods.add(buildPeriodWithTime(ordinaryRule, 1, relatedLeaveId, currentStart, uncoveredEnd, groupNameMap));
                }
            }

            // 更新当前开始时间为请假结束时间
            if (leaveEnd.after(currentStart)) {
                currentStart = leaveEnd;
                relatedLeaveId = leaveRule.getId();
            }

            // 如果已经超出普班结束时间，退出
            if (currentStart.after(ordinaryEnd)) {
                break;
            }
        }

        // 添加最后一段未覆盖的时间
        if (currentStart.before(ordinaryEnd)) {
            periods.add(buildPeriodWithTime(ordinaryRule, 1, relatedLeaveId, currentStart, ordinaryEnd, groupNameMap));
        }

        // 如果没有任何未覆盖的时间段，说明完全被覆盖
        if (periods.isEmpty()) {
            periods.add(buildPeriodWithTime(ordinaryRule, 2, coveringLeaves.get(0).getId(), ordinaryStart, ordinaryEnd, groupNameMap));
        }

        return periods;
    }

    /**
     * 构建带指定时间的时段VO
     */
    private UserDayShiftInfoVo.ShiftPeriodVo buildPeriodWithTime(FtbAttendanceDailyRule rule, Integer coverageStatus, String relatedLeaveId, Date startTime, Date endTime, Map<String, String> groupNameMap) {

        int durationMinutes = startTime != null && endTime != null ? (int) ((endTime.getTime() - startTime.getTime()) / 60000) : 0;

        String groupId = rule.getGroupId();
        String groupName = groupNameMap != null && groupId != null ? groupNameMap.get(groupId) : null;

        return UserDayShiftInfoVo.ShiftPeriodVo.builder().periodId(rule.getId()).groupId(groupId).groupName(groupName).periodType(AttendanceTypeEnum.ORDINARY.getCode()).periodTypeName(AttendanceTypeEnum.ORDINARY.getMsg()).shiftName(rule.getShiftId()).startTime(startTime != null ? DateUtil.dateToString(startTime, "HH:mm") : null).endTime(endTime != null ? DateUtil.dateToString(endTime, "HH:mm") : null).latePoint(rule.getLatePoint() != null ? DateUtil.dateToString(rule.getLatePoint(), "HH:mm") : null).durationMinutes(durationMinutes).isCrossDay(rule.getNextDayEnable() != null ? rule.getNextDayEnable() : 0).coverageStatus(coverageStatus).relatedLeaveId(relatedLeaveId).build();
    }

    /**
     * 构建请假时段VO
     */
    private UserDayShiftInfoVo.ShiftPeriodVo buildLeavePeriod(FtbAttendanceDailyRule rule, Integer coverageStatus, String relatedLeaveId, Map<String, String> groupNameMap) {

        Date startTime = rule.getApplyStart();
        Date endTime = rule.getApplyEnd();
        Double duration = rule.getLeaveDay() != null ? rule.getLeaveDay().doubleValue() : null;
        int durationMinutes = duration != null ? (int) (duration * 60) : 0;

        String groupId = rule.getGroupId();
        String groupName = groupNameMap != null && groupId != null ? groupNameMap.get(groupId) : null;

        return UserDayShiftInfoVo.ShiftPeriodVo.builder().periodId(rule.getId()).groupId(groupId).groupName(groupName).periodType(rule.getAttendanceType()).periodTypeName(AttendanceTypeEnum.getMsg(rule.getAttendanceType())).leaveTypeName(AttendanceTypeEnum.getMsg(rule.getAttendanceType())).startTime(startTime != null ? DateUtil.dateToString(startTime, "yyyy-MM-dd HH:mm") : null).endTime(endTime != null ? DateUtil.dateToString(endTime, "yyyy-MM-dd HH:mm") : null).durationMinutes(durationMinutes).isCrossDay(0).coverageStatus(coverageStatus).relatedLeaveId(relatedLeaveId).build();
    }

    /**
     * 统一排班内部实现
     */
    private String setUnifiedSchedulesInternal(UnifiedSchedulesDto dto) throws HandleException {
        // 按排班类型分组
        Map<String, Map<String, SchedulesDayVo>> fixedUserDayMap = new HashMap<>();
        Map<String, List<UserDaySchedulesDto>> lineUserDayMap = new HashMap<>();

        for (UserDaySchedulesDto item : dto.getUserDaySchedules()) {
            if (CollUtil.isNotEmpty(item.getFixedItems())) {
                // 固定排班：按用户+日期组织
                fixedUserDayMap.computeIfAbsent(item.getUserId(), k -> new TreeMap<>())
                        .put(item.getDay(), SchedulesDayVo.builder().itemVos(item.getFixedItems()).build());
            }
            if (CollUtil.isNotEmpty(item.getLinePeriods())) {
                // 划线排班：按用户分组
                lineUserDayMap.computeIfAbsent(item.getUserId(), k -> new ArrayList<>()).add(item);
            }
        }

        List<String> errors = new ArrayList<>();

        // 处理固定排班
        if (!fixedUserDayMap.isEmpty()) {
            List<SchedulesSetDto> fixedList = new ArrayList<>();
            for (Map.Entry<String, Map<String, SchedulesDayVo>> entry : fixedUserDayMap.entrySet()) {
                SchedulesSetDto dto1 = new SchedulesSetDto();
                dto1.setGroupId(dto.getGroupId());
                dto1.setUserId(entry.getKey());
                dto1.setVal(entry.getValue());
                fixedList.add(dto1);
            }
            String fixedResult = setSchedules(fixedList);
            if (StringUtil.isNotEmpty(fixedResult)) {
                errors.add(fixedResult);
            }
        }

        // 处理划线排班
        if (!lineUserDayMap.isEmpty()) {
            // 按用户构建划线排班数据
            List<LineDrawingUserDto> lineUserList = new ArrayList<>();

            for (Map.Entry<String, List<UserDaySchedulesDto>> entry : lineUserDayMap.entrySet()) {
                String userId = entry.getKey();
                List<UserDaySchedulesDto> userItems = entry.getValue();

                LineDrawingUserDto lineUserDto = new LineDrawingUserDto();
                lineUserDto.setUserId(userId);

                List<LineDrawingPeriodDto> allPeriods = new ArrayList<>();
                BigDecimal lastPayrollHours = null;

                for (UserDaySchedulesDto item : userItems) {

                    if (CollUtil.isNotEmpty(item.getLinePeriods())) {
                        for (LineDrawingPeriodDto period : item.getLinePeriods()) {
                            LineDrawingPeriodDto newPeriod = new LineDrawingPeriodDto();
                            newPeriod.setId(period.getId());
                            newPeriod.setDay(getStr2Date(item.getDay()));
                            newPeriod.setStart(period.getStart());
                            newPeriod.setStartType(period.getStartType());
                            newPeriod.setEnd(period.getEnd());
                            newPeriod.setEndType(period.getEndType());
                            newPeriod.setDuration(period.getDuration());
                            allPeriods.add(newPeriod);
                        }
                    }
                    if (Objects.nonNull(item.getPayrollHours())) {
                        lastPayrollHours = item.getPayrollHours();
                    }
                }

                lineUserDto.setPeriods(allPeriods);
                lineUserDto.setPayrollHours(lastPayrollHours);
                lineUserList.add(lineUserDto);
            }

            // 一次性调用划线排班（使用支持多时段的新方法）
            LineDrawingSchedulesConfigDto lineDto = new LineDrawingSchedulesConfigDto();
            lineDto.setGroupId(dto.getGroupId());
            lineDto.setUserList(lineUserList);

            String lineResult = setLineDrawingSchedulesWithMultiPeriods(lineDto);
            if (StringUtil.isNotEmpty(lineResult)) {
                errors.add(lineResult);
            }
        }

        return errors.isEmpty() ? "" : String.join("\br", errors);
    }

    private static final String LINE_SCHEDULE_SHIFT_NAME = "划线排班";
    private static final int SHIFT_HISTORY_DAY_COUNT = 90;

    @Override
    public List<DayShiftRevenueStatVo> getGroupShiftHistory90Days(String groupId) {
        Date end = cn.hutool.core.date.DateUtil.endOfDay(cn.hutool.core.date.DateUtil.offsetDay(new Date(), -1));
        Date start = cn.hutool.core.date.DateUtil.beginOfDay(
                cn.hutool.core.date.DateUtil.offsetDay(new Date(), -(SHIFT_HISTORY_DAY_COUNT)));
        List<Date> datesByPeriod = DateDetail.getDatesByPeriod(start, end);
        Map<String, BigDecimal> revenueByDay = loadReceivableRevenueAmountByDay(groupId);

        List<AttendanceGroupUser> attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(
                start, end, null, CollUtil.newArrayList(groupId), Boolean.TRUE);
        if (CollUtil.isEmpty(attendanceGroupUserVos)) {
            return buildEmptyDayShiftHistory(datesByPeriod, revenueByDay);
        }

        Map<String, List<AttendanceGroupUser>> groupUserMap = attendanceGroupUserVos.stream()
                .collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));
        List<String> userIds = new ArrayList<>(groupUserMap.keySet());

        List<PartUserInfoVo> infoByIds = userApi.getStaffRosterListInfoByIds(userIds);
        Map<String, PartUserInfoVo> userMap = CollUtil.isEmpty(infoByIds) ? Maps.newHashMap()
                : infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity(), (a, b) -> a));
        List<FtbAttendanceDailyRule> list = lambdaQuery()
                .eq(FtbAttendanceDailyRule::getGroupId, groupId)
                .in(FtbAttendanceDailyRule::getUserId, userIds)
                .eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode())
                .notIn(FtbAttendanceDailyRule::getApplyViewEnable, 0, 2)
                .ge(FtbAttendanceDailyRule::getDay, start)
                .le(FtbAttendanceDailyRule::getDay, end)
                .list();
        list = list.stream()
                .filter(rule -> !Objects.equals(rule.getApplyViewEnable(), 3) || StringUtil.equals(rule.getGroupId(), groupId))
                .collect(Collectors.toList());

        Map<String, Map<String, List<FtbAttendanceDailyRule>>> rulesByUserAndDay = indexRulesByUserAndDay(list);
        Map<String, AttendanceShiftNameEntity> shiftMap = getShiftByShiftIds(
                list.stream().map(FtbAttendanceDailyRule::getShiftId).filter(StringUtil::isNotEmpty).distinct()
                        .collect(Collectors.toList()));
        Map<String, Set<String>> inGroupDaysByUserId = buildInGroupDaysByUserId(groupUserMap, datesByPeriod);
        Map<String, AttendanceGroupUser> latestGroupUserByUserId = buildLatestGroupUserByUserId(groupUserMap);

        List<DayShiftRevenueStatVo> result = CollUtil.newArrayList();
        for (Date day : datesByPeriod) {
            String dayStr = DateDetail.getDate2Str(day, DateDetail.DF);
            List<ShiftPostStatVo> shifts = buildDayShiftPostStats(dayStr, groupUserMap, inGroupDaysByUserId,
                    rulesByUserAndDay, userMap, shiftMap, latestGroupUserByUserId, groupId);
            result.add(DayShiftRevenueStatVo.builder()
                    .day(dayStr)
                    .revenue(revenueByDay.get(dayStr))
                    .shifts(shifts)
                    .build());
        }
        return result;
    }

    /**
     * 预计算 90 天内在组自然日（与 {@link #buildDayShiftPostStats} 原 {@code isExistStatus} 过滤口径一致）。
     */
    private Map<String, Set<String>> buildInGroupDaysByUserId(Map<String, List<AttendanceGroupUser>> groupUserMap,
                                                              List<Date> datesByPeriod) {
        Map<String, Set<String>> result = new HashMap<>(groupUserMap.size());
        groupUserMap.forEach((userId, groupUserVos) -> {
            Set<String> inGroupDays = new HashSet<>();
            for (Date day : datesByPeriod) {
                Integer existStatus = isExistStatus(groupUserVos, day);
                if (Objects.equals(existStatus, -1) || Objects.equals(existStatus, 2) || Objects.equals(existStatus, 0)) {
                    continue;
                }
                inGroupDays.add(DateDetail.getDate2Str(day, DateDetail.DF));
            }
            result.put(userId, inGroupDays);
        });
        return result;
    }

    private Map<String, AttendanceGroupUser> buildLatestGroupUserByUserId(
            Map<String, List<AttendanceGroupUser>> groupUserMap) {
        Map<String, AttendanceGroupUser> result = new HashMap<>(groupUserMap.size());
        groupUserMap.forEach((userId, groupUserVos) -> groupUserVos.stream()
                .max(Comparator.comparing(AttendanceGroupUser::getCreatorTime))
                .ifPresent(u -> result.put(userId, u)));
        return result;
    }

    private Map<String, Map<String, List<FtbAttendanceDailyRule>>> indexRulesByUserAndDay(List<FtbAttendanceDailyRule> list) {
        Map<String, Map<String, List<FtbAttendanceDailyRule>>> rulesByUserAndDay = new HashMap<>();
        for (FtbAttendanceDailyRule rule : list) {
            if (rule == null || rule.getDay() == null || StringUtil.isEmpty(rule.getUserId())) {
                continue;
            }
            String dayStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF);
            rulesByUserAndDay
                    .computeIfAbsent(rule.getUserId(), k -> new HashMap<>())
                    .computeIfAbsent(dayStr, k -> new ArrayList<>())
                    .add(rule);
        }
        return rulesByUserAndDay;
    }

    private List<DayShiftRevenueStatVo> buildEmptyDayShiftHistory(List<Date> datesByPeriod,
                                                                  Map<String, BigDecimal> revenueByDay) {
        return datesByPeriod.stream()
                .map(day -> {
                    String dayStr = DateDetail.getDate2Str(day, DateDetail.DF);
                    return DayShiftRevenueStatVo.builder()
                            .day(dayStr)
                            .revenue(revenueByDay.get(dayStr))
                            .shifts(CollUtil.newArrayList())
                            .build();
                })
                .collect(Collectors.toList());
    }

    @Override
    public List<DayReceivableRevenueVo> listReceivableRevenueByDay(String groupId, Date startTime, Date endTime) {
        Date rangeStart = cn.hutool.core.date.DateUtil.beginOfDay(startTime);
        Date rangeEnd = cn.hutool.core.date.DateUtil.endOfDay(endTime);
        Map<String, BigDecimal> revenueByDay = loadEstimatedTurnoverAmountByDay(groupId, rangeStart, rangeEnd);
        return DateDetail.getDatesByPeriod(rangeStart, rangeEnd).stream()
                .map(day -> {
                    String dayStr = DateDetail.getDate2Str(day, DateDetail.DF);
                    return DayReceivableRevenueVo.builder()
                            .day(dayStr)
                            .revenue(revenueByDay.get(dayStr))
                            .build();
                })
                .collect(Collectors.toList());
    }

    /**
     * 按自然日汇总营业额预估。
     * <p>
     * {@code listEstimatedTurnoverByRange} 的 {@code storeId} 须为门店绑定的收银平台 ID（{@code cashierStoreId}），
     * 与 {@code ftb_analysis_estimated_turnover.F_StoreId} 及营业额预估任务 MQ 落库口径一致。
     */
    private Map<String, BigDecimal> loadEstimatedTurnoverAmountByDay(String groupId, Date rangeStart, Date rangeEnd) {
        AttendanceGroup group = attendanceGroupService.getById(groupId);
        if (group == null || StringUtil.isEmpty(group.getOrgId())) {
            return Collections.emptyMap();
        }
        String cashierStoreId = resolveCashierStoreIdByOrgId(group.getOrgId());
        if (StringUtil.isEmpty(cashierStoreId)) {
            log.warn("loadEstimatedTurnoverAmountByDay 门店未绑定收银平台; groupId={}, orgId={}", groupId, group.getOrgId());
            return Collections.emptyMap();
        }
        LocalDate startLd = rangeStart.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate endLd = rangeEnd.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        List<EstimateTurnoverStoredRowApiVo> rows;
        try {
            rows = estimateTurnoverTaskApi.listEstimatedTurnoverByRange(cashierStoreId, startLd, endLd);
        } catch (Exception ex) {
            log.warn("loadEstimatedTurnoverAmountByDay listEstimatedTurnoverByRange failed; groupId={}, cashierStoreId={}, msg={}",
                    groupId, cashierStoreId, ex.getMessage());
            return Collections.emptyMap();
        }
        if (CollUtil.isEmpty(rows)) {
            return Collections.emptyMap();
        }
        Map<String, BigDecimal> map = new HashMap<>();
        for (EstimateTurnoverStoredRowApiVo row : rows) {
            if (row == null || row.getDate() == null) {
                continue;
            }
            BigDecimal amt = row.getEstimatedTurnover();
            if (amt == null) {
                continue;
            }
            map.merge(row.getDate().toString(), amt, BigDecimal::add);
        }
        return map;
    }

    /**
     * 按自然日汇总门店应收营业额（数据分析服务 {@link AnalysisExternalInterfaceApi#getStore90DayData}）。
     */
    private Map<String, BigDecimal> loadReceivableRevenueAmountByDay(String groupId) {
        AttendanceGroup group = attendanceGroupService.getById(groupId);
        if (group == null || StringUtil.isEmpty(group.getOrgId())) {
            return Collections.emptyMap();
        }
        String cashierStoreId = resolveCashierStoreIdByOrgId(group.getOrgId());
        if (StringUtil.isEmpty(cashierStoreId)) {
            log.warn("loadReceivableRevenueAmountByDay 门店未绑定收银平台; groupId={}, orgId={}", groupId, group.getOrgId());
            return Collections.emptyMap();
        }
        List<StoreDayReceivableRevenueVo> rows;
        try {
            rows = analysisExternalInterfaceApi.getStore90DayData(
                    cashierStoreId);
        } catch (Exception ex) {
            log.error("loadReceivableRevenueAmountByDay getStoreDayData failed; groupId={}, cashierStoreId={}, msg={}",
                    groupId, cashierStoreId, ex.getMessage());
            return Collections.emptyMap();
        }
        if (CollUtil.isEmpty(rows)) {
            return Collections.emptyMap();
        }
        Map<String, BigDecimal> map = new HashMap<>();
        for (StoreDayReceivableRevenueVo row : rows) {
            if (row == null || Objects.isNull(row.getDate())) {
                continue;
            }
            String dayStr = row.getDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
            BigDecimal amt = row.getReceivables();
            if (amt == null) {
                continue;
            }
            map.merge(dayStr, amt, BigDecimal::add);
        }
        return map;
    }

    /**
     * 由组织/门店 ID 解析收银平台门店 ID（营业额预估落库与 Feign 查询均使用该 ID）。
     */
    private String resolveCashierStoreIdByOrgId(String orgId) {
        if (StringUtil.isEmpty(orgId)) {
            return null;
        }
        OrganizeGeneralDetailVO organize = organizeApi.organizeInfoById(null, orgId).getData();
        return organize == null ? null : organize.getCashierStoreId();
    }

    private List<ShiftPostStatVo> buildDayShiftPostStats(String dayStr,
                                                         Map<String, List<AttendanceGroupUser>> groupUserMap,
                                                         Map<String, Set<String>> inGroupDaysByUserId,
                                                         Map<String, Map<String, List<FtbAttendanceDailyRule>>> rulesByUserAndDay,
                                                         Map<String, PartUserInfoVo> userMap,
                                                         Map<String, AttendanceShiftNameEntity> shiftMap,
                                                         Map<String, AttendanceGroupUser> latestGroupUserByUserId,
                                                         String groupId) {
        Map<String, ShiftPostStatVo> nonLineTemplateMap = new LinkedHashMap<>();
        Map<String, Map<String, Set<String>>> nonLinePostUsers = new LinkedHashMap<>();
        List<ShiftPostStatVo> lineShifts = CollUtil.newArrayList();

        groupUserMap.forEach((userId, groupUserVos) -> {
            if (!inGroupDaysByUserId.getOrDefault(userId, Collections.emptySet()).contains(dayStr)) {
                return;
            }
            AttendanceGroupUser attendanceGroupUserVo = latestGroupUserByUserId.get(userId);
            if (Objects.isNull(attendanceGroupUserVo)) {
                return;
            }
            String postId = resolvePostId(userMap.get(userId));
            List<FtbAttendanceDailyRule> dayRules = rulesByUserAndDay
                    .getOrDefault(userId, Collections.emptyMap())
                    .getOrDefault(dayStr, Collections.emptyList());

            for (FtbAttendanceDailyRule dayRule : dayRules) {
                if (shouldSkipHistoryDayRule(dayRule, attendanceGroupUserVo, groupId)) {
                    continue;
                }
                if (Objects.equals(dayRule.getFixedMark(), 2)) {
                    lineShifts.add(buildLineShiftPostStat(dayRule, postId));
                    continue;
                }
                if (StringUtil.isEmpty(dayRule.getShiftId())) {
                    continue;
                }
                nonLineTemplateMap.putIfAbsent(dayRule.getShiftId(), buildNonLineShiftPostTemplate(dayRule, shiftMap));
                nonLinePostUsers
                        .computeIfAbsent(dayRule.getShiftId(), k -> new LinkedHashMap<>())
                        .computeIfAbsent(postId, k -> new LinkedHashSet<>())
                        .add(userId);
            }
        });

        List<ShiftPostStatVo> shifts = CollUtil.newArrayList();
        for (Map.Entry<String, ShiftPostStatVo> entry : nonLineTemplateMap.entrySet()) {
            ShiftPostStatVo template = entry.getValue();
            Map<String, Set<String>> postUserMap = nonLinePostUsers.getOrDefault(entry.getKey(), Collections.emptyMap());
            template.setPosts(buildPostHeadcountList(postUserMap));
            shifts.add(template);
        }
        shifts.addAll(lineShifts);
        shifts.sort(Comparator.comparing(ShiftPostStatVo::getStartTime, Comparator.nullsLast(String::compareTo)));
        return shifts;
    }

    private boolean shouldSkipHistoryDayRule(FtbAttendanceDailyRule dayRule, AttendanceGroupUser attendanceGroupUserVo, String groupId) {
        return Objects.equals(attendanceGroupUserVo.getType(), 2)
                && StringUtil.equals(attendanceGroupUserVo.getGroupId(), groupId)
                && !StringUtil.equals(dayRule.getGroupId(), groupId)
                && Objects.equals(dayRule.getSelfGroup(), 2);
    }

    private String resolvePostId(PartUserInfoVo userInfo) {
        if (Objects.isNull(userInfo) || StringUtil.isEmpty(userInfo.getPositionId())) {
            return "";
        }
        return userInfo.getPositionId();
    }

    private ShiftPostStatVo buildLineShiftPostStat(FtbAttendanceDailyRule dayRule, String postId) {
        return ShiftPostStatVo.builder()
                .shiftId(dayRule.getId())
                .shiftName(LINE_SCHEDULE_SHIFT_NAME)
                .startTime(getDate2Str(dayRule.getOriginInPoint()))
                .endTime(getDate2Str(dayRule.getOriginOutPoint()))
                .lineSchedule(Boolean.TRUE)
                .posts(CollUtil.newArrayList(ShiftPostHeadcountVo.builder().postId(postId).headcount(1).build()))
                .build();
    }

    private ShiftPostStatVo buildNonLineShiftPostTemplate(FtbAttendanceDailyRule dayRule, Map<String, AttendanceShiftNameEntity> shiftMap) {
        ShiftNameVo shiftNameVo = StringUtil.isNotEmpty(dayRule.getPeriodInfo())
                ? JSONUtil.toBean(dayRule.getPeriodInfo(), ShiftNameVo.class) : new ShiftNameVo();
        AttendanceShiftNameEntity shiftEntity = shiftMap.getOrDefault(dayRule.getShiftId(), new AttendanceShiftNameEntity());
        String shiftName = StringUtil.isNotEmpty(shiftNameVo.getName()) ? shiftNameVo.getName() : shiftEntity.getName();
        return ShiftPostStatVo.builder()
                .shiftId(dayRule.getShiftId())
                .shiftName(shiftName)
                .startTime(getDate2Str(dayRule.getOriginInPoint()))
                .endTime(getDate2Str(dayRule.getOriginOutPoint()))
                .lineSchedule(Boolean.FALSE)
                .build();
    }

    private List<ShiftPostHeadcountVo> buildPostHeadcountList(Map<String, Set<String>> postUserMap) {
        return postUserMap.entrySet().stream()
                .map(e -> ShiftPostHeadcountVo.builder().postId(e.getKey()).headcount(e.getValue().size()).build())
                .sorted(Comparator.comparing(ShiftPostHeadcountVo::getPostId, Comparator.nullsLast(String::compareTo)))
                .collect(Collectors.toList());
    }
}