# HkEfficiencyServiceImpl.java

    package cloud.wconnect.workperformance.service.hkefficiency.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.CommonFieldI18nConstant;
    import cloud.wconnect.commonlib.cons.MoudleEventSubjectEnum;
    import cloud.wconnect.commonlib.exception.ListenerException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationDateTimeDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.sns.http.processor.Sender;
    import cloud.wconnect.workperformance.cons.PrecisionCons;
    import cloud.wconnect.workperformance.cons.hkefficiency.HkEfficiencyConstant;
    import cloud.wconnect.workperformance.cons.operatingstatisticsdaily.DataUpdateResult;
    import cloud.wconnect.workperformance.cons.operatingstatisticsdaily.OperatingStatisticsDatasourceEnum;
    import cloud.wconnect.workperformance.cons.sva.SvaSchedulePayCodeEnum;
    import cloud.wconnect.workperformance.entity.hkefficiency.HkEfficiency;
    import cloud.wconnect.workperformance.entity.hkefficiency.HkEfficiencyDetail;
    import cloud.wconnect.workperformance.entity.hkefficiency.HkEfficiencyReportFilterConfig;
    import cloud.wconnect.workperformance.entity.hkefficiency.HkEfficiencySum;
    import cloud.wconnect.workperformance.mapper.hkefficiency.HkEfficiencyMapper;
    import cloud.wconnect.workperformance.mapper.hkefficiency.HkEfficiencySumMapper;
    import cloud.wconnect.workperformance.pojo.bo.hkefficiency.HkEfficiencyListBO;
    import cloud.wconnect.workperformance.pojo.bo.hkefficiency.HkOperatingStatisticsListBO;
    import cloud.wconnect.workperformance.pojo.bo.modelevent.hkefficiency.HKEfficiencyBO;
    import cloud.wconnect.workperformance.pojo.dto.hkefficiency.*;
    import cloud.wconnect.workperformance.pojo.query.hkefficiency.HkEfficiencyListQuery;
    import cloud.wconnect.workperformance.pojo.vo.hkefficiency.HkEfficiencyExportVO;
    import cloud.wconnect.workperformance.service.hkefficiency.IHkEfficiencyDetailService;
    import cloud.wconnect.workperformance.service.hkefficiency.IHkEfficiencyReportFilterConfigService;
    import cloud.wconnect.workperformance.service.hkefficiency.IHkEfficiencyService;
    import cloud.wconnect.workperformance.service.hkefficiency.IHkEfficiencySumService;
    import cloud.wconnect.workperformance.service.hkefficiency.hkefficiency.IHkEfficiencyParseService;
    import cloud.wconnect.workperformance.service.hkefficiency.operatingstatistics.IHkOperatingStatisticsParseService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Maps;
    import jakarta.annotation.Resource;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.collections.MapUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.i18n.LocaleContextHolder;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.io.InputStream;
    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.text.MessageFormat;
    import java.time.LocalDate;
    import java.time.ZoneId;
    import java.time.ZonedDateTime;
    import java.time.format.DateTimeFormatter;
    import java.time.temporal.TemporalAdjusters;
    import java.util.*;
    import java.util.concurrent.Executor;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * 服务实现类：Housekeeping Efficiency
     * </p>
     *
     * @author zc
     * @since 2023-03-29
     */
    @Slf4j
    @Service
    @DS("work_performance")
    public class HkEfficiencyServiceImpl extends ServiceImpl<HkEfficiencyMapper, HkEfficiency> implements IHkEfficiencyService {
        @Resource
        private PropertyI18nRedisService propertyI18nRedisService;
        @Resource
        private IHkEfficiencyDetailService hkEfficiencyDetailService;
        @Resource
        private IHkEfficiencySumService hkEfficiencySumService;
        @Resource
        private IHkEfficiencyReportFilterConfigService reportFilterConfigService;
        @Resource
        private HkEfficiencySumMapper hkEfficiencySumMapper;
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        private Executor executor;
        @Resource
        private ApplicationContext applicationContext;
        @Resource
        private I18nMessageUtils i18nMessageUtils;
        @Resource
        private RedissonClient redissonClient;
        @Resource
        @Qualifier("moduleEventSender")
        private Sender moduleEventSender;

        /**
         * 获取指定月份 HkEfficiency 数据集合
         *
         * @param propertyId    property id
         * @param startOfMonths 月初时间戳
         * @return HkEfficiency 数据集合
         * @author zc
         * @date 2023/3/29
         */
        @Override
        public List<HkEfficiency> listByStartOfMonths(int propertyId, List<Long> startOfMonths) {
            LambdaQueryWrapper<HkEfficiency> queryWrapper = new QueryWrapper<HkEfficiency>().lambda()
                    .eq(HkEfficiency::getPropertyId, propertyId)
                    .in(HkEfficiency::getStartOfMonth, startOfMonths);
            return list(queryWrapper);
        }

        /**
         * 获取指定时间区间的 Housekeeping Efficiency 数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return Housekeeping Efficiency 数据
         * @author zc
         * @date 2023/3/29
         */
        @Override
        public ListPagedDTO<HkEfficiencyDTO> list(PropertyDTO propertyDTO, HkEfficiencyListQuery query) {
            ZonedDateTime startDate = DateTimeUtils.strToLocalDate(String.valueOf(query.getStartDate()),
                    BaseConstant.DATE_FORMAT_YYYYMMDD).atStartOfDay(ZoneId.of(propertyDTO.getTimezone()));
            ZonedDateTime endDate = DateTimeUtils.strToLocalDate(String.valueOf(query.getEndDate()),
                    BaseConstant.DATE_FORMAT_YYYYMMDD).atStartOfDay(ZoneId.of(propertyDTO.getTimezone()));
            long total = DateTimeUtils.listIntervalDates(startDate, endDate).size();
            if (Objects.nonNull(query.getPage()) && Objects.nonNull(query.getSize())) {
                if (endDate.compareTo(startDate) < 0) {
                    return new ListPagedDTO<>();
                }
                startDate = startDate.plusDays((long) (query.getPage() - 1) * query.getSize());
                int numberOfDaysApart = DateTimeUtils.listIntervalDates(startDate, endDate).size();
                endDate = (numberOfDaysApart < query.getSize()) ?
                        startDate.plusDays(numberOfDaysApart - 1) : startDate.plusDays((long) query.getSize() - 1);
            }

            // 获取时间区间数据
            List<HkEfficiency> hkEfficiencyList = list(new QueryWrapper<HkEfficiency>().lambda()
                    .eq(HkEfficiency::getPropertyId, propertyDTO.getId())
                    .between(HkEfficiency::getTheDate, startDate.toEpochSecond(), endDate.toEpochSecond()));

            Map<Long, HkEfficiency> theDate2housekeepingEfficiency = hkEfficiencyList.stream()
                    .collect(Collectors.toMap(HkEfficiency::getTheDate, v -> v));

            // 获取时间区间详情数据
            Map<Long, List<HkEfficiencyDetail>> theDate2HoursWorkedDetails =
                    hkEfficiencyDetailService.listByTheDateRange(propertyDTO.getId(),
                                    startDate.toEpochSecond(), endDate.toEpochSecond()).stream()
                            .filter(item -> StringUtils.isBlank(item.getEarningsCode()))
                            .collect(Collectors.groupingBy(HkEfficiencyDetail::getTheDate));

            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            List<HkEfficiencyDTO> resultList = Lists.newArrayList();
            for (ZonedDateTime date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
                long dateTime = date.toEpochSecond();
                HkEfficiency hkEfficiency = theDate2housekeepingEfficiency.getOrDefault(dateTime, new HkEfficiency());

                HkEfficiencyDTO hkEfficiencyDTO = beanMapper.map(hkEfficiency, HkEfficiencyDTO.class);
                hkEfficiencyDTO.setTheDate(dateTime);
                hkEfficiencyDTO.setHoursWorkedDetails(beanMapper.mapList(theDate2HoursWorkedDetails.getOrDefault(dateTime,
                        Collections.emptyList()), HkEfficiencyDetailDTO.class));

                // 9411 Hours-Housekeeping = Hours worked + Retro
                BigDecimal hoursHousekeeping = null;
                if (Objects.nonNull(hkEfficiency.getHoursWorked())) {
                    hoursHousekeeping = hkEfficiency.getHoursWorked();
                }
                if (Objects.nonNull(hkEfficiency.getRetro())) {
                    hoursHousekeeping = Objects.isNull(hoursHousekeeping) ? hkEfficiency.getRetro() :
                            hoursHousekeeping.add(hkEfficiency.getRetro());
                }
                hkEfficiencyDTO.setHoursHousekeeping(hoursHousekeeping);

                // Total Housekeeping Hours = 9411 Hours-Housekeeping + Temp Hours + Adjust Hours
                BigDecimal totalHousekeepingHours = hoursHousekeeping;
                if (Objects.nonNull(hkEfficiency.getTempHours())) {
                    totalHousekeepingHours = Objects.isNull(totalHousekeepingHours) ? hkEfficiency.getTempHours() :
                            totalHousekeepingHours.add(hkEfficiency.getTempHours());
                }
                if (Objects.nonNull(hkEfficiency.getAdjustHours())) {
                    totalHousekeepingHours = Objects.isNull(totalHousekeepingHours) ? hkEfficiency.getAdjustHours() :
                            totalHousekeepingHours.add(hkEfficiency.getAdjustHours());
                }
                hkEfficiencyDTO.setTotalHousekeepingHours(totalHousekeepingHours);

                // Housekeeping Efficiency Ratio = Total Housekeeping Hours / rooms rented
                if (Objects.nonNull(totalHousekeepingHours) && Objects.nonNull(hkEfficiency.getRoomsRented())) {
                    BigDecimal ratio = hkEfficiency.getRoomsRented() == 0 ? null :
                            totalHousekeepingHours.multiply(new BigDecimal("60"))
                                    .divide(new BigDecimal(hkEfficiency.getRoomsRented().toString()), BaseConstant.DATA_PRECISION, roundingMode);
                    hkEfficiencyDTO.setRatio(ratio);
                }
                resultList.add(hkEfficiencyDTO);
            }
            return new ListPagedDTO<>(total, resultList);
        }

        /**
         * 保存指定月份 Housekeeping Efficiency 数据
         *
         * @param propertyDTO 资产
         * @param operatorId  用户 id
         * @param list        Housekeeping Efficiency 数据
         * @author zc
         * @date 2023/3/29
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void save(PropertyDTO propertyDTO, int operatorId, List<HkEfficiencyDTO> list) {
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            List<HkEfficiency> saveList = Lists.newArrayList();
            for (HkEfficiencyDTO HkEfficiencyDTO : list) {
                HkEfficiency hkEfficiency = new HkEfficiency();
                hkEfficiency.setPropertyId(propertyDTO.getId());
                hkEfficiency.setStartOfMonth(DateTimeUtils.getFirstDayOfMonth(HkEfficiencyDTO.getTheDate(), propertyDTO.getTimezone()));
                hkEfficiency.setTheDate(HkEfficiencyDTO.getTheDate());
                hkEfficiency.setTempHours(HkEfficiencyDTO.getTempHours());
                hkEfficiency.setAdjustHours(HkEfficiencyDTO.getAdjustHours());
                hkEfficiency.setExplanation(HkEfficiencyDTO.getExplanation());
                hkEfficiency.setUpdatedBy(operatorId);
                hkEfficiency.setUpdatedTime(nowTimestamp);

                saveList.add(hkEfficiency);
            }
            // 保存数据
            if (CollectionUtils.isNotEmpty(saveList)) {
                baseMapper.insertOrUpdateBatch(saveList);
            }

            // 提取影响的月起始日
            List<Long> startOfMonthList = saveList.stream().map(HkEfficiency::getStartOfMonth).distinct().toList();

            // 更新统计信息
            updateSum(propertyDTO, startOfMonthList);

            // 数据发生变化时，发送 sns 事件
            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                Map<Integer, List<Long>> propertyId2DateList = Map.of(propertyDTO.getId(),
                        list.stream().map(HkEfficiencyDTO::getTheDate).toList());

                sendModuleEvent(List.of(propertyDTO), propertyId2DateList);
            });
        }

        /**
         * 更新统计信息
         *
         * @param propertyDTO      资产
         * @param startOfMonthList 月起始日集合
         * @author zc
         * @date 2023/3/29
         */
        private void updateSum(PropertyDTO propertyDTO, List<Long> startOfMonthList) {
            if (CollectionUtils.isEmpty(startOfMonthList)) {
                return;
            }
            // 获取指定时间的数据
            Map<Long, List<HkEfficiency>> startOfMonth2HouseList =
                    list(new QueryWrapper<HkEfficiency>().lambda()
                            .eq(HkEfficiency::getPropertyId, propertyDTO.getId())
                            .in(HkEfficiency::getStartOfMonth, startOfMonthList)).stream()
                            .collect(Collectors.groupingBy(HkEfficiency::getStartOfMonth));

            List<HkEfficiencySum> saveList = Lists.newArrayListWithExpectedSize(startOfMonthList.size());
            for (Map.Entry<Long, List<HkEfficiency>> entry : startOfMonth2HouseList.entrySet()) {
                Integer totalRoomsRented = null;
                BigDecimal totalHours = null;
                List<HkEfficiency> hkEfficiencyList = entry.getValue();
                for (HkEfficiency hkEfficiency : hkEfficiencyList) {
                    // 如果当天信息完整 入住房间数有值 且 实际工时有值，则当天的数据参与统计计算
                    Map<String, Object> map = checkAndCumulative(hkEfficiency);
                    if (MapUtils.isNotEmpty(map)) {
                        totalRoomsRented = Objects.isNull(totalRoomsRented) ? 0 : totalRoomsRented;
                        totalRoomsRented += Integer.parseInt(map.get(HkEfficiencyConstant.ROOMS_RENTED).toString());

                        totalHours = Objects.isNull(totalHours) ? BigDecimal.ZERO : totalHours;
                        totalHours = totalHours.add(new BigDecimal(map.get(HkEfficiencyConstant.TOTAL_HOURS).toString()));
                    }
                }
                ZonedDateTime zonedDateTime = DateTimeUtils.toZonedDateTime(entry.getKey(), propertyDTO.getTimezone());

                saveList.add(new HkEfficiencySum()
                        .setPropertyId(propertyDTO.getId())
                        .setTheYear(zonedDateTime.getYear())
                        .setStartOfMonth(entry.getKey())
                        .setRoomsRentedTotal(totalRoomsRented)
                        .setHourTotal(totalHours));
            }
            if (CollectionUtils.isNotEmpty(saveList)) {
                hkEfficiencySumMapper.insertOrUpdateBatch(saveList);
            }
        }

        /**
         * 检查指定的 Housekeeping Efficiency 信息中房间入住数、实际工时是否都存在
         * 如果都存在，则返回当天的房间入住数、实际工时
         *
         * @param hkEfficiency Housekeeping Efficiency
         * @author zc
         * @date 2023/3/29
         */
        @Override
        public Map<String, Object> checkAndCumulative(HkEfficiency hkEfficiency) {
            Map<String, Object> map = Maps.newHashMapWithExpectedSize(2);

            boolean hasActualHours = Objects.nonNull(hkEfficiency.getHoursWorked()) ||
                    Objects.nonNull(hkEfficiency.getRetro()) ||
                    Objects.nonNull(hkEfficiency.getTempHours()) ||
                    Objects.nonNull(hkEfficiency.getAdjustHours());

            if (Objects.nonNull(hkEfficiency.getRoomsRented()) && hasActualHours) {
                // 设置 rooms rented
                map.put(HkEfficiencyConstant.ROOMS_RENTED, hkEfficiency.getRoomsRented());

                // 累加 Hours worked
                BigDecimal totalHours = BigDecimal.ZERO;
                if (Objects.nonNull(hkEfficiency.getHoursWorked())) {
                    totalHours = totalHours.add(hkEfficiency.getHoursWorked());
                }

                // 累加 Retro
                if (Objects.nonNull(hkEfficiency.getRetro())) {
                    totalHours = totalHours.add(hkEfficiency.getRetro());
                }

                // 累加 Temp Hours
                if (Objects.nonNull(hkEfficiency.getTempHours())) {
                    totalHours = totalHours.add(hkEfficiency.getTempHours());
                }

                // 累加 Adjust Hours
                if (Objects.nonNull(hkEfficiency.getAdjustHours())) {
                    totalHours = totalHours.add(hkEfficiency.getAdjustHours());
                }

                // 设置 total hours
                map.put(HkEfficiencyConstant.TOTAL_HOURS, totalHours);
            }
            return map;
        }

        /**
         * 获取统计查询数据
         *
         * @param years           所选年范围
         * @param propertyDTOList 资产集合
         * @return 统计查询数据
         * @author zc
         * @date 2023/3/29
         */
        @Override
        public List<HkEfficiencyPropertyDTO> listStatistics(List<Integer> years, List<PropertyDTO> propertyDTOList) {
            if (CollectionUtils.isEmpty(propertyDTOList)) {
                return Collections.emptyList();
            }
            List<Integer> propertyIds = propertyDTOList.stream().map(PropertyDTO::getId).toList();
            Map<Integer, List<HkEfficiencySum>> propertyId2HousekeepingSums = hkEfficiencySumService
                    .list(new QueryWrapper<HkEfficiencySum>().lambda()
                            .in(HkEfficiencySum::getTheYear, years)
                            .in(HkEfficiencySum::getPropertyId, propertyIds)).stream()
                    .collect(Collectors.groupingBy(HkEfficiencySum::getPropertyId));

            Map<Integer, PropertyDTO> propertyId2Property = propertyI18nRedisService.list(propertyIds).stream()
                    .collect(Collectors.toMap(PropertyDTO::getId, Function.identity()));

            return propertyDTOList.stream().map(property -> {
                PropertyDTO propertyDTO = propertyId2Property.get(property.getId());
                // 获取国际化数字配置
                RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                        .getInternationalization().getNumber().getCalculationRule());

                Map<Integer, Map<Integer, HkEfficiencySum>> month2HkEfficiencySumByYear =
                        propertyId2HousekeepingSums.getOrDefault(property.getId(), Collections.emptyList()).stream()
                                .peek(item -> item.setMonth(DateTimeUtils.toZonedDateTime(item.getStartOfMonth(), property.getTimezone()).getMonthValue()))
                                .collect(Collectors.groupingBy(HkEfficiencySum::getTheYear,
                                        Collectors.toMap(HkEfficiencySum::getMonth, Function.identity())));

                List<HkEfficiencyYearDTO> hkEfficiencyYearDTOList = years.stream().map(year -> {
                    Map<Integer, HkEfficiencySum> month2HkEfficiencySum =
                            month2HkEfficiencySumByYear.getOrDefault(year, Collections.emptyMap());
                    List<HkEfficiencyMonthDTO> hkEfficiencyMonthDTOList = Lists.newArrayListWithExpectedSize(12);

                    Integer totalRoomsRentedOfYear = null;
                    BigDecimal totalHoursOfYear = null;

                    // 遍历年的 12 个月
                    for (int i = 1; i <= 12; i++) {
                        // 统计对应资产下的每个月的数据
                        Integer totalRoomsRented = null;
                        BigDecimal totalHours = null;

                        HkEfficiencySum hkEfficiencySum = month2HkEfficiencySum.getOrDefault(i, new HkEfficiencySum());
                        if (Objects.nonNull(hkEfficiencySum.getRoomsRentedTotal()) &&
                                Objects.nonNull(hkEfficiencySum.getHourTotal())) {
                            totalRoomsRented = hkEfficiencySum.getRoomsRentedTotal();
                            totalHours = hkEfficiencySum.getHourTotal();

                            // 累加年的数值
                            if (Objects.isNull(totalRoomsRentedOfYear)) {
                                totalRoomsRentedOfYear = hkEfficiencySum.getRoomsRentedTotal();
                            } else {
                                totalRoomsRentedOfYear += hkEfficiencySum.getRoomsRentedTotal();
                            }
                            if (Objects.isNull(totalHoursOfYear)) {
                                totalHoursOfYear = hkEfficiencySum.getHourTotal();
                            } else {
                                totalHoursOfYear = totalHoursOfYear.add(hkEfficiencySum.getHourTotal());
                            }
                        }

                        HkEfficiencyMonthDTO hkEfficiencyMonthDTO = new HkEfficiencyMonthDTO();
                        hkEfficiencyMonthDTO.setMonth(i);
                        if (Objects.nonNull(totalRoomsRented)) {
                            // Average Hours = total hours / total room rented
                            hkEfficiencyMonthDTO.setAverageHours(totalRoomsRented == 0 ? BigDecimal.ZERO :
                                    totalHours.divide(BigDecimal.valueOf(totalRoomsRented), BaseConstant.DATA_PRECISION, roundingMode));

                            // Average Minutes = total hours * 60 / total room rented
                            hkEfficiencyMonthDTO.setAverageMinutes(totalRoomsRented == 0 ? BigDecimal.ZERO :
                                    totalHours.multiply(new BigDecimal("60")).divide(BigDecimal.valueOf(totalRoomsRented), BaseConstant.DATA_PRECISION, roundingMode));
                        }
                        hkEfficiencyMonthDTOList.add(hkEfficiencyMonthDTO);
                    }

                    HkEfficiencyYearDTO hkEfficiencyYearDTO = new HkEfficiencyYearDTO();
                    hkEfficiencyYearDTO.setYear(year);
                    hkEfficiencyYearDTO.setMonthList(hkEfficiencyMonthDTOList);
                    if (Objects.nonNull(totalRoomsRentedOfYear)) {
                        // Average Hours = total hours / total room rented
                        hkEfficiencyYearDTO.setAverageHours(totalRoomsRentedOfYear == 0 ? BigDecimal.ZERO :
                                totalHoursOfYear.divide(BigDecimal.valueOf(totalRoomsRentedOfYear), BaseConstant.DATA_PRECISION, roundingMode));

                        // Average Minutes = total hours * 60 / total room rented
                        hkEfficiencyYearDTO.setAverageMinutes(totalRoomsRentedOfYear == 0 ? BigDecimal.ZERO :
                                totalHoursOfYear.multiply(new BigDecimal("60")).divide(BigDecimal.valueOf(totalRoomsRentedOfYear), BaseConstant.DATA_PRECISION, roundingMode));
                    }
                    return hkEfficiencyYearDTO;
                }).collect(Collectors.toList());

                return HkEfficiencyPropertyDTO.builder().propertyId(property.getId())
                        .details(hkEfficiencyYearDTOList).build();
            }).collect(Collectors.toList());
        }

        /**
         * 获取导出报告数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return 报告数据
         * @author zc
         * @date 2023/3/29
         */
        @Override
        public HashMap<String, Object> exportFile(PropertyDTO propertyDTO, HkEfficiencyListQuery query) {
            // 获取数据
            List<HkEfficiencyDTO> hkEfficiencyDTOList = list(propertyDTO, query).getList();

            String timezone = propertyDTO.getTimezone();
            InternationalizationDTO internationalization = propertyDTO.getInternationalization();
            String calculationRule = internationalization.getNumber().getCalculationRule();
            InternationalizationDateTimeDTO dateTimeDTO = internationalization.getDateTime();
            String hourUnit = i18nMessageUtils.message(CommonFieldI18nConstant.HOUR, LocaleContextHolder.getLocale());
            String minuteUnit = i18nMessageUtils.message(CommonFieldI18nConstant.MINUTE, LocaleContextHolder.getLocale());

            List<HkEfficiencyExportVO> monthList = Lists.newArrayList();
            for (HkEfficiencyDTO item : hkEfficiencyDTOList) {
                HkEfficiencyExportVO exportExcel = new HkEfficiencyExportVO();
                exportExcel.setTheDate(InternationalizationUtils.formatDateTime(item.getTheDate(), dateTimeDTO.getLongDate(),
                        StringUtils.EMPTY, timezone, LocaleContextHolder.getLocale()));
                // Rooms Rented
                exportExcel.setRoomsRented(formatValueStr(item.getRoomsRented(), calculationRule, StringUtils.EMPTY));
                // Hours Worked
                exportExcel.setHoursWorked(formatValueStr(item.getHoursWorked(), calculationRule, hourUnit));
                // Retro
                exportExcel.setRetro(formatValueStr(item.getRetro(), calculationRule, StringUtils.EMPTY));
                // Hours Housekeeping
                exportExcel.setHoursHousekeeping(formatValueStr(item.getHoursHousekeeping(), calculationRule, hourUnit));
                // Temp Hours
                exportExcel.setTempHours(formatValueStr(item.getTempHours(), calculationRule, hourUnit));
                // Adjust Hours
                exportExcel.setAdjustHours(formatValueStr(item.getAdjustHours(), calculationRule, hourUnit));
                // Total Housekeeping Hours
                exportExcel.setTotalHousekeepingHours(formatValueStr(item.getTotalHousekeepingHours(), calculationRule, hourUnit));
                // Ratio
                exportExcel.setRatio(formatValueStr(item.getRatio(), calculationRule, minuteUnit));
                // Explanation
                exportExcel.setExplanation(Objects.isNull(item.getExplanation()) ? "--" : item.getExplanation());
                monthList.add(exportExcel);
            }

            // 获取时间段内数据各字段的 Total
            HkEfficiencyTotalDTO totalDTO = getTotal(propertyDTO, hkEfficiencyDTOList);

            String startDate = InternationalizationUtils.formatDateTime(DateTimeUtils.strToLocalDate(String.valueOf(query.getStartDate()),
                            BaseConstant.DATE_FORMAT_YYYYMMDD).atStartOfDay(ZoneId.of(propertyDTO.getTimezone())).toEpochSecond(),
                    dateTimeDTO.getLongDate(), StringUtils.EMPTY, timezone, LocaleContextHolder.getLocale());

            String endDate = InternationalizationUtils.formatDateTime(DateTimeUtils.strToLocalDate(String.valueOf(query.getEndDate()),
                            BaseConstant.DATE_FORMAT_YYYYMMDD).atStartOfDay(ZoneId.of(propertyDTO.getTimezone())).toEpochSecond(),
                    dateTimeDTO.getLongDate(), StringUtils.EMPTY, timezone, LocaleContextHolder.getLocale());
            HashMap<String, Object> data = new HashMap<>();
            data.put("propertyName", propertyDTO.getName());
            data.put("date", startDate + " ~ " + endDate);
            data.put("averageHours", formatValueStr(totalDTO.getAverageHours(), calculationRule, hourUnit));
            data.put("averageMinutes", formatValueStr(totalDTO.getAverageMinutes(), calculationRule, minuteUnit));
            data.put("monthList", monthList);
            data.put("roomsRentedCount", formatValueStr(totalDTO.getRoomsRentedTotal(), calculationRule, StringUtils.EMPTY));
            data.put("hoursWorkedCount", formatValueStr(totalDTO.getHoursWorkedTotal(), calculationRule, hourUnit));
            data.put("retroCount", formatValueStr(totalDTO.getRetroTotal(), calculationRule, StringUtils.EMPTY));
            data.put("hoursHousekeepingCount", formatValueStr(totalDTO.getHoursHousekeepingTotal(), calculationRule, hourUnit));
            data.put("tempHoursCount", formatValueStr(totalDTO.getTempHoursTotal(), calculationRule, hourUnit));
            data.put("adjustHoursCount", formatValueStr(totalDTO.getAdjustHoursTotal(), calculationRule, hourUnit));
            data.put("totalHousekeepingHoursCount", formatValueStr(totalDTO.getTotalHousekeepingHoursTotal(), calculationRule, hourUnit));
            return data;
        }

        /**
         * 格式化数字为字符串
         *
         * @param value           数值
         * @param calculationRule 计数保留规则
         * @param unit            单位符号
         * @return java.lang.String
         * @author zc
         * @date 2025/5/29
         */
        private String formatValueStr(Object value, String calculationRule, String unit) {
            String valueStr = InternationalizationUtils.formatViewValue(value,
                    PrecisionCons.HK_EFFICIENCY_PRECISION, calculationRule, unit);
            return StringUtils.isNotBlank(valueStr) ? valueStr : "--";
        }

        /**
         * 处理 Housekeeping Efficiency 报告数据
         *
         * @param propertyList         资产
         * @param boList               报告文件数据 BO
         * @param operatorId           操作人 id
         * @param propertyId2DateRange 报告数据日期范围(yyyyMMdd)
         * @author zc
         * @date 2024/8/20
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleHousekeepingEfficiency(List<PropertyDTO> propertyList, List<HkEfficiencyListBO> boList,
                                                 int operatorId, Map<Integer, List<Long>> propertyId2DateRange) {
            List<String> keys = getRedisKeys(propertyList, propertyId2DateRange, HkEfficiencyDTO.REPORT_TYPE_ATTENDANCE);
            if (CollectionUtils.isEmpty(keys)) return;
            //锁集合
            RLock[] locks = keys.stream().map(redissonClient::getLock).toList().toArray(new RLock[keys.size()]);
            //获取锁
            RLock multiLock = redissonClient.getMultiLock(locks);

            if (RedissonLockUtil.getLock(multiLock, 3, 2)) {
                try {
                    // 过滤数据
                    List<HkEfficiencyListBO> effectiveBoList = getEffectiveBoList(propertyList, boList);
                    // 保存 HK detail 数据
                    handleImportHKDetailData(propertyList, operatorId, effectiveBoList, propertyId2DateRange);

                    // 转换 BO 数据
                    List<HkEfficiency> hkEfficiencyList = formatBoList(propertyList, effectiveBoList);
                    // 处理导入数据
                    handleImportAttendanceHousekeepingEfficiencyData(hkEfficiencyList, operatorId, propertyList);

                    // 数据发生变化时，发送 sns 事件
                    TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                        Map<Integer, List<Long>> propertyId2DateList = hkEfficiencyList.stream()
                                .collect(Collectors.groupingBy(HkEfficiency::getPropertyId,
                                        Collectors.mapping(HkEfficiency::getTheDate, Collectors.toList())));
                        sendModuleEvent(propertyList, propertyId2DateList);
                    });
                } finally {
                    if (multiLock.isHeldByCurrentThread()) {
                        multiLock.unlock();  // 释放锁
                    }
                }
            } else {
                throw new RuntimeException("handleHousekeepingEfficiency error, The number of execution failures reached the upper limit");
            }
        }

        /**
         * 数据发生变化时，发送 sns 事件
         *
         * @param propertyList        资产集合
         * @param propertyId2DateList 报告数据开始、结束日期(时间戳)
         * @author zc
         * @date 2024/7/23
         */
        @Override
        public void sendModuleEvent(List<PropertyDTO> propertyList, Map<Integer, List<Long>> propertyId2DateList) {
            List<HKEfficiencyBO> HKEfficiencyBOList = Lists.newArrayList();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYYMMDD);
            for (PropertyDTO propertyDTO : propertyList) {
                List<Long> dateTimestampList = propertyId2DateList.get(propertyDTO.getId());
                if (CollectionUtils.isEmpty(dateTimestampList)) {
                    continue;
                }

                // 获取数据
                List<HkEfficiency> hkEfficiencyList = listByDateRange(propertyDTO.getId(),
                        Collections.min(dateTimestampList), Collections.max(dateTimestampList));

                HKEfficiencyBOList.addAll(beanMapper.mapList(hkEfficiencyList, HKEfficiencyBO.class, (entity, bo) -> {
                    ZonedDateTime zonedDateTime = DateTimeUtils.toZonedDateTime(entity.getTheDate(), propertyDTO.getTimezone());
                    bo.setDate(Long.parseLong(zonedDateTime.format(formatter)));
                }));
            }

            if (CollectionUtils.isEmpty(HKEfficiencyBOList)) {
                return;
            }

            String messages = JsonUtils.convertObjListToString(objectMapper, HKEfficiencyBOList);
            moduleEventSender.publish(MoudleEventSubjectEnum.HK_EFFICIENCY.getSubject(), messages);
        }

        /**
         * 保存 HK detail 数据
         *
         * @param propertyList         资产集合
         * @param operatorId           操作人 id
         * @param boList               报告文件数据 BO
         * @param propertyId2DateRange 报告数据日期范围(yyyyMMdd)
         * @author zc
         * @date 2024/2/26
         */
        private void handleImportHKDetailData(List<PropertyDTO> propertyList, int operatorId,
                                              List<HkEfficiencyListBO> boList,
                                              Map<Integer, List<Long>> propertyId2DateRange) {
            HashMap<Integer, List<Long>> propertyId2StartDayOfMonths = new HashMap<>();
            propertyList.forEach(propertyDTO -> {
                List<Long> dateRangeList = propertyId2DateRange.getOrDefault(propertyDTO.getId(), Collections.emptyList());
                if (CollectionUtils.isEmpty(dateRangeList)) {
                    return;
                }
                LocalDate startLocalDate = DateTimeUtils.strToLocalDate(dateRangeList.get(0).toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                long startDate = DateTimeUtils.localDateToLong(startLocalDate, propertyDTO.getTimezone());

                LocalDate endLocalDate = DateTimeUtils.strToLocalDate(dateRangeList.get(1).toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                long endDate = DateTimeUtils.localDateToLong(endLocalDate, propertyDTO.getTimezone());

                // 删除指定日期、资产范围内的 HK details 数据
                hkEfficiencyDetailService.deleteByDateRange(propertyDTO, startDate, endDate);
                // 清除指定日期、资产范围内的 HK efficiency 数据值
                clearHKEfficiencyByDateRange(propertyDTO, startDate, endDate);

                Set<Long> startDayOfMonths = startLocalDate.datesUntil(endLocalDate.plusDays(1)).map(item -> {
                    LocalDate startOfMonth = item.withDayOfMonth(1);
                    return DateTimeUtils.localDateToLong(startOfMonth, propertyDTO.getTimezone());
                }).collect(Collectors.toSet());
                propertyId2StartDayOfMonths.put(propertyDTO.getId(), Lists.newArrayList(startDayOfMonths).stream().sorted().toList());
            });

            if(MapUtils.isNotEmpty(propertyId2StartDayOfMonths)){
                // 重新统计 HK efficiency sum
                Set<Integer> propertyIdsByClearSum = propertyId2StartDayOfMonths.keySet();
                List<PropertyDTO> propertyDTOSByClearSum = propertyList.stream().filter(propertyDTO -> propertyIdsByClearSum.contains(propertyDTO.getId()))
                        .toList();
                hkEfficiencySumService.handleHkEfficiencySum(propertyDTOSByClearSum, propertyId2StartDayOfMonths);
            }

            if (CollectionUtils.isEmpty(boList)) {
                return;
            }
            Map<String, PropertyDTO> abbr2Property = propertyList.stream()
                    .collect(Collectors.toMap(PropertyDTO::getAbbr, v -> v));

            long nowTimestamp = DateTimeUtils.nowTimestamp();
            List<HkEfficiencyDetail> hkEfficiencyDetails = boList.stream().map(bo -> {
                PropertyDTO propertyDTO = abbr2Property.get(bo.getProperty());
                long theDate = DateTimeUtils.strToLocalDate(bo.getTheDay(), BaseConstant.DATE_FORMAT_YYYY_MM_DD)
                        .atStartOfDay(ZoneId.of(propertyDTO.getTimezone())).toEpochSecond();
                return HkEfficiencyDetail.builder()
                        .propertyId(propertyDTO.getId())
                        .theDate(theDate)
                        .hoursWorked(bo.getHours())
                        .payrollName(bo.getPayrollName())
                        .earningsCode(bo.getEarningsCode())
                        .updatedBy(operatorId)
                        .updatedTime(nowTimestamp)
                        .build();
            }).toList();
            // 批量保存
            hkEfficiencyDetailService.saveBatch(hkEfficiencyDetails);
        }

        /**
         * 获取有效的 BO 数据
         *
         * @param propertyList 资产
         * @param boList       报告文件数据 BO
         * @author zc
         * @date 2023/10/23
         */
        private List<HkEfficiencyListBO> getEffectiveBoList(List<PropertyDTO> propertyList, List<HkEfficiencyListBO> boList) {
            List<Integer> propertyIds = propertyList.stream().map(PropertyDTO::getId).toList();
            // 获取文件过滤配置
            Map<Integer, List<String>> propertyId2DepartmentCodes = reportFilterConfigService.list(propertyIds).stream()
                    .collect(Collectors.groupingBy(HkEfficiencyReportFilterConfig::getPropertyId,
                            Collectors.mapping(HkEfficiencyReportFilterConfig::getDepartmentCode, Collectors.toList())));
            if (propertyId2DepartmentCodes.isEmpty()) {
                return Collections.emptyList();
            }
            Map<String, PropertyDTO> abbr2Property = propertyList.stream()
                    .collect(Collectors.toMap(PropertyDTO::getAbbr, v -> v));

            // 获取有效的 BO 数据
            List<HkEfficiencyListBO> effectiveBoList = Lists.newArrayList();
            for (HkEfficiencyListBO item : boList) {
                PropertyDTO propertyDTO = abbr2Property.get(item.getProperty());
                List<String> departmentCodes = propertyId2DepartmentCodes.getOrDefault(propertyDTO.getId(), Collections.emptyList());
                if (departmentCodes.contains(item.getDepartmentCode())) {
                    effectiveBoList.add(item);
                }
            }
            return effectiveBoList;
        }

        /**
         * 格式化 BO 数据
         *
         * @param propertyList 资产
         * @param boList       报告文件数据 BO
         * @return HkEfficiency
         * @author zc
         * @date 2023/10/23
         */
        private List<HkEfficiency> formatBoList(List<PropertyDTO> propertyList, List<HkEfficiencyListBO> boList) {
            if (CollectionUtils.isEmpty(boList)) {
                return Collections.emptyList();
            }
            Map<String, List<HkEfficiencyListBO>> abbr2BoList = boList.stream()
                    .collect(Collectors.groupingBy(HkEfficiencyListBO::getProperty));

            Map<String, PropertyDTO> abbr2Property = propertyList.stream()
                    .collect(Collectors.toMap(PropertyDTO::getAbbr, v -> v));

            List<HkEfficiency> housekeepingEfficiencies = Lists.newArrayList();
            long nowTime = DateTimeUtils.nowTimestamp();
            abbr2BoList.forEach((abbr, bos) -> {
                PropertyDTO propertyDTO = abbr2Property.get(abbr);
                // 考勤信息按照日期分组
                Map<String, List<HkEfficiencyListBO>> theDay2BoList = bos.stream()
                        .collect(Collectors.groupingBy(HkEfficiencyListBO::getTheDay));

                theDay2BoList.forEach((theDay, hoursList) -> {
                    // 当天当前部门下的 pay code 为空的实际工时之和
                    BigDecimal totalHoursWorked = null;

                    // 当天当前部门下的 pay code 为 RETRO 的实际工时之和
                    BigDecimal totalRetro = null;
                    for (HkEfficiencyListBO hoursDTO : hoursList) {
                        if (StringUtils.isEmpty(hoursDTO.getEarningsCode())) {
                            totalHoursWorked = Objects.isNull(totalHoursWorked) ? BigDecimal.ZERO : totalHoursWorked;
                            totalHoursWorked = totalHoursWorked.add(hoursDTO.getHours());
                        } else {
                            if (SvaSchedulePayCodeEnum.RETRO.getFullName().equalsIgnoreCase(hoursDTO.getEarningsCode())) {
                                totalRetro = Objects.isNull(totalRetro) ? BigDecimal.ZERO : totalRetro;
                                totalRetro = totalRetro.add(hoursDTO.getHours());
                            }
                        }
                    }

                    ZonedDateTime zonedDateTime = DateTimeUtils.strToLocalDate(theDay, BaseConstant.DATE_FORMAT_YYYY_MM_DD)
                            .atStartOfDay(ZoneId.of(propertyDTO.getTimezone()));
                    long startOfMonth = zonedDateTime.withDayOfMonth(1).toEpochSecond();

                    HkEfficiency hkEfficiency = new HkEfficiency();
                    hkEfficiency.setPropertyId(propertyDTO.getId());
                    hkEfficiency.setStartOfMonth(startOfMonth);
                    hkEfficiency.setTheDate(zonedDateTime.toEpochSecond());
                    hkEfficiency.setHoursWorked(totalHoursWorked);
                    hkEfficiency.setRetro(totalRetro);
                    hkEfficiency.setUpdatedBy(0);
                    hkEfficiency.setUpdatedTime(nowTime);
                    housekeepingEfficiencies.add(hkEfficiency);
                });
            });
            return housekeepingEfficiencies;
        }

        /**
         * 解析报告 for HK Efficiency
         *
         * @param propertyDTOList      资产集合
         * @param in                   文件流
         * @param source               数据源
         * @param propertyId2DateRange 报告数据日期范围(yyyyMMdd)
         * @return HousekeepingEfficiencyListBO 集合
         * @author zc
         * @date 2024/8/20
         */
        @Override
        public List<HkEfficiencyListBO> parseReportForHKEfficiency(List<PropertyDTO> propertyDTOList, InputStream in,
                                                                   String source, Map<Integer, List<Long>> propertyId2DateRange) {
            Collection<IHkEfficiencyParseService> parserList = applicationContext.getBeansOfType(IHkEfficiencyParseService.class).values();
            for (IHkEfficiencyParseService item : parserList) {
                if (item.supports(source)) {
                    return item.parse(propertyDTOList, in, propertyId2DateRange);
                }
            }
            throw new ListenerException("Not found report parser");
        }

        /**
         * 解析报告 for HK Operating statistics
         *
         * @param propertyDTOList      资产集合
         * @param in                   文件流
         * @param source               数据源
         * @param propertyId2DateRange 报告数据日期范围(yyyyMMdd)
         * @return HKOperatingStatisticsListBO 集合
         * @author zc
         * @date 2024/8/20
         */
        @Override
        public List<HkOperatingStatisticsListBO> parseReportForHKOperatingStatistics(List<PropertyDTO> propertyDTOList, InputStream in,
                                                                                     String source, Map<Integer, List<Long>> propertyId2DateRange) {
            Collection<IHkOperatingStatisticsParseService> parserList = applicationContext.getBeansOfType(IHkOperatingStatisticsParseService.class).values();
            for (IHkOperatingStatisticsParseService item : parserList) {
                if (item.supports(source)) {
                    return item.parse(propertyDTOList, in, propertyId2DateRange);
                }
            }
            throw new ListenerException("Not found report parser");
        }

        /**
         * 处理 HK Operating 报告数据
         *
         * @param propertyList         资产
         * @param boList               报告数据 BO
         * @param operatorId           操作人 id
         * @param propertyId2DateRange 报告数据日期范围(yyyyMMdd)
         * @param datasource           数据源
         * @return java.util.Map<java.lang.Integer, java.lang.Integer>
         * @author lichen
         * @date 2025/11/25
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public Map<Integer, Integer> handleHKOperatingStatistics(List<PropertyDTO> propertyList, List<HkOperatingStatisticsListBO> boList,
                                                                 int operatorId, Map<Integer, List<Long>> propertyId2DateRange, int datasource) {
            List<String> keys = getRedisKeys(propertyList, propertyId2DateRange, HkEfficiencyDTO.REPORT_TYPE_ACCOUNTING);
            if (CollectionUtils.isEmpty(keys)) return Collections.emptyMap();
            //锁集合
            RLock[] locks = keys.stream().map(redissonClient::getLock).toList().toArray(new RLock[keys.size()]);
            //获取锁
            RLock multiLock = redissonClient.getMultiLock(locks);
            if (RedissonLockUtil.getLock(multiLock, 3, 2)) {
                try {
                    // 处理数据
                    return doHandleHKOperatingStatistics(propertyList, boList, operatorId, propertyId2DateRange, datasource);
                } finally {
                    if (multiLock.isHeldByCurrentThread()) {
                        multiLock.unlock();  // 释放锁
                    }
                }
            } else {
                throw new RuntimeException("doHandleHKOperatingStatistics error, The number of execution failures reached the upper limit");
            }
        }

        /**
         * 处理 HK Operating 报告数据
         *
         * @param propertyList         资产
         * @param boList               报告数据 BO
         * @param operatorId           操作人 id
         * @param propertyId2DateRange 报告数据日期范围(yyyyMMdd)
         * @param datasource           数据源
         * @return java.util.HashMap<java.lang.Integer, java.lang.Integer>
         * @author lichen
         * @date 2025/11/25
         */
        private HashMap<Integer, Integer> doHandleHKOperatingStatistics(List<PropertyDTO> propertyList, List<HkOperatingStatisticsListBO> boList,
                                                                        int operatorId, Map<Integer, List<Long>> propertyId2DateRange, int datasource) {
            Set<Long> dateList = propertyId2DateRange.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
            // 日期 + 1 天，因为当天售出的房间是在第二天打扫；由于各个资产的时区不一致，此处还需要 - 1天，防止少查询数据
            // +1、-1抵消后，不用再处理日期
            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(Collections.min(dateList).toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            long startDate = DateTimeUtils.localDateToLong(startLocalDate, ZoneId.systemDefault());

            // 日期 + 1 天，因为当天售出的房间是在第二天打扫；由于各个资产的时区不一致，此处还需要 + 1天，防止少查询数据
            LocalDate maxLocalDate = DateTimeUtils.strToLocalDate(Collections.max(dateList).toString(), BaseConstant.DATE_FORMAT_YYYYMMDD).plusDays(2);
            long endDate = DateTimeUtils.localDateToLong(maxLocalDate, ZoneId.systemDefault());

            Map<Integer, PropertyDTO> propertyId2DTO = propertyList.stream().collect(Collectors.toMap(PropertyDTO::getId, Function.identity()));
            List<Integer> propertyIds = propertyList.stream().map(PropertyDTO::getId).toList();

            // 查询已经存在的数据，按照资产id和日期分组
            Map<Integer, Map<Long, HkEfficiency>> propertyId2TheDate2OriginData = listByDateRange(propertyIds, startDate, endDate)
                    .stream().collect(Collectors.groupingBy(HkEfficiency::getPropertyId, Collectors.toMap(HkEfficiency::getTheDate, Function.identity())));

            // 待处理的数据按照资产和日期进行分组
            Map<String, Map<String, HkOperatingStatisticsListBO>> property2TheDay2BO = boList.stream()
                    .collect(Collectors.groupingBy(HkOperatingStatisticsListBO::getProperty, Collectors.toMap(HkOperatingStatisticsListBO::getTheDay, Function.identity())));

            ArrayList<HkEfficiency> batchInsertList = Lists.newArrayListWithCapacity(boList.size());
            ArrayList<HkEfficiency> batchUpdateWithNullList = Lists.newArrayListWithCapacity(boList.size());
            // 资产对应的，数据发生变化的月起始日集合
            HashMap<Integer, List<Long>> propertyId2StartOfMonthList = new HashMap<>();
            // 资产对应的，数据发生变化的日期集合
            HashMap<Integer, List<Long>> propertyId2TheDateList = new HashMap<>();
            // 资产对应的数据更新结果
            HashMap<Integer, Integer> propertyId2DataUpdateResult = Maps.newHashMapWithExpectedSize(propertyList.size());
            long nowTimestamp = DateTimeUtils.nowTimestamp();

            propertyId2DateRange.forEach((propertyId, dateRange) -> {
                PropertyDTO propertyDTO = propertyId2DTO.get(propertyId);

                // 指定资产原来的数据
                Map<Long, HkEfficiency> theDate2OriginData = propertyId2TheDate2OriginData.getOrDefault(propertyId, Collections.emptyMap());
                // 指定资产待处理的数据
                Map<String, HkOperatingStatisticsListBO> theDay2BO = property2TheDay2BO.getOrDefault(propertyDTO.getAbbr(), Collections.emptyMap());

                // 数据发生变化的日期集合
                HashSet<Long> startOfMonths = new HashSet<>();
                // 数据发生变化的日期集合
                ArrayList<Long> theDates = new ArrayList<>();

                // 未更新的数量
                int nonUpdateCount = 0;
                // 无需处理的日期数量
                int noProcessDateCount = 0;
                // 总的日期数
                int dateCount = 0;

                LocalDate startLocalDateByPropertyId = DateTimeUtils.strToLocalDate(dateRange.get(0).toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                LocalDate endLocalDateByPropertyId = DateTimeUtils.strToLocalDate(dateRange.get(1).toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                // 循环开始日期和结束日期的每一天
                for (LocalDate localDate = startLocalDateByPropertyId; !localDate.isAfter(endLocalDateByPropertyId); localDate = localDate.plusDays(1)) {
                    dateCount++;

                    // 待处理的数据
                    String theDay = DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYY_MM_DD);
                    HkOperatingStatisticsListBO bo = theDay2BO.get(theDay);

                    // 日期 + 1 天，因为当天售出的房间是在第二天打扫
                    LocalDate nextLocalDate = localDate.plusDays(1);
                    long theDate = DateTimeUtils.localDateToLong(nextLocalDate, propertyDTO.getTimezone());
                    // 原来的数据
                    HkEfficiency originData = theDate2OriginData.get(theDate);

                    if (Objects.isNull(bo)) {
                        if (Objects.nonNull(originData)) {
                            if (originData.getDatasource() != OperatingStatisticsDatasourceEnum.M3_API.getValue()) {
                                // 待处理数据不存在，原来数据存在且数据源非M3 API时，则原来的指标设置为null
                                HkEfficiency updateEntity = new HkEfficiency();
                                updateEntity.setId(originData.getId());
                                updateEntity.setRoomsRented(null);
                                updateEntity.setDatasource(datasource);
                                updateEntity.setUpdatedBy(operatorId);
                                updateEntity.setUpdatedTime(nowTimestamp);
                                batchUpdateWithNullList.add(updateEntity);

                                startOfMonths.add(originData.getStartOfMonth());
                                theDates.add(theDate);
                            } else {
                                // 待处理数据不存在，原来数据存在且数据源为M3 API时，则不能更新
                                nonUpdateCount++;
                            }
                        } else {
                            // 待处理数据不存在，原来数据不存在时，则无需处理
                            noProcessDateCount++;
                        }
                    } else {
                        if (Objects.isNull(originData)) {
                            long startOfMonth = DateTimeUtils.localDateToLong(nextLocalDate.withDayOfMonth(1), propertyDTO.getTimezone());

                            HkEfficiency insertEntity = HkEfficiency.builder()
                                    .propertyId(propertyDTO.getId())
                                    .startOfMonth(startOfMonth)
                                    .theDate(theDate)
                                    .roomsRented(bo.getRoomsRented())
                                    .updatedBy(operatorId)
                                    .updatedTime(nowTimestamp)
                                    .datasource(datasource)
                                    .build();
                            batchInsertList.add(insertEntity);

                            startOfMonths.add(startOfMonth);
                            theDates.add(theDate);
                        } else {
                            if (originData.getDatasource() == OperatingStatisticsDatasourceEnum.M3_API.getValue()) {
                                // 存在数据源等于M3 API的数据，则不更新
                                nonUpdateCount++;
                            } else {
                                // 存在数据源不等于M3 API的数据，则更新
                                HkEfficiency updateEntity = new HkEfficiency();
                                updateEntity.setId(originData.getId());
                                updateEntity.setRoomsRented(bo.getRoomsRented());
                                updateEntity.setDatasource(datasource);
                                updateEntity.setUpdatedBy(operatorId);
                                updateEntity.setUpdatedTime(nowTimestamp);
                                batchUpdateWithNullList.add(updateEntity);

                                startOfMonths.add(originData.getStartOfMonth());
                                theDates.add(theDate);
                            }
                        }
                    }
                }

                if (CollectionUtils.isNotEmpty(startOfMonths)) {
                    propertyId2StartOfMonthList.put(propertyDTO.getId(), new ArrayList<>(startOfMonths));
                }
                if (CollectionUtils.isNotEmpty(theDates)) {
                    propertyId2TheDateList.put(propertyDTO.getId(), theDates);
                }

                // 需要处理的日期数
                int needProcessDateCount = dateCount - noProcessDateCount;

                int dataUpdateResult;
                if (nonUpdateCount == needProcessDateCount) {
                    // 未更新的数量等于需要处理的数量，表示全部未更新
                    dataUpdateResult = DataUpdateResult.NOT_UPDATED.getValue();
                } else if (nonUpdateCount == 0) {
                    // 未更新的数量等于0，表示全部更新
                    dataUpdateResult = DataUpdateResult.ALL_UPDATED.getValue();
                } else {
                    // 部分更新
                    dataUpdateResult = DataUpdateResult.PARTIAL_UPDATED.getValue();
                }
                propertyId2DataUpdateResult.put(propertyDTO.getId(), dataUpdateResult);
            });

            // 批量新增
            saveBatch(batchInsertList);

            // 批量更新，支持更新为null
            if (CollectionUtils.isNotEmpty(batchUpdateWithNullList)) {
                baseMapper.batchUpdateRoomsRentedWithNull(batchUpdateWithNullList);
            }

            // 处理汇总数据
            if (MapUtils.isNotEmpty(propertyId2StartOfMonthList)) {
                Set<Integer> needUpdateHkEfficiencySumPropertyIds = propertyId2StartOfMonthList.keySet();
                List<PropertyDTO> needUpdateHkEfficiencySumPropertyDTOS = propertyList.stream()
                        .filter(propertyDTO -> needUpdateHkEfficiencySumPropertyIds.contains(propertyDTO.getId()))
                        .toList();
                hkEfficiencySumService.handleHkEfficiencySum(needUpdateHkEfficiencySumPropertyDTOS, propertyId2StartOfMonthList);
            }

            // 数据发生变化时，发送 sns 事件
            if (MapUtils.isNotEmpty(propertyId2TheDateList)) {
                TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                    Set<Integer> needSendSnsPropertyIds = propertyId2TheDateList.keySet();
                    List<PropertyDTO> needSendSnsPropertyDTOS = propertyList.stream()
                            .filter(propertyDTO -> needSendSnsPropertyIds.contains(propertyDTO.getId()))
                            .toList();
                    sendModuleEvent(needSendSnsPropertyDTOS, propertyId2TheDateList);
                });
            }

            return propertyId2DataUpdateResult;
        }

        /**
         * 处理来源是M3Api的 HK Operating 报告数据
         *
         * @param propertyDTO 资产
         * @param boList      报告数据 BO
         * @param operatorId  操作人 id
         * @param startDate   开始日期(yyyyMMdd)
         * @param endDate     结束日期(yyyyMMdd)
         * @author lichen
         * @date 2025/11/25
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleHKOperatingStatisticsByM3Api(PropertyDTO propertyDTO, List<HkOperatingStatisticsListBO> boList,
                                                       int operatorId, Integer startDate, Integer endDate) {
            if (CollectionUtils.isEmpty(boList)) return;

            //日期 + 1 天，因为当天售出的房间是在第二天打扫，用来计算房间打扫耗时
            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(startDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD).plusDays(1);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(endDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD).plusDays(1);
            // 获取开始日期到结束日期的每一天
            List<LocalDate> dates = startLocalDate.datesUntil(endLocalDate.plusDays(1)).toList();

            // 生成锁key
            List<String> keys = dates.stream().map(date -> {
                String dateStr = DateTimeUtils.localDateToStr(date, BaseConstant.DATE_FORMAT_YYYYMMDD);
                return MessageFormat.format(HkEfficiencyConstant.HANDLE_HK_EFFICIENCY_KEY, propertyDTO.getId(), dateStr);
            }).toList();

            //锁集合
            RLock[] locks = keys.stream().map(redissonClient::getLock).toList().toArray(new RLock[keys.size()]);

            //获取锁
            RLock multiLock = redissonClient.getMultiLock(locks);

            if (RedissonLockUtil.getLock(multiLock, 3, 2)) {
                try {
                    // 处理数据
                    doHandleHKOperatingStatisticsByM3Api(propertyDTO, boList, operatorId,
                            DateTimeUtils.localDateToLong(startLocalDate, propertyDTO.getTimezone()),
                            DateTimeUtils.localDateToLong(endLocalDate, propertyDTO.getTimezone()));
                } finally {
                    if (multiLock.isHeldByCurrentThread()) {
                        multiLock.unlock();  // 释放锁
                    }
                }
            } else {
                throw new RuntimeException("handleHKOperatingStatisticsByM3Api error, The number of execution failures reached the upper limit");
            }
        }

        /**
         * 处理来源是M3Api的 HK Operating 报告数据
         *
         * @param propertyDTO 资产
         * @param boList      报告数据 BO
         * @param operatorId  操作人 id
         * @param startDate   HkEfficiency数据开始日期(yyyyMMdd)
         * @param endDate     HkEfficiency数据结束日期(yyyyMMdd)
         * @author lichen
         * @date 2025/11/25
         */
        private void doHandleHKOperatingStatisticsByM3Api(PropertyDTO propertyDTO, List<HkOperatingStatisticsListBO> boList,
                                                          int operatorId, Long startDate, Long endDate) {
            // 查询已经存在的数据
            Map<Long, HkEfficiency> theDate2OriginData = listByDateRange(propertyDTO.getId(), startDate, endDate)
                    .stream().collect(Collectors.toMap(HkEfficiency::getTheDate, Function.identity()));

            ArrayList<HkEfficiency> insertBatch = Lists.newArrayListWithCapacity(boList.size());
            ArrayList<HkEfficiency> updateBatch = Lists.newArrayListWithCapacity(boList.size());
            ArrayList<Long> theDateList = Lists.newArrayListWithCapacity(boList.size());
            // 月开始日
            Set<Long> startOfMonths = new HashSet<>();
            long nowTime = DateTimeUtils.nowTimestamp();

            for (HkOperatingStatisticsListBO bo : boList) {
                // 日期 + 1 天，因为当天售出的房间是在第二天打扫，用来计算房间打扫耗时
                LocalDate localDate = DateTimeUtils.strToLocalDate(bo.getTheDay(), BaseConstant.DATE_FORMAT_YYYY_MM_DD).plusDays(1);
                long theDate = DateTimeUtils.localDateToLong(localDate, propertyDTO.getTimezone());
                theDateList.add(theDate);

                // 月开始日
                Long startOfMonth = DateTimeUtils.localDateToLong(localDate.withDayOfMonth(1), propertyDTO.getTimezone());
                startOfMonths.add(startOfMonth);

                HkEfficiency originData = theDate2OriginData.get(theDate);
                if (Objects.isNull(originData)) {
                    // 插入
                    HkEfficiency insertEntity = HkEfficiency.builder()
                            .propertyId(propertyDTO.getId())
                            .startOfMonth(startOfMonth)
                            .theDate(theDate)
                            .roomsRented(bo.getRoomsRented())
                            .updatedBy(operatorId)
                            .updatedTime(nowTime)
                            .datasource(OperatingStatisticsDatasourceEnum.M3_API.getValue())
                            .build();
                    insertBatch.add(insertEntity);
                } else {
                    // 更新
                    originData.setRoomsRented(bo.getRoomsRented());
                    originData.setUpdatedBy(operatorId);
                    originData.setUpdatedTime(nowTime);
                    originData.setDatasource(OperatingStatisticsDatasourceEnum.M3_API.getValue());
                    updateBatch.add(originData);
                }
            }

            saveBatch(insertBatch);

            if (CollectionUtils.isNotEmpty(updateBatch)) {
                baseMapper.batchUpdateRoomsRentedWithNull(updateBatch);
            }

            // 处理汇总数据
            HashMap<Integer, List<Long>> propertyId2StartOfMonths = new HashMap<>();
            propertyId2StartOfMonths.put(propertyDTO.getId(), new ArrayList<>(startOfMonths));
            hkEfficiencySumService.handleHkEfficiencySum(Collections.singletonList(propertyDTO), propertyId2StartOfMonths);

            // 数据发生变化时，发送 sns 事件
            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                HashMap<Integer, List<Long>> propertyId2DateList = new HashMap<>();
                propertyId2DateList.put(propertyDTO.getId(), theDateList);
                sendModuleEvent(Collections.singletonList(propertyDTO), propertyId2DateList);
            });
        }

        /**
         * 清除指定日期、资产范围内的 HK efficiency 数据值
         *
         * @param propertyDTO 资产
         * @param startDate   开始日期(时间戳)
         * @param endDate     结束日期(时间戳)
         * @author zc
         * @date 2024/3/2
         */
        private void clearHKEfficiencyByDateRange(PropertyDTO propertyDTO, long startDate, long endDate) {
            update(new UpdateWrapper<HkEfficiency>().lambda()
                    .set(HkEfficiency::getHoursWorked, null)
                    .set(HkEfficiency::getRetro, null)
                    .eq(HkEfficiency::getPropertyId, propertyDTO.getId())
                    .between(HkEfficiency::getTheDate, startDate, endDate));
        }

        /**
         * 导入attendance-housekeeping-efficiency报告数据
         *
         * @param hkEfficiencyList 报告数据
         * @param operatorId       操作人
         * @param propertyList     资产
         * @author zc
         * @date 2023/3/30
         */
        private void handleImportAttendanceHousekeepingEfficiencyData(List<HkEfficiency> hkEfficiencyList, int operatorId, List<PropertyDTO> propertyList) {
            Map<Integer, String> propertyId2Timezone = propertyList.stream()
                    .collect(Collectors.toMap(PropertyDTO::getId, PropertyDTO::getTimezone));
            Map<Integer, List<HkEfficiency>> id2HousekeepingEfficiency = hkEfficiencyList
                    .stream().collect(Collectors.groupingBy(HkEfficiency::getPropertyId));

            List<HkEfficiency> insertHousekeepingEfficiencies = Lists.newArrayList();
            List<HkEfficiency> updateHousekeepingEfficiencies = Lists.newArrayList();
            List<HkEfficiencySum> insertHkEfficiencySums = Lists.newArrayList();
            List<HkEfficiencySum> updateHkEfficiencySums = Lists.newArrayList();

            long nowTime = DateTimeUtils.nowTimestamp();
            for (Map.Entry<Integer, List<HkEfficiency>> entry : id2HousekeepingEfficiency.entrySet()) {
                if (CollectionUtils.isEmpty(entry.getValue())) {
                    continue;
                }
                long startDate = entry.getValue().stream().min(Comparator.comparing(HkEfficiency::getTheDate)).get().getTheDate();
                long endDate = entry.getValue().stream().max(Comparator.comparing(HkEfficiency::getTheDate)).get().getTheDate();

                int propertyId = entry.getKey();
                String timezone = propertyId2Timezone.get(propertyId);
                ZonedDateTime startMonth = DateTimeUtils.toZonedDateTime(startDate, timezone).with(TemporalAdjusters.firstDayOfMonth());
                ZonedDateTime endMonth = DateTimeUtils.toZonedDateTime(endDate, timezone).with(TemporalAdjusters.firstDayOfMonth());
                List<Long> months = Lists.newArrayList();
                for (ZonedDateTime date = startMonth; !date.isAfter(endMonth); date = date.plusMonths(1)) {
                    months.add(date.toEpochSecond());
                }

                Map<Long, List<HkEfficiency>> startOfMonth2OriginHousekeepingEfficiencies = listByStartOfMonths(propertyId, months).stream()
                        .collect(Collectors.groupingBy(HkEfficiency::getStartOfMonth));

                Map<Long, HkEfficiencySum> theDate2OriginHousekeepingEfficiencySums = hkEfficiencySumService.list(propertyId, months).stream()
                        .collect(Collectors.toMap(HkEfficiencySum::getStartOfMonth, v -> v));

                Map<Long, HkEfficiency> startOfMonth2NewHousekeepingEfficiency = entry.getValue().stream()
                        .collect(Collectors.toMap(HkEfficiency::getTheDate, v -> v));

                for (Long month : months) {
                    ZonedDateTime startOfMonth = DateTimeUtils.toZonedDateTime(month, timezone);

                    List<HkEfficiency> originHousekeepingEfficiencies = startOfMonth2OriginHousekeepingEfficiencies
                            .getOrDefault(month, Collections.emptyList());

                    if (CollectionUtils.isEmpty(originHousekeepingEfficiencies)) {
                        // 没有当前月的信息，则直接创建这个月的信息，但是由于这个月的信息不够完整，所以当前月不能参与统计计算
                        ZonedDateTime endOfMonth = startOfMonth.plusMonths(1).minusDays(1);
                        long startOfMontTime = startOfMonth.toEpochSecond();
                        for (ZonedDateTime date = startOfMonth; !date.isAfter(endOfMonth); date = date.plusDays(1)) {
                            long dateTime = date.toEpochSecond();
                            // 检查报告中是否有当天的数据，如果没有则新建初始数据
                            HkEfficiency hkEfficiency = startOfMonth2NewHousekeepingEfficiency.get(dateTime);
                            if (hkEfficiency == null) {
                                hkEfficiency = new HkEfficiency(propertyId, startOfMontTime, dateTime,
                                        operatorId, nowTime);
                            }
                            insertHousekeepingEfficiencies.add(hkEfficiency);
                        }
                    } else {
                        // 如果有当前月的信息，则更新为报告中的数据，并校验信息是否完整，如果信息完整则参与统计
                        Integer totalRoomsRented = null;
                        BigDecimal totalHours = null;
                        for (HkEfficiency existHkEfficiency : originHousekeepingEfficiencies) {
                            Long theDate = existHkEfficiency.getTheDate();

                            HkEfficiency hkEfficiency = startOfMonth2NewHousekeepingEfficiency.get(theDate);
                            if (Objects.nonNull(hkEfficiency)) {
                                // 如果当天的考勤报告中的数据存在，则使用报告中的考勤信息，并更新已存在的数据
                                existHkEfficiency.setHoursWorked(hkEfficiency.getHoursWorked());
                                existHkEfficiency.setRetro(hkEfficiency.getRetro());
                                updateHousekeepingEfficiencies.add(existHkEfficiency);
                            }

                            // 如果当天信息完整、入住房间数有值且实际工时有值，则当天的数据参与统计计算
                            Map<String, Object> map = checkAndCumulative(existHkEfficiency);
                            if (!map.isEmpty()) {
                                totalRoomsRented = Objects.isNull(totalRoomsRented) ? 0 : totalRoomsRented;
                                totalRoomsRented += Integer.parseInt(map.get(HkEfficiencyConstant.ROOMS_RENTED).toString());

                                totalHours = Objects.isNull(totalHours) ? BigDecimal.ZERO : totalHours;
                                totalHours = totalHours.add(new BigDecimal(map.get(HkEfficiencyConstant.TOTAL_HOURS).toString()));
                            }
                        }

                        // 添加/修改当月的统计信息
                        HkEfficiencySum hkEfficiencySum = theDate2OriginHousekeepingEfficiencySums.getOrDefault(month, new HkEfficiencySum());
                        hkEfficiencySum.setRoomsRentedTotal(totalRoomsRented);
                        hkEfficiencySum.setHourTotal(totalHours);
                        if (Objects.isNull(hkEfficiencySum.getId())) {
                            if (Objects.isNull(totalRoomsRented)) {
                                continue;
                            }
                            hkEfficiencySum.setPropertyId(propertyId);
                            hkEfficiencySum.setTheYear(startOfMonth.getYear());
                            hkEfficiencySum.setStartOfMonth(month);
                            insertHkEfficiencySums.add(hkEfficiencySum);
                        } else {
                            updateHkEfficiencySums.add(hkEfficiencySum);
                        }
                    }
                }
            }
            // 保存 Housekeeping Efficiency
            if (CollectionUtils.isNotEmpty(insertHousekeepingEfficiencies)) {
                saveBatch(insertHousekeepingEfficiencies);
            }
            if (CollectionUtils.isNotEmpty(updateHousekeepingEfficiencies)) {
                updateBatchById(updateHousekeepingEfficiencies);
            }

            // 保存统计信息
            if (CollectionUtils.isNotEmpty(insertHkEfficiencySums)) {
                hkEfficiencySumService.saveBatch(insertHkEfficiencySums);
            }
            if (CollectionUtils.isNotEmpty(updateHkEfficiencySums)) {
                hkEfficiencySumService.updateBatchById(updateHkEfficiencySums);
            }
        }

        /**
         * 获取时间段内数据各字段的 Total
         *
         * @param propertyDTO         资产
         * @param hkEfficiencyDTOList 数据
         * @return HousekeepingEfficiencyTotalDTO
         * @author zc
         * @date 2023/12/14
         */
        @Override
        public HkEfficiencyTotalDTO getTotal(PropertyDTO propertyDTO, List<HkEfficiencyDTO> hkEfficiencyDTOList) {
            // 计算 total
            Integer roomsRentedTotal = null;
            BigDecimal hoursWorkedTotal = null;
            BigDecimal retroTotal = null;
            BigDecimal hoursHousekeepingTotal = null;
            BigDecimal tempHoursTotal = null;
            BigDecimal adjustHoursTotal = null;
            BigDecimal totalHousekeepingHoursTotal = null;
            for (HkEfficiencyDTO item : hkEfficiencyDTOList) {
                // Rooms Rented
                if (Objects.nonNull(item.getRoomsRented())) {
                    if (Objects.isNull(roomsRentedTotal)) {
                        roomsRentedTotal = 0;
                    }
                    roomsRentedTotal += item.getRoomsRented();
                }
                // Hours Worked
                if (Objects.nonNull(item.getHoursWorked())) {
                    if (Objects.isNull(hoursWorkedTotal)) {
                        hoursWorkedTotal = BigDecimal.ZERO;
                    }
                    hoursWorkedTotal = hoursWorkedTotal.add(item.getHoursWorked());
                }
                // Retro
                if (Objects.nonNull(item.getRetro())) {
                    if (Objects.isNull(retroTotal)) {
                        retroTotal = BigDecimal.ZERO;
                    }
                    retroTotal = retroTotal.add(item.getRetro());
                }
                // Hours Housekeeping
                if (Objects.nonNull(item.getHoursHousekeeping())) {
                    if (Objects.isNull(hoursHousekeepingTotal)) {
                        hoursHousekeepingTotal = BigDecimal.ZERO;
                    }
                    hoursHousekeepingTotal = hoursHousekeepingTotal.add(item.getHoursHousekeeping());
                }
                // Temp Hours
                if (Objects.nonNull(item.getTempHours())) {
                    if (Objects.isNull(tempHoursTotal)) {
                        tempHoursTotal = BigDecimal.ZERO;
                    }
                    tempHoursTotal = tempHoursTotal.add(item.getTempHours());
                }
                // Adjust Hours
                if (Objects.nonNull(item.getAdjustHours())) {
                    if (Objects.isNull(adjustHoursTotal)) {
                        adjustHoursTotal = BigDecimal.ZERO;
                    }
                    adjustHoursTotal = adjustHoursTotal.add(item.getAdjustHours());
                }
                // Total Housekeeping Hours
                if (Objects.nonNull(item.getTotalHousekeepingHours())) {
                    if (Objects.isNull(totalHousekeepingHoursTotal)) {
                        totalHousekeepingHoursTotal = BigDecimal.ZERO;
                    }
                    totalHousekeepingHoursTotal = totalHousekeepingHoursTotal.add(item.getTotalHousekeepingHours());
                }
            }

            // Average Hours = Total Housekeeping Hours(nonNull) / rooms rented(nonNull)
            List<HkEfficiencyDTO> effectiveList = hkEfficiencyDTOList.stream()
                    .filter(item -> Objects.nonNull(item.getRoomsRented()) && Objects.nonNull(item.getTotalHousekeepingHours()))
                    .toList();

            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            BigDecimal averageHours;
            BigDecimal averageMinutes;
            if (CollectionUtils.isEmpty(effectiveList)) {
                averageHours = null;
                averageMinutes = null;
            } else {
                int roomsRentedCount = effectiveList.stream().map(HkEfficiencyDTO::getRoomsRented)
                        .reduce(Integer::sum).orElse(BaseConstant.ZERO);
                BigDecimal totalHousekeepingHoursCount = effectiveList.stream().map(HkEfficiencyDTO::getTotalHousekeepingHours)
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                averageHours = roomsRentedCount == 0 ? BigDecimal.ZERO :
                        totalHousekeepingHoursCount.divide(new BigDecimal(Integer.toString(roomsRentedCount)), BaseConstant.DATA_PRECISION, roundingMode);

                averageMinutes = roomsRentedCount == 0 ? BigDecimal.ZERO :
                        totalHousekeepingHoursCount.multiply(new BigDecimal("60"))
                                .divide(new BigDecimal(Integer.toString(roomsRentedCount)), BaseConstant.DATA_PRECISION, roundingMode);
            }
            return HkEfficiencyTotalDTO.builder()
                    .averageHours(averageHours)
                    .averageMinutes(averageMinutes)
                    .roomsRentedTotal(roomsRentedTotal)
                    .hoursWorkedTotal(hoursWorkedTotal)
                    .retroTotal(retroTotal)
                    .hoursHousekeepingTotal(hoursHousekeepingTotal)
                    .tempHoursTotal(tempHoursTotal)
                    .adjustHoursTotal(adjustHoursTotal)
                    .totalHousekeepingHoursTotal(totalHousekeepingHoursTotal)
                    .build();
        }

        /**
         * 根据日期范围获取数据
         *
         * @param propertyId 资产 id
         * @param startDate  开始时间戳
         * @param endDate    结束时间戳
         * @return HkEfficiency 集合
         * @author zc
         * @date 2024/7/23
         */
        @Override
        public List<HkEfficiency> listByDateRange(int propertyId, long startDate, long endDate) {
            return list(new LambdaQueryWrapper<HkEfficiency>()
                    .between(HkEfficiency::getTheDate, startDate, endDate)
                    .eq(HkEfficiency::getPropertyId, propertyId));
        }

        /**
         * 根据月开始日范围获取数据
         *
         * @param propertyIds             资产 id集合
         * @param startDateByStartOfMonth 月开始日-开始时间戳
         * @param endDateByStartOfMonth   月开始日-结束时间戳
         * @return HkEfficiency 集合
         * @author lichen
         * @date 2025/11/25
         */
        @Override
        public List<HkEfficiency> listByStartOfMonthRange(List<Integer> propertyIds, long startDateByStartOfMonth, long endDateByStartOfMonth) {
            if (CollectionUtils.isEmpty(propertyIds)) return Collections.emptyList();
            return list(new LambdaQueryWrapper<HkEfficiency>()
                    .between(HkEfficiency::getStartOfMonth, startDateByStartOfMonth, endDateByStartOfMonth)
                    .in(HkEfficiency::getPropertyId, propertyIds));
        }

        /**
         * 根据日期范围获取数据
         *
         * @param propertyIds 资产 id集合
         * @param startDate   开始时间戳
         * @param endDate     结束时间戳
         * @return HkEfficiency 集合
         * @author lichen
         * @date 2025/11/25
         */
        @Override
        public List<HkEfficiency> listByDateRange(List<Integer> propertyIds, long startDate, long endDate) {
            if (CollectionUtils.isEmpty(propertyIds)) return Collections.emptyList();
            return list(new LambdaQueryWrapper<HkEfficiency>()
                    .between(HkEfficiency::getTheDate, startDate, endDate)
                    .in(HkEfficiency::getPropertyId, propertyIds));
        }

        /**
         * 获取缓存key集合
         *
         * @param propertyList         资产
         * @param propertyId2DateRange 报告数据日期范围(yyyyMMdd)
         * @param reportType           报告类型
         * @return java.util.List<java.lang.String>
         * @author lichen
         * @date 2025/11/25
         */
        private List<String> getRedisKeys(List<PropertyDTO> propertyList, Map<Integer, List<Long>> propertyId2DateRange, int reportType) {
            ArrayList<String> keys = new ArrayList<>();
            propertyList.forEach(propertyDTO -> {
                List<Long> dateRangeList = propertyId2DateRange.getOrDefault(propertyDTO.getId(), Collections.emptyList());
                if (CollectionUtils.isEmpty(dateRangeList)) return;

                LocalDate startLocalDate = DateTimeUtils.strToLocalDate(dateRangeList.get(0).toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                if(HkEfficiencyDTO.REPORT_TYPE_ACCOUNTING == reportType){
                    // 日期 + 1 天，因为当天售出的房间是在第二天打扫
                    startLocalDate = startLocalDate.plusDays(1);
                }

                LocalDate endLocalDate = DateTimeUtils.strToLocalDate(dateRangeList.get(1).toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                if(HkEfficiencyDTO.REPORT_TYPE_ACCOUNTING == reportType){
                    // 日期 + 1 天，因为当天售出的房间是在第二天打扫
                    endLocalDate = endLocalDate.plusDays(1);
                }

                // 获取开始日期到结束日期的每一天
                List<LocalDate> dates = startLocalDate.datesUntil(endLocalDate.plusDays(1)).toList();

                // 生成锁key
                List<String> keyByPropertyId = dates.stream().map(date -> {
                    String dateStr = DateTimeUtils.localDateToStr(date, BaseConstant.DATE_FORMAT_YYYYMMDD);
                    return MessageFormat.format(HkEfficiencyConstant.HANDLE_HK_EFFICIENCY_KEY, propertyDTO.getId(), dateStr);
                }).toList();

                keys.addAll(keyByPropertyId);
            });
            return keys;
        }

    }