# HotelKpiAnalysisServiceImpl.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.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiCons;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiExplainConfig;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiMetricsConfig;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiMonthly;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.analysis.HotelKpiAnalysisDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.analysis.HotelKpiAnalysisDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.analysis.HotelKpiAnalysisPackageDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.explain.HotelKpiExplainDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiCalculationFactorDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiMetricsDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.HotelKpiV2MetricsCategoryDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.HotelKpiV2MetricsDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.HotelKpiV2ValueDTO;
    import cloud.wconnect.workperformance.pojo.query.hotelkpi.HotelKpiAnalysisQuery;
    import cloud.wconnect.workperformance.service.hotelkpi.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Maps;
    import jakarta.annotation.Resource;
    import org.springframework.stereotype.Service;

    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.DayOfWeek;
    import java.time.YearMonth;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * Hotel KPI Calculate 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-06-27
     */
    @Service
    @DS("work_performance")
    public class HotelKpiAnalysisServiceImpl implements IHotelKpiAnalysisService {

        @Resource
        private IHotelKpiExplainMonthlyService explainMonthlyService;
        @Resource
        private IHotelKpiExplainConfigService explainConfigService;
        @Resource
        private IHotelKpiService hotelKpiService;
        @Resource
        private IHotelKpiMonthlyService monthlyService;
        @Resource
        private IHotelKpiWeekStartDayService weekStartDayService;
        @Resource
        private BeanMapper beanMapper;

        /**
         * 获取指标对比分析数据
         *
         * @param propertyDTO   资产
         * @param query         查询条件
         * @param metricsConfig 指标配置
         * @return HotelKpiAnalysisPackageDTO
         * @author zc
         * @date 2024/6/27
         */
        @Override
        public HotelKpiAnalysisPackageDTO getAnalysis(PropertyDTO propertyDTO, HotelKpiAnalysisQuery query, HotelKpiMetricsConfig metricsConfig) {
            // 获取指标 Actual 分类的 Explain Config 公式
            HotelKpiExplainConfig explainConfig = explainConfigService.getByMetricsType(propertyDTO.getId());

            // 获取指标 Actual 分类的 Explain 数据
            Map<Long, HotelKpiExplainDTO> date2ExplainMonthlyDTO = explainMonthlyService.listByYearAndMetricsType(propertyDTO.getId(),
                            query.getTargetYear(), query.getMetricsType()).stream()
                    .collect(Collectors.toMap(HotelKpiExplainDTO::getDate, Function.identity()));

            // 提取查询 KPI 数据的年份
            List<Long> queryYears = Lists.newArrayList(query.getTargetYear());
            queryYears.addAll(query.getComparisonYears());
            queryYears.sort(Comparator.naturalOrder());

            // 获取指标数据
            List<HotelKpiMetricsDTO> metricsDTOList = getMetricsDTOList(propertyDTO, queryYears, metricsConfig);

            // 以月分组
            Map<Integer, Map<Integer, HotelKpiMetricsDTO>> year2KpiMetricsDTOByMonth =
                    metricsDTOList.stream().collect(Collectors.groupingBy(item ->
                                    YearMonthUtils.getYearMonth(item.getDate().toString()).getMonthValue(),
                            Collectors.toMap(item ->
                                    YearMonthUtils.getYearMonth(item.getDate().toString()).getYear(), Function.identity())));

            YearMonth startYearMonthForTarget = YearMonth.of(query.getTargetYear().intValue(), 1);
            YearMonth endYearMonthForTarget = startYearMonthForTarget.withMonth(12);
            long startMonthForTarget = YearMonthUtils.parseYearMonthToLong(startYearMonthForTarget);
            long endMonthForTarget = YearMonthUtils.parseYearMonthToLong(endYearMonthForTarget);

            // 提取 Target Year KPI 数据
            List<HotelKpiMetricsDTO> targetYearKpiMetricsDTOList = metricsDTOList.stream().filter(item ->
                    item.getDate() >= startMonthForTarget && item.getDate() <= endMonthForTarget).toList();

            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyDTO.getId());
            // 组装 Target setting 数据
            monthlyService.packageSettingValue(propertyDTO.getId(), targetYearKpiMetricsDTOList,
                    List.of(query.getMetricsType()), startMonthForTarget, endMonthForTarget, dayOfWeek);
            Map<Long, HotelKpiMetricsDTO> date2TargetKpiMetricsDTO = targetYearKpiMetricsDTOList.stream()
                    .collect(Collectors.toMap(HotelKpiMetricsDTO::getDate, Function.identity()));

            List<HotelKpiAnalysisDTO> analysisDTOList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonthForTarget; !indexYearMonth.isAfter(endYearMonthForTarget);
                 indexYearMonth = indexYearMonth.plusMonths(1)) {
                HotelKpiAnalysisDTO analysisDTO = packageKpiAnalysisDTO(propertyDTO, metricsConfig, indexYearMonth, explainConfig,
                        queryYears, year2KpiMetricsDTOByMonth, date2TargetKpiMetricsDTO, date2ExplainMonthlyDTO);
                analysisDTOList.add(analysisDTO);
            }

            // 组装 KPI Avg 数据
            packageKpiAnalysisAvgDTO(propertyDTO, analysisDTOList);

            // 整理顺序
            HotelKpiAnalysisDTO q1q2AnalysisAvgDTO = analysisDTOList.remove(12);
            analysisDTOList.add(6, q1q2AnalysisAvgDTO);

            return new HotelKpiAnalysisPackageDTO()
                    .setVersion(metricsConfig.getVersion())
                    .setList(analysisDTOList);
        }

        /**
         * 获取指标数据
         *
         * @param propertyDTO   资产
         * @param years         年份集合
         * @param metricsConfig 指标配置
         * @return HotelKpiMetricsDTO 集合
         * @author zc
         * @date 2025/8/21
         */
        private List<HotelKpiMetricsDTO> getMetricsDTOList(PropertyDTO propertyDTO, List<Long> years, HotelKpiMetricsConfig metricsConfig) {
            // 获取指标数据
            long startMonthOfYear = YearMonthUtils.parseYearMonthToLong(YearMonth.of(Collections.min(years).intValue(), 1));
            long endMonthOfYear = YearMonthUtils.parseYearMonthToLong(YearMonth.of(Collections.max(years).intValue(), 12));
            List<HotelKpiMonthly> monthlyList = monthlyService.listByDateRange(propertyDTO.getId(),
                    startMonthOfYear, endMonthOfYear, metricsConfig.getMetricsType());

            if (Objects.equals(metricsConfig.getVersion(), HotelKpiCons.V2)) {
                List<HotelKpiV2MetricsDTO> kpiV2MetricsDTOList = monthlyService.convertToMetricsDTOListV2(propertyDTO, monthlyList,
                        startMonthOfYear, endMonthOfYear, List.of(metricsConfig));
                // 将 HotelKpiV2MetricsDTO 转换为 HotelKpiMetricsDTO
                return convertV2ToV1Metrics(kpiV2MetricsDTOList);
            } else {
                return monthlyService.convertToMetricsDTOList(monthlyList, startMonthOfYear, endMonthOfYear,
                        List.of(metricsConfig.getMetricsType()));
            }
        }

        /**
         * 转换 V2 的指标数据为 V1 的指标数据
         *
         * @param kpiV2MetricsDTOList V2 的指标数据
         * @return V1 的指标数据
         * @author zc
         * @date 2025/8/21
         */
        private List<HotelKpiMetricsDTO> convertV2ToV1Metrics(List<HotelKpiV2MetricsDTO> kpiV2MetricsDTOList) {
            List<HotelKpiMetricsDTO> metricsDTOList = Lists.newArrayList();

            for (HotelKpiV2MetricsDTO v2Metrics : kpiV2MetricsDTOList) {
                HotelKpiMetricsDTO metricsDTO = new HotelKpiMetricsDTO();
                metricsDTO.setDate(v2Metrics.getDate());
                metricsDTO.setMetricsType(v2Metrics.getMetricsType());
                metricsDTO.setTitle(v2Metrics.getTitle());
                metricsDTO.setDescription(v2Metrics.getDescription());
                metricsDTO.setDataRange(v2Metrics.getDataRange());
                metricsDTO.setUnit(v2Metrics.getUnit());

                // 从 V2 的 list 中提取第一个分类作为 V1 的数据
                HotelKpiV2MetricsCategoryDTO categoryDTO = v2Metrics.getList().get(0);

                HotelKpiV2ValueDTO actualV2 = categoryDTO.getActual();
                HotelKpiValueDTO actual = HotelKpiValueDTO.builder()
                        .crawlValue(actualV2.getCrawlValue())
                        .manualValue(actualV2.getManualValue())
                        .calculationFactor(beanMapper.mapList(actualV2.getCalculationFactor(),
                                HotelKpiCalculationFactorDTO.class)).build();
                metricsDTO.setActual(actual);

                HotelKpiV2ValueDTO budgetV2 = categoryDTO.getBudget();
                HotelKpiValueDTO budget = HotelKpiValueDTO.builder()
                        .crawlValue(budgetV2.getCrawlValue())
                        .manualValue(budgetV2.getManualValue())
                        .calculationFactor(beanMapper.mapList(budgetV2.getCalculationFactor(),
                                HotelKpiCalculationFactorDTO.class)).build();
                metricsDTO.setTarget(budget);

                metricsDTOList.add(metricsDTO);
            }
            return metricsDTOList;
        }

        /**
         * 组装 KPI Avg 数据
         *
         * @param propertyDTO     资产
         * @param analysisDTOList 1 - 12 月 KPI 数据集合
         * @author zc
         * @date 2024/7/27
         */
        private void packageKpiAnalysisAvgDTO(PropertyDTO propertyDTO, List<HotelKpiAnalysisDTO> analysisDTOList) {
            List<BigDecimal> targetList = Lists.newArrayList();
            Map<Long, List<BigDecimal>> year2ActualList = Maps.newHashMap();
            for (HotelKpiAnalysisDTO analysisDTO : analysisDTOList) {
                targetList.add(analysisDTO.getTarget());

                List<HotelKpiAnalysisDetailDTO> actualList = analysisDTO.getActuals();
                for (HotelKpiAnalysisDetailDTO actual : actualList) {
                    List<BigDecimal> actualListOfYear = year2ActualList.getOrDefault(actual.getYear(), Lists.newArrayList());
                    actualListOfYear.add(actual.getActual());
                    year2ActualList.put(actual.getYear(), actualListOfYear);
                }
            }
            // 13: Q1-Q2 Avg; 14: Q3-Q4 Avg; 15: Q1-Q4 Avg
            for (int monthIndex = 13; monthIndex <= 15; monthIndex++) {
                analysisDTOList.add(calculateKpiAnalysisAvgDTO(propertyDTO, targetList, year2ActualList, monthIndex));
            }
        }

        /**
         * 计算指标对比分析数据平均值
         *
         * @param propertyDTO     资产
         * @param targetList      指标目标值
         * @param year2ActualList 实际值
         * @param monthIndex      月份坐标
         * @return HotelKpiAnalysisDTO
         * @author zc
         * @date 2024/7/27
         */
        private HotelKpiAnalysisDTO calculateKpiAnalysisAvgDTO(PropertyDTO propertyDTO, List<BigDecimal> targetList,
                                                               Map<Long, List<BigDecimal>> year2ActualList, int monthIndex) {
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            int startIndex = 0;
            int endIndex = 12;
            if (monthIndex == 13) {
                endIndex = 6;
            } else if (monthIndex == 14) {
                startIndex = 6;
            }

            HotelKpiAnalysisDTO q1q2AvgDTO = new HotelKpiAnalysisDTO();
            q1q2AvgDTO.setMonth(monthIndex);

            // 计算 Target Avg
            List<BigDecimal> targetValues = targetList.subList(startIndex, endIndex).stream().filter(Objects::nonNull).toList();
            BigDecimal totalTargetValue = targetValues.stream().reduce(BigDecimal::add).orElse(null);
            BigDecimal avgTargetValue = Objects.isNull(totalTargetValue) ? null :
                    totalTargetValue.divide(new BigDecimal(targetValues.size()), BaseConstant.DATA_PRECISION, roundingMode);
            q1q2AvgDTO.setTarget(avgTargetValue);

            // 计算 Actual Avg
            List<HotelKpiAnalysisDetailDTO> actualList = Lists.newArrayList();
            for (Map.Entry<Long, List<BigDecimal>> entry : year2ActualList.entrySet()) {
                List<BigDecimal> actualValues = entry.getValue().subList(startIndex, endIndex).stream().filter(Objects::nonNull).toList();
                BigDecimal totalActualValue = actualValues.stream().reduce(BigDecimal::add).orElse(null);
                BigDecimal avgActualValue = Objects.isNull(totalActualValue) ? null :
                        totalActualValue.divide(new BigDecimal(actualValues.size()), BaseConstant.DATA_PRECISION, roundingMode);

                HotelKpiAnalysisDetailDTO avgActualDTO = new HotelKpiAnalysisDetailDTO();
                avgActualDTO.setYear(entry.getKey());
                avgActualDTO.setActual(avgActualValue);
                actualList.add(avgActualDTO);
            }
            q1q2AvgDTO.setActuals(actualList);
            return q1q2AvgDTO;
        }

        /**
         * 组装 KPI 分析 DTO
         *
         * @param propertyDTO               资产
         * @param metricsConfig             指标配置
         * @param yearMonth                 年月
         * @param explainConfig             Explain 配置
         * @param years                     比较的年份
         * @param year2KpiMetricsDTOByMonth KPI 数据
         * @param date2TargetKpiMetricsDTO  目标年份的 KPI 数据
         * @param date2ExplainMonthlyDTO    Explain 数据
         * @return HotelKpiAnalysisDTO
         * @author zc
         * @date 2024/6/28
         */
        private HotelKpiAnalysisDTO packageKpiAnalysisDTO(PropertyDTO propertyDTO, HotelKpiMetricsConfig metricsConfig,
                                                          YearMonth yearMonth, HotelKpiExplainConfig explainConfig, List<Long> years,
                                                          Map<Integer, Map<Integer, HotelKpiMetricsDTO>> year2KpiMetricsDTOByMonth,
                                                          Map<Long, HotelKpiMetricsDTO> date2TargetKpiMetricsDTO,
                                                          Map<Long, HotelKpiExplainDTO> date2ExplainMonthlyDTO) {
            HotelKpiAnalysisDTO analysisDTO = new HotelKpiAnalysisDTO();
            // Month
            analysisDTO.setMonth(yearMonth.getMonthValue());

            // 当前年月
            long indexDateLong = YearMonthUtils.parseYearMonthToLong(yearMonth);

            // KPI 指标数据
            HotelKpiMetricsDTO targetKpiMetricsDTO = date2TargetKpiMetricsDTO.getOrDefault(indexDateLong, new HotelKpiMetricsDTO());
            // 组装目标年份的指标数值
            if (Objects.equals(metricsConfig.getVersion(), HotelKpiCons.V1)) {
                hotelKpiService.packageMetricsValue(propertyDTO, metricsConfig, targetKpiMetricsDTO);
            }
            HotelKpiValueDTO targetKpiValueDTO = targetKpiMetricsDTO.getTarget();
            BigDecimal target = hotelKpiService.getEffectiveValue(targetKpiValueDTO);
            analysisDTO.setTarget(target);

            // Show Explain
            HotelKpiExplainDTO explainMonthly = date2ExplainMonthlyDTO.get(indexDateLong);
            Boolean isShow = explainConfigService.getExplainIsShow(explainConfig, explainMonthly, metricsConfig.getMetricsType());

            // KPI 指标数据
            Map<Integer, HotelKpiMetricsDTO> year2KpiMetricsDTO = year2KpiMetricsDTOByMonth
                    .getOrDefault(yearMonth.getMonthValue(), Collections.emptyMap());

            // 组装 Target Value Content
            HotelKpiMetricsDTO targetYearMetricsDTO = year2KpiMetricsDTO.get(yearMonth.getYear());
            if (Objects.nonNull(targetYearMetricsDTO)) {
                analysisDTO.setValueContent(targetYearMetricsDTO.getExplain().getTarget().getValueContent());
            }

            List<HotelKpiAnalysisDetailDTO> actualList = Lists.newArrayList();
            for (long year : years) {
                HotelKpiAnalysisDetailDTO analysisDetailDTO = new HotelKpiAnalysisDetailDTO();
                analysisDetailDTO.setYear(year);
                HotelKpiMetricsDTO metricsDTO = year2KpiMetricsDTO.get((int) year);
                if (Objects.nonNull(metricsDTO)) {
                    // 组装其他年份的指标数值
                    if (Objects.equals(metricsConfig.getVersion(), HotelKpiCons.V1)) {
                        hotelKpiService.packageMetricsValue(propertyDTO, metricsConfig, metricsDTO);
                    }

                    HotelKpiValueDTO actualKpiValueDTO = metricsDTO.getActual();
                    BigDecimal actual = hotelKpiService.getEffectiveValue(actualKpiValueDTO);
                    analysisDetailDTO.setActual(actual);

                    // 设置 Actual 值内容
                    analysisDetailDTO.setValueContent(metricsDTO.getExplain().getActual().getValueContent());

                    // 获取 KPI 是否达标
                    Boolean isPass = explainConfigService.getKpiComparison(metricsConfig, actualKpiValueDTO,
                            analysisDetailDTO.getActual(), analysisDTO.getTarget());
                    if (Objects.nonNull(isShow)) {
                        if (Objects.nonNull(isPass) && isPass) {
                            analysisDetailDTO.setPass(!isShow);
                            analysisDetailDTO.setExplainType(explainConfig.getFormula());
                            analysisDetailDTO.setExplainValue(explainConfig.getValue());
                            analysisDetailDTO.setShowExplain(isShow);
                        }
                    } else {
                        analysisDetailDTO.setPass(isPass);
                    }
                }
                actualList.add(analysisDetailDTO);
            }
            analysisDTO.setActuals(actualList);

            return analysisDTO;
        }
    }