package jnpf.attendance.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageInfo;
import jnpf.attendance.antifreeze.UserAntifreeze;
import jnpf.attendance.excel.HeadStyleHandler;
import jnpf.attendance.mapper.AttendanceBookRecordMapper;
import jnpf.attendance.service.*;
import jnpf.base.UserInfo;
import jnpf.constants.AttendanceConstant;
import jnpf.controller.util.ExcelExportTemplate;
import jnpf.entity.AttendanceGroup;
import jnpf.entity.AttendanceGroupUser;
import jnpf.entity.attendance.AttendanceBookConfigEntity;
import jnpf.entity.attendance.AttendanceBookOperationLogEntity;
import jnpf.entity.attendance.AttendanceBookRecordEntity;
import jnpf.entity.attendance.AttendanceLeaveType;
import jnpf.enums.attendance.AttendanceStatusEnum;
import jnpf.enums.attendance.PeriodTypeEnum;
import jnpf.enums.attendance.ScopeBizType;
import jnpf.model.attendance.dto.*;
import jnpf.model.attendance.vo.*;
import jnpf.permission.model.user.PartUserInfoVo;
import jnpf.util.DateDetail;
import jnpf.util.RandomUtil;
import jnpf.util.UserProvider;
import jnpf.util.attendance.AttendanceGroupUserStatusUtil;
import jnpf.util.attendance.RuleScopeUtil;
import jnpf.util.attendance.SecondmentTypeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URL;
import java.text.Normalizer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 考勤本记录表Service实现类
 *
 * 已实现需求：
 * - 需求4：导出功能调整（上下半部分显示+班次列）
 * - 需求5：导入功能增强（表头/手机号/考勤本名称/假期类型校验）
 * - 需求6：请假备注信息列表统计
 * - 需求8：操作日志优化（HTML红色标记状态）
 * - 需求10：批量变更功能
 *
 * @author Generated
 * @create 2026-04-15
 */
@Slf4j
@Service
public class AttendanceBookRecordServiceImpl extends ServiceImpl<AttendanceBookRecordMapper, AttendanceBookRecordEntity> implements AttendanceBookRecordService {

    // ==================== 常量定义 ====================
    // 注：所有业务常量已迁移至 AttendanceConstant 类

    /**
     * 导入模板大标题：与导出一致 {@code yyyy年M月 + 考勤本名称 + 考勤本}
     */
    private static final Pattern IMPORT_BOOK_TITLE_PATTERN = Pattern.compile("^\\d{4}年\\d{1,2}月(.+)考勤本$");

    /**
     * 考勤状态：中文 → 数字
     */
    private static final Map<String, Integer> STATUS_CN_TO_CODE;

    /**
     * 考勤状态：数字 → 中文
     */
    private static final Map<Integer, String> STATUS_CODE_TO_CN;

    static {
        STATUS_CN_TO_CODE = new LinkedHashMap<>();
        for (AttendanceStatusEnum status : AttendanceStatusEnum.values()) {
            STATUS_CN_TO_CODE.put(status.getName(), status.getCode());
        }

        STATUS_CODE_TO_CN = new LinkedHashMap<>();
        STATUS_CN_TO_CODE.forEach((cn, code) -> STATUS_CODE_TO_CN.put(code, cn));
    }

    // ==================== 依赖注入 ====================

    @Resource
    private UserAntifreeze userAntifreeze;

    @Resource
    private ExcelExportTemplate excelExportTemplate;

    @Resource
    private AttendanceBookOperationLogService attendanceBookOperationLogService;

    @Resource
    private AttendanceBookConfigService attendanceBookConfigService;

    @Resource
    private RuleScopeUtil ruleScopeUtil;

    @Autowired
    private AttendanceUserService attendanceUserService;

    @Resource
    private AttendanceLeaveTypeService attendanceLeaveTypeService;

    @Resource
    private AttendanceDayStatisticsService attendanceDayStatisticsService;
    @Autowired
    private AttendanceGroupService attendanceGroupService;

    @Override
    public String saveOrUpdateRecord(AttendanceBookRecordDto dto) {
        // 构建实体对象
        AttendanceBookRecordEntity entity = new AttendanceBookRecordEntity();
        entity.setId(dto.getId());
        entity.setBookId(dto.getBookId());
        entity.setUserId(dto.getUserId());
        entity.setDay(dto.getDay());
        entity.setPeriodType(dto.getPeriodType());
        entity.setAttendanceStatus(dto.getAttendanceStatus());
        entity.setLeaveType(dto.getLeaveType());
        entity.setRemark(dto.getRemark());

        // 自动填充月份字段
        if (StringUtils.isBlank(entity.getMonth()) && entity.getDay() != null) {
            entity.setMonth(entity.getDay().toInstant()
                    .atZone(ZoneId.systemDefault())
                    .toLocalDate()
                    .format(DateTimeFormatter.ofPattern("yyyy-MM")));
        }

        boolean clearRequest = isClearRequest(dto);
        AttendanceBookRecordEntity existingRecord = resolveExistingRecord(dto);

        // 用户当日不在组：不做新增/修改/清除
        List<AttendanceGroupUser> userGroupUsers = loadUserGroupUsersForDay(
                entity.getBookId(), entity.getUserId(), entity.getDay());
        if (!AttendanceGroupUserStatusUtil.isUserInGroupOnDay(userGroupUsers, entity.getDay())) {
            log.info("用户当日不在组，跳过保存，userId: {}, day: {}, periodType: {}",
                    entity.getUserId(), entity.getDay(), entity.getPeriodType());
            return existingRecord != null ? existingRecord.getId() : null;
        }

        // 用户当月已封存（锁定）：不做新增/修改/清除
        if (isUserSealedForMonth(entity.getUserId(), entity.getMonth())) {
            log.info("用户当月已封存，跳过保存，userId: {}, month: {}, periodType: {}",
                    entity.getUserId(), entity.getMonth(), entity.getPeriodType());
            return existingRecord != null ? existingRecord.getId() : null;
        }

        // 获取当前用户信息
        UserInfo user = UserProvider.getUser();
        Date now = new Date();

        String recordId;
        if (clearRequest) {
            if (existingRecord == null) {
                log.info("清除考勤记录：无对应记录，跳过，userId: {}, day: {}, periodType: {}",
                        entity.getUserId(), entity.getDay(), entity.getPeriodType());
                return StringUtils.isNotBlank(dto.getId()) ? dto.getId() : null;
            }
            this.removeById(existingRecord.getId());
            recordId = existingRecord.getId();
            saveOperationLog(existingRecord, existingRecord, AttendanceConstant.LOG_OPERATION_CLEAN);
            log.info("清除考勤记录成功，记录ID: {}, 员工ID: {}, 日期: {}, 时段: {}",
                    recordId, entity.getUserId(), entity.getDay(), entity.getPeriodType());
        } else if (Objects.nonNull(existingRecord)) {
            // 记录操作日志（在修改前记录，保留原始状态用于"调整前"展示）
            saveOperationLog(entity, existingRecord, AttendanceConstant.LOG_OPERATION_UPDATE);

            // 更新现有记录
            existingRecord.setAttendanceStatus(entity.getAttendanceStatus());
            existingRecord.setLeaveType(entity.getLeaveType());
            existingRecord.setRemark(entity.getRemark());
            existingRecord.setLastModifyUserId(user != null ? user.getUserId() : null);
            existingRecord.setLastModifyTime(now);
            this.updateById(existingRecord);
            recordId = existingRecord.getId();
            log.info("更新考勤记录成功，记录ID: {}, 员工ID: {}, 日期: {}, 时段: {}, 状态: {}",
                    recordId, entity.getUserId(), entity.getDay(), entity.getPeriodType(), entity.getAttendanceStatus());
        } else {
            // 新增记录
            entity.setId(RandomUtil.uuId());
            entity.setCreatorUserId(user != null ? user.getUserId() : null);
            entity.setCreatorTime(now);
            entity.setTenantId(user != null ? user.getTenantId() : null);
            this.save(entity);
            recordId = entity.getId();
            log.info("新增考勤记录成功，记录ID: {}, 员工ID: {}, 日期: {}, 时段: {}, 状态: {}",
                    recordId, entity.getUserId(), entity.getDay(), entity.getPeriodType(), entity.getAttendanceStatus());

            // 记录操作日志
            saveOperationLog(entity, null, AttendanceConstant.LOG_OPERATION_ADD);
        }

        return recordId;
    }

    @Override
    public PageInfo<BookRecordMonthStatisticsVo> getMonthStatistics(BookRecordMonthStatisticsDto req) {

        // 2. 查询考勤本配置
        AttendanceBookConfigEntity bookConfig = attendanceBookConfigService.getById(req.getBookId());
        if (bookConfig == null) {
            log.warn("考勤本配置不存在，bookId: {}", req.getBookId());
            return new PageInfo<>();
        }
        String bookName = bookConfig.getBookName();

        // 3. 解析月份，获取该月的所有日期列表
        YearMonth yearMonth = YearMonth.parse(req.getMonth(), DateTimeFormatter.ofPattern("yyyy-MM"));
        List<String> allDaysInMonth = generateMonthDays(yearMonth);
        Date startDate = getMonthStartDate(yearMonth);
        Date endDate = getMonthEndDate(yearMonth);
        // 4. 获取考勤本使用范围的用户列表
        List<AttendanceGroupUser> attendanceGroupUsersOfSecondment = getBookUserIds(bookConfig, req.getUserIds(), startDate, endDate);
        if (CollUtil.isEmpty(attendanceGroupUsersOfSecondment)) {
            log.info("考勤本无有效用户，bookId: {}", req.getBookId());
            return new PageInfo<>();
        }
        List<AttendanceGroup> attendanceGroups = attendanceGroupService.listByIds(attendanceGroupUsersOfSecondment.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList()));
        Map<String, String> attendanceGroupMap = attendanceGroups.stream().collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getGroupName, (existing, replacement) -> replacement));
        Map<String, List<AttendanceGroupUser>> attendanceGroupUsersMap = attendanceGroupUsersOfSecondment.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));

        List<String> allUserIds = attendanceGroupUsersOfSecondment.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList());
        // 7. 仅查询当前页用户的信息（避免全量查询）
        Map<String, PartUserInfoVo> userInfoMap = batchQueryUserInfo(allUserIds);
        List<String> strings = new ArrayList<>(userInfoMap.keySet());
        // 6. 先对用户ID列表进行分页（allUserIds顺序稳定，直接分页）
        List<String> pagedUserIds = paginateUserIds(strings, req.getCurrentPage(), req.getPageSize());
        if (CollUtil.isEmpty(pagedUserIds)) {
            log.info("分页后用户列表为空，bookId: {}, page: {}", req.getBookId(), req.getCurrentPage());
            return new PageInfo<>();
        }


        // 过滤掉查不到用户信息的ID（避免产生空的用户数据）
        pagedUserIds = pagedUserIds.stream()
                .filter(userInfoMap::containsKey)
                .collect(Collectors.toList());

        Map<String, Boolean> sealMap = attendanceDayStatisticsService.selectUserIsSeal(pagedUserIds, req.getMonth());

        // 8. 查询当前页用户的考勤记录
        List<AttendanceBookRecordEntity> records = queryMonthRecords(req.getBookId(), startDate, endDate, pagedUserIds);
        Map<String, List<AttendanceBookRecordEntity>> recordsByUser = records.stream()
                .collect(Collectors.groupingBy(AttendanceBookRecordEntity::getUserId));
        // 5. 查询所有请假类型，构建ID到名称的映射（使用统一方法，自动过滤已删除的）
        Map<String, AttendanceLeaveType> leaveTypeIdToNameMap = getAttendanceLeaveTypeMap(bookConfig, records);
        // 9. 遍历当前页用户构建统计结果
        List<BookRecordMonthStatisticsVo> pageResult = buildMonthStatisticsList(
                req.getBookId(), bookName, pagedUserIds, recordsByUser, userInfoMap, allDaysInMonth, leaveTypeIdToNameMap, attendanceGroupUsersMap, attendanceGroupMap, sealMap,
                startDate, endDate);

        log.info("获取考勤本月统计成功，考勤本ID: {}, 考勤本名称: {}, 月份: {}, 当前页员工数: {}, 总员工数: {}",
                req.getBookId(), bookName, req.getMonth(), pageResult.size(), allUserIds.size());

        // 10. 构建分页结果（总记录数为所有用户数）
        PageInfo<BookRecordMonthStatisticsVo> pageInfo = new PageInfo<>();
        pageInfo.setList(pageResult);
        pageInfo.setTotal(strings.size());
        pageInfo.setPages((int) Math.ceil((double) strings.size() / req.getPageSize()));
        pageInfo.setPageNum(req.getCurrentPage());
        pageInfo.setPageSize(req.getPageSize());
        return pageInfo;
    }
    @NotNull
    private Map<String, AttendanceLeaveType> getAttendanceLeaveTypeMap(AttendanceBookConfigEntity bookConfig, List<AttendanceBookRecordEntity> recordList) {
        List<String> leaveTypeIds = recordList.stream().map(AttendanceBookRecordEntity::getLeaveType).distinct().collect(Collectors.toList());
        return getStringAttendanceLeaveTypeMap(bookConfig, leaveTypeIds);
    }
    @NotNull
    private Map<String, AttendanceLeaveType> getStringAttendanceLeaveTypeMap(AttendanceBookConfigEntity bookConfig,List<String> leaveTypeIds) {
        leaveTypeIds.addAll(Objects.equals(bookConfig.getHolidayScopeType(), 1)
                ? CollUtil.newArrayList()
                : (StringUtils.isNotBlank(bookConfig.getHolidayIds()) ? Arrays.asList(bookConfig.getHolidayIds().split(",")) : CollUtil.newArrayList()));
        List<AttendanceLeaveType> leaveTypes = attendanceLeaveTypeService.getAttendanceLeaveTypesIncludeDeleted(leaveTypeIds);
        return leaveTypes.stream()
                .collect(Collectors.toMap(AttendanceLeaveType::getId, Function.identity(), (v1, v2) -> v1));
    }

    /**
     * 获取考勤本使用范围的用户ID列表
     * <p>参考AttendanceBookConfigServiceImpl.getBookPersonnelList方法的逻辑</p>
     *
     * @param bookConfig  考勤本配置
     * @param filterUserIds 筛选的用户ID列表（如果传入，则从范围用户中筛选）
     * @param startDate 开始时间（用于获取该时间段内的有效用户）
     * @param endDate 结束时间（用于获取该时间段内的有效用户）
     * @return 用户ID列表
     */
    private List<AttendanceGroupUser> getBookUserIds(AttendanceBookConfigEntity bookConfig, List<String> filterUserIds, Date startDate, Date endDate) {
        // 1. 获取使用范围
        MutablePair<List<String>, List<String>> scopeList = ruleScopeUtil.selectScopeList(
                bookConfig.getId(), ScopeBizType.ATTENDANCE_BOOK);

        // 2. 如果使用范围为"全部"（scopeList为null），则查询所有有效用户
        if (scopeList == null) {
            if (!Objects.equals(bookConfig.getScopeType(), 0)) {
                log.warn("考勤本使用范围配置异常，bookId: {}, scopeType: {}", bookConfig.getId(), bookConfig.getScopeType());
                return Collections.emptyList();
            }
            // scopeType=0 表示全部，查询所有有效用户
            List<AttendanceGroupUser> userIdsAndGroupIds = attendanceUserService.getUserIdsAndGroupIds(
                    Collections.emptyList(),
                    Collections.emptyList(),
                    startDate,
                    endDate,
                    bookConfig.getScopeType()
            );
            if (CollUtil.isEmpty(userIdsAndGroupIds)) {
                log.info("考勤本有效用户列表为空，bookId: {}", bookConfig.getId());
                return Collections.emptyList();
            }

            // 如果传入了筛选用户ID，则进行交集筛选
            if (CollUtil.isNotEmpty(filterUserIds)) {
                userIdsAndGroupIds = userIdsAndGroupIds.stream()
                        .filter(vo -> filterUserIds.contains(vo.getUserId()))
                        .collect(Collectors.toList());
                if (CollUtil.isEmpty(userIdsAndGroupIds)) {
                    log.info("筛选后的用户列表为空，bookId: {}", bookConfig.getId());
                    return Collections.emptyList();
                }
            }

            return userIdsAndGroupIds;
        }

        // 3. 指定组织/成员范围：需要同时处理组织和直接指定的用户
        List<String> orgIds = scopeList.getLeft();
        List<String> directUserIds = scopeList.getRight();

        // 调用 getUserIds 获取组织下的所有人员 + 直接指定的用户
        List<AttendanceGroupUser> userIdsAndGroupIds = attendanceUserService.getUserIdsAndGroupIds(
                orgIds,
                directUserIds,
                startDate,
                endDate,
                bookConfig.getScopeType()
        );
        if (CollUtil.isEmpty(userIdsAndGroupIds)) {
            log.info("考勤本有效用户列表为空，bookId: {}", bookConfig.getId());
            return Collections.emptyList();
        }

        // 4. 如果传入了筛选用户ID，则进行交集筛选
        if (CollUtil.isNotEmpty(filterUserIds)) {
            userIdsAndGroupIds = userIdsAndGroupIds.stream()
                    .filter(vo -> filterUserIds.contains(vo.getUserId()))
                    .collect(Collectors.toList());

            if (CollUtil.isEmpty(userIdsAndGroupIds)) {
                log.info("筛选后的用户列表为空，bookId: {}", bookConfig.getId());
                return Collections.emptyList();
            }
        }

        log.info("获取考勤本用户列表成功，bookId: {}, 用户数: {}", bookConfig.getId(), userIdsAndGroupIds.size());
        return userIdsAndGroupIds;
    }


    /**
     * 生成本月所有日期列表
     */
    private List<String> generateMonthDays(YearMonth yearMonth) {
        List<String> allDays = new ArrayList<>();
        for (int day = 1; day <= yearMonth.lengthOfMonth(); day++) {
            allDays.add(yearMonth.atDay(day).format(DateTimeFormatter.ISO_LOCAL_DATE));
        }
        return allDays;
    }

    /**
     * 获取月份起始日期
     */
    private Date getMonthStartDate(YearMonth yearMonth) {
        return Date.from(yearMonth.atDay(1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
    }

    /**
     * 获取月份结束日期
     */
    private Date getMonthEndDate(YearMonth yearMonth) {
        return Date.from(yearMonth.atEndOfMonth().atTime(23, 59, 59).atZone(ZoneId.systemDefault()).toInstant());
    }

    /**
     * 对用户ID列表进行分页
     */
    private List<String> paginateUserIds(List<String> allUserIds, int pageNum, int pageSize) {
        if (CollUtil.isEmpty(allUserIds)) {
            return Collections.emptyList();
        }

        int fromIndex = (pageNum - 1) * pageSize;
        int toIndex = Math.min(fromIndex + pageSize, allUserIds.size());

        if (fromIndex >= allUserIds.size()) {
            return Collections.emptyList();
        }

        return allUserIds.subList(fromIndex, toIndex);
    }

    /**
     * 查询月份考勤记录
     */
    private List<AttendanceBookRecordEntity> queryMonthRecords(String bookId, Date startDate,
                                                               Date endDate, List<String> userIds) {
        return lambdaQuery().eq(AttendanceBookRecordEntity::getBookId, bookId)
                .between(AttendanceBookRecordEntity::getDay, startDate, endDate)
                .in(CollUtil.isNotEmpty(userIds), AttendanceBookRecordEntity::getUserId, userIds)
                .orderByAsc(AttendanceBookRecordEntity::getUserId)
                .orderByAsc(AttendanceBookRecordEntity::getDay)
                .orderByAsc(AttendanceBookRecordEntity::getPeriodType)
                .list();
    }


    /**
     * 批量查询用户信息
     */
    private Map<String, PartUserInfoVo> batchQueryUserInfo(List<String> userIds) {
        List<PartUserInfoVo> userInfoList = userAntifreeze.getStaffRosterListInfoByIds(userIds);
        return userInfoList.stream()
                .collect(Collectors.toMap(PartUserInfoVo::getUserId, user -> user, (u1, u2) -> u1));
    }

    /**
     * 构建月度统计列表
     */
    private List<BookRecordMonthStatisticsVo> buildMonthStatisticsList(
            String bookId,
            String bookName,
            List<String> allUserIds,
            Map<String, List<AttendanceBookRecordEntity>> recordsByUser,
            Map<String, PartUserInfoVo> userInfoMap,
            List<String> allDaysInMonth,
            Map<String, AttendanceLeaveType> leaveTypeIdToNameMap,
            Map<String, List<AttendanceGroupUser>> attendanceGroupUsersMap,
            Map<String, String> attendanceGroupMap,
            Map<String, Boolean> sealMap,
            Date startDate,
            Date endDate) {

        List<BookRecordMonthStatisticsVo> resultList = new ArrayList<>();
        int sortOrder = 1;

        for (String userId : allUserIds) {
            List<AttendanceBookRecordEntity> userRecords = recordsByUser.getOrDefault(userId, new ArrayList<>());
            PartUserInfoVo userInfo = userInfoMap.get(userId);
            List<AttendanceGroupUser> attendanceGroupUsers = attendanceGroupUsersMap.getOrDefault(userId, new ArrayList<>());
            if (CollUtil.isEmpty(attendanceGroupUsers)) {
                continue;
            }
            // 构建该用户的统计VO
            BookRecordMonthStatisticsVo statisticsVo = buildUserStatistics(
                    bookId, bookName, userId, userInfo, userRecords, allDaysInMonth, sortOrder++, leaveTypeIdToNameMap, attendanceGroupUsers, attendanceGroupMap,
                    sealMap.getOrDefault(userId, false), startDate, endDate);
            resultList.add(statisticsVo);
        }

        return resultList;
    }

    /**
     * 构建单个用户的月度统计
     */
    private BookRecordMonthStatisticsVo buildUserStatistics(
            String bookId,
            String bookName,
            String userId,
            PartUserInfoVo userInfo,
            List<AttendanceBookRecordEntity> userRecords,
            List<String> allDaysInMonth,
            int sortOrder,
            Map<String, AttendanceLeaveType> leaveTypeIdToNameMap,
            List<AttendanceGroupUser> attendanceGroupUsers,
            Map<String, String> attendanceGroupMap,
            boolean isSeal,
            Date startDate,
            Date endDate) {

        // 按日期分组记录
        Map<String, List<AttendanceBookRecordEntity>> recordsByDay = userRecords.stream()
                .collect(Collectors.groupingBy(record ->
                        record.getDay().toInstant()
                                .atZone(ZoneId.systemDefault())
                                .toLocalDate()
                                .format(DateTimeFormatter.ISO_LOCAL_DATE)));

        // 初始化统计数据
        MonthlyAttendanceStats stats = new MonthlyAttendanceStats();
        List<BookRecordDailyLatticeVo> dailyLatticeList = new ArrayList<>(allDaysInMonth.size());

        // 遍历所有日期构建格子数据和统计
        for (String day : allDaysInMonth) {
            List<AttendanceBookRecordEntity> dayRecords = recordsByDay.getOrDefault(day, new ArrayList<>());

            // 查找上午和下午记录
            AttendanceBookRecordEntity morningRecord = findRecordByPeriod(dayRecords, PeriodTypeEnum.MORNING.getCode());
            AttendanceBookRecordEntity afternoonRecord = findRecordByPeriod(dayRecords, PeriodTypeEnum.AFTERNOON.getCode());

            // 构建格子数据
            dailyLatticeList.add(buildDailyLattice(day, morningRecord, afternoonRecord, attendanceGroupUsers));

            // 累加统计（仅在有数据时）
            if (morningRecord != null) {
                stats.accumulate(morningRecord);
            }
            if (afternoonRecord != null) {
                stats.accumulate(afternoonRecord);
            }
        }
        int secondmentType = SecondmentTypeUtil.toSecondmentType(AttendanceGroupUserStatusUtil.isExistStatus(attendanceGroupUsers, startDate, endDate));
        AttendanceGroupUser attendanceGroupUser = attendanceGroupUsers.stream().reduce((groupUser1, groupUser2) -> Objects.equals(groupUser1.getDeleteMark(), 0) ? groupUser1 : Objects.equals(groupUser2.getDeleteMark(), 0) ? groupUser2 : groupUser1.getCreatorTime().after(groupUser2.getCreatorTime()) ? groupUser1 : groupUser2).orElse(null);

        // 用户级「是否离组」与每日格子展示无关：按考勤组成员 removeTime 判定（VO：0否 1是）。多条组关系时优先取仍在组(removeTime 为空)的记录，否则取离组时间最晚的一条。
        // 构建并返回统计VO
        return BookRecordMonthStatisticsVo.builder()
                .bookId(bookId)
                .bookName(bookName)
                .userId(userId)
                .realName(getValueOrNull(userInfo, PartUserInfoVo::getRealName))
                .secondmentType(secondmentType)
                .isOutGroup(Objects.isNull(attendanceGroupUser) ? 1 : attendanceGroupUser.getDeleteMark())
                .organizeName(attendanceGroupMap.getOrDefault(attendanceGroupUser.getGroupId(), getValueOrNull(userInfo, PartUserInfoVo::getOrganizeName)))
                .positionName(getValueOrNull(userInfo, PartUserInfoVo::getPositionName))
                .mobilePhone(getValueOrNull(userInfo, PartUserInfoVo::getMobilePhone))
                .sortOrder(sortOrder)
                .isSeal(isSeal)
                .dailyLatticeList(dailyLatticeList)
                .actualAttendDays(stats.actualAttendDays)
                .publicHolidayDays(stats.publicHolidayDays)
                .leaveDaysByType(stats.toLeaveTypeStatisticList(leaveTypeIdToNameMap))
                .lateDays(stats.lateDays)
                .earlyLeaveDays(stats.earlyLeaveDays)
                .missingCardDays(stats.missingCardDays)
                .absenteeismDays(stats.absenteeismDays)
                .businessTripDays(stats.businessTripDays)
                .outOfficeDays(stats.outOfficeDays)
                .fieldWorkDays(stats.fieldWorkDays)
                .actualAttendCount(stats.actualAttendCount)
                .publicHolidayCount(stats.publicHolidayCount)
                .lateCount(stats.lateCount)
                .earlyLeaveCount(stats.earlyLeaveCount)
                .missingCardCount(stats.missingCardCount)
                .absenteeismCount(stats.absenteeismCount)
                .businessTripCount(stats.businessTripCount)
                .outOfficeCount(stats.outOfficeCount)
                .fieldWorkCount(stats.fieldWorkCount)
                .remarkList(stats.remarkList)
                .build();
    }

    /**
     * 根据时段类型查找记录
     */
    private AttendanceBookRecordEntity findRecordByPeriod(List<AttendanceBookRecordEntity> records, Integer periodType) {
        if (CollUtil.isEmpty(records) || periodType == null) {
            return null;
        }
        return records.stream()
                .filter(r -> periodType.equals(r.getPeriodType()))
                .max(Comparator
                        .comparing(AttendanceBookRecordEntity::getLastModifyTime, Comparator.nullsLast(Date::compareTo))
                        .thenComparing(AttendanceBookRecordEntity::getCreatorTime, Comparator.nullsLast(Date::compareTo))
                        .thenComparing(AttendanceBookRecordEntity::getId, Comparator.nullsLast(String::compareTo)))
                .orElse(null);
    }

    /**
     * 构建每日格子数据
     */
    private BookRecordDailyLatticeVo buildDailyLattice(String day,
                                                       AttendanceBookRecordEntity morningRecord,
                                                       AttendanceBookRecordEntity afternoonRecord,
                                                       List<AttendanceGroupUser> attendanceGroupUsers) {
        Date date = DateDetail.getStr2Date10(day);
        Integer existStatus = AttendanceGroupUserStatusUtil.isExistStatus(attendanceGroupUsers, date);
        if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) {
            existStatus = 1;
        }
        return BookRecordDailyLatticeVo.builder()
                .day(day)
                .isOutGroup(existStatus)
                .morningStatus(resolveStatus(morningRecord))
                .morningLeaveType(getValueOrNull(morningRecord, AttendanceBookRecordEntity::getLeaveType))
                .morningChanged(morningRecord != null && morningRecord.getLastModifyTime() != null)
                .afternoonStatus(resolveStatus(afternoonRecord))
                .afternoonLeaveType(getValueOrNull(afternoonRecord, AttendanceBookRecordEntity::getLeaveType))
                .afternoonChanged(afternoonRecord != null && afternoonRecord.getLastModifyTime() != null)
                .build();
    }

    /**
     * 解析每日格子展示用考勤状态：
     * <p>当记录上同时存在 {@code leaveType} 与 {@code attendanceStatus}（例如「调休 + 外出」），
     * 应以请假类型为准展示，避免前端按 {@code status} 渲染时把请假类型显示成外出/外勤等基础状态。
     * 与 {@link MonthlyAttendanceStats#accumulate} 的"leaveType 优先"口径保持一致。</p>
     */
    private Integer resolveStatus(AttendanceBookRecordEntity record) {
        if (record == null) {
            return null;
        }
        if (StringUtils.isNotBlank(record.getLeaveType())) {
            return null;
        }
        return record.getAttendanceStatus();
    }

    /**
     * 安全获取值（避免空指针）
     */
    private <T, R> R getValueOrNull(T obj, java.util.function.Function<T, R> getter) {
        return obj != null ? getter.apply(obj) : null;
    }

    /**
     * 月度考勤统计数据（封装统计逻辑）
     */
    private static class MonthlyAttendanceStats {
        private static final BigDecimal HALF_DAY = new BigDecimal("0.5");

        // 天数统计（每条半天记录算0.5天）
        BigDecimal actualAttendDays = BigDecimal.ZERO;
        BigDecimal publicHolidayDays = BigDecimal.ZERO;
        Map<String, BigDecimal> leaveDaysByTypeMap = new HashMap<>();
        BigDecimal lateDays = BigDecimal.ZERO;
        BigDecimal earlyLeaveDays = BigDecimal.ZERO;
        BigDecimal missingCardDays = BigDecimal.ZERO;
        BigDecimal absenteeismDays = BigDecimal.ZERO;
        BigDecimal businessTripDays = BigDecimal.ZERO;
        BigDecimal outOfficeDays = BigDecimal.ZERO;
        BigDecimal fieldWorkDays = BigDecimal.ZERO;

        // 次数统计（每条记录算1次）
        BigDecimal actualAttendCount = BigDecimal.ZERO;
        BigDecimal publicHolidayCount = BigDecimal.ZERO;
        Map<String, BigDecimal> leaveCountByTypeMap = new HashMap<>();
        BigDecimal lateCount = BigDecimal.ZERO;
        BigDecimal earlyLeaveCount = BigDecimal.ZERO;
        BigDecimal missingCardCount = BigDecimal.ZERO;
        BigDecimal absenteeismCount = BigDecimal.ZERO;
        BigDecimal businessTripCount = BigDecimal.ZERO;
        BigDecimal outOfficeCount = BigDecimal.ZERO;
        BigDecimal fieldWorkCount = BigDecimal.ZERO;

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

        /**
         * 累加单条记录的统计数据
         */
        void accumulate(AttendanceBookRecordEntity record) {
            Integer status = record.getAttendanceStatus();
            if (Objects.isNull(status) && Objects.isNull(record.getLeaveType())) {
                return;
            }

            AttendanceStatusEnum statusEnum = AttendanceStatusEnum.getByCode(status);

            // 收集备注
            if (StringUtils.isNotBlank(record.getRemark())) {
                remarkList.add(record.getRemark());
            }
            String leaveType = record.getLeaveType();
            if (StringUtils.isNotBlank(leaveType)) {
                leaveDaysByTypeMap.merge(leaveType, HALF_DAY, BigDecimal::add);
                leaveCountByTypeMap.merge(leaveType, BigDecimal.ONE, BigDecimal::add);
                log.debug("累加请假: leaveTypeId={}, 天数={}, 次数={}", leaveType, leaveDaysByTypeMap.get(leaveType), leaveCountByTypeMap.get(leaveType));
                return;
            }
            if (statusEnum == null) {
                return;
            }

            // 根据状态累加统计
            switch (statusEnum) {
                case NORMAL:
                    actualAttendDays = actualAttendDays.add(HALF_DAY);
                    actualAttendCount = actualAttendCount.add(BigDecimal.ONE);
                    break;
                case PUBLIC_HOLIDAY:
                    publicHolidayDays = publicHolidayDays.add(HALF_DAY);
                    publicHolidayCount = publicHolidayCount.add(BigDecimal.ONE);
                    break;
                case LATE:
                    lateDays = lateDays.add(HALF_DAY);
                    lateCount = lateCount.add(BigDecimal.ONE);
                    break;
                case EARLY_LEAVE:
                    earlyLeaveDays = earlyLeaveDays.add(HALF_DAY);
                    earlyLeaveCount = earlyLeaveCount.add(BigDecimal.ONE);
                    break;
                case MISSING_CARD:
                    missingCardDays = missingCardDays.add(HALF_DAY);
                    missingCardCount = missingCardCount.add(BigDecimal.ONE);
                    break;
                case ABSENT:
                    absenteeismDays = absenteeismDays.add(HALF_DAY);
                    absenteeismCount = absenteeismCount.add(BigDecimal.ONE);
                    break;
                case BUSINESS_TRIP:
                    businessTripDays = businessTripDays.add(HALF_DAY);
                    businessTripCount = businessTripCount.add(BigDecimal.ONE);
                    break;
                case OUT_OFFICE:
                    outOfficeDays = outOfficeDays.add(HALF_DAY);
                    outOfficeCount = outOfficeCount.add(BigDecimal.ONE);
                    break;
                case OUTER:
                    fieldWorkDays = fieldWorkDays.add(HALF_DAY);
                    fieldWorkCount = fieldWorkCount.add(BigDecimal.ONE);
                    break;
                default:
                    break;
            }
        }

        /**
         * 转换为请假类型统计列表
         * 即使没有请假数据，也要返回所有配置的假期类型（天数为0）
         */
        List<LeaveTypeStatisticVo> toLeaveTypeStatisticList(Map<String, AttendanceLeaveType> leaveTypeIdToNameMap) {
            log.info("转换请假类型统计: leaveDaysByTypeMap={}, leaveTypeIdToNameMap={}",
                    leaveDaysByTypeMap, leaveTypeIdToNameMap);

            if (leaveTypeIdToNameMap == null || leaveTypeIdToNameMap.isEmpty()) {
                return Collections.emptyList();
            }

            // 遍历所有配置的请假类型，确保都出现在结果中
            return leaveTypeIdToNameMap.entrySet().stream()
                    .map(entry -> {
                        String leaveTypeId = entry.getKey();
                        AttendanceLeaveType leaveTypeName = entry.getValue();
                        // 从统计结果中获取天数，如果没有则为0
                        BigDecimal days = leaveDaysByTypeMap.getOrDefault(leaveTypeId, BigDecimal.ZERO);
                        if (days.compareTo(BigDecimal.ZERO) <= 0 && Objects.equals(leaveTypeName.getDeleteMark(), 1)) {
                            return null;
                        }
                        BigDecimal count = leaveCountByTypeMap.getOrDefault(leaveTypeId, BigDecimal.ZERO);

                        return LeaveTypeStatisticVo.builder()
                                .leaveType(leaveTypeName.getName())
                                .days(days)
                                .count(count)
                                .build();
                    }).filter(Objects::nonNull)
                    .collect(Collectors.toList());
        }
    }

    @Override
    public void bookRecordImport(SchedulesImportDto importDto) throws IOException {
        log.info("开始导入考勤本数据，bookId: {}, month: {}", importDto.getBookId(), importDto.getMonth());

        // 1. 校验考勤本配置
        AttendanceBookConfigEntity config = validateBookConfig(importDto.getBookId());

        // 2. 解析月份信息
        YearMonth yearMonth = parseYearMonth(importDto.getMonth());
        int daysInMonth = yearMonth.lengthOfMonth();
        Date startDate = getMonthStartDate(yearMonth);
        Date endDate = getMonthEndDate(yearMonth);

        // 3. 获取考勤本使用范围的用户ID与考勤组ID（一次查询，复用于手机号映射与按天在组判断）
        List<AttendanceGroupUser> bookUsers = getBookUserIds(config, null, startDate, endDate);
        List<String> bookUserIds = bookUsers.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList());
        // 4. 构建手机号→用户信息映射
        Map<String, PartUserInfoVo> phoneUserMap = buildPhoneUserMap(bookUserIds);

        // 5. 构建用户ID→当月考勤组成员关系（用于按天判断用户是否在组）
        Map<String, List<AttendanceGroupUser>> userGroupUsersMap = bookUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));

        // 5.1 查询当月已封存用户（封存月份不做导入，直接过滤）
        Map<String, Boolean> sealMap = CollUtil.isEmpty(bookUserIds)
                ? Collections.emptyMap()
                : attendanceDayStatisticsService.selectUserIsSeal(bookUserIds.stream().distinct().collect(Collectors.toList()), importDto.getMonth());

        // 6. 查询所有请假类型，构建名称→ID映射（用于导入时识别请假类型）
        List<AttendanceLeaveType> leaveTypes = attendanceLeaveTypeService.getAttendanceLeaveTypes(null);
        Map<String, String> leaveTypeNameToIdMap = leaveTypes.stream()
                .collect(Collectors.toMap(AttendanceLeaveType::getName, AttendanceLeaveType::getId, (v1, v2) -> v1));
        Map<String, String> leaveTypeIdToNameMap = leaveTypes.stream()
                .collect(Collectors.toMap(AttendanceLeaveType::getId, AttendanceLeaveType::getName, (v1, v2) -> v1));

        // 7. 读取并解析Excel数据
        ImportParseResult parseResult = parseExcelFile(importDto.getFileUrl(), phoneUserMap, yearMonth, daysInMonth, importDto.getMonth(), leaveTypeNameToIdMap, config.getBookName());

        // 8. 校验解析结果
        validateParseResult(parseResult);

        // 9. 过滤掉用户当日不在考勤组的记录（不导入这部分数据）
        List<AttendanceBookRecordDto> filteredImportData = filterImportDataByGroupMembership(
                parseResult.getImportData(), userGroupUsersMap);

        // 9.1 过滤掉当月已封存用户的记录（封存月份不可变更考勤本数据）
        filteredImportData = filterImportDataByUserSeal(filteredImportData, sealMap);

        // 10. 批量处理导入数据
        batchProcessImportData(filteredImportData, importDto.getBookId(), leaveTypeIdToNameMap);

        log.info("考勤本数据导入完成，bookId: {}, month: {}", importDto.getBookId(), importDto.getMonth());
    }

    /**
     * 校验考勤本配置
     */
    private AttendanceBookConfigEntity validateBookConfig(String bookId) {
        AttendanceBookConfigEntity config = attendanceBookConfigService.getById(bookId);
        Assert.notNull(config, AttendanceConstant.ERR_IMPORT_CONFIG_NOT_FOUND);
        Assert.isFalse(Objects.equals(config.getEnabledMark(), 0), AttendanceConstant.ERR_IMPORT_CONFIG_DISABLED);
        return config;
    }

    /**
     * 解析年月
     */
    private YearMonth parseYearMonth(String month) {
        return YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM"));
    }

    /**
     * 导入比对：去除 BOM、零宽字符、全角/兼容区等与导出/Excel 不一致导致的假「不匹配」
     */
    private static String normalizeImportBookComparableText(String s) {
        if (s == null) {
            return "";
        }
        String t = s.trim();
        if (t.startsWith("\uFEFF")) {
            t = t.substring(1).trim();
        }
        t = Normalizer.normalize(t, Normalizer.Form.NFKC);
        return t.replace("\u200B", "").replace("\u200C", "").replace("\u200D", "").trim();
    }

    /**
     * Excel 大标题是否与当前考勤本名称一致（支持从「yyyy年M月+名称+考勤本」解析嵌入名称）
     */
    private static boolean importExcelTitleMatchesConfiguredBook(String titleRaw, String configBookNameRaw) {
        String expected = normalizeImportBookComparableText(configBookNameRaw);
        String title = normalizeImportBookComparableText(titleRaw);
        if (StringUtils.isBlank(title) || StringUtils.isBlank(expected)) {
            return true;
        }
        if (title.contains(expected) || expected.contains(title)) {
            return true;
        }
        Matcher m = IMPORT_BOOK_TITLE_PATTERN.matcher(title);
        if (m.matches()) {
            String embedded = normalizeImportBookComparableText(m.group(1));
            if (StringUtils.isNotBlank(embedded)) {
                return embedded.equals(expected) || embedded.contains(expected) || expected.contains(embedded);
            }
        }
        return false;
    }

    /**
     * 构建手机号到用户信息的映射
     *
     * @param userIds 考勤本使用范围内的用户ID集合（已由调用方计算）
     */
    private Map<String, PartUserInfoVo> buildPhoneUserMap(List<String> userIds) {
        if (CollUtil.isEmpty(userIds)) {
            return Collections.emptyMap();
        }

        // 查询用户信息并构建映射（过滤掉无手机号的用户）
        List<PartUserInfoVo> userInfoList = userAntifreeze.getStaffRosterListInfoByIds(userIds);
        return userInfoList.stream()
                .filter(u -> StringUtils.isNotBlank(u.getMobilePhone()))
                .collect(Collectors.toMap(PartUserInfoVo::getMobilePhone, u -> u, (u1, u2) -> u1));
    }

    /**
     * 过滤掉用户在某天不在考勤组的记录（不导入这部分数据）
     *
     * <p>按 (userId, day) 缓存判断结果，避免「上半天/下半天」对同一日重复计算。
     * existStatus = 1 视为在组；3（部分被借调）、4（借调）按业务约定也按 1 处理。</p>
     *
     * @param importData        解析得到的待导入记录
     * @param userGroupUsersMap 用户ID → 当月考勤组成员关系
     * @return 仅保留「用户当日在组」的记录
     */
    private List<AttendanceBookRecordDto> filterImportDataByGroupMembership(
            List<AttendanceBookRecordDto> importData,
            Map<String, List<AttendanceGroupUser>> userGroupUsersMap) {
        if (CollUtil.isEmpty(importData)) {
            return importData;
        }
        Map<String, Boolean> inGroupCache = new HashMap<>();
        List<AttendanceBookRecordDto> kept = new ArrayList<>(importData.size());
        int skippedCount = 0;
        for (AttendanceBookRecordDto dto : importData) {
            String userId = dto.getUserId();
            Date day = dto.getDay();
            if (userId == null || day == null) {
                kept.add(dto);
                continue;
            }
            String cacheKey = userId + "_" + day.getTime();
            Boolean inGroup = inGroupCache.computeIfAbsent(cacheKey, k ->
                    AttendanceGroupUserStatusUtil.isUserInGroupOnDay(
                            userGroupUsersMap.getOrDefault(userId, Collections.emptyList()), day));
            if (inGroup) {
                kept.add(dto);
            } else {
                skippedCount++;
            }
        }
        if (skippedCount > 0) {
            log.info("考勤本导入：用户当日不在组，已跳过 {} 条记录（共解析 {} 条）",
                    skippedCount, importData.size());
        }
        return kept;
    }

    /**
     * 加载用户在指定考勤本、指定日期的考勤组成员关系（用于在组判断）
     */
    private List<AttendanceGroupUser> loadUserGroupUsersForDay(String bookId, String userId, Date day) {
        if (StringUtils.isAnyBlank(bookId, userId) || day == null) {
            return Collections.emptyList();
        }
        AttendanceBookConfigEntity config = attendanceBookConfigService.getById(bookId);
        if (config == null) {
            return Collections.emptyList();
        }
        Date start = DateUtil.beginOfDay(day);
        Date end = DateUtil.endOfDay(day);
        List<AttendanceGroupUser> scopeUsers = getBookUserIds(config, Collections.singletonList(userId), start, end);
        if (CollUtil.isEmpty(scopeUsers)) {
            return Collections.emptyList();
        }
        return scopeUsers;
    }

    /**
     * 按批量记录涉及的用户与日期范围，构建用户ID → 考勤组成员关系映射
     */
    private Map<String, List<AttendanceGroupUser>> buildUserGroupUsersMapForRecords(
            String bookId, List<AttendanceBookRecordDto> records) {
        if (StringUtils.isBlank(bookId) || CollUtil.isEmpty(records)) {
            return Collections.emptyMap();
        }
        AttendanceBookConfigEntity config = attendanceBookConfigService.getById(bookId);
        if (config == null) {
            return Collections.emptyMap();
        }
        Set<String> userIds = records.stream()
                .map(AttendanceBookRecordDto::getUserId)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toSet());
        if (userIds.isEmpty()) {
            return Collections.emptyMap();
        }
        Optional<Date> minDay = records.stream()
                .map(AttendanceBookRecordDto::getDay)
                .filter(Objects::nonNull)
                .min(Date::compareTo);
        Optional<Date> maxDay = records.stream()
                .map(AttendanceBookRecordDto::getDay)
                .filter(Objects::nonNull)
                .max(Date::compareTo);
        if (minDay.isEmpty() || maxDay.isEmpty()) {
            return Collections.emptyMap();
        }
        Date start = DateUtil.beginOfDay(minDay.get());
        Date end = DateUtil.endOfDay(maxDay.get());
        List<AttendanceGroupUser> bookUsers = getBookUserIds(config, new ArrayList<>(userIds), start, end);
        if (CollUtil.isEmpty(bookUsers)) {
            return Collections.emptyMap();
        }
        return bookUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));
    }

    /**
     * 从考勤记录 DTO 解析月份（yyyy-MM）
     */
    private String resolveRecordMonth(AttendanceBookRecordDto dto) {
        if (dto == null) {
            return null;
        }
        return resolveMonthFromDay(dto.getDay());
    }

    /**
     * 从日期解析月份（yyyy-MM）
     */
    private String resolveMonthFromDay(Date day) {
        if (day == null) {
            return null;
        }
        return day.toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDate()
                .format(DateTimeFormatter.ofPattern("yyyy-MM"));
    }

    /**
     * 判断用户在该自然月是否已封存（锁定）
     */
    private boolean isUserSealedForMonth(String userId, String month) {
        if (StringUtils.isAnyBlank(userId, month)) {
            return false;
        }
        Map<String, Boolean> sealMap = attendanceDayStatisticsService.selectUserIsSeal(
                Collections.singletonList(userId), month);
        return Boolean.TRUE.equals(sealMap.get(userId));
    }

    /**
     * 按批量记录涉及的用户与月份，构建「用户+月份」封存缓存（key: userId|yyyy-MM）
     */
    private Map<String, Boolean> buildUserMonthSealMap(List<AttendanceBookRecordDto> records) {
        if (CollUtil.isEmpty(records)) {
            return Collections.emptyMap();
        }
        Map<String, Set<String>> monthUserIds = new LinkedHashMap<>();
        for (AttendanceBookRecordDto record : records) {
            String month = resolveRecordMonth(record);
            if (StringUtils.isBlank(month) || StringUtils.isBlank(record.getUserId())) {
                continue;
            }
            monthUserIds.computeIfAbsent(month, k -> new LinkedHashSet<>()).add(record.getUserId());
        }
        Map<String, Boolean> sealedCache = new HashMap<>();
        for (Map.Entry<String, Set<String>> entry : monthUserIds.entrySet()) {
            Map<String, Boolean> sealMap = attendanceDayStatisticsService.selectUserIsSeal(
                    new ArrayList<>(entry.getValue()), entry.getKey());
            for (String userId : entry.getValue()) {
                if (Boolean.TRUE.equals(sealMap.get(userId))) {
                    sealedCache.put(buildUserMonthSealKey(userId, entry.getKey()), Boolean.TRUE);
                }
            }
        }
        return sealedCache;
    }

    private String buildUserMonthSealKey(String userId, String month) {
        return userId + "|" + month;
    }

    private boolean isUserSealedInCache(String userId, String month, Map<String, Boolean> userMonthSealMap) {
        if (StringUtils.isAnyBlank(userId, month) || CollUtil.isEmpty(userMonthSealMap)) {
            return false;
        }
        return Boolean.TRUE.equals(userMonthSealMap.get(buildUserMonthSealKey(userId, month)));
    }

    /**
     * 过滤掉当月已封存用户的导入记录（封存月份不做导入）
     *
     * @param importData 待导入记录
     * @param sealMap    用户ID → 是否封存（{@link AttendanceDayStatisticsService#selectUserIsSeal}）
     */
    private List<AttendanceBookRecordDto> filterImportDataByUserSeal(
            List<AttendanceBookRecordDto> importData,
            Map<String, Boolean> sealMap) {
        if (CollUtil.isEmpty(importData) || CollUtil.isEmpty(sealMap)) {
            return importData;
        }
        List<AttendanceBookRecordDto> kept = new ArrayList<>(importData.size());
        int skippedCount = 0;
        for (AttendanceBookRecordDto dto : importData) {
            String userId = dto.getUserId();
            if (StringUtils.isNotBlank(userId) && Boolean.TRUE.equals(sealMap.get(userId))) {
                skippedCount++;
                continue;
            }
            kept.add(dto);
        }
        if (skippedCount > 0) {
            log.info("考勤本导入：用户当月已封存，已跳过 {} 条记录（共 {} 条）",
                    skippedCount, importData.size());
        }
        return kept;
    }

    /**
     * 解析Excel文件
     */
    private ImportParseResult parseExcelFile(String fileUrl, Map<String, PartUserInfoVo> phoneUserMap,
                                             YearMonth yearMonth, int daysInMonth, String month,
                                             Map<String, String> leaveTypeNameToIdMap, String bookName) throws IOException {
        ImportParseResult result = new ImportParseResult();
        List<AttendanceBookRecordDto> importData = new ArrayList<>();
        List<String> failMessages = new ArrayList<>();
        // 使用 phone+period 组合键去重，支持导出的2行/人格式（上半天、下半天各一行）
        Set<String> seenPhonePeriods = new HashSet<>();
        // 超过 500 行数据时只提示一次
        final boolean[] rowLimitExceeded = {false};
        final List<String> dateHeaders = new ArrayList<>();
        // 跨行保留「上半天」行的有效手机号，用于「下半天」行因合并单元格而读取为空时兜底
        final String[] lastValidMobilePhone = {null};

        EasyExcel.read(new URL(fileUrl).openStream(), new AnalysisEventListener<Map<Integer, String>>() {
            private List<String> headers;
            // 已处理的数据行数（不含表头）。headRowNumber(3) 时 invoke() 只对数据行回调，故等价于"第几条数据"
            private int dataRowCount = 0;
            // 表头日期列只在首次进入数据行时校验一次
            private boolean dateHeadersValidated = false;

            @Override
            public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
                // 按列号排序，避免依赖 Map.values() 的迭代顺序（headRowNumber(3) 会多次回调，最后一次为列名行）
                headers = headMap.entrySet().stream()
                        .sorted(Map.Entry.comparingByKey())
                        .map(Map.Entry::getValue)
                        .collect(Collectors.toList());
            }

            @Override
            public void invoke(Map<Integer, String> data, AnalysisContext context) {
                dataRowCount++;
                try {
                    // 首条数据行进入时一次性校验表头日期列（headers 已在 invokeHeadMap 最后一次回调时填充为列名行）
                    if (!dateHeadersValidated) {
                        dateHeadersValidated = true;
                        int dateStartIndex = getDateStartIndex(headers);
                        for (int i = dateStartIndex; i < headers.size(); i++) {
                            String header = headers.get(i);
                            if (header != null && header.matches(".*\\d+.*")) {
                                dateHeaders.add(header);
                            }
                        }
                        if (!validateDateHeaders(dateHeaders, yearMonth, month)) {
                            failMessages.add(String.format(AttendanceConstant.ERR_IMPORT_HEADER_DATE_MISMATCH, month));
                            // 表头不一致直接终止本行解析，避免后续按错误日期列误判数据
                            return;
                        }
                    }

                    // 单表最多 500 行数据（不含表头）
                    if (dataRowCount > 500) {
                        if (!rowLimitExceeded[0]) {
                            failMessages.add(AttendanceConstant.ERR_IMPORT_EXCEED_MAX_ROWS);
                            rowLimitExceeded[0] = true;
                        }
                        return;
                    }

                    parseRowData(data, dataRowCount, headers, phoneUserMap, yearMonth, daysInMonth, month, bookName,
                            seenPhonePeriods, importData, failMessages, leaveTypeNameToIdMap, lastValidMobilePhone);
                } catch (Exception e) {
                    failMessages.add(AttendanceConstant.ERR_IMPORT_DATA_PARSE_FAILED + " - " + e.getMessage());
                }
            }

            @Override
            public void doAfterAllAnalysed(AnalysisContext context) {
                if (importData.isEmpty() && failMessages.isEmpty()) {
                    failMessages.add(AttendanceConstant.ERR_IMPORT_DATA_EMPTY);
                }
                log.info("Excel读取完成，数据行数：{}", dataRowCount);
            }
        }).sheet().headRowNumber(3).doRead();

        result.setImportData(importData);
        result.setFailMessages(failMessages);
        return result;
    }

    /**
     * 校验表头日期是否与月份匹配
     */
    private boolean validateDateHeaders(List<String> dateHeaders, YearMonth yearMonth, String month) {
        if (CollUtil.isEmpty(dateHeaders)) {
            return false;
        }
        int maxDay = yearMonth.lengthOfMonth();
        boolean any = false;
        // 表头日期列须均为当月有效「日」：任一列解析出的日不在 1..当月天数 则视为与所选月份不一致
        for (String header : dateHeaders) {
            if (StringUtils.isBlank(header)) {
                continue;
            }
            Matcher dm = Pattern.compile("(\\d+)").matcher(header);
            if (!dm.find()) {
                continue;
            }
            try {
                int day = Integer.parseInt(dm.group(1));
                if (day < 1 || day > maxDay) {
                    return false;
                }
                any = true;
            } catch (NumberFormatException e) {
                return false;
            }
        }
        return any;
    }

    /**
     * 解析单行数据
     *
     * @param dataRowNum 当前数据行序号（不含表头，从 1 开始）
     * @param lastValidMobilePhone 跨行保留的「上半天」行手机号，用于「下半天」行因合并单元格读取为空时兜底（长度为 1 的数组）
     */
    private void parseRowData(Map<Integer, String> data, int dataRowNum, List<String> headers,
                              Map<String, PartUserInfoVo> phoneUserMap, YearMonth yearMonth,
                              int daysInMonth, String month, String configBookName, Set<String> seenPhonePeriods,
                              List<AttendanceBookRecordDto> importData, List<String> failMessages,
                              Map<String, String> leaveTypeNameToIdMap, String[] lastValidMobilePhone) {
        // 防御性跳过：模板里若误把"序号/手机号"列名残留到数据区，按表头行处理
        String mobilePhoneRaw = data.getOrDefault(2, null);
        if ("手机号".equals(mobilePhoneRaw) || "序号".equals(mobilePhoneRaw)) {
            return;
        }

        int dateStartIndex = getDateStartIndex(headers);
        boolean hasPeriodColumn = (dateStartIndex == 4);
        String periodStr = hasPeriodColumn ? data.getOrDefault(3, null) : null;
        // 「下半天」行的前三列（考勤本名称、序号、手机号）在导出模板里是合并单元格，EasyExcel 解析时仅首行（上半天）有值
        boolean isMergedAfternoonRow = hasPeriodColumn && AttendanceConstant.PERIOD_AFTERNOON.equals(periodStr);

        // 考勤本名称（首列）：与所选考勤本 ID 对应的配置名称比对；「下」行合并单元格常导致首列为空，仅上半天行强制填名称
        String bookNameCell = normalizeImportBookComparableText(data.getOrDefault(0, null));
        boolean skipBookNameCellChecks = isMergedAfternoonRow && StringUtils.isBlank(bookNameCell);
        if (!skipBookNameCellChecks) {
            if (StringUtils.isBlank(bookNameCell)) {
                failMessages.add(AttendanceConstant.ERR_IMPORT_BOOK_NAME_EMPTY);
                return;
            }
            if (StringUtils.isNotBlank(configBookName)
                    && !StringUtils.equals(bookNameCell, configBookName)) {
                failMessages.add(String.format(AttendanceConstant.ERR_IMPORT_BOOK_NAME_ROW_MISMATCH,
                        StringUtils.trimToEmpty(configBookName)));
                return;
            }
        }

        // 解析手机号（data 第 3 列，索引为 2）
        // 「下半天」行手机号常因合并单元格而为空，沿用上一条「上半天」行的有效手机号
        String mobilePhone = mobilePhoneRaw;
        if (StringUtils.isBlank(mobilePhone)) {
            if (isMergedAfternoonRow && StringUtils.isNotBlank(lastValidMobilePhone[0])) {
                mobilePhone = lastValidMobilePhone[0];
            } else {
                failMessages.add(AttendanceConstant.ERR_IMPORT_PHONE_EMPTY);
                return;
            }
        } else {
            // 更新跨行状态，供紧跟其后的「下半天」行复用
            lastValidMobilePhone[0] = mobilePhone;
        }

        Integer rowPeriodType = null;

        if (hasPeriodColumn) {
            // 2行/人格式：从"班次"列读取时段类型
            if (AttendanceConstant.PERIOD_MORNING.equals(periodStr)) {
                rowPeriodType = PeriodTypeEnum.MORNING.getCode();
            } else if (AttendanceConstant.PERIOD_AFTERNOON.equals(periodStr)) {
                rowPeriodType = PeriodTypeEnum.AFTERNOON.getCode();
            }
            // 使用 phone+period 组合去重，允许同一手机号出现两次（上/下各一次）
            String phonePeriodKey = mobilePhone + "_" + (periodStr != null ? periodStr : "");
            if (!AttendanceConstant.PERIOD_AFTERNOON.equals(periodStr) && !seenPhonePeriods.add(phonePeriodKey)) {
                failMessages.add(String.format(AttendanceConstant.ERR_IMPORT_PHONE_DUPLICATE, mobilePhone));
                return;
            }
        } else {
            // 1行/人格式：手机号不能重复
            if (!seenPhonePeriods.add(mobilePhone)) {
                failMessages.add(String.format(AttendanceConstant.ERR_IMPORT_PHONE_DUPLICATE, mobilePhone));
                return;
            }
        }

        // 查找用户信息
        PartUserInfoVo userInfo = phoneUserMap.get(mobilePhone);
        if (userInfo == null) {
            failMessages.add(String.format(AttendanceConstant.ERR_IMPORT_PHONE_NOT_FOUND, mobilePhone));
            return;
        }
        String userId = userInfo.getUserId();

        // 解析日期数据列
        parseDateColumns(data, headers, dateStartIndex, userId, yearMonth,
                daysInMonth, month, importData, failMessages, leaveTypeNameToIdMap, rowPeriodType);
    }


    /**
     * 获取日期数据起始列索引
     */
    private int getDateStartIndex(List<String> headers) {
        int dateStartIndex = 3;
        if (headers.size() > 3 && AttendanceConstant.HEADER_SHIFT.equals(headers.get(3))) {
            dateStartIndex = 4;
        }
        return dateStartIndex;
    }

    /**
     * 解析日期列数据
     *
     * @param rowPeriodType 行指定的时段类型，非null时仅生成该时段记录；null时生成上午和下午两条记录
     */
    private void parseDateColumns(Map<Integer, String> data, List<String> headers,
                                  int dateStartIndex, String userId, YearMonth yearMonth,
                                  int daysInMonth, String month, List<AttendanceBookRecordDto> importData,
                                  List<String> failMessages, Map<String, String> leaveTypeNameToIdMap,
                                  Integer rowPeriodType) {
        for (int i = dateStartIndex; i < headers.size() && i < data.size(); i++) {
            String statusStr = data.get(i);
            if (StringUtils.isBlank(statusStr)) {
                continue;
            }

            // 校验考勤状态或请假类型（严格按优先级匹配）
            Integer status = STATUS_CN_TO_CODE.get(statusStr);
            String leaveTypeId = null;
            if (status == null) {
                // 优先级2：不是标准考勤状态，尝试匹配请假类型名称
                leaveTypeId = leaveTypeNameToIdMap.get(statusStr);
                if (leaveTypeId == null) {
                    failMessages.add(String.format("%s：" + AttendanceConstant.ERR_IMPORT_STATUS_INVALID,
                            headers.get(i), statusStr));
                    continue;
                }
            }

            // 解析日期
            String header = headers.get(i);
            String dayNum = header.replaceAll("[^0-9]", "");
            if (StringUtils.isBlank(dayNum)) {
                continue;
            }

            try {
                int day = Integer.parseInt(dayNum);
                // 校验日期范围
                if (day < 1 || day > daysInMonth) {
                    failMessages.add(String.format("%s：" + AttendanceConstant.ERR_IMPORT_DATE_OUT_OF_RANGE, header, month));
                    continue;
                }

                // 构建记录
                LocalDate localDate = LocalDate.of(yearMonth.getYear(), yearMonth.getMonthValue(), day);
                Date dayDate = Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());

                if (rowPeriodType != null) {
                    // 2行/人格式：仅生成当前行指定时段的记录
                    importData.add(buildRecord(userId, dayDate, month, rowPeriodType, status, leaveTypeId));
                } else {
                    // 1行/人格式：同时生成上午和下午记录
                    importData.add(buildRecord(userId, dayDate, month, PeriodTypeEnum.MORNING.getCode(), status, leaveTypeId));
                    importData.add(buildRecord(userId, dayDate, month, PeriodTypeEnum.AFTERNOON.getCode(), status, leaveTypeId));
                }
            } catch (NumberFormatException e) {
                failMessages.add(String.format("%s：" + AttendanceConstant.ERR_IMPORT_DATE_PARSE_FAILED, header));
            }
        }
    }

    /**
     * 构建考勤记录实体
     */
    private AttendanceBookRecordDto buildRecord(String userId, Date dayDate, String month,
                                                Integer periodType, Integer status, String leaveTypeId) {
        return AttendanceBookRecordDto.builder()
                .userId(userId)
                .day(dayDate)
                .periodType(periodType)
                .attendanceStatus(status)
                .leaveType(leaveTypeId)
                .build();
    }

    /**
     * 校验解析结果
     */
    private void validateParseResult(ImportParseResult parseResult) {
        if (!parseResult.getFailMessages().isEmpty()) {
            String firstError = parseResult.getFailMessages().get(0);
            throw new RuntimeException(AttendanceConstant.ERR_IMPORT_FAILED_PREFIX + firstError);
        }
    }

    /**
     * 批量处理导入数据
     */
    private void batchProcessImportData(List<AttendanceBookRecordDto> importData, String bookId, Map<String, String> leaveTypeNameToIdMap) {
        if (CollUtil.isEmpty(importData)) {
            log.info("没有需要导入的数据");
            return;
        }

        // 获取当前用户信息
        UserInfo user = UserProvider.getUser();
        Date now = new Date();

        // 批量查询已存在的记录
        Map<String, AttendanceBookRecordEntity> existingRecordMap = batchQueryExistingRecords(
                importData.stream()
                        .map(record -> {
                            AttendanceBookRecordDto dto = new AttendanceBookRecordDto();
                            dto.setBookId(bookId);
                            dto.setUserId(record.getUserId());
                            dto.setDay(record.getDay());
                            dto.setPeriodType(record.getPeriodType());
                            return dto;
                        })
                        .collect(Collectors.toList()), bookId
        );

        List<AttendanceBookRecordEntity> toInsert = new ArrayList<>();
        List<AttendanceBookRecordEntity> toUpdate = new ArrayList<>();
        List<AttendanceBookRecordEntity> toClear = new ArrayList<>();
        // 批量收集操作日志，最后统一插入
        List<AttendanceBookOperationLogEntity> operationLogs = new ArrayList<>();
        for (AttendanceBookRecordDto record : importData) {
            String uniqueKey = buildUniqueKey(bookId, record.getUserId(),
                    record.getDay(), record.getPeriodType());
            record.setBookId(bookId);
            AttendanceBookRecordEntity existingRecord = existingRecordMap.get(uniqueKey);

            if (existingRecord != null && (Objects.nonNull(record.getAttendanceStatus()) || Objects.nonNull(record.getLeaveType()))) {
                // 收集操作日志，不立即插入数据库
                operationLogs.add(buildOperationLogEntity(record, existingRecord, AttendanceConstant.LOG_OPERATION_UPDATE, leaveTypeNameToIdMap));
                // 更新现有记录
                existingRecord.setAttendanceStatus(record.getAttendanceStatus());
                existingRecord.setLeaveType(record.getLeaveType());
                existingRecord.setLastModifyUserId(user != null ? user.getUserId() : null);
                existingRecord.setLastModifyTime(now);
                toUpdate.add(existingRecord);

                continue;
            }
            //清空数据
            if (existingRecord != null) {
                toClear.add(existingRecord);
                // 收集操作日志，不立即插入数据库
                operationLogs.add(buildOperationLogEntity(null, existingRecord, AttendanceConstant.LOG_OPERATION_CLEAN, leaveTypeNameToIdMap));
                continue;
            }
            // 新增记录
            AttendanceBookRecordEntity newEntity = buildEntityFromDto(record);
            newEntity.setId(RandomUtil.uuId());
            newEntity.setBookId(bookId);
            newEntity.setMonth(DateUtil.format(record.getDay(), "yyyy-MM"));
            newEntity.setCreatorUserId(user != null ? user.getUserId() : null);
            newEntity.setCreatorTime(now);
            newEntity.setTenantId(user != null ? user.getTenantId() : null);
            toInsert.add(newEntity);
            // 收集操作日志，不立即插入数据库
            operationLogs.add(buildOperationLogEntity(record, null, AttendanceConstant.LOG_OPERATION_ADD, leaveTypeNameToIdMap));
        }

        // 批量插入和更新
        if (!toInsert.isEmpty()) {
            this.saveBatch(toInsert);
        }
        if (!toUpdate.isEmpty()) {
            this.updateBatchById(toUpdate);
        }
        if (!toClear.isEmpty()) {
            this.removeBatchByIds(toClear);
        }
        // 批量插入操作日志
        if (!operationLogs.isEmpty()) {
            attendanceBookOperationLogService.saveBatch(operationLogs);
            log.info("批量插入操作日志成功，数量: {}", operationLogs.size());
        }
        log.info("考勤本导入成功，共处理{}条记录（新增: {}, 更新: {}）",
                importData.size(), toInsert.size(), toUpdate.size());
    }

    /**
     * 导入解析结果
     */
    private static class ImportParseResult {
        private List<AttendanceBookRecordDto> importData;
        private List<String> failMessages;

        public List<AttendanceBookRecordDto> getImportData() {
            return importData;
        }

        public void setImportData(List<AttendanceBookRecordDto> importData) {
            this.importData = importData;
        }

        public List<String> getFailMessages() {
            return failMessages;
        }

        public void setFailMessages(List<String> failMessages) {
            this.failMessages = failMessages;
        }
    }

    @Override
    /**
     * 导出考勤本记录
     *
     * 导出格式（参考划线排班）：
     * - 3层表头：标题、说明、表头名称
     * - 每个员工2行：上半天和下半天
     * - 前3列合并（考勤本名称、姓名、手机号）
     * - 第4列显示"上"或"下"
     *
     * @param bookId 考勤本ID
     * @param month 月份（yyyy-MM）
     */
    public void bookRecordExport(String bookId, String month) {
        log.info("开始导出考勤本数据，bookId: {}, month: {}", bookId, month);
        // 1. 查询基础数据
        AttendanceBookConfigEntity bookConfig = attendanceBookConfigService.getById(bookId);
        Assert.notNull(bookConfig, "当前考勤本不存在");
        Date start = DateDetail.getMonthBeginDate(month);
        Date end = DateDetail.getMonthEndDate(month);
        List<Date> dates = DateDetail.getDatesByPeriod(start, end);
        List<AttendanceBookRecordEntity> records = queryBookRecords(bookId, start, end);

        // 2. 获取考勤本使用范围内的所有用户（即使没有考勤记录也要导出）
        List<String> allUserIds = getAllUserIdsFromBook(bookConfig, start, end);

        // 3. 数据分组
        Map<String, List<AttendanceBookRecordEntity>> recordsByUser = records.stream()
                .collect(Collectors.groupingBy(AttendanceBookRecordEntity::getUserId));
        // 3.1 批量查询请假类型，构建ID到名称的映射
        Map<String, AttendanceLeaveType> leaveTypeIdToNameMap = getAttendanceLeaveTypeMap(bookConfig, records);
        Map<String, PartUserInfoVo> userInfoMap = getUserInfoMap(allUserIds);
        List<String> strings = new ArrayList<>(userInfoMap.keySet());
        String titleHead = DateUtil.format(start, "yyyy年MM月") + bookConfig.getBookName() + "考勤本";
        // 4. 构建导出配置
        List<List<String>> heads = buildExportHeaders(titleHead, dates, leaveTypeIdToNameMap.values().stream().filter(vo -> Objects.equals(vo.getDeleteMark(), 0)).map(AttendanceLeaveType::getName).distinct().collect(Collectors.toList()));
        List<org.apache.poi.ss.util.CellRangeAddress> mergeCells = buildMergeCells(strings.size());

        // 5. 创建Handler并导出
        jnpf.attendance.excel.AttendanceBookMergeHandler mergeHandler =
                new jnpf.attendance.excel.AttendanceBookMergeHandler(
                        mergeCells, 3, strings.size() * 2, heads.size());

        jnpf.attendance.excel.CompositeWriteHandler compositeHandler =
                new jnpf.attendance.excel.CompositeWriteHandler(new HeadStyleHandler(), mergeHandler);

        excelExportTemplate.exportModule(UserProvider.getLoginUserId(), titleHead, BookRecordMonthStatisticsVo.class,
                heads, null, null, 2000, () -> compositeHandler,
                (page, size) -> buildExportData(recordsByUser, userInfoMap, bookConfig.getBookName(), dates, strings, leaveTypeIdToNameMap));

        log.info("考勤本导出完成，员工数: {}", strings.size());
    }


    /**
     * 获取考勤本使用范围内的所有用户ID
     * 包括：已有考勤记录的用户 + 考勤本配置的使用范围内的用户
     */
    private List<String> getAllUserIdsFromBook(AttendanceBookConfigEntity bookConfig, Date start, Date end) {

        // 2. 添加考勤本使用范围内的用户
        try {
            MutablePair<List<String>, List<String>> scopePair = ruleScopeUtil.selectScopeList(
                    bookConfig.getId(), ScopeBizType.ATTENDANCE_BOOK);
            if (scopePair != null) {

                List<String> orgIds = scopePair.getLeft();
                List<String> directUserIds = scopePair.getRight();
                // 使用 attendanceUserService.getUserIds 直接获取组织下的用户
                List<String> userIds = attendanceUserService.getUserIds(
                        orgIds,  // 组织ID列表
                        directUserIds,           // 直接用户ID列表（无）
                        start,                              // 开始时间（不过滤）
                        end,                              // 结束时间（不过滤）
                        bookConfig.getScopeType()                               // 适用范围（不过滤）
                );
                log.info("考勤本使用范围 - 组织ID数: {}, 直接用户ID数: {}",
                        orgIds != null ? orgIds.size() : 0,
                        directUserIds != null ? directUserIds.size() : 0);
                return userIds.stream().distinct().collect(Collectors.toList());
            }
        } catch (Exception e) {
            log.error("获取考勤本使用范围用户失败，bookId: {}", bookConfig.getId(), e);
        }
        return Collections.emptyList();
    }

    /**
     * 查询考勤记录
     */
    private List<AttendanceBookRecordEntity> queryBookRecords(String bookId, Date start, Date end) {
        LambdaQueryWrapper<AttendanceBookRecordEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AttendanceBookRecordEntity::getBookId, bookId)
                .between(AttendanceBookRecordEntity::getDay, start, end)
                .orderByAsc(AttendanceBookRecordEntity::getUserId)
                .orderByAsc(AttendanceBookRecordEntity::getDay)
                .orderByAsc(AttendanceBookRecordEntity::getPeriodType);
        return this.list(queryWrapper);
    }

    /**
     * 获取员工信息映射
     */
    private Map<String, PartUserInfoVo> getUserInfoMap(List<String> userIds) {
        if (CollUtil.isEmpty(userIds)) {
            return Collections.emptyMap();
        }
        List<PartUserInfoVo> userInfoList = userAntifreeze.getStaffRosterListInfoByIds(userIds);
        return userInfoList.stream()
                .collect(Collectors.toMap(PartUserInfoVo::getUserId, user -> user, (u1, u2) -> u1));
    }

    /**
     * 构建导出表头（3层结构：标题、说明、表头名称）
     */
    private List<List<String>> buildExportHeaders(String titleHead, List<Date> dates, List<String> leaveTypeNames) {

        String shiftHead = "导入说明：\n" +
                "1.考勤本名称、手机号为必填项；表头日期、标题年月须与所选导入月份一致\n" +
                "2.考勤本结果为正常、公休、迟到、早退、缺卡、旷工、外出、外勤、出差、" + String.join("、", leaveTypeNames) + "，日期考勤结果选择为空时，将不会导入考勤本结果，员工没有此项考勤数据\n" +
                "3.请按照数据模板的格式准备导入数据，模板中的表头名称不可更改，表头行不能删除\n" +
                "4.导入已经导入过的员工信息将更新原有信息，请谨慎操作\n" +
                "5.同一张表内同一手机号码不可重复（两行/人格式时，同一号码可分别出现「上」「下」两行）\n" +
                "6.文件大小不超过500MB，最多支持导入500行数据（不含表头），超出请分批导入\n";

        List<List<String>> heads = new ArrayList<>();
        // 基本信息列
        heads.add(CollUtil.newArrayList(titleHead, shiftHead, "考勤本名称"));
        heads.add(CollUtil.newArrayList(titleHead, shiftHead, "姓名"));
        heads.add(CollUtil.newArrayList(titleHead, shiftHead, "手机号"));
        heads.add(CollUtil.newArrayList(titleHead, shiftHead, "班次"));
        // 日期列
        for (Date date : dates) {
            String dateHeader = DateUtil.format(date, "d") + " (" + DateUtil.dayOfWeekEnum(date).toChinese("周") + ")";
            heads.add(CollUtil.newArrayList(titleHead, shiftHead, dateHeader));
        }
        return heads;
    }

    /**
     * 构建合并单元格策略（前3列合并）
     */
    private List<org.apache.poi.ss.util.CellRangeAddress> buildMergeCells(int employeeCount) {
        List<org.apache.poi.ss.util.CellRangeAddress> mergeCells = new ArrayList<>();
        for (int i = 0; i < employeeCount; i++) {
            int rowIndex = i * 2;  // 每个员工2行
            mergeCells.add(new org.apache.poi.ss.util.CellRangeAddress(rowIndex, rowIndex + 1, 0, 0));
            mergeCells.add(new org.apache.poi.ss.util.CellRangeAddress(rowIndex, rowIndex + 1, 1, 1));
            mergeCells.add(new org.apache.poi.ss.util.CellRangeAddress(rowIndex, rowIndex + 1, 2, 2));
        }
        return mergeCells;
    }

    /**
     * 构建导出数据（每个员工2行：上半天、下半天）
     */
    private List<Map<Integer, Object>> buildExportData(
            Map<String, List<AttendanceBookRecordEntity>> recordsByUser,
            Map<String, PartUserInfoVo> userInfoMap,
            String bookName,
            List<Date> dates,
            List<String> allUserIds,
            Map<String, AttendanceLeaveType> leaveTypeIdToNameMap) {

        List<Map<Integer, Object>> data = new ArrayList<>();

        // 遍历所有用户（包括没有考勤记录的用户）
        for (String userId : allUserIds) {
            PartUserInfoVo userInfo = userInfoMap.get(userId);
            String realName = userInfo != null ? userInfo.getRealName() : "";
            String mobilePhone = userInfo != null ? userInfo.getMobilePhone() : "";

            // 获取该用户的考勤记录
            List<AttendanceBookRecordEntity> userRecords = recordsByUser.getOrDefault(userId, Collections.emptyList());

            // 按日期分组
            Map<String, List<AttendanceBookRecordEntity>> recordsByDay = userRecords.stream()
                    .collect(Collectors.groupingBy(record -> DateUtil.format(record.getDay(), "yyyy-MM-dd")));

            // 上半天
            Map<Integer, Object> morningRow = new HashMap<>();
            morningRow.put(0, bookName);
            morningRow.put(1, realName);
            morningRow.put(2, mobilePhone);
            morningRow.put(3, AttendanceConstant.PERIOD_MORNING);
            fillAttendanceStatus(morningRow, recordsByDay, dates, PeriodTypeEnum.MORNING.getCode(), leaveTypeIdToNameMap);

            // 下半天
            Map<Integer, Object> afternoonRow = new HashMap<>();
            afternoonRow.put(0, bookName);
            afternoonRow.put(1, realName);
            afternoonRow.put(2, mobilePhone);
            afternoonRow.put(3, AttendanceConstant.PERIOD_AFTERNOON);
            fillAttendanceStatus(afternoonRow, recordsByDay, dates, PeriodTypeEnum.AFTERNOON.getCode(), leaveTypeIdToNameMap);

            data.add(morningRow);
            data.add(afternoonRow);
        }

        return data;
    }

    /**
     * 填充考勤状态数据
     */
    private void fillAttendanceStatus(
            Map<Integer, Object> row,
            Map<String, List<AttendanceBookRecordEntity>> recordsByDay,
            List<Date> dates,
            Integer periodType,
            Map<String, AttendanceLeaveType> leaveTypeIdToNameMap) {

        for (int i = 0; i < dates.size(); i++) {
            String dateStr = DateUtil.format(dates.get(i), "yyyy-MM-dd");
            List<AttendanceBookRecordEntity> dayRecords = recordsByDay.get(dateStr);

            String status = "";
            if (CollUtil.isNotEmpty(dayRecords)) {
                AttendanceBookRecordEntity record = dayRecords.stream()
                        .filter(r -> r.getPeriodType().equals(periodType))
                        .findFirst()
                        .orElse(null);
                if (record != null) {
                    // 如果有请假类型，从映射中获取名称
                    if (StringUtils.isNotBlank(record.getLeaveType()) && leaveTypeIdToNameMap.containsKey(record.getLeaveType())) {
                        status = leaveTypeIdToNameMap.get(record.getLeaveType()).getName();
                    } else {
                        // 其他状态：使用状态名称
                        status = STATUS_CODE_TO_CN.getOrDefault(record.getAttendanceStatus(), "");
                    }
                }
            }
            row.put(i + 4, status);
        }
    }

    /**
     * 保存操作日志
     *
     * @param entity         考勤记录实体
     * @param existingRecord 已有记录（更新时不为null）
     * @param operationType  操作类型
     */
    private void saveOperationLog(AttendanceBookRecordEntity entity, AttendanceBookRecordEntity existingRecord, String operationType) {
        try {
            // 参数校验
            if (entity == null) {
                log.warn("考勤记录实体为空，跳过记录操作日志");
                return;
            }

            // 构建操作内容描述（按照图片格式）
            // 单条记录时，直接查询请假类型名称
            String operationContent = buildOperationContent(entity, existingRecord, operationType);

            // 获取当前用户信息
            UserInfo user = UserProvider.getUser();

            // 创建操作日志实体
            AttendanceBookOperationLogEntity logEntity = AttendanceBookOperationLogEntity.builder()
                    .bookId(entity.getBookId())
                    .month(entity.getMonth())
                    .recordId(existingRecord != null ? existingRecord.getId() : entity.getId())
                    .employeeId(entity.getUserId())
                    .operationContent(operationContent)
                    .operatorId(user != null ? user.getUserId() : null)
                    .operatorName(user != null ? user.getUserName() : null)
                    .operationTime(new Date())
                    .build();

            attendanceBookOperationLogService.saveLog(logEntity);
            log.debug("记录考勤本操作日志成功，记录ID: {}, 员工ID: {}", entity.getId(), entity.getUserId());
        } catch (Exception e) {
            // 记录操作日志失败不影响主业务，仅记录警告日志
            log.warn("记录操作日志失败，记录ID: {}, 员工ID: {}, 原因: {}",
                    entity.getId(), entity.getUserId(), e.getMessage(), e);
        }
    }

    /**
     * 构建操作内容描述（单条记录，直接查询请假类型）
     *
     * @param entity         考勤记录实体
     * @param existingRecord 已有记录（更新时不为null）
     * @param operationType  操作类型
     * @return 操作内容描述
     */
    private String buildOperationContent(AttendanceBookRecordEntity entity, AttendanceBookRecordEntity existingRecord, String operationType) {
        // 调整前/后可能各带不同请假类型，须同时解析，避免「修改前」仅按新记录的 leaveType 建映射导致旧假别被当成考勤状态展示
        LinkedHashSet<String> leaveTypeIds = new LinkedHashSet<>();
        if (StringUtils.isNotBlank(entity.getLeaveType())) {
            leaveTypeIds.add(entity.getLeaveType().trim());
        }
        if (existingRecord != null && StringUtils.isNotBlank(existingRecord.getLeaveType())) {
            leaveTypeIds.add(existingRecord.getLeaveType().trim());
        }
        Map<String, String> leaveTypeIdToNameMap = buildLeaveTypeIdToNameMap(leaveTypeIds);
        return buildOperationContent(entity, existingRecord, operationType, leaveTypeIdToNameMap);
    }

    /**
     * 构建操作内容描述（使用映射）
     *
     * @param entity         考勤记录实体
     * @param existingRecord 已有记录（更新时不为null）
     * @param operationType  操作类型
     * @param leaveTypeIdToNameMap 请假类型ID到名称的映射
     * @return 操作内容描述
     */
    private String buildOperationContent(AttendanceBookRecordEntity entity, AttendanceBookRecordEntity existingRecord, String operationType, Map<String, String> leaveTypeIdToNameMap) {
        // 获取员工姓名
        String employeeName = getEmployeeName(entity.getUserId());

        // 格式化日期信息
        String formattedDate = formatDateWithWeekday(entity.getDay());

        // 时段类型：上午/下午
        String periodName = getPeriodName(entity.getPeriodType());

        StringBuilder content = new StringBuilder();

        if (AttendanceConstant.LOG_OPERATION_UPDATE.equals(operationType) && existingRecord != null) {
            // 需求8：更新操作，使用红色标记考勤状态
            content.append("对员工\"")
                    .append(employeeName)
                    .append("\"在")
                    .append(formattedDate)
                    .append(periodName)
                    .append("的班次结果由")
                    .append(getStatusDisplayWithRedMark(existingRecord.getAttendanceStatus(), existingRecord.getLeaveType(), leaveTypeIdToNameMap))
                    .append("调整为")
                    .append(getStatusDisplayWithRedMark(entity.getAttendanceStatus(), entity.getLeaveType(), leaveTypeIdToNameMap));
        } else if (AttendanceConstant.LOG_OPERATION_CLEAN.equals(operationType) && existingRecord != null) {
            content.append("清除员工\"")
                    .append(employeeName)
                    .append("\"")
                    .append(formattedDate)
                    .append(periodName)
                    .append("的班次结果")
                    .append(getStatusDisplayWithRedMark(existingRecord.getAttendanceStatus(), existingRecord.getLeaveType(), leaveTypeIdToNameMap));
        } else {
            // 需求8：新增操作，使用红色标记考勤状态
            content.append("设置员工\"")
                    .append(employeeName)
                    .append("\"")
                    .append(formattedDate)
                    .append(periodName)
                    .append("的班次结果为")
                    .append(getStatusDisplayWithRedMark(entity.getAttendanceStatus(), entity.getLeaveType(), leaveTypeIdToNameMap));
        }

        // 添加请假类型和备注信息（使用映射）
        appendLeaveInfoWithMap(content, entity, leaveTypeIdToNameMap);

        return content.toString();
    }

    /**
     * 获取员工姓名
     *
     * @param userId 用户ID
     * @return 员工姓名，如果查询失败则返回用户ID
     */
    private String getEmployeeName(String userId) {
        if (StringUtils.isBlank(userId)) {
            return "未知员工";
        }
        try {
            // 尝试从用户信息中获取真实姓名
            List<PartUserInfoVo> userInfoList = userAntifreeze.getStaffRosterListInfoByIds(Collections.singletonList(userId));
            if (CollUtil.isNotEmpty(userInfoList)) {
                return userInfoList.get(0).getRealName();
            }
        } catch (Exception e) {
            log.warn("获取员工姓名失败，userId: {}, 原因: {}", userId, e.getMessage());
        }
        // 降级处理：返回用户ID
        return userId;
    }

    /**
     * 格式化日期为：yyyy-MM-dd（星期X）
     *
     * @param date 日期
     * @return 格式化后的日期字符串
     */
    private String formatDateWithWeekday(Date date) {
        if (date == null) {
            return "未知日期";
        }
        String formattedDate = DateUtil.format(date, "yyyy-MM-dd");
        String dayOfWeek = DateUtil.dayOfWeekEnum(date).toChinese("星期");
        return formattedDate + "（" + dayOfWeek + "）";
    }

    /**
     * 获取时段名称
     *
     * @param periodType 时段类型
     * @return 时段名称（上半天/下半天）
     */
    private String getPeriodName(Integer periodType) {
        if (periodType == null) {
            return "未知时段";
        }
        return PeriodTypeEnum.MORNING.getCode().equals(periodType) ? "上半天" : "下半天";
    }

    /**
     * 添加请假类型和备注信息（使用映射）
     *
     * @param content 内容构建器
     * @param entity  考勤记录实体
     * @param leaveTypeIdToNameMap 请假类型ID到名称的映射
     */
    private void appendLeaveInfoWithMap(StringBuilder content, AttendanceBookRecordEntity entity, Map<String, String> leaveTypeIdToNameMap) {
        if (entity == null) {
            return;
        }

        // 当为请假类型时，添加备注内容
        if (StringUtils.isNotBlank(entity.getLeaveType())) {
            // 从映射中获取请假类型名称
            String leaveTypeName = leaveTypeIdToNameMap != null ?
                    leaveTypeIdToNameMap.getOrDefault(entity.getLeaveType(), entity.getLeaveType()) :
                    entity.getLeaveType();
            content.append("，请假类型：").append(leaveTypeName);
            if (StringUtils.isNotBlank(entity.getRemark())) {
                content.append("，备注：").append(entity.getRemark());
            }
        }
    }

    /**
     * 根据请假类型 ID 集合构建「ID → 名称」映射（用于操作日志等）。
     * 批量接口须同时纳入修改前记录上的 leaveType，否则日志里「由 xxx 调整为」的上一段会丢失假别名称。
     */
    private Map<String, String> buildLeaveTypeIdToNameMap(Collection<String> leaveTypeIds) {
        if (CollUtil.isEmpty(leaveTypeIds)) {
            return Collections.emptyMap();
        }
        List<String> idList = leaveTypeIds.stream()
                .filter(StringUtils::isNotBlank)
                .map(String::trim)
                .distinct()
                .collect(Collectors.toList());
        if (CollUtil.isEmpty(idList)) {
            return Collections.emptyMap();
        }
        Map<String, String> map = new LinkedHashMap<>();
        List<AttendanceLeaveType> leaveTypes = attendanceLeaveTypeService.getAttendanceLeaveTypes(idList);
        if (CollUtil.isNotEmpty(leaveTypes)) {
            for (AttendanceLeaveType t : leaveTypes) {
                map.put(t.getId(), t.getName());
            }
        }
        for (String id : idList) {
            if (map.containsKey(id)) {
                continue;
            }
            AttendanceLeaveType byId = attendanceLeaveTypeService.getById(id);
            if (byId != null && StringUtils.isNotBlank(byId.getName())) {
                map.put(id, byId.getName());
            } else {
                map.put(id, id);
            }
        }
        return map;
    }

    /**
     * 获取考勤状态中文名称
     *
     * @param status 状态码
     * @return 中文名称
     */
    private String getStatusName(Integer status) {
        if (status == null) {
            return "未知";
        }
        return STATUS_CODE_TO_CN.getOrDefault(status, "未知");
    }

    /**
     * 批量查询请假类型名称，构建ID到名称的映射
     * <p>规则与 {@link #getMonthStatistics(BookRecordMonthStatisticsDto)} 一致：
     * 可排假期为「全部假期」({@code holidayScopeType == 1}) 或未配置具体 ID 时查询全部未删除的假期类型，
     * 避免导出时映射为空导致单元格仅显示假期类型原始 ID（如 {@code 1}）。</p>
     */
    private Map<String, String> batchQueryLeaveTypeNames(AttendanceBookConfigEntity bookConfig) {
        if (bookConfig == null) {
            return Collections.emptyMap();
        }
        List<String> leaveTypeIds = Objects.equals(bookConfig.getHolidayScopeType(), 1)
                ? null
                : (StringUtils.isNotBlank(bookConfig.getHolidayIds())
                   ? Arrays.stream(bookConfig.getHolidayIds().split(","))
                     .filter(StringUtils::isNotBlank)
                     .collect(Collectors.toList())
                   : null);

        List<AttendanceLeaveType> leaveTypes = attendanceLeaveTypeService.getAttendanceLeaveTypes(leaveTypeIds);
        if (CollUtil.isEmpty(leaveTypes)) {
            return Collections.emptyMap();
        }
        return leaveTypes.stream()
                .collect(Collectors.toMap(AttendanceLeaveType::getId, AttendanceLeaveType::getName, (v1, v2) -> v1));
    }

    /**
     * 获取考勤状态展示名称：有请假类型时返回假期名称，否则返回标准状态中文名。
     */
    private String getStatusNameWithLeaveType(AttendanceBookRecordEntity record, Map<String, AttendanceLeaveType> leaveTypeIdToNameMap) {
        if (record == null) {
            return null;
        }
        // 如果有请假类型，从映射中获取名称
        if (StringUtils.isNotBlank(record.getLeaveType()) && leaveTypeIdToNameMap != null && leaveTypeIdToNameMap.containsKey(record.getLeaveType())) {
            return leaveTypeIdToNameMap.get(record.getLeaveType()).getName();
        }
        // 否则使用状态名称
        return getStatusName(record.getAttendanceStatus());
    }

    /**
     * 获取考勤状态显示名称（带红色标记），优先使用假期类型名称
     * 当记录有 leaveType 时展示假别名称（映射缺失时回退到考勤状态中文），不得再用 status 覆盖假别展示（否则修改前请假易被显示成「正常」）
     */
    private String getStatusDisplayWithRedMark(Integer status, String leaveType, Map<String, String> leaveTypeIdToNameMap) {
        String displayName;
        if (StringUtils.isNotBlank(leaveType)) {
            if (leaveTypeIdToNameMap != null) {
                displayName = leaveTypeIdToNameMap.getOrDefault(leaveType, getStatusName(status));
            } else {
                displayName = getStatusName(status);
            }
        } else if (Objects.nonNull(status)) {
            displayName = getStatusName(status);
        } else {
            displayName = "空";
        }
        return AttendanceConstant.RED_MARK_START + "【" + displayName + "】" + AttendanceConstant.RED_MARK_END;
    }


    @Override
    public void exportTemplate(String bookId, String month, HttpServletResponse response) {
        log.info("开始导出考勤本模板，bookId: {}, month: {}", bookId, month);

        try {
            // 1. 查询考勤本配置
            AttendanceBookConfigEntity bookConfig = attendanceBookConfigService.getById(bookId);
            Assert.notNull(bookConfig, "当前考勤本不存在");

            Date start = DateDetail.getMonthBeginDate(month);
            Date end = DateDetail.getMonthEndDate(month);
            List<Date> dates = DateDetail.getDatesByPeriod(start, end);

            // 2. 获取考勤本使用范围内的所有用户（与导出口径一致），用于在模板中预填「考勤本名称 / 姓名 / 手机号」三列
            List<String> allUserIds = getAllUserIdsFromBook(bookConfig, start, end);
            Map<String, PartUserInfoVo> userInfoMap = getUserInfoMap(allUserIds);
            List<String> userIdList = new ArrayList<>(userInfoMap.keySet());

            // 3. 复用考勤本导出的表头结构
            String titleHead = DateUtil.format(start, "yyyy年MM月") + bookConfig.getBookName() + "考勤本";
            List<String> leaveTypeNames = batchQueryLeaveTypeNames(bookConfig).values()
                    .stream().distinct().collect(Collectors.toList());
            List<List<String>> headList = buildExportHeaders(titleHead, dates, leaveTypeNames);

            // 4. 构建模板数据：复用 buildExportData，传入空考勤记录 Map，使日期列保持空白；
            //    仅前 4 列被预填（考勤本名称 / 姓名 / 手机号 / 班次）
            List<Map<Integer, Object>> templateData = buildExportData(
                    Collections.emptyMap(),
                    userInfoMap,
                    bookConfig.getBookName(),
                    dates,
                    userIdList,
                    Collections.emptyMap());

            // 5. 前 3 列「上半天/下半天」合并单元格处理器（与 bookRecordExport 完全一致）
            List<org.apache.poi.ss.util.CellRangeAddress> mergeCells = buildMergeCells(userIdList.size());
            jnpf.attendance.excel.AttendanceBookMergeHandler mergeHandler =
                    new jnpf.attendance.excel.AttendanceBookMergeHandler(
                            mergeCells, 3, userIdList.size() * 2, headList.size());
            jnpf.attendance.excel.CompositeWriteHandler compositeHandler =
                    new jnpf.attendance.excel.CompositeWriteHandler(new HeadStyleHandler(), mergeHandler);

            // 6. 设置响应头，直接流导出
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            String fileName = java.net.URLEncoder.encode(titleHead, "UTF-8").replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

            // 7. 使用 EasyExcel 直接写入响应流（含合并处理器与预填的前 4 列）
            com.alibaba.excel.EasyExcel.write(response.getOutputStream())
                    .head(headList)
                    .registerWriteHandler(compositeHandler)
                    .sheet("考勤本模板")
                    .doWrite(templateData);

            log.info("考勤本模板导出完成，员工数: {}", userIdList.size());
        } catch (Exception e) {
            log.error("考勤本模板导出失败", e);
            throw new RuntimeException("导出模板失败：" + e.getMessage());
        }
    }

    @Override
    public List<BookRecordDayListVo> getDayList(BookRecordDayListDto req) {
        log.info("获取日考勤本列表，bookId: {}, day: {}", req.getBookId(), req.getDay());

        // 参数校验
        Assert.notBlank(req.getBookId(), "考勤本ID不能为空");
        Assert.notBlank(req.getDay(), "日期不能为空");

        // 1. 查询考勤本配置
        AttendanceBookConfigEntity config = attendanceBookConfigService.getById(req.getBookId());
        if (config == null) {
            log.warn("考勤本配置不存在，bookId: {}", req.getBookId());
            return CollUtil.newArrayList();
        }

        // 2. 解析日期，获取该日的起止时间
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        LocalDate localDate = LocalDate.parse(req.getDay(), dateFormatter);
        Date startDate = Date.from(localDate.atStartOfDay().atZone(java.time.ZoneId.systemDefault()).toInstant());
        Date endDate = Date.from(localDate.atTime(23, 59, 59).atZone(java.time.ZoneId.systemDefault()).toInstant());
        MutablePair<List<BookScopeUserVo>, List<AttendanceGroupUser>> userAndGroupIds = getUserIdsFromScope(req.getBookId(), config.getScopeType(), startDate, endDate);
        // 3. 从考勤本使用范围中获取人员列表
        List<BookScopeUserVo> userInfoList = userAndGroupIds.getLeft();
        List<AttendanceGroupUser> groupUsers = userAndGroupIds.getRight();
        // 4. 调用userAntifreeze获取员工详细信息
        if (CollUtil.isEmpty(userInfoList)) {
            return CollUtil.newArrayList();
        }

        // 构建用户信息映射
        List<String> userIds = userInfoList.stream().map(BookScopeUserVo::getUserId).distinct().collect(Collectors.toList());

        // 查询用户考勤组成员信息（用于判断离组状态）
        Map<String, List<AttendanceGroupUser>> groupUserMap = groupUsers.stream()
                .collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));

        // 5. 查询该考勤本该日的所有考勤记录
        LambdaQueryWrapper<AttendanceBookRecordEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AttendanceBookRecordEntity::getBookId, req.getBookId())
                .in(AttendanceBookRecordEntity::getUserId, userIds)
                .between(AttendanceBookRecordEntity::getDay, startDate, endDate)
                .orderByAsc(AttendanceBookRecordEntity::getUserId)
                .orderByAsc(AttendanceBookRecordEntity::getDay)
                .orderByAsc(AttendanceBookRecordEntity::getPeriodType);

        List<AttendanceBookRecordEntity> records = this.list(queryWrapper);

        // 6. 按员工分组
        Map<String, List<AttendanceBookRecordEntity>> recordsByUser = records.stream()
                .collect(Collectors.groupingBy(AttendanceBookRecordEntity::getUserId));
        Map<String, AttendanceLeaveType> stringStringMap = getAttendanceLeaveTypeMap(config, records);

        // 批量查询用户锁定状态
        Map<String, Boolean> sealMap = attendanceDayStatisticsService.selectUserIsSeal(userIds, DateUtil.format(startDate, "yyyy-MM"));

        // 7. 构建返回结果
        List<BookRecordDayListVo> result = new ArrayList<>();
        int sortOrder = 1;

        for (BookScopeUserVo userInfo : userInfoList) {
            String userId = userInfo.getUserId();
            List<AttendanceBookRecordEntity> userRecords = recordsByUser.getOrDefault(userId, CollUtil.newArrayList());

            // 计算用户当日是否离组
            List<AttendanceGroupUser> userGroupUsers = groupUserMap.getOrDefault(userId, CollUtil.newArrayList());
            Integer existStatus = AttendanceGroupUserStatusUtil.isExistStatus(userGroupUsers, startDate);
            if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) {
                existStatus = 1;
            }
            if (!Objects.equals(existStatus, 1)) {
                continue;
            }
            // 构建每日记录列表
            List<BookRecordDayVo> recordList = new ArrayList<>();

            // 按日期分组
            Map<String, List<AttendanceBookRecordEntity>> recordsByDay = userRecords.stream()
                    .collect(Collectors.groupingBy(record -> DateUtil.format(record.getDay(), "yyyy-MM-dd")));
            // 按日期排序
            List<String> sortedDays = recordsByDay.keySet().stream()
                    .sorted()
                    .collect(Collectors.toList());

            for (String day : sortedDays) {
                List<AttendanceBookRecordEntity> dayRecords = recordsByDay.get(day);
                // 为每条记录构建VO
                for (AttendanceBookRecordEntity record : dayRecords) {
                    BookRecordDayVo dayVo = BookRecordDayVo.builder()
                            .id(record.getId())
                            .day(day)
                            .periodType(record.getPeriodType())
                            .periodTypeName(PeriodTypeEnum.MORNING.getCode().equals(record.getPeriodType()) ? "上班卡" : "下班卡")
                            .attendanceStatus(resolveStatus(record))
                            .attendanceStatusName(getStatusNameWithLeaveType(record, stringStringMap))
                            .leaveType(record.getLeaveType())
                            .leaveTypeName(StringUtils.isBlank(record.getLeaveType()) || !stringStringMap.containsKey(record.getLeaveType()) ? null
                                    : stringStringMap.get(record.getLeaveType()).getName())
                            .remark(record.getRemark())
                            .changed(record.getLastModifyTime() != null)
                            .isOutGroup(existStatus)
                            .build();
                    recordList.add(dayVo);
                }
            }

            // 检查是否已有上午和下午的记录，如果没有则补充空实体
            boolean hasMorning = recordList.stream().anyMatch(r -> PeriodTypeEnum.MORNING.getCode().equals(r.getPeriodType()));
            boolean hasAfternoon = recordList.stream().anyMatch(r -> PeriodTypeEnum.AFTERNOON.getCode().equals(r.getPeriodType()));

            // 如果没有上午记录，补充空实体
            if (!hasMorning) {
                recordList.add(BookRecordDayVo.builder()
                        .day(req.getDay())
                        .periodType(PeriodTypeEnum.MORNING.getCode())
                        .periodTypeName(PeriodTypeEnum.MORNING.getName())
                        .isOutGroup(existStatus)
                        .build());
            }

            // 如果没有下午记录，补充空实体
            if (!hasAfternoon) {
                recordList.add(BookRecordDayVo.builder()
                        .day(req.getDay())
                        .periodType(PeriodTypeEnum.AFTERNOON.getCode())
                        .periodTypeName(PeriodTypeEnum.AFTERNOON.getName())
                        .isOutGroup(existStatus)
                        .build());
            }
            int secondmentType = SecondmentTypeUtil.resolveSecondmentType(userGroupUsers, startDate, endDate);
            // 构建员工分组VO
            BookRecordDayListVo dayListVo = BookRecordDayListVo.builder()
                    .userId(userId)
                    .realName(userInfo.getUserName())
                    .positionName(userInfo.getUserPost())
                    .organizeName(userInfo.getOrganization())
                    .mobilePhone(userInfo.getMobilePhone())
                    .secondmentType(secondmentType)
                    .isOutGroup(userInfo.getIsOutGroup())
                    .isSeal(sealMap.getOrDefault(userId, false))
                    .sortOrder(sortOrder++)
                    .recordList(recordList.stream().sorted(Comparator.comparing(BookRecordDayVo::getPeriodType)).collect(Collectors.toList()))
                    .build();

            result.add(dayListVo);
        }

        log.info("获取日考勤本列表成功，bookId: {}, day: {}, 员工数: {}",
                req.getBookId(), req.getDay(), result.size());
        return result;
    }

    /**
     * 从考勤本使用范围中获取用户信息列表
     * 返回包含用户基础信息及借调信息的完整用户信息
     *
     * @param bookId    考勤本ID
     * @param scopeType 使用范围类型（0:全部 1:指定组织/成员）
     * @param start     开始时间
     * @param end       结束时间
     * @return 用户信息列表（包含借调信息）
     */
    private MutablePair<List<BookScopeUserVo>, List<AttendanceGroupUser>> getUserIdsFromScope(String bookId, Integer scopeType, Date start, Date end) {
        // 查询使用范围
        MutablePair<List<String>, List<String>> scopeList = ruleScopeUtil.selectScopeList(bookId, ScopeBizType.ATTENDANCE_BOOK);

        List<String> orgIds = scopeList != null ? scopeList.getLeft() : new ArrayList<>();
        List<String> directUserIds = scopeList != null ? scopeList.getRight() : new ArrayList<>();

        // 获取用户ID列表（scopeType=0时，attendanceUserService.getUserIds会返回所有人员）
        List<AttendanceGroupUser> userAndGroupIds = attendanceUserService.getUserIdsAndGroupIds(orgIds, directUserIds, start, end, scopeType);
        if (CollUtil.isEmpty(userAndGroupIds)) {
            return new MutablePair<>(List.of(), List.of());
        }
        List<String> userIds = userAndGroupIds.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList());
        // 查询用户基础信息
        List<PartUserInfoVo> userInfoList = userAntifreeze.getStaffRosterListInfoByIds(userIds);
        if (CollUtil.isEmpty(userInfoList)) {
            return new MutablePair<>(List.of(), List.of());
        }
        Map<String, List<AttendanceGroupUser>> userGroupUsersMap = userAndGroupIds.stream()
                .collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));

        // 组装返回数据
        List<BookScopeUserVo> result = new ArrayList<>();
        for (PartUserInfoVo userInfo : userInfoList) {
            List<AttendanceGroupUser> userGroupUsers = userGroupUsersMap.getOrDefault(userInfo.getUserId(), Collections.emptyList());
            AttendanceGroupUser attendanceGroupUser = userGroupUsers.stream().reduce((groupUser1, groupUser2) -> Objects.equals(groupUser1.getDeleteMark(), 0) ? groupUser1 : Objects.equals(groupUser2.getDeleteMark(), 0) ? groupUser2 : groupUser1.getCreatorTime().after(groupUser2.getCreatorTime()) ? groupUser1 : groupUser2).orElse(null);
            int secondmentType = SecondmentTypeUtil.toSecondmentType(AttendanceGroupUserStatusUtil.isExistStatus(userGroupUsers, start, end));
            BookScopeUserVo vo = BookScopeUserVo.builder()
                    .userId(userInfo.getUserId())
                    .userName(userInfo.getRealName())
                    .userPost(userInfo.getPositionName())
                    .organization(userInfo.getOrganizeName())
                    .mobilePhone(userInfo.getMobilePhone())
                    .secondmentType(secondmentType)
                    .isOutGroup(Objects.isNull(attendanceGroupUser) ? 1 : attendanceGroupUser.getDeleteMark())
                    .build();
            result.add(vo);
        }
        return new MutablePair<>(result, userAndGroupIds);
    }

    @Override
    public List<BookRecordMonthListVo> getMonthList(BookRecordMonthListDto req) {
        log.info("获取月考勤本列表，bookId: {}, month: {}", req.getBookId(), req.getMonth());

        // 1. 查询考勤本配置
        AttendanceBookConfigEntity config = attendanceBookConfigService.getById(req.getBookId());
        if (config == null) {
            log.warn("考勤本配置不存在，bookId: {}", req.getBookId());
            return CollUtil.newArrayList();
        }

        // 2. 解析月份，获取该月的起止日期
        YearMonth yearMonth = YearMonth.parse(req.getMonth(), DateTimeFormatter.ofPattern("yyyy-MM"));
        Date startDate = Date.from(yearMonth.atDay(1).atStartOfDay().atZone(java.time.ZoneId.systemDefault()).toInstant());
        Date endDate = Date.from(yearMonth.atEndOfMonth().atTime(23, 59, 59).atZone(java.time.ZoneId.systemDefault()).toInstant());
        MutablePair<List<BookScopeUserVo>, List<AttendanceGroupUser>> userAndGroupIds = getUserIdsFromScope(req.getBookId(), config.getScopeType(), startDate, endDate);
        // 3. 从考勤本使用范围中获取人员列表
        List<BookScopeUserVo> userInfoList = userAndGroupIds.getLeft();
        if (CollUtil.isEmpty(userInfoList)) {
            return CollUtil.newArrayList();
        }
        // 4. 构建用户ID列表
        List<String> userIds = userInfoList.stream()
                .map(BookScopeUserVo::getUserId)
                .distinct()
                .collect(Collectors.toList());
        if (CollUtil.isEmpty(userAndGroupIds.getRight())) {
            return CollUtil.newArrayList();
        }
        // 查询用户考勤组成员信息（用于判断每日离组状态）
        Map<String, List<AttendanceGroupUser>> groupUserMap = userAndGroupIds.getRight().stream()
                .collect(Collectors.groupingBy(AttendanceGroupUser::getUserId));

        // 5. 查询该考勤本该月的所有考勤记录
        LambdaQueryWrapper<AttendanceBookRecordEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AttendanceBookRecordEntity::getBookId, req.getBookId())
                .in(AttendanceBookRecordEntity::getUserId, userIds)
                .between(AttendanceBookRecordEntity::getDay, startDate, endDate)
                .orderByAsc(AttendanceBookRecordEntity::getUserId)
                .orderByAsc(AttendanceBookRecordEntity::getDay)
                .orderByAsc(AttendanceBookRecordEntity::getPeriodType);

        List<AttendanceBookRecordEntity> records = this.list(queryWrapper);

        // 6. 按员工分组
        Map<String, List<AttendanceBookRecordEntity>> recordsByUser = records.stream()
                .collect(Collectors.groupingBy(AttendanceBookRecordEntity::getUserId));

        // 7. 批量查询请假类型，构建ID到名称的映射（配置 + 记录中出现的 ID）
        Map<String, AttendanceLeaveType> stringStringMap = getAttendanceLeaveTypeMap(config, records);
        // 批量查询用户锁定状态
        Map<String, Boolean> sealMap = attendanceDayStatisticsService.selectUserIsSeal(userIds, req.getMonth());

        // 8. 生成该月所有日期列表
        List<LocalDate> allDaysInMonth = new ArrayList<>();
        LocalDate currentDate = yearMonth.atDay(1);
        LocalDate lastDay = yearMonth.atEndOfMonth();
        while (!currentDate.isAfter(lastDay)) {
            allDaysInMonth.add(currentDate);
            currentDate = currentDate.plusDays(1);
        }

        // 8. 构建返回结果
        List<BookRecordMonthListVo> result = new ArrayList<>();
        int sortOrder = 1;

        for (BookScopeUserVo userInfo : userInfoList) {
            String userId = userInfo.getUserId();
            List<AttendanceBookRecordEntity> userRecords = recordsByUser.getOrDefault(userId, CollUtil.newArrayList());

            // 获取用户的考勤组成员数据（用于每日离组判断）
            List<AttendanceGroupUser> userGroupUsers = groupUserMap.getOrDefault(userId, CollUtil.newArrayList());

            // 按日期分组
            Map<String, List<AttendanceBookRecordEntity>> recordsByDay = userRecords.stream()
                    .collect(Collectors.groupingBy(record -> DateUtil.format(record.getDay(), "yyyy-MM-dd")));

            // 构建每日记录列表
            List<BookRecordMonthDayVo> dayList = new ArrayList<>();

            for (LocalDate day : allDaysInMonth) {
                String dayStr = day.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
                String weekDay = getWeekDay(day.getDayOfWeek().getValue());

                // 计算用户当日是否离组
                Date dayDate = Date.from(day.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
                Integer existStatus = AttendanceGroupUserStatusUtil.isExistStatus(userGroupUsers, dayDate);
                if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) {
                    existStatus = 1;
                }
                // 获取该日的考勤记录
                List<AttendanceBookRecordEntity> dayRecords = recordsByDay.getOrDefault(dayStr, CollUtil.newArrayList());

                // 上班卡 / 下班卡各取一条：同日同时段若有多条（脏数据），取最后修改时间最新的一条，避免状态码随机错乱（如 2007/2008 串台）
                AttendanceBookRecordEntity morningRecord = findRecordByPeriod(dayRecords, PeriodTypeEnum.MORNING.getCode());
                AttendanceBookRecordEntity eveningRecord = findRecordByPeriod(dayRecords, PeriodTypeEnum.AFTERNOON.getCode());

                // 构建日记录VO
                BookRecordMonthDayVo dayVo = BookRecordMonthDayVo.builder()
                        .day(dayStr)
                        .weekDay(weekDay)
                        .morningRecordId(morningRecord != null ? morningRecord.getId() : null)
                        .morningStatus(resolveStatus(morningRecord))
                        .morningStatusName(getStatusNameWithLeaveType(morningRecord, stringStringMap))
                        .eveningRecordId(eveningRecord != null ? eveningRecord.getId() : null)
                        .eveningStatus(resolveStatus(eveningRecord))
                        .eveningStatusName(getStatusNameWithLeaveType(eveningRecord, stringStringMap))
                        .isOutGroup(existStatus)
                        .build();
                dayList.add(dayVo);
            }
            int secondmentType = SecondmentTypeUtil.resolveSecondmentType(userGroupUsers, startDate, endDate);
            // 构建员工分组VO
            BookRecordMonthListVo monthListVo = BookRecordMonthListVo.builder()
                    .userId(userId)
                    .realName(userInfo.getUserName())
                    .positionName(userInfo.getUserPost())
                    .organizeName(userInfo.getOrganization())
                    .mobilePhone(userInfo.getMobilePhone())
                    .secondmentType(secondmentType)
                    .isOutGroup(userInfo.getIsOutGroup())
                    .isSeal(sealMap.getOrDefault(userId, false))
                    .sortOrder(sortOrder++)
                    .dayList(dayList)
                    .build();

            result.add(monthListVo);
        }

        log.info("获取月考勤本列表成功，bookId: {}, month: {}, 员工数: {}",
                req.getBookId(), req.getMonth(), result.size());
        return result;
    }

    /**
     * 获取星期名称
     *
     * @param dayOfWeek 星期几（1-7，1表示周一）
     * @return 星期名称
     */
    private String getWeekDay(int dayOfWeek) {
        switch (dayOfWeek) {
            case 1:
                return "周一";
            case 2:
                return "周二";
            case 3:
                return "周三";
            case 4:
                return "周四";
            case 5:
                return "周五";
            case 6:
                return "周六";
            case 7:
                return "周日";
            default:
                return "";
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public BatchOperationResultVo batchSaveOrUpdateRecord(BatchAttendanceBookRecordDto dto) {
        log.info("批量新增或修改考勤记录，记录数: {}", dto.getRecords().size());

        int successCount = 0;
        int failCount = 0;
        List<String> successIds = new ArrayList<>();
        List<BatchOperationResultVo.FailDetailVo> failDetails = new ArrayList<>();

        // 优化：批量查询已存在的记录，避免循环中逐条查询
        List<AttendanceBookRecordDto> records = dto.getRecords();
        AttendanceBookRecordDto attendanceBookRecordDto = records.get(0);
        String bookId = attendanceBookRecordDto.getBookId();
        Map<String, AttendanceBookRecordEntity> existingRecordMap = batchQueryExistingRecords(records, bookId);
        Map<String, List<AttendanceGroupUser>> userGroupUsersMap = buildUserGroupUsersMapForRecords(bookId, records);
        Map<String, Boolean> userMonthSealMap = buildUserMonthSealMap(records);

        // 操作日志需同时识别每条 DTO 与对应「修改前」记录上的 leaveType，避免旧假别名缺失后被展示成考勤状态（如正常）
        LinkedHashSet<String> allLeaveTypeIds = new LinkedHashSet<>();
        for (AttendanceBookRecordDto r : records) {
            if (StringUtils.isNotBlank(r.getLeaveType())) {
                allLeaveTypeIds.add(r.getLeaveType().trim());
            }
            String uniqueKey = buildUniqueKey(r.getBookId(), r.getUserId(), r.getDay(), r.getPeriodType());
            AttendanceBookRecordEntity existing = existingRecordMap.get(uniqueKey);
            if (existing != null && StringUtils.isNotBlank(existing.getLeaveType())) {
                allLeaveTypeIds.add(existing.getLeaveType().trim());
            }
        }
        Map<String, String> leaveTypeIdToNameMap = buildLeaveTypeIdToNameMap(allLeaveTypeIds);

        // 获取当前用户信息
        UserInfo user = UserProvider.getUser();
        Date now = new Date();

        // 分类：需要新增和需要更新的记录
        List<AttendanceBookRecordEntity> toInsert = new ArrayList<>();
        List<AttendanceBookRecordEntity> toUpdate = new ArrayList<>();
        List<AttendanceBookRecordEntity> toClear = new ArrayList<>();
        // 批量收集操作日志，最后统一插入
        List<AttendanceBookOperationLogEntity> operationLogs = new ArrayList<>();

        for (int i = 0; i < records.size(); i++) {
            AttendanceBookRecordDto recordDto = records.get(i);
            try {
                // 构建唯一键：bookId+userId+day+periodType
                String uniqueKey = buildUniqueKey(recordDto.getBookId(), recordDto.getUserId(),
                        recordDto.getDay(), recordDto.getPeriodType());

                AttendanceBookRecordEntity existingRecord = existingRecordMap.get(uniqueKey);

                // 用户当日不在组：跳过，不做新增/修改/清除
                List<AttendanceGroupUser> userGroupUsers = userGroupUsersMap.getOrDefault(
                        recordDto.getUserId(), Collections.emptyList());
                if (!AttendanceGroupUserStatusUtil.isUserInGroupOnDay(userGroupUsers, recordDto.getDay())) {
                    log.info("批量保存：用户当日不在组，已跳过，userId: {}, day: {}, periodType: {}",
                            recordDto.getUserId(), recordDto.getDay(), recordDto.getPeriodType());
                    continue;
                }

                String recordMonth = resolveRecordMonth(recordDto);
                if (isUserSealedInCache(recordDto.getUserId(), recordMonth, userMonthSealMap)) {
                    log.info("批量保存：用户当月已封存，已跳过，userId: {}, month: {}, periodType: {}",
                            recordDto.getUserId(), recordMonth, recordDto.getPeriodType());
                    continue;
                }

                boolean clearRequest = isClearRequest(recordDto);
                if (clearRequest) {
                    if (existingRecord == null) {
                        log.info("批量清除：无对应记录，跳过，userId: {}, day: {}, periodType: {}",
                                recordDto.getUserId(), recordDto.getDay(), recordDto.getPeriodType());
                        if (StringUtils.isNotBlank(recordDto.getId())) {
                            successIds.add(recordDto.getId());
                            successCount++;
                        }
                        continue;
                    }
                    toClear.add(existingRecord);
                    operationLogs.add(buildOperationLogEntity(recordDto, existingRecord,
                            AttendanceConstant.LOG_OPERATION_CLEAN, leaveTypeIdToNameMap));
                    successIds.add(existingRecord.getId());
                    successCount++;
                    continue;
                }
                if (existingRecord != null) {
                    // 更新现有记录
                    existingRecord.setAttendanceStatus(recordDto.getAttendanceStatus());
                    existingRecord.setLeaveType(recordDto.getLeaveType());
                    existingRecord.setRemark(recordDto.getRemark());
                    existingRecord.setLastModifyUserId(user != null ? user.getUserId() : null);
                    existingRecord.setLastModifyTime(now);
                    toUpdate.add(existingRecord);

                    successIds.add(existingRecord.getId());
                    operationLogs.add(buildOperationLogEntity(recordDto, existingRecord,
                            AttendanceConstant.LOG_OPERATION_UPDATE, leaveTypeIdToNameMap));
                    successCount++;
                    continue;
                }
                // 新增记录
                AttendanceBookRecordEntity newEntity = buildEntityFromDto(recordDto);
                newEntity.setId(RandomUtil.uuId());
                newEntity.setCreatorUserId(user != null ? user.getUserId() : null);
                newEntity.setCreatorTime(now);
                newEntity.setTenantId(user != null ? user.getTenantId() : null);
                // 自动填充月份字段
                if (StringUtils.isBlank(newEntity.getMonth()) && newEntity.getDay() != null) {
                    newEntity.setMonth(newEntity.getDay().toInstant()
                            .atZone(ZoneId.systemDefault())
                            .toLocalDate()
                            .format(DateTimeFormatter.ofPattern("yyyy-MM")));
                }

                toInsert.add(newEntity);
                // 收集成功ID
                successIds.add(newEntity.getId());
                // 收集操作日志，不立即插入数据库
                operationLogs.add(buildOperationLogEntity(recordDto, null, AttendanceConstant.LOG_OPERATION_ADD, leaveTypeIdToNameMap));
                successCount++;
            } catch (Exception e) {
                failCount++;
                failDetails.add(BatchOperationResultVo.FailDetailVo.builder()
                        .row(i + 1)
                        .message(e.getMessage())
                        .build());
                log.error("批量操作第{}条记录失败", i + 1, e);
            }
        }

        // 批量插入和更新
        if (!toInsert.isEmpty()) {
            this.saveBatch(toInsert);
            log.info("批量新增考勤记录成功，数量: {}", toInsert.size());
        }

        if (!toUpdate.isEmpty()) {
            this.updateBatchById(toUpdate);
            log.info("批量更新考勤记录成功，数量: {}", toUpdate.size());
        }
        if (!toClear.isEmpty()) {
            this.removeBatchByIds(toClear);
            log.info("批量清除考勤记录成功，数量: {}", toClear.size());
        }
        // 批量插入操作日志
        if (!operationLogs.isEmpty()) {
            attendanceBookOperationLogService.saveBatch(operationLogs);
            log.info("批量插入操作日志成功，数量: {}", operationLogs.size());
        }

        BatchOperationResultVo result = BatchOperationResultVo.builder()
                .successCount(successCount)
                .successIds(successIds)
                .failCount(failCount)
                .failDetails(failDetails)
                .build();

        log.info("批量新增或修改考勤记录完成，成功: {}, 失败: {}", successCount, failCount);
        return result;
    }


    /**
     * 构建操作日志实体（不立即插入数据库）
     */
    private AttendanceBookOperationLogEntity buildOperationLogEntity(AttendanceBookRecordDto recordDto,
                                                                     AttendanceBookRecordEntity existingRecord,
                                                                     String operationType,
                                                                     Map<String, String> leaveTypeIdToNameMap) {
        try {
            if (recordDto == null && existingRecord == null) {
                log.warn("考勤记录DTO与已有记录均为空，跳过构建操作日志");
                return null;
            }

            // 构建操作内容描述（清除时 DTO 可能为空，以库中记录为准）
            AttendanceBookRecordEntity tempEntity = recordDto != null ? buildEntityFromDto(recordDto) : existingRecord;
            String operationContent = buildOperationContent(tempEntity, existingRecord, operationType, leaveTypeIdToNameMap);

            // 获取当前用户信息
            UserInfo user = UserProvider.getUser();

            // 创建操作日志实体
            String bookId = recordDto != null ? recordDto.getBookId() : existingRecord.getBookId();
            Date day = recordDto != null ? recordDto.getDay() : existingRecord.getDay();
            String recordId = existingRecord != null ? existingRecord.getId()
                    : (recordDto != null ? recordDto.getId() : null);
            String userId = recordDto != null ? recordDto.getUserId() : existingRecord.getUserId();

            return AttendanceBookOperationLogEntity.builder()
                    .bookId(bookId)
                    .month(day != null ? day.toInstant()
                                         .atZone(ZoneId.systemDefault())
                                         .toLocalDate()
                                         .format(DateTimeFormatter.ofPattern("yyyy-MM")) : null)
                    .recordId(recordId)
                    .employeeId(userId)
                    .operationContent(operationContent)
                    .operatorId(user != null ? user.getUserId() : null)
                    .operatorName(user != null ? user.getUserName() : null)
                    .operationTime(new Date())
                    .build();
        } catch (Exception e) {
            // 构建操作日志失败不影响主业务，仅记录警告日志
            log.warn("构建操作日志失败，记录ID: {}, 员工ID: {}, 原因: {}",
                    recordDto.getId(), recordDto.getUserId(), e.getMessage(), e);
            return null;
        }
    }


    /**
     * 批量查询已存在的记录
     *
     * @param records 考勤记录DTO列表
     * @return 唯一键 -> 实体的映射
     */
    private Map<String, AttendanceBookRecordEntity> batchQueryExistingRecords(List<AttendanceBookRecordDto> records, String bookId) {
        if (CollUtil.isEmpty(records)) {
            return Collections.emptyMap();
        }

        Map<String, AttendanceBookRecordEntity> resultMap = new HashMap<>();
        // 收集该bookId下的所有条件
        Set<String> userIds = new HashSet<>();
        Set<Date> days = new HashSet<>();
        Set<Integer> periodTypes = new HashSet<>();

        for (AttendanceBookRecordDto record : records) {
            userIds.add(record.getUserId());
            days.add(record.getDay());
            periodTypes.add(record.getPeriodType());
        }

        // 批量查询
        LambdaQueryWrapper<AttendanceBookRecordEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AttendanceBookRecordEntity::getBookId, bookId)
                .in(AttendanceBookRecordEntity::getUserId, userIds)
                .in(AttendanceBookRecordEntity::getDay, days)
                .in(AttendanceBookRecordEntity::getPeriodType, periodTypes);

        List<AttendanceBookRecordEntity> existingRecords = this.list(queryWrapper);

        // 构建映射
        for (AttendanceBookRecordEntity record : existingRecords) {
            String uniqueKey = buildUniqueKey(record.getBookId(), record.getUserId(),
                    record.getDay(), record.getPeriodType());
            resultMap.put(uniqueKey, record);
        }
        return resultMap;
    }

    /**
     * 构建唯一键
     */
    private String buildUniqueKey(String bookId, String userId, Date day, Integer periodType) {
        return String.format("%s_%s_%s_%s", bookId, userId,
                day != null ? day.getTime() : "null", periodType);
    }

    /**
     * 从DTO构建Entity
     */
    private AttendanceBookRecordEntity buildEntityFromDto(AttendanceBookRecordDto dto) {
        AttendanceBookRecordEntity entity = new AttendanceBookRecordEntity();
        entity.setId(dto.getId());
        entity.setBookId(dto.getBookId());
        entity.setUserId(dto.getUserId());
        entity.setDay(dto.getDay());
        entity.setPeriodType(dto.getPeriodType());
        entity.setAttendanceStatus(dto.getAttendanceStatus());
        entity.setLeaveType(dto.getLeaveType());
        entity.setRemark(dto.getRemark());
        return entity;
    }

    /**
     * 是否为清除请求：考勤状态与请假类型均为 null
     */
    private boolean isClearRequest(AttendanceBookRecordDto dto) {
        return dto != null && dto.getAttendanceStatus() == null && dto.getLeaveType() == null;
    }

    /**
     * 解析已存在的考勤记录：优先按 id，再按 bookId+userId+day+periodType
     */
    private AttendanceBookRecordEntity resolveExistingRecord(AttendanceBookRecordDto dto) {
        if (dto == null) {
            return null;
        }
        if (StringUtils.isNotBlank(dto.getId())) {
            AttendanceBookRecordEntity byId = this.getById(dto.getId());
            if (byId != null) {
                return byId;
            }
        }
        if (StringUtils.isBlank(dto.getBookId()) || StringUtils.isBlank(dto.getUserId())
                || dto.getDay() == null || dto.getPeriodType() == null) {
            return null;
        }
        Date dayStart = DateUtil.beginOfDay(dto.getDay());
        Date dayEnd = DateUtil.endOfDay(dto.getDay());
        return this.getOne(new LambdaQueryWrapper<AttendanceBookRecordEntity>()
                .eq(AttendanceBookRecordEntity::getBookId, dto.getBookId())
                .eq(AttendanceBookRecordEntity::getUserId, dto.getUserId())
                .eq(AttendanceBookRecordEntity::getPeriodType, dto.getPeriodType())
                .between(AttendanceBookRecordEntity::getDay, dayStart, dayEnd)
                .last("LIMIT 1"));
    }

    @Override
    public LeaveRemarkSummaryVo getLeaveAttendanceSummary(LeaveRemarkSummaryDto dto) {
        log.info("获取假勤汇总信息，bookId: {}, month: {}, userId: {}",
                dto.getBookId(), dto.getMonth(), dto.getUserId());

        // 1. 参数校验
        Assert.notBlank(dto.getBookId(), "考勤本ID不能为空");
        Assert.notBlank(dto.getMonth(), "月份不能为空");

        // 2. 月份范围：与导出等接口一致使用 DateDetail，避免与 F_Month / 服务器时区组合时出现边界漏查
        Date startDate = DateDetail.getMonthBeginDate(dto.getMonth());
        Date endDate = DateDetail.getMonthEndDate(dto.getMonth());

        // 3. 查询考勤记录：优先按 F_Month 命中（与导入/单条保存写入的月份一致）；兼容历史数据 F_Month 为空时按 F_Day 区间回退
        LambdaQueryWrapper<AttendanceBookRecordEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AttendanceBookRecordEntity::getBookId, dto.getBookId())
                .and(w -> w.eq(AttendanceBookRecordEntity::getMonth, dto.getMonth())
                        .or(w2 -> w2.isNull(AttendanceBookRecordEntity::getMonth)
                                .between(AttendanceBookRecordEntity::getDay, startDate, endDate)));

        // 如果传入了用户ID，则只查询该用户
        if (StringUtils.isNotBlank(dto.getUserId())) {
            queryWrapper.eq(AttendanceBookRecordEntity::getUserId, dto.getUserId());
        }

        queryWrapper.orderByAsc(AttendanceBookRecordEntity::getDay)
                .orderByAsc(AttendanceBookRecordEntity::getPeriodType);

        List<AttendanceBookRecordEntity> records = this.list(queryWrapper);
        if (records == null) {
            records = Collections.emptyList();
        }

        AttendanceBookConfigEntity bookConfig = attendanceBookConfigService.getById(dto.getBookId());
        // 与月度统计一致：按考勤本假期范围取全量类型映射，并合并记录中实际出现过的类型，保证假勤汇总里「请假天数统计」列出全部假期类型（无数据为 0）
        // 5. 查询所有请假类型，构建ID到名称的映射（使用统一方法，自动过滤已删除的）
        Map<String, AttendanceLeaveType> leaveTypeIdToNameMap = getAttendanceLeaveTypeMap(bookConfig, records);

        MonthlyAttendanceStats stats = new MonthlyAttendanceStats();
        List<LeaveRemarkItemVo> leaveRemarks = new ArrayList<>();

        for (AttendanceBookRecordEntity record : records) {
            stats.accumulate(record);

            // 收集请假备注信息（只要有leaveType或remark就收集）
            if (StringUtils.isNotBlank(record.getLeaveType()) || StringUtils.isNotBlank(record.getRemark())) {

                // 格式化日期：yyyy-MM-dd（星期X）【时段】
                String formattedDate = formatDateWithWeekday(record.getDay());
                String periodName = getPeriodName(record.getPeriodType());
                String dateInfo = formattedDate + "【" + periodName + "】";

                // 请假类型（转换为名称）
                AttendanceLeaveType leaveTypeName = StringUtils.isNotBlank(record.getLeaveType())
                        ? leaveTypeIdToNameMap.getOrDefault(record.getLeaveType(), null)
                        : null;

                // 备注内容
                String remarkContent = StringUtils.isNotBlank(record.getRemark())
                        ? record.getRemark()
                        : "";

                // 构建请假备注信息：[请假类型]备注信息
                StringBuilder remarkBuilder = new StringBuilder();
                if (Objects.nonNull(leaveTypeName) && StringUtils.isNotBlank(leaveTypeName.getName())) {
                    remarkBuilder.append("【")
                            .append(leaveTypeName.getName())
                            .append("】");
                }
                if (StringUtils.isNotBlank(remarkContent)) {
                    remarkBuilder.append(remarkContent);
                }

                String remark = remarkBuilder.toString();

                // 构建请假备注项
                LeaveRemarkItemVo remarkItem = LeaveRemarkItemVo.builder()
                        .remark(remark)
                        .dateInfo(dateInfo)
                        .recordId(record.getId())
                        .build();

                leaveRemarks.add(remarkItem);
            }
        }

        LeaveRemarkSummaryVo result = LeaveRemarkSummaryVo.builder()
                .leaveRemarks(leaveRemarks)
                .totalCount(leaveRemarks.size())
                .actualAttendDays(stats.actualAttendDays)
                .publicHolidayDays(stats.publicHolidayDays)
                .lateDays(stats.lateDays)
                .earlyLeaveDays(stats.earlyLeaveDays)
                .missingCardDays(stats.missingCardDays)
                .absenteeismDays(stats.absenteeismDays)
                .businessTripDays(stats.businessTripDays)
                .outOfficeDays(stats.outOfficeDays)
                .fieldWorkDays(stats.fieldWorkDays)
                .actualAttendCount(stats.actualAttendCount)
                .publicHolidayCount(stats.publicHolidayCount)
                .lateCount(stats.lateCount)
                .earlyLeaveCount(stats.earlyLeaveCount)
                .missingCardCount(stats.missingCardCount)
                .absenteeismCount(stats.absenteeismCount)
                .businessTripCount(stats.businessTripCount)
                .outOfficeCount(stats.outOfficeCount)
                .fieldWorkCount(stats.fieldWorkCount)
                .leaveDaysByType(stats.toLeaveTypeStatisticList(leaveTypeIdToNameMap))
                .build();

        log.info("获取假勤汇总信息成功，总数: {}", result.getTotalCount());
        return result;
    }
}
