# HotelKpiMetricsSettingServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.workperformance.cons.hotelkpi.*;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiMetricsConfig;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiMetricsSetting;
    import cloud.wconnect.workperformance.mapper.hotelkpi.HotelKpiMetricsSettingMapper;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfig.HotelKpiCalculationFactorConfigDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfig.HotelKpiCalculationFactorConfigItemDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiCalculationFactorDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiComparisonRuleDTO;
    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.HotelKpiV2ComparisonRuleDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.*;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.actual.HotelKpiMetricsSettingActualDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.actual.HotelKpiMetricsSettingActualValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.actual.HotelKpiMetricsSettingIndependentActualDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.target.HotelKpiMetricsSettingIndependentTargetDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.target.HotelKpiMetricsSettingTargetDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.target.HotelKpiMetricsSettingTargetValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.settingv2.HotelKpiV2SettingDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.settingv2.HotelKpiV2SettingItemDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.settingv2.HotelKpiV2SettingMetricsDTO;
    import cloud.wconnect.workperformance.service.hotelkpi.IHotelKpiMetricsConfigService;
    import cloud.wconnect.workperformance.service.hotelkpi.IHotelKpiMetricsSettingService;
    import cloud.wconnect.workperformance.service.hotelkpi.IHotelKpiWeekStartDayService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
    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 org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.collections.MapUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.lang.Nullable;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.time.DayOfWeek;
    import java.time.LocalDate;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * Hotel KPI Metrics Setting 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-06-24
     */
    @Service
    @DS("work_performance")
    public class HotelKpiMetricsSettingServiceImpl extends ServiceImpl<HotelKpiMetricsSettingMapper, HotelKpiMetricsSetting> implements IHotelKpiMetricsSettingService {
        @Resource
        private IHotelKpiMetricsConfigService metricsConfigService;
        @Resource
        private IHotelKpiWeekStartDayService weekStartDayService;
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private ObjectMapper objectMapper;

        /**
         * 获取指定年的设置数据
         *
         * @param propertyId 资产 id
         * @param year       年份(yyyy)
         * @return HotelKpiSettingDetailDTO 集合
         * @author zc
         * @date 2024/6/28
         */
        @Override
        public List<HotelKpiSettingDetailDTO> listSetting(int propertyId, long year) {
            // 获取有效的指标
            List<HotelKpiMetricsConfig> effectiveMetricsConfigs = metricsConfigService.listEffectiveMetricsOfYear(propertyId,
                    HotelKpiDateTypeCons.YEARLY, year, year, Boolean.FALSE, HotelKpiCons.V1);
            Map<Integer, List<HotelKpiMetricsConfig>> metricsParentId2Configs = effectiveMetricsConfigs.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsConfig::getParentId));

            // 包含的指标类型
            List<Integer> metricsTypes = effectiveMetricsConfigs.stream().map(HotelKpiMetricsConfig::getMetricsType)
                    .filter(metricsType -> metricsType > 0).toList();

            // 获取指标设置数据
            Map<Integer, Map<Integer, List<HotelKpiMetricsSetting>>> metricsSettingMap = listByYearRangeAndMetricsTypes(propertyId,
                    metricsTypes, year, year, null).stream().collect(Collectors.groupingBy(HotelKpiMetricsSetting::getMetricsType,
                    Collectors.groupingBy(HotelKpiMetricsSetting::getValueCategory)));

            return effectiveMetricsConfigs.stream().filter(metrics -> metrics.getParentId() == 0)
                    .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                    .map(parent0Metrics -> {
                        List<HotelKpiSettingItemDTO> items = metricsParentId2Configs.get(parent0Metrics.getId()).stream()
                                .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                                .map(parent1Metrics -> {
                                    List<HotelKpiSettingMetricsDTO> metrics = metricsParentId2Configs.get(parent1Metrics.getId()).stream()
                                            .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                                            .map(childMetrics -> {
                                                Map<Integer, List<HotelKpiMetricsSetting>> valueCategory2Settings =
                                                        metricsSettingMap.getOrDefault(childMetrics.getMetricsType(), Collections.emptyMap());
                                                Integer actualRuleCategory = getRuleCategory(valueCategory2Settings.getOrDefault(HotelKpiValueCategoryCons.ACTUAL,
                                                        Collections.emptyList()));
                                                Integer targetRuleCategory = getRuleCategory(valueCategory2Settings.getOrDefault(HotelKpiValueCategoryCons.TARGET,
                                                        Collections.emptyList()));

                                                // 对比规则 json 转换
                                                HotelKpiComparisonRuleDTO comparisonRuleDTO = JsonUtils.convertStringToObject(objectMapper,
                                                        childMetrics.getComparisonRule(), HotelKpiComparisonRuleDTO.class);

                                                return HotelKpiSettingMetricsDTO.builder()
                                                        .metricsId(childMetrics.getId())
                                                        .metricsType(childMetrics.getMetricsType())
                                                        .title(childMetrics.getTitle())
                                                        .unit(childMetrics.getUnit())
                                                        .description(childMetrics.getDescription())
                                                        .actualRuleCategory(actualRuleCategory)
                                                        .targetRuleCategory(targetRuleCategory)
                                                        .comparisonSetting(comparisonRuleDTO)
                                                        .dataSource(childMetrics.getDataSource())
                                                        .build();
                                            }).toList();
                                    return HotelKpiSettingItemDTO.builder()
                                            .title(parent1Metrics.getTitle())
                                            .metrics(metrics)
                                            .build();
                                }).toList();
                        return HotelKpiSettingDetailDTO.builder()
                                .title(parent0Metrics.getTitle())
                                .items(items).build();
                    }).toList();
        }

        /**
         * 获取 Setting 规则分类
         *
         * @param settingList 指标设置
         * @return java.lang.Integer
         * @author zc
         * @date 2025/1/14
         */
        private Integer getRuleCategory(List<HotelKpiMetricsSetting> settingList) {
            if (settingList.size() == 1) {
                return settingList.get(0).getSettingCategory();
            } else if (settingList.size() == 2) {
                return HotelKpiSettingCategoryEnum.CALCULATION_FACTOR.getCategory();
            }
            return null;
        }

        /**
         * 保存指标设置
         *
         * @param metricsConfig     指标配置
         * @param year              年份(yyyy)
         * @param settingMetricsDTO 设置指标 DTO
         * @param operatorId        操作人 id
         * @author zc
         * @date 2024/8/7
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveSettingMetrics(HotelKpiMetricsConfig metricsConfig, long year,
                                       HotelKpiSettingMetricsDTO settingMetricsDTO, int operatorId) {
            // 保存指标配置数据
            metricsConfigService.update(new LambdaUpdateWrapper<HotelKpiMetricsConfig>()
                    .eq(HotelKpiMetricsConfig::getId, metricsConfig.getId())
                    .set(HotelKpiMetricsConfig::getTitle, settingMetricsDTO.getTitle())
                    .set(HotelKpiMetricsConfig::getUnit, StringUtils.isBlank(settingMetricsDTO.getUnit()) ?
                            "" : settingMetricsDTO.getUnit())
                    .set(HotelKpiMetricsConfig::getDescription, settingMetricsDTO.getDescription())
                    .set(HotelKpiMetricsConfig::getComparisonRule, JsonUtils.convertObjToString(objectMapper,
                            settingMetricsDTO.getComparisonSetting()))
                    .set(HotelKpiMetricsConfig::getDataSource, Objects.isNull(settingMetricsDTO.getDataSource()) ?
                            "{}" : settingMetricsDTO.getDataSource()));
        }

        /**
         * 克隆目标年份的指标配置 To 指定年份、指定资产
         *
         * @param propertyId 资产 id
         * @param year       年份(yyyy)
         * @param copyToDTO  Copy To DTO
         * @param operatorId 操作人 id
         * @author zc
         * @date 2024/6/29
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void copyTo(int propertyId, long year, HotelKpiSettingCopyToDTO copyToDTO, int operatorId) {
            // 获取指标设置数据
            Map<Integer, List<HotelKpiMetricsSetting>> metricsType2SettingList = listByYear(propertyId, year).stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsSetting::getMetricsType));
            if (MapUtils.isEmpty(metricsType2SettingList)) {
                return;
            }

            List<Integer> toPropertyIds = copyToDTO.getPropertyIds();
            long toYear = copyToDTO.getToYear();
            // 获取资产的指标 config 数据
            Map<Integer, List<Integer>> propertyId2MetricsTypeList = metricsConfigService.listEffectiveMetricsOfYearByPropertyIds(toPropertyIds,
                    HotelKpiDateTypeCons.YEARLY, toYear).stream().collect(Collectors.groupingBy(HotelKpiMetricsConfig::getPropertyId,
                    Collectors.mapping(HotelKpiMetricsConfig::getMetricsType, Collectors.toList())));

            // 计算目标年的周数
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyId);
            int weekCountInYear = DateTimeUtils.getWeekStartDayCount(LocalDate.of((int) toYear, 1, 1),
                    dayOfWeek, HotelKpiDateTypeCons.YEARLY);

            long nowTimestamp = DateTimeUtils.nowTimestamp();
            List<HotelKpiMetricsSetting> toSettingList = Lists.newArrayList();
            for (int toPropertyId : toPropertyIds) {
                // 如果是当前资产 && 当前年份，则跳过
                if (propertyId == toPropertyId && year == toYear) {
                    continue;
                }
                List<Integer> metricsTypeList = propertyId2MetricsTypeList.get(toPropertyId);
                if (CollectionUtils.isEmpty(metricsTypeList)) {
                    continue;
                }
                for (int metricsType : metricsTypeList) {
                    List<HotelKpiMetricsSetting> fromSettingList = metricsType2SettingList.get(metricsType);
                    if (Objects.isNull(fromSettingList)) {
                        continue;
                    }
                    toSettingList.addAll(beanMapper.mapList(fromSettingList, HotelKpiMetricsSetting.class, (fromSetting, toSetting) -> {
                        toSetting.setPropertyId(toPropertyId);
                        toSetting.setYear(toYear);
                        toSetting.setUpdatedBy(operatorId);
                        toSetting.setUpdatedTime(nowTimestamp);
                        generateIndependentDetail(weekCountInYear, fromSetting, toSetting);
                    }));
                }
            }
            // 批量保存数据
            insertOrUpdateBatchByUnique(toSettingList);
        }

        /**
         * 生成 Independent Detail
         *
         * @param weekCountInYear 全年的周数量
         * @param formSetting     指标设置 from
         * @param toSetting       指标设置 to
         * @author zc
         * @date 2025/3/11
         */
        private void generateIndependentDetail(int weekCountInYear, HotelKpiMetricsSetting formSetting, HotelKpiMetricsSetting toSetting) {
            if (formSetting.getSettingCategory() != HotelKpiSettingCategoryEnum.INDEPENDENT.getCategory()) {
                return;
            }
            if (Objects.equals(formSetting.getValueCategory(), HotelKpiValueCategoryCons.ACTUAL)) {
                HotelKpiMetricsSettingIndependentActualDTO settingIndependentDTO = JsonUtils.convertStringToObject(objectMapper,
                        formSetting.getDetail(), HotelKpiMetricsSettingIndependentActualDTO.class);
                Map<Integer, HotelKpiMetricsSettingActualValueDTO> index2ValueDTO = settingIndependentDTO.getWeek().stream()
                        .collect(Collectors.toMap(HotelKpiMetricsSettingActualValueDTO::getIndex, Function.identity()));

                List<HotelKpiMetricsSettingActualValueDTO> weekList = Lists.newArrayListWithExpectedSize(weekCountInYear);
                for (int index = 1; index <= weekCountInYear; index++) {
                    HotelKpiMetricsSettingActualValueDTO dto = index2ValueDTO.getOrDefault(index,
                            new HotelKpiMetricsSettingActualValueDTO(index, null));
                    weekList.add(dto);
                }
                settingIndependentDTO.setWeek(weekList);
                toSetting.setDetail(JsonUtils.convertObjToString(objectMapper, settingIndependentDTO));
            } else {
                HotelKpiMetricsSettingIndependentTargetDTO settingIndependentDTO = JsonUtils.convertStringToObject(objectMapper,
                        formSetting.getDetail(), HotelKpiMetricsSettingIndependentTargetDTO.class);
                Map<Integer, HotelKpiMetricsSettingTargetValueDTO> index2ValueDTO = settingIndependentDTO.getWeek().stream()
                        .collect(Collectors.toMap(HotelKpiMetricsSettingTargetValueDTO::getIndex, Function.identity()));

                List<HotelKpiMetricsSettingTargetValueDTO> weekList = Lists.newArrayListWithExpectedSize(weekCountInYear);
                for (int index = 1; index <= weekCountInYear; index++) {
                    HotelKpiMetricsSettingTargetValueDTO dto = index2ValueDTO.getOrDefault(index,
                            new HotelKpiMetricsSettingTargetValueDTO(index, null, null));
                    weekList.add(dto);
                }
                settingIndependentDTO.setWeek(weekList);
                toSetting.setDetail(JsonUtils.convertObjToString(objectMapper, settingIndependentDTO));
            }
        }

        /**
         * 获取年指标设置数据
         *
         * @param propertyId    资产 id
         * @param metricsTypes  指标类型
         * @param startYear     开始年份
         * @param endYear       结束年份
         * @param valueCategory 1:Actual 2:Target
         * @return HotelKpiMetricsSetting 集合
         * @author zc
         * @date 2025/1/14
         */
        @Override
        public List<HotelKpiMetricsSetting> listByYearRangeAndMetricsTypes(int propertyId, List<Integer> metricsTypes,
                                                                           long startYear, long endYear, @Nullable Integer valueCategory) {
            if (CollectionUtils.isEmpty(metricsTypes)) {
                return Collections.emptyList();
            }
            return list(new LambdaQueryWrapper<HotelKpiMetricsSetting>()
                    .eq(HotelKpiMetricsSetting::getPropertyId, propertyId)
                    .between(HotelKpiMetricsSetting::getYear, startYear, endYear)
                    .in(HotelKpiMetricsSetting::getMetricsType, metricsTypes)
                    .eq(Objects.nonNull(valueCategory), HotelKpiMetricsSetting::getValueCategory, valueCategory));
        }

        /**
         * 获取指定年份的指标设置数据
         *
         * @param propertyId 资产 id
         * @param year       年份(yyyy)
         * @return HotelKpiMetricsSetting 集合
         * @author zc
         * @date 2025/1/14
         */
        @Override
        public List<HotelKpiMetricsSetting> listByYear(int propertyId, long year) {
            return list(new LambdaQueryWrapper<HotelKpiMetricsSetting>()
                    .eq(HotelKpiMetricsSetting::getYear, year)
                    .eq(HotelKpiMetricsSetting::getPropertyId, propertyId));
        }

        /**
         * 获取 Setting Rule
         *
         * @param metricsConfig 指标配置
         * @param year          年份(yyyy)
         * @param valueCategory 1:Actual 2:Target
         * @return HotelKpiSettingCalculationDTO
         * @author zc
         * @date 2025/1/14
         */
        @Override
        public HotelKpiSettingCalculationDTO getSettingRule(HotelKpiMetricsConfig metricsConfig, long year, int valueCategory) {
            HotelKpiCalculationFactorConfigDTO configDTO = JsonUtils.convertStringToObject(objectMapper,
                    metricsConfig.getCalculationFactorConfig(), HotelKpiCalculationFactorConfigDTO.class);

            // 获取对应 valueCategory 的 calculationFactor
            HotelKpiCalculationFactorConfigItemDTO configItemDTO = (valueCategory == HotelKpiValueCategoryCons.ACTUAL) ?
                    configDTO.getActual() : configDTO.getTarget();

            // 获取指定年份的 Setting rule
            Map<Integer, HotelKpiMetricsSetting> index2Setting = listByYearRangeAndMetricsTypes(metricsConfig.getPropertyId(),
                    Lists.newArrayList(metricsConfig.getMetricsType()), year, year, valueCategory).stream()
                    .collect(Collectors.toMap(HotelKpiMetricsSetting::getIndex, Function.identity()));

            List<HotelKpiSettingCalculationFactorDTO> factorDTOList = configItemDTO.getCalculationFactor().stream().map(item -> {
                HotelKpiMetricsSetting itemSetting = index2Setting.getOrDefault(item.getIndex(), new HotelKpiMetricsSetting());
                return HotelKpiSettingCalculationFactorDTO.builder()
                        .index(item.getIndex())
                        .title(item.getTitle())
                        .valueSetting(HotelKpiSettingCalculationFactorSettingDTO.builder()
                                .category(itemSetting.getSettingCategory())
                                .settingValue(itemSetting.getDetail())
                                .build())
                        .build();
            }).toList();
            return HotelKpiSettingCalculationDTO.builder()
                    .metricsType(metricsConfig.getMetricsType())
                    .percent(configItemDTO.getPercent())
                    .calculationFactor(factorDTOList)
                    .dividerZeroContent(configItemDTO.getValueContent())
                    .weekStatisticsType(HotelKpiMetricsEnum.getDataRangeType(metricsConfig.getMetricsType()))
                    .build();
        }

        /**
         * 保存指标设置
         *
         * @param metricsConfig 指标配置
         * @param year          年份(yyyy)
         * @param settingDTO    指标设置 DTO
         * @param operatorId    操作人 id
         * @author zc
         * @date 2025/1/14
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveSetting(HotelKpiMetricsConfig metricsConfig, long year, HotelKpiSettingCalculationDTO settingDTO, int operatorId) {
            HotelKpiCalculationFactorConfigDTO configDTO = JsonUtils.convertStringToObject(objectMapper,
                    metricsConfig.getCalculationFactorConfig(), HotelKpiCalculationFactorConfigDTO.class);
            HotelKpiCalculationFactorConfigItemDTO configItemDTO = (Objects.equals(settingDTO.getValueCategory(), HotelKpiValueCategoryCons.ACTUAL)) ?
                    configDTO.getActual() : configDTO.getTarget();

            // 多个计算因子时，需要设置计算因子的标题
            if (configItemDTO.getCalculationFactor().size() > 1) {
                Map<Integer, HotelKpiSettingCalculationFactorDTO> index2SettingCalculationFactor = settingDTO.getCalculationFactor().stream()
                        .collect(Collectors.toMap(HotelKpiSettingCalculationFactorDTO::getIndex, Function.identity()));
                configItemDTO.getCalculationFactor().forEach(item -> {
                    HotelKpiSettingCalculationFactorDTO calculationFactorDTO = index2SettingCalculationFactor.get(item.getIndex());
                    if (Objects.nonNull(calculationFactorDTO)) {
                        item.setTitle(calculationFactorDTO.getTitle());
                    }
                });
                configItemDTO.setPercent(settingDTO.getPercent());
                configItemDTO.setValueContent(settingDTO.getDividerZeroContent());
            }
            // 更新指标配置
            metricsConfigService.update(new LambdaUpdateWrapper<HotelKpiMetricsConfig>()
                    .eq(HotelKpiMetricsConfig::getId, metricsConfig.getId())
                    .set(HotelKpiMetricsConfig::getCalculationFactorConfig, JsonUtils.convertObjToString(objectMapper, configDTO)));

            // 保存指标设置
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            List<HotelKpiMetricsSetting> metricsSettingList = settingDTO.getCalculationFactor().stream()
                    .map(item -> {
                        HotelKpiMetricsSetting setting = new HotelKpiMetricsSetting();
                        setting.setPropertyId(metricsConfig.getPropertyId());
                        setting.setYear(year);
                        setting.setMetricsType(settingDTO.getMetricsType());
                        setting.setValueCategory(settingDTO.getValueCategory());
                        setting.setIndex(item.getIndex());
                        setting.setSettingCategory(item.getValueSetting().getCategory());
                        setting.setDetail(item.getValueSetting().getSettingValue());
                        setting.setUpdatedBy(operatorId);
                        setting.setUpdatedTime(nowTimestamp);
                        return setting;
                    }).collect(Collectors.toList());

            // 批量保存数据
            insertOrUpdateBatchByUnique(metricsSettingList);
        }

        /**
         * 批量保存数据
         *
         * @param list 数据
         * @author zc
         * @date 2026/1/27
         */
        private void insertOrUpdateBatchByUnique(List<HotelKpiMetricsSetting> list) {
            if (CollectionUtils.isEmpty(list)) {
                return;
            }
            // 按唯一索引字段排序，避免死锁
            list.sort(Comparator.comparing(HotelKpiMetricsSetting::getPropertyId)
                    .thenComparing(HotelKpiMetricsSetting::getYear)
                    .thenComparing(HotelKpiMetricsSetting::getMetricsType)
                    .thenComparing(HotelKpiMetricsSetting::getValueCategory)
                    .thenComparing(HotelKpiMetricsSetting::getIndex));

            baseMapper.insertOrUpdateBatchByUnique(list);
        }

        /**
         * 组装 Metrics Setting
         *
         * @param metricsDTOList              指标数据
         * @param metricsSettingActualDTOList Actual Setting
         * @param metricsSettingTargetDTOList Target/Cure Setting
         * @author zc
         * @date 2025/1/14
         */
        @Override
        public void packageMetricsSetting(List<HotelKpiMetricsDTO> metricsDTOList, List<HotelKpiMetricsSettingActualDTO> metricsSettingActualDTOList,
                                          List<HotelKpiMetricsSettingTargetDTO> metricsSettingTargetDTOList) {
            // Actual 的设置数据
            Map<Long, Map<Integer, List<HotelKpiMetricsSettingActualDTO>>> settingActualDTOMap =
                    metricsSettingActualDTOList.stream().collect(Collectors.groupingBy(HotelKpiMetricsSettingActualDTO::getDate,
                            Collectors.groupingBy(HotelKpiMetricsSettingActualDTO::getMetricsType)));

            // Target、Cure 设置数据
            Map<Long, Map<Integer, List<HotelKpiMetricsSettingTargetDTO>>> settingTargetDTOMap =
                    metricsSettingTargetDTOList.stream().collect(Collectors.groupingBy(HotelKpiMetricsSettingTargetDTO::getDate,
                            Collectors.groupingBy(HotelKpiMetricsSettingTargetDTO::getMetricsType)));

            for (HotelKpiMetricsDTO metricsDTO : metricsDTOList) {
                int metricsType = metricsDTO.getMetricsType();

                // 组装指标 Actual Setting
                List<HotelKpiMetricsSettingActualDTO> settingActualDTOS = settingActualDTOMap.getOrDefault(metricsDTO.getDate(),
                        Collections.emptyMap()).getOrDefault(metricsType, Collections.emptyList());
                packageActualMetricsSetting(metricsDTO.getActual(), settingActualDTOS);

                // 组装指标 Target Cure Setting
                List<HotelKpiMetricsSettingTargetDTO> settingTargetDTOS = settingTargetDTOMap.getOrDefault(metricsDTO.getDate(),
                        Collections.emptyMap()).getOrDefault(metricsType, Collections.emptyList());
                packageTargetCureMetricsSetting(metricsDTO, settingTargetDTOS);
            }
        }

        /**
         * 组装 setting 配置的计算因子数值
         *
         * @param kpiValueDTO       指标数值DTO
         * @param settingActualDTOS 设置数据
         * @author zc
         * @date 2025/1/15
         */
        private void packageActualMetricsSetting(HotelKpiValueDTO kpiValueDTO, List<HotelKpiMetricsSettingActualDTO> settingActualDTOS) {
            if (CollectionUtils.isEmpty(settingActualDTOS)) {
                return;
            }
            Map<Integer, HotelKpiCalculationFactorDTO> index2factorDTO = kpiValueDTO.getCalculationFactor().stream()
                    .collect(Collectors.toMap(HotelKpiCalculationFactorDTO::getIndex, Function.identity()));
            List<HotelKpiCalculationFactorDTO> calculationFactorDTOList = Lists.newArrayList();
            for (HotelKpiMetricsSettingActualDTO settingActualDTO : settingActualDTOS) {
                HotelKpiCalculationFactorDTO calculationFactorDTO = index2factorDTO.getOrDefault(settingActualDTO.getIndex(),
                        new HotelKpiCalculationFactorDTO());
                calculationFactorDTO.setIndex(settingActualDTO.getIndex());
                if (Objects.nonNull(settingActualDTO.getValue())) {
                    calculationFactorDTO.setCrawl(settingActualDTO.getValue());
                }
                calculationFactorDTO.setSettingCategory(settingActualDTO.getSettingCategory());
                calculationFactorDTO.setStageValue(settingActualDTO.getStageValue());
                calculationFactorDTO.setStageTotalValue(settingActualDTO.getStageTotalValue());

                calculationFactorDTOList.add(calculationFactorDTO);
            }
            kpiValueDTO.setCalculationFactor(calculationFactorDTOList);
        }

        /**
         * 组装指标 Target/Cure Setting
         *
         * @param metricsDTO        指标数值DTO
         * @param settingTargetDTOS 设置数据
         * @author zc
         * @date 2025/1/15
         */
        private void packageTargetCureMetricsSetting(HotelKpiMetricsDTO metricsDTO, List<HotelKpiMetricsSettingTargetDTO> settingTargetDTOS) {
            if (CollectionUtils.isEmpty(settingTargetDTOS)) {
                return;
            }
            // 组装 Target Setting
            Map<Integer, HotelKpiCalculationFactorDTO> index2factorDTOByTarget = metricsDTO.getTarget().getCalculationFactor().stream()
                    .collect(Collectors.toMap(HotelKpiCalculationFactorDTO::getIndex, Function.identity()));
            List<HotelKpiCalculationFactorDTO> calculationFactorDTOListForTarget = Lists.newArrayList();
            for (HotelKpiMetricsSettingTargetDTO settingTargetDTO : settingTargetDTOS) {
                HotelKpiCalculationFactorDTO calculationFactorDTO = index2factorDTOByTarget.getOrDefault(settingTargetDTO.getIndex(),
                        new HotelKpiCalculationFactorDTO());
                calculationFactorDTO.setIndex(settingTargetDTO.getIndex());
                if (Objects.nonNull(settingTargetDTO.getTarget())) {
                    calculationFactorDTO.setCrawl(settingTargetDTO.getTarget());
                }
                calculationFactorDTO.setSettingCategory(settingTargetDTO.getSettingCategory());

                calculationFactorDTOListForTarget.add(calculationFactorDTO);
            }
            metricsDTO.getTarget().setCalculationFactor(calculationFactorDTOListForTarget);

            // 组装 Cure Setting
            Map<Integer, HotelKpiCalculationFactorDTO> index2factorDTOByCure = metricsDTO.getCure().getCalculationFactor().stream()
                    .collect(Collectors.toMap(HotelKpiCalculationFactorDTO::getIndex, Function.identity()));
            List<HotelKpiCalculationFactorDTO> calculationFactorDTOListForCure = Lists.newArrayList();
            for (HotelKpiMetricsSettingTargetDTO settingTargetDTO : settingTargetDTOS) {
                HotelKpiCalculationFactorDTO calculationFactorDTO = index2factorDTOByCure.getOrDefault(settingTargetDTO.getIndex(),
                        new HotelKpiCalculationFactorDTO());
                calculationFactorDTO.setIndex(settingTargetDTO.getIndex());
                if (Objects.nonNull(settingTargetDTO.getCure())) {
                    calculationFactorDTO.setCrawl(settingTargetDTO.getCure());
                }
                Integer settingCategory = settingTargetDTO.getSettingCategory();
                // 若 Target 的设置类型为 Dynamic，则设置 Cure 为 Constant
                if (Objects.equals(settingCategory, HotelKpiSettingCategoryEnum.DYNAMIC.getCategory())) {
                    settingCategory = HotelKpiSettingCategoryEnum.CONSTANT.getCategory();
                }
                calculationFactorDTO.setSettingCategory(settingCategory);

                calculationFactorDTOListForCure.add(calculationFactorDTO);
            }
            metricsDTO.getCure().setCalculationFactor(calculationFactorDTOListForCure);
        }

        /**
         * 获取指标设置 V2
         *
         * @param propertyId 资产 Id
         * @param year       年份(yyyy)
         * @return HotelKpiV2SettingDetailDTO 集合
         * @author zc
         * @date 2025/8/21
         */
        @Override
        public List<HotelKpiV2SettingDetailDTO> listSettingV2(int propertyId, long year) {
            // 获取有效的指标
            List<HotelKpiMetricsConfig> effectiveMetricsConfigs = metricsConfigService.listEffectiveMetricsOfYear(propertyId,
                    HotelKpiDateTypeCons.YEARLY, year, year, Boolean.FALSE, HotelKpiCons.V2);
            Map<Integer, List<HotelKpiMetricsConfig>> metricsParentId2Configs = effectiveMetricsConfigs.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsConfig::getParentId));

            return effectiveMetricsConfigs.stream().filter(metrics -> metrics.getParentId() == 0)
                    .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                    .map(parent0Metrics -> {
                        List<HotelKpiV2SettingItemDTO> items = metricsParentId2Configs.get(parent0Metrics.getId()).stream()
                                .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                                .map(parent1Metrics -> {
                                    List<HotelKpiV2SettingMetricsDTO> metrics = metricsParentId2Configs.get(parent1Metrics.getId()).stream()
                                            .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                                            .map(childMetrics -> {
                                                // 对比规则 json 转换
                                                HotelKpiV2ComparisonRuleDTO comparisonRuleDTO = JsonUtils.convertStringToObject(objectMapper,
                                                        childMetrics.getComparisonRule(), HotelKpiV2ComparisonRuleDTO.class);

                                                return HotelKpiV2SettingMetricsDTO.builder()
                                                        .metricsId(childMetrics.getId())
                                                        .metricsType(childMetrics.getMetricsType())
                                                        .title(childMetrics.getTitle())
                                                        .unit(childMetrics.getUnit())
                                                        .description(childMetrics.getDescription())
                                                        .comparisonSetting(comparisonRuleDTO)
                                                        .build();
                                            }).toList();
                                    return HotelKpiV2SettingItemDTO.builder()
                                            .title(parent1Metrics.getTitle())
                                            .metrics(metrics)
                                            .build();
                                }).toList();
                        return HotelKpiV2SettingDetailDTO.builder()
                                .title(parent0Metrics.getTitle())
                                .items(items).build();
                    }).toList();
        }

        /**
         * 保存指标设置 V2
         *
         * @param metricsConfig     指标配置
         * @param settingMetricsDTO 指标设置 DTO
         * @param operatorId        操作人 Id
         * @author zc
         * @date 2025/8/21
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveSettingMetricsV2(HotelKpiMetricsConfig metricsConfig, HotelKpiV2SettingMetricsDTO settingMetricsDTO, int operatorId) {
            // 保存指标配置数据
            metricsConfigService.update(new LambdaUpdateWrapper<HotelKpiMetricsConfig>()
                    .eq(HotelKpiMetricsConfig::getId, metricsConfig.getId())
                    .set(HotelKpiMetricsConfig::getTitle, settingMetricsDTO.getTitle())
                    .set(HotelKpiMetricsConfig::getUnit, StringUtils.isBlank(settingMetricsDTO.getUnit()) ?
                            "" : settingMetricsDTO.getUnit())
                    .set(HotelKpiMetricsConfig::getDescription, settingMetricsDTO.getDescription())
                    .set(HotelKpiMetricsConfig::getComparisonRule, JsonUtils.convertObjToString(objectMapper,
                            settingMetricsDTO.getComparisonSetting()))
                    .set(HotelKpiMetricsConfig::getDataSource, "{}"));
        }
    }