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.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.google.common.collect.Lists;
import jnpf.attendance.mapper.*;
import jnpf.attendance.service.*;
import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler;
import jnpf.base.ActionResult;
import jnpf.base.UserInfo;
import jnpf.base.service.SuperServiceImpl;
import jnpf.base.vo.PageListVO;
import jnpf.constants.AttendanceConstant;
import jnpf.constants.MessageTopicConstants;
import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo;
import jnpf.entity.AttendanceGroup;
import jnpf.entity.AttendanceGroupUser;
import jnpf.entity.attendance.*;
import jnpf.entity.workflow.ApplyAttendanceChange;
import jnpf.entity.workflow.ApplyAttendanceOutside;
import jnpf.entity.workflow.ApplyAttendanceRepair;
import jnpf.entity.workflow.ApplyAttendanceViolation;
import jnpf.enums.attendance.*;
import jnpf.enums.attendance.v2.ClockOutHandleParam;
import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum;
import jnpf.exception.HandleException;
import jnpf.exception.QueryException;
import jnpf.model.attendance.dto.ClockInDto;
import jnpf.model.attendance.dto.LatticeStatisticsVoDto;
import jnpf.model.attendance.event.StatisticsSingleDto;
import jnpf.model.attendance.model.*;
import jnpf.model.attendance.vo.*;
import jnpf.model.attendance.vo.attendance.*;
import jnpf.model.attendance.vo.event.OvertimeEvent;
import jnpf.model.common.DateRangeDto;
import jnpf.model.personnels.dto.secondment.FtbSecondMentQueryDTO;
import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto;
import jnpf.model.personnels.req.roster.StaffRosterListReq;
import jnpf.permission.V2UserApi;
import jnpf.permission.eum.v2.UserWorkStatusEnums;
import jnpf.permission.model.user.BaseUserInfoVo;
import jnpf.permission.vo.v2.user.UserBoundInfoVO;
import jnpf.permission.vo.v2.user.UserBoundVO;
import jnpf.personnels.FtbPersonneApi;
import jnpf.personnels.FtbPersonnelsRosterManagerApi;
import jnpf.util.*;
import jnpf.util.context.ThreadContext;
import jnpf.workflow.mapper.ApplyAttendanceChangeMapper;
import jnpf.workflow.mapper.ApplyAttendanceOutsideMapper;
import jnpf.workflow.mapper.ApplyAttendanceRepairMapper;
import jnpf.workflow.mapper.ApplyAttendanceViolationMapper;
import jnpf.workflow.service.FlowTaskService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.jetbrains.annotations.Nullable;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static jnpf.constants.AttendanceConstant.HOLIDAYS_COLOR;
import static jnpf.constants.RedisConstant.ATTENDANCE_USER_SET_SCHEDULES;

/**
 * 打卡服务实现
 *
 * @author yanwenfu
 * @create 2023-11-21
 */
@Slf4j
@Service
public class AttendanceClockInServiceImpl extends SuperServiceImpl<AttendanceClockInMapper, FtbAttendanceClockIn> implements AttendanceClockInService {

    private final AttendanceClockInResultService attendanceClockInResultService;
    private final AttendanceDailyRuleService attendanceDailyRuleService;
    private final AttendanceSuperAdminService attendanceSuperAdminService;
    private final AttendanceRepairService attendanceRepairService;
    private final AttendanceLocationSettingService attendanceLocationSettingService;
    private final AttendanceBaseSettingService attendanceBaseSettingService;
    private final AttendanceDayStatisticsService attendanceDayStatisticsService;
    private final AttendanceUserSettingService attendanceUserSettingService;
    private final AttendanceClockInPicService attendanceClockInPicService;
    private final AttendanceUserService attendanceUserService;
    private final AttendanceNoticeHandler attendanceNoticeHandler;
    private final ClockInResultService clockInResultService;
    private final OvertimeRuleService overtimeRuleService;
    private final AttendanceUserBalanceService attendanceUserBalanceService;
    private final FtbPersonneApi ftbPersonneApi;
    @Resource
    private AttendanceClockInMapper attendanceClockInMapper;
    @Resource
    private AttendanceClockInResultMapper attendanceClockInResultMapper;
    @Resource
    private AttendanceDailyRuleMapper attendanceDailyRuleMapper;
    @Resource
    private AttendanceGroupUserMapper attendanceGroupUserMapper;
    @Resource
    private AttendanceGroupMapper attendanceGroupMapper;
    @Resource
    private ApplyAttendanceChangeMapper applyAttendanceChangeMapper;
    @Resource
    private ApplyAttendanceRepairMapper applyAttendanceRepairMapper;
    @Resource
    private ApplyAttendanceOutsideMapper applyAttendanceOutsideMapper;
    @Resource
    private ApplyAttendanceViolationMapper applyAttendanceViolationMapper;
    @Resource
    private AttendanceLeaveApproveMapper attendanceLeaveApproveMapper;
    @Resource
    private FlowTaskService flowTaskService;
    @Autowired
    private V2UserApi v2UserApi;
    @Autowired
    private FtbPersonnelsRosterManagerApi rosterManagerApi;
    @Resource
    private UserProvider userProvider;
    @Resource
    private CustomTenantUtil customTenantUtil;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private ApplicationContext context;
    @Resource
    private RocketMQTemplate rocketMqTemplate;
    @Resource
    private DailyRuleChangeMapper dailyRuleChangeMapper;

    @Autowired
    public AttendanceClockInServiceImpl(@Lazy AttendanceClockInResultService attendanceClockInResultService, @Lazy AttendanceDailyRuleService attendanceDailyRuleService,
                                        @Lazy AttendanceSuperAdminService attendanceSuperAdminService, @Lazy AttendanceRepairService attendanceRepairService,
                                        @Lazy AttendanceLocationSettingService attendanceLocationSettingService, @Lazy ClockInResultService clockInResultService,
                                        @Lazy AttendanceBaseSettingService attendanceBaseSettingService, @Lazy AttendanceDayStatisticsService attendanceDayStatisticsService,
                                        @Lazy AttendanceUserSettingService attendanceUserSettingService, @Lazy AttendanceClockInPicService attendanceClockInPicService,
                                        @Lazy AttendanceUserService attendanceUserService, @Lazy AttendanceNoticeHandler attendanceNoticeHandler,
                                        @Lazy OvertimeRuleService overtimeRuleService, @Lazy AttendanceUserBalanceService attendanceUserBalanceService,
                                        @Lazy FtbPersonneApi ftbPersonneApi) {
        this.attendanceClockInResultService = attendanceClockInResultService;
        this.attendanceDailyRuleService = attendanceDailyRuleService;
        this.attendanceSuperAdminService = attendanceSuperAdminService;
        this.attendanceRepairService = attendanceRepairService;
        this.attendanceLocationSettingService = attendanceLocationSettingService;
        this.clockInResultService = clockInResultService;
        this.attendanceBaseSettingService = attendanceBaseSettingService;
        this.attendanceDayStatisticsService = attendanceDayStatisticsService;
        this.attendanceUserSettingService = attendanceUserSettingService;
        this.attendanceClockInPicService = attendanceClockInPicService;
        this.attendanceUserService = attendanceUserService;
        this.attendanceNoticeHandler = attendanceNoticeHandler;
        this.overtimeRuleService = overtimeRuleService;
        this.attendanceUserBalanceService = attendanceUserBalanceService;
        this.ftbPersonneApi = ftbPersonneApi;
    }

    @Override
    public List<GroupInfoVo> getClockInMainInfo(Date today, UserInfo userInfo, Integer isMainInfo) throws Exception {

        // log.error("当前时间 - begin       ，{}", System.currentTimeMillis());
        List<GroupInfoVo> groupList = new ArrayList<>();
        List<FtbAttendanceDailyRule> dailyRuleList = clockInResultService.getTodayRuleList(today, userInfo);
        // 如果今日没有出勤规则, 返回一条attendanceType为-1的记录, 且id为null
        List<FtbAttendanceDailyRule> collect = dailyRuleList.stream().filter(v -> DateDetail.checkSameDay(today, v.getDay())).collect(Collectors.toList());
        if (collect.isEmpty()) {
            emptyDailyRuleDealNew(today, userInfo, dailyRuleList);
        }
        // log.error("当前时间 - rule-get    ，{}", System.currentTimeMillis());
        boolean hasCurrentRule = false;
        Map<String, UserBoundInfoVO> userMap = new HashMap<>();
        List<String> ruleIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList());
        List<ClockInVo> clockInList = attendanceClockInMapper.getClockInResultByRule(getRuleUserList(ruleIds, userInfo.getUserId()));
        // 已查询过考勤组信息的考勤组
        String lastGroup = "-1";
        Set<String> processedGroups = new HashSet<>();
        for (FtbAttendanceDailyRule dailyRule : dailyRuleList) {
            if (processedGroups.add(dailyRule.getGroupId())) {
                // 查询用户考勤组信息
                try {
                    lastGroup = dailyRule.getGroupId();
                    GroupInfoVo groupInfo = getGroupInfo(dailyRule.getDay(), dailyRule.getGroupId(), dailyRule.getSelfGroup(), userInfo, isMainInfo);
                    groupList.add(groupInfo);
                } catch (Exception e) {
                    // 考勤组异常
                    continue;
                }
            } else {
                if (!lastGroup.equals(dailyRule.getGroupId())) {
                    GroupInfoVo copyInfo = groupList.stream()
                            .filter(v -> v.getGroupId().equals(dailyRule.getGroupId()))
                            .findFirst().orElse(null);
                    assert null != copyInfo;
                    GroupInfoVo copyGroup = JsonUtil.getJsonToBean(copyInfo, GroupInfoVo.class);
                    copyGroup.getAttendanceRuleList().clear();
                    copyGroup.getClockInList().clear();
                    groupList.add(copyGroup);
                }
            }
            if (groupList.isEmpty()) {
                continue;
            }
            AttendanceRuleVo rule = BeanUtil.toBean(dailyRule, AttendanceRuleVo.class);
            // 设置用户出勤打卡记录
            setWorkRecord(clockInList, rule, groupList.get(groupList.size() - 1), userMap);
            // 判断是否当前时间段
            if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || dailyRule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())) {
                // 如果排班是休/外出/出差, 直接true
                if (!hasCurrentRule) {
                    rule.setCurrentPeriod(ConstantUtil.NUM_TRUE);
                }
                groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule);
                break;
            }
            if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) {
                // 请假 直接false
                groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule);
                continue;
            }
            if (dailyRule.getInUnbounded() != 0 && !dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode())
                    && !dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())
                    && dailyRule.getOutUnbounded() != 0 && !dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode())
                    && !dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())) {
                // 上下班同时隐藏
                groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule);
                continue;
            }
            if (dailyRule.getOutUnbounded() != 0 && !dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()) && null != rule.getOnWorkInfoVo()) {
                if (!dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode()) && !dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())) {
                    // 上班隐藏无影响, 下班隐藏如果已经打了上班卡则不用等待下班时间
                    groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule);
                    continue;
                }
            }
            if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) {
                // 普班 在开始打卡时间 - 结束打卡时间内
                Date maxDate = DateDetail.goToMaxSeconds(rule.getOutLackPoint());
                if (!hasCurrentRule && DateDetail.checkTimeBetween(today, rule.getClockStartPoint(), maxDate)) {
                    if (rule.getOffWorkInfoVo() == null) {
                        rule.setCurrentPeriod(ConstantUtil.NUM_TRUE);
                        hasCurrentRule = true;
                    }
                }
                // 设置缺卡时间点
                // 上班缺卡 是否允许迟到, 不允许, 上班时间一到就缺卡
                /*if (rule.getLateEnable().equals(ConstantUtil.NUM_FALSE)) {
                    rule.setInLackPoint(rule.getInPoint());
                }*/
                groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule);
                continue;
            }
            if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                // 在上一个班的下班时间, 下一个班的上班时间 范围内, 且无下班打卡, 设置currentPeriod为true
                if (DateDetail.checkTimeBetween(today, dailyRule.getClockStartPoint(), dailyRule.getOutLackPoint())) {
                    if (rule.getOffWorkInfoVo() == null) {
                        rule.setCurrentPeriod(ConstantUtil.NUM_TRUE);
                        hasCurrentRule = true;
                    }
                }
                groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule);
            }
        }
        // log.error("当前时间 - rule-deal   ，{}", System.currentTimeMillis());
        if (groupList.isEmpty()) {
            if (isMainInfo.equals(ConstantUtil.NUM_TRUE)) {
                throw new QueryException("用户暂无考勤组");
            } else {
                // 查询用户有无考勤组
                String selfGroup = attendanceGroupUserMapper.getSelfGroup(userInfo.getUserId());
                if (StringUtils.isEmpty(selfGroup)) {
                    throw new QueryException("用户暂无排班");
                }
            }
        }
        setCouldUpdate(groupList);
        // log.error("当前时间 - could-update，{}", System.currentTimeMillis());
        return groupList;
    }

    private List<RuleUserVo> getRuleUserList(List<String> ruleIds, String userId) {

        List<RuleUserVo> list = new ArrayList<>();
        ruleIds.forEach(v -> list.add(new RuleUserVo(v, userId)));
        return list;
    }

    private void setApprovalList(List<GroupInfoVo> groupList, Date today, UserInfo userInfo) {

        String queryDate = DateDetail.getDate2Str(today, DateDetail.DF8);
        if (!groupList.isEmpty()) {
            groupList.forEach(group -> {
                List<DailyClockInVo> clockInList = new ArrayList<>();
                group.getAttendanceRuleList().forEach(rule -> {
                    if (null != rule.getOnWorkInfoVo()) {
                        clockInList.add(new DailyClockInVo(rule.getOnWorkInfoVo().getId(), rule.getOnWorkInfoVo().getClockInKind(), rule.getOnWorkInfoVo().getClockInId()));
                    }
                    if (null != rule.getOffWorkInfoVo()) {
                        clockInList.add(new DailyClockInVo(rule.getOffWorkInfoVo().getId(), rule.getOffWorkInfoVo().getClockInKind(), rule.getOffWorkInfoVo().getClockInId()));
                    }
                });
                List<DailyApprovalVo> list = getApprovalList(clockInList, queryDate, group.getUserId(), group.getGroupId(), userInfo.getTenantId());
                list.removeIf(v -> !"已通过".equals(v.getLastResult()));
                group.getApprovalList().addAll(list);
            });
        }
    }

    private void setCouldUpdate(List<GroupInfoVo> groupList) {

        if (!groupList.isEmpty()) {
            List<AttendanceRuleVo> ruleList = groupList.stream()
                    .map(GroupInfoVo::getAttendanceRuleList)
                    .flatMap(List::stream)
                    .filter(v -> v.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || v.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())
                            || v.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode()) || v.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode()))
                    .collect(Collectors.toList());
            for (int i = 0; i < ruleList.size(); i++) {
                AttendanceRuleVo rule = ruleList.get(i);
                if (rule.getOnWorkInfoVo() != null) {
                    rule.getOnWorkInfoVo().setUpdateCheck(rule.getOffWorkInfoVo() == null ? ConstantUtil.NUM_TRUE : ConstantUtil.NUM_FALSE);
                }
                if (rule.getOffWorkInfoVo() != null) {
                    if (i < ruleList.size() - 1) {
                        ClockInVo next = ruleList.get(i + 1).getOnWorkInfoVo();
                        rule.getOffWorkInfoVo().setUpdateCheck(next == null ? ConstantUtil.NUM_TRUE : ConstantUtil.NUM_FALSE);
                    } else {
                        rule.getOffWorkInfoVo().setUpdateCheck(ConstantUtil.NUM_TRUE);
                    }
                }
            }
        }
    }

    private void emptyDailyRuleDealNew(Date today, UserInfo userInfo, List<FtbAttendanceDailyRule> dailyRuleList) throws QueryException {

        // 查询今天属于哪个考勤组
        GroupInfoVo group = getGroupInfoByDate(today, userInfo);
        // 判断考勤组人员关系是否正常
        AttendanceGroupUser groupUser = getGroupUser(userInfo.getUserId(), group.getGroupId());
        if (null == groupUser) {
            throw new QueryException("已不是考勤组成员");
        }
        FtbAttendanceDailyRule dailyRule = generateRestDailyRule(group, null);
        dailyRuleList.add(dailyRule);
    }

    private void emptyDailyRuleDeal(Date today, UserInfo userInfo, List<GroupInfoVo> groupList) throws QueryException {

        // 查询今天属于哪个考勤组
        GroupInfoVo group = getGroupInfoByDate(today, userInfo);
        // 判断考勤组人员关系是否正常
        AttendanceGroupUser groupUser = getGroupUser(userInfo.getUserId(), group.getGroupId());
        if (null == groupUser) {
            throw new QueryException("已不是考勤组成员");
        }
        // 出勤规则是空当休息处理
        AttendanceRuleVo restRule = getRestDailyRule(group);
        boolean flag = false;
        for (GroupInfoVo v : groupList) {
            AttendanceRuleVo vo = v.getAttendanceRuleList().stream().filter(rule -> rule.getCurrentPeriod().equals(ConstantUtil.NUM_TRUE)).findFirst().orElse(null);
            if (null != vo) {
                flag = true;
                break;
            }
        }
        if (!flag) {
            restRule.setCurrentPeriod(ConstantUtil.NUM_TRUE);
        }
        GroupInfoVo existGroup = groupList.stream().filter(v -> group.getGroupId().equals(v.getGroupId())).findFirst().orElse(null);
        if (null == existGroup) {
            group.getAttendanceRuleList().add(restRule);
            groupList.add(group);
        } else {
            existGroup.getAttendanceRuleList().add(restRule);
        }
    }

    private AttendanceRuleVo getRestDailyRule(GroupInfoVo group) throws QueryException {

        RLock lock = redissonClient.getLock(String.format(ATTENDANCE_USER_SET_SCHEDULES, UserProvider.getUser().getTenantId(), group.getGroupId(), group.getUserId()));
        if (lock.isLocked()) {
            throw new QueryException("当前考勤组排班操作正在执行中，请稍后再试");
        }
        FtbAttendanceDailyRule dailyRule = generateRestDailyRule(group, FtbUtil.getId());
        attendanceDailyRuleMapper.insert(dailyRule);
        return JsonUtil.getJsonToBean(dailyRule, AttendanceRuleVo.class);
    }

    /**
     * 生成-1的出勤规则(今日无出勤规则)
     * @param group 考勤组
     * @param id id可以不传(打卡时再insert到数据库)
     * @return jnpf.entity.attendance.FtbAttendanceDailyRule
     */
    private FtbAttendanceDailyRule generateRestDailyRule(GroupInfoVo group, String id) {
        FtbAttendanceDailyRule dailyRule = new FtbAttendanceDailyRule();
        dailyRule.setId(id);
        dailyRule.setGroupId(group.getGroupId());
        dailyRule.setSelfGroup(group.getSelfGroup());
        dailyRule.setDay(group.getDay());
        dailyRule.setAttendanceType(AttendanceTypeEnum.DEFAULT.getCode());
        dailyRule.setUserId(group.getUserId());
        dailyRule.setValidDuration(0);
        dailyRule.setApplyViewEnable(ConstantUtil.NUM_TRUE);
        return dailyRule;
    }

    private MutablePair<Date, Date> getOverTimeWorkRange(AttendanceRuleVo rule, String userId) {

        DateDetail dateDetail = new DateDetail(rule.getInPoint());
        dateDetail.getYesterday();
        Date previousDate = dateDetail.getCurrentDate();
        FtbAttendanceDailyRule previousRule = attendanceDailyRuleMapper.getPreviousOrNextRule(rule.getId(), previousDate, rule.getInPoint(), userId, "previous");
        if (null != previousRule) {
            previousDate = previousRule.getOutPoint();
        }
        dateDetail.getTomorrow();
        dateDetail.getTomorrow();
        Date nextDate = dateDetail.getCurrentDate();
        FtbAttendanceDailyRule nextRule = attendanceDailyRuleMapper.getPreviousOrNextRule(rule.getId(), rule.getInPoint(), nextDate, userId, "next");
        if (null != nextRule) {
            nextDate = nextRule.getInPoint();
        }
        return MutablePair.of(previousDate, nextDate);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public MutablePair<Integer, String> clockIn(ClockInDto clockInDto) throws Exception {
        UserInfo userInfo = userProvider.get();
        if (null == userInfo || StringUtils.isEmpty(userInfo.getUserId())) {
            if (StringUtils.isEmpty(clockInDto.getUserId()) || StringUtils.isEmpty(clockInDto.getTenantId())) {
                return MutablePair.of(0, null);
            }
            userInfo = new UserInfo();
            userInfo.setUserId(clockInDto.getUserId());
            userInfo.setTenantId(clockInDto.getTenantId());
        }
        // 当前考勤组及考勤规则
        MutablePair<GroupInfoVo, AttendanceRuleVo> pair = getGroupInfoAndRule(new Date(), userInfo);
        // 判断是上班打卡还是下班打卡
        Integer clockInType = getClockInType(pair.getRight());
        // 验证打卡方式
        addClockInMethodDeviceInfo(pair.getLeft().getClockInMethodVo(), userInfo.getUserId(), userInfo.getTenantId());
        checkDeviceType(clockInDto.getClockInKind(), clockInDto.getDeviceType(), clockInDto.getDeviceId(), pair.getRight(), pair.getLeft().getClockInMethodVo(), clockInType);
        // 生成打卡记录
        FtbAttendanceClockIn clockIn = setClockInRecord(pair, clockInDto, null, userInfo);
        //生成、修改外勤打卡图片
        List<AttendanceClockInPic> clockInPicAddOrUpdateList = setPicList(clockIn.getId(), clockInDto);
        // 生成打卡结果
        AttendanceClockInResult clockInResult = generateClockInResultRecord(pair.getRight(), clockIn, null, clockInType, userInfo);
        // 判断数据库是否已经存在当前出勤规则和打卡类型的卡, 有则新增失败
        LambdaQueryWrapper<AttendanceClockInResult> resultQuery = new LambdaQueryWrapper<AttendanceClockInResult>()
                .eq(AttendanceClockInResult::getRuleId, clockInResult.getRuleId())
                .eq(AttendanceClockInResult::getClockInType, clockInResult.getClockInType())
                .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE);
        AttendanceClockInResult dbResult = attendanceClockInResultMapper.selectOne(resultQuery);
        if (null != dbResult) {
            String remark = clockInResult.getClockInType().equals(ConstantUtil.ON_WORK) ? "上班卡" : "下班卡";
            throw new HandleException("您已经打过" + remark + "了");
        }
        // 保存数据库
        attendanceClockInMapper.insert(clockIn);
        if (CollUtil.isNotEmpty(clockInPicAddOrUpdateList)) {
            attendanceClockInPicService.saveOrUpdateBatch(clockInPicAddOrUpdateList);
        }
        attendanceClockInResultMapper.insert(clockInResult);
        // 发送消息
        if (null != clockInDto.getFast() && clockInDto.getFast().equals(ConstantUtil.NUM_TRUE)) {
            AttendanceNoticeEnum noticeEnum = clockInResult.getClockInType().equals(ConstantUtil.ON_WORK) ? AttendanceNoticeEnum.CHECK_IN_QUICK : AttendanceNoticeEnum.CHECK_OUT_QUICK;
            FastClockInNoticeModel model = new FastClockInNoticeModel();
            model.setWorkTime(clockInResult.getClockInType().equals(ConstantUtil.ON_WORK) ? pair.getRight().getInPoint() : pair.getRight().getOutPoint());
            model.setClockIn(clockIn);
            model.setAttendanceNoticeModel(clockInResult.getUserId(), pair.getRight().getGroupId(), userInfo.getTenantId(), new Date(), noticeEnum);
            attendanceNoticeHandler.send(model);
        }
        // 发布日统计数据生成事件
        StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder()
                .tenantId(userInfo.getTenantId())
                .groupId(pair.getLeft().getGroupId())
                .userId(clockInResult.getUserId())
                .day(clockIn.getDay())
                .triggerSceneEnum(TriggerSceneEnum.CHECK_IN)
                .build();
        if(StrUtil.isBlank(userInfo.getTenantId()) || StrUtil.isBlank(pair.getLeft().getGroupId())){
            log.error("AttendanceClockInServiceImpl#clockIn 触发日统计租户ID或者考勤组ID为空：{}", JSON.toJSONString(courseEventDTO));
        }
        Message<StatisticsSingleDto> message = MessageBuilder.withPayload(courseEventDTO).build();
        rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2);
        return MutablePair.of(clockInResult.getClockInStatus(), clockInResult.getId());
    }

    private void addClockInMethodDeviceInfo(ClockInMethodVo clockInMethodVo, String userId, String tenantId) {

        List<AttendanceMachineManage> machineList = clockInResultService.getMachineList(userId, tenantId);
        if (!machineList.isEmpty()) {
            List<MethodVo> collect = machineList.stream().map(v -> new MethodVo(v.getName(), v.getMac(), v.getType())).collect(Collectors.toList());
            clockInMethodVo.getDeviceList().addAll(collect);
        }
    }

    private List<AttendanceClockInPic> setPicList(String clockInId, ClockInDto clockInDto) {
        List<AttendanceClockInPic> clockInPicAddOrUpdateList = CollUtil.newArrayList();
        if (StringUtils.isNotEmpty(clockInDto.getApprovalCode())) {
            //审批进来的
            clockInPicAddOrUpdateList = attendanceClockInPicService.lambdaQuery().eq(AttendanceClockInPic::getApprovalCode, clockInDto.getApprovalCode()).list();
            if (CollUtil.isNotEmpty(clockInPicAddOrUpdateList)) {
                clockInPicAddOrUpdateList.forEach(item -> item.setClockInId(clockInId));
            }
        } else {
            //直接打卡进来的
            if (CollUtil.isNotEmpty(clockInDto.getPicUrlList())) {
                clockInPicAddOrUpdateList.addAll(clockInDto.getPicUrlList().stream().map(item -> AttendanceClockInPic.builder()
                        .clockInId(clockInId)
                        .picUrl(item)
                        .build()).collect(Collectors.toList()));
            }
        }
        return clockInPicAddOrUpdateList;
    }

    private void checkDeviceType(Integer kind, Integer deviceType, String address, AttendanceRuleVo rule, ClockInMethodVo clockInMethod, Integer clockInType) throws HandleException {

        if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())
                || AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || getOutsideCheck(rule, clockInType)) {
            return;
        }
        switch (deviceType) {
            case ConstantUtil.DEVICE_PLACE:
                if (clockInMethod.getLocationList().isEmpty() && !kind.equals(ConstantUtil.KIND_OUTSIDE)) {
                    throw new HandleException("无可用的打卡地点，可前往考勤组设置");
                }
                break;
            case ConstantUtil.DEVICE_MACHINE:
                if (clockInMethod.getDeviceList().isEmpty()) {
                    throw new HandleException("无可用的考勤机，可前往考勤组设置");
                } else {
                    MethodVo method = clockInMethod.getDeviceList().stream().filter(v -> v.getAddress().equals(address)).findFirst().orElse(null);
                    if (null == method) {
                        throw new HandleException("非法的考勤机");
                    }
                }
                break;
            case ConstantUtil.DEVICE_WIFI:
                if (clockInMethod.getWifiList().isEmpty()) {
                    throw new HandleException("无可用的WIFI，可前往考勤组设置");
                }
                break;
            default:
                break;
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public MutablePair<Integer, String> updateClockIn(String clockInId, ClockInDto clockInDto) throws Exception {

        UserInfo userInfo = userProvider.get();
        if (null == userInfo || StringUtils.isEmpty(userInfo.getUserId())) {
            if (StringUtils.isEmpty(clockInDto.getUserId()) || StringUtils.isEmpty(clockInDto.getTenantId())) {
                return MutablePair.of(0, null);
            }
            userInfo = new UserInfo();
            userInfo.setUserId(clockInDto.getUserId());
            userInfo.setTenantId(clockInDto.getTenantId());
        }
        // 查询打卡记录
        AttendanceClockInResult dbResult = getClockInResultById(clockInId);
        if (null == dbResult) {
            throw new QueryException("未找到打卡记录");
        }
        // 查询打卡记录的出勤规则
        AttendanceRuleVo rule = getAttendanceRule(dbResult.getRuleId());
        // 查询考勤组规则
        GroupInfoVo groupInfo = getGroupInfo(rule.getDay(), rule.getGroupId(), rule.getSelfGroup(), userInfo);
        // 查询考勤组规则
        Map<String, UserBoundInfoVO> userMap = new HashMap<>();
        List<ClockInVo> clockInList = attendanceClockInMapper.getClockInResultByRule(getRuleUserList(List.of(rule.getId()), userInfo.getUserId()));
        setWorkRecord(clockInList, rule, groupInfo, userMap);
        MutablePair<GroupInfoVo, AttendanceRuleVo> pair = MutablePair.of(groupInfo, rule);
        // 判断是上班打卡还是下班打卡
        Integer clockInType = dbResult.getClockInType();
        // 生成打卡记录
        FtbAttendanceClockIn clockIn = setClockInRecord(pair, clockInDto, clockInId, userInfo);
        // 生成打卡结果
        AttendanceClockInResult clockInResult = generateClockInResultRecord(pair.getRight(), clockIn, null, clockInType, userInfo);
        // V1.8.1 新增更新打卡图片功能
        //生成、修改外勤打卡图片
        List<AttendanceClockInPic> clockInPicAddOrUpdateList = CollUtil.newArrayList();
        if (CollUtil.isNotEmpty(clockInDto.getPicUrlList())) {
            clockInPicAddOrUpdateList.addAll(clockInDto.getPicUrlList().stream().map(item -> AttendanceClockInPic.builder()
                    .clockInId(clockIn.getId())
                    .picUrl(item)
                    .build()).collect(Collectors.toList()));
        }
        if (StringUtils.isNotEmpty(dbResult.getApplyId()) || StringUtils.isNotEmpty(clockInResult.getApplyId())) {
            // 审批中, 更新上一次打卡结果为已删除
            dbResult.setDeleteMark(ConstantUtil.NUM_TRUE);
            dbResult.setDeleteTime(new Date());
            dbResult.setUserId(userInfo.getUserId());
            attendanceClockInResultMapper.updateById(dbResult);
            attendanceClockInResultMapper.insert(clockInResult);
            // 审批中, 标识图片删除
            attendanceClockInPicService.update(new LambdaUpdateWrapper<AttendanceClockInPic>()
                    .eq(AttendanceClockInPic::getClockInId, clockInId)
                    .set(AttendanceClockInPic::getDeleteMark, ConstantUtil.NUM_TRUE));
        } else {
            // 直接在上一次打卡结果上更新
            clockInResult.setId(dbResult.getId());
            clockInResult.setDeleteMark(ConstantUtil.NUM_FALSE);
            attendanceClockInResultMapper.updateById(clockInResult);
            // 删除原来的图片
            QueryWrapper<AttendanceClockInPic> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("F_ClockInId", clockInId);
            attendanceClockInPicService.remove(queryWrapper);
        }
        // 保存数据库
        attendanceClockInMapper.insert(clockIn);
        // 打卡图片不为空
        if (CollUtil.isNotEmpty(clockInPicAddOrUpdateList)) {
            attendanceClockInPicService.saveOrUpdateBatch(clockInPicAddOrUpdateList);
        }
        if (StringUtils.isNotEmpty(clockInDto.getApprovalCode())) {
            attendanceClockInPicService.update(new LambdaUpdateWrapper<AttendanceClockInPic>()
                    .set(AttendanceClockInPic::getClockInId, clockIn.getId())
                    .eq(AttendanceClockInPic::getApprovalCode, clockInDto.getApprovalCode())
                    .eq(AttendanceClockInPic::getDeleteMark, ConstantUtil.NUM_FALSE));
        }
        // 发布日统计数据生成事件
        StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder()
                .tenantId(userInfo.getTenantId())
                .groupId(rule.getGroupId())
                .userId(clockInResult.getUserId())
                .day(clockIn.getDay())
                .triggerSceneEnum(TriggerSceneEnum.CHECK_IN)
                .build();
        if(StrUtil.isBlank(userInfo.getTenantId()) || StrUtil.isBlank(rule.getGroupId())){
            log.error("AttendanceClockInServiceImpl#updateClockIn 触发日统计租户ID或者考勤组ID为空：{}", JSON.toJSONString(courseEventDTO));
        }
        Message<StatisticsSingleDto> message = MessageBuilder.withPayload(courseEventDTO).build();
        rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2);
        return MutablePair.of(clockInResult.getClockInStatus(), clockInResult.getId());
    }

    private AttendanceClockInResult getClockInResultById(String clockInId) {

        LambdaQueryWrapper<AttendanceClockInResult> resultQueryWrapper = new LambdaQueryWrapper<AttendanceClockInResult>()
                .eq(AttendanceClockInResult::getClockInId, clockInId)
                .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE)
                .orderByDesc(AttendanceClockInResult::getCreatorTime)
                .last("limit 1");
        return attendanceClockInResultMapper.selectOne(resultQueryWrapper);
    }

    @Override
    public AttendanceRuleVo getAttendanceRule(String ruleId) throws HandleException {

        LambdaQueryWrapper<FtbAttendanceDailyRule> dailyRuleQueryWrapper = new LambdaQueryWrapper<FtbAttendanceDailyRule>()
                .eq(FtbAttendanceDailyRule::getId, ruleId);
        FtbAttendanceDailyRule dailyRule = attendanceDailyRuleMapper.selectOne(dailyRuleQueryWrapper);
        if (null == dailyRule) {
            throw new HandleException("未查询到当前出勤规则");
        }
        if (null == dailyRule.getInUnbounded()) {
            dailyRule.setInUnbounded(ConstantUtil.NUM_FALSE);
        }
        if (null == dailyRule.getOutUnbounded()) {
            dailyRule.setOutUnbounded(ConstantUtil.NUM_FALSE);
        }
        // 加班获取加班规则 如果加班规则为空, 说明出勤规则有问题
        if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
            if (StringUtils.isNotEmpty(dailyRule.getPeriodInfo())) {
                OvertimeRuleDetailVo overtimeRuleDetail = JsonUtil.getJsonToBean(dailyRule.getPeriodInfo(), OvertimeRuleDetailVo.class);
                dailyRule.setOvertimeRuleDetail(overtimeRuleDetail);
            }
        } else {
            // 无需审批加班需要自己查询加班规则 查询用户加班配置
            OvertimeRuleVo overtimeRule = overtimeRuleService.getEffectDetail(dailyRule.getUserId(), null, dailyRule.getDay());
            if (null != overtimeRule) {
                // 判定用户使用哪个配置 节假日/工作日/公休日
                FtbAttendanceDailyRule nextRule = attendanceDailyRuleMapper.selectOne(new LambdaQueryWrapper<FtbAttendanceDailyRule>()
                        .eq(FtbAttendanceDailyRule::getDay, dailyRule.getDay())
                        .gt(FtbAttendanceDailyRule::getInPoint, dailyRule.getInPoint())
                        .orderByAsc(FtbAttendanceDailyRule::getInPoint)
                        .last("limit 1"));
                Integer attendanceType;
                if (null == nextRule) {
                    attendanceType = getAttendanceType(dailyRule, null, null);
                } else {
                    attendanceType = getAttendanceType(dailyRule, nextRule.getId(), nextRule.getAttendanceType());
                }
                OvertimeRuleDetailVo effectWorkDetail = overtimeRuleService.getEffectWorkDetail(overtimeRule, dailyRule.getDay(), attendanceType);
                if (null != effectWorkDetail) {
                    dailyRule.setOvertimeRuleDetail(effectWorkDetail);
                    if (effectWorkDetail.getCalcMethod().equals(3)) {
                        dailyRule.setOvertime(ConstantUtil.NUM_TRUE);
                    }
                }
            }
        }
        setOtherField(dailyRule);
        return JsonUtil.getJsonToBean(dailyRule, AttendanceRuleVo.class);
    }

    @Override
    public Map<String, AttendanceRuleVo> getAttendanceRuleBatch(List<String> ruleIds) {

        Map<String, AttendanceRuleVo> returnMap = new HashMap<>();
        // 使用 Guava 的 Lists.partition 分批
        List<List<String>> partitions = Lists.partition(ruleIds, 1000);
        for (List<String> batch : partitions) {
            // 批量查询 dailyRule
            LambdaQueryWrapper<FtbAttendanceDailyRule> wrapper = new LambdaQueryWrapper<FtbAttendanceDailyRule>()
                    .in(FtbAttendanceDailyRule::getId, batch);
            List<FtbAttendanceDailyRule> dailyRuleList = attendanceDailyRuleMapper.selectList(wrapper);
            if (dailyRuleList == null || dailyRuleList.isEmpty()) {
                continue;
            }
            // 按 day 分组
            Map<Date, List<FtbAttendanceDailyRule>> dayMap = dailyRuleList.stream()
                    .collect(Collectors.groupingBy(FtbAttendanceDailyRule::getDay));
            for (Map.Entry<Date, List<FtbAttendanceDailyRule>> entry : dayMap.entrySet()) {
                Date day = entry.getKey();
                List<FtbAttendanceDailyRule> dayRuleList = entry.getValue();
                // 获取该天用户列表
                List<String> userIds = dayRuleList.stream()
                        .map(FtbAttendanceDailyRule::getUserId)
                        .distinct()
                        .collect(Collectors.toList());
                // 获取加班规则
                Map<String, OvertimeRuleVo> overtimeMap = overtimeRuleService.getEffectDetailByUserList(userIds, day);
                // 查询每个规则的下一个规则
                List<String> dayRuleIds = dayRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList());
                List<NextRuleVo> nextRuleList = attendanceDailyRuleMapper.getNextRulePartInfo(dayRuleIds, DateDetail.getDate2Str(day, DateDetail.DF));
                Map<String, NextRuleVo> nextRuleMap = nextRuleList.stream().collect(Collectors.toMap(NextRuleVo::getRuleId, Function.identity()));
                for (FtbAttendanceDailyRule dailyRule : dayRuleList) {
                    // 判空设置默认值
                    if (dailyRule.getInUnbounded() == null) dailyRule.setInUnbounded(ConstantUtil.NUM_FALSE);
                    if (dailyRule.getOutUnbounded() == null) dailyRule.setOutUnbounded(ConstantUtil.NUM_FALSE);
                    // 加班规则处理
                    if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(dailyRule.getAttendanceType())) {
                        if (StringUtils.isNotEmpty(dailyRule.getPeriodInfo())) {
                            OvertimeRuleDetailVo overtimeRuleDetail = JsonUtil.getJsonToBean(dailyRule.getPeriodInfo(), OvertimeRuleDetailVo.class);
                            dailyRule.setOvertimeRuleDetail(overtimeRuleDetail);
                        }
                    } else {
                        OvertimeRuleVo overtimeRule = overtimeMap.get(dailyRule.getUserId());
                        if (overtimeRule != null) {
                            NextRuleVo nextRule = nextRuleMap.get(dailyRule.getId());
                            if (nextRule != null) {
                                Integer attendanceType = getAttendanceType(dailyRule, nextRule.getNextRuleId(), nextRule.getNextAttendanceType());
                                OvertimeRuleDetailVo effectWorkDetail = overtimeRuleService.getEffectWorkDetail(overtimeRule, dailyRule.getDay(), attendanceType);
                                if (effectWorkDetail != null) {
                                    dailyRule.setOvertimeRuleDetail(effectWorkDetail);
                                    if (effectWorkDetail.getCalcMethod().equals(3)) {
                                        dailyRule.setOvertime(ConstantUtil.NUM_TRUE);
                                    }
                                }
                            }
                        }
                    }
                    // 设置其他字段
                    setOtherField(dailyRule);
                    // 转成 VO
                    AttendanceRuleVo bean = JsonUtil.getJsonToBean(dailyRule, AttendanceRuleVo.class);
                    returnMap.put(bean.getId(), bean);
                }
            }
        }
        return returnMap;
    }

    private void setOtherField(FtbAttendanceDailyRule dailyRule) {
        // 借调
        if (dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode())) {
            dailyRule.setInHideStatus(ConstantUtil.STR_TRUE);
            dailyRule.setOnWorkIgnore(true);
            dailyRule.setInLackPoint(dailyRule.getInPoint());
        }
        if (dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode())) {
            dailyRule.setOutHideStatus(ConstantUtil.STR_TRUE);
            dailyRule.setOffWorkIgnore(true);
            dailyRule.setOutLackPoint(dailyRule.getOutPoint());
        }
        // 加班
        if (dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode())) {
            dailyRule.setInHideStatus(ConstantUtil.STR_TRUE);
            dailyRule.setOnWorkIgnore(true);
            dailyRule.setClockStartPoint(dailyRule.getInPoint());
            dailyRule.setInLackPoint(dailyRule.getInPoint());
        }
        if (dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode())) {
            dailyRule.setOutHideStatus(ConstantUtil.STR_TRUE);
            dailyRule.setOffWorkIgnore(true);
            dailyRule.setOutLackPoint(dailyRule.getOutPoint());
        }
        // 晚走晚到
        if (dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())) {
            // 在这个时间点生成无需打卡 -2
            dailyRule.setOnWorkIgnore(true);
        }
        if (dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())) {
            // 在这个时间点生成无需打卡 -2
            dailyRule.setOffWorkIgnore(true);
        }
        if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
            // 按审批时长申请计算 无需打卡
            if (dailyRule.getOvertimeRuleDetail().getCalcMethod().equals(1)) {
                dailyRule.setOnWorkIgnore(true);
                dailyRule.setOffWorkIgnore(true);
            }
        }
    }

    private Integer getAttendanceType(FtbAttendanceDailyRule rule, String nextRuleId, Integer nextRuleAttendanceType) {
        Integer attendanceType;
        if (rule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())) {
            return AttendanceTypeEnum.ORDINARY.getCode();
        }
        if (null == nextRuleId) {
            attendanceType = rule.getAttendanceType();
        } else {
            // rule 和 nextRule 中有一个休 则出勤类型为休, 否则为工作日
            if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || nextRuleAttendanceType.equals(AttendanceTypeEnum.REST.getCode())) {
                attendanceType = AttendanceTypeEnum.REST.getCode();
            } else {
                attendanceType = AttendanceTypeEnum.ORDINARY.getCode();
            }
        }
        return attendanceType;
    }

    @Override
    public void changeAttendanceRuleBatch(List<UserDayVo> userDayList, UserInfo updateUser) {

        String requestId = FtbUtil.getId();
        log.error("用户打卡重新匹配 - {} - init...", requestId);
        String updateUserId = updateUser.getUserId();
        String tenantId = updateUser.getTenantId();
        if (null == userDayList || userDayList.isEmpty()) {
            return;
        }
        // 备份list
        Date now = new Date();
        List<UserDayVo> backupList = new ArrayList<>(userDayList);
        Map<String, List<String>> userGroupMap = userDayList.stream()
                .collect(Collectors.groupingBy(
                        item -> item.getUserId() + "_" + item.getDayStr(),
                        Collectors.mapping(UserDayVo::getGroupId, Collectors.toList())
                ));
        // 只有今天以及今天之前的班次变动才需要匹配打卡
        userDayList.removeIf(v -> v.getDay().after(new Date()));
        if (userDayList.isEmpty()) {
            return;
        }
        log.error("用户打卡重新匹配 - {} - 匹配日期: {}", requestId, JsonUtil.getObjectToString(userDayList));
        // 查询用户当日 + 昨日的出勤规则
        List<FtbAttendanceDailyRule> dailyRuleList = attendanceDailyRuleMapper.getDailyRuleListBatch(userDayList);
        if (CollUtil.isEmpty(dailyRuleList)) {
            log.error("用户打卡重新匹配 - {} - 未查询到出勤规则", requestId);
            return;
        }
        log.error("用户打卡重新匹配 - {} - 查询排班结果: {}", requestId,
                JsonUtil.getObjectToString(dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).distinct().collect(Collectors.toList())));
        // 按目标日期 + 用户分组
        Map<String, List<FtbAttendanceDailyRule>> groupedMap = dailyRuleList.stream()
                .collect(Collectors.groupingBy(rule -> rule.getUserId() + "_" + rule.getTargetDate()));
        Map<String, List<FtbAttendanceDailyRule>> ruleListByUserDay = clockInResultService.getRuleListByUserDay(groupedMap, userGroupMap);
        // 当前的打卡记录列表
        List<FtbAttendanceClockIn> clockInList = new ArrayList<>();
        int batchSize = 150;
        for (int i = 0; i < userDayList.size(); i += batchSize) {
            List<UserDayVo> batch = userDayList.subList(i, Math.min(i + batchSize, userDayList.size()));
            if (batch.isEmpty()) {
                continue;
            }
            List<FtbAttendanceClockIn> list = attendanceClockInMapper.selectListBatch(batch, ConstantUtil.PASS_TRUE);
            if (list != null && !list.isEmpty()) {
                clockInList.addAll(list);
            }
        }
        log.error("用户打卡重新匹配 - {} - 查询到的所有打卡记录: {}", requestId, clockInList.stream().map(FtbAttendanceClockIn::getId).collect(Collectors.toList()));
        Map<String, AttendanceClockInResult> dbClockInMap;
        if (!clockInList.isEmpty()) {
            List<String> clockInIds = clockInList.stream().map(FtbAttendanceClockIn::getId).collect(Collectors.toList());
            List<AttendanceClockInResult> clockInResultList = attendanceClockInResultMapper.selectBatchByClockInIds(clockInIds);
            dbClockInMap = clockInResultList.stream().collect(Collectors.toMap(AttendanceClockInResult::getClockInId, Function.identity()));
        } else {
            dbClockInMap = new HashMap<>();
        }
        log.error("用户打卡重新匹配 - {} - 过滤后可用的打卡: {}", requestId, clockInList.stream().map(FtbAttendanceClockIn::getId).collect(Collectors.toList()));
        // 删除这些出勤规则关联的打卡记录
        ConcurrentMap<String, List<FtbAttendanceDailyRule>> map = dailyRuleList.stream().collect(Collectors.groupingByConcurrent(FtbAttendanceDailyRule::getUserId));
        ConcurrentMap<String, List<FtbAttendanceClockIn>> clockInMap = clockInList.stream()
                .collect(Collectors.groupingByConcurrent(clockIn -> clockIn.getUserId() + "_" + DateDetail.getDate2Str(clockIn.getDay(), DateDetail.DF)));
        // 生成打卡结果
        List<AttendanceClockInResult> resultList = new ArrayList<>();
        Set<String> distinctSet = new HashSet<>();
        List<OvertimeInfoVo> overtimeInfoList = new ArrayList<>();
        ruleListByUserDay.forEach((k, ruleList) -> {
            UserInfo userInfo = new UserInfo();
            String[] split = k.split("_");
            String userId = split[0];
            String targetDate = split[1];
            userInfo.setUserId(userId);
            userInfo.setTenantId(tenantId);
            List<FtbAttendanceDailyRule> collect = ruleList == null ?
                    new ArrayList<>() : ruleList.stream().filter(rule -> DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(targetDate))
                    .collect(Collectors.toList());
            List<FtbAttendanceClockIn> clockInCollect = clockInMap.getOrDefault(k, new ArrayList<>());
            // 生成打卡结果列表
            List<AttendanceClockInResult> matchList = getMatchList(collect, userInfo, clockInCollect, dbClockInMap, overtimeInfoList);
            matchList.forEach(match -> {
                String mStr = match.getRuleId() + "_" + match.getClockInType();
                if (distinctSet.add(mStr)) {
                    resultList.add(match);
                }
            });
        });
        log.error("用户打卡重新匹配 - {} - 开始匹配新的打卡结果", requestId);
        attendanceClockInResultService.dbMatchDeal(requestId, resultList, userDayList, updateUserId);
        log.error("用户打卡重新匹配 - {} - DB处理完成", requestId);
        if (!overtimeInfoList.isEmpty()) {
            attendanceUserBalanceService.overTime(overtimeInfoList, tenantId);
            log.error("用户打卡重新匹配 - {} - 加班通知结束", requestId);
        }
        // 发送生日日统计通知
        String groupId = CollUtil.isNotEmpty(dailyRuleList) && Objects.nonNull(dailyRuleList.get(0)) ? dailyRuleList.get(0).getGroupId() : null;
        Map<Date, List<UserDayVo>> daysByPeriod = userDayList.stream().collect(Collectors.groupingBy(UserDayVo::getDay));
        daysByPeriod.forEach((day, list) -> {
            //调用生成日统计数据接口
            StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder()
                    .tenantId(tenantId)
                    .groupId(groupId)
                    .day(day)
                    .build();
            List<String> userIds = list.stream().map(UserDayVo::getUserId).distinct().collect(Collectors.toList());
            userIds.forEach(item -> {
                courseEventDTO.setUserId(item);
                if(StrUtil.isBlank(tenantId) || StrUtil.isBlank(groupId)){
                    log.error("AttendanceClockInServiceImpl#changeAttendanceRuleBatch 触发日统计租户ID或者考勤组ID为空：{}", JSON.toJSONString(courseEventDTO));
                    return;
                }
                Message<StatisticsSingleDto> message = MessageBuilder.withPayload(courseEventDTO).build();
                rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2);
            });
        });
        // 用于删除变更记录表的记录
        List<UserDayVo> todayList = new ArrayList<>();
        List<UserDayVo> otherList = new ArrayList<>();
        backupList.forEach(v -> {
            if (DateDetail.checkSameDay(v.getDay(), now)) {
                todayList.add(v);
            } else {
                otherList.add(v);
            }
        });
        if (!otherList.isEmpty()) {
            dailyRuleChangeMapper.removeBatch(otherList, now);
        }
        log.error("用户打卡重新匹配 - 新结果: {}", JsonUtil.getObjectToString(resultList));
        CompletableFuture.runAsync(ThreadContext.wrap(() -> {
            // 缺卡规则变更
            UserInfo user = UserProvider.getUser();
            customTenantUtil.checkOutTenant(user.getTenantId());
            userDayList.forEach(v -> {
                if (DateDetail.checkSameDay(v.getDay(), new Date())) {
                    List<FtbAttendanceDailyRule> ruleList = map.get(v.getUserId());
                    personalAbsenceTimeChange(requestId, v.getUserId(), tenantId, ruleList);
                }
            });
            dailyRuleChangeMapper.removeBatch(todayList, now);
        }, updateUser));
    }

    private List<AttendanceClockInResult> getMatchList(List<FtbAttendanceDailyRule> dailyRuleList, UserInfo userInfo, List<FtbAttendanceClockIn> clockInList,
                                                       Map<String, AttendanceClockInResult> dbClockInMap, List<OvertimeInfoVo> overtimeInfoList) {

        List<AttendanceClockInResult> resultList = new ArrayList<>();
        List<AttendanceRuleVo> ruleList = JsonUtil.getJsonToList(dailyRuleList, AttendanceRuleVo.class);
        for (AttendanceRuleVo rule : ruleList) {
            try {
                if (rule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) {
                    // 假 无需打卡
                    continue;
                }
                if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())) {
                    if (ruleList.size() > 1) {
                        continue;
                    }
                    // 休 匹配第一条和最后一条
                    if (clockInList.size() >= 1) {
                        AttendanceClockInResult result = generateClockInResultRecord(rule, clockInList.get(0),
                                dbClockInMap.get(clockInList.get(0).getId()), ConstantUtil.ON_WORK, userInfo);
                        resultList.add(result);
                    }
                    if (clockInList.size() >= 2) {
                        AttendanceClockInResult result = generateClockInResultRecord(rule, clockInList.get(clockInList.size() - 1),
                                dbClockInMap.get(clockInList.get(clockInList.size() - 1).getId()), ConstantUtil.OFF_WORK, userInfo);
                        resultList.add(result);
                    }
                    break;
                }
                if (rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) {
                    // 班 优先选择正常, 次选迟到、早退, 无 则缺卡
                    // 选择打卡记录后 移除本身和打卡时间小于此打卡记录的所有记录
                    if (!clockInList.isEmpty()) {
                        // 选择上班记录 靠近上班时间的打卡记录, 优选正常, 次选迟到
                        Date endDate = rule.getLateEnable().equals(ConstantUtil.NUM_TRUE) ? rule.getLatePoint() : rule.getInPoint();
                        endDate = setSecondTo59(endDate);
                        Map<Integer, FtbAttendanceClockIn> chooseMap = getChooseClockRecord(rule.getClockStartPoint(), endDate, clockInList);
                        if (!chooseMap.isEmpty()) {
                            dealChooseRecord(resultList, clockInList, dbClockInMap, chooseMap, ConstantUtil.MAX, rule, ConstantUtil.ON_WORK, userInfo);
                        } else {
                            // 是否允许迟到, 不允许直接缺卡
                            if (rule.getLateEnable().equals(ConstantUtil.NUM_TRUE)) {
                                // 允许迟到, 筛选迟到记录
                                Map<Integer, FtbAttendanceClockIn> lateChooseMap = getChooseClockRecord(rule.getLatePoint(), setSecondTo59(rule.getInLackPoint()), clockInList);
                                if (!lateChooseMap.isEmpty()) {
                                    dealChooseRecord(resultList, clockInList, dbClockInMap, lateChooseMap, ConstantUtil.MIN, rule, ConstantUtil.ON_WORK, userInfo);
                                } else {
                                    // 无记录则判断当前时间是否超过缺卡时间, 超过则生成缺卡记录
                                    AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, ConstantUtil.ON_WORK, rule.getInLackPoint(), userInfo);
                                    if (null != result) {
                                        resultList.add(result);
                                    } else {
                                        break;
                                    }
                                }
                            } else {
                                // 不允许迟到, 生成缺卡
                                AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, ConstantUtil.ON_WORK, rule.getInLackPoint(), userInfo);
                                if (null != result) {
                                    resultList.add(result);
                                } else {
                                    break;
                                }
                            }
                        }
                    } else {
                        // 上班缺卡
                        AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, ConstantUtil.ON_WORK, rule.getInLackPoint(), userInfo);
                        if (null != result) {
                            resultList.add(result);
                        } else {
                            break;
                        }
                    }
                    if (!clockInList.isEmpty()) {
                        // 选择下班记录 靠近下班时间的打卡记录, 优选正常, 次选早退
                        Date beginDate = rule.getOutPoint();
                        Map<Integer, FtbAttendanceClockIn> chooseMap = getChooseClockRecord(beginDate, setSecondTo59(rule.getOutLackPoint()), clockInList);
                        if (!chooseMap.isEmpty()) {
                            dealChooseRecord(resultList, clockInList, dbClockInMap, chooseMap, ConstantUtil.MIN, rule, ConstantUtil.OFF_WORK, userInfo);
                        } else {
                            // 是否允许早退, 不允许直接缺卡
                            if (rule.getEarlyEnable().equals(ConstantUtil.NUM_TRUE)) {
                                Date endPoint = rule.getOutPoint();
                                Map<Integer, FtbAttendanceClockIn> earlyChooseMap = getChooseClockRecord(rule.getEarlyPoint(), endPoint, clockInList);
                                if (!earlyChooseMap.isEmpty()) {
                                    dealChooseRecord(resultList, clockInList, dbClockInMap, earlyChooseMap, ConstantUtil.MAX, rule, ConstantUtil.OFF_WORK, userInfo);
                                } else {
                                    // 无记录则判断当前时间是否超过缺卡时间, 超过则生成缺卡记录
                                    if (new Date().after(rule.getOutLackPoint())) {
                                        AttendanceClockInResult r1 = resultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).findFirst().orElse(null);
                                        AttendanceClockInResult result = generateNoClockRecordRematch(rule, r1, ConstantUtil.OFF_WORK, rule.getOutLackPoint(), userInfo);
                                        if (null != result) {
                                            resultList.add(result);
                                        } else {
                                            break;
                                        }
                                    }
                                }
                            } else {
                                // 不允许早退, 生成缺卡
                                AttendanceClockInResult r1 = resultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).findFirst().orElse(null);
                                AttendanceClockInResult result = generateNoClockRecordRematch(rule, r1, ConstantUtil.OFF_WORK, rule.getOutLackPoint(), userInfo);
                                if (null != result) {
                                    resultList.add(result);
                                } else {
                                    break;
                                }
                            }
                        }
                    } else {
                        // 下班缺卡
                        AttendanceClockInResult r1 = resultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).findFirst().orElse(null);
                        AttendanceClockInResult result = generateNoClockRecordRematch(rule, r1, ConstantUtil.OFF_WORK, rule.getOutLackPoint(), userInfo);
                        if (null != result) {
                            resultList.add(result);
                        } else {
                            break;
                        }
                    }
                }
                if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                    Date today = new Date();
                    if (null != rule.getOutLackPoint() && !DateDetail.checkSameDay(rule.getOutLackPoint(), today) && rule.getOutLackPoint().before(today)) {
                        // 变更的过去的加班 如果加班类型是按申请时长加班, 则转存休
                        if (null != rule.getOvertimeRuleDetail() && rule.getOvertimeRuleDetail().getCalcMethod().equals(1)) {
                            overtimeInfoList.add(new OvertimeInfoVo(rule.getId(), rule.getDay(), rule.getGroupId(), rule.getOvertimeRuleDetail()));
                        }
                    }
                    // 加班 查询开始时间 结束时间
                    if (!clockInList.isEmpty()) {
                        Map<Integer, FtbAttendanceClockIn> onWorkChooseMap = getChooseClockRecord(rule.getClockStartPoint(), rule.getInPoint(), clockInList);
                        if (!onWorkChooseMap.isEmpty()) {
                            dealChooseRecord(resultList, clockInList, dbClockInMap, onWorkChooseMap, ConstantUtil.MAX, rule, ConstantUtil.ON_WORK, userInfo);
                        } else {
                            Map<Integer, FtbAttendanceClockIn> lateChooseMap = getChooseClockRecord(rule.getInPoint(), rule.getOutPoint(), clockInList);
                            if (!lateChooseMap.isEmpty()) {
                                dealChooseRecord(resultList, clockInList, dbClockInMap, lateChooseMap, ConstantUtil.MIN, rule, ConstantUtil.ON_WORK, userInfo);
                            } else {
                                AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, ConstantUtil.ON_WORK, rule.getOutPoint(), userInfo);
                                if (null != result) {
                                    resultList.add(result);
                                } else {
                                    break;
                                }
                            }
                        }
                    } else {
                        AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, ConstantUtil.ON_WORK, rule.getOutPoint(), userInfo);
                        if (null != result) {
                            resultList.add(result);
                        } else {
                            break;
                        }
                    }
                    if (!clockInList.isEmpty()) {
                        Map<Integer, FtbAttendanceClockIn> offWorkChooseMap = getChooseClockRecord(rule.getOutPoint(), rule.getOutLackPoint(), clockInList);
                        if (!offWorkChooseMap.isEmpty()) {
                            dealChooseRecord(resultList, clockInList, dbClockInMap, offWorkChooseMap, ConstantUtil.MIN, rule, ConstantUtil.OFF_WORK, userInfo);
                        } else {
                            Map<Integer, FtbAttendanceClockIn> earlyChooseMap = getChooseClockRecord(rule.getInPoint(), rule.getOutPoint(), clockInList);
                            if (!earlyChooseMap.isEmpty()) {
                                dealChooseRecord(resultList, clockInList, dbClockInMap, earlyChooseMap, ConstantUtil.MAX, rule, ConstantUtil.OFF_WORK, userInfo);
                            } else {
                                AttendanceClockInResult r1 = resultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).findFirst().orElse(null);
                                AttendanceClockInResult result = generateNoClockRecordRematch(rule, r1, ConstantUtil.OFF_WORK, rule.getOutLackPoint(), userInfo);
                                if (null != result) {
                                    resultList.add(result);
                                } else {
                                    break;
                                }
                            }
                        }
                    } else {
                        AttendanceClockInResult r1 = resultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).findFirst().orElse(null);
                        AttendanceClockInResult result = generateNoClockRecordRematch(rule, r1, ConstantUtil.OFF_WORK, rule.getOutLackPoint(), userInfo);
                        if (null != result) {
                            resultList.add(result);
                        } else {
                            break;
                        }
                    }
                }
            } catch (Exception e) {
                log.error(e.getMessage());
            }
        }
        return resultList;
    }

    public Date setSecondTo59(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.set(Calendar.SECOND, 59);
        calendar.set(Calendar.MILLISECOND, 0);
        return calendar.getTime();
    }

    /**
     * 更新指定用户的缺卡时间点
     *
     * @param userId        用户id
     * @param dailyRuleList 用户的出勤规则
     */
    private void personalAbsenceTimeChange(String requestId, String userId, String tenantId, List<FtbAttendanceDailyRule> dailyRuleList) {
        log.error("定时器缺卡任务更新 - {} - init: {}, {}", requestId, userId, LocalDateTime.now());
        Date today = new Date();
        // 查询缺卡缓存
        String todayStr = DateDetail.getDate2Str(today, DateDetail.DF);
        String hashKey = tenantId + ":" + todayStr;
        // 旷工时间更新
        absenceTimeChange(userId, today, hashKey);
        log.error("定时器缺卡任务更新 - {} - 旷工时间更新: {}, {}", requestId, userId, LocalDateTime.now());
        // 打卡提醒时间更新
        remindTimeChange(dailyRuleList, hashKey, today);
        log.error("定时器缺卡任务更新 - {} - 执行结束: {}, {}", requestId, userId, LocalDateTime.now());
    }

    private void remindTimeChange(List<FtbAttendanceDailyRule> dailyRuleList, String hashKey, Date today) {

        DateDetail dateDetail = new DateDetail(today);
        String redisKey = ConstantUtil.FTB_REMIND_KEY + hashKey;
        Map<String, List<RemindClockInVo>> remindMap = new HashMap<>();
        Set<String> userIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getUserId).collect(Collectors.toSet());
        // 查询班次用户的提醒配置
        List<UserSettingVo> settingList = attendanceUserSettingService.getSettingList(new ArrayList<>(userIds), UserSettingTypeEnum.USER.getCode(),
                Stream.of(UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE.getCode(),
                                UserSettingEnum.ATTENDANCE_SETTING_PRE_WORK_REMINDER.getCode(),
                                UserSettingEnum.ATTENDANCE_SETTING_END_WORK_REMINDER.getCode())
                        .collect(Collectors.toList()));
        ConcurrentMap<String, List<UserSettingVo>> settingMap = settingList.stream().collect(Collectors.groupingByConcurrent(UserSettingVo::getAssociationId));
        for (FtbAttendanceDailyRule rule : dailyRuleList) {
            List<UserSettingVo> userSettingList = settingMap.get(rule.getUserId());
            if (null == userSettingList || userSettingList.isEmpty()) {
                continue;
            }
            Map<String, UserSettingVo> codeMap = userSettingList.stream().collect(Collectors.toMap(UserSettingVo::getCode, Function.identity()));
            UserSettingVo receiveSetting = codeMap.get(UserSettingEnum.MESSAGE_RECEIVE);
            if (null == receiveSetting || ConstantUtil.NUM_TRUE != receiveSetting.getStatus()) {
                continue;
            }
            // 判断上班时间是否在今天
            if (null != rule.getInPoint() && DateDetail.getDate2Str(today, DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF))) {
                UserSettingVo preWorkSetting = codeMap.get(UserSettingEnum.PRE_WORK_REMINDER);
                if (null != preWorkSetting && null != preWorkSetting.getValue()) {
                    // 计算提醒时间
                    dateDetail.addMinute(rule.getInPoint(), -preWorkSetting.getValue());
                    if (new Date().before(dateDetail.getCurrentDate()) && DateDetail.getDate2Str(today, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) {
                        List<RemindClockInVo> voList = remindMap.computeIfAbsent(rule.getUserId() + ":" + rule.getId(), k -> new ArrayList<>());
                        RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2));
                        voList.add(vo);
                    }
                }
            }
            // 判断下班时间是否在今天
            if (null != rule.getOutPoint() && DateDetail.getDate2Str(today, DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF))) {
                UserSettingVo preWorkSetting = codeMap.get(UserSettingEnum.END_WORK_REMINDER);
                if (null != preWorkSetting && null != preWorkSetting.getValue()) {
                    // 计算提醒时间
                    dateDetail.addMinute(rule.getOutPoint(), preWorkSetting.getValue());
                    if (new Date().before(dateDetail.getCurrentDate()) && DateDetail.getDate2Str(today, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) {
                        List<RemindClockInVo> voList = remindMap.computeIfAbsent(rule.getUserId() + ":" + rule.getId(), k -> new ArrayList<>());
                        RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2));
                        voList.add(vo);
                    }
                }
            }
        }
        if (!remindMap.isEmpty()) {
            remindMap.forEach((k, v) -> redisUtil.insertHash(redisKey, k, JsonUtil.getObjectToString(v)));
        }
    }

    private void absenceTimeChange(String userId, Date date, String hashKey) {

        String redisKey = ConstantUtil.FTB_CLOCK_KEY + hashKey;
        Map<String, List<AbsenceClockInVo>> userMap = initAbsenceInfo2(date, userId);
        if (!userMap.isEmpty()) {
            userMap.forEach((k, v) -> {
                if (redisUtil.hasKey(redisKey, k)) {
                    v.forEach(value -> value.setV(value.getV() + 1));
                }
                redisUtil.insertHash(redisKey, k, JsonUtil.getObjectToString(v));
            });
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void outsideClockIn(String approvalCode, String tenantId, String clockInId) throws Exception {

        // 查询审批记录
        ApplyAttendanceOutside applyAttendanceOutside = applyAttendanceOutsideMapper.selectById(approvalCode);
        if (null == applyAttendanceOutside) {
            throw new QueryException("未找到审批记录");
        }
        ClockInDto clockInDto = new ClockInDto();
        clockInDto.setClockInKind(ConstantUtil.KIND_OUTSIDE);
        clockInDto.setAddress(applyAttendanceOutside.getAddress());
        clockInDto.setLng(applyAttendanceOutside.getLng());
        clockInDto.setLat(applyAttendanceOutside.getLat());
        clockInDto.setDeviceType(ConstantUtil.DEVICE_PLACE);
        clockInDto.setApprovalCode(approvalCode);
        clockInDto.setApplyType(ApplyTypeEnum.OUTSIDE);
        clockInDto.setUserId(applyAttendanceOutside.getApplyUserId());
        clockInDto.setTenantId(tenantId);
        clockInDto.setRemark(applyAttendanceOutside.getRemark());
        if (StringUtils.isEmpty(clockInId)) {
            // 打卡
            clockIn(clockInDto);
        } else {
            // 更新打卡
            updateClockIn(clockInId, clockInDto);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void approvalOutsideClockIn(String applyId, String passed, UserInfo userInfo) throws Exception {

        log.error("外勤审批 -> applyId: {}, passed: {}, userInfo: {}", applyId, passed, userInfo.getUserId());
        // 查询打卡记录
        FtbAttendanceClockIn clockIn = getClockInRecord(applyId);
        if (null == clockIn) {
            log.error("未找到打卡记录");
            throw new Exception("当天班次已变更，请联系申请人重新发起！");
        }
        LambdaQueryWrapper<AttendanceClockInResult> resultQueryWrapper = new LambdaQueryWrapper<AttendanceClockInResult>()
                .eq(AttendanceClockInResult::getClockInId, clockIn.getId());
        AttendanceClockInResult dbResult = attendanceClockInResultMapper.selectOne(resultQueryWrapper);
        if (null == dbResult) {
            log.error("未找到打卡结果");
            throw new Exception("当天班次已变更，请联系申请人重新发起！");
        }
        // 当前生效中的打卡记录
        AttendanceClockInResult currentResult = getClockInResultByRule(dbResult.getRuleId(), dbResult.getUserId(), dbResult.getClockInType(), ConstantUtil.NUM_FALSE);
        Integer status = null;
        // String title = "您发起的【外勤】审批%s";
        switch (passed) {
            case "0":
                // 拒绝, 更新打卡记录为已拒绝, 退回之前的打卡结果
                // title = String.format(title, "已驳回");
                status = ConstantUtil.STATUS_REFUSE;
                clockIn.setRelationId(null);
                clockIn.setApprovalStatus(ConstantUtil.PASS_FALSE);
                // 退回之前的打卡结果
                rollbackLastRecord(currentResult, dbResult);
                attendanceClockInMapper.updateById(clockIn);
                break;
            case "1":
                // 通过, 更新打卡记录为已通过, 关联id置为空
                // title = String.format(title, "已通过");
                status = ConstantUtil.STATUS_PASS;
                clockIn.setApprovalStatus(ConstantUtil.PASS_TRUE);
                // 查询当前生效中的打卡结果, 如果是缺卡, 回退到之前的外勤结果
                if (currentResult.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) {
                    AttendanceClockInResult clockInResult = rollbackLastRecord(currentResult, dbResult);
                    // 判断是否为旷工, 是旷工则将成对的那条记录的旷工状态去除
                    if (currentResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) {
                        Integer clockInType = dbResult.getClockInType().equals(ConstantUtil.ON_WORK) ? ConstantUtil.OFF_WORK : ConstantUtil.ON_WORK;
                        AttendanceClockInResult anotherRecord = getClockInResultByRule(dbResult.getRuleId(), dbResult.getUserId(), clockInType, ConstantUtil.NUM_FALSE);
                        if (null != anotherRecord) {
                            attendanceClockInResultMapper.updateAbsenceToNormal(anotherRecord.getId());
                        }
                    }
                    updateClockInResult(clockInResult);
                } else {
                    updateClockInResult(currentResult);
                }
                attendanceClockInMapper.updateById(clockIn);
                attendanceClockInResultMapper.updateReplyToNull(clockIn.getId(), applyId);
                break;
            case "2":
                // 撤回, 删除审批中的打卡记录, 退回之前的打卡结果
                status = ConstantUtil.STATUS_ROLLBACK;
                rollbackLastRecord(currentResult, dbResult);
                attendanceClockInMapper.deleteById(clockIn.getId());
                break;
            default:
                break;
        }
        // 更新审批记录
        LambdaUpdateWrapper<ApplyAttendanceOutside> outsideUpdate = new LambdaUpdateWrapper<ApplyAttendanceOutside>()
                .set(ApplyAttendanceOutside::getApproveUserId, userInfo.getUserId())
                .set(ApplyAttendanceOutside::getApproveTime, new Date())
                .set(ApplyAttendanceOutside::getStatus, status)
                .eq(ApplyAttendanceOutside::getId, applyId);
        ActionResult<List<UserBoundVO>> userList = v2UserApi.getAllUserInfoBatch(Stream.of(userInfo.getUserId()).collect(Collectors.toList()), null);
        if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) {
            outsideUpdate.set(ApplyAttendanceOutside::getApproveUserName, userList.getData().get(0).getUserName());
        }
        applyAttendanceOutsideMapper.update(null, outsideUpdate);
        //sendToIm(applyId, clockIn.getUserId(), title, approveUser.getUserName(), ConstantUtil.APPROVE_NOTICE_OUTSIDE, passed);
        statisticsMq(userInfo.getTenantId(), currentResult.getRuleId());
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void approvalUnusualPhoneClockIn(String applyId, String passed, UserInfo userInfo) throws Exception {
        log.error("异常审批 -> applyId: {}, passed: {}, userInfo: {}", applyId, passed, userInfo.getUserId());
        // 查询打卡记录
        FtbAttendanceClockIn clockIn = getClockInRecord(applyId);
        if (null == clockIn) {
            throw new Exception("当天班次已变更，请联系申请人重新发起！");
        }
        LambdaQueryWrapper<AttendanceClockInResult> resultQueryWrapper = new LambdaQueryWrapper<AttendanceClockInResult>()
                .eq(AttendanceClockInResult::getClockInId, clockIn.getId());
        AttendanceClockInResult dbResult = attendanceClockInResultMapper.selectOne(resultQueryWrapper);
        if (null == dbResult) {
            throw new Exception("当天班次已变更，请联系申请人重新发起！");
        }
        // 当前生效中的打卡记录
        AttendanceClockInResult currentResult = getClockInResultByRule(dbResult.getRuleId(), dbResult.getUserId(), dbResult.getClockInType(), ConstantUtil.NUM_FALSE);
        Integer status = null;
        switch (passed) {
            case "0":
                // 拒绝, 更新打卡记录为已拒绝, 退回之前的打卡结果
                status = ConstantUtil.STATUS_REFUSE;
                clockIn.setRelationId(null);
                clockIn.setApprovalStatus(ConstantUtil.PASS_FALSE);
                // 退回之前的打卡结果
                rollbackLastRecord(currentResult, dbResult);
                attendanceClockInMapper.updateById(clockIn);
                break;
            case "1":
                // 通过, 更新打卡记录为已通过, 关联id置为空
                status = ConstantUtil.STATUS_PASS;
                clockIn.setApprovalStatus(ConstantUtil.PASS_TRUE);
                // 查询当前生效中的打卡结果, 如果是缺卡, 回退到之前的异常结果
                if (currentResult.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) {
                    AttendanceClockInResult clockInResult = rollbackLastRecord(currentResult, dbResult);
                    // 判断是否为旷工, 是旷工则将成对的那条记录的旷工状态去除
                    if (currentResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) {
                        Integer clockInType = dbResult.getClockInType().equals(ConstantUtil.ON_WORK) ? ConstantUtil.OFF_WORK : ConstantUtil.ON_WORK;
                        AttendanceClockInResult anotherRecord = getClockInResultByRule(dbResult.getRuleId(), dbResult.getUserId(), clockInType, ConstantUtil.NUM_FALSE);
                        if (null != anotherRecord) {
                            attendanceClockInResultMapper.updateAbsenceToNormal(anotherRecord.getId());
                        }
                    }
                    updateClockInResult(clockInResult);
                } else {
                    updateClockInResult(currentResult);
                }
                attendanceClockInMapper.updateById(clockIn);
                attendanceClockInResultMapper.updateReplyToNull(clockIn.getId(), applyId);
                break;
            case "2":
                // 撤回, 删除审批中的打卡记录, 退回之前的打卡结果
                // title = String.format(title, "已撤回");
                status = ConstantUtil.STATUS_ROLLBACK;
                rollbackLastRecord(currentResult, dbResult);
                attendanceClockInMapper.deleteById(clockIn.getId());
                break;
            default:
                break;
        }
        // 更新审批记录
        LambdaUpdateWrapper<ApplyAttendanceViolation> violationUpdate = new LambdaUpdateWrapper<ApplyAttendanceViolation>()
                .set(ApplyAttendanceViolation::getApproveUserId, userInfo.getUserId())
                .set(ApplyAttendanceViolation::getApproveTime, new Date())
                .set(ApplyAttendanceViolation::getStatus, status)
                .eq(ApplyAttendanceViolation::getId, applyId);
        ActionResult<List<UserBoundVO>> userList = v2UserApi.getAllUserInfoBatch(Stream.of(userInfo.getUserId()).collect(Collectors.toList()), null);
        if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) {
            violationUpdate.set(ApplyAttendanceViolation::getApproveUserName, userList.getData().get(0).getUserName());
        }
        applyAttendanceViolationMapper.update(null, violationUpdate);

        statisticsMq(userInfo.getTenantId(), currentResult.getRuleId());
    }
    /**
     * 打卡通知统计
     *
     * @param tenantId   租户id
     * @param ruleId     规则id
     */
    private void statisticsMq(String tenantId, String ruleId) {
        try {
            FtbAttendanceDailyRule byId = attendanceDailyRuleService.getById(ruleId);
            if (null == byId) {
                return;
            }
            StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder()
                    .tenantId(tenantId)
                    .groupId(byId.getGroupId())
                    .userId(byId.getUserId())
                    .day(byId.getDay())
                    .build();
            Message<StatisticsSingleDto> message = MessageBuilder.withPayload(courseEventDTO).build();
            rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 2000L, 2);
        }catch (Exception e){
            log.error("打卡通知统计失败",e);
        }
    }

    /**
     * 更新打卡结果(由于班次变更为外出、出差)
     *
     * @param currentResult 打卡结果
     */
    private void updateClockInResult(AttendanceClockInResult currentResult) {
        // 查询出勤规则, 判断出勤规则是否已变更为外出、外勤
        FtbAttendanceDailyRule rule = attendanceDailyRuleMapper.selectById(currentResult.getRuleId());
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), currentResult.getClockInType())) {
            // 有效打卡时间为满勤(有效打卡时间为上班时间或下班时间), -休息时间
            if (currentResult.getClockInType().equals(ConstantUtil.ON_WORK)) {
                currentResult.setEffectiveTime(rule.getInPoint());
            } else {
                if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) {
                    int restMinute;
                    if (null == rule.getBreakStartPoint() || null == rule.getBreakEndPoint()) {
                        restMinute = 0;
                    } else {
                        restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint());
                    }
                    currentResult.setRestMinute(restMinute);
                }
                currentResult.setEffectiveTime(rule.getOutPoint());
            }
            currentResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue());
            currentResult.setAbnormalMinute(0);
            attendanceClockInResultMapper.updateById(currentResult);
        }
    }

    private AttendanceClockInResult rollbackLastRecord(AttendanceClockInResult currentResult, AttendanceClockInResult dbResult) {
        // 查询上一次打卡记录
        AttendanceClockInResult lastResult = getClockInResultByRule(dbResult.getRuleId(), dbResult.getUserId(), dbResult.getClockInType(), ConstantUtil.NUM_TRUE);
        if (null != lastResult) {
            lastResult.setDeleteMark(ConstantUtil.NUM_FALSE);
            attendanceClockInResultMapper.updateById(lastResult);
            if (StringUtils.isNotEmpty(lastResult.getClockInId())) {
                // 有打卡图片的话, 恢复打卡图片
                attendanceClockInPicService.update(new LambdaUpdateWrapper<AttendanceClockInPic>()
                        .eq(AttendanceClockInPic::getClockInId, lastResult.getClockInId())
                        .set(AttendanceClockInPic::getDeleteMark, ConstantUtil.NUM_FALSE));
            }
        }
        attendanceClockInResultMapper.deleteById(currentResult.getId());
        // 删除当前的图片
        if (StringUtils.isNotEmpty(currentResult.getClockInId())) {
            attendanceClockInPicService.remove(new LambdaQueryWrapper<AttendanceClockInPic>()
                    .eq(AttendanceClockInPic::getClockInId, currentResult.getClockInId()));
        }
        return lastResult;
    }

    private AttendanceClockInResult getClockInResultByRule(String ruleId, String userId, Integer clockInType, Integer delStatus) {

        LambdaQueryWrapper<AttendanceClockInResult> queryWrapper = new LambdaQueryWrapper<AttendanceClockInResult>()
                .eq(AttendanceClockInResult::getUserId, userId)
                .eq(AttendanceClockInResult::getRuleId, ruleId)
                .eq(AttendanceClockInResult::getClockInType, clockInType)
                .eq(AttendanceClockInResult::getDeleteMark, delStatus)
                .orderByDesc(AttendanceClockInResult::getCreatorTime)
                .last("limit 1");
        return attendanceClockInResultMapper.selectOne(queryWrapper);
    }

    private FtbAttendanceClockIn getClockInRecord(String approvalCode) {

        // 查询审批关联的打卡记录
        LambdaQueryWrapper<FtbAttendanceClockIn> queryWrapper = new LambdaQueryWrapper<FtbAttendanceClockIn>()
                .eq(FtbAttendanceClockIn::getApprovalCode, approvalCode)
                .eq(FtbAttendanceClockIn::getApprovalStatus, ConstantUtil.PASS_APPROVAL);
        return attendanceClockInMapper.selectOne(queryWrapper);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void repairClockIn(String applyId, String passed, String approveUserId, String tenantId) throws Exception {

        log.error("补卡 -> applyId: {}, passed: {}", applyId, passed);
        // 查询审批记录
        ApplyAttendanceRepair applyAttendanceRepair = applyAttendanceRepairMapper.selectById(applyId);
        if (null == applyAttendanceRepair) {
            throw new QueryException("未找到审批记录");
        }
        // 根据打卡结果id查询打卡结果
        AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(applyAttendanceRepair.getClockInResultId());
        if (null == clockInResult) {
            throw new QueryException("当天班次已变更，请联系申请人重新发起！");
        }
        int pass = Integer.parseInt(passed.substring(0, 1));
        Integer status = null;
        // 查询考勤规则
        AttendanceRuleVo rule = null;
        // String title = "您发起的【补卡】审批%s";
        switch (pass) {
            case 0:
                status = ConstantUtil.STATUS_REFUSE;
                // title = String.format(title, "已驳回");
            case 2:
                if (null == status) {
                    status = ConstantUtil.STATUS_ROLLBACK;
                    // title = String.format(title, "已撤回");
                }
                emptyApplyInfo(clockInResult.getId(), null, null);
                break;
            case 1:
                // title = String.format(title, "已通过");
                status = ConstantUtil.STATUS_PASS;
                // 查询考勤规则
                rule = getAttendanceRule(clockInResult.getRuleId());
                // 查询打卡记录
                LambdaQueryWrapper<FtbAttendanceClockIn> clockInQueryWrapper = new LambdaQueryWrapper<FtbAttendanceClockIn>()
                        .eq(FtbAttendanceClockIn::getId, clockInResult.getClockInId());
                FtbAttendanceClockIn clockIn = attendanceClockInMapper.selectOne(clockInQueryWrapper);
                if (null == clockIn) {
                    clockIn = new FtbAttendanceClockIn();
                    clockIn.setDay(rule.getDay());
                    clockIn.setUserId(clockInResult.getUserId());
                    clockIn.setApprovalStatus(ConstantUtil.PASS_TRUE);
                    clockIn.setClockInKind(ConstantUtil.KIND_NORMAL);
                    // clockIn.setDeviceType(ConstantUtil.DEVICE_PLACE);
                }
                // 判断是否封账
                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 = getGroupRule(rule.getGroupId(), clockInResult.getUserId());
                // 判断有无补卡次数
                MutablePair<Date, Date> datePair = couldRepairRecord(rule.getDay(), clockInResult, ConstantUtil.PASS_TRUE, groupRule);
                // 判断打卡记录是否在时间范围内
                if (!DateDetail.checkTimeBetween(rule.getDay(), datePair.getLeft(), datePair.getRight())) {
                    throw new QueryException("当前打卡记录不在补卡时间范围内");
                }
                // 生成新的打卡记录
                clockIn.setId(FtbUtil.getId());
                clockIn.setCreatorTime(new Date());
                // 重新绑定考勤规则
                clockInResult.setClockInId(clockIn.getId());
                if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) {
                    clockIn.setClockInTime(rule.getInPoint());
                    clockInResult.setEffectiveTime(rule.getInPoint());
                } else {
                    clockIn.setClockInTime(rule.getOutPoint());
                    clockInResult.setEffectiveTime(rule.getOutPoint());
                    // 设置打卡休息时间
                    if (null != rule.getBreakEnable() && rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) {
                        int restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint());
                        clockInResult.setRestMinute(restMinute);
                    }
                }
                clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue());
                clockInResult.setRepaired(ConstantUtil.NUM_TRUE);
                if (ConstantUtil.NUM_TRUE == clockInResult.getAbsence()) {
                    clockInResult.setAbsence(ConstantUtil.NUM_FALSE);
                    // 管理员变更的记录可以被补卡，补卡后管理员变更的标识取消，成对的那条打卡记录变更人不受影响
                    if (StringUtils.isNotEmpty(clockInResult.getAbsenceLeader())) {
                        clockInResult.setLastAbsenceLeader(clockInResult.getAbsenceLeader());
                        clockInResult.setAbsenceLeader(null);
                    }
                    // 查询另一个打卡结果, 并设置为不旷工
                    LambdaQueryWrapper<AttendanceClockInResult> queryWrapper = new LambdaQueryWrapper<AttendanceClockInResult>()
                            .eq(AttendanceClockInResult::getRuleId, clockInResult.getRuleId())
                            .eq(AttendanceClockInResult::getClockInType, clockInResult.getClockInType().equals(ConstantUtil.ON_WORK) ? ConstantUtil.OFF_WORK : ConstantUtil.ON_WORK)
                            .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE);
                    AttendanceClockInResult workRecord = attendanceClockInResultMapper.selectOne(queryWrapper);
                    if (null != workRecord) {
                        attendanceClockInResultMapper.updateAbsenceToNormal(workRecord.getId());
                    }
                }
                clockInResult.setAbsenceLeader(null);
                clockInResult.setAbnormalMinute(0);
                // 数据库操作
                // 补卡次数维护
                String applyDate = DateDetail.getDate2Str(applyAttendanceRepair.getApplyDate(), DateDetail.DF);
                AttendanceRepair repair = attendanceRepairService.getAttendanceRepairByDate(applyDate, clockInResult.getUserId(), groupRule.getSelfGroupId());
                LambdaUpdateWrapper<AttendanceRepair> updateWrapper = new LambdaUpdateWrapper<AttendanceRepair>()
                        .set(AttendanceRepair::getRepairNum, repair.getRepairNum() - 1)
                        .set(AttendanceRepair::getRepairRecord, StringUtils.isEmpty(repair.getRepairRecord()) ? clockInResult.getId() : repair.getRepairRecord() + "," + clockInResult.getId())
                        .eq(AttendanceRepair::getId, repair.getId());
                attendanceRepairService.update(null, updateWrapper);
                attendanceClockInMapper.insert(clockIn);
                attendanceClockInResultMapper.updateById(clockInResult);
                attendanceClockInResultMapper.updateResultReplyToNull(clockInResult.getId(), null);
                if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                    // 如果补的加班卡
                    attendanceUserBalanceService.overTime(List.of(new OvertimeInfoVo(rule.getId(), rule.getDay(), rule.getGroupId(), rule.getOvertimeRuleDetail())), tenantId);
                }
                break;
            default:
                break;
        }
        ActionResult<List<UserBoundVO>> userList = v2UserApi.getAllUserInfoBatch(Stream.of(approveUserId).collect(Collectors.toList()), null);
        LambdaUpdateWrapper<ApplyAttendanceRepair> repairUpdate = new LambdaUpdateWrapper<ApplyAttendanceRepair>()
                .set(ApplyAttendanceRepair::getApproveUserId, approveUserId)
                .set(ApplyAttendanceRepair::getApproveTime, new Date())
                .set(ApplyAttendanceRepair::getStatus, status)
                .eq(ApplyAttendanceRepair::getId, applyId);

        if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) {
            repairUpdate.set(ApplyAttendanceRepair::getApproveUserName, userList.getData().get(0).getUserName());
        }
        applyAttendanceRepairMapper.update(null, repairUpdate);
        //审批通过
        if(pass == 1){
            // 发布日统计数据生成事件
            StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder()
                    .tenantId(tenantId)
                    .groupId(rule.getGroupId())
                    .userId(clockInResult.getUserId())
                    .day(rule.getDay())
                    .triggerSceneEnum(TriggerSceneEnum.CHECK_IN)
                    .build();
            if(StrUtil.isBlank(tenantId) || StrUtil.isBlank(rule.getGroupId())){
                log.error("AttendanceClockInServiceImpl#repairClockIn 触发日统计租户ID或者考勤组ID为空：{}", JSON.toJSONString(courseEventDTO));
            }
            Message<StatisticsSingleDto> message = MessageBuilder.withPayload(courseEventDTO).build();
            rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2);
        }
    }

    private void sendToIm(String applyId, String sendTo, String title, String userName, String head, String passed) {

        String key = head + applyId;
        Object obj = redisUtil.getString(key);
        if (null != obj) {
            JSONObject redisJson = new JSONObject(obj);
            ApproveImVo approveImVo = redisJson.toBean(ApproveImVo.class);
            approveImVo.getUserIds().clear();
            approveImVo.getUserIds().add(sendTo);
            approveImVo.getApproveBaseImVo().setTitle(title);
            approveImVo.getApproveBaseImVo().setCreateUserName(userName);
            int pass = Integer.parseInt(passed.substring(0, 1));
            String urlParam = approveImVo.getApproveBaseImVo().getUrl().substring(AttendanceConstant.APPROVE_URL.length());
            JSONObject json = new JSONObject(urlParam);
            if (ConstantUtil.STATUS_PASS == pass) {
                json.set("formType", null);
                json.set("opType", 0);
                json.set("status", 2);
            } else {
                json.set("formType", 1);
                json.set("opType", -1);
                json.set("status", 3);
            }
            urlParam = json.toString();
            approveImVo.getApproveBaseImVo().setUrl(AttendanceConstant.APPROVE_URL + urlParam);
            attendanceNoticeHandler.send(approveImVo);
            redisUtil.remove(key);
        }
    }

    private void emptyApplyInfo(String id, String ruleId, String dealUser) {

        LambdaUpdateWrapper<AttendanceClockInResult> updateWrapper = new LambdaUpdateWrapper<AttendanceClockInResult>()
                .set(AttendanceClockInResult::getApplyId, null)
                .set(AttendanceClockInResult::getApplyType, null)
                .set(AttendanceClockInResult::getAbsenceLeader, dealUser)
                .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE);
        if (StringUtils.isNotEmpty(ruleId)) {
            updateWrapper.eq(AttendanceClockInResult::getRuleId, ruleId);
        } else {
            updateWrapper.eq(AttendanceClockInResult::getId, id);
        }
        attendanceClockInResultService.update(updateWrapper);
    }

    @Override
    public Boolean generateBeforeWorkRemind(String tenantId) {

        DateDetail dateDetail = new DateDetail();
        String now = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2);
        String todayStr = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF);
        String redisKey = ConstantUtil.FTB_REMIND_KEY + tenantId + ":" + todayStr;
        // 缓存不存在的话就生成
        if (!redisUtil.exists(redisKey)) {
            // redis不存在缺勤信息，昨天(时间段最后可能会跨天) + 今天排班可能缺卡的时间点
            Map<String, List<RemindClockInVo>> remindMap = initRemindInfo(dateDetail);
            remindMap.forEach((k, v) -> redisUtil.insertHash(redisKey, k, JsonUtil.getObjectToString(v)));
            redisUtil.expire(redisKey, ConstantUtil.EXPIRE_TIME_REDIS);
        }
        Map<String, List<RemindClockInVo>> remindMap = new HashMap<>();
        List<RemindClockInVo> needDealList = new ArrayList<>();
        // 分布式锁的key
        String lockKey = "LOCK:FTB_REMIND:" + redisKey;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
                try {
                    // 获取锁成功，执行逻辑
                    setObjectMapValue(redisKey, remindMap, RemindClockInVo.class);
                    if (remindMap.isEmpty()) {
                        return Boolean.TRUE;
                    }
                    // 判断当前时间需要提醒的记录
                    remindMap.values().forEach(values -> {
                        values.forEach(value -> {
                            if (now.equals(value.getRemindTime())) {
                                needDealList.add(value);
                            } else {
                                // 在当前时间之前 且 未被处理过的记录 且 小于上班/下班时间
                                boolean flag = DateDetail.getStr2DateTime(value.getRemindTime()).before(DateDetail.getStr2DateTime(now));
                                boolean remindFlag = DateDetail.getStr2DateTime(value.getRemindTime()).before(DateDetail.getStr2DateTime(value.getWorkTime()));
                                if (flag && value.getDeal().equals(ConstantUtil.NUM_FALSE) && remindFlag) {
                                    needDealList.add(value);
                                }
                            }
                        });
                    });
                    if (!needDealList.isEmpty()) {
                        // 发送消息
                        needDealList.forEach(needDeal -> {
                            RemindClockInNoticeModel model = new RemindClockInNoticeModel();
                            AttendanceNoticeEnum noticeEnum = needDeal.getClockInType().equals(ConstantUtil.ON_WORK) ? AttendanceNoticeEnum.CHECK_IN : AttendanceNoticeEnum.CHECK_OUT;
                            model.setAttendanceNoticeModel(needDeal.getUserId(), null, tenantId, null, noticeEnum);
                            model.setWorkTime(DateDetail.getStr2DateTime(needDeal.getWorkTime()));
                            model.setClockInType(needDeal.getClockInType());
                            attendanceNoticeHandler.send(model);
                            needDeal.setDeal(ConstantUtil.NUM_TRUE);
                        });
                        // 更新redis缓存
                        updateConditionListToRedis(
                                redisKey,
                                needDealList,
                                RemindClockInVo::getOrDefaultV,
                                vo -> vo.getUserId() + ":" + vo.getRuleId(),
                                vo -> vo.getRuleId() + "_" + vo.getClockInType(),
                                RemindClockInVo.class
                        );
                    }
                    return Boolean.TRUE;
                } finally {
                    // 结束时间
                    lock.unlock();
                }
            } else {
                log.error("定时提醒 {}- 获取分布式锁失败，可能已有其他实例在处理相同任务: {}", tenantId, lockKey);
                return Boolean.FALSE;
            }
        } catch (InterruptedException e) {
            // 恢复中断状态
            Thread.currentThread().interrupt();
            log.error("获取锁过程中被中断", e);
        }
        return true;
    }

    private Map<String, List<RemindClockInVo>> initRemindInfo(DateDetail dateDetail) {

        dateDetail.changeDay(new Date());
        Date today = dateDetail.getCurrentDate();
        dateDetail.getTomorrow();
        Date tomorrow = dateDetail.getCurrentDate();
        // 查询昨天和今天的排班信息
        Map<String, List<RemindClockInVo>> map = new HashMap<>();
        // 查询所有上/下班时间在今天的班次
        List<FtbAttendanceDailyRule> list = attendanceDailyRuleMapper.getListWorkTimeInToday(today);
        // 查询明天00:00到00:30的班次(减掉提前提醒分钟数后提醒时间可能在今天)
        String begin = DateDetail.getDate2Str(tomorrow, DateDetail.DF16);
        Date beginDate = DateDetail.getStr2DateTime(begin);
        Date endDate = dateDetail.addMinute(beginDate, DateDetail.MAX_REMIND);
        List<FtbAttendanceDailyRule> tomorrowList = attendanceDailyRuleMapper.getListByWorkTime(beginDate, endDate);
        // 今天的班次需要今天提醒的
        if (!list.isEmpty()) {
            initRemindDetail(map, list, dateDetail, today, ConstantUtil.NUM_TRUE);
        }
        // 需要在今天提醒的明天的班次
        if (!tomorrowList.isEmpty()) {
            initRemindDetail(map, tomorrowList, dateDetail, tomorrow, ConstantUtil.NUM_FALSE);
        }
        return map;
    }

    private void initRemindDetail(Map<String, List<RemindClockInVo>> map, List<FtbAttendanceDailyRule> list, DateDetail dateDetail, Date currentDay, int isToday) {
        Set<String> userIds = list.stream().map(FtbAttendanceDailyRule::getUserId).collect(Collectors.toSet());
        // 查询班次用户的提醒配置
        List<UserSettingVo> settingList = attendanceUserSettingService.getSettingList(new ArrayList<>(userIds), UserSettingTypeEnum.USER.getCode(),
                Stream.of(UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE.getCode(),
                                UserSettingEnum.ATTENDANCE_SETTING_PRE_WORK_REMINDER.getCode(),
                                UserSettingEnum.ATTENDANCE_SETTING_END_WORK_REMINDER.getCode())
                        .collect(Collectors.toList()));
        ConcurrentMap<String, List<UserSettingVo>> settingMap = settingList.stream().collect(Collectors.groupingByConcurrent(UserSettingVo::getAssociationId));
        // 根据提醒配置计算提醒时间
        for (FtbAttendanceDailyRule rule : list) {
            List<UserSettingVo> userSettingList = settingMap.get(rule.getUserId());
            if (null == userSettingList || userSettingList.isEmpty()) {
                continue;
            }
            Map<String, UserSettingVo> codeMap = userSettingList.stream().collect(Collectors.toMap(UserSettingVo::getCode, Function.identity()));
            UserSettingVo receiveSetting = codeMap.get(UserSettingEnum.MESSAGE_RECEIVE);
            if (null == receiveSetting || ConstantUtil.NUM_TRUE != receiveSetting.getStatus()) {
                continue;
            }
            // 判断上班时间是否在今天
            if (null != rule.getInPoint() && DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF))) {
                UserSettingVo preWorkSetting = codeMap.get(UserSettingEnum.PRE_WORK_REMINDER);
                if (null != preWorkSetting && null != preWorkSetting.getValue()) {
                    List<RemindClockInVo> voList = map.computeIfAbsent(rule.getUserId() + ":" + rule.getId(), k -> new ArrayList<>());
                    // 计算提醒时间
                    dateDetail.addMinute(rule.getInPoint(), -preWorkSetting.getValue());
                    // 排除计算出来的提醒时间是昨天的
                    if (ConstantUtil.NUM_TRUE == isToday && DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) {
                        RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2));
                        voList.add(vo);
                    }
                    if (ConstantUtil.NUM_FALSE == isToday && !DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) {
                        RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2));
                        voList.add(vo);
                    }
                }
            }
            // 判断下班时间是否在今天
            if (null != rule.getOutPoint() && DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF))) {
                UserSettingVo preWorkSetting = codeMap.get(UserSettingEnum.END_WORK_REMINDER);
                if (null != preWorkSetting && null != preWorkSetting.getValue()) {
                    List<RemindClockInVo> voList = map.computeIfAbsent(rule.getUserId() + ":" + rule.getId(), k -> new ArrayList<>());
                    // 计算提醒时间
                    dateDetail.addMinute(rule.getOutPoint(), -preWorkSetting.getValue());
                    // 排除计算出来的提醒时间是昨天的
                    if (ConstantUtil.NUM_TRUE == isToday && DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) {
                        RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2));
                        voList.add(vo);
                    }
                    if (ConstantUtil.NUM_FALSE == isToday && !DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) {
                        RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2));
                        voList.add(vo);
                    }
                }
            }
        }
    }

    @Override
    public Boolean generateAbsenceTask(Integer flag, String tenantId) {

        DateDetail dateDetail = new DateDetail();
        String todayStr = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF);
        // 每个租户一个独立的缺卡任务缓存
        String redisKey = ConstantUtil.FTB_CLOCK_KEY + tenantId + ":" + todayStr;
        boolean needRebuild = !redisUtil.exists(redisKey) || flag == ConstantUtil.NUM_TRUE;
        // 是否需要重新生成
        if (needRebuild) {
            redisUtil.remove(redisKey);
            log.error("定时缺卡 {}- 开始生成缺卡任务, 日期: {}", tenantId, todayStr);
            Map<String, List<AbsenceClockInVo>> absenceMap = initAbsenceInfo2(dateDetail.getCurrentDate(), null);
            absenceMap.forEach((k, v) -> redisUtil.insertHash(redisKey, k, JsonUtil.getObjectToString(v)));
            redisUtil.expire(redisKey, ConstantUtil.EXPIRE_TIME_REDIS);
        }
        return Boolean.TRUE;
    }

    @Override
    public List<GroupApprovalVo> getGroupApprovalList(ApprovalQueryDto queryDto) {

        List<GroupApprovalVo> returnList = new ArrayList<>();
        queryDto.getGroupQueryList().forEach(group -> {
            List<DailyApprovalVo> list = getApprovalList(group.getClockInList(), queryDto.getQueryDate(),
                    queryDto.getUserId(), group.getGroupId(), userProvider.get().getTenantId());
            list.removeIf(v -> !"已通过".equals(v.getLastResult()));
            returnList.add(new GroupApprovalVo(group.getGroupId(), list));
        });
        return returnList;
    }

    @Override
    public Boolean generateFtbAbsenceRecord(String tenantId) {

        DateDetail dateDetail = new DateDetail();
        String now = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2);
        String todayStr = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF);
        // 每个租户一个独立的缺卡任务缓存
        String redisKey = ConstantUtil.FTB_CLOCK_KEY + tenantId + ":" + todayStr;
        // 分布式锁的key
        String lockKey = "LOCK:FTB_ABSENCE:" + redisKey;
        RLock lock = redissonClient.getLock(lockKey);
        // 判定哪些condition需要执行
        List<AbsenceClockInVo> conditionList = new ArrayList<>();
        Map<String, List<AbsenceClockInVo>> absenceMap = new HashMap<>();
        if (!redisUtil.exists(redisKey)) {
            generateAbsenceTask(ConstantUtil.NUM_FALSE, tenantId);
        }
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                try {
                    // 获取锁成功，执行逻辑
                    // 获取缺卡任务
                    setObjectMapValue(redisKey, absenceMap, AbsenceClockInVo.class);
                    if (absenceMap.isEmpty()) {
                        return Boolean.TRUE;
                    }
                    absenceMap.forEach((k, v) -> {
                        for (AbsenceClockInVo value : v) {
                            if (value.getDeal().equals(ConstantUtil.NUM_FALSE)) {
                                // 匹配到缺卡 相等 或 小于 || 在当前时间之前 且 未被处理过的记录
                                boolean flag = DateDetail.getStr2DateTime(value.getAbsenceTime()).before(DateDetail.getStr2DateTime(now));
                                if (now.equals(value.getAbsenceTime()) || flag) {
                                    // 判断有没有打卡(包括标识为删除的), 没有, 新增缺卡, 有, 判断是否为审批中的外勤, 是, 标识为删除, 新增缺卡
                                    AbsenceClockInVo bean = JsonUtil.getJsonToBean(value, AbsenceClockInVo.class);
                                    bean.setDeal(ConstantUtil.NUM_TRUE);
                                    conditionList.add(bean);
                                }
                            }
                        }
                    });
                    if (!conditionList.isEmpty()) {
                        // 更新redis缓存 更新了部分任务为执行中
                        updateConditionListToRedis(
                                redisKey,
                                conditionList,
                                AbsenceClockInVo::getOrDefaultV,
                                vo -> vo.getUserId() + ":" + vo.getRuleId(),
                                vo -> vo.getRuleId() + "_" + vo.getClockInType(),
                                AbsenceClockInVo.class
                        );
                    }
                } finally {
                    // 结束时间
                    lock.unlock();
                }
                // log.error("定时缺卡 {}- 当前缓存的redis数据：{}", tenantId, JSONUtil.toJsonStr(absenceMap));
                AttendanceClockInService bean = context.getBean(AttendanceClockInService.class);
                bean.asyncDeal(tenantId, redisKey, conditionList, now);
                return Boolean.TRUE;
            } else {
                log.error("定时缺卡 {}- 获取分布式锁失败，可能已有其他实例在处理相同任务: {}", tenantId, lockKey);
                return Boolean.FALSE;
            }
        } catch (InterruptedException e) {
            // 恢复中断状态
            Thread.currentThread().interrupt();
            log.error("获取锁过程中被中断", e);
        }
        return Boolean.TRUE;
    }

    private <T> List<String> updateConditionListToRedis(
            String redisKey,
            List<T> conditionList,
            Function<T, Integer> mapFunc,
            Function<T, String> groupKeyFunc,
            Function<T, String> uniqueKeyFunc,
            Class<T> clazz) {

        List<String> exceptRuleIds = new ArrayList<>();
        Map<String, List<T>> map = changeConditionToMap(conditionList, groupKeyFunc);
        map.forEach((k, v) -> {
            String hashValues = redisUtil.getHashValues(redisKey, k);
            List<T> oldList;
            Integer oldVersion = v.stream().map(mapFunc).findFirst().orElse(null);
            Integer newVersion = null;
            if (StringUtil.isEmpty(hashValues)) {
                oldList = new ArrayList<>();
            } else {
                oldList = JSONUtil.toList(hashValues, clazz);
                newVersion = oldList.stream().map(mapFunc).findFirst().orElse(null);
            }
            if (oldVersion != null && newVersion != null && !oldVersion.equals(newVersion)) {
                exceptRuleIds.add(k.split(":")[1]);
            } else {
                // ===== 合并逻辑（唯一键）=====
                Map<String, T> mergeMap = new LinkedHashMap<>();
                // 1. 旧数据
                for (T vo : oldList) {
                    mergeMap.put(uniqueKeyFunc.apply(vo), vo);
                }
                // 2. 新数据覆盖旧数据
                for (T vo : v) {
                    mergeMap.put(uniqueKeyFunc.apply(vo), vo);
                }
                List<T> mergedList = new ArrayList<>(mergeMap.values());
                // ===== 合并结束 =====
                redisUtil.insertHash(redisKey, k, JsonUtil.getObjectToString(mergedList));
            }
        });
        return exceptRuleIds;
    }

    private <T> Map<String, List<T>> changeConditionToMap(
            List<T> conditionList,
            Function<T, String> groupKeyFunc) {

        return conditionList.stream()
                .collect(Collectors.groupingBy(
                        groupKeyFunc,
                        HashMap::new,
                        Collectors.toList()
                ));
    }

    private <T> void setObjectMapValue(String redisKey, Map<String, List<T>> objectMap, Class<T> clazz) {

        Map<Object, Object> map = redisUtil.getMap(redisKey);
        if (map == null || map.isEmpty()) {
            return;
        }

        map.forEach((k, v) -> {
            List<T> list = JSONUtil.toList(v.toString(), clazz);
            objectMap.put(k.toString(), list);
        });
    }

    @Async
    @Transactional(rollbackFor = Exception.class)
    @Override
    public CompletableFuture<Void> asyncDeal(String tenantId, String redisKey, List<AbsenceClockInVo> conditionList, String now) {
        log.info("init...  ：{}", LocalDate.now());
        List<AttendanceClockInResult> addList = new ArrayList<>();
        List<ConditionVo> sqlConditionList = new ArrayList<>();
        List<String> delList = new ArrayList<>();
        // 储存加班下班的通知
        List<OvertimeInfoVo> overtimeInfoList = new ArrayList<>();
        // 排班已变更导致本次执行不入库的RuleIds
        List<String> exceptRuleIds = new ArrayList<>();
        Map<String, AttendanceRuleVo> ruleMap;
        if (conditionList.isEmpty()) {
            ruleMap = new HashMap<>();
        } else {
            List<String> ruleIds = conditionList.stream().map(AbsenceClockInVo::getRuleId).distinct().collect(Collectors.toList());
            ruleMap = getAttendanceRuleBatch(ruleIds);
            log.info("获取到所有出勤规则...  ：{}", LocalDateTime.now());
            List<AbsenceClockInVo> changeList = new ArrayList<>();
            // 查询用户打卡记录
            try {
                List<AttendanceClockInResult> resultList = attendanceClockInResultMapper.selectUserClockInHistory(now);
                log.info("获取到所有打卡记录...  ：{}", LocalDateTime.now());
                Map<String, List<AttendanceClockInResult>> resultMap = resultList.stream().filter(v -> null != v.getApprovalStatus()).collect(Collectors.groupingBy(
                        r -> r.getUserId() + ":" + r.getRuleId() + ":" + r.getClockInType()
                ));
                log.info("开始循环...  ：{}", LocalDateTime.now());
                for (AbsenceClockInVo condition : conditionList) {
                    List<AttendanceClockInResult> collectList = resultMap.getOrDefault(
                            condition.getUserId() + ":" + condition.getRuleId() + ":" + condition.getClockInType(),
                            Collections.emptyList()
                    );
                    AttendanceRuleVo rule = ruleMap.get(condition.getRuleId());
                    if (null == rule) {
                        log.error("定时缺卡 {}- 查询用户{}出勤规则失败: {}", tenantId, condition.getUserId(), condition.getRuleId());
                        // 更新为已完成
                        condition.setDeal(ConstantUtil.NUM_FINISH);
                        changeList.add(condition);
                        continue;
                    }
                    UserInfo userInfo = new UserInfo();
                    userInfo.setUserId(condition.getUserId());
                    // 是否需要生成缺卡
                    if (needAddNoClockInRecord(collectList, delList)) {
                        addNoClockInRecord(rule, condition.getClockInType(), userInfo, addList, sqlConditionList);
                    }
                    // 如果是加班下班, 添加到通知中 如果 overtimeRuleDetail 为空, 说明加班出勤规则有问题
                    if (condition.getClockInType().equals(ConstantUtil.OFF_WORK) && rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                        if (null == rule.getOvertimeRuleDetail()) {
                            log.error("加班出勤规则获取不到加班规则, ruleId：{}", rule.getId());
                        } else {
                            OvertimeInfoVo overtimeInfo = new OvertimeInfoVo(rule.getId(), rule.getDay(), rule.getGroupId(), rule.getOvertimeRuleDetail());
                            overtimeInfoList.add(overtimeInfo);
                        }
                    }
                    // 设置为已处理
                    condition.setDeal(ConstantUtil.NUM_FINISH);
                    changeList.add(condition);
                }
                log.info("循环结束...  ：{}", LocalDateTime.now());
            } catch (Exception e) {
                log.info("定时缺卡 {}- 执行报错, message: {}", tenantId, e.getMessage());
            }
            if (!changeList.isEmpty()) {
                List<String> ids = updateConditionListToRedis(
                        redisKey,
                        changeList,
                        AbsenceClockInVo::getOrDefaultV,
                        vo -> vo.getUserId() + ":" + vo.getRuleId(),
                        vo -> vo.getRuleId() + "_" + vo.getClockInType(),
                        AbsenceClockInVo.class
                );
                exceptRuleIds.addAll(ids);
                log.info("更新redis...  ：{}", LocalDateTime.now());
            }
        }
        // 由于redis缺卡任务变更导致出勤规则被排除(ruleId未改变, 缺卡时间改变)
        if (!exceptRuleIds.isEmpty()) {
            delList.removeIf(exceptRuleIds::contains);
            addList.removeIf(v -> exceptRuleIds.contains(v.getRuleId()));
            sqlConditionList.removeIf(v -> exceptRuleIds.contains(v.getRuleId()));
        }
        if (!delList.isEmpty()) {
            attendanceClockInResultMapper.updateBatchToDel(delList);
            log.info("数据库删除操作...  ：{}", LocalDateTime.now());
        }
        if (!addList.isEmpty()) {
            if (!sqlConditionList.isEmpty()) {
                // 查询需要更新为缺勤的打卡结果
                List<ConditionVo> updateResultList = attendanceClockInResultMapper.selectRuleIdsForAbsence(sqlConditionList);
                if (!updateResultList.isEmpty()) {
                    List<String> updateResultIds = updateResultList.stream().map(ConditionVo::getResultId).distinct().collect(Collectors.toList());
                    attendanceClockInResultMapper.updateToAbsenceBatch(updateResultIds);
                    Set<String> updateRuleIds = updateResultList.stream().map(ConditionVo::getRuleId).collect(Collectors.toSet());
                    addList.forEach(v -> {
                        if (updateRuleIds.contains(v.getRuleId())) {
                            v.setAbsence(ConstantUtil.NUM_TRUE);
                        }
                    });
                }
            }
            // 查询数据库已存在的数据, 并排除
            List<AttendanceClockInResult> existList = attendanceClockInResultMapper.selectExistRecord(addList, ConstantUtil.NUM_TRUE);
            List<String> existIds = new ArrayList<>();
            if (!existList.isEmpty()) {
                existList.forEach(v -> {
                    addList.stream().filter(add -> add.getRuleId().equals(v.getRuleId()) && add.getUserId().equals(v.getUserId())
                            && add.getClockInType().equals(v.getClockInType())).findFirst().ifPresent(exist -> existIds.add(exist.getId()));
                });
            }
            if (!existIds.isEmpty()) {
                addList.removeIf(v -> existIds.contains(v.getId()));
            }
            // 用于防止上下班同时生成缺卡时, 数据库查不到另外一条记录, 导致只缺卡不旷工(加班不旷工)
            Map<String, List<AttendanceClockInResult>> resultByRuleMap = addList.stream().collect(Collectors.groupingBy(AttendanceClockInResult::getRuleId));
            resultByRuleMap.forEach((ruleId, list) -> {
                if (list.size() != 2) {
                    return;
                }
                for (AttendanceClockInResult v : list) {
                    if (!AttendanceTypeEnum.WORKOVERTIME.getCode().equals(v.getAttendanceType())) {
                        v.setAbsence(ConstantUtil.NUM_TRUE);
                    }
                }
            });
            attendanceClockInResultService.saveBatch(addList);
            // 根据出勤规则判断哪些是缺卡
            List<AttendanceClockInResult> noClockList = addList.stream().filter(v -> v.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())).collect(Collectors.toList());
            if (!noClockList.isEmpty()) {
                // 缺卡/旷工 发送消息
                noClockList.forEach(add -> {
                    AttendanceRuleVo rule = ruleMap.get(add.getRuleId());
                    if (null != rule) {
                        NoClockInNoticeModel model = new NoClockInNoticeModel();
                        AttendanceNoticeEnum noticeEnum = getNoticeEnum(add.getClockInType(), add.getAbsence());
                        model.setAttendanceNoticeModel(add.getUserId(), rule.getGroupId(), tenantId, add.getDay(), noticeEnum);
                        model.setAbsence(add.getAbsence());
                        model.setClockInType(add.getClockInType());
                        model.setOnWorkTime(rule.getInPoint());
                        model.setOffWorkTime(rule.getOutPoint());
                        model.setClockInResultId(add.getId());
                        attendanceNoticeHandler.send(model);
                    }
                });
            }
            // 加班通知
            if (!overtimeInfoList.isEmpty()) {
                log.error("发送加班通知消息：{}", overtimeInfoList);
                rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_OVERTIME_TOPIC, new OvertimeEvent(tenantId, overtimeInfoList));
            }
            //调用批量生成日统计数据接口
            Map<String, List<AttendanceClockInResult>> groupMap = addList.stream().collect(Collectors.groupingBy(AttendanceClockInResult::getGroupId));
            groupMap.forEach((groupId, inResultList) -> {
                Map<Date, List<AttendanceClockInResult>> dayMap = inResultList.stream().collect(Collectors.groupingBy(AttendanceClockInResult::getDay));
                dayMap.forEach((day, list) -> {
                    //调用生成日统计数据接口
                    StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder()
                            .tenantId(tenantId)
                            .groupId(groupId)
                            .day(day)
                            .triggerSceneEnum(TriggerSceneEnum.CHECK_IN)
                            .build();
                    List<String> userIds = list.stream().map(AttendanceClockInResult::getUserId).distinct().collect(Collectors.toList());
                    userIds.forEach(item ->{
                        courseEventDTO.setUserId(item);
                        if(StrUtil.isBlank(tenantId) || StrUtil.isBlank(groupId)){
                            log.error("AttendanceClockInServiceImpl#asyncDeal 触发日统计租户ID或者考勤组ID为空：{}", JSON.toJSONString(courseEventDTO));
                            return;
                        }
                        Message<StatisticsSingleDto> message = MessageBuilder.withPayload(courseEventDTO).build();
                        rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2);
                    });
                });
            });
        }
        log.info("执行结束...  ：{}", LocalDateTime.now());
        return CompletableFuture.completedFuture(null);
    }

    private boolean needAddNoClockInRecord(List<AttendanceClockInResult> collectList, List<String> delList) {

        // 没有任何打卡记录
        if (collectList.isEmpty()) {
            return true;
        }
        // 判断有没有审批中的打卡记录, 标记为删除
        AttendanceClockInResult approvalResult = collectList.stream()
                .filter(v ->
                        v.getApprovalStatus().equals(ConstantUtil.PASS_APPROVAL)
                        && v.getDeleteMark().equals(ConstantUtil.NUM_FALSE)
                )
                .findFirst()
                .orElse(null);
        // 有审批中的记录 同一时间只有一个记录，在审批中，被删除之后就没有打卡记录了，所以需要缺卡
        if (approvalResult != null) {
            delList.add(approvalResult.getId());
            return true;
        }
        // 有打卡记录，但全部是删除状态
        long deletedCount = collectList.stream()
                .filter(v -> v.getDeleteMark().equals(ConstantUtil.NUM_TRUE))
                .count();
        return deletedCount == collectList.size();
    }

    private AttendanceNoticeEnum getNoticeEnum(Integer clockInType, Integer absence) {

        if (null != absence && ConstantUtil.NUM_TRUE == absence) {
            return AttendanceNoticeEnum.ABSENCE;
        } else if (ConstantUtil.ON_WORK.equals(clockInType)) {
            return AttendanceNoticeEnum.IN_MISSING;
        } else {
            return AttendanceNoticeEnum.OUT_MISSING;
        }
    }

    private void addNoClockInRecord(AttendanceRuleVo rule, Integer clockInType, UserInfo userInfo, List<AttendanceClockInResult> addList, List<ConditionVo> conditionList) {

        MutablePair<AttendanceClockInResult, ConditionVo> pair = generateNoClockAndCondition(rule, clockInType, userInfo);
        pair.getLeft().setGroupId(rule.getGroupId());
        pair.getLeft().setDay(rule.getDay());
        addList.add(pair.getLeft());
        if (null != pair.getRight()) {
            conditionList.add(pair.getRight());
        }
    }

    @Override
    public List<GroupRepairVo> getRepairList(String userId) throws Exception {

        List<GroupRepairVo> returnList = new ArrayList<>();
        DateDetail dateDetail = new DateDetail();
        int year = dateDetail.getYear();
        int month = dateDetail.getMonth();
        int day = dateDetail.getDay();
        Date currentDate = dateDetail.getCurrentDate();
        Date beginDate = dateDetail.addNum(dateDetail.getCurrentDate(), -60, Calendar.DATE);
        // 查询用户本月所在的考勤组
        List<AttendanceGroupUser> groupUserList = attendanceUserService.getAttendanceGroupUsersOfSecondment(beginDate, currentDate, List.of(userId), null);
        if (null == groupUserList || groupUserList.isEmpty()) {
            return returnList;
        }
        List<String> groupIds = groupUserList.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList());
        // 用于封账判定
        Map<String, Boolean> monthMap = new HashMap<>();
        // 考勤组打卡列表
        for (String groupId : groupIds) {
            // 查询考勤组规则
            GroupRuleVo groupRule = getGroupRule(groupId, userId);
            if (null == groupRule) {
                throw new HandleException("请先配置考勤组规则");
            }
            GroupRepairVo groupRepair = new GroupRepairVo();
            groupRepair.setGroupId(groupId);
            groupRepair.setGroupName(attendanceGroupMapper.getGroupName(groupId));
            if (StringUtils.isEmpty(groupRule.getPatchType())) {
                groupRepair.setMessage(groupRepair.getMessage() + "当前考勤组暂无补卡类型. ");
            }
            if (null == groupRule.getPatchCycleDay()) {
                groupRepair.setMessage(groupRepair.getMessage() + "当前考勤组未设置补卡周期天数. ");
            }
            // 查询每个考勤组是否可以补卡, 已补卡次数
            if (null == groupRule.getPatchClockStatus() || groupRule.getPatchClockStatus().equals(ConstantUtil.NUM_FALSE)) {
                groupRepair.setMessage(groupRepair.getMessage() + "当前考勤组不允许补卡. ");
            }
            // 查询补卡刷新日
            if (null == groupRule.getPatchRefreshDay()) {
                groupRepair.setMessage(groupRepair.getMessage() + "当前考勤组未设置补卡次数刷新日. ");
            }
            // 查询补卡刷新次数
            AttendanceRepair repair = attendanceRepairService.getAttendanceRepair(userId, groupId);
            if (null == repair) {
                // 当月补卡次数未生成
                log.error("当月补卡次数未生成 -> 月份: {}, user: {}, groupId: {}", dateDetail.getMonth(), userId, groupRule.getSelfGroupId());
                LocalDate now = LocalDate.now();
                Date expireDate = dateDetail.changeMonthAndDate(currentDate, 1, groupRule.getPatchRefreshDay());
                repair = new AttendanceRepair(groupRule.getSelfGroupId(), userId, year, month, day,
                        groupRule.getPatchClockNumber(), expireDate);
                attendanceRepairService.save(repair);
            } else if (repair.getRepairNum() == 0) {
                groupRepair.setMessage(groupRepair.getMessage() + "您的补卡次数已用完. ");
            } else {
                String startDate = String.format("%d-%02d-%02d", repair.getGenerateYear(), repair.getGenerateMonth(), repair.getGenerateDay());
                String endDate = DateDetail.getDate2Str(repair.getExpireDate(), DateDetail.DF);
                int applyCount = attendanceClockInResultMapper.selectRepairApplyCount(userId, groupId, startDate, endDate);
                if (repair.getRepairNum() - applyCount <= 0) {
                    groupRepair.setMessage(groupRepair.getMessage() + "您的补卡次数已用完. ");
                }
                repair.setRepairNum(repair.getRepairNum() - applyCount);
            }
            // 还有剩余次数, 返回考勤组时间段内可以补卡的异常打卡记录
            dateDetail.changeDay(currentDate);
            Date queryBeginDate = dateDetail.addNum(dateDetail.getCurrentDate(), -groupRule.getPatchCycleDay(), Calendar.DATE);
            MutablePair<Integer, List<Integer>> queryPair = getQueryPair(groupRule.getPatchType());
            List<AbnormalClockInVo> clockInList = attendanceClockInMapper.getAbnormalClockInList(userId, groupId, queryBeginDate, currentDate, queryPair.getLeft(), queryPair.getRight());
            if (!clockInList.isEmpty()) {
                if (StringUtils.isNotEmpty(groupRepair.getMessage())) {
                    returnList.add(groupRepair);
                    continue;
                }
                assert repair != null;
                groupRepair.setUsedTimes(repair.getTotalNum() - repair.getRepairNum());
                groupRepair.setResidueTimes(repair.getRepairNum());
                // 有变更人的记录需要查出变更人名字
                List<String> leaderList = clockInList.stream().filter(v -> StringUtils.isNotEmpty(v.getAbsenceLeader())).map(AbnormalClockInVo::getAbsenceLeader).collect(Collectors.toList());
                Map<String, UserBoundVO> userMap = new HashMap<>();
                if (!leaderList.isEmpty()) {
                    ActionResult<List<UserBoundVO>> userList = v2UserApi.getAllUserInfoBatch(leaderList, null);
                    if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) {
                        userMap = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity()));
                    }
                }
                for (AbnormalClockInVo clockIn : clockInList) {
                    if (StringUtils.isNotEmpty(clockIn.getAbsenceLeader())) {
                        UserBoundVO user = userMap.get(clockIn.getAbsenceLeader());
                        if (null != user) {
                            clockIn.setAbsenceLeaderName(user.getUserName());
                        }
                    }
                    // 判定是否封账
                    String monthDate = DateDetail.getDate2Str(clockIn.getDay(), DateDetail.DF15);
                    Boolean b = monthMap.get(monthDate);
                    if (null == b) {
                        Map<String, Boolean> map = attendanceDayStatisticsService.selectUserIsSeal(List.of(userId), monthDate);
                        monthMap.put(monthDate, map.get(userId));
                    }
                    if (monthMap.get(monthDate)) {
                        // true 已封账 不可操作
                        clockIn.setEnable(ConstantUtil.NUM_FALSE);
                    } else {
                        clockIn.setEnable(ConstantUtil.NUM_TRUE);
                    }
                }
                // 考勤组锁定的打卡记录不返回
                clockInList = clockInList.stream().filter(v -> v.getEnable().equals(ConstantUtil.NUM_TRUE)).collect(Collectors.toList());
                groupRepair.setClockInList(clockInList);
            }
            returnList.add(groupRepair);
        }
        return returnList;
    }

    private MutablePair<Integer, List<Integer>> getQueryPair(String patchType) {

        List<Integer> queryList = new ArrayList<>();
        int b = 0;
        if (patchType.contains("1")) {
            queryList.add(ClockInStatusEnum.WORK_LATE.getValue());
        }
        if (patchType.contains("2")) {
            queryList.add(ClockInStatusEnum.HOME_EARLY.getValue());
        }
        if (patchType.contains("3")) {
            queryList.add(ClockInStatusEnum.NO_CLOCK.getValue());
        }
        if (patchType.contains("4")) {
            b = 1;
        }
        if (queryList.isEmpty()) {
            queryList.add(ClockInStatusEnum.UNUSED.getValue());
        }
        return MutablePair.of(b, queryList);
    }

    @Override
    public DailyInfoVo getDailyClockInRecordV2(String userId, String queryDate, String currentGroupId, Integer queryOldData) throws QueryException {

        DailyInfoVo dailyInfoVo = new DailyInfoVo();
        Date date = DateDetail.getStr2Date(queryDate);
        // 查询基本信息
        StaffRosterListReq req = new StaffRosterListReq();
        req.setPageSize(-1);
        req.setIsQueryAuth(ConstantUtil.STR_FALSE);
        req.setUserIds(List.of(userId));
        ActionResult<PageListVO<FtbPersonnelsStaffRosterDto>> page = rosterManagerApi.postWithSalary(req);
        if (null == page || !page.getCode().equals(200) || null == page.getData() || CollUtil.isEmpty(page.getData().getList())) {
            throw new QueryException("查询当前用户信息失败...");
        }
        FtbPersonnelsStaffRosterDto dto = page.getData().getList().get(0);
        DailyBaseInfoVo baseInfo = setDailyBaseInfoNew(date, dto);
        // 查询考勤组信息
        List<MiniGroupVo> miniGroupList = attendanceGroupUserMapper.getGroupUserList(Stream.of(currentGroupId).collect(Collectors.toSet()), userId, ConstantUtil.DEL);
        if (null == miniGroupList || miniGroupList.isEmpty()) {
            throw new QueryException("查询考勤组信息失败...");
        }
        // 查询考勤组信息
        String groupName = attendanceGroupMapper.getGroupName(currentGroupId);
        baseInfo.setGroupId(currentGroupId);
        baseInfo.setGroupName(groupName);
        // 查询班次信息
        List<SchedulesItemVo> currentDateShift = new ArrayList<>();
        dailyInfoVo.setBaseInfo(baseInfo);
        // 查询考勤详情
        // 打卡列表
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(userId);
        // 查询当日出勤规则
        List<FtbAttendanceDailyRule> dailyRuleList = getCurrentDailyRuleListOfWithdraw(DateDetail.getStr2Date(queryDate), userInfo);
        List<String> shiftIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getShiftId).collect(Collectors.toList());
        Map<String, AttendanceShiftNameEntity> shiftMap;
        if (!shiftIds.isEmpty()) {
            shiftMap = attendanceDailyRuleService.getShiftByShiftIds(shiftIds);
        } else {
            shiftMap = new HashMap<>();
        }
        boolean isContainsOrdinary = dailyRuleList.stream().anyMatch(dayRule -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), dayRule.getAttendanceType()));
        dailyRuleList.forEach(rule -> {
            AttendanceShiftNameEntity shift = shiftMap.get(rule.getShiftId());
            SchedulesItemVo item = setItemInfo(shift, rule, isContainsOrdinary);
            if (Objects.nonNull(item) && rule.getGroupId().equals(currentGroupId)) {
                currentDateShift.add(item);
            }
        });
        List<String> allRuleIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList());
        // 需要展示打卡记录的出勤规则
        dailyRuleList = dailyRuleList.stream().filter(v -> v.getInPoint() != null && v.getOutPoint() != null)
                .sorted(Comparator.comparing(FtbAttendanceDailyRule::getInPoint))
                .collect(Collectors.toList());
        List<String> ruleIdList = dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList());
        DailyClockInDetailVo clockInDetail = new DailyClockInDetailVo();
        if (!dailyRuleList.isEmpty()) {
            // 查询出勤规则关联的打卡记录
            List<ClockInVo> clockInResultList = attendanceClockInMapper.getClockInResultByRuleList(ruleIdList);
            // 根据请假规则判断顺序 上班、下班、加班上班、加班下班、请假离岗、请假回岗、借调离岗、借调回岗
            for (FtbAttendanceDailyRule rule : dailyRuleList) {
                if (rule.getAttendanceType().equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode())
                        || rule.getAttendanceType().equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())
                        || rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())
                        || rule.getAttendanceType().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode())
                        || rule.getAttendanceType().equals(AttendanceTypeEnum.STEP_OUT.getCode())) {
                    continue;
                }
                if (rule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) {
                    // 请假 生成请假记录 index++
                    DailyClockInVo v1 = getDailyClockInVo(null, rule, 0, null);
                    setLastResult(v1, rule);
                    DailyClockInVo v2 = getDailyClockInVo(null, rule, 1, null);
                    setLastResult(v2, rule);
                    clockInDetail.getClockInList().add(v1);
                    clockInDetail.getClockInList().add(v2);
                    continue;
                }
                List<ClockInVo> collect = clockInResultList.stream().filter(v -> rule.getId().equals(v.getRuleId()))
                        .sorted(Comparator.comparing(ClockInVo::getClockInType)).collect(Collectors.toList());
                String sortStr = null;
                for (int i = 0; i < 2; i++) {
                    ClockInVo clockIn;
                    try {
                        if (i == 0) {
                            clockIn = collect.stream().filter(v -> ConstantUtil.ON_WORK.equals(v.getClockInType())).findFirst().orElse(null);
                            if (rule.getInUnbounded().equals(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode())) {
                                sortStr = "借调回岗";
                            }
                        } else {
                            clockIn = collect.stream().filter(v -> ConstantUtil.OFF_WORK.equals(v.getClockInType())).findFirst().orElse(null);
                            if (rule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode())) {
                                sortStr = "借调离岗";
                            }
                        }
                    } catch (Exception e) {
                        clockIn = null;
                    }
                    DailyClockInVo dailyClockIn = getDailyClockInVo(clockIn, rule, i, miniGroupList.get(0).getNeedApproval());
                    if (null != sortStr) {
                        dailyClockIn.setSortStr(sortStr);
                        sortStr = null;
                    }
                    clockInDetail.getClockInList().add(dailyClockIn);
                }
            }
        }
        // 添加请假记录
        if (!clockInDetail.getClockInList().isEmpty()) {
            ConcurrentMap<String, List<DailyClockInVo>> shiftClockMap = clockInDetail.getClockInList().stream().filter(v -> StringUtils.isNotEmpty(v.getShiftId())).collect(Collectors.groupingByConcurrent(DailyClockInVo::getShiftId));
            // 处理请假逻辑
            shiftClockMap.forEach((k, v) -> {
                ConcurrentMap<String, List<DailyClockInVo>> shiftTimeMap = v.stream().collect(Collectors.groupingByConcurrent(DailyClockInVo::getShiftTimeStr));
                shiftTimeMap.forEach((time, list) -> {
                    if (list.size() > 1) {
                        DailyClockInVo vo1 = list.get(0);
                        DailyClockInVo vo2 = list.get(1);
                        String v1LastResult = StringUtils.isNotEmpty(vo1.getLastResult()) ? vo1.getLastResult() : "";
                        String v2LastResult = StringUtils.isNotEmpty(vo2.getLastResult()) ? vo2.getLastResult() : "";
                        if ((ConstantUtil.LEAVE_STR.equals(v1LastResult) && !ConstantUtil.LEAVE_STR.equals(v2LastResult) && ConstantUtil.ON_WORK_STR.equals(vo2.getSortStr()))
                                || (ConstantUtil.ON_WORK_STR.equals(vo1.getSortStr()) && !ConstantUtil.LEAVE_STR.equals(v1LastResult) && ConstantUtil.LEAVE_STR.equals(v2LastResult))) {
                            // 请假 + 上班 = 请假回岗
                            setLeaveValue(vo1, ConstantUtil.BACK_WORK_STR);
                            setLeaveValue(vo2, ConstantUtil.BACK_WORK_STR);
                        }
                        if ((ConstantUtil.LEAVE_STR.equals(v1LastResult) && !ConstantUtil.LEAVE_STR.equals(v2LastResult) && ConstantUtil.OFF_WORK_STR.equals(vo2.getSortStr()))
                                || (ConstantUtil.OFF_WORK_STR.equals(vo1.getSortStr()) && !ConstantUtil.LEAVE_STR.equals(v1LastResult) && ConstantUtil.LEAVE_STR.equals(v2LastResult))) {
                            // 请假 + 下班 = 请假离岗
                            setLeaveValue(vo1, ConstantUtil.LEAVE_WORK_STR);
                            setLeaveValue(vo2, ConstantUtil.LEAVE_WORK_STR);
                        }
                    }
                });
            });
            clockInDetail.getClockInList().removeIf(DailyClockInVo::isNeedDel);
            clockInDetail.getClockInList().forEach(v -> {
                // 如果最终结果为外出或出差, 则设置最终结果为外出或出差
                if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(v.getApplyViewEnable())) {
                    v.setLastResult(AttendanceTypeEnum.BUSINESS_TRIP.getMsg());
                }
                // 外出判定变更为 判断上/下班是否有外出标识
                if (getOutsideCheck(new AttendanceRuleVo(v.getApplyViewEnable(), v.getOutStepOutType(), v.getInStepOutType()), v.getClockInType())) {
                    v.setLastResult(AttendanceTypeEnum.STEP_OUT.getMsg());
                }
            });
        }
        // 移除非当前考勤组的打卡信息
        clockInDetail.getClockInList().removeIf(v -> !currentGroupId.equals(v.getGroupId()));
        if (queryOldData.equals(ConstantUtil.NUM_TRUE)) {
            // 查看原始数据
            setOldData(clockInDetail, allRuleIds, ruleIdList, userId, queryDate);
        }
        currentDateShift.stream().filter(vo -> StringUtil.isNotEmpty(vo.getName())).collect(Collectors.groupingBy(SchedulesItemVo::getName)).forEach((name, items) -> {
            if (StringUtil.isEmpty(name) || CollUtil.isEmpty(items) || items.size() < 2) {
                return;
            }
            items.stream().skip(1).forEach(lastItemVo -> {
                lastItemVo.setName("");
                lastItemVo.setShortName("");
                lastItemVo.setIsShow(0);
            });
        });
        // 设置班次信息
        dailyInfoVo.getBaseInfo().getCurrentDateShift().addAll(currentDateShift);
        // 查询审批记录
        List<DailyApprovalVo> approvalList = getApprovalList(clockInDetail.getClockInList(), queryDate, userId, currentGroupId, userProvider.get().getTenantId());
        clockInDetail.getApprovalList().addAll(approvalList);
        // 统计数据
        LatticeStatisticsVo latticeStatistics = attendanceDayStatisticsService.getLatticeStatistics(new LatticeStatisticsVoDto(currentGroupId, userId, date));
        clockInDetail.setTotalData(latticeStatistics);
        List<String> clockIds = clockInDetail.getClockInList().stream().map(DailyClockInVo::getClockInId).filter(StringUtils::isNoneBlank).distinct().collect(Collectors.toList());
        Map<String, List<AttendanceClockInPic>> clockInPicMap = CollUtil.isEmpty(clockIds) ? new HashMap<>() : attendanceClockInPicService.getClockInPicByIds(clockIds, null);
        if (CollUtil.isNotEmpty(clockInDetail.getClockInList())) {
            clockInDetail.getClockInList().forEach(v -> {
                if (StringUtil.isNotEmpty(v.getClockInId()) && clockInPicMap.containsKey(v.getClockInId())) {
                    v.setFieldPersonnelPic(!clockInPicMap.get(v.getClockInId()).isEmpty() ? 1 : 0);
                    if (v.getFieldPersonnelPic().equals(ConstantUtil.NUM_TRUE)) {
                        List<String> picList = clockInPicMap.getOrDefault(v.getClockInId(), List.of())
                                .stream()
                                .map(AttendanceClockInPic::getPicUrl)
                                .collect(Collectors.toList());
                        v.setPicList(picList);
                    }
                }
            });
        }
        dailyInfoVo.setClockInDetail(clockInDetail);
        // 处理打卡结果展示审批备注问题
        if (!CollUtil.isEmpty(clockInDetail.getApprovalList())) {
            // 通过审批类型分组
            Map<String, DailyApprovalVo> idMap = clockInDetail.getApprovalList().stream().collect(Collectors.toMap(DailyApprovalVo::getTaskId, v -> v));
            if (!CollUtil.isEmpty(clockInDetail.getClockInList())) {
                clockInDetail.getClockInList().forEach(v -> {
                    if (null != v.getLastResult() && !v.getLastResult().isEmpty()) {
                        // 判断 请假、加班、外出、出差
                        if ("请假".equals(v.getLastResult()) || "加班".equals(v.getLastResult())) {
                            DailyApprovalVo dailyApprovalVo = idMap.get(v.getAssociationApplyId());
                            if (null != dailyApprovalVo) {
                                v.setRemark(dailyApprovalVo.getReason());
                            }
                        }
                        if ("外出".equals(v.getLastResult())) {
                            DailyApprovalVo dailyApprovalVo = idMap.get(v.getStepOutApplyId());
                            if (null != dailyApprovalVo) {
                                v.setRemark(dailyApprovalVo.getReason());
                            }
                        }
                    }
                    if ("出差".equals(v.getLastResult())) {
                        clockInDetail.getApprovalList().stream().filter(item -> item.getApprovalName().contains("出差申请")).findFirst().ifPresent(dailyApprovalVo -> v.setRemark(dailyApprovalVo.getReason()));
                    }
                });
            }
        }

        // 上一天/下一天
        MutablePair<Integer, Integer> pair = setLastAndNext(date, userId, currentGroupId);
        dailyInfoVo.setHasLast(pair.getLeft());
        dailyInfoVo.setHasNext(pair.getRight());
        return dailyInfoVo;
    }

    private DailyBaseInfoVo setDailyBaseInfoNew(Date date, FtbPersonnelsStaffRosterDto userBound) {

        DailyBaseInfoVo dailyBaseInfo = new DailyBaseInfoVo();
        dailyBaseInfo.setUserId(userBound.getUserId());
        dailyBaseInfo.setDay(date);
        dailyBaseInfo.setOrganizeNames(userBound.getCurrOrgName());
        dailyBaseInfo.setPositionName(userBound.getCurrPositionName());
        dailyBaseInfo.setUserName(userBound.getName());
        UserWorkStatusEnums workStatusEnum = StringUtil.isEmpty(userBound.getWorkerStatus()) ? UserWorkStatusEnums.NONE
                : UserWorkStatusEnums.getUserWorkStatusEnumsByValue(userBound.getWorkerStatus());
        dailyBaseInfo.setWorkStatus(workStatusEnum.getDescription());
        return dailyBaseInfo;
    }

    /**
     * 添加原始数据到列表
     * @param clockInDetail 打卡详情
     * @param allRuleIds 今日所有出勤规则
     * @param ruleIds 需要排除的记录
     */
    private void setOldData(DailyClockInDetailVo clockInDetail, List<String> allRuleIds, List<String> ruleIds, String userId, String queryDate) {

        // 查询今日所有打卡
        List<DailyClockInVo> clockInList = attendanceClockInMapper.getListByRuleAndDate(userId, queryDate);
        if (clockInList.isEmpty()) {
            return;
        }
        // 在使用中的打卡记录(即在clock-in-result中有效)
        List<String> c = clockInDetail.getClockInList().stream().map(DailyClockInVo::getClockInId).filter(clockInId -> clockInId != null).collect(Collectors.toList());
        clockInList.removeIf(v -> c.contains(v.getClockInId()));
        List<DailyClockInVo> collect = clockInList.stream().filter(v -> allRuleIds.contains(v.getRuleId())).collect(Collectors.toList());
        if (!collect.isEmpty()) {
            List<Integer> indexList = new ArrayList<>();
            collect.forEach(v -> indexList.add(clockInList.indexOf(v)));
            Integer minDelIndex = null;
            Integer maxDelIndex = null;
            // 获取最大 和 最小的 index
            int minIndex = indexList.get(0);
            int maxIndex = indexList.get(indexList.size() - 1);
            if (minIndex > 0) {
                // 往上查找不在 allRuleIds 中的 ruleId 不为空的记录 并移除往上所有记录
                for (int i = indexList.get(0) - 1; i > 0; i--) {
                    DailyClockInVo vo = clockInList.get(i);
                    if (vo.getRuleId() != null && !allRuleIds.contains(vo.getRuleId())) {
                        minDelIndex = i;
                        break;
                    }
                }
            }
            if (maxIndex < clockInList.size() - 1) {
                // 往下查找不在 allRuleIds 中的 ruleId 不为空的记录 并移除往下所有记录
                for (int i = indexList.get(0) + 1; i < clockInList.size(); i++) {
                    DailyClockInVo vo = clockInList.get(i);
                    if (vo.getRuleId() != null && !allRuleIds.contains(vo.getRuleId())) {
                        maxDelIndex = i;
                        break;
                    }
                }
            }
            // 排除 ruleId 在 ruleIds 中的记录 和 minDelIndex 往上的记录 和 maxDelIndex 往下的记录
            List<DailyClockInVo> removeList = new ArrayList<>();
            if (null != minDelIndex) {
                removeList.addAll(clockInList.subList(0, minDelIndex));
            }
            if (null != maxDelIndex) {
                removeList.addAll(clockInList.subList(maxDelIndex, clockInList.size() - 1));
            }
            clockInList.removeAll(removeList);
            clockInList.removeIf(v -> ruleIds.contains(v.getRuleId()) && v.getDeleteMark() == 0);
        }
        if (!clockInList.isEmpty()) {
            // 转换数据 clockInTime 格式化, 打卡方式 转换成文字
            clockInList.forEach(v -> {
                String str = queryDate.equals(DateDetail.getDate2Str(v.getClockInTime(), DateDetail.DF)) ? "" : "次日";
                v.setClockInTimeStr(str + DateDetail.getDate2Str(v.getClockInTime(), DateDetail.DF10));
                v.setClockInMethod(getClockInMethod(null == v.getClockInMethod() ? ConstantUtil.DEVICE_UNKNOWN : Integer.parseInt(v.getClockInMethod())));
            });
            // 将clockInList插入到今日出勤中[插入到两个班次时间之间]
            if (clockInDetail.getClockInList().isEmpty()) {
                clockInDetail.getClockInList().addAll(clockInList.stream().sorted(Comparator.comparing(DailyClockInVo::getClockInTime)).collect(Collectors.toList()));
            } else {
                Map<DailyClockInVo, List<DailyClockInVo>> map = new HashMap<>();
                for (int i = 0; i < clockInDetail.getClockInList().size(); i++) {
                    DailyClockInVo v1 = clockInDetail.getClockInList().get(i);
                    DailyClockInVo v2 = null;
                    if (i + 1 < clockInDetail.getClockInList().size()) {
                        v2 = clockInDetail.getClockInList().get(i + 1);
                    }
                    if (i == 0) {
                        // 判断小于v1的clockInList
                        List<DailyClockInVo> list = clockInList.stream()
                                .filter(clockIn -> clockIn.getClockInTime().before(v1.getWorkDate()))
                                .collect(Collectors.toList());
                        map.put(null, list);
                    }
                    if (null != v2) {
                        // 如果v2不为空, 判断哪些clockInList在v1, v2班次之间, 如果有无需打卡 before的判定时间需要改为缺卡时间
                        Date beforeTime;
                        if (null != v2.getClockInStatus() && v2.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue())) {
                            beforeTime = v1.getLackTime();
                        } else {
                            beforeTime = v2.getWorkDate();
                        }
                        List<DailyClockInVo> list = clockInList.stream()
                                .filter(clockIn -> clockIn.getClockInTime().after(v1.getWorkDate()) && clockIn.getClockInTime().before(beforeTime))
                                .collect(Collectors.toList());
                        map.putIfAbsent(v1, list);
                    } else {
                        // 如果v2为空, 判断哪些clockInList大于v1班次
                        List<DailyClockInVo> list = clockInList.stream().filter(clockIn -> clockIn.getClockInTime().after(v1.getWorkDate())).collect(Collectors.toList());
                        map.putIfAbsent(v1, list);

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

    private void setLeaveValue(DailyClockInVo v, String sortValue) {
        String lastResult = StringUtils.isNotEmpty(v.getLastResult()) ? v.getLastResult() : "";
        if (lastResult.equals(ConstantUtil.LEAVE_STR)) {
            v.setNeedDel(true);
        } else {
            v.setSortStr(sortValue);
        }
    }

    private List<DailyApprovalVo> getApprovalList(List<DailyClockInVo> clockInList, String queryDate, String userId, String currentGroupId, String tenantId) {

        Date date = DateDetail.getStr2Date(queryDate);
        List<DailyApprovalVo> approvalList = new ArrayList<>();
        if (!clockInList.isEmpty()) {
            List<String> idList = clockInList.stream().map(DailyClockInVo::getClockInResultId).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
            // 考勤变更审批
            approvalList.addAll(getApplyChangeList(idList, date));
            // 补卡审批
            approvalList.addAll(getApplyRepairList(idList, date));
            // 外勤审批
            List<String> clockInIds = clockInList.stream().filter(v -> null != v.getClockInKind() && v.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE) && StringUtils.isNotEmpty(v.getClockInId()))
                    .map(DailyClockInVo::getClockInId).collect(Collectors.toList());
            approvalList.addAll(getApplyOutsideList(clockInIds, date));
            // 异常打卡审批
            approvalList.addAll(getApplyViolationList(idList));
        }
        // 加班审批
        approvalList.addAll(getApplyWorkOverTimeList(userId, queryDate, currentGroupId));
        // 外出审批
        approvalList.addAll(getApplyGoOutList(userId, queryDate, currentGroupId));
        // 出差审批
        approvalList.addAll(getApplyBusinessTripList(userId, queryDate, currentGroupId));
        // 请假审批
        approvalList.addAll(getApplyLeaveList(userId, queryDate, currentGroupId));
        // 借调审批 2.0 人事提供
        approvalList.addAll(getApplySelfList(userId, queryDate, currentGroupId));
        // 排序
        if (!approvalList.isEmpty()) {
            approvalList.forEach(approval -> {
                if (!"审批中".equals(approval.getLastResult())) {
                    ApproverByTaskIdAndNodeIdVo info = flowTaskService.getApproveInfo(approval.getTaskId(), null, tenantId, 0);
                    if (null != info) {
                        approval.setFlowId(info.getFlowId());
                        approval.setEnCode(info.getEnCode());
                        approval.setId(info.getId());
                    }
                }
            });
            approvalList = approvalList.stream().sorted(Comparator.comparing(DailyApprovalVo::getSubmitTime)).collect(Collectors.toList());
        }
        return approvalList;
    }

    private void setLastResult(DailyClockInVo v, FtbAttendanceDailyRule rule) {

        /*String lastResult = ConstantUtil.LEAVE_STR;
        if (rule.getApplyViewEnable().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode())) {
            lastResult = AttendanceTypeEnum.BUSINESS_TRIP.getMsg();
        }
        if (rule.getApplyViewEnable().equals(AttendanceTypeEnum.STEP_OUT.getCode())) {
            lastResult = AttendanceTypeEnum.STEP_OUT.getMsg();
        }*/
        v.setLastResult(ConstantUtil.LEAVE_STR);
        v.setClockInTimeStr(ClockInStatusEnum.NEED_NOT.getDescription());
    }

    /**
     * 获取打卡记录
     *
     * @param clockIn      打卡记录
     * @param rule         出勤规则
     * @param workStatus   0: 上班, 1: 下班
     * @param needApproval 是否需要审批
     * @return jnpf.model.attendance.vo.DailyClockInVo
     */
    private DailyClockInVo getDailyClockInVo(ClockInVo clockIn, FtbAttendanceDailyRule rule, int workStatus, Integer needApproval) {

        DailyClockInVo dailyClockIn;
        if (null == clockIn) {
            // 还未打卡
            dailyClockIn = new DailyClockInVo();
            dailyClockIn.setClockInType(workStatus == 0 ? ConstantUtil.ON_WORK : ConstantUtil.OFF_WORK);
            dailyClockIn.setSortStr(workStatus == 0 ? PuncheTypeEnum.ON_DUTY.getTitle() : PuncheTypeEnum.OFF_DUTY.getTitle());
            if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                dailyClockIn.setSortStr("加班" + dailyClockIn.getSortStr());
            }
            String shiftTime = getShiftTimeStr(rule, workStatus);
            dailyClockIn.setShiftTimeStr(shiftTime);
            if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable())
                    || getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), dailyClockIn.getClockInType())) {
                dailyClockIn.setWorkTime(ClockInStatusEnum.NEED_NOT.getDescription());
            }
        } else {
            dailyClockIn = setDailyClockInData(clockIn, rule, needApproval);
        }
        dailyClockIn.setGroupId(rule.getGroupId());
        dailyClockIn.setShiftId(rule.getShiftId());
        dailyClockIn.setApplyViewEnable(rule.getApplyViewEnable());
        dailyClockIn.setOutStepOutType(rule.getOutStepOutType());
        dailyClockIn.setInStepOutType(rule.getInStepOutType());
        dailyClockIn.setWorkDate(workStatus == 0 ? rule.getInPoint() : rule.getOutPoint());
        dailyClockIn.setLackTime(workStatus == 0 ? rule.getInLackPoint() : rule.getOutLackPoint());
        dailyClockIn.setRuleId(rule.getId());
        dailyClockIn.setAssociationApplyId(rule.getApplyId());
        dailyClockIn.setStepOutApplyId(workStatus == 0 ? rule.getInStepOutApplyId() : rule.getOutStepOutApplyId());
        return dailyClockIn;
    }

    @Override
    public String getShiftTimeStr(FtbAttendanceDailyRule rule, int workStatus) {

        String onStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF)) ? "" : "次日 ";
        String offStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF)) ? "" : "次日 ";
        return workStatus == 0 ? onStr + DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF10) : offStr + DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF10);
    }

    @Override
    public Boolean generateRepairNum() {

        // 查询需要生成补卡次数的考勤组
        LambdaQueryWrapper<AttendanceGroup> groupQuery = new LambdaQueryWrapper<AttendanceGroup>()
                .eq(AttendanceGroup::getDeleteMark, ConstantUtil.NUM_FALSE);
        List<AttendanceGroup> allGroupList = attendanceGroupMapper.selectList(groupQuery);
        if (allGroupList.isEmpty()) {
            return true;
        }
        List<String> groupIds = allGroupList.stream().map(AttendanceGroup::getId).collect(Collectors.toList());
        Map<String, AttendanceBaseSetting> settingMap = attendanceBaseSettingService.getEnableBaseSetting(groupIds);
        // 查询今天是否为本月最后一天(是: 查询配置天数>=今天的所有配置, 否: 查询配置天数=今天的所有配置)
        DateDetail dateDetail = new DateDetail();
        Date today = dateDetail.getCurrentDate();
        int currentDay = dateDetail.getDay();
        int lastDay = dateDetail.getLastDateOfMonth(dateDetail.getYear(), dateDetail.getMonth());
        // 满足条件的基础配置
        Map<String, AttendanceBaseSetting> map = new HashMap<>();
        if (currentDay == lastDay) {
            settingMap.keySet().forEach(key -> {
                AttendanceBaseSetting v = settingMap.get(key);
                if (null != v.getPatchRefreshDay() && v.getPatchRefreshDay() >= currentDay) {
                    map.put(key, v);
                }
            });
        } else {
            settingMap.keySet().forEach(key -> {
                AttendanceBaseSetting v = settingMap.get(key);
                if (null != v && null != v.getPatchRefreshDay() && v.getPatchRefreshDay() == currentDay) {
                    map.put(key, v);
                }
            });
        }
        generateRepairStart(map, today);
        return true;
    }

    @Override
    public Boolean generateRepairNumAll() {

        // 查询需要生成补卡次数的考勤组
        LambdaQueryWrapper<AttendanceGroup> groupQuery = new LambdaQueryWrapper<AttendanceGroup>()
                .eq(AttendanceGroup::getDeleteMark, ConstantUtil.NUM_FALSE);
        List<AttendanceGroup> allGroupList = attendanceGroupMapper.selectList(groupQuery);
        if (allGroupList.isEmpty()) {
            return true;
        }
        List<String> groupIds = allGroupList.stream().map(AttendanceGroup::getId).collect(Collectors.toList());
        Map<String, AttendanceBaseSetting> settingMap = attendanceBaseSettingService.getEnableBaseSetting(groupIds);
        generateRepairStart(settingMap, new Date());
        return true;
    }

    @Override
    public List<FtbAttendanceClockIn> getDailyClockInRecord(String userId, DayClockRange clockRecord) {
        LambdaQueryChainWrapper<FtbAttendanceClockIn> queryChainWrapper = this.lambdaQuery().eq(FtbAttendanceClockIn::getUserId, userId);
        if (clockRecord.getStartTimeIsOpenClose()) {
            queryChainWrapper.ge(FtbAttendanceClockIn::getClockInTime, clockRecord.getStartTime());
        } else {
            queryChainWrapper.gt(FtbAttendanceClockIn::getClockInTime, clockRecord.getStartTime());
        }
        if (clockRecord.getEndTimeIsOpenClose()) {
            queryChainWrapper.le(FtbAttendanceClockIn::getClockInTime, clockRecord.getEndTime());
        } else {
            queryChainWrapper.lt(FtbAttendanceClockIn::getClockInTime, clockRecord.getEndTime());
        }
        queryChainWrapper.in(FtbAttendanceClockIn::getApprovalStatus, ConstantUtil.PASS_TRUE, ConstantUtil.PASS_APPROVAL);
        List<FtbAttendanceClockIn> clockInList = queryChainWrapper.orderByAsc(FtbAttendanceClockIn::getClockInTime).list();
        return CollUtil.isNotEmpty(clockInList) ? clockInList : Lists.newArrayList();
    }

    @Override
    public Boolean continuousCheck(String tenantId) {

        // 查询用户连续旷工配置
        UserSettingVo userSettingVo = attendanceUserSettingService.checkWebSetting(UserSettingEnum.ATTENDANCE_GROUP_NO_CHECKIN_REMIND_ADMIN);
        if (null != userSettingVo && userSettingVo.getStatus().equals(ConstantUtil.NUM_TRUE)) {
            // 新建一个列表装需要发送的消息
            List<ConsecutiveAbsenceNoticeModel> returnList = new ArrayList<>();
            // 开启连续旷工配置提醒 判定企业用户连续旷工情况
            List<MonthRecordVo> list = attendanceDailyRuleMapper.selectMonthRecord();
            if (!list.isEmpty()) {
                ConcurrentMap<String, List<MonthRecordVo>> groupMap = list.stream().collect(Collectors.groupingByConcurrent(MonthRecordVo::getGroupId));
                groupMap.forEach((groupId, groupRecordList) -> {
                    ConcurrentMap<String, List<MonthRecordVo>> userMap = groupRecordList.stream().collect(Collectors.groupingByConcurrent(MonthRecordVo::getUserId));
                    userMap.forEach((userId, userRecordList) -> {
                        // 判断连续旷工天数, 大于配置的天数则生成要发送的消息
                        LinkedHashMap<String, List<MonthRecordVo>> dayMap = userRecordList.stream().collect(Collectors.groupingBy(MonthRecordVo::getDay, LinkedHashMap::new, Collectors.toList()));
                        // 所有每天的出勤记录
                        int consecutiveDays = (int) dayMap.entrySet().stream()
                                .takeWhile(entry -> entry.getValue().stream().allMatch(v -> v.getAbsenceCount() >= 2))
                                .count();
                        if (consecutiveDays >= userSettingVo.getValue()) {
                            // 生成消息
                            Object[] array = dayMap.keySet().toArray();
                            String dateStr = getContinuousDate(array, consecutiveDays);
                            ConsecutiveAbsenceNoticeModel model = new ConsecutiveAbsenceNoticeModel();
                            model.setAttendanceNoticeModel(userId, groupId, tenantId, new Date(), AttendanceNoticeEnum.CONSECUTIVE_ABSENCE);
                            model.setAbsenceDate(dateStr);
                            // 设置详情
                            AbsenceDetailModel absenceDetailModel = new AbsenceDetailModel(AttendanceConstant.TITLE_CONSECUTIVE_ABSENCE,
                                    DateDetail.getDate2Str(new Date(), DateDetail.DF10), "", "", dateStr);
                            List<NoticeDetailModel> detailList = new ArrayList<>();
                            for (int i = 0; i < consecutiveDays; i++) {
                                List<MonthRecordVo> recordList = dayMap.get(array[i].toString());
                                recordList.forEach(record -> detailList.add(new NoticeDetailModel(record.getDay().substring(5), record.getRule(), record.getRuleTime())));
                            }
                            absenceDetailModel.setDetailList(detailList);
                            model.setAbsenceDetailModel(absenceDetailModel);
                            returnList.add(model);
                        }
                    });
                });
            }
            // 若要发送的消息不为空, 则发送消息
            if (!returnList.isEmpty()) {
                List<String> userIds = returnList.stream().map(ConsecutiveAbsenceNoticeModel::getUserId).collect(Collectors.toList());
//                List<PartUserInfoVo> userList = userApi.getInfoByIdsNoData(userIds, tenantId);
//                Map<String, PartUserInfoVo> userMap = userList.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity()));

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

                List<String> groupIds = returnList.stream().map(ConsecutiveAbsenceNoticeModel::getGroupId).collect(Collectors.toList());
                List<AttendanceGroup> groupList = attendanceGroupMapper.getGroupDetailList(groupIds);
                Map<String, AttendanceGroup> groupMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, Function.identity()));
                Map<String, List<String>> groupAdminMap = attendanceSuperAdminService.queryUserForCurrAndUpChildAndSuper(groupIds);
                // 查询用户名和考勤组名
                Map<String, UserBoundVO> finalUserMap = userMap;
                returnList.forEach(v -> {
//                    PartUserInfoVo user = userMap.get(v.getUserId());
                    UserBoundVO user = finalUserMap.get(v.getUserId());
                    v.setUserName(null == user ? "--" : user.getUserName());
                    AttendanceGroup group = groupMap.get(v.getGroupId());
                    v.setGroupName(null == group ? "--" : group.getGroupName());
                    v.getAbsenceDetailModel().setUserName(v.getUserName());
                    v.getAbsenceDetailModel().setGroupName(v.getGroupName());
                    v.setToUserList(groupAdminMap.get(v.getGroupId()));
                    // 补全数据 发送消息给考勤组管理员
                    attendanceNoticeHandler.send(v);
                });
            }
        }
        return true;
    }

    private String getContinuousDate(Object[] array, int consecutiveDays) {

        String max = array[0].toString().substring(5);
        String min = array[consecutiveDays - 1].toString().substring(5);
        return min + " 至 " + max;
    }

    private void generateRepairStart(Map<String, AttendanceBaseSetting> settingMap, Date today) {

        if (settingMap.isEmpty()) {
            return;
        }
        // 查询配置关联的考勤组及考勤组成员
        LambdaQueryWrapper<AttendanceGroupUser> userQuery = new LambdaQueryWrapper<AttendanceGroupUser>()
                .ne(AttendanceGroupUser::getType, GroupUserTypeEnum.REMOVE.getCode())
                .eq(AttendanceGroupUser::getDeleteMark, ConstantUtil.NUM_FALSE)
                .in(AttendanceGroupUser::getGroupId, settingMap.keySet());
        // 本组成员 + 借调成员(需要再判断timeJson是否符合)
        List<AttendanceGroupUser> userList = attendanceGroupUserMapper.selectList(userQuery);
        if (userList.isEmpty()) {
            return;
        }
        List<AttendanceRepair> addList = new ArrayList<>();
        ConcurrentMap<String, List<AttendanceGroupUser>> groupUserMap = userList.stream().collect(Collectors.groupingByConcurrent(AttendanceGroupUser::getGroupId));
        for (String groupId : settingMap.keySet()) {
            AttendanceBaseSetting setting = settingMap.get(groupId);
            List<AttendanceGroupUser> groupUserList = groupUserMap.get(groupId);
            if (null == groupUserList || groupUserList.isEmpty()) {
                continue;
            }
            if (null == setting || null == setting.getPatchRefreshDay() || null == setting.getPatchClockNumber()) {
                continue;
            }
            // 拆分本组成员和借调成员
            List<AttendanceGroupUser> currentGroupUserList = groupUserList.stream().filter(v -> v.getType().equals(GroupUserTypeEnum.CUR.getCode())).collect(Collectors.toList());
            List<AttendanceGroupUser> borrowGroupUserList = groupUserList.stream().filter(v -> v.getType().equals(GroupUserTypeEnum.BORROW.getCode())).collect(Collectors.toList());
            if (!currentGroupUserList.isEmpty()) {
                for (AttendanceGroupUser user : currentGroupUserList) {
                    addList.add(getRepairSetting(groupId, setting, user));
                }
            }
            if (!borrowGroupUserList.isEmpty()) {
                // 判断是否还在借调时间段内, 是则生成
                firstLoop:
                for (AttendanceGroupUser borrowUser : borrowGroupUserList) {
                    if (StringUtil.isEmpty(borrowUser.getTimeJson())) {
                        continue;
                    }
                    JSONArray jsonArray = new JSONArray(borrowUser.getTimeJson());
                    List<JSONObject> jsonList = jsonArray.toList(JSONObject.class);
                    for (JSONObject json : jsonList) {
                        String startTimeStr = json.get("startTime").toString();
                        String endTimeStr = json.get("endTime").toString();
                        boolean b = DateDetail.checkTimeBetween(today, DateDetail.getStr2DateTime(startTimeStr), DateDetail.getStr2DateTime(endTimeStr));
                        if (b) {
                            addList.add(getRepairSetting(groupId, setting, borrowUser));
                            break firstLoop;
                        }
                    }
                }
            }
        }
        if (addList.isEmpty()) {
            return;
        }
        attendanceRepairService.saveBatch(addList);
    }

    @Override
    public Boolean generateRepairNumForUser(String groupId, String userId, Integer generateType) {

        // 查询当前考勤组是否已经存在当前用户的补卡次数记录
        AttendanceRepair repair = attendanceRepairService.getAttendanceRepair(userId, groupId);
        if (null != repair) {
            return true;
        }
        // 查询有没有同组成员的配置
        repair = attendanceRepairService.getAttendanceRepair(null, groupId);
        if (null != repair) {
            // 使用同组的配置生成当前用户的配置
            repair.setId(FtbUtil.getId());
            repair.setUserId(userId);
            repair.setRepairNum(repair.getTotalNum());
            repair.setRepairRecord(null);
        } else {
            // 查询当前考勤组的基础配置, 生成记录
            Map<String, AttendanceBaseSetting> settingMap = attendanceBaseSettingService.getEnableBaseSetting(Stream.of(groupId).collect(Collectors.toList()));
            AttendanceBaseSetting setting = settingMap.get(groupId);
            if (null == setting) {
                return false;
            }
            AttendanceGroupUser user = new AttendanceGroupUser();
            user.setUserId(userId);
            repair = getRepairSetting(groupId, setting, user);
        }
        if (generateType.equals(2)) {
            // 借调到新组, 根据借调时间判断是否需要生成
            LambdaQueryWrapper<AttendanceGroupUser> userQuery = new LambdaQueryWrapper<AttendanceGroupUser>()
                    .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.BORROW.getCode())
                    .eq(AttendanceGroupUser::getDeleteMark, ConstantUtil.NUM_FALSE)
                    .eq(AttendanceGroupUser::getGroupId, groupId)
                    .eq(AttendanceGroupUser::getUserId, userId);
            AttendanceGroupUser groupUser = attendanceGroupUserMapper.selectOne(userQuery);
            if (null == groupUser || StringUtils.isEmpty(groupUser.getTimeJson())) {
                return false;
            }
            JSONArray jsonArray = new JSONArray(groupUser.getTimeJson());
            List<JSONObject> jsonList = jsonArray.toList(JSONObject.class);
            boolean b = false;
            for (JSONObject json : jsonList) {
                String endTimeStr = json.get("endTime").toString();
                if (DateDetail.getStr2DateTime(endTimeStr).before(repair.getExpireDate())) {
                    b = true;
                    break;
                }
            }
            if (!b) {
                return true;
            }
        }
        attendanceRepairService.save(repair);
        return true;
    }

    @Override
    public RepairRuleVo getClockInRepairCheck(String groupId) {

        Map<String, AttendanceBaseSetting> settingMap = attendanceBaseSettingService.getEnableBaseSetting(Stream.of(groupId).collect(Collectors.toList()));
        AttendanceBaseSetting setting = settingMap.get(groupId);
        if (null == setting) {
            log.info("查询考勤组补卡规则失败, {}", groupId);
            return null;
        }
        return JsonUtil.getJsonToBean(setting, RepairRuleVo.class);
    }

    private AttendanceRepair getRepairSetting(String groupId, AttendanceBaseSetting setting, AttendanceGroupUser user) {

        DateDetail dateDetail = new DateDetail();
        AttendanceRepair attendanceRepair = new AttendanceRepair();
        attendanceRepair.setId(FtbUtil.getId());
        attendanceRepair.setGroupId(groupId);
        attendanceRepair.setUserId(user.getUserId());
        attendanceRepair.setGenerateYear(dateDetail.getYear());
        attendanceRepair.setGenerateMonth(dateDetail.getMonth());
        attendanceRepair.setGenerateDay(dateDetail.getDay());
        attendanceRepair.setTotalNum(setting.getPatchClockNumber());
        attendanceRepair.setRepairNum(setting.getPatchClockNumber());
        // 计算过期日期
        dateDetail.addNum(dateDetail.getCurrentDate(), 1, Calendar.MONTH);
        dateDetail.changeDate(dateDetail.getCurrentDate(), setting.getPatchRefreshDay());
        attendanceRepair.setExpireDate(dateDetail.getCurrentDate());
        return attendanceRepair;
    }

    private MutablePair<Integer, Integer> setLastAndNext(Date date, String userId, String currentGroupId) {

        // 无法查看的情况
        int hasLast = ConstantUtil.NUM_TRUE;
        int hasNext = ConstantUtil.NUM_TRUE;
        // 上一天/下一天 为 上一个月/下一个月
        DateDetail dateDetail = new DateDetail(date);
        dateDetail.getYesterday();
        Date lastDay = dateDetail.getCurrentDate();
        if (!DateDetail.checkSameMonth(date, lastDay)) {
            hasLast = ConstantUtil.NUM_FALSE;
        }
        dateDetail.getTomorrow();
        dateDetail.getTomorrow();
        Date nextDay = dateDetail.getCurrentDate();
        if (!DateDetail.checkSameMonth(date, nextDay)) {
            hasNext = ConstantUtil.NUM_FALSE;
        }
        // 离组后未来的日期 & 借调之后不在借调范围内的日期(借调考勤组)
        LambdaQueryWrapper<AttendanceGroupUser> groupUserQuery = new LambdaQueryWrapper<AttendanceGroupUser>()
                .eq(AttendanceGroupUser::getUserId, userId)
                .eq(AttendanceGroupUser::getGroupId, currentGroupId);
        List<AttendanceGroupUser> groupUserList = attendanceGroupUserMapper.selectList(groupUserQuery);
        if (attendanceDailyRuleService.findUserIsExistsByDay(groupUserList, lastDay).equals(ConstantUtil.NUM_FALSE)) {
            hasLast = ConstantUtil.NUM_FALSE;
        }
        if (attendanceDailyRuleService.findUserIsExistsByDay(groupUserList, nextDay).equals(ConstantUtil.NUM_FALSE)) {
            hasNext = ConstantUtil.NUM_FALSE;
        }
        return MutablePair.of(hasLast, hasNext);
    }

    private SchedulesItemVo setItemInfo(AttendanceShiftNameEntity shift, FtbAttendanceDailyRule rule, boolean hasOrdinary) {
        Integer type = getType(rule.getAttendanceType());
        if (Objects.isNull(type) || (AttendanceTypeEnum.LEAVE.getCode().equals(rule.getAttendanceType()) && hasOrdinary)) {
            return null;
        }
        SchedulesItemVo item = new SchedulesItemVo();
        item.setSchedulesType(rule.getSchedulesType());
        item.setType(type);
        item.setColour(HOLIDAYS_COLOR);
        if(Objects.equals(rule.getFixedMark(),2)){
            item.setName("划线排班");
            item.setShortName("划线排班");
            return item;
        }
        if (Objects.nonNull(shift)) {
            if (Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode())
                    || Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())) {
                item.setColour(shift.getColour());
                item.setShortName(shift.getShortName());
                item.setName(shift.getName());
            }
        }
        if (Objects.nonNull(rule.getPeriodInfo())) {
            ShiftNameVo shiftNameVo = JSONUtil.toBean(rule.getPeriodInfo(), ShiftNameVo.class);
            if (Objects.nonNull(shiftNameVo)) {
                item.setShortName(shiftNameVo.getShortName());
                item.setColour(shiftNameVo.getColour());
                item.setName(shiftNameVo.getName());
            }
        }
        return item;
    }

    private Integer getType(Integer attendanceType) {
        if (AttendanceTypeEnum.ORDINARY.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.NORMAL.getCode();
        }
        if (AttendanceTypeEnum.REST.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.REST.getCode();
        }
        if (AttendanceTypeEnum.LEAVE.getCode().equals(attendanceType)) {
            return SchedulesTypeEnum.LEAVE.getCode();
        }
        return null;
    }

    private Collection<? extends DailyApprovalVo> getApplyViolationList(List<String> idList) {

        List<DailyApprovalVo> list = new ArrayList<>();
        if (CollUtil.isEmpty(idList)) {
            return list;
        }
        LambdaQueryWrapper<ApplyAttendanceViolation> violationQuery = new LambdaQueryWrapper<ApplyAttendanceViolation>()
                .in(ApplyAttendanceViolation::getClockInResultId, idList);
        List<ApplyAttendanceViolation> vioList = applyAttendanceViolationMapper.selectList(violationQuery);
        if (vioList.isEmpty()) {
            return list;
        }
        vioList.forEach(vio -> {
            String applyTime = "";
            if (null != vio.getApplyDate()) {
                applyTime = DateDetail.getDate2Str(vio.getApplyDate(), DateDetail.DF9);
            }
            list.add(new DailyApprovalVo(vio.getId(), vio.getId(), "违规打卡", applyTime, vio.getCreatorTime(),
                    vio.getApproveTime(), vio.getApproveUserId(), vio.getApproveUserName(), getLastResult(vio.getStatus())));
        });
        return list;
    }

    private Collection<? extends DailyApprovalVo> getApplyChangeList(List<String> idList, Date date) {

        List<DailyApprovalVo> list = new ArrayList<>();
        if (CollUtil.isEmpty(idList)) {
            return list;
        }
        LambdaQueryWrapper<ApplyAttendanceChange> changeQuery = new LambdaQueryWrapper<ApplyAttendanceChange>()
                .in(ApplyAttendanceChange::getClockInResultId, idList);
        List<ApplyAttendanceChange> changeList = applyAttendanceChangeMapper.selectList(changeQuery);
        if (changeList.isEmpty()) {
            return list;
        }
        changeList.forEach(change -> {
            String applyTime = "";
            if (null != change.getOnWorkTime()) {
                applyTime = DateDetail.getDate2Str(change.getOnWorkTime(), DateDetail.DF9);
            }
            if (null != change.getOffWorkTime()) {
                if (StringUtils.isEmpty(applyTime)) {
                    applyTime = DateDetail.getDate2Str(change.getOffWorkTime(), DateDetail.DF9);
                } else {
                    applyTime = applyTime + " 至 " + DateDetail.getDate2Str(change.getOffWorkTime(), DateDetail.DF10);
                }
            }
            list.add(new DailyApprovalVo(change.getId(), change.getFlowId(), "出勤变更", applyTime, change.getCreatorTime(),
                    change.getApproveTime(), change.getApproveUserId(), change.getApproveUserName(), getLastResult(change.getStatus())));
        });
        return list;
    }

    private Collection<? extends DailyApprovalVo> getApplyRepairList(List<String> idList, Date date) {

        List<DailyApprovalVo> list = new ArrayList<>();
        if (CollUtil.isEmpty(idList)) {
            return list;
        }
        LambdaQueryWrapper<ApplyAttendanceRepair> repairQuery = new LambdaQueryWrapper<ApplyAttendanceRepair>()
                .in(ApplyAttendanceRepair::getClockInResultId, idList);
        List<ApplyAttendanceRepair> repairList = applyAttendanceRepairMapper.selectList(repairQuery);
        if (repairList.isEmpty()) {
            return list;
        }
        repairList.forEach(repair -> list.add(new DailyApprovalVo(repair.getId(), repair.getFlowId(), "补卡申请", repair.getRepairDateStr() + " " + repair.getRepairTimeStr(), repair.getCreatorTime(),
                repair.getApproveTime(), repair.getApproveUserId(), repair.getApproveUserName(), getLastResult(repair.getStatus()))));
        return list;
    }

    private Collection<? extends DailyApprovalVo> getApplyOutsideList(List<String> idList, Date date) {

        List<DailyApprovalVo> list = new ArrayList<>();
        if (idList.isEmpty()) {
            return list;
        }
        // 查询外勤打卡的任务id
        LambdaQueryWrapper<FtbAttendanceClockIn> clockInQuery = new LambdaQueryWrapper<FtbAttendanceClockIn>()
                .eq(FtbAttendanceClockIn::getClockInKind, ConstantUtil.KIND_OUTSIDE)
                .eq(FtbAttendanceClockIn::getDeleteMark, ConstantUtil.NUM_FALSE)
                .isNotNull(FtbAttendanceClockIn::getApprovalCode)
                .in(FtbAttendanceClockIn::getId, idList);
        List<FtbAttendanceClockIn> clockInList = attendanceClockInMapper.selectList(clockInQuery);
        if (clockInList.isEmpty()) {
            return list;
        }
        List<String> taskIds = clockInList.stream().map(FtbAttendanceClockIn::getApprovalCode).collect(Collectors.toList());

        LambdaQueryWrapper<ApplyAttendanceOutside> outsideQuery = new LambdaQueryWrapper<ApplyAttendanceOutside>()
                .in(ApplyAttendanceOutside::getId, taskIds);
        List<ApplyAttendanceOutside> outsideList = applyAttendanceOutsideMapper.selectList(outsideQuery);
        if (outsideList.isEmpty()) {
            return list;
        }
        outsideList.forEach(outside -> list.add(new DailyApprovalVo(outside.getId(), outside.getFlowId(), "外勤申请", DateDetail.getDate2Str(outside.getApplyDate(), DateDetail.DF9), outside.getCreatorTime(),
                outside.getApproveTime(), outside.getApproveUserId(), outside.getApproveUserName(), getLastResult(outside.getStatus()))));
        return list;
    }


    private Collection<? extends DailyApprovalVo> getApplyWorkOverTimeList(String userId, String queryDate, String currentGroupId) {
        List<DailyApprovalVo> list1 = attendanceLeaveApproveMapper.getApplyWorkOverTimeList(userId, queryDate, currentGroupId, AttendanceTypeEnum.WORKOVERTIME.getCode());
        List<DailyApprovalVo> list = new ArrayList<>(list1);
        // 审批中的查询
        List<DailyApprovalVo> list2 = attendanceLeaveApproveMapper.getApplyingWorkOverTimeList(userId, queryDate);
        list.addAll(list2);
        return setResult(list);
    }

    @Nullable
    private List<DailyApprovalVo> setResult(List<DailyApprovalVo> list) {
        if (null != list && !list.isEmpty()) {
            // 处理审批状态
            list.forEach(v -> {
                v.setLastResult(getLastResult(Integer.parseInt(v.getLastResult())));
            });
        }
        return list;
    }

    private Collection<? extends DailyApprovalVo> getApplyGoOutList(String userId, String queryDate, String currentGroupId) {
        List<DailyApprovalVo> list1 = attendanceLeaveApproveMapper.getApplyGoOutList(userId, queryDate, currentGroupId, AttendanceTypeEnum.STEP_OUT.getCode());
        List<DailyApprovalVo> list = new ArrayList<>(list1);
        // 审批中的查询
        List<DailyApprovalVo> list2 = attendanceLeaveApproveMapper.getApplyingGoOutList(userId, queryDate);
        list.addAll(list2);
        return setResult(list);
    }

    private Collection<? extends DailyApprovalVo> getApplyLeaveList(String userId, String queryDate, String currentGroupId) {
        List<DailyApprovalVo> list1 = attendanceLeaveApproveMapper.getApplyLeaveList(userId, queryDate, currentGroupId, AttendanceTypeEnum.LEAVE.getCode());
        List<DailyApprovalVo> list = new ArrayList<>(list1);
        // 审批中的查询
        List<DailyApprovalVo> list2 = attendanceLeaveApproveMapper.getApplyingLeaveList(userId, queryDate);
        list.addAll(list2);
        return setResult(list);
    }

    private Collection<? extends DailyApprovalVo> getApplySelfList(String userId, String queryDate, String currentGroupId) {
        // 查询出考勤组对应的组织
        AttendanceGroup groupDetail = attendanceGroupMapper.getGroupDetail(currentGroupId);
        if (null == groupDetail) {
            return Collections.emptyList();
        }
        FtbSecondMentQueryDTO ftbSecondMentQueryDTO = new FtbSecondMentQueryDTO();
        ftbSecondMentQueryDTO.setOrgId(groupDetail.getOrgId());
        ftbSecondMentQueryDTO.setUserId(userId);
        ftbSecondMentQueryDTO.setStartTime(DateDetail.getStr2Date(queryDate));
        ftbSecondMentQueryDTO.setEndTime(DateDetail.getStr2Date(queryDate));
        List<DailyApprovalVo> list = ftbPersonneApi.queryListApproval(ftbSecondMentQueryDTO);
        //  处理借调状态对齐 人事方：1审批中,2,审核通过,3,审核不通过   考勤方：0.待审核  1.通过 2.未通过 3.撤回
        list.forEach(v -> {
            if ("1".equals(v.getLastResult())) {
                v.setLastResult("0");
            } else if ("2".equals(v.getLastResult())) {
                v.setLastResult("1");
            } else if ("3".equals(v.getLastResult())) {
                v.setLastResult("2");
            }
        });
        return setResult(list);
    }

    private Collection<? extends DailyApprovalVo> getApplyBusinessTripList(String userId, String queryDate, String currentGroupId) {

        List<DailyApprovalVo> list1 = attendanceLeaveApproveMapper.getApplyBusinessTripList(userId, queryDate, currentGroupId, AttendanceTypeEnum.BUSINESS_TRIP.getCode());
        List<DailyApprovalVo> list = new ArrayList<>(list1);
        // 审批中的查询
        List<DailyApprovalVo> list2 = attendanceLeaveApproveMapper.getApplyingBusinessTripList(userId, queryDate);
        list.addAll(list2);
        return setResult(list);
    }

    private String getLastResult(Integer status) {

        switch (status) {
            case 0:
                return "审批中";
            case 1:
                return "已通过";
            case 2:
                return "已驳回";
            case 3:
                return "已撤回";
            default:
                return "";
        }
    }

    private DailyClockInVo setDailyClockInData(ClockInVo clockIn, FtbAttendanceDailyRule rule, Integer needApproval) {

        DailyClockInVo dailyClockIn = new DailyClockInVo();
        dailyClockIn.setRuleType(ConstantUtil.RULE_TYPE_WORK);
        dailyClockIn.setClockInResultId(clockIn.getId());
        dailyClockIn.setRepaired(clockIn.getRepaired());
        dailyClockIn.setAttendanceType(rule.getAttendanceType());
        // 判断有无次日
        String onStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF)) ? "" : "次日 ";
        String offStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF)) ? "" : "次日 ";
        dailyClockIn.setSortStr(clockIn.getClockInType().equals(ConstantUtil.ON_WORK) ? PuncheTypeEnum.ON_DUTY.getTitle() : PuncheTypeEnum.OFF_DUTY.getTitle());
        if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
            dailyClockIn.setSortStr("加班" + dailyClockIn.getSortStr());
        }
        dailyClockIn.setShiftTimeStr(clockIn.getClockInType().equals(ConstantUtil.ON_WORK) ? onStr + DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF10) : offStr + DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF10));
        dailyClockIn.setWorkTime(dailyClockIn.getShiftTimeStr());
        dailyClockIn.setTotalShiftTimeStr(onStr + DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF10) + "-" + offStr + DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF10));
        String clockInTimeStr = getCheckNextDay(clockIn.getClockInTime(), rule.getDay());
        dailyClockIn.setClockInTimeStr(clockIn.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) ? ClockInStatusEnum.NEED_NOT.getDescription() : clockInTimeStr);
        dailyClockIn.setClockInMethod(getClockInMethod(null == clockIn.getDeviceType() ? ConstantUtil.DEVICE_UNKNOWN : clockIn.getDeviceType()));
        dailyClockIn.setClockInPlace(clockIn.getAddress());
        dailyClockIn.setClockInKind(clockIn.getClockInKind());
        dailyClockIn.setClockInStatus(clockIn.getClockInStatus());
        if (clockIn.getAbsence().equals(ConstantUtil.NUM_TRUE)) {
            dailyClockIn.setClockInStatus(ClockInStatusEnum.ABSENCE.getValue());
        }
        boolean flag = StringUtils.isNotEmpty(clockIn.getAbsenceLeader()) && StringUtils.isEmpty(clockIn.getApplyId());
        dailyClockIn.setChanged(flag ? ConstantUtil.NUM_TRUE : ConstantUtil.NUM_FALSE);
        dailyClockIn.setClockInId(clockIn.getClockInId());
        dailyClockIn.setRuleId(clockIn.getRuleId());
        dailyClockIn.setClockInTime(clockIn.getClockInTime());
        dailyClockIn.setEffectiveTime(clockIn.getEffectiveTime());
        dailyClockIn.setApprovalStatus(clockIn.getApprovalStatus());
        dailyClockIn.setClockInType(clockIn.getClockInType());
        // 新增备注
        dailyClockIn.setRemark(clockIn.getRemark());
        dailyClockIn.setAssociationApplyId(clockIn.getAssociationApplyId());
        dailyClockIn.setNeedApproval(needApproval);
        // 设置最终结果
        String lastResult = getClockInLastResult(dailyClockIn.getAttendanceType(), dailyClockIn.getRepaired(), dailyClockIn.getChanged(),
                dailyClockIn.getClockInStatus(), dailyClockIn.getClockInKind(), dailyClockIn.getClockInType(), rule);
        dailyClockIn.setLastResult(lastResult);
        // 判断能否变更
        if (rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) {
            if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || ConstantUtil.KIND_OUTSIDE == dailyClockIn.getClockInKind()) {
                dailyClockIn.setCouldChange(ConstantUtil.NUM_FALSE);
            } else if (!getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), clockIn.getClockInType())) {
                dailyClockIn.setCouldChange(ConstantUtil.NUM_TRUE);
            }
        }
        return dailyClockIn;
    }

    private String getCheckNextDay(Date clockInTime, Date day) {

        String str = clockInTime == null ? "" : DateDetail.getDate2Str(day, DateDetail.DF).equals(DateDetail.getDate2Str(clockInTime, DateDetail.DF)) ? "" : "次日 ";
        return clockInTime == null ? "" : str + DateDetail.getDate2Str(clockInTime, DateDetail.DF10);
    }

    /**
     * 获取最终结果
     *
     * @param attendanceType  出勤类型(4: 加班)
     * @param repaired        是否补卡(1: 是, 0: 否)
     * @param changed         结果是否变更(1: 是, 0: 否)
     * @param clockInStatus   打卡状态(-1: 缺卡, 1: 正常, 2: 迟到, 3: 早退)
     * @param clockInKind     打卡种类(1: 普通打卡, 2: 外勤打卡)
     * @param rule 审核数据展示标识(1.普通班 9.标识出差 10.标识外出)
     * @return java.lang.String
     */
    private String getClockInLastResult(Integer attendanceType, Integer repaired, Integer changed, Integer clockInStatus, Integer clockInKind, Integer clockInType, FtbAttendanceDailyRule rule) {

        String lastResult = "";
        if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(attendanceType)) {
            lastResult = "加班";
        }
        if (clockInKind.equals(ConstantUtil.KIND_OUTSIDE)) {
            lastResult = "外勤";
        }
        switch (clockInStatus) {
            case -1:
                lastResult = "缺卡";
                break;
            case 1:
                if (StringUtils.isEmpty(lastResult)) {
                    lastResult = "正常";
                }
                break;
            case 2:
                lastResult = "迟到";
                break;
            case 3:
                lastResult = "早退";
                break;
            case 4:
                lastResult = "旷工";
                break;
            default:
                break;
        }
        if (null != repaired && ConstantUtil.NUM_TRUE == repaired) {
            lastResult += "（补卡）";
        }
        if (null != changed && ConstantUtil.NUM_TRUE == changed) {
            lastResult += "（变更）";
        }
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable())) {
            lastResult = AttendanceTypeEnum.BUSINESS_TRIP.getMsg();
        }
        if (getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), clockInType)) {
            lastResult = AttendanceTypeEnum.STEP_OUT.getMsg();
        }
        return lastResult;
    }

    private String getClockInMethod(Integer deviceType) {

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

    private DailyBaseInfoVo setDailyBaseInfo(Date date, BaseUserInfoVo baseUserInfo) {

        DailyBaseInfoVo dailyBaseInfo = new DailyBaseInfoVo();
        dailyBaseInfo.setUserId(baseUserInfo.getUserId());
        dailyBaseInfo.setDay(date);
        dailyBaseInfo.setOrganizeNames(baseUserInfo.getOrganizeNames());
        dailyBaseInfo.setPositionName(baseUserInfo.getPositionName());
        dailyBaseInfo.setUserName(baseUserInfo.getUserName());
        dailyBaseInfo.setWorkStatus(baseUserInfo.getWorkStatusName());
        return dailyBaseInfo;
    }

    @Override
    public List<MiniGroupVo> getDailyClockInRecord(String userId, String queryDate, String currentGroupId) {

        List<MiniGroupVo> list = new ArrayList<>();
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(userId);
        // 查询当日出勤规则
        List<FtbAttendanceDailyRule> dailyRuleList = getCurrentDailyRuleListOfWithdraw(DateDetail.getStr2Date(queryDate), userInfo);
        if (dailyRuleList.isEmpty()) {
            return new ArrayList<>();
        }
        // 查询出勤规则关联的打卡记录
        List<String> ruleIdList = dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList());
        List<ClockInVo> clockInResultList = attendanceClockInMapper.getClockInResultByRuleList(ruleIdList);
        Set<String> groupIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getGroupId).collect(Collectors.toSet());
        // 查询考勤组信息
        List<MiniGroupVo> miniGroupList = attendanceGroupUserMapper.getGroupUserList(groupIds, userId, ConstantUtil.DEL);
        // 查询用户自己的考勤组
        String selfGroupId = attendanceGroupUserMapper.getSelfGroup(userId);
        // 判断 我的/原/借出/借入考勤组
        if (currentGroupId.equals(selfGroupId)) {
            // 查询 我的 和 借出的考勤组
            miniGroupList.forEach(group -> {
                if (currentGroupId.equals(group.getGroupId())) {
                    group.setGroupKind(ConstantUtil.GROUP_KIND_MINE);
                } else {
                    group.setGroupKind(ConstantUtil.GROUP_KIND_LEND);
                }
            });
        } else {
            miniGroupList.forEach(group -> {
                if (currentGroupId.equals(group.getGroupId())) {
                    if (group.getType().equals(3)) {
                        // 当前考勤组 关联的 用户 是移除状态 -> 原考勤组
                        group.setGroupKind(ConstantUtil.GROUP_KIND_OLD);
                    } else {
                        // 当前考勤组 == 借调考勤组 -> 借入考勤组
                        group.setGroupKind(ConstantUtil.GROUP_KIND_BORROW);
                    }
                }
            });
        }
        miniGroupList.removeIf(group -> group.getGroupKind() == null);
        if (miniGroupList.isEmpty()) {
            new ArrayList<>();
        }
        // 生成打卡结果信息
        miniGroupList.forEach(group -> {
            List<FtbAttendanceDailyRule> ruleList = dailyRuleList.stream().filter(v -> v.getGroupId().equals(group.getGroupId()))
                    .sorted(Comparator.comparing(FtbAttendanceDailyRule::getInPoint, Comparator.nullsLast(Comparator.naturalOrder()))).collect(Collectors.toList());
            if (!ruleList.isEmpty()) {
                List<RuleAndClockVo> ruleAndClockList = new ArrayList<>();
                ruleList.forEach(rule -> {
                    if (rule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) {
                        // 请假
                        String onRestStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF)) ? "" : "次日 ";
                        String offRestStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF)) ? "" : "次日 ";
                        RuleAndClockVo leaveWorkVo = new RuleAndClockVo(ConstantUtil.RULE_TYPE_LEAVE, rule.getId(), null,
                                onRestStr + DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF10) + " - " + offRestStr + DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF10));
                        ruleAndClockList.add(leaveWorkVo);
                    } else if (!rule.getAttendanceType().equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode())
                            && !rule.getAttendanceType().equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())
                            && !rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())) {
                        // 根据规则获取打卡信息
                        List<ClockInVo> collect = clockInResultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).collect(Collectors.toList());
                        // 上班时间
                        String onStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF)) ? "" : "次日 ";
                        RuleAndClockVo onWorkVo = new RuleAndClockVo(ConstantUtil.RULE_TYPE_WORK, rule.getId(), ConstantUtil.ON_WORK, onStr + DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF10));
                        collect.stream().filter(v -> v.getClockInType().equals(ConstantUtil.ON_WORK)).findFirst().ifPresent(onWorkVo::setClockInInfo);
                        ruleAndClockList.add(onWorkVo);
                        // 休息时间
                        if (null != rule.getBreakEnable() && rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE) && null != rule.getBreakStartPoint() && null != rule.getBreakEndPoint()) {
                            String onRestStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getBreakStartPoint(), DateDetail.DF)) ? "" : "次日 ";
                            String offRestStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getBreakEndPoint(), DateDetail.DF)) ? "" : "次日 ";
                            RuleAndClockVo restWorkVo = new RuleAndClockVo(ConstantUtil.RULE_TYPE_REST, rule.getId(), null,
                                    onRestStr + DateDetail.getDate2Str(rule.getBreakStartPoint(), DateDetail.DF10) + " - " + offRestStr + DateDetail.getDate2Str(rule.getBreakEndPoint(), DateDetail.DF10));
                            ruleAndClockList.add(restWorkVo);
                        }
                        // 下班时间
                        String offStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF)) ? "" : "次日 ";
                        RuleAndClockVo offWorkVo = new RuleAndClockVo(ConstantUtil.RULE_TYPE_WORK, rule.getId(), ConstantUtil.OFF_WORK, offStr + DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF10));
                        collect.stream().filter(v -> v.getClockInType().equals(ConstantUtil.OFF_WORK)).findFirst().ifPresent(offWorkVo::setClockInInfo);
                        ruleAndClockList.add(offWorkVo);
                        // 判断是否旷工
                        if (rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                            if (null != onWorkVo.getAbsence() && null != offWorkVo.getAbsence() && onWorkVo.getAbsence().equals(ConstantUtil.NUM_TRUE) && offWorkVo.getAbsence().equals(ConstantUtil.NUM_TRUE)) {
                                onWorkVo.setClockInStatus(ClockInStatusEnum.ABSENCE.getValue());
                                offWorkVo.setClockInStatus(ClockInStatusEnum.ABSENCE.getValue());
                            }
                        }
                    }
                });
                group.getList().addAll(ruleAndClockList);
                list.add(group);
            }
        });
        return list;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void attendanceChange(String applyId, String passed, String approveUserId, String tenantId) throws HandleException {

        log.info("考勤变更 -> applyId: {}, passed: {}, approveUserId: {}", applyId, passed, approveUserId);
        // 根据审批id查询审批内容
        ApplyAttendanceChange applyAttendanceChange = applyAttendanceChangeMapper.selectById(applyId);
        if (null == applyAttendanceChange) {
            log.info("未找到审批记录");
            return;
        }
        log.info("审批内容：{}", applyAttendanceChange);
        // 查询打卡结果记录
        LambdaQueryWrapper<AttendanceClockInResult> resultQueryWrapper = new LambdaQueryWrapper<AttendanceClockInResult>()
                .eq(AttendanceClockInResult::getId, applyAttendanceChange.getClockInResultId())
                .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE);
        AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectOne(resultQueryWrapper);
        if (null == clockInResult) {
            log.info("未找到打卡结果");
            return;
        }
        FtbAttendanceDailyRule rule = attendanceDailyRuleMapper.selectById(clockInResult.getRuleId());
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable())
                || getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), clockInResult.getClockInType())) {
            log.info("操作失败，班次结果已变更");
        }
        int pass = Integer.parseInt(passed.substring(0, 1));
        Integer status = null;
        // String title = "您发起的【出勤变更】审批%s";
        switch (pass) {
            case 0:
                status = ConstantUtil.STATUS_REFUSE;
                // title = String.format(title, "已驳回");
            case 2:
                log.info("拒绝或撤销...");
                if (null == status) {
                    status = ConstantUtil.STATUS_ROLLBACK;
                    // title = String.format(title, "已撤回");
                }
                String ruleId = null;
                if (applyAttendanceChange.getChangeType().equals(1) || applyAttendanceChange.getChangeType().equals(2)) {
                    ruleId = clockInResult.getRuleId();
                }
                emptyApplyInfo(clockInResult.getId(), ruleId, clockInResult.getLastAbsenceLeader());
                break;
            case 1:
                // title = String.format(title, "已通过");
                status = ConstantUtil.STATUS_PASS;
                changePassed(clockInResult, applyAttendanceChange.getChangeType(), applyAttendanceChange.getCreatorUserId(), tenantId);
                break;
            default:
                break;
        }
        // 更新审批记录
        ActionResult<UserBoundInfoVO> usersBound = v2UserApi.getUsersBound(approveUserId, null);
        LambdaUpdateWrapper<ApplyAttendanceChange> changeUpdate = new LambdaUpdateWrapper<ApplyAttendanceChange>()
                .set(ApplyAttendanceChange::getApproveUserId, approveUserId)
                .set(ApplyAttendanceChange::getApproveTime, new Date())
                .set(ApplyAttendanceChange::getStatus, status)
                .eq(ApplyAttendanceChange::getId, applyId);
        if (200 == usersBound.getCode() && null != usersBound.getData()) {
            changeUpdate.set(ApplyAttendanceChange::getApproveUserName, usersBound.getData().getUserName());
        }
        applyAttendanceChangeMapper.update(null, changeUpdate);
        //sendToIm(applyId, applyAttendanceChange.getCreatorUserId(), title, approveUser.getUserName(), ConstantUtil.APPROVE_NOTICE_CHANGE, passed);
        //sendResultChangedToIm(clockInResult, tenantId, applyAttendanceChange.getCreatorUserId(), applyAttendanceChange.getChangeType());
    }

    private void changePassed(AttendanceClockInResult clockInResult, Integer changeType, String handleUser, String tenantId) throws HandleException {
        log.info("变更通过中...");
        AttendanceRuleVo rule = getAttendanceRule(clockInResult.getRuleId());
        if (rule == null) {
            log.info("未找到出勤规则");
            return;
        }
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(clockInResult.getUserId());
        // 判断考勤组是否被封账
        String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15);
        Map<String, Boolean> map = attendanceDayStatisticsService.selectUserIsSeal(List.of(rule.getUserId()), monthDate);
        if (map.get(rule.getUserId())) {
            throw new HandleException(monthDate + "已封账，过去的考勤记录无法变更！");
        }
        switch (changeType) {
            case 1:
                log.info("变更为旷工...");
                // 变更为旷工 根据ruleId找到对应的上下班打卡, 同时更新为旷工
                // 判断有没有下班打卡记录, 有, 同时变更, 无, 新增缺卡记录标识为旷工
                AttendanceClockInResult offWork = getClockInResultByWorkType(clockInResult.getRuleId(), clockInResult.getUserId(), ConstantUtil.OFF_WORK);
                if (null == offWork) {
                    AttendanceClockInResult noClockRecord = generateNoClockRecord(rule, ConstantUtil.OFF_WORK, null, userInfo);
                    noClockRecord.setAbsenceLeader(handleUser);
                    noClockRecord.setLastAbsenceLeader(handleUser);
                    attendanceClockInResultMapper.insert(noClockRecord);
                } else {
                    emptyApplyInfo(offWork.getId(), null, handleUser);
                }
                attendanceClockInMapper.updateToAbsence(clockInResult.getRuleId(), clockInResult.getUserId(), handleUser);
                emptyApplyInfo(clockInResult.getId(), clockInResult.getRuleId(), handleUser);
                break;
            case 2:
                log.info("撤销旷工...");
                // 撤销旷工 根据ruleId找到对应的上下班打卡, 同时取消旷工
                // 查询下班打卡记录, 普班/加班, 缺卡时间, 缺卡还没结束
                attendanceClockInMapper.updateToUnAbsence(clockInResult.getRuleId(), clockInResult.getUserId(), handleUser);
                emptyApplyInfo(clockInResult.getId(), clockInResult.getRuleId(), handleUser);
                AttendanceClockInResult offWorkRecord = getClockInResultByWorkType(clockInResult.getRuleId(), clockInResult.getUserId(), ConstantUtil.OFF_WORK);
                // emptyApplyInfo(offWorkRecord.getId(), handleUser);
                Date absenceTime;
                if (offWorkRecord.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) {
                    if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                        // 查询加班缺卡时间点
                        MutablePair<Date, Date> datePair = getOverTimeWorkRange(rule, userInfo.getUserId());
                        absenceTime = datePair.getRight();
                    } else {
                        // 普班缺卡时间点
                        absenceTime = rule.getOutLackPoint();
                    }
                    if (null != absenceTime && absenceTime.after(new Date())) {
                        // 删除缺卡记录
                        attendanceClockInResultMapper.deleteById(offWorkRecord.getId());
                    }
                }
                break;
            case 3:
                log.info("变更为正常...");
                if (null != clockInResult.getAbsence() && clockInResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) {
                    throw new HandleException("旷工的卡无法变更为正常");
                }
                // 变更为正常 将对应的打卡结果更改为正常, 异常时间归零
                clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue());
                clockInResult.setAbnormalMinute(0);
                if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) {
                    clockInResult.setEffectiveTime(rule.getInPoint());
                } else {
                    clockInResult.setEffectiveTime(rule.getOutPoint());
                }
                clockInResult.setApplyType(null);
                clockInResult.setApplyId(null);
                clockInResult.setLastAbsenceLeader(handleUser);
                attendanceClockInResultMapper.updateById(clockInResult);
                attendanceClockInResultMapper.updateResultReplyToNull(clockInResult.getId(), handleUser);
                break;
            case 4:
                log.info("变更为补卡...");
                // 补卡 新增打卡记录并绑定, 不标识为补卡
                FtbAttendanceClockIn repairClockIn = new FtbAttendanceClockIn();
                repairClockIn.setId(FtbUtil.getId());
                repairClockIn.setUserId(clockInResult.getUserId());
                repairClockIn.setDay(rule.getDay());
                if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) {
                    repairClockIn.setClockInTime(rule.getInPoint());
                } else {
                    repairClockIn.setClockInTime(rule.getOutPoint());
                }
                repairClockIn.setApprovalStatus(ConstantUtil.PASS_TRUE);
                repairClockIn.setClockInKind(ConstantUtil.KIND_NORMAL);
                // repairClockIn.setDeviceType(ConstantUtil.DEVICE_PLACE);
                repairClockIn.setDeleteMark(ConstantUtil.NUM_FALSE);
                // 生成打卡结果
                AttendanceClockInResult insertResult = generateClockInResultRecord(rule, repairClockIn, null, clockInResult.getClockInType(), userInfo);
                insertResult.setApplyId(null);
                insertResult.setApplyType(null);
                insertResult.setAbsenceLeader(handleUser);
                insertResult.setLastAbsenceLeader(handleUser);
                this.save(repairClockIn);
                attendanceClockInResultMapper.deleteById(clockInResult);
                insertResult.setId(clockInResult.getId());
                attendanceClockInResultMapper.insert(insertResult);
                break;
            default:
                break;
        }
        // 发布日统计数据生成事件
        StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder()
                .tenantId(tenantId)
                .groupId(rule.getGroupId())
                .userId(clockInResult.getUserId())
                .day(rule.getDay())
                .triggerSceneEnum(TriggerSceneEnum.CHECK_IN)
                .build();
        if(StrUtil.isBlank(tenantId) || StrUtil.isBlank(rule.getGroupId())){
            log.error("AttendanceClockInServiceImpl#changePassed 触发日统计租户ID或者考勤组ID为空：{}", JSON.toJSONString(courseEventDTO));
        }
        Message<StatisticsSingleDto> message = MessageBuilder.withPayload(courseEventDTO).build();
        rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2);
    }

    @Override
    public void attendanceChangeNoApproval(String clockInResultId, Integer changeType) throws HandleException {

        UserInfo userInfo = userProvider.get();
        AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectById(clockInResultId);
        int count = attendanceClockInResultMapper.getReplyingCount(clockInResultId);
        if (count > 0) {
            throw new HandleException("当前记录正在审批中，请审批结束后再重新执行操作");
        }
        changePassed(clockInResult, changeType, userInfo.getUserId(), userInfo.getTenantId());
        // 发送消息
        //sendResultChangedToIm(clockInResult, userInfo.getTenantId(), userInfo.getUserId(), changeType);
    }

    private AttendanceClockInResult getClockInResultByWorkType(String ruleId, String userId, Integer workType) {

        LambdaQueryWrapper<AttendanceClockInResult> queryWrapper = new LambdaQueryWrapper<AttendanceClockInResult>()
                .eq(AttendanceClockInResult::getRuleId, ruleId)
                .eq(AttendanceClockInResult::getUserId, userId)
                .eq(AttendanceClockInResult::getClockInType, workType)
                .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE);
        return attendanceClockInResultMapper.selectOne(queryWrapper);
    }

    /**
     * 初始化缺卡信息v2.0
     * @param day 日期
     * @return java.util.Map<java.lang.String, java.util.List < jnpf.model.attendance.vo.AbsenceClockInVo>>
     */
    private Map<String, List<AbsenceClockInVo>> initAbsenceInfo2(Date day, String uId) {

        Map<String, List<FtbAttendanceDailyRule>> userRuleMap = clockInResultService.getTodayRuleListByUser(day, uId);
        if (userRuleMap.isEmpty()) {
            // 今日没有需要缺卡的
            return new HashMap<>();
        }
        Map<String, List<AbsenceClockInVo>> map = new HashMap<>();
        userRuleMap.forEach((userId, list) -> {
            if (null != list && !list.isEmpty()) {
                Map<String, List<FtbAttendanceDailyRule>> ruleCollect = list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getId));
                ruleCollect.forEach((ruleId, v) -> {
                    List<AbsenceClockInVo> absenceList = map.computeIfAbsent(userId + ":" + ruleId, k -> new ArrayList<>());
                    // 昨天 -> 今天
                    v.sort(Comparator.comparingInt(FtbAttendanceDailyRule::getRn).reversed());
                    v.forEach(rule -> {
                        if (null != rule.getInLackPoint()) {
                            if (DateDetail.checkSameDay(rule.getInLackPoint(), day)) {
                                AbsenceClockInVo onWork = new AbsenceClockInVo(userId, rule.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(rule.getInLackPoint(), DateDetail.DF2));
                                absenceList.add(onWork);
                            }
                        }
                        if (null != rule.getOutLackPoint()) {
                            if (DateDetail.checkSameDay(rule.getOutLackPoint(), day)) {
                                AbsenceClockInVo offWork = new AbsenceClockInVo(userId, rule.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(rule.getOutLackPoint(), DateDetail.DF2));
                                absenceList.add(offWork);
                            }
                        }
                    });
                });
            }
        });
        return map;
    }

    private Map<String, List<AbsenceClockInVo>> initAbsenceInfo(DateDetail dateDetail) {

        dateDetail.changeDay(new Date());
        Date today = dateDetail.getCurrentDate();
        dateDetail.getYesterday();
        Date yesterday = dateDetail.getCurrentDate();
        // 查询昨天和今天的排班信息
        Map<String, List<AbsenceClockInVo>> map = new HashMap<>();
        // 查询昨天所有下班缺卡时间是今天的出勤规则(普班)
        log.error("查询昨天所有下班缺卡时间是今天的出勤规则, 参数: {}, {}", today, yesterday);
        List<FtbAttendanceDailyRule> list = attendanceDailyRuleMapper.getRuleListByDate(today, yesterday, null);
        if (!list.isEmpty()) {
            list.forEach(v -> {
                List<AbsenceClockInVo> voList = map.computeIfAbsent(v.getUserId(), k -> new ArrayList<>());
                // 请假可以导致昨天的班次上班时间是今天
                if (null != v.getInLackPoint() && DateDetail.getDate2Str(v.getInLackPoint(), DateDetail.DF).equals(DateDetail.getDate2Str(today, DateDetail.DF))) {
                    AbsenceClockInVo vo = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(v.getInLackPoint(), DateDetail.DF2));
                    voList.add(vo);
                }
                AbsenceClockInVo vo = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(v.getOutLackPoint(), DateDetail.DF2));
                voList.add(vo);
            });
            log.error("数据不为空, 条数为: {}", list.size());
        }
        // 查询昨天所有最后一个班次是加班的出勤规则(加班)
        List<FtbAttendanceDailyRule> overTimeList = attendanceDailyRuleMapper.getOverTimeRuleListByDate(yesterday, null);
        if (!overTimeList.isEmpty()) {
            AtomicInteger count = new AtomicInteger(0);
            overTimeList.forEach(v -> {
                List<AbsenceClockInVo> voList = map.computeIfAbsent(v.getUserId(), k -> new ArrayList<>());
                // 加班下班缺卡时间
                Date nextDate = getOverTimeOffWorkAbsenceTime(dateDetail, v.getId(), v.getInPoint(), v.getUserId());
                String dateStr = DateDetail.getDate2Str(today, DateDetail.DF16);
                if (nextDate.after(DateDetail.getStr2DateTime(dateStr))) {
                    // 缺卡时间在今天
                    AbsenceClockInVo vo = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(nextDate, DateDetail.DF2));
                    voList.add(vo);
                    count.getAndIncrement();
                }
            });
            log.error("定时缺卡 {}- 出勤规则为昨日加班, 需要在今天缺卡的条数: {}", UserProvider.getUser().getTenantId(), count.get());
        }
        // 查询今天所有出勤规则(加班判断出勤规则是否是今天, 否, 不生成旷工时间点到redis)
        List<FtbAttendanceDailyRule> currentDailyRuleList = getCurrentDailyRuleList(today, null);
        calculateAbsenceTime("生成缺卡任务", today, currentDailyRuleList, map, dateDetail);
        return map;
    }

    private void calculateAbsenceTime(String entrance, Date today, List<FtbAttendanceDailyRule> currentDailyRuleList, Map<String, List<AbsenceClockInVo>> map, DateDetail dateDetail) {
        if (!currentDailyRuleList.isEmpty()) {
            // 筛选出 普班/加班
            currentDailyRuleList = currentDailyRuleList.stream()
                    .filter(v -> v.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || v.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode()))
                    .collect(Collectors.toList());
            // 今天晚上24点的日期
            String dateStr = DateDetail.getDate2Str(today, DateDetail.DF16);
            dateDetail.changeDay(DateDetail.getStr2DateTime(dateStr));
            dateDetail.getTomorrow();
            Date tomorrow = dateDetail.getCurrentDate();
            log.error("定时缺卡 {}- {}- 计算旷工时间, 参数: {}, {}", UserProvider.getUser().getTenantId(), entrance, dateStr, tomorrow);
            AtomicInteger count = new AtomicInteger(0);
            currentDailyRuleList.forEach(v -> {
                List<AbsenceClockInVo> voList = map.computeIfAbsent(v.getUserId(), k -> new ArrayList<>());
                if (v.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) {
                    // 普班
                    AbsenceClockInVo vo1 = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(v.getInLackPoint(), DateDetail.DF2));
                    voList.add(vo1);
                    count.getAndIncrement();
                    if (v.getOutLackPoint().before(tomorrow)) {
                        AbsenceClockInVo vo2 = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(v.getOutLackPoint(), DateDetail.DF2));
                        voList.add(vo2);
                        count.getAndIncrement();
                    }
                } else {
                    // 加班
                    if (v.getOutPoint().before(tomorrow)) {
                        // 下班时间在今天, 上班缺卡就在今天
                        AbsenceClockInVo vo3 = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(v.getOutPoint(), DateDetail.DF2));
                        voList.add(vo3);
                        count.getAndIncrement();
                        Date nextDate = getOverTimeOffWorkAbsenceTime(dateDetail, v.getId(), v.getInPoint(), v.getUserId());
                        if (nextDate.before(tomorrow)) {
                            // 下班缺卡时间在明天之前
                            AbsenceClockInVo vo4 = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(nextDate, DateDetail.DF2));
                            voList.add(vo4);
                            count.getAndIncrement();
                        }
                    }
                }
            });
            log.error("定时缺卡 {}- {}- 今日需要缺卡的条数: {}", UserProvider.getUser().getTenantId(), entrance, count.get());
        }
    }

    /**
     * 获取加班下班缺卡时间
     *
     * @param dateDetail 工具
     * @param ruleId     规则id
     * @param inPoint    上班时间
     * @param userId     用户id
     * @return java.util.Date
     */
    private Date getOverTimeOffWorkAbsenceTime(DateDetail dateDetail, String ruleId, Date inPoint, String userId) {

        dateDetail.changeDay(inPoint);
        dateDetail.getTomorrow();
        Date nextDate = dateDetail.getCurrentDate();
        FtbAttendanceDailyRule nextRule = attendanceDailyRuleMapper.getPreviousOrNextRule(ruleId, inPoint, nextDate, userId, "next");
        if (null != nextRule) {
            nextDate = nextRule.getInPoint();
        }
        return nextDate;
    }

    private void dealChooseRecord(List<AttendanceClockInResult> resultList, List<FtbAttendanceClockIn> clockInList, Map<String, AttendanceClockInResult> dbResultMap, Map<Integer, FtbAttendanceClockIn> chooseMap,
                                  String type, AttendanceRuleVo rule, Integer workType, UserInfo userInfo) throws HandleException {
        if (onWorkNotNeed(rule, workType) && !AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) && !getOutsideCheck(rule, workType)) {
            // 上班无需打卡, 生成-2
            AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, workType, rule.getInLackPoint(), userInfo);
            resultList.add(result);
            return;
        }
        if (offWorkNotNeed(rule, workType) && !AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) && !getOutsideCheck(rule, workType)) {
            // 下班无需打卡, 生成-2
            AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, workType, rule.getOutLackPoint(), userInfo);
            resultList.add(result);
            return;
        }
        assert !chooseMap.isEmpty();
        int index;
        if (type.equals(ConstantUtil.MAX)) {
            index = chooseMap.keySet().stream().max(Integer::compareTo).get();
        } else {
            index = chooseMap.keySet().stream().min(Integer::compareTo).get();
        }
        FtbAttendanceClockIn clockIn = chooseMap.get(index);
        AttendanceClockInResult result = generateClockInResultRecord(rule, clockIn, dbResultMap.get(clockIn.getId()), workType, userInfo);
        resultList.add(result);
        clockInList.remove(index);
        clockInList.removeIf(v -> v.getClockInTime().before(clockIn.getClockInTime()));
    }

    /**
     * 生成旷工记录
     *
     * @param rule     出勤规则
     * @param workType 打卡类型(1: 上班, 2: 下班)
     * @param userInfo 用户信息
     * @return jnpf.entity.attendance.AttendanceClockInResult
     */
    private AttendanceClockInResult generateNoClockRecord(AttendanceRuleVo rule, Integer workType, Date keyPoint, UserInfo userInfo) {

        if (null == keyPoint) {
            return generateNoClock(rule, workType, userInfo);
        } else {
            if (new Date().after(keyPoint)) {
                return generateNoClock(rule, workType, userInfo);
            }
        }
        return null;
    }

    private AttendanceClockInResult generateNoClock(AttendanceRuleVo rule, Integer workType, UserInfo userInfo) {

        AttendanceClockInResult result = new AttendanceClockInResult();
        result.setId(FtbUtil.getId());
        result.setRuleId(rule.getId());
        result.setUserId(userInfo.getUserId());
        result.setClockInType(workType);
        result.setClockInKind(ConstantUtil.KIND_NORMAL);
        result.setClockInStatus(ClockInStatusEnum.NO_CLOCK.getValue());
        result.setAbsence(ConstantUtil.NUM_FALSE);
        result.setAbnormalMinute(0);
        if (onWorkNotNeed(rule, workType) || offWorkNotNeed(rule, workType)
                || Objects.equals(rule.getApplyViewEnable(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || getOutsideCheck(rule, workType)) {
            result.setClockInStatus(ClockInStatusEnum.NEED_NOT.getValue());
            if (workType.equals(ConstantUtil.ON_WORK)) {
                result.setEffectiveTime(rule.getInPoint());
            } else {
                result.setEffectiveTime(rule.getOutPoint());
                if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) {
                    int restMinute;
                    if (null == rule.getBreakStartPoint() || null == rule.getBreakEndPoint()) {
                        restMinute = 0;
                    } else {
                        restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint());
                    }
                    result.setRestMinute(restMinute);
                }
            }
        } else {
            Integer wt = workType.equals(ConstantUtil.OFF_WORK) ? ConstantUtil.ON_WORK : ConstantUtil.OFF_WORK;
            // 判断成对的班次是否也是缺卡
            LambdaQueryWrapper<AttendanceClockInResult> query = new LambdaQueryWrapper<AttendanceClockInResult>()
                    .eq(AttendanceClockInResult::getRuleId, rule.getId())
                    .eq(AttendanceClockInResult::getClockInType, wt)
                    .eq(AttendanceClockInResult::getClockInStatus, ClockInStatusEnum.NO_CLOCK.getValue())
                    .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE)
                    .last("limit 1");
            AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectOne(query);
            result.setAttendanceType(rule.getAttendanceType());
            if (null != clockInResult && !rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                // 加班不旷工
                LambdaUpdateWrapper<AttendanceClockInResult> update = new LambdaUpdateWrapper<AttendanceClockInResult>()
                        .set(AttendanceClockInResult::getAbsence, ConstantUtil.NUM_TRUE)
                        .eq(AttendanceClockInResult::getId, clockInResult.getId());
                attendanceClockInResultMapper.update(null, update);
                result.setAbsence(ConstantUtil.NUM_TRUE);
            }
        }
        return result;
    }

    private MutablePair<AttendanceClockInResult, ConditionVo> generateNoClockAndCondition(AttendanceRuleVo rule, Integer workType, UserInfo userInfo) {

        ConditionVo condition = null;
        AttendanceClockInResult result = new AttendanceClockInResult();
        result.setId(FtbUtil.getId());
        result.setRuleId(rule.getId());
        result.setUserId(userInfo.getUserId());
        result.setClockInType(workType);
        result.setClockInKind(ConstantUtil.KIND_NORMAL);
        result.setClockInStatus(ClockInStatusEnum.NO_CLOCK.getValue());
        result.setAbsence(ConstantUtil.NUM_FALSE);
        result.setAbnormalMinute(0);
        if (onWorkNotNeed(rule, workType) || offWorkNotNeed(rule, workType)
                || Objects.equals(rule.getApplyViewEnable(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || getOutsideCheck(rule, workType)) {
            result.setClockInStatus(ClockInStatusEnum.NEED_NOT.getValue());
            if (workType.equals(ConstantUtil.ON_WORK)) {
                result.setEffectiveTime(rule.getInPoint());
            } else {
                result.setEffectiveTime(rule.getOutPoint());
                if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) {
                    int restMinute;
                    if (null == rule.getBreakStartPoint() || null == rule.getBreakEndPoint()) {
                        restMinute = 0;
                    } else {
                        restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint());
                    }
                    result.setRestMinute(restMinute);
                }
            }
        } else {
            Integer wt = workType.equals(ConstantUtil.OFF_WORK) ? ConstantUtil.ON_WORK : ConstantUtil.OFF_WORK;
            condition = new ConditionVo(rule.getId(), wt);
        }
        return MutablePair.of(result, condition);
    }

    /**
     * 上班无需打卡
     */
    private boolean onWorkNotNeed(AttendanceRuleVo rule, Integer workType) {

        return workType.equals(ConstantUtil.ON_WORK) && rule.isOnWorkIgnore();
    }

    /**
     * 下班无需打卡
     */
    private boolean offWorkNotNeed(AttendanceRuleVo rule, Integer workType) {

        return workType.equals(ConstantUtil.OFF_WORK) && rule.isOffWorkIgnore();
    }

    /**
     * 生成缺卡记录[重新匹配打卡记录]
     * @param rule 出勤规则
     * @param onWorkResult 上班打卡(workTye为下班时传)
     * @param workType 上班/下班
     * @param keyPoint 缺卡时间点
     * @param userInfo 用户信息
     * @return jnpf.entity.attendance.AttendanceClockInResult
     */
    private AttendanceClockInResult generateNoClockRecordRematch(AttendanceRuleVo rule, AttendanceClockInResult onWorkResult, Integer workType, Date keyPoint, UserInfo userInfo) {

        if (null == keyPoint) {
            return generateNoClockRematch(rule, onWorkResult, workType, userInfo);
        } else {
            if (new Date().after(keyPoint)) {
                return generateNoClockRematch(rule, onWorkResult, workType, userInfo);
            }
        }
        return null;
    }

    private AttendanceClockInResult generateNoClockRematch(AttendanceRuleVo rule, AttendanceClockInResult onWorkResult, Integer workType, UserInfo userInfo) {

        AttendanceClockInResult result = new AttendanceClockInResult();
        result.setId(FtbUtil.getId());
        result.setRuleId(rule.getId());
        result.setUserId(userInfo.getUserId());
        result.setClockInType(workType);
        result.setClockInKind(ConstantUtil.KIND_NORMAL);
        result.setClockInStatus(ClockInStatusEnum.NO_CLOCK.getValue());
        result.setAbsence(ConstantUtil.NUM_FALSE);
        result.setAbnormalMinute(0);
        if (onWorkNotNeed(rule, workType) || offWorkNotNeed(rule, workType)
                || Objects.equals(rule.getApplyViewEnable(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || getOutsideCheck(rule, workType)) {
            result.setClockInStatus(ClockInStatusEnum.NEED_NOT.getValue());
            if (workType.equals(ConstantUtil.ON_WORK)) {
                result.setEffectiveTime(rule.getInPoint());
            } else {
                result.setEffectiveTime(rule.getOutPoint());
                if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) {
                    int restMinute;
                    if (null == rule.getBreakStartPoint() || null == rule.getBreakEndPoint()) {
                        restMinute = 0;
                    } else {
                        restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint());
                    }
                    result.setRestMinute(restMinute);
                }
            }
        } else {
            if (workType.equals(ConstantUtil.OFF_WORK)) {
                if (null != onWorkResult && !rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                    // 判断上班是否也是缺卡(加班不计旷工)
                    if (onWorkResult.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) {
                        // 设置为旷工
                        onWorkResult.setAbsence(ConstantUtil.NUM_TRUE);
                        result.setAbsence(ConstantUtil.NUM_TRUE);
                    }
                }
            }
        }
        return result;
    }

    /**
     * 筛选打卡记录
     *
     * @param startPoint 开始时间
     * @param endPoint   结束时间
     * @return java.util.Map<java.lang.Integer, jnpf.entity.attendance.FtbAttendanceClockIn>
     */
    private Map<Integer, FtbAttendanceClockIn> getChooseClockRecord(Date startPoint, Date endPoint, List<FtbAttendanceClockIn> clockInList) {

        Map<Integer, FtbAttendanceClockIn> map = new HashMap<>();
        for (int i = 0; i < clockInList.size(); i++) {
            if (DateDetail.checkTimeBetween(clockInList.get(i).getClockInTime(), startPoint, endPoint)) {
                map.put(i, clockInList.get(i));
            } else {
                if (clockInList.get(i).getClockInTime().after(endPoint)) {
                    break;
                }
            }
        }
        return map;
    }

    private AttendanceClockInResult generateClockInResultRecord(AttendanceRuleVo rule, FtbAttendanceClockIn clockIn, AttendanceClockInResult dbResult, Integer clockInType, UserInfo userInfo) throws HandleException {

        if (null == dbResult) {
            // 根据打卡id查询打卡结果(变更多次会产生id绑定了多个结果, 时间从大到小获取第一个)
            LambdaQueryWrapper<AttendanceClockInResult> lambdaQueryWrapper = new LambdaQueryWrapper<AttendanceClockInResult>()
                    .eq(AttendanceClockInResult::getClockInId, clockIn.getId())
                    .orderByDesc(AttendanceClockInResult::getCreatorTime)
                    .last("limit 1");
            dbResult = attendanceClockInResultMapper.selectOne(lambdaQueryWrapper);
        }

        AttendanceClockInResult clockInResult = new AttendanceClockInResult();
        clockInResult.setId(FtbUtil.getId());
        clockInResult.setRuleId(getRuleId(rule));
        clockInResult.setUserId(userInfo.getUserId());
        clockInResult.setRepaired(null == dbResult ? ConstantUtil.NUM_FALSE : dbResult.getRepaired());
        clockInResult.setClockInId(clockIn.getId());
        clockInResult.setClockInType(clockInType);
        clockInResult.setClockInKind(clockIn.getClockInKind());
        clockInResult.setAbsence(ConstantUtil.NUM_FALSE);
        clockInResult.setOldResultId(null == dbResult ? null : dbResult.getId());
        AttendanceTypeEnum attendanceType = AttendanceTypeEnum.getEnum(rule.getAttendanceType());
        if (null == attendanceType) {
            throw new HandleException("未知类型");
        }
        if (clockIn.getApprovalStatus().equals(ConstantUtil.PASS_APPROVAL)) {
            if (null == clockIn.getApplyType()) {
                throw new HandleException("打卡异常，请刷新页面后重试");
            }
            clockInResult.setApplyType(clockIn.getApplyType().getValue());
            clockInResult.setApplyId(clockIn.getApprovalCode());
        }
        if (clockInType.equals(ConstantUtil.OFF_WORK) && rule.getOvertime().equals(ConstantUtil.NUM_TRUE)) {
            // 判定打卡时间是否超过配置的加班最低时间, 超过则申请打加班卡, 并生成当前出勤规则的下班无需打卡, 和加班的上班无需打卡
            int minute;
            Date beginDate;
            if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())) {
                beginDate = rule.getOnWorkInfoVo().getClockInTime();
            } else {
                beginDate = rule.getOutPoint();
            }
            minute = DateDetail.calculateMinuteDiff(beginDate, clockIn.getClockInTime());
            if (minute >= rule.getOvertimeRuleDetail().getMinOvertimeMinute()) {
                // 加班规则
                FtbAttendanceDailyRule overtimeRule = attendanceDailyRuleService.workOvertimeNotApprove(generateHandleParam(rule.getOvertimeRuleDetail(), userInfo, rule, beginDate, clockIn.getClockInTime()));
                AttendanceRuleVo rVo = JsonUtil.getJsonToBean(overtimeRule, AttendanceRuleVo.class);
                if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())) {
                    // 删除上班卡与休/未排班的关联, 关联到加班打卡上
                    attendanceClockInResultMapper.updateResultRelation(rule.getOnWorkInfoVo().getId(), overtimeRule.getId());
                } else {
                    // 下班无需打卡
                    rule.setOffWorkIgnore(true);
                    AttendanceClockInResult result = generateNoClock(rule, clockInType, userInfo);
                    // 加班上班无需打卡
                    rVo.setOnWorkIgnore(true);
                    AttendanceClockInResult notNeedOnWork = generateNoClock(rVo, ConstantUtil.ON_WORK, userInfo);
                    attendanceClockInResultMapper.insert(result);
                    attendanceClockInResultMapper.insert(notNeedOnWork);
                    attendanceDailyRuleService.clockOutHandle(generateHandleParam(null, userInfo, rVo, rVo.getInPoint(), clockIn.getClockInTime()));
                }
                // 加班下班卡
                clockInResult.setRuleId(rVo.getId());
                clockInResult.setEffectiveTime(clockIn.getClockInTime());
                clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue());
                clockInResult.setAbnormalMinute(0);
                // 生成redis缺卡缓存 用于加班存休
                String todayStr = DateDetail.getDate2Str(overtimeRule.getDay(), DateDetail.DF);
                String hashKey = userInfo.getTenantId() + ":" + todayStr;
                absenceTimeChange(overtimeRule.getUserId(), overtimeRule.getDay(), hashKey);
                return clockInResult;
            }
        }
        // 如果标识为外出和出差 且 出勤规则不是请假、休、未排班
        if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || getOutsideCheck(rule, clockInType)) {
            if (AttendanceTypeEnum.ORDINARY.equals(attendanceType) || AttendanceTypeEnum.WORKOVERTIME.equals(attendanceType)) {
                // 有效打卡时间为满勤(有效打卡时间为上班时间或下班时间), -休息时间
                if (clockInType.equals(ConstantUtil.ON_WORK)) {
                    clockInResult.setEffectiveTime(rule.getInPoint());
                } else {
                    if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) {
                        int restMinute;
                        if (null == rule.getBreakStartPoint() || null == rule.getBreakEndPoint()) {
                            restMinute = 0;
                        } else {
                            restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint());
                        }
                        clockInResult.setRestMinute(restMinute);
                    }
                    clockInResult.setEffectiveTime(rule.getOutPoint());
                }
                clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue());
                clockInResult.setAbnormalMinute(0);
                return clockInResult;
            }
        }
        switch (attendanceType) {
            case ORDINARY:
                if (clockIn.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE)) {
                    dealWorkOutside(clockInResult, clockInType, clockIn, rule, userInfo);
                } else {
                    DateDetail dateDetail = new DateDetail();
                    if (clockInType.equals(ConstantUtil.ON_WORK)) {
                        // 上班 是否开启休息 当前是否在休息时间 是否允许迟到
                        Date onWorkTime = clockIn.getClockInTime();
                        if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) {
                            if (null != rule.getBreakStartPoint() && null != rule.getBreakEndPoint()) {
                                onWorkTime = DateDetail.checkTimeBetween(clockIn.getClockInTime(), rule.getBreakStartPoint(), rule.getBreakEndPoint()) ? rule.getBreakStartPoint() : clockIn.getClockInTime();
                            }
                        }
                        Date effectiveTime;
                        int abnormalMinute = 0;
                        ClockInStatusEnum statusEnum = ClockInStatusEnum.NORMAL;
                        // 是否允许迟到, 是 判断迟到了多久, 否 迟到提示无法打卡
                        if (rule.getLateEnable().equals(ConstantUtil.NUM_FALSE)) {
                            // 不允许迟到
                            Date lateDate = dateDetail.setSecond(rule.getInPoint(), 0);
                            if (onWorkTime.after(lateDate)) {
                                throw new HandleException("暂不允许迟到打卡");
                            }
                            effectiveTime = rule.getInPoint();
                        } else {
                            // 允许迟到 迟到时间 +59 秒[暂弃]
                            Date lateDate = dateDetail.setSecond(rule.getLatePoint(), 0);
                            Date inLackPoint = DateDetail.goToMaxSeconds(rule.getInLackPoint());
                            if (clockIn.getClockInTime().after(inLackPoint)) {
                                throw new HandleException("上班打卡时间已结束");
                            }
                            if (clockIn.getClockInTime().after(lateDate)) {
                                statusEnum = ClockInStatusEnum.WORK_LATE;
                            }
                            if (onWorkTime.after(lateDate)) {
                                abnormalMinute = DateDetail.calculateSecondDiff(rule.getLatePoint(), onWorkTime);
                                effectiveTime = dateDetail.addNum(rule.getInPoint(), abnormalMinute, Calendar.SECOND);
                                // 计算迟到分钟数 如果打卡时间在休息时间后, 迟到分钟数 - 休息分钟数
                                /*if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE) && onWorkTime.after(rule.getBreakEndPoint())) {
                                    int restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint());
                                    abnormalMinute -= restMinute;
                                }*/
                            } else {
                                effectiveTime = rule.getInPoint();
                            }
                        }
                        clockInResult.setEffectiveTime(effectiveTime);
                        clockInResult.setClockInStatus(statusEnum.getValue());
                        clockInResult.setAbnormalMinute(abnormalMinute);
                    } else {
                        // 下班 是否开启休息 当前是否在休息时间 是否允许早退 (下班需要计算休息时间是多少分钟)
                        Date offWorkTime = clockIn.getClockInTime();
                        // 下班开始时间
                        Date offWorkBeginTime;
                        if (rule.getEarlyEnable().equals(ConstantUtil.NUM_FALSE)) {
                            offWorkBeginTime = rule.getOutPoint();
                        } else {
                            offWorkBeginTime = rule.getEarlyPoint();
                        }
                        // 打卡时间不在 上班缺卡时间点 - 下班缺卡时间点之间, 无法打卡
                        Date outLackPoint = DateDetail.goToMaxSeconds(rule.getOutLackPoint());
                        if (!DateDetail.checkTimeBetween(offWorkTime, offWorkBeginTime, outLackPoint)) {
                            throw new HandleException("未到下班打卡时间");
                        }
                        int restMinute;
                        if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) {
                            if (null != rule.getBreakStartPoint() && null != rule.getBreakEndPoint()) {
                                offWorkTime = DateDetail.checkTimeBetween(clockIn.getClockInTime(), rule.getBreakStartPoint(), rule.getBreakEndPoint()) ? rule.getBreakEndPoint() : clockIn.getClockInTime();
                                restMinute = getRestMinute(rule, clockIn.getClockInTime());
                            } else {
                                offWorkTime = clockIn.getClockInTime();
                                restMinute = 0;
                            }
                            clockInResult.setRestMinute(restMinute);
                        }
                        Date effectiveTime;
                        int abnormalMinute = 0;
                        ClockInStatusEnum statusEnum = ClockInStatusEnum.NORMAL;
                        // 是否允许早退
                        if (rule.getEarlyEnable().equals(ConstantUtil.NUM_FALSE)) {
                            // 不允许早退
                            if (offWorkTime.before(rule.getOutPoint())) {
                                throw new HandleException("暂不允许早退打卡");
                            }
                            effectiveTime = rule.getOutPoint();
                        } else {
                            // 允许早退
                            if (DateDetail.checkTimeBetween(clockIn.getClockInTime(), rule.getEarlyPoint(), rule.getOutPoint())) {
                                statusEnum = ClockInStatusEnum.HOME_EARLY;
                                effectiveTime = offWorkTime;
                                abnormalMinute = DateDetail.calculateSecondDiff(DateDetail.getStr2DateTime(DateDetail.getDate2Str(clockIn.getClockInTime(), DateDetail.DF2)), rule.getOutPoint());
                                // 计算早退分钟数 如果打卡时间在休息时间前, 早退分钟数 - 休息分钟数
                                /*if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE) && offWorkTime.before(rule.getBreakStartPoint())) {
                                    abnormalMinute -= restMinute;
                                }*/
                            } else {
                                // 正常下班
                                effectiveTime = rule.getOutPoint();
                            }
                        }
                        clockInResult.setEffectiveTime(effectiveTime);
                        clockInResult.setClockInStatus(statusEnum.getValue());
                        clockInResult.setAbnormalMinute(abnormalMinute);
                        // 晚走晚到通知
                        attendanceDailyRuleService.clockOutHandle(generateHandleParam(null, userInfo, rule, rule.getOutPoint(), clockIn.getClockInTime()));
                    }
                }
                break;
            case LEAVE:
                throw new HandleException("请假无需打卡");
            case WORKOVERTIME:
                dealWorkOvertime(clockInResult, clockInType, clockIn, rule, userInfo);
                break;
            case DEFAULT:
            case REST:
            case BUSINESS_TRIP:
            case STEP_OUT:
                clockInResult.setEffectiveTime(clockIn.getClockInTime());
                clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue());
                clockInResult.setAbnormalMinute(0);
                break;
            default:
                throw new HandleException("未知打卡种类");
        }
        return clockInResult;
    }

    private String getRuleId(AttendanceRuleVo rule) {

        if (StringUtils.isNotEmpty(rule.getId())) {
            return rule.getId();
        }
        // 未排班(-1) 需要赋值id并insert到数据库
        rule.setId(FtbUtil.getId());
        FtbAttendanceDailyRule dailyRule = JsonUtil.getJsonToBean(rule, FtbAttendanceDailyRule.class);
        attendanceDailyRuleMapper.insert(dailyRule);
        return dailyRule.getId();
    }

    private ClockOutHandleParam generateHandleParam(OvertimeRuleDetailVo ruleDetail, UserInfo userInfo, AttendanceRuleVo rule, Date startTime, Date endTime) {

        return ClockOutHandleParam.builder()
                .groupId(rule.getGroupId())
                .ruleDetail(ruleDetail)
                .userId(userInfo.getUserId())
                .tenantId(userInfo.getTenantId())
                .ruleId(rule.getId())
                .attendanceType(rule.getAttendanceType())
                .day(rule.getDay())
                .applyId(rule.getApplyId())
                .start(startTime)
                .end(endTime)
                .build();
    }

    @Override
    public boolean getOutsideCheck(AttendanceRuleVo rule, Integer clockInType) {

        if (!rule.getApplyViewEnable().equals(AttendanceTypeEnum.STEP_OUT.getCode())) {
            return false;
        }
        if (ConstantUtil.ON_WORK.equals(clockInType)) {
            return rule.getInStepOutType() != null && !rule.getInStepOutType().equals(ConstantUtil.NUM_FALSE);
        }
        if (ConstantUtil.OFF_WORK.equals(clockInType)) {
            return rule.getOutStepOutType() != null && !rule.getOutStepOutType().equals(ConstantUtil.NUM_FALSE);
        }
        return false;
    }

    private int getRestMinute(AttendanceRuleVo rule, Date clockInTime) {

        // 上班时间在休息结束时间后, 休息时间为0
        int restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint());
        if (null != rule.getOnWorkInfoVo() && !rule.getOnWorkInfoVo().getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())
                && !rule.getOnWorkInfoVo().getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue())
                && rule.getOnWorkInfoVo().getClockInTime().after(rule.getBreakEndPoint())) {
            // 上班时间在休息结束时间之后
            restMinute = 0;
        }
        if (clockInTime.before(rule.getBreakStartPoint())) {
            // 下班时间在休息开始时间之前
            restMinute = 0;
        }
        return restMinute;
    }

    /**
     * 外勤处理
     *
     * @param clockInResult 打卡结果
     * @param clockInType   上班/下班
     * @param clockIn       打卡记录
     * @param rule          出勤规则
     */
    private void dealWorkOutside(AttendanceClockInResult clockInResult, Integer clockInType, FtbAttendanceClockIn clockIn, AttendanceRuleVo rule, UserInfo userInfo) {
        Date effective;
        if (clockInType.equals(ConstantUtil.ON_WORK)) {
            effective = clockIn.getClockInTime().after(rule.getInPoint()) ? clockIn.getClockInTime() : rule.getInPoint();
            if (null != rule.getBreakEnable() && rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) {
                effective = DateDetail.checkTimeBetween(effective, rule.getBreakStartPoint(), rule.getBreakEndPoint()) ? rule.getBreakStartPoint() : effective;
            }
        } else {
            effective = clockIn.getClockInTime().before(rule.getOutPoint()) ? clockIn.getClockInTime() : rule.getOutPoint();
            if (null != rule.getBreakEnable() && rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)
                    && (null != rule.getBreakStartPoint() || null != rule.getBreakEndPoint())) {
                effective = DateDetail.checkTimeBetween(effective, rule.getBreakStartPoint(), rule.getBreakEndPoint()) ? rule.getBreakEndPoint() : effective;
                int restMinute = getRestMinute(rule, clockIn.getClockInTime());
                clockInResult.setRestMinute(restMinute);
            }
            // 晚走晚到通知
            attendanceDailyRuleService.clockOutHandle(generateHandleParam(null, userInfo, rule, rule.getOutPoint(), clockIn.getClockInTime()));
        }
        clockInResult.setEffectiveTime(effective);
        clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue());
        clockInResult.setAbnormalMinute(0);
    }

    /**
     * 加班处理
     *
     * @param clockInResult 打卡结果
     * @param clockInType   上班/下班
     * @param clockIn       打卡记录
     * @param rule          出勤规则
     */
    private void dealWorkOvertime(AttendanceClockInResult clockInResult, Integer clockInType, FtbAttendanceClockIn clockIn, AttendanceRuleVo rule, UserInfo userInfo) {
        Date effective;
        if (clockInType.equals(ConstantUtil.ON_WORK)) {
            if (rule.getOvertimeRuleDetail().getCalcMethod().equals(1)) {
                effective = rule.getInPoint();
            } else {
                effective = clockIn.getClockInTime().after(rule.getInPoint()) ? clockIn.getClockInTime() : rule.getInPoint();
            }
        } else {
            if (rule.getOvertimeRuleDetail().getCalcMethod().equals(1)) {
                effective = rule.getOutPoint();
            } else if (rule.getOvertimeRuleDetail().getCalcMethod().equals(2)) {
                effective = clockIn.getClockInTime().before(rule.getOutPoint()) ? clockIn.getClockInTime() : rule.getOutPoint();
            } else {
                // 无需审批 加班更新打卡 tips: 无需审批的加班下班卡不在此处, 前面的逻辑就处理后返回了, 正常情况只有加班更新打卡才会走到这个地方
                attendanceDailyRuleService.workOvertimeNotApprove(generateHandleParam(rule.getOvertimeRuleDetail(), userInfo, rule, rule.getInPoint(), clockIn.getClockInTime()));
                effective = clockIn.getClockInTime();
            }
        }
        clockInResult.setEffectiveTime(effective);
        clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue());
        clockInResult.setAbnormalMinute(0);
        attendanceDailyRuleService.clockOutHandle(generateHandleParam(null, userInfo, rule, rule.getInPoint(), clockIn.getClockInTime()));
    }

    private FtbAttendanceClockIn setClockInRecord(MutablePair<GroupInfoVo, AttendanceRuleVo> pair, ClockInDto clockInDto, String relationId, UserInfo userInfo) throws HandleException {

        FtbAttendanceClockIn clockIn = new FtbAttendanceClockIn();
        clockIn.setId(FtbUtil.getId());
        clockIn.setDay(pair.getRight().getDay());
        clockIn.setUserId(userInfo.getUserId());
        clockIn.setClockInTime(new Date());
        clockIn.setApprovalStatus(ConstantUtil.PASS_TRUE);
        clockIn.setClockInKind(clockInDto.getClockInKind());
        clockIn.setApprovalCode(clockInDto.getApprovalCode());
        clockIn.setApplyType(clockInDto.getApplyType());
        if (clockInDto.getClockInKind() == ConstantUtil.KIND_OUTSIDE) {
            // 外勤打卡
            if (pair.getLeft().getGroupRule().getFieldClockStatus().equals(ConstantUtil.NUM_FALSE)) {
                throw new HandleException("暂未开启外勤打卡");
            } else {
                if (pair.getLeft().getGroupRule().getApproveEnable().equals(ConstantUtil.NUM_TRUE)) {
                    clockIn.setApprovalStatus(ConstantUtil.PASS_APPROVAL);
                    clockIn.setRelationId(relationId);
                }
            }
        }
        if (ApplyTypeEnum.VIOLATION.equals(clockIn.getApplyType())) {
            // 异常打卡
            clockIn.setApprovalStatus(ConstantUtil.PASS_APPROVAL);
            clockIn.setRelationId(relationId);
        }
        clockIn.setAddress(clockInDto.getAddress());
        clockIn.setLng(clockInDto.getLng());
        clockIn.setLat(clockInDto.getLat());
        clockIn.setDeviceType(clockInDto.getDeviceType());
        clockIn.setDeviceId(clockInDto.getDeviceId());
        clockIn.setDeviceName(clockInDto.getDeviceName());
        clockIn.setRemark(clockInDto.getRemark());
        return clockIn;
    }

    private MutablePair<GroupInfoVo, AttendanceRuleVo> getGroupInfoAndRule(Date date, UserInfo userInfo) throws Exception {

        List<GroupInfoVo> groupList = getClockInMainInfo(date, userInfo, ConstantUtil.NUM_FALSE);
        if (null == groupList || groupList.isEmpty()) {
            throw new QueryException("您尚未加入考勤组");
        }
        GroupInfoVo groupInfo = null;
        AttendanceRuleVo currentRule = null;
        for (GroupInfoVo vo : groupList) {
            AttendanceRuleVo rule = vo.getAttendanceRuleList().stream().filter(v -> ConstantUtil.NUM_TRUE == v.getCurrentPeriod()).findFirst().orElse(null);
            if (null != rule) {
                groupInfo = vo;
                currentRule = rule;
                break;
            }
        }
        if (null == groupInfo) {
            throw new QueryException("当天班次已变更，请重试！");
        }
        return MutablePair.of(groupInfo, currentRule);
    }

    private Integer getClockInType(AttendanceRuleVo currentRule) throws QueryException {

        Integer clockInType;
        if (null == currentRule.getOnWorkInfoVo()) {
            clockInType = ConstantUtil.ON_WORK;
        } else if (null == currentRule.getOffWorkInfoVo()) {
            clockInType = ConstantUtil.OFF_WORK;
        } else {
            throw new QueryException("当前无需打卡");
        }
        return clockInType;
    }

    /**
     * 设置用户出勤打卡记录
     *
     * @param rule     出勤规则
     */
    private void setWorkRecord(List<ClockInVo> clockInList, AttendanceRuleVo rule, GroupInfoVo groupInfo, Map<String, UserBoundInfoVO> userMap) {

        GroupRuleVo groupRule = groupInfo.getGroupRule();
        if (null == rule.getId()) {
            return;
        }
        // 设置上班打卡记录
        ClockInVo onWorkRecord = clockInList.stream().filter(v -> v.getRuleId().equals(rule.getId()) && v.getClockInType().equals(ConstantUtil.ON_WORK))
                .findFirst().orElse(null);
        // 设置下班打卡记录
        ClockInVo offWorkRecord = clockInList.stream().filter(v -> v.getRuleId().equals(rule.getId()) && v.getClockInType().equals(ConstantUtil.OFF_WORK))
                .findFirst().orElse(null);
        // 设置归属日期, 设置能否补卡
        setCouldRepairRecord(rule.getDay(), rule, onWorkRecord, groupRule);
        setCouldRepairRecord(rule.getDay(), rule, offWorkRecord, groupRule);
        if (null != onWorkRecord) {
            groupInfo.getClockInList().add(new DailyClockInVo(onWorkRecord.getId(), onWorkRecord.getClockInKind(), onWorkRecord.getClockInId()));
            if (StringUtils.isNotEmpty(onWorkRecord.getAbsenceLeader())) {
                onWorkRecord.setAbsenceLeaderName(getAbsenceLeaderName(userMap, onWorkRecord.getAbsenceLeader()));
            }
            if (!StringUtils.isEmpty(onWorkRecord.getApplyId())) {
                onWorkRecord.setApprovalStatus(ConstantUtil.PASS_APPROVAL);
            }
        }
        if (null != offWorkRecord) {
            groupInfo.getClockInList().add(new DailyClockInVo(offWorkRecord.getId(), offWorkRecord.getClockInKind(), offWorkRecord.getClockInId()));
            if (StringUtils.isNotEmpty(offWorkRecord.getAbsenceLeader())) {
                offWorkRecord.setAbsenceLeaderName(getAbsenceLeaderName(userMap, offWorkRecord.getAbsenceLeader()));
            }
            if (!StringUtils.isEmpty(offWorkRecord.getApplyId())) {
                offWorkRecord.setApprovalStatus(ConstantUtil.PASS_APPROVAL);
            }
        }
        rule.setOnWorkInfoVo(onWorkRecord);
        rule.setOffWorkInfoVo(offWorkRecord);
    }

    private String getAbsenceLeaderName(Map<String, UserBoundInfoVO> userMap, String absenceLeader) {

        // 查询变更人名称
        UserBoundInfoVO userBound = userMap.get(absenceLeader);
        if (null == userBound) {
            ActionResult<UserBoundInfoVO> usersBoundResult = v2UserApi.getUsersBound(absenceLeader, null);
            if (200 == usersBoundResult.getCode() && null != usersBoundResult.getData()) {
                userBound = usersBoundResult.getData();
            } else {
                userBound = new UserBoundInfoVO();
            }
            userMap.put(absenceLeader, userBound);
        }
        return userBound.getUserName();
    }

    private void setCouldRepairRecord(Date day, AttendanceRuleVo rule, ClockInVo workRecord, GroupRuleVo groupRule) {
        if (null == workRecord) {
            return;
        }
        // 设置归属日期
        workRecord.setDay(day);
        // 设置能否补卡 按月初1号 - 月末最后一天 统计
        try {
            AttendanceClockInResult clockInResult = new AttendanceClockInResult();
            clockInResult.setUserId(workRecord.getUserId());
            clockInResult.setRuleId(workRecord.getRuleId());
            clockInResult.setClockInStatus(workRecord.getClockInStatus());
            clockInResult.setAbsence(workRecord.getAbsence());
            if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || getOutsideCheck(rule, workRecord.getClockInType())) {
                // 外出和出差无需补卡
                workRecord.setCouldRepair(ConstantUtil.NUM_FALSE);
            } else {
                MutablePair<Date, Date> datePair = couldRepairRecord(day, clockInResult, workRecord.getApprovalStatus(), groupRule);
                if (DateDetail.checkTimeBetween(workRecord.getDay(), datePair.getLeft(), datePair.getRight())) {
                    workRecord.setCouldRepair(ConstantUtil.NUM_TRUE);
                } else {
                    workRecord.setCouldRepair(ConstantUtil.NUM_FALSE);
                }
            }
        } catch (Exception e) {
            workRecord.setCouldRepair(ConstantUtil.NUM_FALSE);
        }
    }

    public MutablePair<Date, Date> couldRepairRecord(Date day, AttendanceClockInResult result, Integer approvalStatus, GroupRuleVo groupRule) throws Exception {

        if (null == groupRule.getPatchClockStatus() || groupRule.getPatchClockStatus().equals(ConstantUtil.NUM_FALSE)) {
            throw new Exception("暂未开启允许补卡设置");
        }
        if (checkAbsence(result.getRuleId(), result.getUserId())) {
            throw new Exception("旷工无法补卡");
        }
        if (result.getClockInStatus().equals(ClockInStatusEnum.NORMAL.getValue())) {
            throw new Exception("正常打卡无需补卡");
        }
        approvalStatus = null == approvalStatus ? 0 : approvalStatus;
        if (!approvalStatus.equals(ConstantUtil.PASS_TRUE)) {
            throw new Exception("审批未通过的卡无法补卡");
        }
        // 查询申请补卡记录
        AttendanceRepair repair;
        int applyCount = 0;
        if (StringUtils.isNotEmpty(result.getApplyId())) {
            ApplyAttendanceRepair applyAttendanceRepair = applyAttendanceRepairMapper.selectById(result.getApplyId());
            String applyDate = DateDetail.getDate2Str(applyAttendanceRepair.getApplyDate(), DateDetail.DF);
            repair = attendanceRepairService.getAttendanceRepairByDate(applyDate, result.getUserId(), groupRule.getSelfGroupId());
        } else {
            // 查询补卡刷新次数
            repair = attendanceRepairService.getAttendanceRepair(result.getUserId(), groupRule.getSelfGroupId());
            if (null != repair) {
                // 统计当月已补卡次数
                String beginDate = String.format("%d-%02d-%02d", repair.getGenerateYear(), repair.getGenerateMonth(), repair.getGenerateDay());
                String endDate = DateDetail.getDate2Str(repair.getExpireDate(), DateDetail.DF);
                // 审批id不为空 并且 approvalStatus=1 说明是通过补卡审批进来的, 不需要将审批中的记录加到已审批次数上
                applyCount = attendanceClockInResultMapper.selectRepairApplyCount(result.getUserId(), groupRule.getSelfGroupId(), beginDate, endDate);
            } else {
                // 当月补卡次数未生成
                DateDetail dateDetail = new DateDetail(new Date());
                int year = dateDetail.getYear();
                int month = dateDetail.getMonth();
                int curDay = dateDetail.getDay();
                log.error("当月补卡次数未生成 -> 月份: {}, user: {}, groupId: {}", dateDetail.getMonth(), result.getUserId(), groupRule.getSelfGroupId());
                Date expireDate = dateDetail.changeMonthAndDate(dateDetail.getCurrentDate(), 1, groupRule.getPatchRefreshDay());
                repair = new AttendanceRepair(groupRule.getSelfGroupId(), result.getUserId(), year, month, curDay,
                        groupRule.getPatchClockNumber(), expireDate);
                attendanceRepairService.save(repair);
            }
        }
        if (null == repair) {
            throw new HandleException("未配置补卡次数刷新日期，user: " + result.getUserId() + ", groupId: " + groupRule.getSelfGroupId());
        }
        if (repair.getRepairNum() - applyCount <= 0) {
            throw new QueryException("当前暂无补卡次数");
        }
        MutablePair<Integer, List<Integer>> queryPair = getQueryPair(groupRule.getPatchType());
        boolean flag = result.getAbsence().equals(ConstantUtil.NUM_TRUE) && queryPair.getLeft().equals(ConstantUtil.NUM_TRUE);
        if (queryPair.getRight().contains(result.getClockInStatus())) {
            flag = !ClockInStatusEnum.NO_CLOCK.getValue().equals(result.getClockInStatus()) || !result.getAbsence().equals(ConstantUtil.NUM_TRUE) || !queryPair.getLeft().equals(ConstantUtil.NUM_FALSE);
        }
        if (!flag) {
            throw new Exception("当前考勤状态暂不允许补卡");
        }
        DateDetail dateDetail = new DateDetail();
        Date queryEndDate = DateDetail.endOfDay(dateDetail.getCurrentDate());
        Date queryBeginDate = DateDetail.beginOfDay(dateDetail.addNum(dateDetail.getCurrentDate(), -groupRule.getPatchCycleDay(), Calendar.DATE));
        return MutablePair.of(queryBeginDate, queryEndDate);
    }

    /**
     * 判断是否旷工
     *
     * @param ruleId 出勤规则
     * @param userId 用户id
     * @return boolean
     */
    private boolean checkAbsence(String ruleId, String userId) {

        int num = attendanceClockInResultMapper.countAbsence(ruleId, userId);
        return num == 2;
    }

    public GroupInfoVo getGroupInfoByDate(Date today, UserInfo userInfo) throws QueryException {

        customTenantUtil.checkOutTenant(userInfo.getTenantId());
        // 查询考勤组用户信息
        List<AttendanceGroupUser> groupUserList = getMyGroupList(userInfo.getUserId());
        // 自己的考勤组
        AttendanceGroupUser selfGroup = groupUserList.stream().filter(v -> v.getType().equals(1)).findFirst().orElse(null);
        if (selfGroup == null) {
            throw new QueryException("当前用户暂无考勤组");
        }
        // 借调的考勤组
        List<AttendanceGroupUser> borrowGroupList = groupUserList.stream().filter(v -> v.getType().equals(2)).collect(Collectors.toList());
        // 判断有无借调, 无借调, 取当前考勤组, 有借调, 根据timeJson判断用户当前是否处于借调时间内, 在, 取借调考勤组
        String groupId = selfGroup.getGroupId();
        int selfGroupInt = 1;
        if (!borrowGroupList.isEmpty()) {
            firstLoop:
            for (AttendanceGroupUser group : borrowGroupList) {
                if (StringUtil.isEmpty(group.getTimeJson())) {
                    continue;
                }
                JSONArray jsonArray = new JSONArray(group.getTimeJson());
                List<JSONObject> list = jsonArray.toList(JSONObject.class);
                for (JSONObject json : list) {
                    String startTimeStr = json.get("startTime").toString();
                    String endTimeStr = json.get("endTime").toString();
                    boolean b = DateDetail.checkTimeBetween(today, DateDetail.getStr2DateTime(startTimeStr), DateDetail.getStr2DateTime(endTimeStr));
                    if (b) {
                        groupId = group.getGroupId();
                        selfGroupInt = 0;
                        break firstLoop;
                    }
                }
            }
        }
        return getGroupInfo(today, groupId, selfGroupInt, userInfo);
    }

    /**
     * 用户一个月内所在的考勤组
     *
     * @param userId     用户id
     * @param monthBegin 月初
     * @param monthEnd   月末
     * @return java.util.List<java.lang.String>
     */
    private List<String> getMyGroupByMonth(String userId, Date monthBegin, Date monthEnd) throws QueryException {

        List<String> groupIds = new ArrayList<>();
        // 查询考勤组用户信息
        List<AttendanceGroupUser> groupUserList = getMyGroupList(userId);
        // 自己的考勤组
        AttendanceGroupUser selfGroup = groupUserList.stream().filter(v -> v.getType().equals(1)).findFirst().orElse(null);
        assert selfGroup != null;
        groupIds.add(selfGroup.getGroupId());
        // 借调的考勤组
        List<AttendanceGroupUser> borrowGroupList = groupUserList.stream().filter(v -> v.getType().equals(2)).collect(Collectors.toList());
        if (!borrowGroupList.isEmpty()) {
            for (AttendanceGroupUser group : borrowGroupList) {
                if (StringUtil.isEmpty(group.getTimeJson())) {
                    continue;
                }
                JSONArray jsonArray = new JSONArray(group.getTimeJson());
                List<JSONObject> list = jsonArray.toList(JSONObject.class);
                for (JSONObject json : list) {
                    String startTimeStr = json.get("startTime").toString();
                    String endTimeStr = json.get("endTime").toString();
                    if (monthBegin.before(Objects.requireNonNull(DateDetail.getStr2DateTime(endTimeStr))) && Objects.requireNonNull(DateDetail.getStr2DateTime(startTimeStr)).before(monthEnd)) {
                        groupIds.add(group.getGroupId());
                        break;
                    }
                }
            }
        }
        return groupIds;
    }

    private List<AttendanceGroupUser> getMyGroupList(String userId) throws QueryException {

        LambdaQueryWrapper<AttendanceGroupUser> groupUserQueryWrapper = new LambdaQueryWrapper<AttendanceGroupUser>()
                .eq(AttendanceGroupUser::getUserId, userId)
                .eq(AttendanceGroupUser::getDeleteMark, 0);
        List<AttendanceGroupUser> groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper);
        if (groupUserList.isEmpty()) {
            throw new QueryException("当前用户暂无考勤组");
        }
        return groupUserList;
    }

    @Override
    public GroupInfoVo getGroupInfo(Date today, String groupId, int selfGroupInt, UserInfo userInfo) throws QueryException {
        return getGroupInfo(today, groupId, selfGroupInt, userInfo, ConstantUtil.NUM_TRUE);
    }

    @Override
    public GroupInfoVo getGroupInfo(Date today, String groupId, int selfGroupInt, UserInfo userInfo, Integer isMainInfo) throws QueryException {

        // 查询考勤组信息
        LambdaQueryWrapper<AttendanceGroup> groupQueryWrapper = new LambdaQueryWrapper<AttendanceGroup>()
                .eq(AttendanceGroup::getId, groupId)
                .eq(AttendanceGroup::getDeleteMark, ConstantUtil.NUM_FALSE);
        AttendanceGroup group = attendanceGroupMapper.selectOne(groupQueryWrapper);
        if (null == group) {
            throw new QueryException("当前用户暂无考勤组");
        }
        // 考勤组用户绑定关系
        AttendanceGroupUser groupUser = getGroupUser(userInfo.getUserId(), groupId, isMainInfo);
        if (null == groupUser) {
            throw new QueryException("已不是考勤组成员");
        }
        GroupInfoVo groupInfo = new GroupInfoVo(today, group.getId(), group.getGroupName(), group.getLockedDate(), selfGroupInt, userInfo.getUserId());
        Map<String, List<AttendanceLocationSetting>> locationMap = attendanceLocationSettingService.getEnableLocationSetting(Stream.of(group.getId()).collect(Collectors.toList()));
        List<AttendanceLocationSetting> locationList = new ArrayList<>();
        if (!locationMap.isEmpty()) {
            locationList = locationMap.get(group.getId());
        }
        if (null != locationList && !locationList.isEmpty()) {
            ConcurrentMap<Integer, List<AttendanceLocationSetting>> map = locationList.stream().filter(v -> v.getEnable().equals(ConstantUtil.NUM_TRUE)).collect(Collectors.groupingByConcurrent(AttendanceLocationSetting::getType));
            if (!map.isEmpty()) {
                map.keySet().forEach(key -> {
                    List<AttendanceLocationSetting> methodList = map.get(key);
                    setClockInMethod(groupInfo, key, methodList);
                });
            }
        }
        // 查询考勤组规则
        GroupRuleVo groupRule = getGroupRule(groupId, userInfo.getUserId());
        groupInfo.setGroupRule(groupRule);
        return groupInfo;
    }

    private AttendanceGroupUser getGroupUser(String userId, String groupId, Integer isMainInfo) {
        LambdaQueryWrapper<AttendanceGroupUser> groupUserQuery = new LambdaQueryWrapper<AttendanceGroupUser>()
                .eq(AttendanceGroupUser::getUserId, userId)
                .eq(AttendanceGroupUser::getGroupId, groupId)
                .eq(!Objects.equals(isMainInfo, ConstantUtil.NUM_WITHDRAW), AttendanceGroupUser::getDeleteMark, ConstantUtil.NUM_FALSE)
                .last("limit 1");
        return attendanceGroupUserMapper.selectOne(groupUserQuery);
    }

    private AttendanceGroupUser getGroupUser(String userId, String groupId) {
        return getGroupUser(userId, groupId, ConstantUtil.NUM_TRUE);
    }

    @Override
    public GroupRuleVo getGroupRule(String groupId, String userId) {

        Map<String, AttendanceBaseSetting> settingMap = attendanceBaseSettingService.getEnableBaseSetting(List.of(groupId));
        if (null != settingMap && !settingMap.isEmpty()) {
            AttendanceBaseSetting setting = settingMap.get(groupId);
            if (null != setting) {
                // 查询考勤组锁定时间
                // Date lockDate = attendanceGroupMapper.getGroupLockDate(groupId);
                GroupRuleVo bean = JsonUtil.getJsonToBean(setting, GroupRuleVo.class);
                // 处理该用户在考勤中是否是允许外勤人员
                try {
                    bean.setFieldClockStatus(attendanceBaseSettingService.getFieldClockStatusSelf(setting) ? 1 : 0);
                } catch (Exception e) {
                    bean.setFieldClockStatus(ConstantUtil.NUM_FALSE);
                }
                bean.setFace(ConstantUtil.NUM_TRUE == setting.getFace());
                bean.setSelfGroupId(groupId);
                return bean;
            }
        }
        return null;
    }

    private void setClockInMethod(GroupInfoVo groupInfo, Integer key, List<AttendanceLocationSetting> methodList) {
        if (null == methodList) {
            methodList = new ArrayList<>();
        }
        List<MethodVo> methodLocationList = JsonUtil.getJsonToList(methodList, MethodVo.class);
        switch (key) {
            case ConstantUtil.DEVICE_PLACE:
                groupInfo.getClockInMethodVo().getLocationList().addAll(methodLocationList);
                break;
            case ConstantUtil.DEVICE_MACHINE:
                groupInfo.getClockInMethodVo().getDeviceList().addAll(methodLocationList);
                break;
            case ConstantUtil.DEVICE_WIFI:
                groupInfo.getClockInMethodVo().getWifiList().addAll(methodLocationList);
                break;
            default:
                break;
        }
    }

    private List<FtbAttendanceDailyRule> getDailyRuleList(Date today, UserInfo userInfo) {

        List<FtbAttendanceDailyRule> list = new ArrayList<>();
        DateDetail dateDetail = new DateDetail(today);
        dateDetail.getYesterday();
        // 查询昨天的最后一个出勤规则
        FtbAttendanceDailyRule yesterdayRule = attendanceDailyRuleMapper.getLastDailyRule(dateDetail.getCurrentDate(), userInfo.getUserId());
        if (null != yesterdayRule) {
            if (yesterdayRule.getAttendanceType().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || yesterdayRule.getAttendanceType().equals(AttendanceTypeEnum.STEP_OUT.getCode())) {
                if (DateDetail.checkSameDay(today, yesterdayRule.getOutLackPoint())) {
                    // 外出或出差, 且无班次则设置班次类型为-1
                    yesterdayRule.setAttendanceType(AttendanceTypeEnum.DEFAULT.getCode());
                    yesterdayRule.setInStepOutType(ConstantUtil.NUM_TRUE);
                    yesterdayRule.setOutStepOutType(ConstantUtil.NUM_TRUE);
                    list.add(yesterdayRule);
                }
            }
            // 休, 无 跳过
            if (yesterdayRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) {
                // 加班, 添加到今日出勤中
                list.add(yesterdayRule);
            }
            if (yesterdayRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) {
                // 普班, 判断下班结束时间是否为今天, 是, 添加到今日出勤中
                if (DateDetail.checkSameDay(today, yesterdayRule.getOutLackPoint())) {
                    list.add(yesterdayRule);
                }
            }
            if (yesterdayRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) {
                // 假, 判断请假结束时间(下班时间)是否是今天, 是, 添加到今日出勤中
                if (DateDetail.checkSameDay(today, yesterdayRule.getOutPoint())) {
                    list.add(yesterdayRule);
                }
            }
        }
        List<FtbAttendanceDailyRule> currentDailyRuleList = getCurrentDailyRuleList(today, userInfo);
        currentDailyRuleList.forEach(v -> {
            if (v.getAttendanceType().equals(AttendanceTypeEnum.HOLIDAYS.getCode()) || v.getAttendanceType().equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode())
                    || v.getAttendanceType().equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())) {
                v.setAttendanceType(AttendanceTypeEnum.REST.getCode());
            }
        });
        long count = currentDailyRuleList.stream().filter(v -> v.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).count();
        if (count > 0 && currentDailyRuleList.size() > 1) {
            // 半天休问题处理
            currentDailyRuleList.removeIf(v -> v.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()));
        }
        // 今日出勤规则
        list.addAll(currentDailyRuleList);
        return list;
    }

    private List<FtbAttendanceDailyRule> getCurrentDailyRuleList(Date today, UserInfo userInfo) {

        // 今日出勤规则
        LambdaQueryWrapper<FtbAttendanceDailyRule> dailyRuleQueryWrapper = new LambdaQueryWrapper<>();
        if (null != userInfo) {
            dailyRuleQueryWrapper.eq(FtbAttendanceDailyRule::getUserId, userInfo.getUserId());
        }
        dailyRuleQueryWrapper.eq(FtbAttendanceDailyRule::getDay, DateDetail.getDate2Str(today, DateDetail.DF))
                .in(FtbAttendanceDailyRule::getApplyViewEnable, List.of(1, 3, 9, 10))
                .eq(FtbAttendanceDailyRule::getDeleteMark, ConstantUtil.NUM_FALSE)
                .orderByAsc(FtbAttendanceDailyRule::getInPoint);
        List<FtbAttendanceDailyRule> dbList = attendanceDailyRuleMapper.selectList(dailyRuleQueryWrapper);
        if (!dbList.isEmpty()) {
            // 排除考勤组被删除的出勤规则
            List<String> groupIdList = dbList.stream().map(FtbAttendanceDailyRule::getGroupId).collect(Collectors.toList());
            List<String> delIds = attendanceGroupMapper.selectDelIds(groupIdList);
            if (!delIds.isEmpty()) {
                dbList.removeIf(v -> delIds.contains(v.getGroupId()));
            }
            dbList.forEach(rule -> {
                // 如果今日外出或出差, 且无排班, 设置班次类型为-1
                if (rule.getAttendanceType().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.STEP_OUT.getCode())) {
                    rule.setInStepOutType(ConstantUtil.NUM_TRUE);
                    rule.setOutStepOutType(ConstantUtil.NUM_TRUE);
                    rule.setAttendanceType(AttendanceTypeEnum.DEFAULT.getCode());
                }
            });
        }
        return new ArrayList<>(dbList);
    }

    @Override
    public List<FtbAttendanceDailyRule> getCurrentDailyRuleListOfWithdraw(Date today, UserInfo userInfo) {

        // 今日出勤规则
        LambdaQueryWrapper<FtbAttendanceDailyRule> dailyRuleQueryWrapper = new LambdaQueryWrapper<>();
        if (null != userInfo) {
            dailyRuleQueryWrapper.eq(FtbAttendanceDailyRule::getUserId, userInfo.getUserId());
        }
        dailyRuleQueryWrapper.eq(FtbAttendanceDailyRule::getDay, DateDetail.getDate2Str(today, DateDetail.DF))
                .in(FtbAttendanceDailyRule::getApplyViewEnable, ConstantUtil.NUM_TRUE, ConstantUtil.NUM_WITHDRAW, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode())
                .eq(FtbAttendanceDailyRule::getDeleteMark, ConstantUtil.NUM_FALSE)
                .orderByAsc(FtbAttendanceDailyRule::getSchedulesType)
                .orderByAsc(FtbAttendanceDailyRule::getInPoint);
        List<FtbAttendanceDailyRule> dbList = attendanceDailyRuleMapper.selectList(dailyRuleQueryWrapper);
        if (!dbList.isEmpty()) {
            // 排除考勤组被删除的出勤规则
            /*List<String> groupIdList = dbList.stream().map(FtbAttendanceDailyRule::getGroupId).collect(Collectors.toList());
            List<String> delIds = attendanceGroupMapper.selectDelIds(groupIdList);
            if (!delIds.isEmpty()) {
                dbList.removeIf(v -> delIds.contains(v.getGroupId()));
            }*/
            dbList.forEach(rule -> {
                if (null == rule.getInUnbounded()) {
                    rule.setInUnbounded(0);
                }
                if (null == rule.getOutUnbounded()) {
                    rule.setOutUnbounded(0);
                }
            });
        }
        return new ArrayList<>(dbList);
    }

    @Override
    public List<FtbAttendanceDailyRule> getCurrentDailyRuleListOfDay(DateRangeDto dateRangeDto, List<ClockInExportVo> usersByGroupVos) {
        // 时间段内出勤规则
        LambdaQueryWrapper<FtbAttendanceDailyRule> dailyRuleQueryWrapper = new LambdaQueryWrapper<>();
        dailyRuleQueryWrapper.between(FtbAttendanceDailyRule::getDay, dateRangeDto.getStartDate(), dateRangeDto.getEndDate())
                .in(FtbAttendanceDailyRule::getApplyViewEnable, ConstantUtil.NUM_TRUE, ConstantUtil.NUM_WITHDRAW, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode())
                .in(FtbAttendanceDailyRule::getUserId, usersByGroupVos.stream().map(ClockInExportVo::getUserId).distinct().collect(Collectors.toList()))
                .in(FtbAttendanceDailyRule::getGroupId, usersByGroupVos.stream().map(ClockInExportVo::getGroupId).distinct().collect(Collectors.toList()))
                .eq(FtbAttendanceDailyRule::getDeleteMark, ConstantUtil.NUM_FALSE)
                .orderByAsc(FtbAttendanceDailyRule::getInPoint);
        return attendanceDailyRuleMapper.selectList(dailyRuleQueryWrapper);
    }
}
