# HotelKpiYearlyServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiCons;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiDateTypeCons;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiValueCategoryCons;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiMetricsConfig;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiWarningComment;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiYearly;
    import cloud.wconnect.workperformance.mapper.hotelkpi.HotelKpiYearlyMapper;
    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.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.ZoneId;
    import java.time.temporal.TemporalAdjusters;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * Hotel KPI Yearly 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-06-24
     */
    @Slf4j
    @Service
    @DS("work_performance")
    public class HotelKpiYearlyServiceImpl extends ServiceImpl<HotelKpiYearlyMapper, HotelKpiYearly> implements IHotelKpiYearlyService {

        @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.YEARLY;

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

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

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

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

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

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

        /**
         * 根据日期、指标类型 获取 KPI 原始数据
         *
         * @param propertyId  资产 id
         * @param date        日期(yyyy)
         * @param metricsType 指标类型
         * @return HotelKpiYearly
         * @author zc
         * @date 2024/8/6
         */
        @Override
        public HotelKpiYearly getByDateAndMetricsType(int propertyId, long date, int metricsType) {
            return getOne(new LambdaQueryWrapper<HotelKpiYearly>()
                    .eq(HotelKpiYearly::getPropertyId, propertyId)
                    .eq(HotelKpiYearly::getYear, date)
                    .eq(HotelKpiYearly::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) {
            // 获取有效的指标
            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));

            // 多获取前一年的数据用于设置对比结果
            long lastYear = query.getStartDate() - 1;

            // 提取非父级数据的指标配置
            List<Integer> metricsTypes = effectiveMetrics.stream().map(HotelKpiMetricsConfig::getMetricsType)
                    .filter(metricsType -> metricsType > 0).toList();

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

            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyDTO.getId());
            // 组装 Metrics Setting
            packageSettingValue(propertyDTO.getId(), metricsDTOList, metricsTypes, lastYear, query.getEndDate(), 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<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 lastMonthYearMonth = YearMonth.now(ZoneId.of(propertyDTO.getTimezone())).minusMonths(1);

            List<HotelKpiDTO> hotelKpiDTOList = Lists.newArrayList();
            for (long indexYear = query.getStartDate(); indexYear <= query.getEndDate(); indexYear++) {
                // 获取当前年的 KPI 原始数据
                Map<Integer, HotelKpiMetricsDTO> metricsType2KpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(indexYear, Collections.emptyMap());

                // 获取上一年的 KPI 原始数据
                long lastIndexYear = indexYear - 1;
                Map<Integer, HotelKpiMetricsDTO> metricsType2LastKpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(lastIndexYear, Collections.emptyMap());

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

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

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

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

        /**
         * 组装指标数据日期范围
         *
         * @param kpiDetailDTOList   指标数据
         * @param lastMonthYearMonth 当前上个月日期
         * @author zc
         * @date 2025/3/12
         */
        private void packageDataRange(List<HotelKpiDetailDTO> kpiDetailDTOList, YearMonth lastMonthYearMonth) {
            for (HotelKpiDetailDTO kpiDetailDTO : kpiDetailDTOList) {
                for (HotelKpiItemDTO item : kpiDetailDTO.getItems()) {
                    for (HotelKpiMetricsDTO metric : item.getMetrics()) {
                        metric.setDataRange(getDataRange(metric.getDate(), lastMonthYearMonth));
                    }
                }
            }
        }

        /**
         * 获取指标数据日期范围
         *
         * @param date               日期（yyyy）
         * @param lastMonthYearMonth 当前上个月日期
         * @return HotelKpiDataRangeDTO
         * @author zc
         * @date 2025/3/12
         */
        private HotelKpiDataRangeDTO getDataRange(long date, YearMonth lastMonthYearMonth) {
            LocalDate localDate = YearMonth.of((int) date, 1).atDay(1);

            HotelKpiDataRangeDTO dataRangeDTO = new HotelKpiDataRangeDTO();

            LocalDate startLocalDate = localDate.withDayOfYear(1);
            dataRangeDTO.setStartDate(Long.parseLong(DateTimeUtils.localDateToStr(startLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD)));

            int asOfMonth = date < lastMonthYearMonth.getYear() ? 12 : lastMonthYearMonth.getMonthValue();
            LocalDate endLocalDate = localDate.withMonth(asOfMonth).with(TemporalAdjusters.lastDayOfMonth());
            dataRangeDTO.setEndDate(Long.parseLong(DateTimeUtils.localDateToStr(endLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD)));
            return dataRangeDTO;
        }

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

            List<HotelKpiMetricsDTO> kpiMetricsDTOList = Lists.newArrayList();
            for (long indexYear = startDate; indexYear <= endDate; indexYear++) {
                Map<Integer, HotelKpiYearly> metricsType2Kpi = metricsType2KpiByDate.getOrDefault(indexYear, Collections.emptyMap());
                for (Integer metricsType : metricsTypes) {
                    HotelKpiMetricsDTO kpiMetricsDTO = new HotelKpiMetricsDTO();
                    HotelKpiYearly kpiYearly = metricsType2Kpi.get(metricsType);
                    if (Objects.isNull(kpiYearly)) {
                        kpiMetricsDTO.setDate(indexYear);
                        kpiMetricsDTO.setMetricsType(metricsType);
                    } else {
                        HotelKpiOriginDetailDTO originDetailDTO = JsonUtils.convertStringToObject(objectMapper,
                                kpiYearly.getDetail(), HotelKpiOriginDetailDTO.class);
                        kpiMetricsDTO = beanMapper.map(kpiYearly, HotelKpiMetricsDTO.class, dto -> {
                            dto.setDate(kpiYearly.getYear());
                            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/26
         */
        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.listYearlySettingByDateRange(propertyId, metricsTypes, startDate,
                            endDate, metricsDTOList, dayOfWeek);
            // 获取 Target、Cure 设置数据
            List<HotelKpiMetricsSettingTargetDTO> metricsSettingTargetDTOS =
                    metricsSettingTargetService.listYearlySettingByDateRange(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) {
            HotelKpiYearly originKpiYearly = getByDateAndMetricsType(propertyDTO.getId(), metricsDTO.getDate(), metricsDTO.getMetricsType());
            HotelKpiOriginDetailDTO originDetailDTO;
            if (Objects.isNull(originKpiYearly)) {
                originDetailDTO = new HotelKpiOriginDetailDTO();
            } else {
                originDetailDTO = JsonUtils.convertStringToObject(objectMapper, originKpiYearly.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);

            HotelKpiYearly updateKpiYearly = new HotelKpiYearly();
            updateKpiYearly.setPropertyId(propertyDTO.getId());
            updateKpiYearly.setYear(metricsDTO.getDate());
            updateKpiYearly.setMetricsType(metricsDTO.getMetricsType());
            updateKpiYearly.setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTO));

            insertOrUpdate(updateKpiYearly);
        }

        /**
         * 获取 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));

            // 多获取前一年的数据用于设置对比结果
            long lastYear = query.getStartDate() - 1;

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

            // 获取 KPI 原始数据
            List<HotelKpiV2MetricsDTO> metricsDTOList = listKpiMetricsDTOListV2(propertyDTO, lastYear, 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 = YearMonth.of(query.getStartDate().intValue(), 1);
            long startMonthOfYear = YearMonthUtils.parseYearMonthToLong(startYearMonth);
            YearMonth endYearMonth = YearMonth.of(query.getEndDate().intValue(), 12);
            long endMonthOfYear = YearMonthUtils.parseYearMonthToLong(endYearMonth);
            // 获取季度范围内的 Monthly 数据，用于设置 Summary List
            List<HotelKpiV2MetricsDTO> monthlyMetricsDTOList = monthlyService.listKpiMetricsDTOListV2(propertyDTO, startMonthOfYear,
                    endMonthOfYear, metricsConfigList);

            // 获取当前日期上一月的日期
            YearMonth lastMonthYearMonth = YearMonth.now(ZoneId.of(propertyDTO.getTimezone())).minusMonths(1);

            List<HotelKpiV2DTO> hotelKpiDTOList = Lists.newArrayList();
            for (long indexYear = query.getStartDate(); indexYear <= query.getEndDate(); indexYear++) {
                // 获取当前年的 KPI 原始数据
                Map<Integer, HotelKpiV2MetricsDTO> metricsType2KpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(indexYear, Collections.emptyMap());

                // 获取上一年的 KPI 原始数据
                long lastIndexYear = indexYear - 1;
                Map<Integer, HotelKpiV2MetricsDTO> metricsType2LastKpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(lastIndexYear, Collections.emptyMap());

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

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

                // 截取数据
                int asOfMonth = indexYear < lastMonthYearMonth.getYear() ? 12 : lastMonthYearMonth.getMonthValue();
                long indexStartMonth = YearMonthUtils.parseYearMonthToLong(YearMonth.of((int) indexYear, 1));
                long indexEndMonth = YearMonthUtils.parseYearMonthToLong(YearMonth.of((int) indexYear, asOfMonth));
                List<HotelKpiV2MetricsDTO> effectiveMonthlyList = monthlyMetricsDTOList.stream().filter(item ->
                        item.getDate() >= indexStartMonth && item.getDate() <= indexEndMonth).toList();

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

                hotelKpiDTOList.add(HotelKpiV2DTO.builder()
                        .date(indexYear)
                        .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<HotelKpiYearly> kpiYearlyList = listByDateRangeAndMetricsTypes(propertyDTO.getId(), startDate, endDate, metricsTypes);
            Map<Long, Map<Integer, HotelKpiYearly>> metricsType2KpiByDate = kpiYearlyList.stream()
                    .collect(Collectors.groupingBy(HotelKpiYearly::getYear,
                            Collectors.toMap(HotelKpiYearly::getMetricsType, Function.identity())));

            List<HotelKpiV2MetricsDTO> kpiMetricsDTOList = Lists.newArrayList();
            for (long indexYear = startDate; indexYear <= endDate; indexYear++) {
                Map<Integer, HotelKpiYearly> metricsType2Kpi = metricsType2KpiByDate.getOrDefault(indexYear, Collections.emptyMap());
                for (HotelKpiMetricsConfig metricsConfig : metricsConfigList) {
                    HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                            metricsConfig.getCalculationFactorConfig(), HotelKpiV2FactorConfigDTO.class);

                    HotelKpiV2MetricsDTO kpiMetricsDTO = new HotelKpiV2MetricsDTO(DATE_TYPE, factorConfigDTO);
                    HotelKpiYearly kpiYearly = metricsType2Kpi.get(metricsConfig.getMetricsType());
                    if (Objects.isNull(kpiYearly)) {
                        kpiMetricsDTO.setDate(indexYear);
                        kpiMetricsDTO.setMetricsType(metricsConfig.getMetricsType());
                    } else {
                        List<HotelKpiV2OriginDetailDTO> originDetailDTOList = JsonUtils.convertStringToList(objectMapper,
                                kpiYearly.getDetail(), HotelKpiV2OriginDetailDTO.class);
                        kpiMetricsDTO.setDate(kpiYearly.getYear());
                        kpiMetricsDTO.setMetricsType(kpiYearly.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 lastMonthYearMonth 当前上个月日期
         * @author zc
         * @date 2025/8/19
         */
        private void packageDataRangeV2(List<HotelKpiV2DetailDTO> kpiDetailDTOList, List<HotelKpiV2MetricsDTO> monthlyMetrics,
                                        YearMonth lastMonthYearMonth) {
            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()) {
                        metric.setDataRange(getDataRange(metric.getDate(), lastMonthYearMonth));

                        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();

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

            HotelKpiYearly hotelKpiYearly = new HotelKpiYearly()
                    .setPropertyId(propertyDTO.getId())
                    .setYear(date).setMetricsType(metricsType)
                    .setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTOList));

            insertOrUpdate(hotelKpiYearly);

            // 触发重新计算当前指标的数值
            LocalDate localDate = YearMonth.of((int) date, 1).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();

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

            HotelKpiYearly hotelKpiYearly = new HotelKpiYearly()
                    .setPropertyId(propertyDTO.getId())
                    .setYear(date).setMetricsType(metricsType)
                    .setDetail(JsonUtils.convertObjToString(objectMapper, originDetailDTOList));

            insertOrUpdate(hotelKpiYearly);

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