# HotelKpiQuarterlyServiceImpl.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.*;
    import cloud.wconnect.workperformance.cons.hotelkpi.*;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiMetricsConfig;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiQuarterly;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiWarningComment;
    import cloud.wconnect.workperformance.mapper.hotelkpi.HotelKpiQuarterlyMapper;
    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.YearMonth;
    import java.time.temporal.TemporalAdjusters;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * Hotel KPI Quarterly 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-06-24
     */
    @Slf4j
    @Service
    @DS("work_performance")
    public class HotelKpiQuarterlyServiceImpl extends ServiceImpl<HotelKpiQuarterlyMapper, HotelKpiQuarterly> implements IHotelKpiQuarterlyService {

        @Resource
        private IHotelKpiService hotelKpiService;
        @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 IHotelKpiWeekStartDayService weekStartDayService;
        @Resource
        private IHotelKpiCommonSummaryService commonSummaryService;
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        private BeanMapper beanMapper;

        private static final Integer DATE_TYPE = HotelKpiDateTypeCons.QUARTERLY;

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

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

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

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

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

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

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

        /**
         * 获取 Hotel KPI 数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return Hotel KPI 数据
         * @author zc
         * @date 2024/6/27
         */
        @Override
        public List<HotelKpiDTO> listHotelKpi(PropertyDTO propertyDTO, HotelKpiQuery query) {
            // 获取数据筛选范围
            YearMonth startYearMonth = YearMonthUtils.getYearMonth(String.valueOf(query.getStartDate()));
            YearMonth endYearMonth = YearMonthUtils.getYearMonth(String.valueOf(query.getEndDate()));
            // 获取周起始日
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyDTO.getId());

            // 获取日期：当前年第一个季度到当前季度的下个季度的数据用于计算【递减剩余】类型的指标数值
            long firstQuarterInYear = YearMonthWeekUtils.getFirstQuarterOfCurrentYear(query.getStartDate());
            long nextQuarter = YearMonthUtils.parseYearMonthToLong(endYearMonth.plusMonths(3));

            // 获取有效的指标配置数据
            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();

            // 获取 KPI 原始数据
            List<HotelKpiMetricsDTO> metricsDTOList = listKpiMetricsDTOList(propertyDTO.getId(), firstQuarterInYear, nextQuarter, metricsTypes);

            // 组装 Metrics Setting
            packageSettingValue(propertyDTO.getId(), metricsDTOList, metricsTypes, firstQuarterInYear, nextQuarter, dayOfWeek);

            // 获取操作人姓名
            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<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<HotelKpiDTO> hotelKpiDTOList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonth; !indexYearMonth.isAfter(endYearMonth); indexYearMonth = indexYearMonth.plusMonths(3)) {
                // 获取当前季度的 KPI 原始数据
                long indexDate = YearMonthUtils.parseYearMonthToLong(indexYearMonth);
                Map<Integer, HotelKpiMetricsDTO> metricsType2KpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(indexDate, Collections.emptyMap());

                // 获取上一季度的 KPI 原始数据
                long lastIndexDate = YearMonthWeekUtils.getLastQuarterDate(indexDate);
                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, HotelKpiDateTypeCons.QUARTERLY);

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

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

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

        /**
         * 组装指标数据日期范围
         *
         * @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 = YearMonthUtils.getYearMonth(String.valueOf(date)).atDay(1);
            LocalDate startLocalDate;
            LocalDate endLocalDate;
            if (dataRangeType == HotelKpiDataRangeTypeCons.QTD) {
                startLocalDate = LocalDate.of(localDate.getYear(), localDate.getMonth().firstMonthOfQuarter(), 1);
            } else if (dataRangeType == HotelKpiDataRangeTypeCons.SIX_MTD) {
                if (localDate.getMonthValue() <= 6) {
                    startLocalDate = localDate.withDayOfYear(1);
                } else {
                    startLocalDate = LocalDate.of(localDate.getYear(), 7, 1);
                }
            } else {
                startLocalDate = localDate.withDayOfMonth(1);
            }
            endLocalDate = localDate.plusMonths(2).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 indexYearMonth             日期
         * @param metricsType2MetricsDTOList 包含历史和未来的 KPI 数据
         * @author zc
         * @date 2025/3/6
         */
        private void packageCalculationFactor(PropertyDTO propertyDTO, List<HotelKpiDetailDTO> kpiDetailDTOList, YearMonth indexYearMonth,
                                              Map<Integer, List<HotelKpiMetricsDTO>> metricsType2MetricsDTOList) {
            for (HotelKpiDetailDTO kpiDetailDTO : kpiDetailDTOList) {
                for (HotelKpiItemDTO item : kpiDetailDTO.getItems()) {
                    for (HotelKpiMetricsDTO metric : item.getMetrics()) {
                        List<HotelKpiMetricsDTO> metricsDTOList = metricsType2MetricsDTOList.get(metric.getMetricsType());
                        if (CollectionUtils.isEmpty(metricsDTOList)) {
                            continue;
                        }
                        // 生成 Actual 计算因子详情数据，Target 没有递减剩余规则不用处理
                        Map<Long, HotelKpiValueDTO> date2ActualValueDTO = metricsDTOList.stream()
                                .collect(Collectors.toMap(HotelKpiMetricsDTO::getDate, HotelKpiMetricsDTO::getActual));
                        List<HotelKpiCalculationFactorDTO> actualFactors = metric.getActual().getCalculationFactor();
                        packageCalculationFactorDetail(propertyDTO, indexYearMonth, actualFactors, date2ActualValueDTO,
                                metric.getExplain().getActual());
                    }
                }
            }
        }

        /**
         * 组装计算因子详情数据
         *
         * @param propertyDTO       资产
         * @param indexYearMonth    日期
         * @param currentFactorDTOS 当前指标的计算因子
         * @param date2ValueDTO     包含历史和未来的 KPI 数据
         * @param explainValueDTO   待组装DTO
         * @author zc
         * @date 2025/3/6
         */
        private void packageCalculationFactorDetail(PropertyDTO propertyDTO, YearMonth indexYearMonth, List<HotelKpiCalculationFactorDTO> currentFactorDTOS,
                                                    Map<Long, HotelKpiValueDTO> date2ValueDTO, 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.setCompletionRate(generateCompletionRate(propertyDTO, indexYearMonth, date2ValueDTO));
            // 生成当前周期计算因子详情数据
            explainValueDTO.setCalculationFactorDetail(generateCalculationFactorValue(indexYearMonth, indexYearMonth, date2ValueDTO));
            // 生成下一周期计算因子详情数据
            explainValueDTO.setCalculationFactorDetailNext(generateCalculationFactorValueNext(indexYearMonth, date2ValueDTO));
        }

        /**
         * 格式化负数数值
         *
         * @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 currentYearMonth 当前日期
         * @param date2ValueDTO    包含历史和未来的 KPI 数据
         * @return HotelKpiCompletionRateDTO
         * @author zc
         * @date 2025/3/7
         */
        private HotelKpiCompletionRateDTO generateCompletionRate(PropertyDTO propertyDTO, YearMonth currentYearMonth, Map<Long, HotelKpiValueDTO> date2ValueDTO) {
            long currentDate = YearMonthUtils.parseYearMonthToLong(currentYearMonth);
            HotelKpiCalculationFactorDTO denominatorFactorDTO = date2ValueDTO.getOrDefault(currentDate, new HotelKpiValueDTO()).getCalculationFactor().stream()
                    .filter(factor -> factor.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO());

            List<Long> dateList = denominatorFactorDTO.getStageValue().stream()
                    .map(HotelKpiStageValueDTO::getDate).toList();
            // 当前周期已完成量
            BigDecimal currentCompleted = null;
            // 取当前周期之前的数据
            List<Long> preDateList = denominatorFactorDTO.getStageValue().stream()
                    .map(HotelKpiStageValueDTO::getDate).filter(item -> item <= currentDate).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;
                }
                if (Objects.isNull(currentCompleted)) {
                    currentCompleted = BigDecimal.ZERO;
                }
                currentCompleted = currentCompleted.add(currentValue);
            }

            HotelKpiCompletionRateDTO completionRateDTO = new HotelKpiCompletionRateDTO();
            completionRateDTO.setStartDate(Collections.min(dateList));
            completionRateDTO.setEndDate(Collections.max(dateList));
            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 indexYearMonth  日期
         * @param targetYearMonth 目标日期
         * @param date2ValueDTO   包含历史和未来的 KPI 数据
         * @return Actual 计算因子详情数据
         * @author zc
         * @date 2025/3/6
         */
        private List<HotelKpiCalculationFactorDetailDTO> generateCalculationFactorValue(YearMonth indexYearMonth, YearMonth targetYearMonth,
                                                                                        Map<Long, HotelKpiValueDTO> date2ValueDTO) {
            List<HotelKpiCalculationFactorDetailDTO> factorDetailDTOList = Lists.newArrayList();

            YearMonth currentYearMonth = indexYearMonth;
            while (!currentYearMonth.isBefore(targetYearMonth)) {
                long indexDate = YearMonthUtils.parseYearMonthToLong(currentYearMonth);
                HotelKpiValueDTO indexValueDTO = date2ValueDTO.getOrDefault(indexDate, new HotelKpiValueDTO());

                // 获取当前周期的计算因子值 - 被除数（分子）
                HotelKpiCalculationFactorDTO indexMoleculeFactorDTO = indexValueDTO.getCalculationFactor().stream()
                        .filter(item -> item.getIndex() == 1).findFirst().orElse(new HotelKpiCalculationFactorDTO());
                BigDecimal indexMoleculeValue = formatNegativeValue(indexMoleculeFactorDTO.getCrawl());

                HotelKpiCalculationFactorDetailValueDTO indexMolecule = new HotelKpiCalculationFactorDetailValueDTO();
                indexMolecule.setIndex(1);
                indexMolecule.setValue(indexMoleculeValue);

                // 获取当前周期的计算因子值 - 除数（分母）
                HotelKpiCalculationFactorDTO indexDenominatorFactorDTO = indexValueDTO.getCalculationFactor().stream()
                        .filter(item -> item.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO());
                BigDecimal indexDenominatorValue = formatNegativeValue(indexDenominatorFactorDTO.getCrawl());

                HotelKpiCalculationFactorDetailValueDTO indexDenominator = new HotelKpiCalculationFactorDetailValueDTO();
                indexDenominator.setIndex(2);
                indexDenominator.setValue(indexDenominatorValue);

                HotelKpiCalculationFactorDetailDTO currentFactor = new HotelKpiCalculationFactorDetailDTO();
                currentFactor.setIndex(1);
                currentFactor.setStartDate(indexDate);
                currentFactor.setEndDate(indexDate);
                currentFactor.setCalculationFactor(Lists.newArrayList(indexMolecule, indexDenominator));

                factorDetailDTOList.add(currentFactor);

                // 往前减三个月
                currentYearMonth = currentYearMonth.minusMonths(3);
            }
            return factorDetailDTOList;
        }

        /**
         * 生成下一周期计算因子详情数据
         *
         * @param indexYearMonth 日期
         * @param date2ValueDTO  包含历史和未来的 KPI 数据
         * @return HotelKpiCalculationFactorDetailNextDTO 集合
         * @author zc
         * @date 2025/3/10
         */
        private List<HotelKpiCalculationFactorDetailNextDTO> generateCalculationFactorValueNext(YearMonth indexYearMonth, Map<Long, HotelKpiValueDTO> date2ValueDTO) {
            YearMonth nextYearMonth = indexYearMonth.plusMonths(3);
            long targetDate = YearMonthUtils.parseYearMonthToLong(nextYearMonth);

            HotelKpiValueDTO targetValueDTO = date2ValueDTO.getOrDefault(targetDate, new HotelKpiValueDTO());
            HotelKpiCalculationFactorDTO targetDenominatorFactorDTO = targetValueDTO.getCalculationFactor().stream()
                    .filter(item -> item.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO());
            BigDecimal nextDateValue = formatNegativeValue(targetDenominatorFactorDTO.getCrawl());

            HotelKpiCalculationFactorDetailNextValueDTO factorValueDTO = new HotelKpiCalculationFactorDetailNextValueDTO();
            factorValueDTO.setIndex(1);
            factorValueDTO.setValue(nextDateValue);

            HotelKpiCalculationFactorDetailNextDTO factorDTO = new HotelKpiCalculationFactorDetailNextDTO();
            factorDTO.setIndex(1);
            factorDTO.setStartDate(targetDate);
            factorDTO.setEndDate(targetDate);
            factorDTO.setCalculationFactor(Lists.newArrayList(factorValueDTO));

            return Lists.newArrayList(factorDTO);
        }

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

            YearMonth startYearMonth = YearMonthUtils.getYearMonth(String.valueOf(startDate));
            YearMonth endYearMonth = YearMonthUtils.getYearMonth(String.valueOf(endDate));

            List<HotelKpiMetricsDTO> kpiMetricsDTOList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonth; !indexYearMonth.isAfter(endYearMonth); indexYearMonth = indexYearMonth.plusMonths(3)) {
                long indexDate = YearMonthUtils.parseYearMonthToLong(indexYearMonth);
                Map<Integer, HotelKpiQuarterly> metricsType2Kpi = metricsType2KpiByDate.getOrDefault(indexDate, Collections.emptyMap());
                for (Integer metricsType : metricsTypes) {
                    HotelKpiMetricsDTO kpiMetricsDTO = new HotelKpiMetricsDTO();
                    HotelKpiQuarterly kpiQuarterly = metricsType2Kpi.get(metricsType);
                    if (Objects.isNull(kpiQuarterly)) {
                        kpiMetricsDTO.setDate(indexDate);
                        kpiMetricsDTO.setMetricsType(metricsType);
                    } else {
                        HotelKpiOriginDetailDTO originDetailDTO = JsonUtils.convertStringToObject(objectMapper,
                                kpiQuarterly.getDetail(), HotelKpiOriginDetailDTO.class);
                        kpiMetricsDTO = beanMapper.map(kpiQuarterly, HotelKpiMetricsDTO.class, dto -> {
                            dto.setDate(kpiQuarterly.getMonth());
                            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 指标数据 DTO 容器
         * @param metricsTypes   指标类型集合
         * @param startDate      开始日期
         * @param endDate        结束日期
         * @param dayOfWeek      周几
         * @author zc
         * @date 2024/6/27
         */
        private void packageSettingValue(int propertyId, List<HotelKpiMetricsDTO> metricsDTOList,
                                         List<Integer> metricsTypes, long startDate, long endDate, DayOfWeek dayOfWeek) {
            if (CollectionUtils.isEmpty(metricsDTOList)) {
                return;
            }
            // 获取 Actual 的设置数据
            List<HotelKpiMetricsSettingActualDTO> metricsSettingActualDTOS =
                    metricsSettingActualService.listQuarterlySettingByDateRange(propertyId, metricsTypes, startDate,
                            endDate, metricsDTOList, dayOfWeek);
            // 获取 Target、Cure 设置数据
            List<HotelKpiMetricsSettingTargetDTO> metricsSettingTargetDTOS =
                    metricsSettingTargetService.listQuarterlySettingByDateRange(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) {
            HotelKpiQuarterly originKpiQuarterly = getByDateAndMetricsType(propertyDTO.getId(), metricsDTO.getDate(), metricsDTO.getMetricsType());
            HotelKpiOriginDetailDTO originDetailDTO;
            if (Objects.isNull(originKpiQuarterly)) {
                originDetailDTO = new HotelKpiOriginDetailDTO();
            } else {
                originDetailDTO = JsonUtils.convertStringToObject(objectMapper, originKpiQuarterly.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);

            HotelKpiQuarterly updateKpiQuarterly = new HotelKpiQuarterly();
            updateKpiQuarterly.setPropertyId(propertyDTO.getId());
            updateKpiQuarterly.setMonth(metricsDTO.getDate());
            updateKpiQuarterly.setMetricsType(metricsDTO.getMetricsType());
            updateKpiQuarterly.setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTO));

            insertOrUpdate(updateKpiQuarterly);
        }

        /**
         * 获取 Hotel KPI 数据 V2
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return Hotel KPI 数据
         * @author zc
         * @date 2025/8/19
         */
        @Override
        public List<HotelKpiV2DTO> listHotelKpiV2(PropertyDTO propertyDTO, HotelKpiQuery query) {
            // 获取有效的指标配置数据
            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();

            // 获取 KPI 原始数据
            long firstQuarterInYear = YearMonthWeekUtils.getFirstQuarterOfCurrentYear(query.getStartDate());
            List<HotelKpiV2MetricsDTO> metricsDTOList = listKpiMetricsDTOListV2(propertyDTO, firstQuarterInYear, 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()))));

            // 获取数据筛选范围
            YearMonth startYearMonth = YearMonthUtils.getYearMonth(String.valueOf(query.getStartDate()));
            YearMonth endYearMonth = YearMonthUtils.getYearMonth(String.valueOf(query.getEndDate()));

            // 获取季度范围内的 Monthly 数据，用于设置 Summary List
            List<HotelKpiV2MetricsDTO> monthlyMetricsDTOList = monthlyService.listKpiMetricsDTOListV2(propertyDTO, query.getStartDate(),
                    YearMonthUtils.parseYearMonthToLong(endYearMonth.plusMonths(2)), metricsConfigList);

            List<HotelKpiV2DTO> hotelKpiDTOList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonth; !indexYearMonth.isAfter(endYearMonth); indexYearMonth = indexYearMonth.plusMonths(3)) {
                // 获取当前季度的 KPI 原始数据
                long indexDate = YearMonthUtils.parseYearMonthToLong(indexYearMonth);
                Map<Integer, HotelKpiV2MetricsDTO> metricsType2KpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(indexDate, Collections.emptyMap());

                // 获取上一季度的 KPI 原始数据
                long lastIndexDate = YearMonthWeekUtils.getLastQuarterDate(indexDate);
                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, HotelKpiDateTypeCons.QUARTERLY);

                // 截取数据
                YearMonth finalIndexYearMonth = indexYearMonth;
                List<HotelKpiV2MetricsDTO> quarterlyMonthlyMetrics = monthlyMetricsDTOList.stream().filter(monthlyKpiDTO -> {
                    YearMonth entryYearMonth = YearMonthUtils.getYearMonth(String.valueOf(monthlyKpiDTO.getDate()));
                    return !entryYearMonth.isBefore(finalIndexYearMonth) && entryYearMonth.isBefore(finalIndexYearMonth.plusMonths(3));
                }).toList();

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

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

        /**
         * 获取 KPI 原始数据 V2
         *
         * @param propertyDTO       资产
         * @param startDate         开始日期
         * @param endDate           结束日期
         * @param metricsConfigList 指标配置集合
         * @return HotelKpiV2MetricsDTO 集合
         * @author zc
         * @date 2025/8/19
         */
        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<HotelKpiQuarterly> kpiQuarterlyList = listByDateRangeAndMetricsTypes(propertyDTO.getId(), startDate, endDate, metricsTypes);
            Map<Long, Map<Integer, HotelKpiQuarterly>> metricsType2KpiByDate = kpiQuarterlyList.stream()
                    .collect(Collectors.groupingBy(HotelKpiQuarterly::getMonth,
                            Collectors.toMap(HotelKpiQuarterly::getMetricsType, Function.identity())));

            YearMonth startYearMonth = YearMonthUtils.getYearMonth(String.valueOf(startDate));
            YearMonth endYearMonth = YearMonthUtils.getYearMonth(String.valueOf(endDate));

            List<HotelKpiV2MetricsDTO> kpiMetricsDTOList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonth; !indexYearMonth.isAfter(endYearMonth); indexYearMonth = indexYearMonth.plusMonths(3)) {
                long indexDate = YearMonthUtils.parseYearMonthToLong(indexYearMonth);
                Map<Integer, HotelKpiQuarterly> metricsType2Kpi = metricsType2KpiByDate.getOrDefault(indexDate, Collections.emptyMap());
                for (HotelKpiMetricsConfig metricsConfig : metricsConfigList) {
                    HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                            metricsConfig.getCalculationFactorConfig(), HotelKpiV2FactorConfigDTO.class);

                    HotelKpiV2MetricsDTO kpiMetricsDTO = new HotelKpiV2MetricsDTO(HotelKpiDateTypeCons.QUARTERLY, factorConfigDTO);
                    HotelKpiQuarterly kpiQuarterly = metricsType2Kpi.get(metricsConfig.getMetricsType());
                    if (Objects.isNull(kpiQuarterly)) {
                        kpiMetricsDTO.setDate(indexDate);
                        kpiMetricsDTO.setMetricsType(metricsConfig.getMetricsType());
                    } else {
                        List<HotelKpiV2OriginDetailDTO> originDetailDTOList = JsonUtils.convertStringToList(objectMapper,
                                kpiQuarterly.getDetail(), HotelKpiV2OriginDetailDTO.class);
                        kpiMetricsDTO.setDate(kpiQuarterly.getMonth());
                        kpiMetricsDTO.setMetricsType(kpiQuarterly.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;
        }

        /**
         * 组装指标数据日期范围 V2
         *
         * @param kpiDetailDTOList 指标数据
         * @param monthlyMetrics   季度内的月度指标数据
         * @author zc
         * @date 2025/8/19
         */
        private void packageDataRangeV2(List<HotelKpiV2DetailDTO> kpiDetailDTOList, List<HotelKpiV2MetricsDTO> monthlyMetrics) {
            Map<Integer, List<HotelKpiV2MetricsDTO>> metricsType2MonthlyKpiDTOList = monthlyMetrics.stream()
                    .collect(Collectors.groupingBy(HotelKpiV2MetricsDTO::getMetricsType));
            for (HotelKpiV2DetailDTO kpiDetailDTO : kpiDetailDTOList) {
                for (HotelKpiV2ItemDTO item : kpiDetailDTO.getItems()) {
                    for (HotelKpiV2MetricsDTO metric : item.getMetrics()) {
                        int dataRangeType = HotelKpiMetricsEnum.getDataRangeType(metric.getMetricsType());
                        metric.setDataRange(getDataRange(metric.getDate(), dataRangeType));

                        List<HotelKpiV2MetricsDTO> monthlyKpiDTOList = metricsType2MonthlyKpiDTOList.getOrDefault(metric.getMetricsType(),
                                Collections.emptyList());
                        // 将季度内的月度数据整合到 metric 的 summaryList 字段中
                        metric.getList().forEach(categoryDTO -> {
                            List<HotelKpiV2SummaryDTO> actualSummaryList = generateSummaryList(monthlyKpiDTOList,
                                    categoryDTO.getCategory(), HotelKpiValueCategoryCons.ACTUAL);
                            categoryDTO.getActual().setSummaryList(actualSummaryList);
                            List<HotelKpiV2SummaryDTO> budgetSummaryList = generateSummaryList(monthlyKpiDTOList,
                                    categoryDTO.getCategory(), HotelKpiValueCategoryCons.BUDGET);
                            categoryDTO.getBudget().setSummaryList(budgetSummaryList);
                            List<HotelKpiV2SummaryDTO> lySummaryList = generateSummaryList(monthlyKpiDTOList,
                                    categoryDTO.getCategory(), HotelKpiValueCategoryCons.LY);
                            categoryDTO.getLy().setSummaryList(lySummaryList);
                        });
                    }
                }
            }
        }

        /**
         * 生成summaryList数据
         *
         * @param monthlyMetrics 季度内的月度指标数据
         * @param category       分类
         * @param indexTitle     值类型 (actual, budget, ly)
         * @return summaryList
         * @author zc
         * @date 2025/8/21
         */
        private List<HotelKpiV2SummaryDTO> generateSummaryList(List<HotelKpiV2MetricsDTO> monthlyMetrics, int category, int indexTitle) {
            List<HotelKpiV2SummaryDTO> summaryList = Lists.newArrayList();

            // 按日期排序，确保月份按顺序排列
            List<HotelKpiV2MetricsDTO> sortedMonthlyMetrics = monthlyMetrics.stream()
                    .sorted(Comparator.comparing(HotelKpiV2MetricsDTO::getDate)).toList();

            for (HotelKpiV2MetricsDTO monthlyMetric : sortedMonthlyMetrics) {
                HotelKpiV2SummaryDTO summaryDTO = new HotelKpiV2SummaryDTO();
                summaryDTO.setDate(monthlyMetric.getDate());

                // 查找匹配的分类
                HotelKpiV2MetricsCategoryDTO monthlyCategory = monthlyMetric.getList().stream()
                        .filter(item -> item.getCategory().equals(category))
                        .findFirst().orElse(new HotelKpiV2MetricsCategoryDTO());

                BigDecimal value = null;
                if (indexTitle == HotelKpiValueCategoryCons.ACTUAL) {
                    value = hotelKpiService.getEffectiveValueV2(monthlyCategory.getActual());
                } else if (indexTitle == HotelKpiValueCategoryCons.BUDGET) {
                    value = hotelKpiService.getEffectiveValueV2(monthlyCategory.getBudget());
                } else if (indexTitle == HotelKpiValueCategoryCons.LY) {
                    value = hotelKpiService.getEffectiveValueV2(monthlyCategory.getLy());
                }
                summaryDTO.setValue(value);

                summaryList.add(summaryDTO);
            }
            return summaryList;
        }

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

            HotelKpiQuarterly originKpiQuarterly = getByDateAndMetricsType(propertyDTO.getId(), date, metricsType);
            List<HotelKpiV2OriginDetailDTO> originDetailDTOList;
            if (Objects.isNull(originKpiQuarterly)) {
                HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                        metricsConfig.getCalculationFactorConfig(), HotelKpiV2FactorConfigDTO.class);
                originDetailDTOList = HotelKpiV2OriginDetailDTO.initV2OriginDetailDTOList(DATE_TYPE, factorConfigDTO);
            } else {
                originDetailDTOList = JsonUtils.convertStringToList(objectMapper, originKpiQuarterly.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);
            });

            HotelKpiQuarterly hotelKpiQuarterly = new HotelKpiQuarterly()
                    .setPropertyId(propertyDTO.getId())
                    .setMonth(date).setMetricsType(metricsType)
                    .setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTOList));

            insertOrUpdate(hotelKpiQuarterly);

            // 触发重新计算当前指标的数值
            LocalDate localDate = YearMonthUtils.getYearMonth(String.valueOf(date)).atDay(1);
            commonSummaryService.setMetricsData(propertyDTO, DATE_TYPE, localDate, localDate, metricsType);
        }

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

            HotelKpiQuarterly originKpiQuarterly = getByDateAndMetricsType(propertyDTO.getId(), date, metricsType);
            List<HotelKpiV2OriginDetailDTO> originDetailDTOList;
            if (Objects.isNull(originKpiQuarterly)) {
                HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                        metricsConfig.getCalculationFactorConfig(), HotelKpiV2FactorConfigDTO.class);
                originDetailDTOList = HotelKpiV2OriginDetailDTO.initV2OriginDetailDTOList(DATE_TYPE, factorConfigDTO);
            } else {
                originDetailDTOList = JsonUtils.convertStringToList(objectMapper, originKpiQuarterly.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());
            });

            HotelKpiQuarterly hotelKpiQuarterly = new HotelKpiQuarterly()
                    .setPropertyId(propertyDTO.getId())
                    .setMonth(date).setMetricsType(metricsType)
                    .setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTOList));

            insertOrUpdate(hotelKpiQuarterly);

            // 触发重新计算当前指标的数值
            LocalDate localDate = YearMonthUtils.getYearMonth(String.valueOf(date)).atDay(1);
            commonSummaryService.setMetricsData(propertyDTO, DATE_TYPE, localDate, localDate, metricsType);
        }
    }