package jnpf.attendance.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import jnpf.attendance.antifreeze.UserAntifreeze;
import jnpf.attendance.mapper.*;
import jnpf.attendance.service.*;
import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler;
import jnpf.base.ActionResult;
import jnpf.constants.AttendanceConstant;
import jnpf.constants.AttendancePermissionConstant;
import jnpf.database.util.TenantDataSourceUtil;
import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo;
import jnpf.engine.vo.UserOperatorVo;
import jnpf.entity.AttendanceApprovalSetting;
import jnpf.entity.AttendanceGroup;
import jnpf.entity.AttendanceGroupUser;
import jnpf.entity.AttendanceManagerPermission;
import jnpf.entity.attendance.*;
import jnpf.entity.workflow.ApplyAttendanceChange;
import jnpf.entity.workflow.ApplyAttendanceRepair;
import jnpf.enums.attendance.*;
import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum;
import jnpf.exception.ApproveException;
import jnpf.exception.HandleException;
import jnpf.exception.LoginException;
import jnpf.exception.QueryException;
import jnpf.model.attendance.dto.*;
import jnpf.model.attendance.vo.*;
import jnpf.model.attendance.vo.attendance.*;
import jnpf.model.attendance.vo.flow.FlowTaskVo;
import jnpf.model.doclibrary.vo.UseDetailVo;
import jnpf.model.personnels.dto.secondment.FtbSecondMentQueryDTO;
import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentVO;
import jnpf.model.workflow.dto.AttendanceBusinessTripApproveOaDto;
import jnpf.model.workflow.dto.AttendanceWorkOvertimeApproveDto;
import jnpf.permission.V2OrganizeApi;
import jnpf.permission.V2UserApi;
import jnpf.permission.model.user.PartUserInfoVo;
import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO;
import jnpf.permission.vo.v2.user.UserBoundVO;
import jnpf.personnels.FtbPersonneApi;
import jnpf.util.*;
import jnpf.workflow.mapper.ApplyAttendanceChangeMapper;
import jnpf.workflow.mapper.ApplyAttendanceRepairMapper;
import jnpf.workflow.service.BusinessTripApproveService;
import jnpf.workflow.service.FlowTaskService;
import jnpf.workflow.service.GoOutApproveService;
import jnpf.workflow.service.LeaveApproveService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * describe
 *
 * @author HuangLinPan
 * @date 2023/11/22
 */

@Service
@Slf4j
public class AttendanceApproveServiceImpl implements AttendanceApproveService {


    @Autowired
    private UserProvider userProvider;

    @Resource
    private AttendanceBalanceRecordMapper attendanceBalanceRecordMapper;

    @Resource
    private AttendanceClockInResultMapper attendanceClockInResultMapper;

    @Resource
    private V2UserApi v2UserApi;
    @Autowired
    private V2OrganizeApi organizeApi;

    @Resource
    private AttendanceBalanceUseRecordMapper attendanceBalanceUseRecordMapper;

    @Resource
    private AttendanceClockInMapper attendanceClockInMapper;

    @Resource
    private ApplyAttendanceRepairMapper applyAttendanceRepairMapper;

    @Resource
    private ApplyAttendanceChangeMapper applyAttendanceChangeMapper;

    @Resource
    private AttendanceClockInResultService attendanceClockInResultService;

    @Resource
    private AttendanceClockInService attendanceClockInService;
    @Resource
    private AttendanceDayStatisticsService attendanceDayStatisticsService;

    @Resource
    private AttendanceDailyRuleMapper attendanceDailyRuleMapper;

    @Resource
    private AttendanceApprovalSettingMapper attendanceApprovalSettingMapper;
    @Resource
    private AttendanceManagerPermissionMapper attendanceManagerPermissionMapper;
    @Resource
    private AttendanceGroupMapper attendanceGroupMapper;

    @Resource
    private AttendanceGroupService attendanceGroupService;

    @Resource
    private AttendanceLeaveApproveMapper attendanceLeaveApproveMapper;

    @Resource
    private AttendanceDailyRuleService attendanceDailyRuleService;

    @Resource
    private AttendanceBaseSettingService attendanceBaseSettingService;

    @Resource
    private AttendanceGroupUserMapper attendanceGroupUserMapper;

    @Resource
    private AttendanceSuperAdminService attendanceSuperAdminService;


    @Autowired
    private AttendanceUserService attendanceGroupUserService;

    @Resource
    private StorageRestMapper storageRestMapper;

    @Resource
    private FlowTaskService flowTaskService;

    @Autowired
    private AttendanceNoticeHandler attendanceNoticeHandler;

    @Autowired
    private UserAntifreeze userAntifreeze;

    @Autowired
    private RedisUtil redisUtil;

    @Resource
    private AttendanceUserService attendanceUserService;

    @Resource
    private BusinessTripApproveService businessTripApproveService;

    @Resource
    private GoOutApproveService goOutApproveService;

    @Resource
    private LeaveApproveService leaveApproveService;

    @Resource
    private AttendanceUserBalanceRecordService attendanceUserBalanceRecordService;

    @Resource
    @Lazy
    private AttendanceLeaveTypeService attendanceLeaveTypeService;

    @Resource
    @Lazy
    private FtbPersonneApi ftbPersonneApi;

    @Resource
    @Lazy
    private AttendanceUserBalanceMapper attendanceUserBalanceMapper;



    @Override
    public PageInfo<UseDetailVo> getUseDetail(String id, BalanceQueryDto balanceQueryDto) {
        PageHelper.startPage(balanceQueryDto.getCurrentPage(), balanceQueryDto.getPageSize());
        PageInfo<UseDetailVo> page = new PageInfo<>(attendanceBalanceUseRecordMapper.getUserBalanceDetail(id));
        if (!page.getList().isEmpty()) {
            //使用日期指我用在哪一天，记录日期是实际扣除的时间。比如我申请11-17 -- 11-18请假抵扣  使用日期 11-17 -- 11-18，但是我是11月10号申请的且在当天通过的。记录日期 11-10
            //处理使用日期
            page.getList().forEach(v -> {
                v.setUseDate(v.getStartTime().equals(v.getEndTime()) ? v.getStartTime() : v.getStartTime() + "--" + v.getEndTime());
                String unitStr = Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), v.getUnit()) ? BalanceEnum.BALANCE_UNIT_XH.getMsg() : BalanceEnum.BALANCE_UNIT_T.getMsg();
                v.setQuotaStr(v.getQuota().setScale(2, RoundingMode.HALF_UP) + unitStr);
            });
        }
        return page;
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean invalidationCoupons(String tenantId) {
        if (StrUtil.isBlank(tenantId)) {
            log.error("租户ID不能为空");
            throw new IllegalArgumentException("租户ID不能为空");
        }
        //切换成租户库
        try {
            TenantDataSourceUtil.switchTenant(tenantId);
        } catch (LoginException e) {
            log.error("切换租户失败, 租户id: {}", tenantId);
            throw new RuntimeException("invalidationCoupons方法，切换租户数据源失败，无法执行券失效操作", e);
        }
        // 查询需要定时失效的劵
        List<AttendanceBalanceRecordEntity> list = attendanceBalanceRecordMapper.selectInvalidationCoupons();
        if (CollUtil.isEmpty(list)) {
            log.debug("租户 {} 没有需要失效的券", tenantId);
            return Boolean.TRUE;
        }
        // 生成过期记录
        List<AttendanceUserBalanceDetailVo> balanceDetailVoList = buildBalanceDetails(list);
        if (!balanceDetailVoList.isEmpty()) {
            // 消费记录 仅记录生成劵的使用记录
            attendanceUserBalanceMapper.batchAddBalanceDetail(balanceDetailVoList);
        }
        //定时过期
        List<String> ids = list.stream().map(AttendanceBalanceRecordEntity::getId).collect(Collectors.toList());
        if (!ids.isEmpty()) {
            // 过期劵
            attendanceBalanceRecordMapper.invalidationCoupons(ids);
        }
        return Boolean.TRUE;
    }



    private List<AttendanceUserBalanceDetailVo> buildBalanceDetails(List<AttendanceBalanceRecordEntity> coupons) {
        return coupons.stream()
                .map(coupon -> {
                    AttendanceUserBalanceDetailVo detail = new AttendanceUserBalanceDetailVo();
                    detail.setId(FtbUtil.getId());
                    detail.setObjectId(coupon.getObjectId());
                    detail.setUserId(coupon.getUserId());
                    detail.setQuota(coupon.getBalance().negate()); // 负数表示扣减
                    detail.setContent(buildExpireMessage(coupon));
                    return detail;
                })
                .collect(Collectors.toList());
    }

    private String buildExpireMessage(AttendanceBalanceRecordEntity coupon) {
        String issueDate = DateDetail.getDate2Str(coupon.getCreatorTime(), DateDetail.DF);
        BigDecimal remainingBalance = coupon.getBalance().setScale(2, RoundingMode.HALF_UP);
        return String.format("系统已将%s发放未使用剩余的%.2f天余额，因到期自动清除", issueDate, remainingBalance);
    }

    @Override
    public List<UserClassesVo> getClasses(BalanceQueryDto balanceQueryDto) {
        List<UserClassesVo> list = new ArrayList<>();
        //开始日期选择后触发接口返回当天及后一天的排班信息
        String userId = userProvider.get().getUserId();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date startTime = new Date(Long.parseLong(balanceQueryDto.getStartDay()));
        String startTimeStr = sdf.format(startTime);
        //获取指定日期的排班
        setUserClasses(startTimeStr, list, userId);
        //获取下一天的排班
        try {
            Date startDate = sdf.parse(startTimeStr);
            Date nextDay = DateDetail.getNextDay(startDate);
            String nextDayStr = sdf.format(nextDay);
            setUserClasses(nextDayStr, list, userId);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
        return list;
    }

    @Override
    public LeaveShiftVo getWorkOverTimeShifts(BalanceQueryDto balanceQueryDto) throws HandleException {
        // 2024年10月23日 本版本确认该接口没有任何校验，直接返回用户涉及班次及时长
        if (StringUtils.isNotEmpty(balanceQueryDto.getTaskId())) {
            AttendanceWorkOverTimeVo workOverTime = attendanceLeaveApproveMapper.getWorkOverTime(balanceQueryDto.getTaskId());
            if (1 == workOverTime.getStatus()) {
                return JsonUtil.getJsonToBean(workOverTime.getShiftInvolved(), LeaveShiftVo.class);
            }
        }
        String userId = StringUtil.isEmpty(balanceQueryDto.getUserId()) ? userProvider.get().getUserId() : balanceQueryDto.getUserId();
        LeaveShiftVo leaveShiftVo = new LeaveShiftVo();
        Date startTime = new Date(Long.parseLong(balanceQueryDto.getStartTime()));
        Date endTime = new Date(Long.parseLong(balanceQueryDto.getEndTime()));
        DateTime dateTime = cn.hutool.core.date.DateUtil.offsetDay(startTime, 1);
        Assert.isFalse(dateTime.before(endTime), "所选时间超过24小时");
        log.info("请求涉及班次入参 userId: {},startTime : {},endTime : {}  ", userId, startTime, endTime);
        //  新加上下半天参数及请假类型参数 // applyParam 对象中start 和 end 会被一下方法重新赋值 表示命中的开始和结束时间
        ApplyParam applyParam = new ApplyParam(userId, startTime, endTime, AttendanceTypeEnum.WORKOVERTIME, null, null, null);
        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(applyParam);
        log.info("dailyRuleResultVos : {}", dailyRuleResultVos);
        leaveShiftVo.getDailyRuleVos().addAll(dailyRuleResultVos);
        // 根据请假类型进行小时/天的时长统计
        // 计算加班申请时长小时
        leaveShiftVo.setTotalDuration(FtbUtil.getTimeDifference(startTime, endTime).toString());
        return leaveShiftVo;
    }

    @Override
    public LeaveShiftVo getLeaveDuration(LeaveQueryDto leaveQueryDto) throws HandleException {
        // 2024年10月23日 本版本确认该接口没有任何校验，直接返回用户涉及班次及时长

        String userId = StringUtil.isEmpty(leaveQueryDto.getUserId()) ? userProvider.get().getUserId() : leaveQueryDto.getUserId();
        LeaveShiftVo leaveShiftVo = new LeaveShiftVo();
        boolean flg = true;
        if (StringUtils.isNotEmpty(leaveQueryDto.getTaskId())) {
            leaveQueryDto.setApplyId(leaveQueryDto.getTaskId());
        }
        Integer unit = leaveQueryDto.getUnit();
        if (StringUtils.isNotEmpty(leaveQueryDto.getApplyId())) {
            log.info("leaveQueryDto.getApplyId()" + leaveQueryDto.getApplyId());
            AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(leaveQueryDto.getApplyId());
            if (null == attendanceLeaveApproveVo) {
                log.error("未找到对应的请假审批");
                throw new HandleException("未找到对应的请假审批");
            }
            // 有对应的审批
            if (1 == attendanceLeaveApproveVo.getStatus()) {
                leaveShiftVo = JsonUtil.getJsonToBean(attendanceLeaveApproveVo.getShiftInvolved(), LeaveShiftVo.class);
                return leaveShiftVo;
            }
            flg = false;
            userId = attendanceLeaveApproveVo.getUserId();
            unit = attendanceLeaveApproveVo.getUnit();
        }
        log.info("flg === {}", flg);
        Date startTime = new Date(Long.parseLong(leaveQueryDto.getStartTime()));
        Date endTime = new Date(Long.parseLong(leaveQueryDto.getEndTime()));
        if (Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), unit)) {
            // 和出勤换算比做对比，算出时长
            BigDecimal timeDifference = DateDetail.getTimeDifference(startTime, endTime);
            // 小时假不能超过24小时
            if (timeDifference.compareTo(BigDecimal.valueOf(24)) > 0) {
                log.error("小时假不能超过24小时");
                throw new HandleException("小时假不能超过24小时");
            }
        }
        log.info("请求涉及班次入参 userId: {},startTime : {},endTime : {}  ", userId, startTime, endTime);
        //  新加上下半天参数及请假类型参数 // applyParam 对象中start 和 end 会被一下方法重新赋值 表示命中的开始和结束时间
        ApplyParam applyParam = new ApplyParam(userId, startTime, endTime, AttendanceTypeEnum.LEAVE, leaveQueryDto.getStartTimeType(), leaveQueryDto.getEndTimeType(), leaveQueryDto.getUnit());
        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(applyParam);
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (1 == dailyRuleResultVo.getType()) {
                // 有异常直接报错
                log.error(dailyRuleResultVo.getFailMsg());
                throw new HandleException(dailyRuleResultVo.getFailMsg());
            }
        }
        log.info("dailyRuleResultVos : {}", dailyRuleResultVos);
        List<DailyRuleResultVo> dailyCopy = dailyRuleResultVos;
        leaveShiftVo.getDailyRuleVos().addAll(dailyRuleResultVos);
        // 根据请假类型进行小时/天的时长统计
        //申请时长小时
        BigDecimal day = BigDecimal.ZERO;
        dailyRuleResultVos = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).collect(Collectors.toList());
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (null != dailyRuleResultVo.getInPoint() && null != dailyRuleResultVo.getOutPoint()) {
                day = day.add(dailyRuleResultVo.getLeaveDays());
            }
        }
        //先根据日期date分组
        DailyRuleResultVo min = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).min(Comparator.comparing(DailyRuleResultVo::getInPoint)).orElse(null);
        if (null != min) {
            leaveQueryDto.setStartTimeHit(min.getInPoint());
        } else {
            leaveQueryDto.setStartTimeHit(startTime);
        }
        DailyRuleResultVo max = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).max(Comparator.comparing(DailyRuleResultVo::getOutPoint)).orElse(null);
        if (null != max) {
            leaveQueryDto.setEndTimeHit(max.getOutPoint());
        } else {
            leaveQueryDto.setEndTimeHit(endTime);
        }
        // 命中时长为0 表示未命中班次且需要特殊处理半天及天的请假时间
        if (BigDecimal.ZERO.compareTo(day) == 0 && !Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(),leaveQueryDto.getUnit())) {
            leaveQueryDto.setStartTimeHit(DateDetail.getDayBeginByDay(applyParam.getStart()));
            // endtime 取日期的23:59:59
            leaveQueryDto.setEndTimeHit(DateDetail.getDayEndByDay(applyParam.getEnd()));
        }
        // 处理划线排班
        day = getUnderline(dailyCopy, userId, applyParam.getStart(), applyParam.getEnd(), day,leaveQueryDto.getUnit());
        // 如果在这儿day的值还是=0 则说明没有命中班次，则需要重新计算请假时长
        if (BigDecimal.ZERO.compareTo(day) == 0) {
            // 请假时长公式：
            day = getDay(unit, leaveQueryDto, userId, day,null);
        }
        // 保留两位
        leaveShiftVo.setTotalDayDuration(day.setScale(2, RoundingMode.HALF_UP).toString());
        return leaveShiftVo;
    }

    /**
     * 获取请假时长
     *
     * @param unit
     * @param leaveQueryDto
     * @param userId
     * @param day
     * @param attendanceBaseSetting
     **/
    @NotNull
    private BigDecimal getDay(Integer unit, LeaveQueryDto leaveQueryDto, String userId, BigDecimal day,AttendanceBaseSetting attendanceBaseSetting) throws HandleException {
        // 小时、半天、全天只区分命中和未命中，命中了时长=命中时长，未命中班次 请假时长 = 结束-开始（区分小时1天限制，半天取一个半天0.5，天一天取1）
        if (Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), unit)) {
            // 和出勤换算比做对比，算出时长
            BigDecimal timeDifference = DateDetail.getTimeDifference(leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit());
            // 小时假不能超过24小时
            if (timeDifference.compareTo(BigDecimal.ZERO) == 0) {
                return BigDecimal.ZERO;
            }
            if(null == attendanceBaseSetting) {
                attendanceBaseSetting = getAttendanceRatio(leaveQueryDto, userId);
                attendanceBaseSetting.setAttendanceRatio(null == attendanceBaseSetting.getAttendanceRatio() ? new BigDecimal("8") : attendanceBaseSetting.getAttendanceRatio());
            }

            if (!DateDetail.getDate2Str(leaveQueryDto.getStartTimeHit(), DateDetail.DF).equals(DateDetail.getDate2Str(leaveQueryDto.getEndTimeHit(), DateDetail.DF))) {
                // 小时假跨天,单独拆分每天的时间进行计算时长
                // 获取前一天的时长
                BigDecimal dayTime1 = DateDetail.getTimeDifference(leaveQueryDto.getStartTimeHit(), DateDetail.getDayEndByDay(leaveQueryDto.getStartTimeHit()));
                // 获取后一天的时长
                BigDecimal dayTime2 = DateDetail.getTimeDifference(DateDetail.getDayBeginByDay(leaveQueryDto.getEndTimeHit()),  leaveQueryDto.getEndTimeHit());
                BigDecimal hours1 = dayTime1.divide(attendanceBaseSetting.getAttendanceRatio(), 3, RoundingMode.HALF_UP);
                BigDecimal hours2 = dayTime2.divide(attendanceBaseSetting.getAttendanceRatio(), 3, RoundingMode.HALF_UP);
                day = (hours1.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : hours1).add(hours2.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : hours2).setScale(2, RoundingMode.HALF_UP);
            } else {
                BigDecimal hours = timeDifference.divide(attendanceBaseSetting.getAttendanceRatio(), 2, RoundingMode.HALF_UP);
                day = hours.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : hours;
            }
        } else if (Objects.equals(BalanceEnum.BALANCE_UNIT_T.getCode(), unit)) {
            // 天类型请假计算时长结束日期减开始日期
            day = DateDetail.calculateDayDiff(leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit()).add(BigDecimal.ONE);
        } else {
            // 半天计算时长
            day = DateDetail.calculateHalfDayDiff(leaveQueryDto.getStartTimeHit(), leaveQueryDto.getStartTimeType(), leaveQueryDto.getEndTimeHit(), leaveQueryDto.getEndTimeType());
        }
        return day;
    }

    private AttendanceBaseSetting getAttendanceRatio(LeaveQueryDto leaveQueryDto, String userId) {
        List<AttendanceGroupUser> attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit(), Collections.singletonList(userId), null);
        String groupId = attendanceGroupUsers.get(0).getGroupId();
        Map<String, AttendanceBaseSetting> settingMap = attendanceBaseSettingService.getEnableBaseSettingAll(Collections.singletonList(groupId));
        return settingMap.get(groupId);
    }

    /**
     * 获取划线排班涉及时长
     * @param userId 用户
     * @param startTime 开始时间
     * @param endTime 结束时间
     * @param dailyCopy 排班集合
     * @param day 天数
     * @param unit 单位 :  1 :小时 2: 天  3.半天
     */
    private BigDecimal getUnderline(List<DailyRuleResultVo> dailyCopy, String userId, Date startTime, Date endTime , BigDecimal day,Integer unit) throws HandleException {
        dailyCopy = dailyCopy.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).collect(Collectors.toList());
        if (!dailyCopy.isEmpty()) {
            if (Objects.equals(unit, BalanceEnum.BALANCE_UNIT_XH.getCode())){
                BigDecimal attendanceRatio = getAttendanceRatio(userId, startTime, endTime);
                // 根据日期进行分组
                Map<Date, List<DailyRuleResultVo>> dayDailys = dailyCopy.stream().collect(Collectors.groupingBy(DailyRuleResultVo::getDate));
                for (Map.Entry<Date, List<DailyRuleResultVo>> entry : dayDailys.entrySet()) {
                    List<DailyRuleResultVo> dailyRuleResultVos1 = entry.getValue();
                    BigDecimal zero = BigDecimal.ZERO;
                    for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos1) {
                        zero = zero.add(dailyRuleResultVo.getDuration());
                    }
                    // 一天最多扣一天的劵
                    zero = zero.divide(attendanceRatio, 3, RoundingMode.HALF_UP).compareTo(BigDecimal.ONE) >= 0 ? BigDecimal.ONE : zero.divide(attendanceRatio, 3, RoundingMode.HALF_UP);
                    day = day.add(zero);
                }
            } else {
                // 获取数据去重后的日期
                List<Date> list = dailyCopy.stream().map(DailyRuleResultVo::getDate).distinct().collect(Collectors.toList());
                day = day.add(new BigDecimal(list.size()));
            }
        }
        return day.setScale(2, RoundingMode.HALF_UP);
    }


    @NotNull
    private BigDecimal getAttendanceRatio(String userId, Date startTime, Date endTime) throws HandleException {
        Set<String> groupIds = getGroupIdsNew(userId, startTime, endTime);
        if (groupIds.size() > 1) {
            // 选择的时间段范围内有多个考勤组 请假失败
            log.error("选择的时间段范围内有其他借调信息");
            throw new HandleException("选择的时间段范围内有其他借调信息");
        }
        if (groupIds.isEmpty()) {
            log.error("未找到用户的考勤组");
            throw new HandleException("未找到用户的考勤组");
        }
        List<String> list = new ArrayList<>(groupIds);
        Map<String, AttendanceBaseSetting> enableBaseSetting = attendanceBaseSettingService.getEnableBaseSetting(list);
        AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(list.get(0));
        // 查询用户对应的考勤组配置的基础规则
        return null == attendanceBaseSetting ? new BigDecimal("8") : attendanceBaseSetting.getAttendanceRatio();
    }


    /**
     * 获取用户时间段内所在考勤组
     *
     * @param userId    用户
     * @param startTime 开始时间
     * @param endTime   结束时间
     * @return 考勤组ID
     * @throws HandleException 获取失败
     */

    private Set<String> getGroupIdsNew(String userId, Date startTime, Date endTime) throws HandleException {
        // 获取用户时间段范围内的所有考勤组
        GroupCheckVo groupCheckVo = new GroupCheckVo();
        List<AttendanceGroupUserVo> userGroupList = attendanceGroupUserMapper.getUserGroupList(userId);
        if (null != userGroupList && !userGroupList.isEmpty()) {
            List<AttendanceGroupUserVo> selfGroup = userGroupList.stream().filter(v -> 2 == v.getType()).collect(Collectors.toList());
            if (CollectionUtil.isEmpty(selfGroup)) {
                setMyGroup(userGroupList, groupCheckVo);
            }
            checkSelfGroupNew(groupCheckVo, selfGroup, startTime, endTime);
            if (groupCheckVo.isMyType()) {
                setMyGroup(userGroupList, groupCheckVo);
            }
            if (CollectionUtil.isEmpty(groupCheckVo.getGroupIds()) || groupCheckVo.getGroupIds().isEmpty()) {
                throw new HandleException("未找到你所属考勤组!");
            }
        }
        return groupCheckVo.getGroupIds();
    }

    private void checkSelfGroupNew(GroupCheckVo groupCheckVo, List<AttendanceGroupUserVo> selfGroup, Date startTime, Date endTime) {
        for (AttendanceGroupUserVo attendanceGroupUserVo : selfGroup) {
            List<SecondmentDateVo> secondmentDateVos = StringUtil.isEmpty(attendanceGroupUserVo.getTimeJson()) ? new ArrayList<>() : JsonUtil.getJsonToList(attendanceGroupUserVo.getTimeJson(), SecondmentDateVo.class);
            if (secondmentDateVos.isEmpty()) {
                groupCheckVo.getGroupIds().add(attendanceGroupUserVo.getGroupId());
                break;
            } else {
                for (SecondmentDateVo dateVo : secondmentDateVos) {
                    if (DateDetail.checkTimeBetween(startTime, dateVo.getStartTime(), dateVo.getEndTime())
                            || DateDetail.checkTimeBetween(endTime, dateVo.getStartTime(), dateVo.getEndTime())
                            || DateDetail.checkTimeBetween(dateVo.getStartTime(), startTime, endTime)
                            || DateDetail.checkTimeBetween(dateVo.getEndTime(), startTime, endTime)) {
                        groupCheckVo.getGroupIds().add(attendanceGroupUserVo.getGroupId());
                        if (DateDetail.checkTimeBetween(startTime, dateVo.getStartTime(), dateVo.getEndTime())
                                && DateDetail.checkTimeBetween(endTime, dateVo.getStartTime(), dateVo.getEndTime())) {
                            groupCheckVo.setMyType(false);
                        }
                        break;
                    }
                }
            }
        }
    }


    /**
     * 校验借调考勤组信息
     *
     * @param groupIds  考勤组ids
     * @param selfGroup 借调考勤组
     * @param startTime 开始时间
     * @param endTime   结束时间
     */
    private void checkSelfGroup(Set<String> groupIds, List<AttendanceGroupUserVo> selfGroup, Date startTime, Date endTime) {
        for (AttendanceGroupUserVo attendanceGroupUserVo : selfGroup) {
            List<SecondmentDateVo> secondmentDateVos = StringUtil.isEmpty(attendanceGroupUserVo.getTimeJson()) ? new ArrayList<>() : JsonUtil.getJsonToList(attendanceGroupUserVo.getTimeJson(), SecondmentDateVo.class);
            if (secondmentDateVos.isEmpty()) {
                groupIds.add(attendanceGroupUserVo.getGroupId());
                break;
            } else {
                for (SecondmentDateVo dateVo : secondmentDateVos) {
                    if (DateDetail.checkTimeBetween(startTime, dateVo.getStartTime(), dateVo.getEndTime())
                            || DateDetail.checkTimeBetween(endTime, dateVo.getStartTime(), dateVo.getEndTime())
                            || DateDetail.checkTimeBetween(dateVo.getStartTime(), startTime, endTime)
                            || DateDetail.checkTimeBetween(dateVo.getEndTime(), startTime, endTime)) {
                        groupIds.add(attendanceGroupUserVo.getGroupId());
                        break;
                    }
                }
            }
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void leaveApprove(String id, Integer status, String tenantId, String userId, String userName) throws ApproveException {
        AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(id);
        if (null != attendanceLeaveApproveVo) {
            consumption(id, attendanceLeaveApproveVo, status, tenantId, userId, userName);
        }
    }


    // 2.1.1 已重构 加入解耦班次逻辑
    @Override
    public LeaveConsumptionDetailVo getResidueBalance(LeaveQueryDto leaveQueryDto) throws HandleException, ApproveException {
        log.error("leaveQueryDto:{}", leaveQueryDto);
        // (leaveTypeUnit=null, startTime=1730131200000, endTime=1730131200000, startTimeType=1, endTimeType=1, leaveTypeId=1850802520956473344, balanceStatus=0, paid=1, noPaid=1, applyId=null, typeId=1850802520956473344, unit=3)
        String userId = userProvider.get().getUserId();
        Integer unit = leaveQueryDto.getUnit();
        Date startTime = new Date(Long.parseLong(leaveQueryDto.getStartTime()));
        Date endTime = new Date(Long.parseLong(leaveQueryDto.getEndTime()));
        if (Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), unit)) {
            // 和出勤换算比做对比，算出时长
            BigDecimal timeDifference = DateDetail.getTimeDifference(startTime, endTime);
            // 小时假不能超过24小时
            if (timeDifference.compareTo(BigDecimal.valueOf(24)) > 0) {
                log.error("小时假不能超过24小时");
                throw new HandleException("小时假不能超过24小时");
            }
        }

        if (StringUtil.isNotEmpty(leaveQueryDto.getApplyId())) {
            AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(leaveQueryDto.getApplyId());
            if (Objects.isNull(attendanceLeaveApproveVo)) {
                log.error("未找到本次请假审批提交记录");
                throw new ApproveException("未找到本次请假审批提交记录");
            }
            userId = attendanceLeaveApproveVo.getUserId();
        }
        //通过请假类型id查询请假类型详情
        AttendanceLeaveRulesVo userLeaveDetail = attendanceLeaveTypeService.getUserLeaveDetail(leaveQueryDto.getTypeId(), userId);
        if (null == userLeaveDetail) {
            throw new HandleException("未找到本次请假审批选择的类型");
        }
        log.info("leaveSetting: {}", userLeaveDetail);
        // 划线排班不能请半天类型的假
        if (Objects.equals(BalanceEnum.BALANCE_UNIT_BT.getCode(), userLeaveDetail.getUnit())) {
            boolean lineScheduleByUserId = attendanceDailyRuleService.hasLinearRulesByPeriod(userId, startTime, endTime);
            if (lineScheduleByUserId) {
                log.error("划线排班不能请半天类型的假");
                throw new HandleException("划线排班不能请半天类型的假");
            }
        }
        ApplyParam applyParam = new ApplyParam(userId, startTime, endTime, AttendanceTypeEnum.LEAVE, leaveQueryDto.getStartTimeType(), leaveQueryDto.getEndTimeType(), unit);
        // applyParam 对象中start 和 end 会被一下方法重新赋值 表示命中的开始和结束时间
        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(applyParam);
        List<DailyRuleResultVo> dailyCopy = dailyRuleResultVos;
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (1 == dailyRuleResultVo.getType()) {
                // 有异常直接报错
                log.error(dailyRuleResultVo.getFailMsg());
                throw new HandleException(dailyRuleResultVo.getFailMsg());
            }
        }
        // 计算申请时长
        BigDecimal day = BigDecimal.ZERO;
        dailyRuleResultVos = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).collect(Collectors.toList());
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            day = day.add(dailyRuleResultVo.getLeaveDays());
        }

        //先根据日期date分组
        DailyRuleResultVo min = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).min(Comparator.comparing(DailyRuleResultVo::getInPoint)).orElse(null);
        if (null != min) {
            leaveQueryDto.setStartTimeHit(min.getInPoint());
        } else {
            leaveQueryDto.setStartTimeHit(startTime);
        }
        DailyRuleResultVo max = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).max(Comparator.comparing(DailyRuleResultVo::getOutPoint)).orElse(null);
        if (null != max) {
            leaveQueryDto.setEndTimeHit(max.getOutPoint());
        } else {
            leaveQueryDto.setEndTimeHit(endTime);
        }

        // 命中时长为0 表示未命中班次且需要特殊处理半天及天的请假时间
        if (BigDecimal.ZERO.compareTo(day) == 0 && !Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(),userLeaveDetail.getUnit())) {
            leaveQueryDto.setStartTimeHit(DateDetail.getDayBeginByDay(applyParam.getStart()));
            // endtime 取日期的23:59:59
            leaveQueryDto.setEndTimeHit(DateDetail.getDayEndByDay(applyParam.getEnd()));
        }
        // 校验加班时间是否重叠
        checkWorkTimeOverlap(attendanceLeaveApproveMapper.getUserWorkByTimeSlot(userId, leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit()), "选择的时间段范围内有其他加班(待审核/审核已通过)");
        // 校验考勤组是否锁定
        // 通过开始/结束时间获取申请时长及设计班次
        // 校验请假时间是否重叠  2024年10月30日更改逻辑为只校验当前审批与已审核通过的审批时间是否重叠
        // 考勤1.8.3版本新增审核中判断2025年08月28日
        checkLeaveTimeOverlap(userId,  leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit(), leaveQueryDto.getApplyId(),leaveQueryDto.getStartTimeType(), leaveQueryDto.getEndTimeType(), unit);
        // 新增借调校验
        ActionResult<Void> result = new ActionResult<>();
        result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode());
        result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg());
        checkGroupClockForOa(userId,  leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit(), result);
        if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
            throw new ApproveException(result.getMsg());
        }
        // 处理划线排班
        day = getUnderline(dailyCopy, userId, leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit(), day,userLeaveDetail.getUnit());
        // 如果在这儿day的值还是=0 则说明没有命中班次，则需要重新计算请假时长
        if (BigDecimal.ZERO.compareTo(day) == 0) {
            // 请假时长公式：
            // 小时、半天、全天只区分命中和未命中，命中了时长=命中时长，未命中班次 请假时长 = 结束-开始（区分小时1天限制，半天取一个半天0.5，天一天取1）
            day = getDay(userLeaveDetail.getUnit(), leaveQueryDto, userId, day,null);
        }
        if (new BigDecimal(userLeaveDetail.getDuration()).compareTo(day) < 0) {
            throw new HandleException("请假时长超出可请假的最大时长");
        }
        return attendanceUserBalanceRecordService.leaveSimulationConsumption(userId,  day, userLeaveDetail);
    }


    /**
     * 校验考勤组是否锁定
     *
     * @param userId    用户Id
     * @param startTime 开始时间
     * @param endTime   结束时间
     */
    private void checkClock(String userId, Date startTime, Date endTime) throws HandleException {

        Set<String> groupIds = getGroupIdsNew(userId, startTime, endTime);
        if (groupIds.size() > 1) {
            log.error("找到多个考勤组");
            throw new HandleException("申请时间不可以跨两个考勤组");
        } else if (groupIds.size() == 1) {
            String monthDate = DateDetail.getDate2Str(startTime, DateDetail.DF15);
            Map<String, Boolean> map = attendanceDayStatisticsService.selectUserIsSeal(List.of(userId), monthDate);
            if (map.get(userId)) {
                throw new HandleException(monthDate + "已封账，过去的考勤记录无法修改！");
            }
        }
    }

    /**
     * 外出和出差专用
     * 疑似作废
     * @param userId    用户
     * @param startTime 开始时间
     * @param endTime   结束时间
     * @throws HandleException 错误
     */
    private void checkGroupClock(String userId, Date startTime, Date endTime) throws HandleException {
        GroupCheckVo groupBusinessAndGoOut = getGroupBusinessAndGoOut(userId, startTime, endTime);
        if (groupBusinessAndGoOut.getGroupIds().size() > 1) {
            log.error("找到多个考勤组");
            // 申请时间与借调时间(XX月XX日)冲突！请重新选择
            String dayStr = groupBusinessAndGoOut.getRepeatDay().stream().sorted().collect(Collectors.joining(","));
            throw new HandleException("申请时间与借调时间" + dayStr + "冲突！请重新选择");
        } else if (groupBusinessAndGoOut.getGroupIds().size() == 1) {
            // 判定是否封账
            String monthDate = DateDetail.getDate2Str(startTime, DateDetail.DF15);
            Map<String, Boolean> map = attendanceDayStatisticsService.selectUserIsSeal(List.of(userId), monthDate);
            if (map.get(userId)) {
                throw new HandleException(monthDate + "已封账，过去的考勤记录无法修改！");
            }
        }
    }

    /**
     * 校验借调与考勤组锁定  考勤1.8.3版本新增审核中判断
     * 2.0 借调划归人事  该公共方法只校验考勤组和封账
     * @param userId    用户Id
     * @param startTime 开始时间
     * @param endTime   结束时间
     * @param result    ActionResult
     */
    private void checkGroupClockForOa(String userId, Date startTime, Date endTime, ActionResult<Void> result) {

        List<AttendanceGroupUser> attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(startTime, endTime, Collections.singletonList(userId), null);
        if (null == attendanceGroupUsers || attendanceGroupUsers.isEmpty()) {
            result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
            result.setMsg("所选时间范围内未找到考勤组");
            return;
        }
        if (attendanceGroupUsers.stream().map(AttendanceGroupUser::getGroupId).distinct().count() > 1) {
            log.info("找到多个考勤组");
            result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
            result.setMsg("申请时间与借调时间冲突！请重新选择");
        } else {
            // 校验用户是否在该时间范围被其它考勤组借调（审批中） 2.0 借调划归人事
            FtbSecondMentQueryDTO ftbSecondMentQueryDTO = new FtbSecondMentQueryDTO();
            ftbSecondMentQueryDTO.setUserIds(Collections.singletonList(userId));
            ftbSecondMentQueryDTO.setStartTime(startTime);
            ftbSecondMentQueryDTO.setEndTime(endTime);
            List<FtbPersonnelsSecondmentVO> secondmentRecordBath = ftbPersonneApi.getSecondmentRecordBath(ftbSecondMentQueryDTO);
            if (CollectionUtil.isNotEmpty(secondmentRecordBath)) {
                List<String> uIds = Optional.ofNullable(secondmentRecordBath)
                        .orElse(Collections.emptyList())
                        .stream()
                        .filter(Objects::nonNull) // 过滤掉null元素
                        .map(FtbPersonnelsSecondmentVO::getUserId)
                        .filter(Objects::nonNull) // 再次过滤掉null的userId
                        .collect(Collectors.toList());
                if (!uIds.isEmpty()) {
                    ActionResult<List<UserBoundVO>> userList = v2UserApi.getAllUserInfoBatch(Stream.of(userId).collect(Collectors.toList()), null);
                    if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) {
                        result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
                        result.setMsg("用户:" + userList.getData().get(0).getUserName() + "已处于借调审批中，请勿重复操作！");
                    }
                }
            }
            // 判定是否封账
            String monthDate = DateDetail.getDate2Str(startTime, DateDetail.DF15);
            Map<String, Boolean> map = attendanceDayStatisticsService.selectUserIsSeal(List.of(userId), monthDate);
            if (map.get(userId)) {
                result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
                result.setMsg(monthDate + "已封账，过去的考勤记录无法修改！");
            }
        }
    }


    /**
     * 校验时间段内用户有无外出 考勤1.8.3版本新增审核中判断
     *
     * @param userId    用户Id
     * @param startTime 开始时间
     * @param endTime   结束时间
     */
    private void checkGoOutForOa(String userId, Date startTime, Date endTime, ActionResult<Void> result, String taskId) {
        Integer userWorkByTimeSlot = attendanceLeaveApproveMapper.getUserGoOutForOa(userId, startTime, endTime, taskId);
        Integer goOutForDay = attendanceLeaveApproveMapper.getUserGoOutForDay(userId, startTime, endTime, taskId);
        if ((null != userWorkByTimeSlot && userWorkByTimeSlot > 0) || (null != goOutForDay && goOutForDay > 0)) {
            result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
            result.setMsg("申请时间段内已有申请中或申请通过的外出申请，请重新选择！");
        }
    }

    /**
     * 校验时间段内用户有无出差 考勤1.8.3版本新增审核中判断
     *
     * @param userId    用户Id
     * @param startTime 开始时间
     * @param endTime   结束时间
     */
    private void checkBusinessTripForOa(String userId, Date startTime, Date endTime, ActionResult<Void> result, String taskId) {
        Integer userWorkByTimeSlot = attendanceLeaveApproveMapper.getBusinessTripForOa(userId, startTime, endTime, taskId);
        if (null != userWorkByTimeSlot && userWorkByTimeSlot > 0) {
            result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
            result.setMsg("申请时间段内已有申请中或申请通过的出差申请，请重新选择！");
        }
    }


    /**
     * 获取用户时间段范围内的所有考勤组
     * 外出出差专用校验
     *
     * @param userId    用户Id
     * @param startTime 开始时间
     * @param endTime   结束时间
     *                  1.时间段完全在开始时间之前
     *                  2.时间段包含开始时间但不包含结束时间
     *                  3.时间段完全包含开始时间结束时间
     *                  4.时间段完全在开始时间与结束时间之间
     *                  5.时间段完全在开始时间之后包含结束时间
     *                  6.时间段完全在结束时间之后
     */
    private GroupCheckVo getGroupBusinessAndGoOut(String userId, Date startTime, Date endTime) throws HandleException {
        GroupCheckVo groupCheckVo = new GroupCheckVo();
        SimpleDateFormat sdfday = new SimpleDateFormat("yyyy-MM-dd");
        // 开始时间结束时间是否完整的借调
        List<AttendanceGroupUserVo> userGroupList = attendanceGroupUserMapper.getUserGroupList(userId);
        if (null != userGroupList && !userGroupList.isEmpty()) {
            List<AttendanceGroupUserVo> selfGroup = userGroupList.stream().filter(v -> 2 == v.getType()).collect(Collectors.toList());
            if (CollectionUtil.isEmpty(selfGroup)) {
                setMyGroup(userGroupList, groupCheckVo);
            }
            checkSelfGroupNew(selfGroup, startTime, endTime, sdfday, groupCheckVo);
            if (groupCheckVo.isMyType()) {
                setMyGroup(userGroupList, groupCheckVo);
            }
            if (CollectionUtil.isEmpty(groupCheckVo.getGroupIds()) || groupCheckVo.getGroupIds().isEmpty()) {
                throw new HandleException("未找到你所属考勤组!");
            }
        }
        return groupCheckVo;

    }

    /**
     * 我的考勤组
     *
     * @param userGroupList 考勤组集合
     * @param groupCheckVo  用户考勤组信息
     */
    private static void setMyGroup(List<AttendanceGroupUserVo> userGroupList, GroupCheckVo groupCheckVo) {
        AttendanceGroupUserVo myGroup = userGroupList.stream().filter(v -> 1 == v.getType()).findFirst().orElse(null);
        if (myGroup != null) {
            groupCheckVo.setMyGroupId(myGroup.getGroupId());
            groupCheckVo.getGroupIds().add(myGroup.getGroupId());
        }
    }

    /**
     * 校验借调考勤组
     *
     * @param selfGroup    借调考勤组
     * @param startTime    开始时间
     * @param endTime      结束时间
     * @param sdfday       年月日格式
     * @param groupCheckVo 考勤组
     */
    private void checkSelfGroupNew(List<AttendanceGroupUserVo> selfGroup, Date startTime, Date endTime, SimpleDateFormat sdfday, GroupCheckVo groupCheckVo) {
        for (AttendanceGroupUserVo attendanceGroupUserVo : selfGroup) {
            List<SecondmentDateVo> secondmentDateVos = StringUtil.isEmpty(attendanceGroupUserVo.getTimeJson()) ? new ArrayList<>() : JsonUtil.getJsonToList(attendanceGroupUserVo.getTimeJson(), SecondmentDateVo.class);
            if (secondmentDateVos.isEmpty()) {
                groupCheckVo.getGroupIds().add(attendanceGroupUserVo.getGroupId());
                // 借调 拼接时间集合
                List<Date> allDays = DateDetail.getAllDays(startTime, endTime);
                allDays.forEach(v -> {
                    groupCheckVo.getRepeatDay().add(sdfday.format(v));
                });
                break;
            } else {
                for (SecondmentDateVo dateVo : secondmentDateVos) {
                    if (DateDetail.checkTimeBetween(startTime, dateVo.getStartTime(), dateVo.getEndTime())
                            || DateDetail.checkTimeBetween(endTime, dateVo.getStartTime(), dateVo.getEndTime())
                            || DateDetail.checkTimeBetween(dateVo.getStartTime(), startTime, endTime)
                            || DateDetail.checkTimeBetween(dateVo.getEndTime(), startTime, endTime)) {
                        groupCheckVo.getGroupIds().add(attendanceGroupUserVo.getGroupId());
                        // 借调 拼接时间集合
                        List<String> allDays = DateDetail.getDateIntersection(startTime, endTime, dateVo.getStartTime(), dateVo.getEndTime());
                        if (!allDays.isEmpty()) {
                            groupCheckVo.getRepeatDay().addAll(allDays);
                        }
                    }
                    if (DateDetail.checkTimeBetween(startTime, dateVo.getStartTime(), dateVo.getEndTime())
                            && DateDetail.checkTimeBetween(endTime, dateVo.getStartTime(), dateVo.getEndTime())) {
                        groupCheckVo.setMyType(false);
                    }

                }
            }
        }
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void workApprove(String id, Integer status, String tenantId, String userId, String userName) throws ApproveException {
        AttendanceWorkOverTimeVo workOverTime = attendanceLeaveApproveMapper.getWorkOverTime(id);
        if (null == workOverTime) {
            log.error("未找到对应的加班审批");
            throw new ApproveException("未找到对应的加班审批");
        }
        LeaveShiftVo workOverTimeShifts = new LeaveShiftVo();
        if (Objects.equals(BalanceEnum.APPROVE_STATUS_PASS.getCode(), status)) {
            try {
                // 查询加班明细 及保存
                BalanceQueryDto balanceQueryDto = new BalanceQueryDto(workOverTime.getUserId(), workOverTime.getStartTime().getTime() + "", workOverTime.getEndTime().getTime() + "");
                workOverTimeShifts = getWorkOverTimeShifts(balanceQueryDto);
                // 生成班次
                attendanceDailyRuleService.applyDailyRuleHandle(new ApplyParam(workOverTime.getUserId(), id, workOverTime.getStartTime(), workOverTime.getEndTime(), BigDecimal.ZERO, AttendanceTypeEnum.WORKOVERTIME));
            } catch (HandleException e) {
                throw new ApproveException(e.getMessage());
            }
        }

        //修改审批状态
        attendanceLeaveApproveMapper.updateWorkOverTimeApprove(id, status, userId, userName, JSONUtil.toJsonStr(workOverTimeShifts));
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void selfApprove(String id, String departureTime, String backTime, Integer status, String userId, String userName, String tenantId) throws ApproveException, HandleException {

        //获取待审批的借调审批详情
        AttendanceSelfApproveVo selfApprove = attendanceLeaveApproveMapper.getSelfApprove(id);
        if (null != selfApprove) {
            if (status != 1) {
                attendanceLeaveApproveMapper.updateSelfApprove(selfApprove.getId(), null, null, status, userId, userName);
            } else {
                //校验参数不能为空
                Date departure = StringUtils.isEmpty(departureTime) ? selfApprove.getStartTime() : new Date(Long.parseLong(departureTime));
                Date back = StringUtils.isEmpty(backTime) ? selfApprove.getEndTime() : new Date(Long.parseLong(backTime));
                // 通过时校验借调离岗回岗时间 测试提供
                // 离岗时间， 在借调开始时间之前
                // 回岗时间，在借调结束时间之后
                if (departure.after(selfApprove.getStartTime())) {
                    throw new ApproveException("离岗时间需在借调开始时间之前");
                }
                if (back.before(selfApprove.getEndTime())) {
                    throw new ApproveException("回岗时间需在借调结束时间之后");
                }
                List<String> userIds = attendanceLeaveApproveMapper.getUserList(selfApprove.getId());
                //查出审批对应的用户
                if (null != userIds && !userIds.isEmpty()) {
                    // 修改用户进入考勤组
                    selfApprove.setUserIds(userIds);
                    AttendanceSecondedDto bean = BeanUtil.toBean(selfApprove, AttendanceSecondedDto.class);
                    bean.setBackTime(back);
                    bean.setDepartureTime(departure);
                    selfApprove.setBackTime(back);
                    selfApprove.setDepartureTime(departure);
                    try {
                        attendanceGroupService.secondedStart(bean);
                    } catch (HandleException e) {
                        throw new ApproveException(e.getMessage());
                    }
                    //修改用户排班
                    try {
                        attendanceDailyRuleService.secondmentDailyRuleHandle(userIds, selfApprove.getSelfGroupId(), selfApprove.getGroupId(), selfApprove.getStartTime(), selfApprove.getEndTime(), departure, back, tenantId);
                    } catch (HandleException e) {
                        throw new ApproveException(e.getMessage());
                    }
                }

                //修改审批状态  保存离岗回岗时间数据
                attendanceLeaveApproveMapper.updateSelfApprove(selfApprove.getId(), departure, back, status, userId, userName);
                // 发送Im消息 注意借调开始时间是小于当前时间点 ，小于的时候出发考勤组变动提示，否则定时通知
                log.error("准备立即开始提示被借调用户借调信息入参StartTime：{}，now:{}", selfApprove.getStartTime().getTime(), new Date().getTime());
                if (null != selfApprove.getStartTime() && (selfApprove.getStartTime().before(new Date()))) {
                    log.error("准备立即开始提示被借调用户借调信息");
                    secondedUserSendIm(selfApprove, tenantId);
                }
            }
        } else {
            log.error("未找到对应的借调审批");
            throw new ApproveException("未找到对应的借调审批");
        }

    }


    private void secondedUserSendIm(AttendanceSelfApproveVo approveVo, String tenantId) {
        DateDetail dateDetail = new DateDetail();
        //与您相关新的【借调】信息
        //借调开始时间：2024年5月16日 16:23
        //借调结束时间：2024年5月17日 09:00
        //借调考勤组：小露测试考勤组
        //被借调考勤组：小露验收考勤组
        List<String> userIds = attendanceLeaveApproveMapper.getUserList(approveVo.getId());
        List<PartUserInfoVo> allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(userIds), tenantId);
        // 组装消息
        ApproveImVo approveImVo = new ApproveImVo();
        approveImVo.setTenantId(tenantId);
        approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE);
        approveImVo.setUserIds(userIds);
        ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo();
        approveBaseImVo.setType(ApprovalSettingTypeEnum.SECONDED.getCode());
        approveBaseImVo.setStartTime(approveVo.getStartTime());
        approveBaseImVo.setEndTime(approveVo.getEndTime());
        approveBaseImVo.setGroupName(attendanceGroupService.getById(approveVo.getGroupId()).getGroupName());
        approveBaseImVo.setSecondedGroupName(attendanceGroupService.getById(approveVo.getSelfGroupId()).getGroupName());
        approveBaseImVo.setUrl(AttendanceConstant.APPROVE_SECONDED_URL);
        approveBaseImVo.setTitle("与您相关新的【借调】信息");
        //时长：1.23天
        //被借调人员：小露、张三、李四、王五
        //离岗时间：2024年5月16日  16:00
        //回岗时间：2024年5月17日  10:00
        StringBuilder userNames = new StringBuilder();
        for (int i = 0; i < allByIds.size(); i++) {
            if (i == 0) {
                userNames.append(allByIds.get(i).getRealName());
            } else {
                userNames.append("、").append(allByIds.get(i).getRealName());
            }
        }
        approveBaseImVo.setSecondedUsersName(userNames.toString());
        approveBaseImVo.setBackTime(approveVo.getBackTime());
        approveBaseImVo.setDepartureTime(approveVo.getDepartureTime());
        approveBaseImVo.setDuration(dateDetail.calculateDaysDifference(approveVo.getStartTime(), approveVo.getEndTime()) + "天");
        approveImVo.setApproveBaseImVo(approveBaseImVo);
        attendanceNoticeHandler.send(approveImVo);
    }


    @Override
    public ActionResult<Object> getApprovalAdmin(String taskId, String type) {
        try {
            switch (type) {
                // 审批类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.借调审批 7.外出  8.出差
                case "1":
                    ApplyAttendanceRepair applyAttendanceRepair = applyAttendanceRepairMapper.selectById(taskId);
                    submitCheckRepair(JSONUtil.toJsonStr(applyAttendanceRepair), "apply");
                    break;
                case "2":
                    checkChange(taskId);
                    break;
                case "3":
                    checkOutside(taskId);
                    break;
                case "4":
                    checkLeave(taskId);
                    break;
                case "5":
//                    checkWork(taskId);
                    break;
                case "6":
                    checkSeconded(taskId);
                    break;
                case "7":
                    checkOut(taskId);
                    break;
                case "8":
                    checkBusiness(taskId);
                    break;
                default:
                    throw new HandleException("请传入合法的Type");
            }
        } catch (Exception e) {
            log.error("考勤审批校验失败：{}", e.getMessage());
            return ActionResult.fail(e.getMessage());
        }
        return ActionResult.success();
    }


    /**
     * 校验时间段内用户有无外出
     *
     * @param userId    用户Id
     * @param startTime 开始时间
     * @param endTime   结束时间
     */
    private void checkGoOut(String userId, Date startTime, Date endTime) throws HandleException {
        Integer userWorkByTimeSlot = attendanceLeaveApproveMapper.getUserGoOutByTimeSlot(userId, startTime, endTime);
        if (null != userWorkByTimeSlot && userWorkByTimeSlot > 0) {
            throw new HandleException("申请时间段内已有申请中或申请通过的外出申请，请重新选择！");
        }
    }

    /**
     * 校验时间段内用户有无出差
     *
     * @param userId    用户Id
     * @param startTime 开始时间
     * @param endTime   结束时间
     */
    private void checkBusinessTrip(String userId, Date startTime, Date endTime) throws HandleException {
        Integer userWorkByTimeSlot = attendanceLeaveApproveMapper.getUserBusinessTripByTimeSlot(userId, startTime, endTime);
        if (null != userWorkByTimeSlot && userWorkByTimeSlot > 0) {
            throw new HandleException("申请时间段内已有申请中或申请通过的出差申请，请重新选择！");
        }
    }

    /**
     * 出差校验
     *
     * @param taskId 任务Id
     * @throws HandleException 异常
     */
    private void checkBusiness(String taskId) throws HandleException {
        // 校验是否和借调冲突
        BusinessTripVo vo = attendanceLeaveApproveMapper.getBusinessTrip(taskId);
        //  校验是否处于请假，加班，借调
        checkGroupClock(vo.getUserId(), vo.getStartTime(), vo.getEndTime());
        // 考勤1.5.2 去掉请假及加班校验
        checkGoOut(vo.getUserId(), vo.getStartTime(), vo.getEndTime());
        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(vo.getUserId(), vo.getStartTime(), vo.getEndTime(), AttendanceTypeEnum.BUSINESS_TRIP));
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (1 == dailyRuleResultVo.getType()) {
                // 有异常直接报错
                log.error(dailyRuleResultVo.getFailMsg());
                throw new HandleException(dailyRuleResultVo.getFailMsg());
            }
        }
    }

    /**
     * 外出校验
     *
     * @param taskId 任务Id
     * @throws HandleException 异常
     */
    private void checkOut(String taskId) throws HandleException {
        // 校验是否和借调冲突
        GoOutVo vo = attendanceLeaveApproveMapper.getGoOut(taskId);
        //  校验是否处于请假，加班，借调
        checkGroupClock(vo.getUserId(), vo.getStartTime(), vo.getEndTime());
        // 考勤1.5.2 去掉请假及加班校验
        checkBusinessTrip(vo.getUserId(), vo.getStartTime(), vo.getEndTime());
        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(vo.getUserId(), vo.getStartTime(), vo.getEndTime(), AttendanceTypeEnum.STEP_OUT));
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (1 == dailyRuleResultVo.getType()) {
                // 有异常直接报错
                log.error(dailyRuleResultVo.getFailMsg());
                throw new HandleException(dailyRuleResultVo.getFailMsg());
            }
        }
    }

    /**
     * 外勤校验
     *
     * @param taskId 任务Id
     * @throws HandleException 异常
     */
    private void checkOutside(String taskId) throws HandleException {

        LambdaQueryWrapper<FtbAttendanceClockIn> queryWrapper = new LambdaQueryWrapper<FtbAttendanceClockIn>()
                .eq(FtbAttendanceClockIn::getApprovalCode, taskId)
                .eq(FtbAttendanceClockIn::getApprovalStatus, ConstantUtil.PASS_APPROVAL);
        FtbAttendanceClockIn clockIn = attendanceClockInMapper.selectOne(queryWrapper);
        if (null == clockIn) {
            throw new HandleException("未找到打卡记录");
        }
        LambdaQueryWrapper<AttendanceClockInResult> resultQueryWrapper = new LambdaQueryWrapper<AttendanceClockInResult>()
                .eq(AttendanceClockInResult::getClockInId, clockIn.getId());
        AttendanceClockInResult dbResult = attendanceClockInResultMapper.selectOne(resultQueryWrapper);
        if (null == dbResult) {
            throw new HandleException("未找到打卡结果");
        }
        checkLock(dbResult.getId());
    }

    /**
     * 出勤变更校验
     *
     * @param taskId 任务Id
     * @throws HandleException 异常
     */
    private void checkChange(String taskId) throws HandleException {

        ApplyAttendanceChange applyAttendanceChange = applyAttendanceChangeMapper.selectById(taskId);
        checkLock(applyAttendanceChange.getClockInResultId());
    }

    /**
     * 校验考勤组是否锁定
     *
     * @param clockInResultId 打卡Id
     * @throws HandleException 异常
     */
    private void checkLock(String clockInResultId) throws HandleException {

        // 判断考勤组是否被锁定
        AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(clockInResultId);
        // 查询考勤规则
        AttendanceRuleVo rule = attendanceClockInService.getAttendanceRule(clockInResult.getRuleId());
        // 判定是否封账
        String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15);
        Map<String, Boolean> map = attendanceDayStatisticsService.selectUserIsSeal(List.of(clockInResult.getUserId()), monthDate);
        if (map.get(clockInResult.getUserId())) {
            throw new HandleException(monthDate + "已封账，过去的考勤记录无法修改！");
        }
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || attendanceClockInService.getOutsideCheck(rule, clockInResult.getClockInType())) {
            throw new HandleException("操作失败，班次结果已变更！");
        }
    }

    @Override
    public ActionResult<Object> submitValidation(String body, String type) {
        try {
            switch (type) {
                // 审批类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.借调审批  7.外出  8.出差
                case "1":
                    submitCheckRepair(body, "submit");
                    break;
                case "2":
                    submitCheckChange(body);
                    break;
                case "3":
                    log.info("外勤审批验证: {}", body);
                    break;
                case "4":
                    submitCheckLeave(body);
                    break;
                case "5":
                    submitCheckWork(body);
                    break;
                case "6":
                    submitCheckSeconded(body);
                    break;
                case "7":
                    submitCheckOut(body);
                    break;
                case "8":
                    submitCheckBusiness(body);
                    break;
                default:
                    throw new HandleException("请传入合法的Type");
            }
        } catch (Exception e) {
            e.printStackTrace();
            return ActionResult.fail(e.getMessage());
        }
        return ActionResult.success();
    }

    /**
     * 出差提交校验
     *
     * @param body 提交对象
     * @throws HandleException 异常
     */
    private void submitCheckBusiness(String body) throws HandleException {
        BusinessTripVo vo = JsonUtil.getJsonToBean(body, BusinessTripVo.class);
        //  校验是否处于请假，加班，借调
        checkGroupClock(vo.getUserId(), vo.getStartTime(), vo.getEndTime());
        // 考勤1.5.2 去掉请假及加班校验
//        checkLeaveBYPeriod(vo.getUserId(), vo.getStartTime(), vo.getEndTime());
//        checkWorkBYPeriod(vo.getUserId(), vo.getStartTime(), vo.getEndTime());
        // 校验是否与外出重叠
        checkGoOut(vo.getUserId(), vo.getStartTime(), vo.getEndTime());
        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(vo.getUserId(), vo.getStartTime(), vo.getEndTime(), AttendanceTypeEnum.BUSINESS_TRIP));
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (1 == dailyRuleResultVo.getType()) {
                // 有异常直接报错
                log.error(dailyRuleResultVo.getFailMsg());
                throw new HandleException(dailyRuleResultVo.getFailMsg());
            }
        }
    }

    /**
     * 外出提交校验
     *
     * @param body 提交对象
     * @throws HandleException 异常
     */
    private void submitCheckOut(String body) throws HandleException {
        GoOutVo vo = JsonUtil.getJsonToBean(body, GoOutVo.class);
        //  校验是否处于请假，加班，借调
        checkGroupClock(vo.getUserId(), vo.getStartTime(), vo.getEndTime());
        // 考勤1.5.2 去掉请假及加班校验
        // 校验是否与出差重叠
        checkBusinessTrip(vo.getUserId(), vo.getStartTime(), vo.getEndTime());
        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(vo.getUserId(), vo.getStartTime(), vo.getEndTime(), AttendanceTypeEnum.STEP_OUT));
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (1 == dailyRuleResultVo.getType()) {
                // 有异常直接报错
                log.error(dailyRuleResultVo.getFailMsg());
                throw new HandleException(dailyRuleResultVo.getFailMsg());
            }
        }
    }

    /**
     * 出勤变更提交校验
     *
     * @param body 提交对象
     * @throws HandleException 异常
     */
    private void submitCheckChange(String body) throws Exception {
        JSONObject json = new JSONObject(body);
        Object clockInResultIdObj = json.get("clockInResultId");
        if (null == clockInResultIdObj) {
            throw new HandleException("clockInResultId不能为空");
        }
        String clockInResultId = clockInResultIdObj.toString();
        // 判断考勤组是否被锁定
        AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(clockInResultId);
        // 查询考勤规则
        AttendanceRuleVo rule = attendanceClockInService.getAttendanceRule(clockInResult.getRuleId());
        // 判定是否封账
        String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15);
        Map<String, Boolean> map = attendanceDayStatisticsService.selectUserIsSeal(List.of(clockInResult.getUserId()), monthDate);
        if (map.get(clockInResult.getUserId())) {
            throw new HandleException(monthDate + "已封账，过去的考勤记录无法修改！");
        }
        // 判断当前出勤结果是否在进行别的审批
        int count = attendanceClockInResultMapper.getReplyingCount(clockInResultId);
        if (count > 0) {
            throw new QueryException("当前记录正在审批中，请审批结束后再重新执行操作");
        }
        json.clear();
    }

    private void submitCheckRepair(String body, String handleType) throws Exception {

        JSONObject json = new JSONObject(body);
        Object clockInResultIdObj = json.get("clockInResultId");
        if (null == clockInResultIdObj) {
            throw new HandleException("clockInResultId不能为空");
        }
        String clockInResultId = clockInResultIdObj.toString();
        // 判断当前出勤结果是否在进行别的审批
        if (handleType.equals("submit")) {
            int count = attendanceClockInResultMapper.getReplyingCount(clockInResultId);
            if (count > 0) {
                throw new QueryException("当前记录正在审批中，请审批结束后再重新执行操作");
            }
        }
        AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(clockInResultId);
        // 查询考勤规则
        AttendanceRuleVo rule = attendanceClockInService.getAttendanceRule(clockInResult.getRuleId());
        // 判定是否封账
        String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15);
        Map<String, Boolean> map = attendanceDayStatisticsService.selectUserIsSeal(List.of(clockInResult.getUserId()), monthDate);
        if (map.get(clockInResult.getUserId())) {
            throw new HandleException(monthDate + "已封账，过去的考勤记录无法修改！");
        }
        // 查询考勤组规则
        GroupRuleVo groupRule = attendanceClockInService.getGroupRule(rule.getGroupId(), clockInResult.getUserId());
        // 判断有无补卡次数
        MutablePair<Date, Date> datePair = attendanceClockInService.couldRepairRecord(rule.getDay(), clockInResult, ConstantUtil.PASS_TRUE, groupRule);
        // 判断打卡记录是否在时间范围内
        if (!DateDetail.checkTimeBetween(rule.getDay(), datePair.getLeft(), datePair.getRight())) {
            throw new QueryException("当前打卡记录不在补卡时间范围内");
        }
        json.clear();
    }

    /**
     * 借调时校验(考勤组是否被删除，用户是否还在该考勤组)
     *
     * @param groupId     借调考勤组id
     * @param selfGroupId 被借调考勤组id
     * @param userIds     被借调用户ids
     */
    private void check(String groupId, String selfGroupId, List<String> userIds) throws HandleException {

        /* 校验借调组是否已解散*/
        AttendanceGroup group = attendanceGroupMapper.selectById(groupId);
        if (group.getDeleteMark() == 1) {
            throw new HandleException("考勤组或借调组已不存在!");
        }
        /* 校验被借调考勤组是否已解散*/
        AttendanceGroup selfGroup = attendanceGroupMapper.selectById(selfGroupId);
        if (selfGroup.getDeleteMark() == 1) {
            throw new HandleException("考勤组或借调组已不存在!");
        }

        /* 校验借调过程中借调人员是否离开借调考勤组*/
        QueryWrapper<AttendanceGroupUser> groupUserQueryWrapper = new QueryWrapper<>();
        groupUserQueryWrapper.lambda()
                .eq(AttendanceGroupUser::getGroupId, selfGroupId)
                .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.CUR.getCode());
        List<AttendanceGroupUser> groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper);

        if (CollectionUtil.isEmpty(groupUserList)) {
            throw new HandleException("被借调考勤组下无成员或已被全部移除该考勤组!");
        }
        List<String> dbUserIds = groupUserList.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList());

        List<String> differenceUserIds = userIds.stream().filter(userId -> {
            return !dbUserIds.contains(userId);
        }).collect(Collectors.toList());

        if (CollectionUtil.isNotEmpty(differenceUserIds)) {
            if (CollectionUtil.size(differenceUserIds) > 5) {
                differenceUserIds = differenceUserIds.subList(0, 4);

                ActionResult<List<UserBoundVO>> userList = v2UserApi.getAllUserInfoBatch(differenceUserIds, null);
                List<String> userNameList = new ArrayList<>();
                if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) {
                    userNameList = userList.getData().stream().map(UserBoundVO::getUserName).collect(Collectors.toList());
                }
                String join = CollectionUtil.join(userNameList, ",") + "...";
                throw new HandleException(join + "已离开被借调考勤组!");
            }

            ActionResult<List<UserBoundVO>> userList = v2UserApi.getAllUserInfoBatch(differenceUserIds, null);
            List<String> userNameList = new ArrayList<>();
            if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) {
                userNameList = userList.getData().stream().map(UserBoundVO::getUserName).collect(Collectors.toList());
            }

            String join = CollectionUtil.join(userNameList, ",");
            throw new HandleException(join + "已离开被借调考勤组!");
        }
    }

    /**
     * 加班提交校验
     *
     * @param body 提交对象
     * @throws HandleException 异常
     */
    private void submitCheckWork(String body) throws HandleException {
        AttendanceWorkOverTimeVo workOverTime = JsonUtil.getJsonToBean(body, AttendanceWorkOverTimeVo.class);
        if (Math.abs(workOverTime.getEndTime().getTime() - workOverTime.getStartTime().getTime()) > 24 * 60 * 60 * 1000) {
            log.error("加班时间不许超过24小时");
            throw new HandleException("加班时间不许超过24小时");
        }
        // 时间范围内考勤组是否锁定
        checkClock(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime());
        // 校验加班时间是否重叠
        checkWorkTimeOverlap(attendanceLeaveApproveMapper.getUserWorkByTimeSlot(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()), "选择的时间段范围内有其他加班(待审核/审核已通过)");
        //校验 加班提交期间被排了班
        checkWorkTimeOverlap(attendanceDailyRuleMapper.getUserShift(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()), "选择的加班时间段范围内有其他排班");
        checkMoreGroup(workOverTime);
        // 校验请假时间是否重叠
        checkLeaveTimeOverlap(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), null,null,null,1);

        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), AttendanceTypeEnum.WORKOVERTIME));
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (1 == dailyRuleResultVo.getType()) {
                // 有异常直接报错
                log.error(dailyRuleResultVo.getFailMsg());
                throw new HandleException(dailyRuleResultVo.getFailMsg());
            }
        }
    }

    /**
     * 加班提交校验
     *
     * @param workOverTime 提交对象
     * @throws HandleException 异常
     */
    @Override
    public void submitCheckWorkForOa(AttendanceWorkOvertimeApproveDto workOverTime) throws HandleException {
        if (Math.abs(workOverTime.getEndTime().getTime() - workOverTime.getStartTime().getTime()) > 24 * 60 * 60 * 1000) {
            log.error("加班时间不许超过24小时");
            throw new HandleException("加班时间不许超过24小时");
        }
        // 时间范围内考勤组是否锁定
        // 校验加班时间是否重叠
        checkWorkTimeOverlap(attendanceLeaveApproveMapper.getUserWorkByTimeSlotForOa(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), workOverTime.getTaskId()), "选择的时间段范围内有其他申请中或已通过的加班");
        //校验 加班提交期间被排了班
        checkWorkTimeOverlap(attendanceDailyRuleMapper.getUserShift(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()), "选择的加班时间段范围内有其他排班");
        checkMoreGroup(workOverTime);
        // 校验请假时间是否重叠
//        checkLeaveTimeOverlap(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), null);

        // 新增借调校验
        ActionResult<Void> result = new ActionResult<>();
        result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode());
        result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg());
        checkGroupClockForOa(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), result);
        if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
            throw new HandleException(result.getMsg());
        }

        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), AttendanceTypeEnum.WORKOVERTIME));
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (1 == dailyRuleResultVo.getType()) {
                // 有异常直接报错
                log.error(dailyRuleResultVo.getFailMsg());
                throw new HandleException(dailyRuleResultVo.getFailMsg());
            }
        }
    }

    private void checkMoreGroup(AttendanceWorkOverTimeVo workOverTime) throws HandleException {
        //校验考勤组是否存在
        Set<String> groupIds = getGroupIdsNew(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime());

        if (CollectionUtil.isNotEmpty(groupIds) && groupIds.size() > 1) {
            throw new HandleException("加班时间不可以跨两个考勤组");
        }
    }

    private void checkMoreGroup(AttendanceWorkOvertimeApproveDto workOverTime) throws HandleException {
        //校验考勤组是否存在
        Set<String> groupIds = getGroupIdsNew(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime());

        if (CollectionUtil.isNotEmpty(groupIds) && groupIds.size() > 1) {
            throw new HandleException("加班时间不可以跨两个考勤组");
        }
    }

    private void checkWorkTimeOverlap(Integer attendanceLeaveApproveMapper, String s) throws HandleException {
        if (null != attendanceLeaveApproveMapper && attendanceLeaveApproveMapper > 0) {
            log.error(s);
            throw new HandleException(s);
        }
    }

    /**
     * 校验请假时间是否重叠  过期（ 2024年10月30日更改逻辑为只校验当前审批与已审核通过的审批时间是否重叠）
     * 考勤1.8.3版本新增审核中判断2025年08月28日
     * 2026年03月09日 考勤2.1.1 优化逻辑
     * @param userId    用户Id
     * @param startTime 本次请假命中的开始时间
     * @param endTime   本次请假命中的结束时间
     * @param applyId   申请Id
     * @param startTimeType  当请假类型为天时开始时间选择的班次信息 同ftb_attendance_shift_setting_period表F_Type类型：1 上半天 2 下半天 （半天制班次使用）当请假类型单位为半天时生效
     * @param endTimeType 当请假类型为天时开始时间选择的班次信息 同ftb_attendance_shift_setting_period表F_Type类型：1 上半天 2 下半天 （半天制班次使用）当请假类型单位为半天时生效
     * @param unit 单位 :  1 :小时 2: 天  3.半天
     */
    private void checkLeaveTimeOverlap(String userId, Date startTime, Date endTime, String applyId,Integer startTimeType,Integer endTimeType,Integer unit) throws HandleException {
        // 校验是否与请假时间重叠
        List<AttendanceLeaveApproveVo> userLeaveByTimeSlot = attendanceLeaveApproveMapper.getUserLeaveByTimeSlot(userId, startTime, endTime);
        if (null == userLeaveByTimeSlot || userLeaveByTimeSlot.isEmpty()) {
           return;
         }
        userLeaveByTimeSlot = userLeaveByTimeSlot.stream().filter(x -> !x.getId().equals(applyId)).collect(Collectors.toList());
        if ( userLeaveByTimeSlot.isEmpty()) {
            return;
        }
        // 同一个日期范围内只能提交同一种类型的请假
        if (userLeaveByTimeSlot.stream().anyMatch(x -> !x.getUnit().equals(unit))) {
            log.error("选择的时间段范围内有其他请假");
            throw new HandleException("选择的时间段范围内有其他申请中或已通过的请假");
        }
        // 已有请假的类型和本次请假类型相同，全天假不能请;
        if (Objects.equals(BalanceEnum.BALANCE_UNIT_T.getCode(), unit)) {
            log.error("选择的时间段范围内有其他请假");
            throw new HandleException("选择的时间段范围内有其他申请中或已通过的请假");
        }
        // 已有请假的类型和本次请假类型相同，小时类型的假不能请重复
        if (Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), unit)) {
            List<AttendanceLeaveApproveVo> list = userLeaveByTimeSlot.stream().filter(x -> x.getUnit().equals(BalanceEnum.BALANCE_UNIT_XH.getCode())).collect(Collectors.toList());
            // 校验小时范围有没有交叉
            if (list.stream().anyMatch(x -> {
                return x.getStartTime().before(endTime) && x.getEndTime().after(startTime);
            })) {
                log.error("选择的时间段范围内有其他请假");
                throw new HandleException("选择的时间段范围内有其他申请中或已通过的请假");
            }
        } else {
            List<AttendanceLeaveApproveVo> list = userLeaveByTimeSlot.stream().filter(x -> x.getUnit().equals(BalanceEnum.BALANCE_UNIT_BT.getCode())).collect(Collectors.toList());
            // 半天校验
            long startNowNum = Long.parseLong(DateDetail.getDate2Str(startTime, DateDetail.DF6) + startTimeType);
            long endNowNum = Long.parseLong(DateDetail.getDate2Str(endTime, DateDetail.DF6) + endTimeType);
            if (list.stream().anyMatch(x -> {
                long startNum = Long.parseLong(DateDetail.getDate2Str(x.getStartTime(), DateDetail.DF6) + x.getStartTimeType());
                long endNum = Long.parseLong(DateDetail.getDate2Str(x.getEndTime(), DateDetail.DF6) + x.getEndTimeType());
                return startNum <= endNowNum && endNum >= startNowNum;
            })) {
                log.error("选择的时间段范围内有其他请假");
                throw new HandleException("选择的时间段范围内有其他申请中或已通过的请假");
            }
        }
    }

    /**
     * 请假提交校验
     *
     * @param body 提交对象
     * @throws HandleException 异常
     */
    private void submitCheckLeave(String body) throws HandleException, ApproveException, ParseException {
        log.error("请假提交校验: {}", body);
        LeaveQueryDto leaveQueryDto = JsonUtil.getJsonToBean(body, LeaveQueryDto.class);
        leaveQueryDto.setApplyId(null);
        leaveQueryDto.setLeaveTypeId(leaveQueryDto.getTypeId());
        // 2.0已重构
        getResidueBalance(leaveQueryDto);
    }

    /**
     * 获取考勤组对应权限的审批管理员
     *
     * @param type            类型
     * @param startTime       开始时间
     * @param endTime         结束时间
     * @param clockInResultId 打卡Id
     * @param selfGroupId     借调考勤组
     * @throws HandleException 异常
     */
    @Override
    public AttendanceApprovalAdminVo getGroupAdminInfo(Integer type, Date startTime, Date endTime, String clockInResultId, String selfGroupId) throws HandleException {
        String loginUserId = UserProvider.getLoginUserId();
        if (ApprovalSettingTypeEnum.ROUTINE.getCode().equals(type)) {
            /* 补卡审批*/
            String curGroupId = groupIdByClockInResultId(clockInResultId);
            return getApproveUser(curGroupId, type);
        }
        if (ApprovalSettingTypeEnum.ACTION_RESULT.getCode().equals(type)) {
            /* 调整出勤结果*/
            String curGroupId = groupIdByClockInResultId(clockInResultId);
            return getApproveUser(curGroupId, type);
        }
        if (ApprovalSettingTypeEnum.OUT.getCode().equals(type)) {
            String curGroupId = getGroupIdByUserId(loginUserId);
            return getApproveUser(curGroupId, type);
        }
        if (ApprovalSettingTypeEnum.LEAVE.getCode().equals(type)) {
            /* 请假审批*/
            Set<String> groupIds = getGroupIdsNew(loginUserId, startTime, endTime);
            if (groupIds.size() > 1) {
                throw new HandleException("请假审批不可以跨两个考勤组");
            }
            if (CollectionUtil.isEmpty(groupIds)) {
                throw new HandleException("未找到你所属考勤组!");
            }
            String nextGroupId = groupIds.iterator().next();
            return getApproveUser(nextGroupId, type);
        }
        if (ApprovalSettingTypeEnum.OVERTIME.getCode().equals(type)) {
            /* 加班审批*/
            Set<String> groupIds = getGroupIdsNew(loginUserId, startTime, endTime);
            if (groupIds.size() > 1) {
                throw new HandleException("请假审批不可以跨两个考勤组");
            }
            if (CollectionUtil.isEmpty(groupIds)) {
                throw new HandleException("未找到你所属考勤组!");
            }
            String nextGroupId = groupIds.iterator().next();
            return getApproveUser(nextGroupId, type);
        }
        if (ApprovalSettingTypeEnum.GO_OUT.getCode().equals(type)) {
            /* 外出审批*/
            String groupId = getGroupIdByUserId(loginUserId);
            if (null == groupId) {
                throw new HandleException("没找到考勤组");
            }
            return getApproveUser(groupId, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode());
        }
        if (ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode().equals(type)) {
            /* 出差审批*/
            String groupId = getGroupIdByUserId(loginUserId);
            if (null == groupId) {
                throw new HandleException("没找到考勤组");
            }
            return getApproveUser(groupId, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode());
        }
        /* 借调审批*/
        AttendanceApprovalAdminVo approveUser = new AttendanceApprovalAdminVo();
        List<String> userIds = getSecondAdmins(selfGroupId);
        List<AttendanceUserVo> userVoList = toUserVoInfo(userIds);
        approveUser.setIsApproval(true);
        approveUser.setUserList(userVoList);
        return approveUser;
    }

    @Override
    public List<UserBalanceVo> getBalanceDetailS(String groupId, String month) {
        Date start = DateDetail.getMonthBeginDate(month);
        Date end = DateDetail.getMonthEndDate(month);
        List<AttendanceGroupUser> attendanceGroupUserVos = attendanceGroupUserService.queryByUsersAndGroup(start, end, null, CollUtil.newArrayList(groupId));
        if (null != attendanceGroupUserVos && !attendanceGroupUserVos.isEmpty()) {
            attendanceGroupUserVos = attendanceGroupUserVos.stream().filter(v -> 1 != v.getDeleteMark()).collect(Collectors.toList());
            List<String> userIds = attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList());
            if (userIds.isEmpty()) {
                return new ArrayList<>();
            }
            return getUsersBalance(userIds);
        }
        return new ArrayList<>();
    }

    @Override
    public List<UserBalanceVo> getUsersBalance(List<String> userIds) {
        List<UserBalanceVo> list = new ArrayList<>();
        // userIds 去重
        userIds = userIds.stream().distinct().collect(Collectors.toList());
        List<UserBalanceVo> userBalanceVo = attendanceBalanceRecordMapper.getBalanceByUserIds(userIds);

        ActionResult<List<UserBoundVO>> userList = v2UserApi.getAllUserInfoBatch(userIds, null);
        Map<String, UserBoundVO> map = new HashMap<>();
        if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) {
            map = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity()));
        }

        for (Map.Entry<String, UserBoundVO> entry : map.entrySet()) {
            //赋值用户名称
            UserBalanceVo userBalanceVo1 = new UserBalanceVo();
            userBalanceVo1.setUserId(entry.getKey());
            userBalanceVo1.setUserName(entry.getValue().getUserName());
            list.add(userBalanceVo1);
        }
        Map<String, UserBalanceVo> listMap = list.stream().collect(Collectors.toMap(UserBalanceVo::getUserId, Function.identity()));
        userBalanceVo.forEach(v -> {
            UserBalanceVo balanceVo = listMap.get(v.getUserId());
            if (null != balanceVo) {
                // 赋值余额
                balanceVo.setBalance(v.getBalance());
                balanceVo.setPaidBalance(v.getPaidBalance());
                balanceVo.setUnpaidBalance(v.getUnpaidBalance());
                balanceVo.setAdventBalance(v.getAdventBalance());
            }
        });
        return list;
    }


    @Override
    public AttendanceApprovalAdminVo getGoOutApproval() {
        String loginUserId = UserProvider.getLoginUserId();
        String groupId = getGroupIdByUserId(loginUserId);
        return getApproveUser(groupId, ApprovalSettingTypeEnum.GO_OUT.getCode());
    }

    @Override
    public AttendanceApprovalAdminVo getBusinessTripApproval() {
        String loginUserId = UserProvider.getLoginUserId();
        String groupId = getGroupIdByUserId(loginUserId);
        return getApproveUser(groupId, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode());
    }

    @Override
    public Boolean storageRest(String tenantId) {
        // 每月定时生成用户存休记录
        // 获取上个月的年月
        SimpleDateFormat Y_M = new SimpleDateFormat("yyyy-MM");
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.MONTH, -1);
        // 获取上个月的年月
        String lastMonthDate = Y_M.format(calendar.getTime());
        // 预防重复调用产生脏数据
        attendanceBalanceRecordMapper.deleteByYearMonth(lastMonthDate);
        // 找到现在还有劵的用户  2.0 改为查询存休余额
        List<AttendanceStorageRest> list = attendanceBalanceRecordMapper.getStraightBalanceList();
        if (null != list && !list.isEmpty()) {
            list.forEach(v -> {
                v.setId(FtbUtil.getId());
                v.setYearMonth(lastMonthDate);
            });
            // 批量保存存休信息
            storageRestMapper.saveBatch(list);
        }
        return true;
    }

    @Override
    public void sendIm(WorkflowImQueryDto workflowImQueryDto) {
        // 查询下一节点审批人
        ApproverByTaskIdAndNodeIdVo data = flowTaskService.getApproveInfo(workflowImQueryDto.getTaskId(), workflowImQueryDto.getTaskNodeId(), workflowImQueryDto.getTenantId(), null);
        if (null == data) {
            return;
        }
        AttendanceApproveImVo jsonToBean = JsonUtil.getJsonToBean(data, AttendanceApproveImVo.class);
        jsonToBean.setId(workflowImQueryDto.getTaskId());
        try {
            switch (workflowImQueryDto.getType()) {
                // 审批类型 0.借调  1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批   6.外出 7.出差
                case "0":
                    secondedSendIm(workflowImQueryDto, data.getUserOperators(), jsonToBean);
                    break;
                case "1":
                    // 审批发送给下一节点
                    sendMessageToIm(data.getUserOperators(), jsonToBean, ConstantUtil.APPROVE_NOTICE_REPAIR);
                    break;
                case "2":
                    sendMessageToIm(data.getUserOperators(), jsonToBean, ConstantUtil.APPROVE_NOTICE_CHANGE);
                    break;
                case "3":
                    sendMessageToIm(data.getUserOperators(), jsonToBean, ConstantUtil.APPROVE_NOTICE_OUTSIDE);
                    break;
                case "4":
                    leaveSendIm(workflowImQueryDto, data.getUserOperators(), jsonToBean);
                    break;
                case "5":
                    workSendIm(workflowImQueryDto, data.getUserOperators(), jsonToBean);
                    break;
                case "6":
                    goOutSendIm(workflowImQueryDto, data.getUserOperators(), jsonToBean);
                    break;
                case "7":
                    businessSendIm(workflowImQueryDto, data.getUserOperators(), jsonToBean);
                    break;
                default:
                    throw new HandleException("请传入合法的Type");
            }
        } catch (Exception e) {
            e.printStackTrace();
            ActionResult.fail(e.getMessage());
            return;
        }
        ActionResult.success();
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public ActionResult<Void> createBusinessTrip(AttendanceBusinessTripApproveOaDto approveOaDto) {

        ActionResult<Void> result = new ActionResult<>();
        result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode());
        result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg());
        // 处理收到的数组用户
        // 字符串专数组
        String userId = JSONUtil.toList(approveOaDto.getUserId(), String.class).get(0);
        if (StringUtils.isEmpty(userId)) {
            result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
            result.setMsg("未找到以用户");
            return result;
        }
        approveOaDto.setUserId(userId);
        // 计算时间差  出差只有天
        // 计算两个时间之间的日期差
        approveOaDto.setDayNum(new BigDecimal(FtbUtil.dateDiff(approveOaDto.getStartTime(), approveOaDto.getEndTime()) + 1));
        log.error("FoeOa处理后的出差参数................: {}", approveOaDto);
        //  校验出差开始与结束时间与已通过的借调时间重叠
        checkGroupClockForOa(approveOaDto.getUserId(), approveOaDto.getStartTime(), approveOaDto.getEndTime(), result);
        if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
            return result;
        }
        // 出差开始与结束时间与已通过的外出时间重叠 考勤1.8.3版本新增审核中判断
        checkGoOutForOa(approveOaDto.getUserId(), approveOaDto.getStartTime(), approveOaDto.getEndTime(), result, approveOaDto.getTaskId());
        if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
            return result;
        }
        // 出差开始与结束时间与已通过的出差时间重叠 考勤1.8.3版本新增审核中判断
        checkBusinessTripForOa(approveOaDto.getUserId(), approveOaDto.getStartTime(), approveOaDto.getEndTime(), result, approveOaDto.getTaskId());
        if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
            return result;
        }
        try {
            List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(approveOaDto.getUserId(), approveOaDto.getStartTime(), approveOaDto.getEndTime(), AttendanceTypeEnum.BUSINESS_TRIP));
            for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
                if (1 == dailyRuleResultVo.getType()) {
                    // 有异常直接报错
                    result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
                    result.setMsg(dailyRuleResultVo.getFailMsg());
                    break;
                }
            }
            if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
                return result;
            }
        } catch (HandleException e) {
            throw new RuntimeException(e);
        }
        approveOaDto.setId(approveOaDto.getTaskId());
        businessTripApproveService.saveForOa(approveOaDto);
        return result;
    }

    @Override
    public ActionResult<Void> createGoOutForOa(GoOutApproveForOaDto dto) {
        ActionResult<Void> result = new ActionResult<>();
        result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode());
        result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg());
        // 处理时长
        String[] time = dto.getTime();
        Date start = new Date(Long.parseLong(time[0]));
        Date end = new Date(Long.parseLong(time[1]));
        if (dto.getUnit().equals(LeaveUnitEnum.DAY.getCode())) {
            int i = FtbUtil.dateDiff(start, end) + 1;
            dto.setDayNum(new BigDecimal(i));
        } else {
            dto.setDayNum(FtbUtil.getTimeDifference(start, end));
        }
        log.error("createGoOutForOa处理完后的参数................: {}", dto);
        // 校验是否和借调冲突
//        GoOutVo vo = attendanceLeaveApproveMapper.getGoOut(taskId);
        String userId = JSONUtil.toList(dto.getUserId(), String.class).get(0);
        if (StringUtils.isEmpty(userId)) {
            result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
            result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getMsg());
            return result;
        }
        dto.setUserId(userId);

        //  校验出差开始与结束时间与已通过的借调时间重叠
        checkGroupClockForOa(dto.getUserId(), dto.getStartTime(), dto.getEndTime(), result);
        if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
            return result;
        }
        // 出差开始与结束时间与已通过的外出时间重叠
        checkGoOutForOa(dto.getUserId(), dto.getStartTime(), dto.getEndTime(), result, dto.getTaskId());
        if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
            return result;
        }
        // 出差开始与结束时间与已通过的出差时间重叠
        checkBusinessTripForOa(dto.getUserId(), dto.getStartTime(), dto.getEndTime(), result, dto.getTaskId());
        if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
            return result;
        }
        List<DailyRuleResultVo> dailyRuleResultVos = null;
        try {
            dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(dto.getUserId(), dto.getStartTime(), dto.getEndTime(), AttendanceTypeEnum.STEP_OUT));
        } catch (HandleException e) {
            result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
            result.setMsg("内部异常" + e.getMessage());
            return result;
        }
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (1 == dailyRuleResultVo.getType()) {
                // 有异常直接报错
                result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
                result.setMsg(dailyRuleResultVo.getFailMsg());
                break;
            }
        }
        if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
            return result;
        }
        dto.setId(dto.getTaskId());
        goOutApproveService.saveForOa(dto);
        return result;
    }

    @Override
    public ActionResult<Void> getDurationForOa(DurationForOaDto dto) {
        String[] time = dto.getTime();
        Date start = new Date(Long.parseLong(time[0]));
        Date end = new Date(Long.parseLong(time[1]));
        if (dto.getUnit().equals(LeaveUnitEnum.DAY.getCode())) {
            int i = FtbUtil.dateDiff(start, end) + 1;
            return ActionResult.success(String.valueOf(i));
        }
        BigDecimal timeDifference = FtbUtil.getTimeDifference(start, end);
        return ActionResult.success(timeDifference.toString());
    }

    @Override
    public ActionResult<Void> createLeaveForOa(LeaveApproveForOaDto dto) {
        ActionResult<Void> result = new ActionResult<>();
        result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode());
        result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg());
        LeaveQueryDto leaveQueryDto = new LeaveQueryDto();
        leaveQueryDto.setLeaveTypeId(dto.getTypeId());
        leaveQueryDto.setStartTime(String.valueOf(dto.getStartTime().getTime()));
        leaveQueryDto.setEndTime(String.valueOf(dto.getEndTime().getTime()));
        leaveQueryDto.setBalanceStatus(dto.getBalanceStatus());
        leaveQueryDto.setPaid(dto.getPaid());
        leaveQueryDto.setNoPaid(dto.getNoPaid());
        leaveQueryDto.setUnit(dto.getUnit());
        leaveQueryDto.setTypeId(dto.getTypeId());
        leaveQueryDto.setStartTimeType(dto.getStartTimeType());
        leaveQueryDto.setEndTimeType(dto.getEndTimeType());
        try {
            log.error("createLeaveForOa:{}", leaveQueryDto);
            // 校验请假数据  2.0已重构
            LeaveConsumptionDetailVo residueBalance = getResidueBalance(leaveQueryDto);
            dto.setBalanceJsonNew(JSONUtil.toJsonStr(residueBalance));
        } catch (Exception e) {
            result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode());
            result.setMsg(e.getMessage());
            return result;
        }
        dto.setId(dto.getTaskId());
        dto.setStartTimeHit(leaveQueryDto.getStartTimeHit());
        dto.setEndTimeHit(leaveQueryDto.getEndTimeHit());

        leaveApproveService.saveForOa(dto);
        return result;
    }

    @Override
    public ActionResult getUserGroupByTime(GroupOaDto groupOaDto) {
        ActionResult<Void> result = new ActionResult<>();
        result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode());
        result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg());
        List<GroupMiniVo> list = new ArrayList<>();
        // 获取用户所选时间段所在考勤组
        List<String> userList = new ArrayList<>();
        userList.add(groupOaDto.getUserIdStr());
        //查询当前时间所属考勤组
        //查询请假时间内所有考勤组
        List<AttendanceGroupUser> attendanceGroupUsers = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(groupOaDto.getStartTime(), groupOaDto.getEndTime() , userList, null);
        if (attendanceGroupUsers.isEmpty()) {
            return ActionResult.fail("当前用户暂无考勤组");
        }
        //如果考勤组数量大于1，判断是否包含当前时间所属考勤组，包含则取当前所属考勤组，不包含直接报错
        Assert.isFalse(attendanceGroupUsers.stream().map(AttendanceGroupUser::getGroupId).distinct().count() > 1, "当前申请时段存在多个考勤组");
        if (StringUtils.isEmpty(attendanceGroupUsers.get(0).getGroupId())) {
            return ActionResult.fail("该用户暂无考勤组");
        }
        // 通过考勤组id获取考勤组信息 当只有一个考勤组的时候才返回数据
        AttendanceGroup groupDetail = attendanceGroupMapper.getGroupDetail(attendanceGroupUsers.get(0).getGroupId());
        GroupMiniVo groupMiniVo = new GroupMiniVo();
        groupMiniVo.setGroupId(groupDetail.getId());
        OrganizeGeneralDetailVO infoById = organizeApi.organizeInfoById(null, groupDetail.getOrgId()).getData();
        Assert.isFalse(Objects.isNull(infoById), "组织不存在");
        groupMiniVo.setGroupName(infoById.getName());
        list.add(groupMiniVo);
        return ActionResult.success(list);
    }

    @Override
    public ActionResult getNowGroup() {
        List<GroupMiniVo> list = new ArrayList<>();
        // 查询用户现在所在考勤组
        List<AttendanceGroupUser> attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(new Date(), new Date(), Collections.singletonList(UserProvider.getLoginUserId()), null);
        if (null == attendanceGroupUsers || attendanceGroupUsers.isEmpty()) {
            return ActionResult.success(list);
        }
        AttendanceGroupUser groupUser = attendanceGroupUsers.get(0);
        if (StringUtils.isEmpty(groupUser.getGroupId())) {
            return ActionResult.fail("该用户暂无考勤组");
        }
        // 通过考勤组id获取考勤组信息 当只有一个考勤组的时候才返回数据
        AttendanceGroup groupDetail = attendanceGroupMapper.getGroupDetail(groupUser.getGroupId());
        GroupMiniVo groupMiniVo = new GroupMiniVo();
        groupMiniVo.setGroupId(groupDetail.getId());
        OrganizeGeneralDetailVO infoById = organizeApi.organizeInfoById(null, groupDetail.getOrgId()).getData();
        Assert.isFalse(Objects.isNull(infoById), "组织不存在");
        groupMiniVo.setGroupName(infoById.getName());
        list.add(groupMiniVo);
        return ActionResult.success(list);
    }

    @Override
    public AttendanceToThousandsFacesVo attendanceToThousandsFaces() {
        AttendanceToThousandsFacesVo attendanceToThousandsFacesVo = new AttendanceToThousandsFacesVo();
        int totalUser = 0;
        int schedulingUser = 0;
        String userId = userProvider.get().getUserId();
        // 统计打卡人数
        // 打卡结果表单日有一次正常的打卡记录，记为1天（迟到、早退、正常）跨天班次一次排班天算1天
        Integer clockInDayNum = attendanceClockInResultMapper.getClockInDayNum(userId);
        attendanceToThousandsFacesVo.setClockInDayNum(null == clockInDayNum ? 0 : clockInDayNum);
        // 统计排班人数
        // 今日所有具有管理权限的考勤组已排班人数（含今日排休人数，不含今日固定排班）/所有具有权限的考勤组总人数（含今日排休人数，不含今日固定排班）
        // 获取进入具有本组排班及子考勤组的管理权限的考勤组 考勤组ids集合
        List<String> noFixedGroupIds = attendanceGroupService.getGroupIdsByManagePermission(userId, true);
        if (null != noFixedGroupIds && !noFixedGroupIds.isEmpty()) {
            // 通过考勤组ids集合获取非固定排班总人数(包含当天借调过来的人数)
            List<String> noFixedGroupUserIds = attendanceGroupUserService.getGroupUserIds(noFixedGroupIds);
            if (null != noFixedGroupUserIds && !noFixedGroupUserIds.isEmpty()) {
                totalUser = noFixedGroupUserIds.size();
                // 考勤组ids集合获取已排班的人数
                Integer groupUserNum = attendanceDailyRuleMapper.getGroupUserNum(noFixedGroupUserIds, noFixedGroupIds);
                schedulingUser = null == groupUserNum ? 0 : groupUserNum;
            }
        }
        attendanceToThousandsFacesVo.setSchedulingProportion(schedulingUser + "/" + totalUser);
        Date dayBegin = DateUtil.getDayBegin();
        Date date = DateUtil.dateAddDays(dayBegin, 1);
        List<YesterdayRuleVo> yesterdayRule = attendanceDailyRuleMapper.getYesterdayRule(UserProvider.getLoginUserId(), dayBegin, date);
        attendanceToThousandsFacesVo.setYesterdayRuleList(yesterdayRule);
        return attendanceToThousandsFacesVo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void leaveCompensation(String applyId, String userId,String day, BigDecimal balance) throws ApproveException {

//        // 定义常量
//        final int MAX_RETRIES = 30;
//        final long LOCK_WAIT_TIME = 10; // 秒
//        final long LOCK_LEASE_TIME = 60; // 秒
//        final long RETRY_INTERVAL = 1000; // 毫秒
//        String executingDayKey = "attendance:leave:compensation:executing:" + applyId;
//        String lockKey = "attendance:leave:compensation:lock:" + applyId;
//        RLock lock = redisson.getLock(lockKey);
//        // 使用分布式锁确保同一申请的同一天只被处理一次
//        int retryCount = 0;
//        while (retryCount < MAX_RETRIES) {
//            try {
//                // 尝试获取分布式锁
//                if (lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS)) {
//                    try {
//                        // 在获取锁后再次检查当前正在执行的day
//                        String currentExecutingDay = (String) redisTemplate.opsForValue().get(executingDayKey);
//                        if (currentExecutingDay != null && !currentExecutingDay.equals(day)) {
//                            // 如果正在执行的day和传入的day不同，等待之前的执行完成
//                            log.info("当前申请正在处理day: {}, 传入day: {}, 需要等待上一个执行完成", currentExecutingDay, day);
//                            // 继续循环重试
//                        } else if (currentExecutingDay != null && currentExecutingDay.equals(day)) {
//                            // 如果正在执行的day和传入的day相同，打印日志并退出
//                            log.warn("当前申请正在处理中，请勿重复操作 - applyId: {}, userId: {}, day: {}, balance: {}",
//                                    applyId, userId, day, balance);
//                            return;
//                        } else {
//                            // 没有正在执行的记录，或者当前执行的day与传入day相同但需要继续处理
//                            // 设置当前执行的day标记
//                            redisTemplate.opsForValue().set(executingDayKey, day, LOCK_LEASE_TIME, TimeUnit.SECONDS);
//                            break; // 可以继续执行业务逻辑
//                        }
//                    } finally {
//                        // 释放锁
//                        if (lock.isHeldByCurrentThread()) {
//                            lock.unlock();
//                        }
//                    }
//                }
//            } catch (InterruptedException e) {
//                Thread.currentThread().interrupt();
//                throw new ApproveException("线程被中断，操作失败");
//            }
//
//            try {
//                Thread.sleep(RETRY_INTERVAL); // 等待后重试
//                retryCount++;
//            } catch (InterruptedException e) {
//                Thread.currentThread().interrupt();
//                throw new ApproveException("线程被中断，操作失败");
//            }
//        }
//
//        if (retryCount >= MAX_RETRIES) {
//            throw new ApproveException("等待上一个申请执行超时，请稍后重试");
//        }
//
//        // 执行业务逻辑
//        try {
//            AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(applyId);
//            // 特殊情况-请假类型单位使用创建审批时的历史纪录，其他配置获取实时更新的请假类型信息
//            AttendanceLeaveRulesVo userLeaveDetail = attendanceLeaveTypeService.getUserLeaveDetail(attendanceLeaveApproveVo.getTypeId(), attendanceLeaveApproveVo.getUserId());
//            LeaveConsumptionDetailVo leaveConsumptionDetailVo = attendanceUserBalanceRecordService.leaveConsumption(attendanceLeaveApproveVo.getUserId(), balance, userLeaveDetail, attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime(), attendanceLeaveApproveVo.getId());
//
//            // 处理完成后清理执行标记
//            redisTemplate.delete(executingDayKey);
//        } catch (Exception e) {
//            // 发生异常时也要清理执行标记
//            redisTemplate.delete(executingDayKey);
//            if (e instanceof ApproveException) {
//                throw e;
//            } else {
//                throw new ApproveException("请假补偿处理失败: " + e.getMessage());
//            }
//        } finally {
//            // 确保锁被释放
//            if (lock.isHeldByCurrentThread()) {
//                lock.unlock();
//            }
//        }

    }

    private void sendMessageToIm(List<UserOperatorVo> operatorList, AttendanceApproveImVo approveIm, String key) {

        key += approveIm.getId();
        if (redisUtil.exists(key)) {
            Object value = redisUtil.getString(key);
            ApproveImVo approveImVo = JsonUtil.getJsonToBean(value, ApproveImVo.class);
            sendIm(operatorList, approveIm, approveImVo.getApproveBaseImVo(), approveImVo);
        }
    }

    private void secondedSendIm(WorkflowImQueryDto workflowImQueryDto, List<UserOperatorVo> userOperators, AttendanceApproveImVo jsonToBean) {
        //张三的【借调】审批
        //借调开始时间：2024年5月16日 16:23
        //借调结束时间：2024年5月17日 09:00
        //借调考勤组：小露测试考勤组
        //被借调考勤组：小露验收考勤组
        AttendanceSelfApproveVo selfApprove = attendanceLeaveApproveMapper.getSelfApprove(workflowImQueryDto.getTaskId());
        List<PartUserInfoVo> allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(selfApprove.getCreatorUserId()), workflowImQueryDto.getTenantId());
        PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null);
        // 组装消息
        ApproveImVo approveImVo = new ApproveImVo();
        approveImVo.setTenantId(workflowImQueryDto.getTenantId());
        approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE);
        ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo();
        approveBaseImVo.setType(ApprovalSettingTypeEnum.SECONDED.getCode());
        approveBaseImVo.setStartTime(selfApprove.getStartTime());
        approveBaseImVo.setEndTime(selfApprove.getEndTime());
        approveBaseImVo.setGroupName(selfApprove.getGroupName());
        approveBaseImVo.setSecondedGroupName(selfApprove.getSelfGroupName());
        StringBuilder stringBuffer = new StringBuilder();
        if (!Objects.isNull(partUserInfoVo)) {
            stringBuffer.append(partUserInfoVo.getRealName());
        }
        approveBaseImVo.setTitle(stringBuffer.append("的【借调】审批").toString());
        sendIm(userOperators, jsonToBean, approveBaseImVo, approveImVo);
    }

    private void businessSendIm(WorkflowImQueryDto workflowImQueryDto, List<UserOperatorVo> userOperators, AttendanceApproveImVo jsonToBean) {
        // 张三的【出差】审批
        // 出发地：地点名称地点名称地点名称地点名称
        // 目的地：地点名称地点名称地点名称地点名称
        BusinessTripVo vo = attendanceLeaveApproveMapper.getBusinessTrip(workflowImQueryDto.getTaskId());
        List<PartUserInfoVo> allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(vo.getUserId()), workflowImQueryDto.getTenantId());
        PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null);
        // 组装消息
        ApproveImVo approveImVo = new ApproveImVo();
        approveImVo.setTenantId(workflowImQueryDto.getTenantId());
        approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE);
        ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo();
        approveBaseImVo.setType(ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode());
        approveBaseImVo.setDestination(vo.getDestination());
        approveBaseImVo.setDeparture(vo.getDeparture());
        StringBuilder stringBuffer = new StringBuilder();
        if (!Objects.isNull(partUserInfoVo)) {
            stringBuffer.append(partUserInfoVo.getRealName());
        }
        approveBaseImVo.setTitle(stringBuffer.append("的【出差】审批").toString());
        sendIm(userOperators, jsonToBean, approveBaseImVo, approveImVo);
    }

    private void sendIm(List<UserOperatorVo> userOperators, AttendanceApproveImVo jsonToBean, ApproveBaseImVo approveBaseImVo, ApproveImVo approveImVo) {
        userOperators.forEach(v -> {
            jsonToBean.setTaskId(v.getTaskOperatorId());
            // URL 拼接
//            String encode = Base64Util.encode(JSONUtil.toJsonStr(jsonToBean));
            String encode = FtbUtil.encodeJson(jsonToBean);
            String url = AttendanceConstant.APPROVE_URL + encode;
            approveBaseImVo.setUrl(url);
            approveImVo.setUserIds(CollUtil.newArrayList(v.getUserId()));
            approveImVo.setApproveBaseImVo(approveBaseImVo);
            //  轮询用户发送消息 因每一个用户跳转的信息都不同
            attendanceNoticeHandler.send(approveImVo);
        });
    }

    private void goOutSendIm(WorkflowImQueryDto workflowImQueryDto, List<UserOperatorVo> userOperators, AttendanceApproveImVo jsonToBean) {
        //张三的【外出】审批
        //开始时间：2024年5月16日
        //结束时间：2024年5月17日
        //时长：2天
        GoOutVo vo = attendanceLeaveApproveMapper.getGoOut(workflowImQueryDto.getTaskId());
        List<PartUserInfoVo> allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(vo.getUserId()), workflowImQueryDto.getTenantId());
        PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null);
        // 组装消息
        ApproveImVo approveImVo = new ApproveImVo();
        approveImVo.setTenantId(workflowImQueryDto.getTenantId());
        approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE);

        ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo();
        approveBaseImVo.setType(ApprovalSettingTypeEnum.GO_OUT.getCode());
        approveBaseImVo.setStartTime(vo.getStartTime());
        approveBaseImVo.setEndTime(vo.getEndTime());
        approveBaseImVo.setDuration(vo.getDayNum() + "天");

        StringBuilder stringBuffer = new StringBuilder();
        if (!Objects.isNull(partUserInfoVo)) {
            stringBuffer.append(partUserInfoVo.getRealName());
        }
        approveBaseImVo.setTitle(stringBuffer.append("的【外出】审批").toString());

        sendIm(userOperators, jsonToBean, approveBaseImVo, approveImVo);
    }

    private void workSendIm(WorkflowImQueryDto workflowImQueryDto, List<UserOperatorVo> userOperators, AttendanceApproveImVo jsonToBean) {
        //张三的【加班】审批
        //选择日期：2024年5月16日
        //开始时间：22:00
        //结束时间：次日08:00
        AttendanceWorkOverTimeVo workOverTime = attendanceLeaveApproveMapper.getWorkOverTime(workflowImQueryDto.getTaskId());
        List<PartUserInfoVo> allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(workOverTime.getUserId()), workflowImQueryDto.getTenantId());
        PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null);
        // 组装消息
        ApproveImVo approveImVo = new ApproveImVo();
        approveImVo.setTenantId(workflowImQueryDto.getTenantId());
        approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE);

        ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo();
        approveBaseImVo.setType(ApprovalSettingTypeEnum.OVERTIME.getCode());
        approveBaseImVo.setSelectTime(workOverTime.getWorkDay());
        approveBaseImVo.setStartTime(workOverTime.getStartTime());
        approveBaseImVo.setEndTime(workOverTime.getEndTime());

        StringBuilder stringBuffer = new StringBuilder();
        if (!Objects.isNull(partUserInfoVo)) {
            stringBuffer.append(partUserInfoVo.getRealName());
        }
        approveBaseImVo.setTitle(stringBuffer.append("的【加班】审批").toString());

        sendIm(userOperators, jsonToBean, approveBaseImVo, approveImVo);
    }


    private void leaveSendIm(WorkflowImQueryDto workflowImQueryDto, List<UserOperatorVo> userOperators, AttendanceApproveImVo jsonToBean) throws HandleException {
        AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(workflowImQueryDto.getTaskId());
        List<PartUserInfoVo> allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(attendanceLeaveApproveVo.getUserId()), workflowImQueryDto.getTenantId());
        PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null);
        // 组装消息
        ApproveImVo approveImVo = new ApproveImVo();
        approveImVo.setTenantId(workflowImQueryDto.getTenantId());
        approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE);

        ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo();
        approveBaseImVo.setType(ApprovalSettingTypeEnum.LEAVE.getCode());
        approveBaseImVo.setStartTime(attendanceLeaveApproveVo.getStartTime());
        approveBaseImVo.setEndTime(attendanceLeaveApproveVo.getEndTime());
        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(attendanceLeaveApproveVo.getUserId(), attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime(), AttendanceTypeEnum.LEAVE));
        // 实时获取申请时长
        BigDecimal hours = new BigDecimal("0");
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            hours = hours.add(dailyRuleResultVo.getDuration());
        }
        approveBaseImVo.setDuration(hours + "小时");
        // 请假类型
        approveBaseImVo.setLeaveType(attendanceLeaveApproveVo.getType());
        StringBuilder stringBuffer = new StringBuilder();
        if (!Objects.isNull(partUserInfoVo)) {
            stringBuffer.append(partUserInfoVo.getRealName());
        }
        approveBaseImVo.setTitle(stringBuffer.append("的【请假】审批").toString());

        sendIm(userOperators, jsonToBean, approveBaseImVo, approveImVo);
    }

    /**
     * 初始化设置
     */
    private static List<AttendanceLeaveSettingsVo> getList() {
        List<AttendanceLeaveSettingsVo> list = new ArrayList<>();
        AttendanceLeaveSettingsVo sickVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.SICK_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.SICK_LEAVE.getCode());
        list.add(sickVo);
        AttendanceLeaveSettingsVo medicalVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.MEDICAL_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.MEDICAL_LEAVE.getCode());
        list.add(medicalVo);
        AttendanceLeaveSettingsVo worInjuryVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.WORK_INJURY_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.WORK_INJURY_LEAVE.getCode());
        list.add(worInjuryVo);
        AttendanceLeaveSettingsVo annualVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.ANNUAL_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.ANNUAL_LEAVE.getCode());
        list.add(annualVo);
        AttendanceLeaveSettingsVo weddingVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.WEDDING_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.WEDDING_LEAVE.getCode());
        list.add(weddingVo);
        AttendanceLeaveSettingsVo maternityVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.MATERNITY_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.MATERNITY_LEAVE.getCode());
        list.add(maternityVo);
        AttendanceLeaveSettingsVo paternityVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.PATERNITY_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.PATERNITY_LEAVE.getCode());
        list.add(paternityVo);
        AttendanceLeaveSettingsVo funeralVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.FUNERAL_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.FUNERAL_LEAVE.getCode());
        list.add(funeralVo);
        AttendanceLeaveSettingsVo familyVisitVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.FAMILY_VISIT_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.FAMILY_VISIT_LEAVE.getCode());
        list.add(familyVisitVo);
        AttendanceLeaveSettingsVo childCareVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.CHILD_CARE_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.CHILD_CARE_LEAVE.getCode());
        list.add(childCareVo);
        return list;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void goOutApprove(String applyId, Integer status, String tenantId, String userId, String userName) throws ApproveException {
        GoOutVo goOutVo = attendanceLeaveApproveMapper.getGoOut(applyId);
        if (null == goOutVo) {
            log.error("未找到对应的外出审批");
            throw new ApproveException("未找到对应的外出审批");
        }

        if (Objects.equals(BalanceEnum.APPROVE_STATUS_PASS.getCode(), status)) {
            ActionResult<Void> result = new ActionResult<>();
            result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode());
            result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg());
            //  校验出差开始与结束时间与已通过的借调时间重叠
            checkGroupClockForOa(goOutVo.getUserId(), goOutVo.getStartTime(), goOutVo.getEndTime(), result);
            if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
                throw new ApproveException(result.getMsg());
            }
            // 出差开始与结束时间与已通过的外出时间重叠
            checkGoOutForOa(goOutVo.getUserId(), goOutVo.getStartTime(), goOutVo.getEndTime(), result, goOutVo.getId());
            if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
                throw new ApproveException(result.getMsg());
            }
            // 出差开始与结束时间与已通过的出差时间重叠
            checkBusinessTripForOa(goOutVo.getUserId(), goOutVo.getStartTime(), goOutVo.getEndTime(), result, goOutVo.getId());
            if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
                throw new ApproveException(result.getMsg());
            }
            //  通过时生成
            try {
                BigDecimal attendanceRatio = getAttendanceRatio(goOutVo.getUserId(), goOutVo.getStartTime(), goOutVo.getEndTime());
                BigDecimal subtract = goOutVo.getDayNum().multiply(attendanceRatio);

                attendanceDailyRuleService.applyDailyRuleHandle(new ApplyParam(goOutVo.getUserId(), goOutVo.getId(), goOutVo.getStartTime(), goOutVo.getEndTime(), subtract, AttendanceTypeEnum.STEP_OUT, null, null, goOutVo.getUnit(), null));
            } catch (HandleException e) {
                throw new ApproveException(e.getMessage());
            }
        }
        //修改审批状态
        attendanceLeaveApproveMapper.updateGoOutApprove(applyId, status, userId, userName);
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void businessTripApprove(String applyId, Integer status, String tenantId, String userId, String userName) throws ApproveException {
        BusinessTripVo businessTripVo = attendanceLeaveApproveMapper.getBusinessTrip(applyId);
        if (null == businessTripVo) {
            log.error("未找到对应的出差审批");
            throw new ApproveException("未找到对应的出差审批");
        }
        if (Objects.equals(BalanceEnum.APPROVE_STATUS_PASS.getCode(), status)) {
            ActionResult<Void> result = new ActionResult<>();
            result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode());
            result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg());
            //  校验出差开始与结束时间与已通过的借调时间重叠
            checkGroupClockForOa(businessTripVo.getUserId(), businessTripVo.getStartTime(), businessTripVo.getEndTime(), result);
            if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
                throw new ApproveException(result.getMsg());
            }
            // 出差开始与结束时间与已通过的外出时间重叠
            checkGoOutForOa(businessTripVo.getUserId(), businessTripVo.getStartTime(), businessTripVo.getEndTime(), result, businessTripVo.getId());
            if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
                throw new ApproveException(result.getMsg());
            }
            // 出差开始与结束时间与已通过的出差时间重叠
            checkBusinessTripForOa(businessTripVo.getUserId(), businessTripVo.getStartTime(), businessTripVo.getEndTime(), result, businessTripVo.getId());
            if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
                throw new ApproveException(result.getMsg());
            }
            //  通过时生成
            try {
                BigDecimal attendanceRatio = getAttendanceRatio(businessTripVo.getUserId(), businessTripVo.getStartTime(), businessTripVo.getEndTime());
                BigDecimal subtract = businessTripVo.getDayNum().multiply(attendanceRatio);
                attendanceDailyRuleService.applyDailyRuleHandle(new ApplyParam(businessTripVo.getUserId(), businessTripVo.getId(), businessTripVo.getStartTime(), businessTripVo.getEndTime(), subtract, AttendanceTypeEnum.BUSINESS_TRIP));
            } catch (HandleException e) {
                throw new ApproveException(e.getMessage());
            }
        }
        //修改审批状态
        attendanceLeaveApproveMapper.updateBusinessTripApprove(applyId, status, userId, userName);
    }


    private static void setUrlParam(Integer status, StringBuilder stringBuffer, AttendanceApproveImVo jsonToBean) {
        if (Objects.equals(BalanceEnum.APPROVE_STATUS_PASS.getCode(), status)) {
            stringBuffer.append("已通过");
            jsonToBean.setFormType(null);
            jsonToBean.setOpType(0);
            jsonToBean.setStatus(2);
        } else {
            stringBuffer.append("已驳回");
            jsonToBean.setFormType(1);
            jsonToBean.setOpType(-1);
            jsonToBean.setStatus(3);
        }
    }

    /**
     * 获取借调审批管理员
     *
     * @param selfGroupId 被借调考勤组id
     * @return List<String> 用户ids
     */
    private List<String> getSecondAdmins(String selfGroupId) {
        return getSecondedAdminUserId(selfGroupId);
    }

    /**
     * 借调校验
     */
    @Override
    public void checkSeconded(String taskId) throws HandleException {
        AttendanceSelfApproveVo selfApprove = attendanceLeaveApproveMapper.getSelfApprove(taskId);
        String selfGroupId = selfApprove.getSelfGroupId();
        List<String> userIds = attendanceLeaveApproveMapper.getUserList(taskId);
        check(selfApprove.getGroupId(), selfApprove.getSelfGroupId(), userIds);
        if (CollectionUtil.isEmpty(userIds)) {
            throw new HandleException("请选择借调用户");
        }
        // 判断考勤组是否被锁定
        checkSelfGroupLock(selfApprove.getGroupId(), selfApprove.getStartTime());
        checkSelfGroupLock(selfApprove.getSelfGroupId(), selfApprove.getStartTime());
        // 校验借调时间范围内是否被锁定
//        attendanceGroupMapper.getClock(selfApprove.getGroupId(),selfApprove.getStartTime(),selfApprove.getEndTime());
        QueryWrapper<AttendanceGroupUser> groupUserQueryWrapper = new QueryWrapper<>();
        groupUserQueryWrapper.lambda()
                .eq(AttendanceGroupUser::getGroupId, selfGroupId)
                .in(AttendanceGroupUser::getUserId, userIds);
        List<AttendanceGroupUser> groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper);
        if (CollectionUtil.isEmpty(groupUserList)) {
            return;
        }
        Map<String, AttendanceGroupUser> groupUserMap = new HashMap<>();
        for (AttendanceGroupUser groupUser : groupUserList) {
            groupUserMap.put(groupUser.getUserId(), groupUser);
        }
//        List<PartUserInfoVo> infoByIds = userApi.getInfoByIds(userIds);
        ActionResult<List<UserBoundVO>> infoByIds = v2UserApi.getAllUserInfoBatch(userIds, null);
        if (200 == infoByIds.getCode() && CollectionUtil.isNotEmpty(infoByIds.getData())) {
            Map<String, UserBoundVO> userMap = infoByIds.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, v -> v));
            /* 校验用户是否在该时间范围被其它考勤组借调*/
            for (String userId : userIds) {
                AttendanceGroupUser attendanceGroupUser = groupUserMap.get(userId);
                List<SecondmentDateVo> dateVoList = JsonUtil.getJsonToList(attendanceGroupUser.getTimeJson(), SecondmentDateVo.class);
                checkClock(userId, selfApprove.getStartTime(), selfApprove.getEndTime());
                // 2025年08月28日优化之前的校验方式
                checkSecondDateNew2(dateVoList, selfApprove.getStartTime(), selfApprove.getEndTime(), userId, userMap);
            }
            // 校验用户是否在该时间范围被其它考勤组借调（审批中） 2.0 借调划归人事
            FtbSecondMentQueryDTO ftbSecondMentQueryDTO = new FtbSecondMentQueryDTO();
            ftbSecondMentQueryDTO.setUserIds(userIds);
            ftbSecondMentQueryDTO.setStartTime(selfApprove.getStartTime());
            ftbSecondMentQueryDTO.setEndTime(selfApprove.getEndTime());
            List<FtbPersonnelsSecondmentVO> secondmentRecordBath = ftbPersonneApi.getSecondmentRecordBath(ftbSecondMentQueryDTO);
            if (CollectionUtil.isNotEmpty(secondmentRecordBath)) {
                List<String> uIds = Optional.ofNullable(secondmentRecordBath)
                        .orElse(Collections.emptyList())
                        .stream()
                        .filter(Objects::nonNull) // 过滤掉null元素
                        .map(FtbPersonnelsSecondmentVO::getUserId)
                        .filter(Objects::nonNull) // 再次过滤掉null的userId
                        .collect(Collectors.toList());

//            List<String> uIds = attendanceLeaveApproveMapper.checkSelfApprove(userIds, selfApprove.getStartTime(), selfApprove.getEndTime(), taskId);
            if (CollectionUtil.isNotEmpty(uIds)) {
                // 获取用户名
                // 逗号拼接用户 名
                String userNames = userMap.values().stream().filter(v -> uIds.contains(v.getId())).map(UserBoundVO::getName).collect(Collectors.joining(","));
                throw new HandleException("用户:" + userNames + "已处于借调审批中，请勿重复操作！");
            }
            }
        }
    }

    private void checkSelfGroupLock(String groupId, Date startTime) throws HandleException {
        Date groupLockDate = attendanceGroupMapper.getGroupLockDate(groupId);
        if (null != groupLockDate && startTime.before(groupLockDate)) {
            throw new HandleException("考勤组已锁定，过去的考勤记录无法修改！");
        }
    }

    /**
     * 提交时借调校验
     */
    private void submitCheckSeconded(String body) throws HandleException {
        FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class);
        //借调时校验(考勤组是否被删除，用户是否还在该考勤组)
        check(flowTaskVo.getGroupId(), flowTaskVo.getSelfGroupId(), flowTaskVo.getUserIds());
        // 判断考勤组是否被锁定
        checkSelfGroupLock(flowTaskVo.getGroupId(), flowTaskVo.getStartTime());
        checkSelfGroupLock(flowTaskVo.getSelfGroupId(), flowTaskVo.getStartTime());
        String selfGroupId = flowTaskVo.getSelfGroupId();
        List<String> userIds = flowTaskVo.getUserIds();
        if (CollectionUtil.isEmpty(userIds)) {
            throw new HandleException("请选择借调用户");
        }
        QueryWrapper<AttendanceGroupUser> groupUserQueryWrapper = new QueryWrapper<>();
        groupUserQueryWrapper.lambda()
                .eq(AttendanceGroupUser::getGroupId, selfGroupId)
                .in(AttendanceGroupUser::getUserId, userIds);
        List<AttendanceGroupUser> groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper);
        if (CollectionUtil.isEmpty(groupUserList)) {
            return;
        }
        Map<String, AttendanceGroupUser> groupUserMap = new HashMap<>();
        for (AttendanceGroupUser groupUser : groupUserList) {
            groupUserMap.put(groupUser.getUserId(), groupUser);
        }

        /* 校验用户是否在该时间范围被其它考勤组借调*/
        for (String userId : userIds) {
            AttendanceGroupUser attendanceGroupUser = groupUserMap.get(userId);
            List<SecondmentDateVo> dateVoList = JsonUtil.getJsonToList(attendanceGroupUser.getTimeJson(), SecondmentDateVo.class);
            checkClock(userId, flowTaskVo.getStartTime(), flowTaskVo.getEndTime());
            checkSecondDate(dateVoList, flowTaskVo.getStartTime(), flowTaskVo.getEndTime(), userId);

            List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(userId, flowTaskVo.getStartTime(), flowTaskVo.getEndTime(), AttendanceTypeEnum.SECONDMENT));
            for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
                if (1 == dailyRuleResultVo.getType()) {
                    // 有异常直接报错
                    log.error(dailyRuleResultVo.getFailMsg());
                    throw new HandleException(dailyRuleResultVo.getFailMsg());
                }
            }
        }
    }



    @Override
    public void checkWork(String taskId) throws HandleException {
        AttendanceWorkOverTimeVo workOverTime = attendanceLeaveApproveMapper.getWorkOverTime(taskId);
        if (Math.abs(workOverTime.getEndTime().getTime() - workOverTime.getStartTime().getTime()) > 24 * 60 * 60 * 1000) {
            log.error("加班时间不许超过24小时");
            throw new HandleException("加班时间不许超过24小时");
        }
        //校验加班提交期间被排了班
        checkWorkTimeOverlap(attendanceDailyRuleMapper.getUserShift(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()), "选择的加班时间段范围内有其他排班");
        //校验考勤组是否存在跨组（已通过的借调）
        checkMoreGroup(workOverTime);
        // 考勤组是否锁定
        checkClock(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime());
        List<DailyRuleResultVo> dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), AttendanceTypeEnum.WORKOVERTIME));
        for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
            if (1 == dailyRuleResultVo.getType()) {
                // 有异常直接报错
                log.error(dailyRuleResultVo.getFailMsg());
                throw new HandleException(dailyRuleResultVo.getFailMsg());
            }
        }
        ActionResult<List<UserBoundVO>> infoByIds = v2UserApi.getAllUserInfoBatch(Collections.singletonList(workOverTime.getUserId()), null);
        if (200 == infoByIds.getCode() && CollectionUtil.isNotEmpty(infoByIds.getData())) {
            Map<String, UserBoundVO> userMap = infoByIds.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, v -> v));
//             校验用户是否在该时间范围被其它考勤组借调（审批中） 2.0 借调划归人事
//            List<String> uIds = attendanceLeaveApproveMapper.checkSelfApprove(Collections.singletonList(workOverTime.getUserId()), workOverTime.getStartTime(), workOverTime.getEndTime(), taskId);
            FtbSecondMentQueryDTO ftbSecondMentQueryDTO = new FtbSecondMentQueryDTO();
            ftbSecondMentQueryDTO.setUserIds(Collections.singletonList(workOverTime.getUserId()));
            ftbSecondMentQueryDTO.setStartTime(workOverTime.getStartTime());
            ftbSecondMentQueryDTO.setEndTime(workOverTime.getEndTime());
            List<FtbPersonnelsSecondmentVO> secondmentRecordBath = ftbPersonneApi.getSecondmentRecordBath(ftbSecondMentQueryDTO);
            if (CollectionUtil.isNotEmpty(secondmentRecordBath)) {
                List<String> uIds = Optional.ofNullable(secondmentRecordBath)
                        .orElse(Collections.emptyList())
                        .stream()
                        .filter(Objects::nonNull) // 过滤掉null元素
                        .map(FtbPersonnelsSecondmentVO::getUserId)
                        .filter(Objects::nonNull) // 再次过滤掉null的userId
                        .collect(Collectors.toList());
                if (CollectionUtil.isNotEmpty(uIds)) {
                    // 获取用户名
                    // 逗号拼接用户 名
                    String userNames = userMap.values().stream().filter(v -> uIds.contains(v.getId())).map(UserBoundVO::getName).collect(Collectors.joining(","));
                    throw new HandleException("用户:" + userNames + "已处于借调审批中，请勿重复操作！");
                }
            }
        }
    }

    private void checkLeave(String taskId) throws HandleException, ParseException, ApproveException {
        AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(taskId);
        if (null != attendanceLeaveApproveVo) {
            LeaveQueryDto leaveQueryDto = new LeaveQueryDto();
            leaveQueryDto.setLeaveTypeId(attendanceLeaveApproveVo.getTypeId());
            leaveQueryDto.setStartTime(String.valueOf(attendanceLeaveApproveVo.getStartTime().getTime()));
            leaveQueryDto.setEndTime(String.valueOf(attendanceLeaveApproveVo.getEndTime().getTime()));
            leaveQueryDto.setBalanceStatus(attendanceLeaveApproveVo.getBalanceStatus());
            leaveQueryDto.setPaid(attendanceLeaveApproveVo.getPaid());
            leaveQueryDto.setNoPaid(attendanceLeaveApproveVo.getNoPaid());
            leaveQueryDto.setApplyId(attendanceLeaveApproveVo.getId());
            leaveQueryDto.setUnit(attendanceLeaveApproveVo.getUnit());
            leaveQueryDto.setTypeId(attendanceLeaveApproveVo.getTypeId());
            leaveQueryDto.setStartTimeType(attendanceLeaveApproveVo.getStartTimeType());
            leaveQueryDto.setEndTimeType(attendanceLeaveApproveVo.getEndTimeType());
            // 2.0已重构
            getResidueBalance(leaveQueryDto);
        }

    }


    /**
     * 设置使用劵规则
     *
     * @param balanceStatus 是否使用余额抵扣 0 否 1是
     * @param paid          带薪是否选中 1是0否
     * @param noPaid        不带薪是否选中 1是0否
     */
    private List<Integer> setPaidList(Integer balanceStatus, Integer paid, Integer noPaid) {
        List<Integer> list = new ArrayList<>();
        if (Objects.equals(BalanceEnum.BALANCE_TRUE.getCode(), balanceStatus)) {
            if (Objects.equals(BalanceEnum.BALANCE_TRUE.getCode(), paid)) {
                list.add(BalanceEnum.BALANCE_PAID_DH.getCode());
            }
            if (Objects.equals(BalanceEnum.BALANCE_TRUE.getCode(), noPaid)) {
                list.add(BalanceEnum.BALANCE_PAID_DK.getCode());
            }
        }
        return list;
    }

    /**
     * 时间校验
     *
     * @param start  借调开始时间
     * @param end    借调结束时间
     * @param userId 用户id
     */
    private void checkSecondDate(List<SecondmentDateVo> timeJsons, Date start, Date end, String userId) throws HandleException {
        if (CollectionUtil.isEmpty(timeJsons)) {
            return;
        }
        for (SecondmentDateVo dateVo : timeJsons) {

            Date startTime = dateVo.getStartTime();
            Date endTime = dateVo.getEndTime();
            Boolean startCompareStart = startTime != null && (DateUtil.dateCompare(start, startTime) >= 0 && DateUtil.dateCompare(end, endTime) <= 0);
//                    || (DateUtil.dateCompare(departureTime, startTime) >= 0 && DateUtil.dateCompare(backTime, endTime) <= 0);
            /* 时间比较 用户借调开始时间<=用户已被借调结束时间 || 用户回岗时间<用户已被借调结束时间*/
            Boolean endCompareStart = endTime != null && (DateUtil.dateCompare(start, endTime) <= 0 && DateUtil.dateCompare(end, endTime) >= 0);
//                    || (DateUtil.dateCompare(backTime, endTime) <= 0 && DateUtil.dateCompare(backTime, endTime) >= 0);
            if (startCompareStart || endCompareStart) {
//                UserEntity userEntity = userApi.getInfoById(userId);
                ActionResult<List<UserBoundVO>> userList = v2UserApi.getAllUserInfoBatch(Stream.of(userId).collect(Collectors.toList()), null);
                if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) {
//                    map = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())) ;
                    throw new HandleException(userList.getData().get(0).getUserName() + "已被借调");
                }

            }
        }
    }


    /**
     * 时间校验 2.0 版本
     *
     * @param timeJsons
     * @param start     借调开始时间
     * @param end       借调结束时间
     * @param userId    用户id
     * @throws HandleException
     */
    private void checkSecondDateNew2(List<SecondmentDateVo> timeJsons, Date start, Date end, String userId, Map<String, UserBoundVO> userMap) throws HandleException {
        if (CollectionUtil.isEmpty(timeJsons)) {
            return;
        }
        for (SecondmentDateVo dateVo : timeJsons) {

            Date startTime = dateVo.getStartTime();
            Date endTime = dateVo.getEndTime();
            Boolean startCompareStart = startTime != null && (DateUtil.dateCompare(start, startTime) >= 0 && DateUtil.dateCompare(end, endTime) <= 0);
            /** 时间比较 用户借调开始时间<=用户已被借调结束时间 || 用户回岗时间<用户已被借调结束时间*/
            Boolean endCompareStart = endTime != null && (DateUtil.dateCompare(start, endTime) <= 0 && DateUtil.dateCompare(end, endTime) >= 0);
            if (startCompareStart || endCompareStart) {

                throw new HandleException(userMap.get(userId).getName() + "已被借调");
            }
        }
    }

    /**
     * 请假审批通过后触发接口
     *
     * @param id                       审批的唯一id
     * @param attendanceLeaveApproveVo 请假信息
     * @param status                   是否审核通过 0.待审核 2.未通过 1.通过 3.撤销
     * @author hlp
     */
    private void consumption(String id, AttendanceLeaveApproveVo attendanceLeaveApproveVo, Integer status, String tenantId, String approveUserId, String approveUserName) throws ApproveException {
        if (0 == attendanceLeaveApproveVo.getStatus()) {
            Integer unit = attendanceLeaveApproveVo.getUnit();
            if (1 != status) {
                // 修改请假审批记录
                attendanceLeaveApproveMapper.updateLeaveApproveStatus(id, status, approveUserId, approveUserName);
            } else {
                //获取请假类型信息
                // 特殊情况-请假类型单位使用创建审批时的历史纪录，其他配置获取实时更新的请假类型信息
                AttendanceLeaveRulesVo userLeaveDetail = attendanceLeaveTypeService.getUserLeaveDetail(attendanceLeaveApproveVo.getTypeId(), attendanceLeaveApproveVo.getUserId());
                if (null == userLeaveDetail) {
                    throw new ApproveException("未找到本次请假审批选择的类型");
                }
                log.info("leaveSetting: {}", userLeaveDetail);
                // 涉及班次信息
                LeaveShiftVo leaveDuration = new LeaveShiftVo();
                LeaveQueryDto leaveQueryDto = new LeaveQueryDto();
                leaveQueryDto.setUserId(attendanceLeaveApproveVo.getUserId());
                leaveQueryDto.setTypeId(attendanceLeaveApproveVo.getTypeId());
                leaveQueryDto.setStartTime(attendanceLeaveApproveVo.getStartTime().getTime() + "");
                leaveQueryDto.setEndTime(attendanceLeaveApproveVo.getEndTime().getTime() + "");
                leaveQueryDto.setEndTimeType(attendanceLeaveApproveVo.getEndTimeType());
                leaveQueryDto.setStartTimeType(attendanceLeaveApproveVo.getStartTimeType());
                leaveQueryDto.setUnit(attendanceLeaveApproveVo.getUnit());
                // 划线排班不能请半天类型的假
                if (Objects.equals(BalanceEnum.BALANCE_UNIT_BT.getCode(), unit)) {
                    boolean lineScheduleByUserId = attendanceDailyRuleService.hasLinearRulesByPeriod(attendanceLeaveApproveVo.getUserId(), attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime());
                    if (lineScheduleByUserId) {
                        log.error("划线排班不能请半天类型的假");
                        throw new ApproveException("划线排班不能请半天类型的假");
                    }
                }
                if (Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), unit)) {
                    // 和出勤换算比做对比，算出时长
                    BigDecimal timeDifference = DateDetail.getTimeDifference(attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime());
                    // 小时假不能超过24小时
                    if (timeDifference.compareTo(BigDecimal.valueOf(24)) > 0) {
                        log.error("小时假不能超过24小时");
                        throw new ApproveException("小时假不能超过24小时");
                    }
                }
                // 通过请假时的请假类型判断走什么逻辑
                // 获取时段涉及的班次和时间
                List<DailyRuleResultVo> dailyRuleResultVos = null;
                ApplyParam applyParam = new ApplyParam(attendanceLeaveApproveVo.getUserId(), attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime(), AttendanceTypeEnum.LEAVE, attendanceLeaveApproveVo.getStartTimeType(), attendanceLeaveApproveVo.getEndTimeType(), unit);
                try {
                    // applyParam 对象中start 和 end 会被一下方法重新赋值 表示命中的开始和结束时间
                    // 主要处理半天时判断时间使用
                    dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(applyParam);
                    leaveDuration.getDailyRuleVos().addAll(dailyRuleResultVos);
                    for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
                        if (1 == dailyRuleResultVo.getType()) {
                            // 有异常直接报错
                            log.error(dailyRuleResultVo.getFailMsg());
                            throw new HandleException(dailyRuleResultVo.getFailMsg());
                        }
                    }
                } catch (HandleException e) {
                    log.error(e.getMessage(), e);
                    throw new ApproveException(e.getMessage());
                }
                List<DailyRuleResultVo> dailyCopy = dailyRuleResultVos;
                //申请时长
                BigDecimal day = BigDecimal.ZERO;
                dailyRuleResultVos = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).collect(Collectors.toList());
                for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) {
                    day = day.add(dailyRuleResultVo.getLeaveDays());
                }
                try {
                    // 处理划线排班
                    day = getUnderline(dailyCopy, attendanceLeaveApproveVo.getUserId(), applyParam.getStart(), applyParam.getEnd(), day,unit);
                } catch (HandleException e) {
                    log.error(e.getMessage(), e);
                    throw new ApproveException(e.getMessage());
                }
                //先根据日期date分组
                DailyRuleResultVo min = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).min(Comparator.comparing(DailyRuleResultVo::getInPoint)).orElse(null);
                if (null != min) {
                    leaveQueryDto.setStartTimeHit(min.getInPoint());
                } else {
                    leaveQueryDto.setStartTimeHit(attendanceLeaveApproveVo.getStartTime());
                }
                DailyRuleResultVo max = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).max(Comparator.comparing(DailyRuleResultVo::getOutPoint)).orElse(null);
                if (null != max) {
                    leaveQueryDto.setEndTimeHit(max.getOutPoint());
                } else {
                    leaveQueryDto.setEndTimeHit(attendanceLeaveApproveVo.getEndTime());
                }
                AttendanceBaseSetting attendanceBaseSetting = getAttendanceRatio(leaveQueryDto, attendanceLeaveApproveVo.getUserId());
                leaveDuration.setAttendanceRatio(attendanceBaseSetting.getAttendanceRatio());
                if (day.compareTo(BigDecimal.ZERO) <= 0) {
                    // 命中时长为0 表示未命中班次且需要特殊处理半天及天的请假时间
                    if (BigDecimal.ZERO.compareTo(day) == 0 && !Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(),unit)) {
                        leaveQueryDto.setStartTimeHit(DateDetail.getDayBeginByDay(applyParam.getStart()));
                        // endtime 取日期的23:59:59
                        leaveQueryDto.setEndTimeHit(DateDetail.getDayEndByDay(applyParam.getEnd()));
                    }
                    // 请假时长公式：
                    // 小时、半天、全天只区分命中和未命中，命中了时长=命中时长，未命中班次 请假时长 = 结束-开始（区分小时1天限制，半天取一个半天0.5，天一天取1）
                    try {
                        day = getDay(unit, leaveQueryDto, attendanceLeaveApproveVo.getUserId(), day,attendanceBaseSetting);
                    } catch (HandleException e) {
                        log.error("getDay", e);
                        throw new ApproveException(e.getMessage());
                    }
                }
                try {
                    // 校验加班时间是否重叠
                    checkWorkTimeOverlap(attendanceLeaveApproveMapper.getUserWorkByTimeSlot(attendanceLeaveApproveVo.getUserId(), applyParam.getStart(), applyParam.getEnd()), "选择的时间段范围内有其他加班(待审核/审核已通过)");
                    // 校验请假时间是否重叠  2024年10月30日更改逻辑为只校验当前审批与已审核通过的审批时间是否重叠
                    checkLeaveTimeOverlap(attendanceLeaveApproveVo.getUserId(), applyParam.getStart(), applyParam.getEnd(), attendanceLeaveApproveVo.getId(),attendanceLeaveApproveVo.getStartTimeType(),attendanceLeaveApproveVo.getEndTimeType(),attendanceLeaveApproveVo.getUnit());
                    // 新增借调校验
                    ActionResult<Void> result = new ActionResult<>();
                    result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode());
                    result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg());
                    checkGroupClockForOa(attendanceLeaveApproveVo.getUserId(), applyParam.getStart(), applyParam.getEnd(), result);
                    if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) {
                        throw new ApproveException(result.getMsg());
                    }
                } catch (HandleException e) {
                    log.error(e.getMessage(), e);
                    throw new ApproveException(e.getMessage());
                }

                // 备份半天、天类型请假时长存数据库使用
                BigDecimal applicationDurationDay = day;
                int applicationDurationSecond = 0;
                // 备份小时类型的请假天数
                BigDecimal applicationDuration = BigDecimal.ZERO;
                int undeductedTimeSecond = 0;
                // 单次最大时长校验
                if (new BigDecimal(userLeaveDetail.getDuration()).compareTo(day) < 0) {
                    throw new ApproveException("请假时长超出可请假的最大时长");
                }
                // 2.0 消费假期劵  2.0.1 可能有0小时的请假
                LeaveConsumptionDetailVo leaveConsumptionDetailVo = attendanceUserBalanceRecordService.leaveConsumption(attendanceLeaveApproveVo.getUserId(),  day, userLeaveDetail, attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime(), attendanceLeaveApproveVo.getId());
                leaveConsumptionDetailVo.setAttendanceRatio(attendanceBaseSetting.getAttendanceRatio());
                // 调用生成排班班次信息
                try {
                    String s = attendanceDailyRuleService.applyDailyRuleHandle(new ApplyParam(attendanceLeaveApproveVo.getUserId(), id, attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime(),  BigDecimal.ZERO, AttendanceTypeEnum.LEAVE, attendanceLeaveApproveVo.getStartTimeType(), attendanceLeaveApproveVo.getEndTimeType(), unit,  BigDecimal.ZERO));
                    if (StringUtils.isNotEmpty(s)) {
                        throw new ApproveException(s);
                    }
                } catch (HandleException e) {
                    log.error(e.getMessage(), e);
                    throw new ApproveException(e.getMessage());
                }
                //  修改请假审批记录 2024-1-22 1请假通过保留请假类型
                //  新增未抵扣时长天与请假时间天
                attendanceLeaveApproveMapper.updateLeaveApprove(id, BigDecimal.ZERO, applicationDuration, userLeaveDetail.getName(), applicationDurationSecond, undeductedTimeSecond, approveUserId, approveUserName, applicationDurationDay, leaveConsumptionDetailVo.getUnconsumedBalance(), applyParam.getStart(), applyParam.getEnd(), JSONUtil.toJsonStr(leaveDuration), JSONUtil.toJsonStr(leaveConsumptionDetailVo));

            }
        }
    }


    /**
     * 获取考勤组审批的管理员
     *
     * @param groupId 考勤组id
     * @param type    审批类型 0 借调 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批    6.外出    7.出差
     * @return Map<String, Object>
     */
    @Override
    public AttendanceApprovalAdminVo getApproveUser(String groupId, Integer type) {
        AttendanceApprovalAdminVo approvalAdminVo = new AttendanceApprovalAdminVo();
        approvalAdminVo.setIsApproval(true);
        QueryWrapper<AttendanceApprovalSetting> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda()
                .eq(AttendanceApprovalSetting::getGroupId, groupId)
                .eq(AttendanceApprovalSetting::getType, type);
        AttendanceApprovalSetting approvalSetting = attendanceApprovalSettingMapper.selectOne(queryWrapper);
        if (approvalSetting == null && (Objects.equals(type, ApprovalSettingTypeEnum.GO_OUT.getCode()) || Objects.equals(type, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode()))) {
            if (Objects.equals(type, ApprovalSettingTypeEnum.GO_OUT.getCode())) {
                approvalSetting = new AttendanceApprovalSetting(groupId, ApprovalSettingTypeEnum.GO_OUT.getCode(), ApprovalPermissionTypeEnum.SUPERIOR.getCode());
            }
            if (Objects.equals(type, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode())) {
                approvalSetting = new AttendanceApprovalSetting(groupId, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode(), ApprovalPermissionTypeEnum.SUPERIOR.getCode());
            }
        }
        if (approvalSetting == null) {
            return approvalAdminVo;
        }

        if (approvalSetting.getPermission().equals(ApprovalPermissionTypeEnum.NOT.getCode())) {
            /* 无需审批*/
            approvalAdminVo.setIsApproval(false);
            approvalAdminVo.setUserList(new ArrayList<>());
            return approvalAdminVo;
        }
        if (approvalSetting.getPermission().equals(ApprovalPermissionTypeEnum.ATTENDANCE_SUPER_ADMIN.getCode())) {
            /* 考勤超级管理员*/
            QueryWrapper<AttendanceManagerPermission> permissionQueryWrapper = new QueryWrapper<>();
            permissionQueryWrapper.lambda()
                    .eq(AttendanceManagerPermission::getDeleteMark, 0)
                    .eq(AttendanceManagerPermission::getType, PermissionTypeEnum.SUPER_ADMIN.getCode());
            List<AttendanceManagerPermission> superAdminList = attendanceManagerPermissionMapper.selectList(permissionQueryWrapper);
            if (CollectionUtil.isEmpty(superAdminList)) {
                approvalAdminVo.setUserList(new ArrayList<>());
                return approvalAdminVo;
            }
            List<String> userIdList = superAdminList.stream().map(AttendanceManagerPermission::getUserId).collect(Collectors.toList());
            List<AttendanceUserVo> userVoList = toUserVoInfo(userIdList);
            approvalAdminVo.setUserList(userVoList);
            return approvalAdminVo;
        }
        if (approvalSetting.getPermission().equals(ApprovalPermissionTypeEnum.SUPERIOR.getCode())) {
            /* 上级考勤组管理员*/
            AttendanceGroup group = attendanceGroupMapper.selectById(groupId);
            List<AttendanceGroupAdminVo> permissionVos = recursionGetLeaderPermission(group, type);
            if (CollectionUtil.isEmpty(permissionVos)) {
                approvalAdminVo.setUserList(new ArrayList<>());
                return approvalAdminVo;
            }

            List<String> userIdList = permissionVos.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList());
            List<AttendanceUserVo> userVoList = toUserVoInfo(userIdList);
            approvalAdminVo.setUserList(userVoList);
            return approvalAdminVo;
        }
        /* 当前考勤组管理员*/
        List<AttendanceGroupAdminVo> groupAdminVoList = attendanceManagerPermissionMapper.queryGroupUser(groupId);
        if (CollectionUtil.isEmpty(groupAdminVoList)) {
            List<AttendanceGroupAdminVo> superiorAdminList = superiorAdmin(groupId, type);
            if (CollectionUtil.isEmpty(superiorAdminList)) {
                approvalAdminVo.setUserList(new ArrayList<>());
                return approvalAdminVo;
            }

            List<String> userIdList = superiorAdminList.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList());
            List<AttendanceUserVo> userVoList = toUserVoInfo(userIdList);
            approvalAdminVo.setUserList(userVoList);
            return approvalAdminVo;
        }
        /* 对应权限审批管理员*/
        List<AttendanceGroupAdminVo> hasPermissionAdminList = groupAdminVoList.stream().filter(admin -> {
            String byCode = ApprovalSettingTypeEnum.getByCode(type);
//            return hasPermission(admin.getCurPermissionJson(), type);
            return checkPermissionJson(admin.getCurPermissionJson(), byCode);
        }).collect(Collectors.toList());
        if (CollectionUtil.isEmpty(hasPermissionAdminList)) {
            List<AttendanceGroupAdminVo> superiorAdminList = superiorAdmin(groupId, type);
            if (CollectionUtil.isEmpty(superiorAdminList)) {
                approvalAdminVo.setUserList(new ArrayList<>());
                return approvalAdminVo;
            }
            List<String> userIdList = superiorAdminList.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList());
            List<AttendanceUserVo> userVoList = toUserVoInfo(userIdList);
            approvalAdminVo.setUserList(userVoList);
            return approvalAdminVo;
        }
        List<String> userIdList = hasPermissionAdminList.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList());
//        List<PartUserInfoVo> infoByIds = userApi.getInfoByIds(userIdList);
//        List<AttendanceUserVo> userList = JsonUtil.getJsonToList(infoByIds, AttendanceUserVo.class);
        List<AttendanceUserVo> userVoList = toUserVoInfo(userIdList);
        approvalAdminVo.setUserList(userVoList);
        return approvalAdminVo;
    }

    private List<AttendanceUserVo> toUserVoInfo(List<String> userIdList) {
        if (CollectionUtil.isEmpty(userIdList)) {
            return new ArrayList<>();
        }


        ActionResult<List<UserBoundVO>> result = v2UserApi.getAllUserInfoBatch(userIdList, null);
        List<AttendanceUserVo> userList = new ArrayList<>();
        if (200 == result.getCode() && CollectionUtil.isNotEmpty(result.getData())) {
            userList = JsonUtil.getJsonToList(result.getData(), AttendanceUserVo.class);
        }

        for (AttendanceUserVo attendanceUserVo : userList) {
            String uploaderImg = UploaderUtil.uploaderImg(attendanceUserVo.getHeadIcon());
            attendanceUserVo.setHeadIcon(uploaderImg);
        }
        return userList;
    }

    /**
     * 获取上级对应权限考勤管理员
     *
     * @param groupId 考勤组id
     * @param type    类型
     * @return List<AttendanceGroupAdminVo>
     */
    private List<AttendanceGroupAdminVo> superiorAdmin(String groupId, Integer type) {
        AttendanceGroup attendanceGroup = attendanceGroupMapper.selectById(groupId);
        return recursionGetLeaderPermission(attendanceGroup, type);
    }

    /**
     * 获取请假申请审批管理员
     *
     * @param body 请假开始时间
     * @return AttendanceApprovalAdminVo
     */
    @Override
    public AttendanceApprovalAdminVo getLeaveApproveUser(String body) throws HandleException {

        FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class);

        String loginUserId = UserProvider.getLoginUserId();
        Set<String> groupIds = this.getGroupIdsNew(loginUserId, flowTaskVo.getApplyDate(), flowTaskVo.getEndTime());
        if (CollectionUtil.isNotEmpty(groupIds) && groupIds.size() > 1) {
            throw new HandleException("请假时间不可以跨两个考勤组");
        }

        if (CollectionUtil.isEmpty(groupIds)) {
            throw new HandleException("你不属于任何考勤组");
        }
        String groupId = groupIds.iterator().next();
        return getApproveUser(groupId, ApprovalSettingTypeEnum.LEAVE.getCode());
    }

    /**
     * 加班审批
     */
    @Override
    public AttendanceApprovalAdminVo getOvertimeApproveUser(String body) throws Exception {
        String loginUserId = UserProvider.getLoginUserId();
        FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class);
        System.out.println("加班开始时间:" + flowTaskVo.getStartTime());
        System.out.println("加班结束时间:" + flowTaskVo.getEndTime());
        Set<String> groupIds = getGroupIdsNew(loginUserId, flowTaskVo.getStartTime(), flowTaskVo.getEndTime());
        if (CollectionUtil.isNotEmpty(groupIds) && groupIds.size() > 1) {
            throw new HandleException("加班时间不可以跨两个考勤组");
        }
        if (CollectionUtil.isEmpty(groupIds)) {
            throw new HandleException("你不在任何考勤组!");
        }
        String nextGroupId = groupIds.iterator().next();

        if (nextGroupId == null) {
            throw new HandleException("你不属于任何考勤组");
        }
//        AttendanceApprovalAdminVo approveUser = getApproveUser(curGroupUser.getGroupId(), ApprovalSettingTypeEnum.OVERTIME.getCode());
        return getApproveUser(nextGroupId, ApprovalSettingTypeEnum.OVERTIME.getCode());
    }

    /**
     * 获取补卡审批管理员
     *
     * @return AttendanceApprovalAdminVo
     */
    @Override
    public AttendanceApprovalAdminVo getReplacementCardApproveUser(String body) throws Exception {
//        FlowTaskEntity flowTask = flowTaskApi.findByTaskId(taskId);
        FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class);
        String groupId = groupIdByClockInResultId(flowTaskVo.getClockInResultId());
        return getApproveUser(groupId, ApprovalSettingTypeEnum.ROUTINE.getCode());
    }

    /**
     * 考勤变更
     */
    @Override
    public AttendanceApprovalAdminVo getAttendanceAlter(String body) throws Exception {
//        FlowTaskEntity flowTask = flowTaskApi.findByTaskId(taskId);
        FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class);
        String groupId = groupIdByClockInResultId(flowTaskVo.getClockInResultId());
        return getApproveUser(groupId, ApprovalSettingTypeEnum.ACTION_RESULT.getCode());
    }

    /**
     * 获取外勤审批管理员
     *
     * @return AttendanceApprovalAdminVo
     */
    @Override
    public AttendanceApprovalAdminVo getFieldApproval() {
        String loginUserId = UserProvider.getLoginUserId();
        String groupId = getGroupIdByUserId(loginUserId);
        return getApproveUser(groupId, ApprovalSettingTypeEnum.OUT.getCode());
    }

    /**
     * 借调审批
     */
    @Override
    public List<String> secondedApproval(String body) throws Exception {
        FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class);

        String selfGroupId = flowTaskVo.getSelfGroupId();

        return getSecondedAdminUserId(selfGroupId);
//        List<String> managerIds = recursionGetSecondedManager(selfGroupId, managerUserIds);
    }


    /**
     * 获取用户所属考勤组id
     *
     * @param userId 用户id
     * @return String
     */
    private String getGroupIdByUserId(String userId) {
        QueryWrapper<AttendanceGroupUser> groupUserQueryWrapper = new QueryWrapper<>();
        groupUserQueryWrapper
                .lambda()
                .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.CUR.getCode())
                .eq(AttendanceGroupUser::getUserId, userId).eq(AttendanceGroupUser::getDeleteMark, 0);
        AttendanceGroupUser groupUser = attendanceGroupUserMapper.selectOne(groupUserQueryWrapper);
        return groupUser != null ? groupUser.getGroupId() : null;
    }

    /**
     * 通过打卡结果获取考勤组id
     *
     * @return String
     */
    private String groupIdByClockInResultId(String clockInResultId) {
        AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectById(clockInResultId);
        String ruleId = clockInResult.getRuleId();
        FtbAttendanceDailyRule attendanceDailyRule = attendanceDailyRuleMapper.selectById(ruleId);
        return attendanceDailyRule.getGroupId();
    }

    /**
     * 递归获取上级考勤组管理员
     *
     * @param group 考勤组id
     * @return List<AttendancePermissionVo>
     */
    private List<AttendanceGroupAdminVo> recursionGetLeaderPermission(AttendanceGroup group, Integer type) {
        List<AttendanceGroupAdminVo> adminVoList = new ArrayList<>();
        AttendanceGroup parent = attendanceGroupMapper.selectById(group.getParentId());
        if (parent == null) {
            // 新增逻辑找考勤组超管
            /* 考勤超级管理员*/
            QueryWrapper<AttendanceManagerPermission> permissionQueryWrapper = new QueryWrapper<>();
            permissionQueryWrapper.lambda()
                    .eq(AttendanceManagerPermission::getDeleteMark, 0)
                    .eq(AttendanceManagerPermission::getType, PermissionTypeEnum.SUPER_ADMIN.getCode());
            List<AttendanceManagerPermission> superAdminList = attendanceManagerPermissionMapper.selectList(permissionQueryWrapper);
            if (CollectionUtil.isEmpty(superAdminList)) {
                return adminVoList;
            }
            List<String> userIdList = superAdminList.stream().map(AttendanceManagerPermission::getUserId).collect(Collectors.toList());
            userIdList.forEach(v -> {
                AttendanceGroupAdminVo attendanceGroupAdminVo = new AttendanceGroupAdminVo();
                attendanceGroupAdminVo.setUserId(v);
                adminVoList.add(attendanceGroupAdminVo);
            });
            return adminVoList;
        }
        List<AttendanceGroupAdminVo> groupAdminVoList = attendanceManagerPermissionMapper.queryGroupUser(parent.getId());
        if (CollectionUtil.isEmpty(groupAdminVoList)) {
            return recursionGetLeaderPermission(parent, type);
        }
        String byCode = ApprovalSettingTypeEnum.getByCode(type);
        List<AttendanceGroupAdminVo> approvalPermissions = groupAdminVoList.stream().filter(permission -> {
            return checkPermissionJson(permission.getChildPermissionJson(), byCode);
        }).collect(Collectors.toList());
        if (CollectionUtil.isEmpty(approvalPermissions)) {
            return recursionGetLeaderPermission(parent, type);
        }

        adminVoList.addAll(approvalPermissions);
        return adminVoList;
    }


    /**
     * 设置班次时段信息
     *
     * @param startDay 开始日期 2023-11-11
     * @param list     集合
     * @param userId   用户id
     * @author hlp
     */
    private void setUserClasses(String startDay, List<UserClassesVo> list, String userId) {
        //查询指定日期的排班
        List<ClassesDetailVo> classesDetailVos = attendanceDailyRuleMapper.getShiftByDay(startDay, userId);
        UserClassesVo userClassesVo = new UserClassesVo();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        try {
            userClassesVo.setDay(sdf.parse(startDay));
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
        SimpleDateFormat hm = new SimpleDateFormat("HH:mm");
        classesDetailVos.forEach(v -> {
            // 处理次日
            String inPoint = sdf.format(v.getInPointDate());
            String outPoint = sdf.format(v.getOutPointDate());
            String breakStartPoint = null;
            String breakEndPoint = null;
            String breakStartPointHm = null;
            String breakEndPointHm = null;
            if (null != v.getBreakStartPointDate()) {
                breakStartPoint = sdf.format(v.getBreakStartPointDate());
                breakStartPointHm = hm.format(v.getBreakStartPointDate());
            }
            if (null != v.getBreakStartPointDate()) {
                breakEndPoint = sdf.format(v.getBreakEndPointDate());
                breakEndPointHm = hm.format(v.getBreakEndPointDate());
            }
            String inPointHm = hm.format(v.getInPointDate());
            String outPointHm = hm.format(v.getOutPointDate());
            v.setInPoint(inPointHm);
            v.setOutPoint(inPoint.equals(outPoint) ? outPointHm : "次日" + outPointHm);
            v.setBreakStartPoint(breakStartPointHm);
            v.setBreakEndPoint(null == breakStartPoint ? null : (breakStartPoint.equals(breakEndPoint) ? breakEndPointHm : "次日" + breakEndPointHm));
        });
        userClassesVo.getClassesDetailVoList().addAll(classesDetailVos);
        if (CollUtil.isNotEmpty(classesDetailVos)) {
            list.add(userClassesVo);
        }
    }

    /**
     * 权限校验
     */
    private static Boolean checkPermissionJson(String permissionJson, String permissionMsg) {
        if (StrUtil.isBlank(permissionJson)) {
            return false;
        }
        /* 获取权限之间的交集*/
        List<String> toPermissions = ListUtil.toList(permissionJson.split(","));
        List<String> toMsgs = ListUtil.toList(permissionMsg.split(","));
        Collection<String> intersection = CollectionUtil.intersection(toPermissions, toMsgs);
        return !intersection.isEmpty();
    }

    /**
     * 获取借调权限的管理员用户id
     *
     * @param groupId 考勤组id
     * @return List<String>
     */
    private List<String> getSecondedAdminUserId(String groupId) {
        List<String> managerUserIds = new ArrayList<>();
        Map<String, Object> map = attendanceSuperAdminService.listGroupAdmin(groupId);
        List<AttendanceGroupAdminVo> managerList = (List<AttendanceGroupAdminVo>) map.get("managerList");
        System.out.println("managerList:" + managerList);
        if (CollectionUtil.isNotEmpty(managerList)) {
            List<AttendanceGroupAdminVo> groupAdminVos = managerList.stream().filter(manager -> {
                return StrUtil.isNotBlank(manager.getCurPermissionJson()) && manager.getCurPermissionJson().contains(AttendancePermissionConstant.SECONDED_APPROVAL);
            }).collect(Collectors.toList());
            if (CollectionUtil.isNotEmpty(groupAdminVos)) {
                managerUserIds.addAll(groupAdminVos.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList()));
            }
        }


        /* 获取父考勤组拥有子借调权限的管理员*/
        AttendanceGroup attendanceGroup = attendanceGroupMapper.selectById(groupId);
        String[] parentGroupIdArr = attendanceGroup.getLevelCode().split("#");
        List<String> groupIds = Arrays.stream(parentGroupIdArr).collect(Collectors.toList());

        List<QueryGroupAdminVo> queryGroupAdminVos = attendanceManagerPermissionMapper.queryManagerByGroupIds(groupIds, PermissionTypeEnum.CHILD.getCode());
        if (CollectionUtil.isNotEmpty(queryGroupAdminVos)) {
            List<QueryGroupAdminVo> adminList = queryGroupAdminVos.stream().filter(manager -> {
                return manager.getPermissions().contains(AttendancePermissionConstant.SECONDED_APPROVAL);
            }).collect(Collectors.toList());
            if (CollectionUtil.isNotEmpty(adminList)) {
                managerUserIds.addAll(adminList.stream().map(QueryGroupAdminVo::getUserId).collect(Collectors.toList()));
            }
        }
        log.info("借调管理员id:{}", managerUserIds);
        return managerUserIds;
    }
}
