# HotelKpiMonthlyServiceImpl.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.HotelKpiMonthly;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiWarningComment;
    import cloud.wconnect.workperformance.mapper.hotelkpi.HotelKpiMonthlyMapper;
    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 Monthly 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-06-24
     */
    @Slf4j
    @Service
    @DS("work_performance")
    public class HotelKpiMonthlyServiceImpl extends ServiceImpl<HotelKpiMonthlyMapper, HotelKpiMonthly> implements IHotelKpiMonthlyService {

        @Resource
        private IHotelKpiService hotelKpiService;
        @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.MONTHLY;

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

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

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

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

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

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

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

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

        /**
         * 获取 Hotel KPI 数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return Hotel KPI 数据
         * @author zc
         * @date 2024/6/26
         */
        @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 firstMonthInYear = YearMonthWeekUtils.getFirstMonthOfCurrentYear(query.getStartDate());
            long nextMonth = YearMonthUtils.parseYearMonthToLong(endYearMonth.plusMonths(1));

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

            // 组装 Metrics Setting
            packageSettingValue(propertyDTO.getId(), metricsDTOList, metricsTypes, firstMonthInYear, nextMonth, 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(1)) {
                // 获取当前月的 KPI 原始数据
                long indexDate = YearMonthUtils.parseYearMonthToLong(indexYearMonth);
                Map<Integer, HotelKpiMetricsDTO> metricsType2KpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(indexDate, Collections.emptyMap());

                // 获取上一月的 KPI 原始数据
                long lastIndexDate = YearMonthWeekUtils.getLastMonthDate(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.MONTHLY);

                // 组装指标数据日期范围
                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);
                endLocalDate = localDate.with(TemporalAdjusters.lastDayOfMonth());
            } else if (dataRangeType == HotelKpiDataRangeTypeCons.SIX_MTD) {
                if (localDate.getMonthValue() <= 6) {
                    startLocalDate = localDate.withDayOfYear(1);
                } else {
                    startLocalDate = LocalDate.of(localDate.getYear(), 7, 1);
                }
                endLocalDate = localDate.with(TemporalAdjusters.lastDayOfMonth());
            } 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 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(1);
            }
            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(1);
            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<HotelKpiMonthly> kpiMonthlyList = listByDateRangeAndMetricsTypes(propertyId, startDate, endDate, metricsTypes);
            // 转换数据为 DTO
            return convertToMetricsDTOList(kpiMonthlyList, startDate, endDate, metricsTypes);
        }

        /**
         * 组装 Target, Cure setting 数据
         *
         * @param propertyId     资产 id
         * @param metricsDTOList 指标数据 DTO 容器
         * @param metricsTypes   指标类型集合
         * @param startDate      开始日期
         * @param endDate        结束日期
         * @param dayOfWeek      周几
         * @author zc
         * @date 2024/6/26
         */
        @Override
        public 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.listMonthlySettingByDateRange(propertyId, metricsTypes, startDate,
                            endDate, metricsDTOList, dayOfWeek);
            // 获取 Target、Cure 设置数据
            List<HotelKpiMetricsSettingTargetDTO> metricsSettingTargetDTOS =
                    metricsSettingTargetService.listMonthlySettingByDateRange(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) {
            HotelKpiMonthly originKpiMonthly = getByDateAndMetricsType(propertyDTO.getId(), metricsDTO.getDate(), metricsDTO.getMetricsType());
            HotelKpiOriginDetailDTO originDetailDTO;
            if (Objects.isNull(originKpiMonthly)) {
                originDetailDTO = new HotelKpiOriginDetailDTO();
            } else {
                originDetailDTO = JsonUtils.convertStringToObject(objectMapper, originKpiMonthly.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);

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

            insertOrUpdate(updateKpiMonthly);
        }

        /**
         * 转换数据为 DTO
         *
         * @param kpiMonthlyList 指标数据
         * @param startDate      开始日期(yyyyMM)
         * @param endDate        结束日期(yyyyMM)
         * @param metricsTypes   指标类型
         * @return HotelKpiMetricsDTO 集合
         * @author zc
         * @date 2024/6/28
         */
        @Override
        public List<HotelKpiMetricsDTO> convertToMetricsDTOList(List<HotelKpiMonthly> kpiMonthlyList,
                                                                long startDate, long endDate, List<Integer> metricsTypes) {
            Map<Long, Map<Integer, HotelKpiMonthly>> metricsType2KpiByDate = kpiMonthlyList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMonthly::getMonth,
                            Collectors.toMap(HotelKpiMonthly::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(1)) {
                long indexDate = YearMonthUtils.parseYearMonthToLong(indexYearMonth);
                Map<Integer, HotelKpiMonthly> metricsType2Kpi = metricsType2KpiByDate.getOrDefault(indexDate, Collections.emptyMap());
                for (Integer metricsType : metricsTypes) {
                    HotelKpiMetricsDTO kpiMetricsDTO = new HotelKpiMetricsDTO();
                    HotelKpiMonthly kpiMonthly = metricsType2Kpi.get(metricsType);
                    if (Objects.isNull(kpiMonthly)) {
                        kpiMetricsDTO.setDate(indexDate);
                        kpiMetricsDTO.setMetricsType(metricsType);
                    } else {
                        HotelKpiOriginDetailDTO originDetailDTO = JsonUtils.convertStringToObject(objectMapper,
                                kpiMonthly.getDetail(), HotelKpiOriginDetailDTO.class);
                        kpiMetricsDTO = beanMapper.map(kpiMonthly, HotelKpiMetricsDTO.class, dto -> {
                            dto.setDate(kpiMonthly.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;
        }

        /**
         * 获取 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 firstMonthInYear = YearMonthWeekUtils.getFirstMonthOfCurrentYear(query.getStartDate());
            List<HotelKpiV2MetricsDTO> metricsDTOList = listKpiMetricsDTOListV2(propertyDTO, firstMonthInYear, 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()));

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

                // 获取上一月的 KPI 原始数据
                long lastIndexDate = YearMonthWeekUtils.getLastMonthDate(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.MONTHLY);

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

                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
         */
        @Override
        public List<HotelKpiV2MetricsDTO> listKpiMetricsDTOListV2(PropertyDTO propertyDTO, long startDate, long endDate,
                                                                  List<HotelKpiMetricsConfig> metricsConfigList) {
            List<Integer> metricsTypes = metricsConfigList.stream().map(HotelKpiMetricsConfig::getMetricsType).toList();

            List<HotelKpiMonthly> kpiMonthlyList = listByDateRangeAndMetricsTypes(propertyDTO.getId(), startDate, endDate, metricsTypes);
            // 转换数据为 DTO
            return convertToMetricsDTOListV2(propertyDTO, kpiMonthlyList, startDate, endDate, metricsConfigList);
        }

        /**
         * 转换数据为 DTO
         *
         * @param propertyDTO       资产
         * @param kpiMonthlyList    指标数据
         * @param startDate         开始日期(yyyyMM)
         * @param endDate           结束日期(yyyyMM)
         * @param metricsConfigList 指标配置集合
         * @return HotelKpiV2MetricsDTO 集合
         * @author zc
         * @date 2025/8/19
         */
        @Override
        public List<HotelKpiV2MetricsDTO> convertToMetricsDTOListV2(PropertyDTO propertyDTO, List<HotelKpiMonthly> kpiMonthlyList,
                                                                    long startDate, long endDate, List<HotelKpiMetricsConfig> metricsConfigList) {
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            Map<Long, Map<Integer, HotelKpiMonthly>> metricsType2KpiByDate = kpiMonthlyList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMonthly::getMonth,
                            Collectors.toMap(HotelKpiMonthly::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(1)) {
                long indexDate = YearMonthUtils.parseYearMonthToLong(indexYearMonth);
                Map<Integer, HotelKpiMonthly> 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);
                    HotelKpiMonthly kpiMonthly = metricsType2Kpi.get(metricsConfig.getMetricsType());
                    if (Objects.isNull(kpiMonthly)) {
                        kpiMetricsDTO.setDate(indexDate);
                        kpiMetricsDTO.setMetricsType(metricsConfig.getMetricsType());
                    } else {
                        List<HotelKpiV2OriginDetailDTO> originDetailDTOList = JsonUtils.convertStringToList(objectMapper,
                                kpiMonthly.getDetail(), HotelKpiV2OriginDetailDTO.class);
                        kpiMetricsDTO.setDate(kpiMonthly.getMonth());
                        kpiMetricsDTO.setMetricsType(kpiMonthly.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 指标数据
         * @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(getDataRange(metric.getDate(), dataRangeType));
                    }
                }
            }
        }

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

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

            HotelKpiMonthly hotelKpiMonthly = new HotelKpiMonthly()
                    .setPropertyId(propertyDTO.getId())
                    .setMonth(date).setMetricsType(metricsType)
                    .setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTOList));
            insertOrUpdate(hotelKpiMonthly);

            // 触发重新计算当前指标的数值
            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
        @Transactional(rollbackFor = Exception.class)
        public void saveCrawlValueV2(PropertyDTO propertyDTO, HotelKpiV2CrawlValueDTO crawlValueDTO, HotelKpiMetricsConfig metricsConfig) {
            long date = crawlValueDTO.getDate();
            int metricsType = crawlValueDTO.getMetricsType();

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

            HotelKpiMonthly hotelKpiMonthly = new HotelKpiMonthly()
                    .setPropertyId(propertyDTO.getId())
                    .setMonth(date).setMetricsType(metricsType)
                    .setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTOList));
            insertOrUpdate(hotelKpiMonthly);

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