# HotelKpiWeeklyServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.workperformance.cons.hotelkpi.*;
    import cloud.wconnect.workperformance.entity.hotelkpi.*;
    import cloud.wconnect.workperformance.mapper.hotelkpi.HotelKpiWeeklyMapper;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfigv2.HotelKpiV2FactorConfigDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.internal.crawl.value.v2.HotelKpiV2CrawlValueCategoryDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.internal.crawl.value.v2.HotelKpiV2CrawlValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.*;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.explain.*;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.save.HotelKpiMetricsSaveDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.*;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.save.HotelKpiV2MetricsSaveCategoryDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.save.HotelKpiV2MetricsSaveDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpiv2.HotelKpiV2OriginDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.actual.HotelKpiMetricsSettingActualDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.target.HotelKpiMetricsSettingTargetDTO;
    import cloud.wconnect.workperformance.pojo.query.hotelkpi.HotelKpiQuery;
    import cloud.wconnect.workperformance.service.hotelkpi.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Resource;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.DayOfWeek;
    import java.time.LocalDate;
    import java.time.temporal.TemporalAdjusters;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * Hotel KPI Weekly 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-06-24
     */
    @Slf4j
    @Service
    @DS("work_performance")
    public class HotelKpiWeeklyServiceImpl extends ServiceImpl<HotelKpiWeeklyMapper, HotelKpiWeekly> implements IHotelKpiWeeklyService {

        @Resource
        private IHotelKpiService hotelKpiService;
        @Resource
        private IHotelKpiDailyService dailyService;
        @Resource
        private IHotelKpiMonthlyService monthlyService;
        @Resource
        private IHotelKpiMetricsSettingTargetService metricsSettingTargetService;
        @Resource
        private IHotelKpiMetricsSettingActualService metricsSettingActualService;
        @Resource
        private IHotelKpiMetricsConfigService metricsConfigService;
        @Resource
        private IHotelKpiMetricsSettingService metricsSettingService;
        @Resource
        private IHotelKpiWarningCommentService warningCommentService;
        @Resource
        private IHotelKpiCommonSummaryService commonSummaryService;
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        private BeanMapper beanMapper;

        private static final Integer DATE_TYPE = HotelKpiDateTypeCons.WEEKLY;

        /**
         * 根据唯一索引保存数据
         *
         * @param hotelKpiWeekly 数据
         * @author zc
         * @date 2024/9/11
         */
        @Override
        public void insertOrUpdate(HotelKpiWeekly hotelKpiWeekly) {
            if (Objects.isNull(hotelKpiWeekly)) {
                return;
            }
            try {
                baseMapper.insertOrUpdateByUnique(hotelKpiWeekly);
            } catch (Exception e) {
                log.error("Failed to insertOrUpdate for HotelKpiWeekly, data: {}, error: {}",
                        JsonUtils.convertObjToString(objectMapper, hotelKpiWeekly), e);
            }
        }

        /**
         * 根据唯一索引批量保存数据
         *
         * @param hotelKpiWeeklyList 数据
         * @author zc
         * @date 2024/9/11
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void insertOrUpdateBatch(List<HotelKpiWeekly> hotelKpiWeeklyList) {
            if (CollectionUtils.isEmpty(hotelKpiWeeklyList)) {
                return;
            }
            try {
                // 按唯一索引字段排序，避免死锁
                hotelKpiWeeklyList.sort(Comparator.comparing(HotelKpiWeekly::getPropertyId)
                        .thenComparing(HotelKpiWeekly::getWeek)
                        .thenComparing(HotelKpiWeekly::getMetricsType));

                baseMapper.insertOrUpdateBatchByUnique(hotelKpiWeeklyList);
            } catch (Exception e) {
                log.error("Failed to insertOrUpdateBatchByUnique for HotelKpiWeekly, data: {}, error: {}",
                        JsonUtils.convertObjListToString(objectMapper, hotelKpiWeeklyList), e);
            }
        }

        /**
         * 根据日期范围、指标类型集合 获取 KPI 原始数据
         *
         * @param propertyId   资产 id
         * @param startDate    开始日期(yyyyMMdd)
         * @param endDate      结束日期(yyyyMMdd)
         * @param metricsTypes 指标类型集合
         * @return HotelKpiWeekly 集合
         * @author zc
         * @date 2024/6/25
         */
        @Override
        public List<HotelKpiWeekly> listByDateRangeAndMetricsTypes(int propertyId, long startDate, long endDate, List<Integer> metricsTypes) {
            if (CollectionUtils.isEmpty(metricsTypes)) {
                return Collections.emptyList();
            }
            return list(new LambdaQueryWrapper<HotelKpiWeekly>()
                    .eq(HotelKpiWeekly::getPropertyId, propertyId)
                    .between(HotelKpiWeekly::getWeek, startDate, endDate)
                    .in(HotelKpiWeekly::getMetricsType, metricsTypes));
        }

        /**
         * 根据日期范围、指标类型 获取 KPI 原始数据
         *
         * @param propertyId  资产 id
         * @param startDate   开始日期(yyyyMMdd)
         * @param endDate     结束日期(yyyyMMdd)
         * @param metricsType 指标类型
         * @return HotelKpiWeekly 集合
         * @author zc
         * @date 2025/8/24
         */
        @Override
        public List<HotelKpiWeekly> listByDateRangeAndMetricsType(int propertyId, long startDate, long endDate, int metricsType) {
            return list(new LambdaQueryWrapper<HotelKpiWeekly>()
                    .eq(HotelKpiWeekly::getPropertyId, propertyId)
                    .between(HotelKpiWeekly::getWeek, startDate, endDate)
                    .eq(HotelKpiWeekly::getMetricsType, metricsType));
        }

        /**
         * 根据日期、指标类型集合 获取 KPI 原始数据
         *
         * @param propertyId   资产 id
         * @param date         日期(yyyyMMdd)
         * @param metricsTypes 指标类型集合
         * @return HotelKpiWeekly 集合
         * @author zc
         * @date 2024/8/8
         */
        @Override
        public List<HotelKpiWeekly> listByDateAndMetricsTypes(int propertyId, long date, List<Integer> metricsTypes) {
            if (CollectionUtils.isEmpty(metricsTypes)) {
                return Collections.emptyList();
            }
            return list(new LambdaQueryWrapper<HotelKpiWeekly>()
                    .eq(HotelKpiWeekly::getPropertyId, propertyId)
                    .eq(HotelKpiWeekly::getWeek, date)
                    .in(HotelKpiWeekly::getMetricsType, metricsTypes));
        }

        /**
         * 根据日期、指标类型 获取 KPI 原始数据
         *
         * @param propertyId  资产 id
         * @param date        日期(yyyyMMdd)
         * @param metricsType 指标类型
         * @return HotelKpiWeekly
         * @author zc
         * @date 2024/8/6
         */
        @Override
        public HotelKpiWeekly getByDateAndMetricsType(int propertyId, long date, int metricsType) {
            return getOne(new LambdaQueryWrapper<HotelKpiWeekly>()
                    .eq(HotelKpiWeekly::getPropertyId, propertyId)
                    .eq(HotelKpiWeekly::getWeek, date)
                    .eq(HotelKpiWeekly::getMetricsType, metricsType));
        }

        /**
         * 获取 Hotel KPI 数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return Hotel KPI 数据
         * @author zc
         * @date 2024/6/25
         */
        @Override
        public List<HotelKpiDTO> listHotelKpi(PropertyDTO propertyDTO, HotelKpiQuery query) {
            // 获取数据筛选范围
            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(query.getStartDate()), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(query.getEndDate()), BaseConstant.DATE_FORMAT_YYYYMMDD);

            // 获取周起始日
            DayOfWeek dayOfWeek = startLocalDate.getDayOfWeek();

            // 获取日期：当前年第一个周到当前周的后两周的数据用于计算【递减剩余】类型的指标数值
            LocalDate firstWeekLocalDateInYear = startLocalDate.with(TemporalAdjusters.firstDayOfYear()).with(TemporalAdjusters.nextOrSame(dayOfWeek));
            long firstWeekInYear = Long.parseLong(DateTimeUtils.localDateToStr(firstWeekLocalDateInYear, BaseConstant.DATE_FORMAT_YYYYMMDD));
            LocalDate nextLocalDate = endLocalDate.plusWeeks(2);
            long nextWeek = Long.parseLong(DateTimeUtils.localDateToStr(nextLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));

            // 获取有效的指标配置数据
            List<HotelKpiMetricsConfig> effectiveMetrics = metricsConfigService.listEffectiveMetricsOfYear(propertyDTO.getId(),
                    query.getDateType(), query.getStartDate(), query.getEndDate(), Boolean.TRUE, HotelKpiCons.V1);
            Map<Integer, List<HotelKpiMetricsConfig>> metricsParentId2Configs = effectiveMetrics.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsConfig::getParentId));

            // 提取指标类型集合
            List<Integer> metricsTypes = effectiveMetrics.stream().map(HotelKpiMetricsConfig::getMetricsType)
                    .filter(metricsType -> metricsType > 0).toList();

            // 获取 Weekly KPI 原始数据
            List<HotelKpiMetricsDTO> metricsDTOList = listKpiMetricsDTOList(propertyDTO.getId(), firstWeekInYear, nextWeek, metricsTypes);
            // 获取 Monthly KPI 原始数据，用于计算【递减剩余】类型的指标数值
            List<HotelKpiMetricsDTO> metricsDTOMonthlyList = listKpiMetricsDTOMonthlyList(propertyDTO.getId(), startLocalDate, nextLocalDate, metricsTypes);

            // 组装 Metrics Setting
            packageSettingValue(propertyDTO.getId(), metricsDTOList, metricsTypes, firstWeekInYear, nextWeek, dayOfWeek, metricsDTOMonthlyList);

            // 组装 Monthly Metrics Setting
            packageSettingValueForMonthly(propertyDTO.getId(), startLocalDate, nextLocalDate, metricsTypes, dayOfWeek, metricsDTOMonthlyList);

            // 获取操作人姓名
            Map<Integer, String> userId2Name = hotelKpiService.extractOperatorName(propertyDTO.getTenantId(), metricsDTOList);

            Map<Long, Map<Integer, HotelKpiMetricsDTO>> metricsType2KpiMetricsDTOByDate = metricsDTOList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsDTO::getDate,
                            Collectors.toMap(HotelKpiMetricsDTO::getMetricsType, Function.identity())));

            Map<Integer, List<HotelKpiMetricsDTO>> metricsType2MetricsDTOList = metricsDTOList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsDTO::getMetricsType));

            Map<Integer, List<HotelKpiMetricsDTO>> metricsType2MonthMetricsDTOList = metricsDTOMonthlyList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsDTO::getMetricsType));

            // 获取指定日期警示备注数量
            Map<Long, Map<Integer, Map<Integer, Long>>> metricsType2WarningCountByDate = warningCommentService.listByDateRangeAndMetricsTypes(propertyDTO.getId(),
                            query.getDateType(), query.getStartDate(), query.getEndDate(), metricsTypes).stream()
                    .collect(Collectors.groupingBy(HotelKpiWarningComment::getDate,
                            Collectors.groupingBy(HotelKpiWarningComment::getMetricsType,
                                    Collectors.groupingBy(HotelKpiWarningComment::getCategory, Collectors.counting()))));

            // 获取指标每日数据
            Map<Integer, List<HotelKpiDaily>> metricsType2DailyList = listDailyByDateRange(propertyDTO.getId(), metricsTypes,
                    startLocalDate.withDayOfMonth(1), endLocalDate).stream().collect(Collectors.groupingBy(HotelKpiDaily::getMetricsType));

            List<HotelKpiDTO> hotelKpiDTOList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = indexLocalDate.plusDays(7)) {
                // 获取当前周期的 KPI 原始数据
                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                Map<Integer, HotelKpiMetricsDTO> metricsType2KpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(indexDate, Collections.emptyMap());

                // 获取上一周期的 KPI 原始数据
                long lastIndexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate.minusDays(7), BaseConstant.DATE_FORMAT_YYYYMMDD));
                Map<Integer, HotelKpiMetricsDTO> metricsType2LastKpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(lastIndexDate, Collections.emptyMap());

                Map<Integer, Map<Integer, Long>> metricsType2WarningCount = metricsType2WarningCountByDate.getOrDefault(indexDate, Collections.emptyMap());

                // 生成 KPI 详情数据
                List<HotelKpiDetailDTO> kpiDetailDTOList = hotelKpiService.generateKpiDetailDTOList(propertyDTO, userId2Name, effectiveMetrics,
                        metricsParentId2Configs, metricsType2KpiMetricsDTO, metricsType2LastKpiMetricsDTO, metricsType2WarningCount,
                        indexDate, lastIndexDate, DATE_TYPE);

                // 组装指标数据日期范围
                packageDataRange(kpiDetailDTOList);

                // 组装当前周期的计算因子详情数据
                packageCalculationFactor(propertyDTO, kpiDetailDTOList, indexLocalDate, metricsType2MetricsDTOList,
                        metricsType2MonthMetricsDTOList, metricsType2DailyList);

                hotelKpiDTOList.add(HotelKpiDTO.builder()
                        .date(indexDate)
                        .details(kpiDetailDTOList)
                        .build());
            }
            return hotelKpiDTOList;
        }

        /**
         * 获取月 KPI 原始数据
         *
         * @param propertyId     资产 id
         * @param startLocalDate 开始日期
         * @param nextLocalDate  结束日期
         * @param metricsTypes   指标类型集合
         * @return HotelKpiMetricsDTO 集合
         * @author zc
         * @date 2025/3/6
         */
        private List<HotelKpiMetricsDTO> listKpiMetricsDTOMonthlyList(int propertyId, LocalDate startLocalDate, LocalDate nextLocalDate,
                                                                      List<Integer> metricsTypes) {
            long startMonth = Long.parseLong(DateTimeUtils.localDateToStr(startLocalDate.withMonth(1), BaseConstant.DATE_FORMAT_YYYYMM));
            long endMonth = Long.parseLong(DateTimeUtils.localDateToStr(nextLocalDate.plusDays(6), BaseConstant.DATE_FORMAT_YYYYMM));
            return monthlyService.listKpiMetricsDTOList(propertyId, startMonth, endMonth, metricsTypes);
        }

        /**
         * 组装 Monthly Metrics Setting
         *
         * <p>组装范围：查询开始日期的月份 ~ 查询结束日期后两周的最后一天的月份</p>
         *
         * @param propertyId            资产 id
         * @param startLocalDate        开始日期
         * @param nextLocalDate         结束日期
         * @param metricsTypes          指标类型集合
         * @param dayOfWeek             周几
         * @param metricsDTOMonthlyList Monthly KPI 原始数据
         * @author zc
         * @date 2025/3/26
         */
        private void packageSettingValueForMonthly(int propertyId, LocalDate startLocalDate, LocalDate nextLocalDate,
                                                   List<Integer> metricsTypes, DayOfWeek dayOfWeek,
                                                   List<HotelKpiMetricsDTO> metricsDTOMonthlyList) {
            long startMonth = Long.parseLong(DateTimeUtils.localDateToStr(startLocalDate, BaseConstant.DATE_FORMAT_YYYYMM));
            long endMonth = Long.parseLong(DateTimeUtils.localDateToStr(nextLocalDate.plusDays(6), BaseConstant.DATE_FORMAT_YYYYMM));
            monthlyService.packageSettingValue(propertyId, metricsDTOMonthlyList, metricsTypes, startMonth, endMonth, dayOfWeek);
        }

        /**
         * 获取指定日期范围内的每日指标数据
         *
         * @param propertyId     酒店ID
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @return 指标数据
         * @author zc
         * @date 2025/3/12
         */
        private List<HotelKpiDaily> listDailyByDateRange(int propertyId, List<Integer> metricsTypes, LocalDate startLocalDate, LocalDate endLocalDate) {
            long startDate = Long.parseLong(DateTimeUtils.localDateToStr(startLocalDate.minusDays(7), BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDate = Long.parseLong(DateTimeUtils.localDateToStr(endLocalDate.plusDays(6), BaseConstant.DATE_FORMAT_YYYYMMDD));
            return dailyService.listByDateRange(propertyId, metricsTypes, startDate, endDate);
        }

        /**
         * 组装指标数据日期范围
         *
         * @param kpiDetailDTOList 指标数据
         * @author zc
         * @date 2025/3/12
         */
        private void packageDataRange(List<HotelKpiDetailDTO> kpiDetailDTOList) {
            for (HotelKpiDetailDTO kpiDetailDTO : kpiDetailDTOList) {
                for (HotelKpiItemDTO item : kpiDetailDTO.getItems()) {
                    for (HotelKpiMetricsDTO metric : item.getMetrics()) {
                        int dataRangeType = HotelKpiMetricsEnum.getDataRangeType(metric.getMetricsType());
                        metric.setDataRange(getDataRange(metric.getDate(), dataRangeType));
                    }
                }
            }
        }

        /**
         * 获取指标数据日期范围
         *
         * @param date          当前日期
         * @param dataRangeType 数据日期范围类型
         * @return HotelKpiDataRangeDTO
         * @author zc
         * @date 2025/3/12
         */
        private HotelKpiDataRangeDTO getDataRange(long date, int dataRangeType) {
            LocalDate localDate = DateTimeUtils.strToLocalDate(String.valueOf(date), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate startLocalDate;
            LocalDate endLocalDate;
            if (dataRangeType == HotelKpiDataRangeTypeCons.WTD) {
                startLocalDate = localDate;
                endLocalDate = localDate.plusDays(6);
            } else if (dataRangeType == HotelKpiDataRangeTypeCons.MTD) {
                startLocalDate = localDate.withDayOfMonth(1);
                endLocalDate = localDate.plusDays(6);
                if (endLocalDate.getMonthValue() != localDate.getMonthValue()) {
                    endLocalDate = localDate.with(TemporalAdjusters.lastDayOfMonth());
                }
            } else if (dataRangeType == HotelKpiDataRangeTypeCons.QTD) {
                startLocalDate = LocalDate.of(localDate.getYear(), localDate.getMonth().firstMonthOfQuarter(), 1);
                LocalDate quarterEndDate = startLocalDate.plusMonths(2).with(TemporalAdjusters.lastDayOfMonth());
                endLocalDate = localDate.plusDays(6);
                if (endLocalDate.isAfter(quarterEndDate)) {
                    endLocalDate = quarterEndDate;
                }
            } else if (dataRangeType == HotelKpiDataRangeTypeCons.SIX_MTD) {
                if (localDate.getMonthValue() <= 6) {
                    startLocalDate = localDate.withDayOfYear(1);
                } else {
                    startLocalDate = LocalDate.of(localDate.getYear(), 7, 1);
                }
                LocalDate sixMonthEndDate = startLocalDate.plusMonths(5).with(TemporalAdjusters.lastDayOfMonth());
                endLocalDate = localDate.plusDays(6);
                if (endLocalDate.isAfter(sixMonthEndDate)) {
                    endLocalDate = sixMonthEndDate;
                }
            } else {
                startLocalDate = localDate.withDayOfMonth(1);
                endLocalDate = localDate.with(TemporalAdjusters.lastDayOfMonth());
            }
            HotelKpiDataRangeDTO dataRangeDTO = new HotelKpiDataRangeDTO();
            dataRangeDTO.setStartDate(Long.parseLong(DateTimeUtils.localDateToStr(startLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD)));
            dataRangeDTO.setEndDate(Long.parseLong(DateTimeUtils.localDateToStr(endLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD)));
            return dataRangeDTO;
        }

        /**
         * 组装当前周期的计算因子详情数据
         *
         * @param propertyDTO                     资产
         * @param kpiDetailDTOList                待组装的 KPI 详情数据
         * @param indexLocalDate                  日期
         * @param metricsType2MetricsDTOList      包含历史和未来的 KPI 数据
         * @param metricsType2MonthMetricsDTOList 包含历史和未来的 Monthly KPI 数据
         * @param metricsType2DailyList           包含历史的每日 KPI 数据
         * @author zc
         * @date 2025/3/6
         */
        private void packageCalculationFactor(PropertyDTO propertyDTO, List<HotelKpiDetailDTO> kpiDetailDTOList, LocalDate indexLocalDate,
                                              Map<Integer, List<HotelKpiMetricsDTO>> metricsType2MetricsDTOList,
                                              Map<Integer, List<HotelKpiMetricsDTO>> metricsType2MonthMetricsDTOList,
                                              Map<Integer, List<HotelKpiDaily>> metricsType2DailyList) {
            for (HotelKpiDetailDTO kpiDetailDTO : kpiDetailDTOList) {
                for (HotelKpiItemDTO item : kpiDetailDTO.getItems()) {
                    for (HotelKpiMetricsDTO metric : item.getMetrics()) {
                        List<HotelKpiMetricsDTO> weeklyMetricsDTOList = metricsType2MetricsDTOList.get(metric.getMetricsType());
                        if (CollectionUtils.isEmpty(weeklyMetricsDTOList)) {
                            continue;
                        }
                        List<HotelKpiMetricsDTO> monthlyMetricsDTOList = metricsType2MonthMetricsDTOList.getOrDefault(metric.getMetricsType(),
                                Collections.emptyList());
                        List<HotelKpiDaily> kpiDailyList = metricsType2DailyList.getOrDefault(metric.getMetricsType(),
                                Collections.emptyList());
                        // 生成 Actual 计算因子详情数据，Target 没有递减剩余规则不用处理
                        Map<Long, HotelKpiValueDTO> week2ActualValueDTO = weeklyMetricsDTOList.stream()
                                .collect(Collectors.toMap(HotelKpiMetricsDTO::getDate, HotelKpiMetricsDTO::getActual));
                        Map<Long, HotelKpiValueDTO> month2ActualValueDTO = monthlyMetricsDTOList.stream()
                                .collect(Collectors.toMap(HotelKpiMetricsDTO::getDate, HotelKpiMetricsDTO::getActual));
                        List<HotelKpiCalculationFactorDTO> actualFactors = metric.getActual().getCalculationFactor();
                        packageCalculationFactorDetail(propertyDTO, indexLocalDate, actualFactors, week2ActualValueDTO,
                                month2ActualValueDTO, kpiDailyList, metric.getExplain().getActual());
                    }
                }
            }
        }

        /**
         * 组装计算因子详情数据
         *
         * @param propertyDTO       资产
         * @param indexLocalDate    日期
         * @param currentFactorDTOS 当前指标的计算因子
         * @param date2ValueDTO     包含历史和未来的 KPI 数据
         * @param kpiDailyList      包含历史的每日 KPI 数据
         * @param explainValueDTO   待组装DTO
         * @author zc
         * @date 2025/3/6
         */
        private void packageCalculationFactorDetail(PropertyDTO propertyDTO, LocalDate indexLocalDate, List<HotelKpiCalculationFactorDTO> currentFactorDTOS,
                                                    Map<Long, HotelKpiValueDTO> date2ValueDTO, Map<Long, HotelKpiValueDTO> month2ValueDTO,
                                                    List<HotelKpiDaily> kpiDailyList, HotelKpiExplainValueDTO explainValueDTO) {
            // 若计算因子数量不为 2，则跳过该指标
            if (currentFactorDTOS.size() != 2) {
                return;
            }
            HotelKpiCalculationFactorDTO currentFactor = currentFactorDTOS.stream()
                    .filter(factor -> factor.getIndex() == 2).findFirst().orElse(null);
            if (Objects.isNull(currentFactor)) {
                return;
            }
            // 若计算因子除数不是递减剩余，则跳过该指标
            if (!Objects.equals(currentFactor.getSettingCategory(), HotelKpiSettingCategoryEnum.DIMINISHING_SURPLUS.getCategory())) {
                return;
            }
            // 计算当前周期完成度
            explainValueDTO.setCompletionRateInMonth(generateCompletionRateInMonth(propertyDTO, indexLocalDate, date2ValueDTO));
            // 计算整个周期完成度
            explainValueDTO.setCompletionRate(generateCompletionRate(propertyDTO, indexLocalDate, explainValueDTO.getCompletionRateInMonth(), month2ValueDTO));
            // 生成当前周期计算因子详情数据
            explainValueDTO.setCalculationFactorDetail(generateCalculationFactorValue(indexLocalDate, date2ValueDTO, kpiDailyList));
            // 生成下一周期计算因子详情数据
            explainValueDTO.setCalculationFactorDetailNext(generateCalculationFactorValueNext(indexLocalDate, date2ValueDTO, kpiDailyList));
        }

        /**
         * 格式化负数数值
         *
         * @param value 数值
         * @return java.math.BigDecimal
         * @author zc
         * @date 2025/3/25
         */
        private BigDecimal formatNegativeValue(BigDecimal value) {
            if (Objects.nonNull(value)) {
                if (value.compareTo(BigDecimal.ZERO) < 0) {
                    // 若值小于 0，则置为 0
                    value = BigDecimal.ZERO;
                }
            }
            return value;
        }

        /**
         * 计算整个周期完成度
         *
         * @param propertyDTO           资产
         * @param indexLocalDate        日期
         * @param completionRateInMonth 当前周期完成度
         * @param month2ValueDTO        包含历史和未来的 Month KPI 数据
         * @return HotelKpiCompletionRateDTO
         * @author zc
         * @date 2025/3/25
         */
        private HotelKpiCompletionRateDTO generateCompletionRate(PropertyDTO propertyDTO, LocalDate indexLocalDate, HotelKpiCompletionRateDTO completionRateInMonth,
                                                                 Map<Long, HotelKpiValueDTO> month2ValueDTO) {
            long currentDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMM));
            HotelKpiCalculationFactorDTO denominatorFactorDTO = month2ValueDTO.getOrDefault(currentDate, new HotelKpiValueDTO()).getCalculationFactor().stream()
                    .filter(factor -> factor.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO());

            // 获取截至到当前月已完成量
            BigDecimal currentCompleted = null;
            // 获取截至到当前月的日期
            List<Long> preDateList = denominatorFactorDTO.getStageValue().stream()
                    .map(HotelKpiStageValueDTO::getDate).sorted().toList();
            for (long indexDate : preDateList) {
                // 累加历史月份的完成量（不包含当前月）
                if (indexDate >= currentDate) {
                    break;
                }
                HotelKpiValueDTO indexValueDTO = month2ValueDTO.getOrDefault(indexDate, new HotelKpiValueDTO());
                HotelKpiCalculationFactorDTO indexMoleculeFactorDTO = indexValueDTO.getCalculationFactor().stream()
                        // 取 index = 1，实际完成的数值
                        .filter(factor -> factor.getIndex() == 1).findFirst().orElse(new HotelKpiCalculationFactorDTO());
                BigDecimal currentValue = indexMoleculeFactorDTO.getCrawl();
                if (Objects.isNull(currentValue)) {
                    continue;
                }
                if (Objects.isNull(currentCompleted)) {
                    currentCompleted = BigDecimal.ZERO;
                }
                currentCompleted = currentCompleted.add(currentValue);
            }

            // 累加当前月已完成量
            BigDecimal completedInMonth = completionRateInMonth.getCompleted();
            if (Objects.nonNull(completedInMonth)) {
                if (Objects.nonNull(currentCompleted)) {
                    currentCompleted = currentCompleted.add(completedInMonth);
                } else {
                    currentCompleted = completedInMonth;
                }
            }
            HotelKpiCompletionRateDTO completionRateDTO = new HotelKpiCompletionRateDTO();
            completionRateDTO.setStartDate(Collections.min(preDateList));
            completionRateDTO.setEndDate(Collections.max(preDateList));
            completionRateDTO.setCompleted(currentCompleted);

            // 当前周期总数量
            BigDecimal currentTotal = formatNegativeValue(denominatorFactorDTO.getStageTotalValue());
            completionRateDTO.setTotal(currentTotal);

            // 当前周期完成率
            BigDecimal currentCompletionRate = null;
            if (Objects.nonNull(currentCompleted) && Objects.nonNull(currentTotal)) {
                if (currentTotal.compareTo(BigDecimal.ZERO) != 0) {
                    RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                            .getInternationalization().getNumber().getCalculationRule());
                    currentCompletionRate = currentCompleted.multiply(new BigDecimal("100"))
                            .divide(currentTotal, BaseConstant.DATA_PRECISION, roundingMode);
                } else {
                    currentCompletionRate = new BigDecimal("100");
                }
            }
            completionRateDTO.setCompletionRate(currentCompletionRate);
            return completionRateDTO;
        }

        /**
         * 生成当前周期完成度
         *
         * @param propertyDTO    资产
         * @param indexLocalDate 日期
         * @param date2ValueDTO  包含历史和未来的 KPI 数据
         * @return HotelKpiCompletionRateDTO
         * @author zc
         * @date 2025/3/6
         */
        private HotelKpiCompletionRateDTO generateCompletionRateInMonth(PropertyDTO propertyDTO, LocalDate indexLocalDate, Map<Long, HotelKpiValueDTO> date2ValueDTO) {
            long currentDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            HotelKpiCalculationFactorDTO denominatorFactorDTO = date2ValueDTO.getOrDefault(currentDate, new HotelKpiValueDTO()).getCalculationFactor().stream()
                    .filter(factor -> factor.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO());

            // 当前周期已完成量
            BigDecimal currentCompleted = null;
            // 取当前周期之前的数据
            List<Long> preDateList = denominatorFactorDTO.getStageValue().stream()
                    .map(HotelKpiStageValueDTO::getDate).filter(item ->
                            item <= currentDate).sorted(Comparator.reverseOrder()).toList();
            for (long indexDate : preDateList) {
                HotelKpiValueDTO indexValueDTO = date2ValueDTO.getOrDefault(indexDate, new HotelKpiValueDTO());
                HotelKpiCalculationFactorDTO indexMoleculeFactorDTO = indexValueDTO.getCalculationFactor().stream()
                        // 取 index = 1，实际完成的数值
                        .filter(factor -> factor.getIndex() == 1).findFirst().orElse(new HotelKpiCalculationFactorDTO());
                BigDecimal currentValue = indexMoleculeFactorDTO.getCrawl();
                if (Objects.isNull(currentValue)) {
                    continue;
                }
                currentCompleted = currentValue;
                break;
            }
            HotelKpiCompletionRateDTO completionRateDTO = new HotelKpiCompletionRateDTO();
            completionRateDTO.setStartDate(Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate.withDayOfMonth(1),
                    BaseConstant.DATE_FORMAT_YYYYMMDD)));
            completionRateDTO.setEndDate(Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate.with(TemporalAdjusters.lastDayOfMonth()),
                    BaseConstant.DATE_FORMAT_YYYYMMDD)));
            completionRateDTO.setCompleted(currentCompleted);

            // 当前周期总量
            BigDecimal currentTotal = formatNegativeValue(denominatorFactorDTO.getStageTotalValue());
            completionRateDTO.setTotal(currentTotal);

            // 当前周期完成率
            BigDecimal currentCompletionRate = null;
            if (Objects.nonNull(currentCompleted) && Objects.nonNull(currentTotal)) {
                if (currentTotal.compareTo(BigDecimal.ZERO) != 0) {
                    RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                            .getInternationalization().getNumber().getCalculationRule());
                    currentCompletionRate = currentCompleted.multiply(new BigDecimal("100"))
                            .divide(currentTotal, BaseConstant.DATA_PRECISION, roundingMode);
                } else {
                    currentCompletionRate = new BigDecimal("100");
                }
            }
            completionRateDTO.setCompletionRate(currentCompletionRate);
            return completionRateDTO;
        }

        /**
         * 生成计算因子详情数据
         *
         * @param localDate     日期
         * @param date2ValueDTO 包含历史和未来的 KPI 数据
         * @param kpiDailyList  包含历史的每日 KPI 数据
         * @return Actual 计算因子详情数据
         * @author zc
         * @date 2025/3/24
         */
        private List<HotelKpiCalculationFactorDetailDTO> generateCalculationFactorValue(LocalDate localDate, Map<Long, HotelKpiValueDTO> date2ValueDTO,
                                                                                        List<HotelKpiDaily> kpiDailyList) {
            int weekIndex = 1;
            List<HotelKpiCalculationFactorDetailDTO> factorDetailDTOList = Lists.newArrayList();
            LocalDate firstWeekStartDateInMonth = localDate.withDayOfMonth(1).with(TemporalAdjusters.nextOrSame(localDate.getDayOfWeek()));
            for (LocalDate indexStartLocalDate = firstWeekStartDateInMonth; !indexStartLocalDate.isAfter(localDate); indexStartLocalDate = indexStartLocalDate.plusWeeks(1)) {
                // 生成当前周期的开始、结束日期
                long currentStartDate = Long.parseLong(DateTimeUtils.localDateToStr(indexStartLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                LocalDate endLocalDate = indexStartLocalDate.plusDays(6);
                if (indexStartLocalDate.getMonthValue() != endLocalDate.getMonthValue()) {
                    endLocalDate = indexStartLocalDate.with(TemporalAdjusters.lastDayOfMonth());
                }

                HotelKpiValueDTO currentValueDTO = date2ValueDTO.getOrDefault(currentStartDate, new HotelKpiValueDTO());
                // 获取当前周期的计算因子值 - 被除数（分子）
                HotelKpiCalculationFactorDTO currentMoleculeFactorDTO = currentValueDTO.getCalculationFactor().stream()
                        .filter(item -> item.getIndex() == 1).findFirst().orElse(new HotelKpiCalculationFactorDTO());
                BigDecimal currentMtdMoleculeValue = formatNegativeValue(currentMoleculeFactorDTO.getCrawl());
                // 获取当前周期的计算因子值 - 除数（分母）
                HotelKpiCalculationFactorDTO currentDenominatorFactorDTO = currentValueDTO.getCalculationFactor().stream()
                        .filter(item -> item.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO());
                BigDecimal currentMtdDenominatorValue = formatNegativeValue(currentDenominatorFactorDTO.getCrawl());

                // 获取上周期的计算因子值 - 被除数（分子）
                BigDecimal lastMtdMoleculeValue;
                // 获取上周期的计算因子值 - 除数（分母）
                BigDecimal lastMtdDenominatorValue = null;
                // 若上周期与当前周期不在同一个月，则需要组装 Week0
                LocalDate lastStartLocalDate = indexStartLocalDate.minusDays(7);
                if (lastStartLocalDate.getMonthValue() != indexStartLocalDate.getMonthValue()) {
                    long startDateWeek0 = Long.parseLong(DateTimeUtils.localDateToStr(indexStartLocalDate.withDayOfMonth(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
                    LocalDate endLocalDateWeek0 = indexStartLocalDate.minusDays(1);
                    long endDateWeek0 = Long.parseLong(DateTimeUtils.localDateToStr(endLocalDateWeek0, BaseConstant.DATE_FORMAT_YYYYMMDD));
                    lastMtdMoleculeValue = kpiDailyList.stream().filter(item -> item.getDate() >= startDateWeek0 && item.getDate() <= endDateWeek0)
                            .map(HotelKpiDaily::getValue).filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);

                    HotelKpiCalculationFactorDetailValueDTO week0Molecule = new HotelKpiCalculationFactorDetailValueDTO();
                    week0Molecule.setIndex(1);
                    week0Molecule.setValue(lastMtdMoleculeValue);
                    week0Molecule.setWeekValue(lastMtdMoleculeValue);

                    // 计算出 Week0 的占比数值
                    if (Objects.nonNull(currentMtdDenominatorValue)) {
                        lastMtdDenominatorValue = currentMtdDenominatorValue.multiply(new BigDecimal(String.valueOf(endLocalDateWeek0.getDayOfMonth())))
                                // 由于 Week0 天数 小于 7，所以这里使用向下取整
                                .divide(new BigDecimal(String.valueOf(indexStartLocalDate.plusDays(6).getDayOfMonth())), 0, RoundingMode.DOWN);
                    }

                    HotelKpiCalculationFactorDetailValueDTO week0Denominator = new HotelKpiCalculationFactorDetailValueDTO();
                    week0Denominator.setIndex(2);
                    week0Denominator.setValue(lastMtdDenominatorValue);
                    week0Denominator.setWeekValue(lastMtdDenominatorValue);

                    HotelKpiCalculationFactorDetailDTO week0Factor = new HotelKpiCalculationFactorDetailDTO();
                    week0Factor.setIndex(weekIndex - 1);
                    week0Factor.setStartDate(startDateWeek0);
                    week0Factor.setEndDate(endDateWeek0);
                    week0Factor.setCalculationFactor(Lists.newArrayList(week0Molecule, week0Denominator));

                    factorDetailDTOList.add(week0Factor);
                } else {
                    long lastStartDate = Long.parseLong(DateTimeUtils.localDateToStr(lastStartLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                    HotelKpiValueDTO lastValueDTO = date2ValueDTO.getOrDefault(lastStartDate, new HotelKpiValueDTO());
                    HotelKpiCalculationFactorDTO lastMoleculeFactorDTO = lastValueDTO.getCalculationFactor().stream()
                            .filter(item -> item.getIndex() == 1).findFirst().orElse(new HotelKpiCalculationFactorDTO());
                    lastMtdMoleculeValue = formatNegativeValue(lastMoleculeFactorDTO.getCrawl());

                    HotelKpiCalculationFactorDTO lastDenominatorFactorDTO = lastValueDTO.getCalculationFactor().stream()
                            .filter(item -> item.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO());
                    lastMtdDenominatorValue = formatNegativeValue(lastDenominatorFactorDTO.getCrawl());
                }

                // 当前周期的计算因子值 - 被除数（分子）
                BigDecimal currentMoleculeValue = null;
                if (Objects.nonNull(currentMtdMoleculeValue)) {
                    if (Objects.isNull(lastMtdMoleculeValue)) {
                        lastMtdMoleculeValue = BigDecimal.ZERO;
                    }
                    // 由于数值是 MTD，所以需要减去前一周的数值，表示当前周期完成的数值
                    currentMoleculeValue = formatNegativeValue(currentMtdMoleculeValue.subtract(lastMtdMoleculeValue));
                }
                HotelKpiCalculationFactorDetailValueDTO currentMolecule = new HotelKpiCalculationFactorDetailValueDTO();
                currentMolecule.setIndex(1);
                currentMolecule.setValue(currentMtdMoleculeValue);
                currentMolecule.setWeekValue(currentMoleculeValue);

                // 当前周期的计算因子值 - 除数（分母）
                BigDecimal currentDenominatorValue = null;
                if (Objects.nonNull(currentMtdDenominatorValue)) {
                    if (Objects.isNull(lastMtdDenominatorValue)) {
                        lastMtdDenominatorValue = BigDecimal.ZERO;
                    }
                    // 由于数值是 MTD，所以需要减去前一周的数值，表示当前周期完成的数值
                    currentDenominatorValue = formatNegativeValue(currentMtdDenominatorValue.subtract(lastMtdDenominatorValue));
                }
                HotelKpiCalculationFactorDetailValueDTO currentDenominator = new HotelKpiCalculationFactorDetailValueDTO();
                currentDenominator.setIndex(2);
                currentDenominator.setValue(currentMtdDenominatorValue);
                currentDenominator.setWeekValue(currentDenominatorValue);

                HotelKpiCalculationFactorDetailDTO currentFactor = new HotelKpiCalculationFactorDetailDTO();
                currentFactor.setIndex(weekIndex);
                currentFactor.setStartDate(currentStartDate);
                long currentEndDate = Long.parseLong(DateTimeUtils.localDateToStr(endLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                currentFactor.setEndDate(currentEndDate);
                currentFactor.setCalculationFactor(Lists.newArrayList(currentMolecule, currentDenominator));
                factorDetailDTOList.add(currentFactor);

                // 若当前周跨月，需要组装下个周期的 Week0
                handleCurrentWeekCrossMonths(indexStartLocalDate, date2ValueDTO, kpiDailyList, weekIndex, factorDetailDTOList);

                weekIndex++;
            }
            return factorDetailDTOList;
        }

        /**
         * 处理当前周期跨月，若当前周跨月，需要组装下个周期的 Week0
         *
         * @param currentStartLocalDate 当前周期的开始日期
         * @param date2ValueDTO         包含历史和未来的 KPI 数据
         * @param kpiDailyList          包含历史的每日 KPI 数据
         * @param weekIndex             周期所在月的坐标
         * @param factorDetailDTOList   待组装集合
         * @author zc
         * @date 2025/3/26
         */
        private void handleCurrentWeekCrossMonths(LocalDate currentStartLocalDate, Map<Long, HotelKpiValueDTO> date2ValueDTO,
                                                  List<HotelKpiDaily> kpiDailyList, int weekIndex,
                                                  List<HotelKpiCalculationFactorDetailDTO> factorDetailDTOList) {
            // 若当前周跨月，需要组装下个周期的 Week0
            LocalDate currentEndLocalDate = currentStartLocalDate.plusDays(6);
            if (currentStartLocalDate.getMonthValue() == currentEndLocalDate.getMonthValue()) {
                return;
            }
            LocalDate startLocalDateWeek0 = currentEndLocalDate.withDayOfMonth(1);
            long startDateWeek0 = Long.parseLong(DateTimeUtils.localDateToStr(startLocalDateWeek0, BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDateWeek0 = Long.parseLong(DateTimeUtils.localDateToStr(currentEndLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            BigDecimal lastMolecule = kpiDailyList.stream().filter(item -> item.getDate() >= startDateWeek0 && item.getDate() <= endDateWeek0)
                    .map(HotelKpiDaily::getValue).filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);

            HotelKpiCalculationFactorDetailValueDTO week0Molecule = new HotelKpiCalculationFactorDetailValueDTO();
            week0Molecule.setIndex(1);
            week0Molecule.setValue(lastMolecule);
            week0Molecule.setWeekValue(lastMolecule);

            LocalDate nextStartLocalDate = currentStartLocalDate.plusWeeks(1);
            long nextStartDate = Long.parseLong(DateTimeUtils.localDateToStr(nextStartLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            HotelKpiValueDTO nextValueDTO = date2ValueDTO.getOrDefault(nextStartDate, new HotelKpiValueDTO());
            HotelKpiCalculationFactorDTO nextDenominatorFactorDTO = nextValueDTO.getCalculationFactor().stream()
                    .filter(item -> item.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO());
            // 下周期 MTD 除数（分母）
            BigDecimal nextMtdDenominatorValue = formatNegativeValue(nextDenominatorFactorDTO.getCrawl());

            BigDecimal nextMtdDenominatorWeek0Value = null;
            // 计算出 Week0 的占比数值
            if (Objects.nonNull(nextMtdDenominatorValue)) {
                nextMtdDenominatorWeek0Value = nextMtdDenominatorValue.multiply(new BigDecimal(String.valueOf(currentEndLocalDate.getDayOfMonth())))
                        .divide(new BigDecimal(String.valueOf(nextStartLocalDate.plusDays(6).getDayOfMonth())), 0, RoundingMode.DOWN);
            }

            HotelKpiCalculationFactorDetailValueDTO week0Denominator = new HotelKpiCalculationFactorDetailValueDTO();
            week0Denominator.setIndex(2);
            week0Denominator.setValue(nextMtdDenominatorWeek0Value);
            week0Denominator.setWeekValue(nextMtdDenominatorWeek0Value);

            HotelKpiCalculationFactorDetailDTO week0Factor = new HotelKpiCalculationFactorDetailDTO();
            week0Factor.setIndex(weekIndex);
            week0Factor.setStartDate(startDateWeek0);
            week0Factor.setEndDate(endDateWeek0);
            week0Factor.setCalculationFactor(Lists.newArrayList(week0Molecule, week0Denominator));

            factorDetailDTOList.add(week0Factor);
        }

        /**
         * 生成下周期计算因子详情数据
         *
         * @param lastStartLocalDate 日期
         * @param date2ValueDTO      包含历史和未来的 KPI 数据
         * @return HotelKpiCalculationFactorDetailNextDTO 集合
         * @author zc
         * @date 2025/3/10
         */
        private List<HotelKpiCalculationFactorDetailNextDTO> generateCalculationFactorValueNext(LocalDate lastStartLocalDate, Map<Long, HotelKpiValueDTO> date2ValueDTO,
                                                                                                List<HotelKpiDaily> kpiDailyList) {
            List<HotelKpiCalculationFactorDetailNextDTO> factorDetailDTOList = Lists.newArrayList();

            LocalDate currentStartLocalDate = lastStartLocalDate.plusWeeks(1);
            // 获取目标周期的周期索引
            int currentWeekIndex = DateTimeUtils.getWeekIndexOfMonth(currentStartLocalDate);

            // 获取目标周期的开始、结束日期（若跨月则取月底）
            long currentStartDate = Long.parseLong(DateTimeUtils.localDateToStr(currentStartLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            LocalDate currentEndLocalDate = currentStartLocalDate.plusDays(6);
            if (currentStartLocalDate.getMonthValue() != currentEndLocalDate.getMonthValue()) {
                currentEndLocalDate = currentStartLocalDate.with(TemporalAdjusters.lastDayOfMonth());
            }
            long currentEndDate = Long.parseLong(DateTimeUtils.localDateToStr(currentEndLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));

            // 若当前周期跨月，需要组装 week0
            handleNextWeekCrossMonths(currentStartLocalDate, date2ValueDTO, currentWeekIndex, factorDetailDTOList);

            HotelKpiValueDTO currentValueDTO = date2ValueDTO.getOrDefault(currentStartDate, new HotelKpiValueDTO());
            HotelKpiCalculationFactorDTO currentDenominatorFactorDTO = currentValueDTO.getCalculationFactor().stream()
                    .filter(item -> item.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO());

            // 取当前周期之前已完成的数值
            BigDecimal completionValue = null;
            // 若上周期和当前周期不在同一个月，需要去除当前月的 Week0 数值
            if (lastStartLocalDate.getMonthValue() != currentStartLocalDate.getMonthValue()) {
                long startDateWeek0 = Long.parseLong(DateTimeUtils.localDateToStr(currentStartLocalDate.withDayOfMonth(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
                long endDateWeek0 = Long.parseLong(DateTimeUtils.localDateToStr(currentStartLocalDate.minusDays(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
                completionValue = kpiDailyList.stream().filter(item -> item.getDate() >= startDateWeek0 && item.getDate() <= endDateWeek0)
                        .map(HotelKpiDaily::getValue).filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
            } else {
                List<Long> preDateList = currentDenominatorFactorDTO.getStageValue().stream().map(HotelKpiStageValueDTO::getDate)
                        .filter(item -> item < currentStartDate).sorted(Comparator.reverseOrder()).toList();
                for (long indexDate : preDateList) {
                    HotelKpiValueDTO indexValueDTO = date2ValueDTO.getOrDefault(indexDate, new HotelKpiValueDTO());
                    HotelKpiCalculationFactorDTO indexMoleculeFactorDTO = indexValueDTO.getCalculationFactor().stream()
                            // 取 index = 1，实际完成的数值
                            .filter(factor -> factor.getIndex() == 1).findFirst().orElse(new HotelKpiCalculationFactorDTO());
                    BigDecimal currentValue = formatNegativeValue(indexMoleculeFactorDTO.getCrawl());
                    if (Objects.isNull(currentValue)) {
                        continue;
                    }
                    completionValue = currentValue;
                    break;
                }
            }
            BigDecimal targetMtdDenominatorValue = formatNegativeValue(currentDenominatorFactorDTO.getCrawl());
            // 已完成的
            BigDecimal targetDenominatorValue = null;
            if (Objects.nonNull(targetMtdDenominatorValue)) {
                if (Objects.isNull(completionValue)) {
                    completionValue = BigDecimal.ZERO;
                }
                // 由于数值是 MTD，所以需要减去前一周的数值，表示当前周期完成的数值
                targetDenominatorValue = formatNegativeValue(targetMtdDenominatorValue.subtract(completionValue));
            }
            HotelKpiCalculationFactorDetailNextValueDTO factorValueDTO = new HotelKpiCalculationFactorDetailNextValueDTO();
            factorValueDTO.setIndex(1);
            factorValueDTO.setValue(targetDenominatorValue);

            HotelKpiCalculationFactorDetailNextDTO factorDTO = new HotelKpiCalculationFactorDetailNextDTO();
            factorDTO.setIndex(currentWeekIndex);
            factorDTO.setStartDate(currentStartDate);
            factorDTO.setEndDate(currentEndDate);
            factorDTO.setCalculationFactor(Lists.newArrayList(factorValueDTO));

            factorDetailDTOList.add(factorDTO);

            return factorDetailDTOList;
        }

        /**
         * 处理当前周期跨月，若当前周跨月，需要组装下个周期的 Week0
         *
         * @param currentStartLocalDate 当前周期的开始日期
         * @param date2ValueDTO         包含历史和未来的 KPI 数据
         * @param weekIndex             周期所在月的坐标
         * @param factorDetailDTOList   待组装集合
         * @author zc
         * @date 2025/3/26
         */
        private void handleNextWeekCrossMonths(LocalDate currentStartLocalDate, Map<Long, HotelKpiValueDTO> date2ValueDTO,
                                               int weekIndex, List<HotelKpiCalculationFactorDetailNextDTO> factorDetailDTOList) {
            // 若当前周期跨月，需要组装 week0
            LocalDate currentEndLocalDate = currentStartLocalDate.plusDays(6);
            if (currentStartLocalDate.getMonthValue() == currentEndLocalDate.getMonthValue()) {
                return;
            }
            LocalDate nextStartLocalDate = currentStartLocalDate.plusDays(7);
            long nextStartDate = Long.parseLong(DateTimeUtils.localDateToStr(nextStartLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            LocalDate nextEndLocalDate = nextStartLocalDate.plusDays(6);

            HotelKpiValueDTO nextValueDTO = date2ValueDTO.getOrDefault(nextStartDate, new HotelKpiValueDTO());
            HotelKpiCalculationFactorDTO nextDenominatorFactorDTO = nextValueDTO.getCalculationFactor().stream()
                    .filter(item -> item.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO());
            BigDecimal nextMtdDenominatorValue = formatNegativeValue(nextDenominatorFactorDTO.getCrawl());

            BigDecimal nextMtdDenominatorWeek0Value = null;
            if (Objects.nonNull(nextMtdDenominatorValue)) {
                nextMtdDenominatorWeek0Value = nextMtdDenominatorValue.multiply(new BigDecimal(String.valueOf(currentEndLocalDate.getDayOfMonth())))
                        .divide(new BigDecimal(String.valueOf(nextEndLocalDate.getDayOfMonth())), 0, RoundingMode.DOWN);
            }
            HotelKpiCalculationFactorDetailNextValueDTO factorValueDTO = new HotelKpiCalculationFactorDetailNextValueDTO();
            factorValueDTO.setIndex(1);
            factorValueDTO.setValue(nextMtdDenominatorWeek0Value);

            long startDateByWeek0 = Long.parseLong(DateTimeUtils.localDateToStr(currentEndLocalDate.withDayOfMonth(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDateByWeek0 = Long.parseLong(DateTimeUtils.localDateToStr(currentEndLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            HotelKpiCalculationFactorDetailNextDTO factorDTO = new HotelKpiCalculationFactorDetailNextDTO();
            factorDTO.setIndex(weekIndex);
            factorDTO.setStartDate(startDateByWeek0);
            factorDTO.setEndDate(endDateByWeek0);
            factorDTO.setCalculationFactor(Lists.newArrayList(factorValueDTO));

            factorDetailDTOList.add(factorDTO);
        }

        /**
         * 获取 KPI 原始数据
         *
         * @param propertyId   资产 id
         * @param startDate    开始日期(yyyyMMdd)
         * @param endDate      结束日期(yyyyMMdd)
         * @param metricsTypes 指标类型
         * @return HotelKpiMetricsDTO 集合
         * @author zc
         * @date 2024/6/26
         */
        @Override
        public List<HotelKpiMetricsDTO> listKpiMetricsDTOList(int propertyId, long startDate, long endDate, List<Integer> metricsTypes) {
            List<HotelKpiWeekly> kpiWeeklyList = listByDateRangeAndMetricsTypes(propertyId, startDate, endDate, metricsTypes);
            Map<Long, Map<Integer, HotelKpiWeekly>> metricsType2KpiByDate = kpiWeeklyList.stream()
                    .collect(Collectors.groupingBy(HotelKpiWeekly::getWeek,
                            Collectors.toMap(HotelKpiWeekly::getMetricsType, Function.identity())));

            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(startDate), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(endDate), BaseConstant.DATE_FORMAT_YYYYMMDD);

            List<HotelKpiMetricsDTO> kpiMetricsDTOList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = indexLocalDate.plusWeeks(1)) {
                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                Map<Integer, HotelKpiWeekly> metricsType2Kpi = metricsType2KpiByDate.getOrDefault(indexDate, Collections.emptyMap());
                for (int metricsType : metricsTypes) {
                    HotelKpiMetricsDTO kpiMetricsDTO = new HotelKpiMetricsDTO();
                    HotelKpiWeekly kpiWeekly = metricsType2Kpi.get(metricsType);
                    if (Objects.isNull(kpiWeekly)) {
                        kpiMetricsDTO.setDate(indexDate);
                        kpiMetricsDTO.setMetricsType(metricsType);
                    } else {
                        HotelKpiOriginDetailDTO originDetailDTO = JsonUtils.convertStringToObject(objectMapper,
                                kpiWeekly.getDetail(), HotelKpiOriginDetailDTO.class);
                        kpiMetricsDTO = beanMapper.map(kpiWeekly, HotelKpiMetricsDTO.class, dto -> {
                            dto.setDate(kpiWeekly.getWeek());
                            dto.setActual(beanMapper.map(originDetailDTO.getActual(), HotelKpiValueDTO.class));
                            dto.setTarget(beanMapper.map(originDetailDTO.getTarget(), HotelKpiValueDTO.class));
                            dto.setCure(beanMapper.map(originDetailDTO.getCure(), HotelKpiValueDTO.class));
                        });
                    }
                    kpiMetricsDTOList.add(kpiMetricsDTO);
                }
            }
            return kpiMetricsDTOList;
        }

        /**
         * 组装 Target, Cure setting 数据
         *
         * @param propertyId            资产 id
         * @param metricsDTOList        Weekly 指标数据 DTO 容器
         * @param metricsTypes          指标类型集合
         * @param startDate             开始日期(yyyyMMdd)
         * @param endDate               结束日期(yyyyMMdd)
         * @param dayOfWeek             周几
         * @param metricsDTOMonthlyList Monthly 指标数据
         * @author zc
         * @date 2024/6/26
         */
        private void packageSettingValue(int propertyId, List<HotelKpiMetricsDTO> metricsDTOList, List<Integer> metricsTypes,
                                         long startDate, long endDate, DayOfWeek dayOfWeek, List<HotelKpiMetricsDTO> metricsDTOMonthlyList) {
            if (CollectionUtils.isEmpty(metricsDTOList)) {
                return;
            }
            // 获取 Actual 的设置数据
            List<HotelKpiMetricsSettingActualDTO> metricsSettingActualDTOS =
                    metricsSettingActualService.listWeeklySettingByDateRange(propertyId, metricsTypes, startDate,
                            endDate, metricsDTOMonthlyList, dayOfWeek);
            // 获取 Target、Cure 设置数据
            List<HotelKpiMetricsSettingTargetDTO> metricsSettingTargetDTOS =
                    metricsSettingTargetService.listWeeklySettingByDateRange(propertyId, metricsTypes, startDate,
                            endDate, dayOfWeek);
            // 组装 Metrics Setting
            metricsSettingService.packageMetricsSetting(metricsDTOList, metricsSettingActualDTOS, metricsSettingTargetDTOS);
        }

        /**
         * 保存指标数据
         *
         * @param propertyDTO 资产
         * @param metricsDTO  指标数据
         * @param operatorId  操作人 id
         * @author zc
         * @date 2024/6/27
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void save(PropertyDTO propertyDTO, HotelKpiMetricsSaveDTO metricsDTO, int operatorId) {
            HotelKpiWeekly originKpiWeekly = getByDateAndMetricsType(propertyDTO.getId(), metricsDTO.getDate(), metricsDTO.getMetricsType());
            HotelKpiOriginDetailDTO originDetailDTO;
            if (Objects.isNull(originKpiWeekly)) {
                originDetailDTO = new HotelKpiOriginDetailDTO();
            } else {
                originDetailDTO = JsonUtils.convertStringToObject(objectMapper, originKpiWeekly.getDetail(),
                        HotelKpiOriginDetailDTO.class);
            }
            // 设置 Actual
            hotelKpiService.setMetricsManualValue(originDetailDTO.getActual().getCalculationFactor(), metricsDTO.getActual(), operatorId);
            // 设置 Target
            hotelKpiService.setMetricsManualValue(originDetailDTO.getTarget().getCalculationFactor(), metricsDTO.getTarget(), operatorId);
            // 设置 Cure
            hotelKpiService.setMetricsManualValue(originDetailDTO.getCure().getCalculationFactor(), metricsDTO.getCure(), operatorId);

            HotelKpiWeekly updateKpiWeekly = new HotelKpiWeekly();
            updateKpiWeekly.setPropertyId(propertyDTO.getId());
            updateKpiWeekly.setWeek(metricsDTO.getDate());
            updateKpiWeekly.setMetricsType(metricsDTO.getMetricsType());
            updateKpiWeekly.setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTO));

            insertOrUpdate(updateKpiWeekly);
        }

        /**
         * 获取 Hotel KPI V2 数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return Hotel KPI 数据
         * @author zc
         * @date 2025/8/18
         */
        @Override
        public List<HotelKpiV2DTO> listHotelKpiV2(PropertyDTO propertyDTO, HotelKpiQuery query) {
            // 获取数据筛选范围
            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(query.getStartDate()), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(query.getEndDate()), BaseConstant.DATE_FORMAT_YYYYMMDD);

            // 获取周起始日
            DayOfWeek dayOfWeek = startLocalDate.getDayOfWeek();

            LocalDate firstWeekLocalDateInYear = startLocalDate.with(TemporalAdjusters.firstDayOfYear()).with(TemporalAdjusters.previousOrSame(dayOfWeek));
            long firstWeekInYear = Long.parseLong(DateTimeUtils.localDateToStr(firstWeekLocalDateInYear, BaseConstant.DATE_FORMAT_YYYYMMDD));

            // 获取有效的指标配置数据
            List<HotelKpiMetricsConfig> effectiveMetrics = metricsConfigService.listEffectiveMetricsOfYear(propertyDTO.getId(),
                    query.getDateType(), query.getStartDate(), query.getEndDate(), Boolean.TRUE, HotelKpiCons.V2);
            Map<Integer, List<HotelKpiMetricsConfig>> metricsParentId2Configs = effectiveMetrics.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsConfig::getParentId));

            // 提取指标类型集合
            List<HotelKpiMetricsConfig> metricsConfigList = effectiveMetrics.stream()
                    .filter(item -> item.getMetricsType() > 0).toList();
            List<Integer> metricsTypes = metricsConfigList.stream().map(HotelKpiMetricsConfig::getMetricsType).toList();

            // 获取 Weekly KPI 原始数据
            List<HotelKpiV2MetricsDTO> metricsDTOList = listKpiMetricsDTOListV2(propertyDTO, firstWeekInYear, query.getEndDate(), metricsConfigList);

            // 提取操作人名称
            Map<Integer, String> userId2Name = hotelKpiService.extractOperatorNameV2(propertyDTO.getTenantId(), metricsDTOList);

            Map<Long, Map<Integer, HotelKpiV2MetricsDTO>> metricsType2KpiMetricsDTOByDate = metricsDTOList.stream()
                    .collect(Collectors.groupingBy(HotelKpiV2MetricsDTO::getDate,
                            Collectors.toMap(HotelKpiV2MetricsDTO::getMetricsType, Function.identity())));

            // 获取指定日期警示备注数量
            Map<Long, Map<Integer, Map<Integer, Long>>> metricsType2WarningCountByDate = warningCommentService.listByDateRangeAndMetricsTypes(propertyDTO.getId(),
                            query.getDateType(), query.getStartDate(), query.getEndDate(), metricsTypes).stream()
                    .collect(Collectors.groupingBy(HotelKpiWarningComment::getDate,
                            Collectors.groupingBy(HotelKpiWarningComment::getMetricsType,
                                    Collectors.groupingBy(HotelKpiWarningComment::getCategory, Collectors.counting()))));

            List<HotelKpiV2DTO> hotelKpiDTOList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = indexLocalDate.plusDays(7)) {
                // 获取当前周期的 KPI 原始数据
                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                Map<Integer, HotelKpiV2MetricsDTO> metricsType2KpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(indexDate, Collections.emptyMap());

                // 获取上一周期的 KPI 原始数据
                long lastIndexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate.minusDays(7), BaseConstant.DATE_FORMAT_YYYYMMDD));
                Map<Integer, HotelKpiV2MetricsDTO> metricsType2LastKpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(lastIndexDate, Collections.emptyMap());

                Map<Integer, Map<Integer, Long>> metricsType2WarningCount = metricsType2WarningCountByDate.getOrDefault(indexDate, Collections.emptyMap());

                // 生成 KPI 详情数据
                List<HotelKpiV2DetailDTO> kpiDetailDTOList = hotelKpiService.generateKpiDetailDTOListV2(propertyDTO, userId2Name, effectiveMetrics,
                        metricsParentId2Configs, metricsType2KpiMetricsDTO, metricsType2LastKpiMetricsDTO, metricsType2WarningCount,
                        indexDate, lastIndexDate, DATE_TYPE);

                // 组装指标数据日期范围
                packageDataRangeV2(kpiDetailDTOList);

                hotelKpiDTOList.add(HotelKpiV2DTO.builder()
                        .date(indexDate)
                        .details(kpiDetailDTOList)
                        .build());
            }
            return hotelKpiDTOList;
        }

        /**
         * 获取 KPI 原始数据 V2
         *
         * @param propertyDTO       资产
         * @param startDate         开始日期(yyyyMMdd)
         * @param endDate           结束日期(yyyyMMdd)
         * @param metricsConfigList 指标配置集合
         * @return HotelKpiV2MetricsDTO 集合
         * @author zc
         * @date 2025/8/18
         */
        private List<HotelKpiV2MetricsDTO> listKpiMetricsDTOListV2(PropertyDTO propertyDTO, long startDate, long endDate,
                                                                   List<HotelKpiMetricsConfig> metricsConfigList) {
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            List<Integer> metricsTypes = metricsConfigList.stream().map(HotelKpiMetricsConfig::getMetricsType).toList();

            List<HotelKpiWeekly> kpiWeeklyList = listByDateRangeAndMetricsTypes(propertyDTO.getId(), startDate, endDate, metricsTypes);
            Map<Long, Map<Integer, HotelKpiWeekly>> metricsType2KpiByDate = kpiWeeklyList.stream()
                    .collect(Collectors.groupingBy(HotelKpiWeekly::getWeek,
                            Collectors.toMap(HotelKpiWeekly::getMetricsType, Function.identity())));

            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(startDate), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(endDate), BaseConstant.DATE_FORMAT_YYYYMMDD);

            List<HotelKpiV2MetricsDTO> kpiMetricsDTOList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = indexLocalDate.plusWeeks(1)) {
                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                Map<Integer, HotelKpiWeekly> metricsType2Kpi = metricsType2KpiByDate.getOrDefault(indexDate, Collections.emptyMap());
                for (HotelKpiMetricsConfig metricsConfig : metricsConfigList) {
                    HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                            metricsConfig.getCalculationFactorConfig(), HotelKpiV2FactorConfigDTO.class);

                    HotelKpiV2MetricsDTO kpiMetricsDTO = new HotelKpiV2MetricsDTO(DATE_TYPE, factorConfigDTO);
                    HotelKpiWeekly kpiWeekly = metricsType2Kpi.get(metricsConfig.getMetricsType());
                    if (Objects.isNull(kpiWeekly)) {
                        kpiMetricsDTO.setDate(indexDate);
                        kpiMetricsDTO.setMetricsType(metricsConfig.getMetricsType());
                    } else {
                        List<HotelKpiV2OriginDetailDTO> originDetailDTOList = JsonUtils.convertStringToList(objectMapper,
                                kpiWeekly.getDetail(), HotelKpiV2OriginDetailDTO.class);
                        kpiMetricsDTO.setDate(kpiWeekly.getWeek());
                        kpiMetricsDTO.setMetricsType(kpiWeekly.getMetricsType());

                        List<HotelKpiV2MetricsCategoryDTO> list = originDetailDTOList.stream()
                                .map(originDetail -> {
                                    HotelKpiV2MetricsCategoryDTO metricsCategoryDTO = new HotelKpiV2MetricsCategoryDTO();
                                    metricsCategoryDTO.setCategory(originDetail.getCategory());
                                    metricsCategoryDTO.setActual(beanMapper.map(originDetail.getActual(), HotelKpiV2ValueDTO.class));
                                    metricsCategoryDTO.setBudget(beanMapper.map(originDetail.getBudget(), HotelKpiV2ValueDTO.class));
                                    metricsCategoryDTO.setLy(beanMapper.map(originDetail.getLy(), HotelKpiV2ValueDTO.class));
                                    return metricsCategoryDTO;
                                }).toList();
                        // 组装指标数值
                        hotelKpiService.packageMetricsValueV2(list, roundingMode);
                        kpiMetricsDTO.setList(list);
                    }
                    kpiMetricsDTOList.add(kpiMetricsDTO);
                }
            }
            return kpiMetricsDTOList;
        }

        /**
         * 组装指标数据日期范围
         *
         * @param kpiDetailDTOList 指标数据
         * @author zc
         * @date 2025/8/19
         */
        private void packageDataRangeV2(List<HotelKpiV2DetailDTO> kpiDetailDTOList) {
            for (HotelKpiV2DetailDTO kpiDetailDTO : kpiDetailDTOList) {
                for (HotelKpiV2ItemDTO item : kpiDetailDTO.getItems()) {
                    for (HotelKpiV2MetricsDTO metric : item.getMetrics()) {
                        int dataRangeType = HotelKpiMetricsEnum.getDataRangeType(metric.getMetricsType());
                        metric.setDataRange(getDataRangeV2(metric.getDate(), dataRangeType));
                    }
                }
            }
        }

        /**
         * 获取指标数据日期范围 V2
         *
         * @param date          当前日期
         * @param dataRangeType 数据日期范围类型
         * @return HotelKpiDataRangeDTO
         * @author zc
         * @date 2025/8/19
         */
        private HotelKpiDataRangeDTO getDataRangeV2(long date, int dataRangeType) {
            LocalDate localDate = DateTimeUtils.strToLocalDate(String.valueOf(date), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate startLocalDate;
            LocalDate endLocalDate;
            if (dataRangeType == HotelKpiDataRangeTypeCons.WTD) {
                startLocalDate = localDate;
                endLocalDate = localDate.plusDays(6);
            } else if (dataRangeType == HotelKpiDataRangeTypeCons.MTD) {
                endLocalDate = localDate.plusDays(6);
                if (endLocalDate.getMonthValue() != localDate.getMonthValue()) {
                    startLocalDate = endLocalDate.withDayOfMonth(1);
                } else {
                    startLocalDate = localDate.withDayOfMonth(1);
                }
            } else if (dataRangeType == HotelKpiDataRangeTypeCons.QTD) {
                startLocalDate = LocalDate.of(localDate.getYear(), localDate.getMonth().firstMonthOfQuarter(), 1);
                LocalDate quarterEndDate = startLocalDate.plusMonths(2).with(TemporalAdjusters.lastDayOfMonth());
                endLocalDate = localDate.plusDays(6);
                if (endLocalDate.isAfter(quarterEndDate)) {
                    endLocalDate = quarterEndDate;
                }
            } else if (dataRangeType == HotelKpiDataRangeTypeCons.SIX_MTD) {
                if (localDate.getMonthValue() <= 6) {
                    startLocalDate = localDate.withDayOfYear(1);
                } else {
                    startLocalDate = LocalDate.of(localDate.getYear(), 7, 1);
                }
                LocalDate sixMonthEndDate = startLocalDate.plusMonths(5).with(TemporalAdjusters.lastDayOfMonth());
                endLocalDate = localDate.plusDays(6);
                if (endLocalDate.isAfter(sixMonthEndDate)) {
                    endLocalDate = sixMonthEndDate;
                }
            } else {
                LocalDate lastLocalDate = localDate.plusDays(6);
                if (lastLocalDate.getMonthValue() != localDate.getMonthValue()) {
                    startLocalDate = lastLocalDate.withDayOfMonth(1);
                } else {
                    startLocalDate = localDate.withDayOfMonth(1);
                }
                endLocalDate = startLocalDate.with(TemporalAdjusters.lastDayOfMonth());
            }
            HotelKpiDataRangeDTO dataRangeDTO = new HotelKpiDataRangeDTO();
            dataRangeDTO.setStartDate(Long.parseLong(DateTimeUtils.localDateToStr(startLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD)));
            dataRangeDTO.setEndDate(Long.parseLong(DateTimeUtils.localDateToStr(endLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD)));
            return dataRangeDTO;
        }

        /**
         * 保存指标数据 V2
         *
         * @param propertyDTO   资产
         * @param saveDTO       指标数据
         * @param metricsConfig 指标配置
         * @param operatorId    操作人 id
         * @author zc
         * @date 2025/8/19
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveV2(PropertyDTO propertyDTO, HotelKpiV2MetricsSaveDTO saveDTO, HotelKpiMetricsConfig metricsConfig, int operatorId) {
            long date = saveDTO.getDate();
            int metricsType = saveDTO.getMetricsType();

            HotelKpiWeekly originKpiWeekly = getByDateAndMetricsType(propertyDTO.getId(), date, metricsType);
            List<HotelKpiV2OriginDetailDTO> originDetailDTOList;
            if (Objects.isNull(originKpiWeekly)) {
                HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                        metricsConfig.getCalculationFactorConfig(), HotelKpiV2FactorConfigDTO.class);
                originDetailDTOList = HotelKpiV2OriginDetailDTO.initV2OriginDetailDTOList(DATE_TYPE, factorConfigDTO);
            } else {
                originDetailDTOList = JsonUtils.convertStringToList(objectMapper, originKpiWeekly.getDetail(),
                        HotelKpiV2OriginDetailDTO.class);
            }

            Map<Integer, HotelKpiV2MetricsSaveCategoryDTO> category2SaveDTO = saveDTO.getList().stream()
                    .collect(Collectors.toMap(HotelKpiV2MetricsSaveCategoryDTO::getCategory, Function.identity()));
            originDetailDTOList.forEach(categoryDTO -> {
                HotelKpiV2MetricsSaveCategoryDTO saveCategoryDTO = category2SaveDTO.getOrDefault(categoryDTO.getCategory(),
                        new HotelKpiV2MetricsSaveCategoryDTO());
                hotelKpiService.setMetricsManualValue(categoryDTO.getActual().getCalculationFactor(), saveCategoryDTO.getActual(), operatorId);
                hotelKpiService.setMetricsManualValue(categoryDTO.getBudget().getCalculationFactor(), saveCategoryDTO.getBudget(), operatorId);
                hotelKpiService.setMetricsManualValue(categoryDTO.getLy().getCalculationFactor(), saveCategoryDTO.getLy(), operatorId);
            });

            HotelKpiWeekly updateKpiWeekly = new HotelKpiWeekly()
                    .setPropertyId(propertyDTO.getId())
                    .setWeek(date).setMetricsType(metricsType)
                    .setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTOList));
            insertOrUpdate(updateKpiWeekly);

            // 触发重新计算当前指标的数值
            LocalDate localDate = DateTimeUtils.strToLocalDate(String.valueOf(date), BaseConstant.DATE_FORMAT_YYYYMMDD);
            commonSummaryService.setMetricsData(propertyDTO, DATE_TYPE, localDate, localDate, metricsType);
        }

        /**
         * 保存指标采集数据 V2
         *
         * @param propertyDTO   资产
         * @param crawlValueDTO 采集的指标数据
         * @param metricsConfig 指标配置
         * @author zc
         * @date 2025/8/25
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveCrawlValueV2(PropertyDTO propertyDTO, HotelKpiV2CrawlValueDTO crawlValueDTO, HotelKpiMetricsConfig metricsConfig) {
            long date = crawlValueDTO.getDate();
            int metricsType = crawlValueDTO.getMetricsType();

            HotelKpiWeekly originKpiWeekly = getByDateAndMetricsType(propertyDTO.getId(), date, metricsType);
            List<HotelKpiV2OriginDetailDTO> originDetailDTOList;
            if (Objects.isNull(originKpiWeekly)) {
                HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                        metricsConfig.getCalculationFactorConfig(), HotelKpiV2FactorConfigDTO.class);
                originDetailDTOList = HotelKpiV2OriginDetailDTO.initV2OriginDetailDTOList(DATE_TYPE, factorConfigDTO);
            } else {
                originDetailDTOList = JsonUtils.convertStringToList(objectMapper, originKpiWeekly.getDetail(),
                        HotelKpiV2OriginDetailDTO.class);
            }

            Map<Integer, HotelKpiV2CrawlValueCategoryDTO> category2SaveDTO = crawlValueDTO.getList().stream()
                    .collect(Collectors.toMap(HotelKpiV2CrawlValueCategoryDTO::getCategory, Function.identity()));
            originDetailDTOList.forEach(categoryDTO -> {
                HotelKpiV2CrawlValueCategoryDTO crawlCategoryDTO = category2SaveDTO.getOrDefault(categoryDTO.getCategory(),
                        new HotelKpiV2CrawlValueCategoryDTO());
                hotelKpiService.setMetricsCrawlValueV2(categoryDTO.getActual().getCalculationFactor(), crawlCategoryDTO.getActual());
                hotelKpiService.setMetricsCrawlValueV2(categoryDTO.getBudget().getCalculationFactor(), crawlCategoryDTO.getBudget());
                hotelKpiService.setMetricsCrawlValueV2(categoryDTO.getLy().getCalculationFactor(), crawlCategoryDTO.getLy());
            });

            HotelKpiWeekly updateKpiWeekly = new HotelKpiWeekly()
                    .setPropertyId(propertyDTO.getId())
                    .setWeek(date).setMetricsType(metricsType)
                    .setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTOList));
            insertOrUpdate(updateKpiWeekly);

            // 触发重新计算当前指标的数值
            LocalDate localDate = DateTimeUtils.strToLocalDate(String.valueOf(date), BaseConstant.DATE_FORMAT_YYYYMMDD);
            commonSummaryService.setMetricsData(propertyDTO, DATE_TYPE, localDate, localDate, metricsType);
        }
    }