# HotelKpiServiceImpl.java

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

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiDateTypeCons;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiMetricsEnum;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiValueCategoryCons;
    import cloud.wconnect.workperformance.cons.hotelkpi.comparison.HotelKpiJudgmentCons;
    import cloud.wconnect.workperformance.cons.hotelkpi.comparison.HotelKpiTrendCons;
    import cloud.wconnect.workperformance.cons.hotelkpi.formula.HotelKpiFormulaActualCons;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiMetricsConfig;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfig.HotelKpiCalculationFactorConfigDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfig.HotelKpiCalculationFactorConfigIndexDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfig.HotelKpiCalculationFactorConfigItemDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.factorconfigv2.HotelKpiV2FactorConfigDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.internal.crawl.value.v1.HotelKpiCrawlValueIndexDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.internal.crawl.value.v2.HotelKpiV2CrawlValueIndexDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.*;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.explain.HotelKpiExplainMainDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.explain.HotelKpiExplainValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.save.HotelKpiMetricsSaveValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.*;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.editconfig.HotelKpiV2EditConfigDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.editconfig.HotelKpiV2EditConfigDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginCalculationFactorDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginCrawlValueIndexDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpiv2.HotelKpiV2OriginDetailDTO;
    import cloud.wconnect.workperformance.service.hotelkpi.IHotelKpiService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    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.apache.commons.lang3.StringUtils;
    import org.springframework.stereotype.Service;

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

    /**
     * Service: Hotel KPI
     *
     * @author zc
     * @date 2024/06/24
     */
    @Slf4j
    @Service
    @DS("work_performance")
    public class HotelKpiServiceImpl implements IHotelKpiService {
        @Resource
        private UserInternalService userInternalService;
        @Resource
        private ObjectMapper objectMapper;

        /**
         * 设置手动数值
         *
         * @param originFactorDTOList 原始计算因子集合
         * @param saveValueDTOList    保存数值集合
         * @param operatorId          操作人 id
         * @author zc
         * @date 2025/4/3
         */
        @Override
        public void setMetricsManualValue(List<HotelKpiOriginCalculationFactorDTO> originFactorDTOList,
                                          List<HotelKpiMetricsSaveValueDTO> saveValueDTOList, int operatorId) {
            if (CollectionUtils.isEmpty(saveValueDTOList)) {
                return;
            }
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            saveValueDTOList.forEach(saveValueDTO -> {
                Optional<HotelKpiOriginCalculationFactorDTO> foundIndex = originFactorDTOList.stream()
                        .filter(item -> Objects.equals(item.getIndex(), saveValueDTO.getIndex()))
                        .findFirst();
                // 若存在则替换掉对应坐标的指标原始数值，否则新增
                if (foundIndex.isPresent()) {
                    HotelKpiOriginCalculationFactorDTO factorDTO = foundIndex.get();
                    if (saveValueDTO.getUseSystem()) {
                        factorDTO.setManual(null);
                        factorDTO.setUpdatedBy(0);
                        factorDTO.setUpdatedTime(0L);
                    } else {
                        if (!NumberUtil.equals(factorDTO.getManual(), saveValueDTO.getValue())) {
                            factorDTO.setManual(saveValueDTO.getValue());
                            factorDTO.setUpdatedBy(operatorId);
                            factorDTO.setUpdatedTime(nowTimestamp);
                        }
                    }
                } else {
                    if (!saveValueDTO.getUseSystem()) {
                        HotelKpiOriginCalculationFactorDTO newFactorDTO = new HotelKpiOriginCalculationFactorDTO();
                        newFactorDTO.setIndex(saveValueDTO.getIndex());
                        newFactorDTO.setManual(saveValueDTO.getValue());
                        newFactorDTO.setUpdatedBy(operatorId);
                        newFactorDTO.setUpdatedTime(nowTimestamp);
                        originFactorDTOList.add(newFactorDTO);
                    }
                }
            });
        }

        /**
         * 设置采集数值 V2
         *
         * @param originFactorDTOList 原始计算因子集合
         * @param crawlValueDTOList   采集数值集合
         * @author zc
         * @date 2025/8/25
         */
        @Override
        public void setMetricsCrawlValueV2(List<HotelKpiOriginCalculationFactorDTO> originFactorDTOList, List<HotelKpiV2CrawlValueIndexDTO> crawlValueDTOList) {
            if (CollectionUtils.isEmpty(crawlValueDTOList)) {
                return;
            }
            crawlValueDTOList.forEach(crawlValueIndexDTO -> {
                Optional<HotelKpiOriginCalculationFactorDTO> foundIndex = originFactorDTOList.stream()
                        .filter(item -> Objects.equals(item.getIndex(), crawlValueIndexDTO.getIndex()))
                        .findFirst();
                // 若存在则替换掉对应坐标的指标原始数值，否则新增
                if (foundIndex.isPresent()) {
                    HotelKpiOriginCalculationFactorDTO factorDTO = foundIndex.get();
                    factorDTO.setCrawl(crawlValueIndexDTO.getValue());
                    factorDTO.setCrawlOrigin(List.of(HotelKpiOriginCrawlValueIndexDTO.builder()
                            .index(1).value(crawlValueIndexDTO.getValue()).build()));
                } else {
                    HotelKpiOriginCalculationFactorDTO newFactorDTO = new HotelKpiOriginCalculationFactorDTO();
                    newFactorDTO.setIndex(crawlValueIndexDTO.getIndex());
                    newFactorDTO.setCrawl(crawlValueIndexDTO.getValue());
                    newFactorDTO.setCrawlOrigin(List.of(HotelKpiOriginCrawlValueIndexDTO.builder()
                            .index(1).value(crawlValueIndexDTO.getValue()).build()));
                    originFactorDTOList.add(newFactorDTO);
                }
            });
        }

        /**
         * 提取有效数值
         *
         * <p>判断使用手动录入值还是系统采集值</p>
         *
         * @param kpiValueDTO KPI 数值 DTO
         * @return 有效数值
         * @author zc
         * @date 2025/4/3
         */
        @Override
        public BigDecimal getEffectiveValue(HotelKpiValueDTO kpiValueDTO) {
            boolean byManual = kpiValueDTO.getCalculationFactor().stream()
                    .anyMatch(item -> item.getUpdatedBy() > 0);
            return byManual ? kpiValueDTO.getManualValue() : kpiValueDTO.getCrawlValue();
        }

        /**
         * 提取有效数值
         *
         * <p>判断使用手动录入值还是系统采集值</p>
         *
         * @param factorDTO 计算因子 DTO
         * @return 有效数值
         * @author zc
         * @date 2025/4/4
         */
        @Override
        public BigDecimal getEffectiveValue(HotelKpiCalculationFactorDTO factorDTO) {
            return factorDTO.getUpdatedBy() > 0 ? factorDTO.getManual() : factorDTO.getCrawl();
        }

        /**
         * 提取有效数值 V2
         *
         * <p>判断使用手动录入值还是系统采集值</p>
         *
         * @param kpiValueDTO KPI 数值 DTO
         * @return 有效数值
         * @author zc
         * @date 2025/8/19
         */
        @Override
        public BigDecimal getEffectiveValueV2(HotelKpiV2ValueDTO kpiValueDTO) {
            boolean byManual = kpiValueDTO.getCalculationFactor().stream()
                    .anyMatch(item -> item.getUpdatedBy() > 0);
            return byManual ? kpiValueDTO.getManualValue() : kpiValueDTO.getCrawlValue();
        }

        /**
         * 提取有效数值
         *
         * <p>判断使用手动录入值还是系统采集值</p>
         *
         * @param factorDTO 计算因子 DTO
         * @return 有效数值
         * @author zc
         * @date 2025/8/20
         */
        @Override
        public BigDecimal getEffectiveOriginValue(HotelKpiOriginCalculationFactorDTO factorDTO) {
            return factorDTO.getUpdatedBy() > 0 ? factorDTO.getManual() : factorDTO.getCrawl();
        }

        /**
         * 从 KPI Detail 中获取有效的值 V2
         *
         * @param detail     KPI detail
         * @param category   分类
         * @param indexTitle 指标标题
         * @return 有效的值
         * @author zc
         * @date 2025/8/20
         */
        @Override
        public BigDecimal getEffectiveValueForKpiDetailV2(String detail, int category, int indexTitle) {
            if (StringUtils.isBlank(detail)) {
                return null;
            }

            List<HotelKpiV2OriginDetailDTO> originDetailDTOList = JsonUtils.convertStringToList(objectMapper,
                    detail, HotelKpiV2OriginDetailDTO.class);

            HotelKpiOriginValueDTO originValueDTO;
            if (indexTitle == HotelKpiValueCategoryCons.ACTUAL) {
                originValueDTO = originDetailDTOList.stream()
                        .filter(originDetailDTO -> originDetailDTO.getCategory().equals(category))
                        .findFirst().orElse(new HotelKpiV2OriginDetailDTO()).getActual();
            } else if (indexTitle == HotelKpiValueCategoryCons.BUDGET) {
                originValueDTO = originDetailDTOList.stream()
                        .filter(originDetailDTO -> originDetailDTO.getCategory().equals(category))
                        .findFirst().orElse(new HotelKpiV2OriginDetailDTO()).getBudget();
            } else {
                originValueDTO = originDetailDTOList.stream()
                        .filter(originDetailDTO -> originDetailDTO.getCategory().equals(category))
                        .findFirst().orElse(new HotelKpiV2OriginDetailDTO()).getLy();
            }

            HotelKpiOriginCalculationFactorDTO factorDTO = originValueDTO.getCalculationFactor().stream()
                    .findFirst().orElse(new HotelKpiOriginCalculationFactorDTO());

            return getEffectiveOriginValue(factorDTO);
        }

        /**
         * 替换指标原始数值的计算因子
         *
         * @param factorDTOList 原始的计算因子集合
         * @param newFactorDTO  新计算因子
         * @author zc
         * @date 2025/8/20
         */
        @Override
        public void replaceOriginFactorList(List<HotelKpiOriginCalculationFactorDTO> factorDTOList,
                                            HotelKpiOriginCalculationFactorDTO newFactorDTO) {
            // 查找并替换或新增新的指标计算因子原始数据
            int targetIndex = newFactorDTO.getIndex();
            for (int i = 0; i < factorDTOList.size(); i++) {
                HotelKpiOriginCalculationFactorDTO item = factorDTOList.get(i);
                if (Objects.equals(item.getIndex(), targetIndex)) {
                    newFactorDTO.setManual(item.getManual());
                    newFactorDTO.setUpdatedBy(item.getUpdatedBy());
                    newFactorDTO.setUpdatedTime(item.getUpdatedTime());

                    factorDTOList.set(i, newFactorDTO);
                    return;
                }
            }
            factorDTOList.add(newFactorDTO);
        }

        /**
         * 转换为 KPI V1 Detail DTO 列表
         *
         * @param detail KPI Detail
         * @return KPI V1 Detail DTO 列表
         * @author zc
         * @date 2025/9/13
         */
        @Override
        public HotelKpiOriginDetailDTO convert2OriginDetailDTOList(String detail) {
            return StringUtils.isBlank(detail) ? new HotelKpiOriginDetailDTO() :
                    JsonUtils.convertStringToObject(objectMapper, detail, HotelKpiOriginDetailDTO.class);
        }

        /**
         * 转换为 KPI V2 Detail DTO 列表
         *
         * @param detail   KPI Detail
         * @param dateType 日期类型
         * @return KPI V2 Detail DTO 列表
         * @author zc
         * @date 2025/10/8
         */
        @Override
        public List<HotelKpiV2OriginDetailDTO> convert2V2OriginDetailDTOList(String detail, int dateType, String factorConfig) {
            if (StringUtils.isBlank(detail)) {
                HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                        factorConfig, HotelKpiV2FactorConfigDTO.class);
                return HotelKpiV2OriginDetailDTO.initV2OriginDetailDTOList(dateType, factorConfigDTO);
            }
            return JsonUtils.convertStringToList(objectMapper, detail, HotelKpiV2OriginDetailDTO.class);
        }

        /**
         * 获取 KPI 对比结果
         *
         * @param currentMetricsDTO 当前周期的 kpi DTO
         * @param lastMetricsDTO    上个周期的 kpi DTO
         * @param comparisonRule    对比规则
         * @return HotelKpiComparisonDTO
         * @author zc
         * @date 2024/6/26
         */
        @Override
        public HotelKpiComparisonDTO getKpiComparison(HotelKpiMetricsDTO currentMetricsDTO, HotelKpiMetricsDTO lastMetricsDTO, String comparisonRule) {
            HotelKpiComparisonRuleDTO rule = JsonUtils.convertStringToObject(objectMapper,
                    comparisonRule, HotelKpiComparisonRuleDTO.class);
            // 若比较规则关闭，则不参与比较
            if (!rule.getOn()) {
                return new HotelKpiComparisonDTO();
            }
            String actualGreaterTarget = rule.getActualGreaterTarget();
            String actualGreaterLast = rule.getActualGreaterLast();

            if (StringUtils.isBlank(actualGreaterTarget) && StringUtils.isBlank(actualGreaterLast)) {
                return new HotelKpiComparisonDTO();
            } else if (StringUtils.isBlank(actualGreaterLast)) {
                if (HotelKpiJudgmentCons.GOOD.equals(actualGreaterTarget)) {
                    return getKpiComparisonActualVSTargetGreater(currentMetricsDTO);
                } else {
                    return getKpiComparisonActualVSTargetLess(currentMetricsDTO);
                }
            } else if (StringUtils.isBlank(actualGreaterTarget)) {
                if (HotelKpiJudgmentCons.GOOD.equals(actualGreaterLast)) {
                    return getKpiComparisonActualVSLastGreater(currentMetricsDTO, lastMetricsDTO);
                } else {
                    return getKpiComparisonActualVSLastLess(currentMetricsDTO, lastMetricsDTO);
                }
            } else {
                if (HotelKpiJudgmentCons.GOOD.equals(actualGreaterTarget)) {
                    if (HotelKpiJudgmentCons.GOOD.equals(actualGreaterLast)) {
                        return getKpiComparisonActualVSTargetGreater(currentMetricsDTO);
                    } else {
                        return getKpiComparisonActualVSTargetGreaterAndLastLess(currentMetricsDTO, lastMetricsDTO);
                    }
                } else {
                    if (HotelKpiJudgmentCons.GOOD.equals(actualGreaterLast)) {
                        return getKpiComparisonActualVSTargetLessAndLastGreater(currentMetricsDTO, lastMetricsDTO);
                    } else {
                        return getKpiComparisonActualVSTargetLess(currentMetricsDTO);
                    }
                }
            }
        }

        /**
         * 获取对比结果 for 【Actual vs Target greater and Actual vs Last less】
         *
         * @param currentMetricsDTO 当前周期的 kpi DTO
         * @param lastMetricsDTO    上个周期的 kpi DTO
         * @return HotelKpiComparisonDTO
         * @author zc
         * @date 2024/8/7
         */
        private HotelKpiComparisonDTO getKpiComparisonActualVSTargetGreaterAndLastLess(HotelKpiMetricsDTO currentMetricsDTO, HotelKpiMetricsDTO lastMetricsDTO) {
            BigDecimal currentActual = getEffectiveValue(currentMetricsDTO.getActual());
            BigDecimal currentTarget = getEffectiveValue(currentMetricsDTO.getTarget());
            BigDecimal currentCure = getEffectiveValue(currentMetricsDTO.getCure());

            HotelKpiComparisonDTO kpiComparisonDTO = new HotelKpiComparisonDTO();
            if (Objects.isNull(currentActual) || Objects.isNull(currentTarget)) {
                return kpiComparisonDTO;
            }
            if (currentActual.compareTo(currentTarget) > 0) {
                HotelKpiValueDTO lastActualValueDTO = lastMetricsDTO.getActual();
                BigDecimal lastActual = getEffectiveValue(lastActualValueDTO);
                if (Objects.isNull(lastActual)) {
                    return kpiComparisonDTO;
                }

                if (currentActual.compareTo(lastActual) < 0) {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
                    kpiComparisonDTO.setTrend(HotelKpiTrendCons.DECREASE);
                } else if (currentActual.compareTo(lastActual) == 0) {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
                    kpiComparisonDTO.setTrend(HotelKpiTrendCons.STABLE);
                } else {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.BAD);
                    kpiComparisonDTO.setTrend(HotelKpiTrendCons.INCREASE);
                }
            } else if (currentActual.compareTo(currentTarget) == 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
            } else if (Objects.nonNull(currentCure) && currentActual.compareTo(currentCure) >= 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.PASS);
            } else {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.BAD);
            }
            return kpiComparisonDTO;
        }

        /**
         * 获取对比结果 for 【Actual vs Target less and Actual vs Last greater】
         *
         * @param currentMetricsDTO 当前周期的 kpi DTO
         * @param lastMetricsDTO    上个周期的 kpi DTO
         * @return HotelKpiComparisonDTO
         * @author zc
         * @date 2024/7/18
         */
        private HotelKpiComparisonDTO getKpiComparisonActualVSTargetLessAndLastGreater(HotelKpiMetricsDTO currentMetricsDTO, HotelKpiMetricsDTO lastMetricsDTO) {
            BigDecimal currentActual = getEffectiveValue(currentMetricsDTO.getActual());
            BigDecimal currentTarget = getEffectiveValue(currentMetricsDTO.getTarget());
            BigDecimal currentCure = getEffectiveValue(currentMetricsDTO.getCure());

            HotelKpiComparisonDTO kpiComparisonDTO = new HotelKpiComparisonDTO();
            if (Objects.isNull(currentActual) || Objects.isNull(currentTarget)) {
                return kpiComparisonDTO;
            }
            if (currentActual.compareTo(currentTarget) < 0) {
                HotelKpiValueDTO lastActualValueDTO = lastMetricsDTO.getActual();
                BigDecimal lastActual = getEffectiveValue(lastActualValueDTO);
                if (Objects.isNull(lastActual)) {
                    return kpiComparisonDTO;
                }

                if (currentActual.compareTo(lastActual) > 0) {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
                    kpiComparisonDTO.setTrend(HotelKpiTrendCons.INCREASE);
                } else if (currentActual.compareTo(lastActual) == 0) {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
                    kpiComparisonDTO.setTrend(HotelKpiTrendCons.STABLE);
                } else {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.BAD);
                    kpiComparisonDTO.setTrend(HotelKpiTrendCons.DECREASE);
                }
            } else if (currentActual.compareTo(currentTarget) == 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
            } else if (Objects.nonNull(currentCure) && currentActual.compareTo(currentCure) <= 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.PASS);
            } else {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.BAD);
            }
            return kpiComparisonDTO;
        }

        /**
         * 获取对比结果 for 【Actual vs Target greater】
         *
         * @param metricsDTO 指标 kpi DTO
         * @return HotelKpiComparisonDTO
         * @author zc
         * @date 2024/6/26
         */
        private HotelKpiComparisonDTO getKpiComparisonActualVSTargetGreater(HotelKpiMetricsDTO metricsDTO) {
            BigDecimal actual = getEffectiveValue(metricsDTO.getActual());
            BigDecimal target = getEffectiveValue(metricsDTO.getTarget());
            BigDecimal cure = getEffectiveValue(metricsDTO.getCure());

            HotelKpiComparisonDTO kpiComparisonDTO = new HotelKpiComparisonDTO();
            if (Objects.isNull(target)) {
                return kpiComparisonDTO;
            }
            if (Objects.isNull(actual)) {
                kpiComparisonDTO.setJudgment(getJudgmentWhenActualIsNull(metricsDTO.getMetricsType(),
                        metricsDTO.getActual().getCalculationFactor()));
                return kpiComparisonDTO;
            }

            // 针对 Loss of Revenue (by Adjustment) 指标特殊处理
            if (metricsDTO.getMetricsType() == HotelKpiMetricsEnum.LOSS_OF_REVENUE_ADJUSTMENT.getMetricsType()) {
                BigDecimal draft = getDraftForLossOfRevenueAdjustment(metricsDTO.getActual()
                        .getCalculationFactor());
                if (actual.compareTo(target) >= 0) {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
                } else if (draft.add(actual).compareTo(target) >= 0) {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.PASS);
                } else {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.BAD);
                }
                return kpiComparisonDTO;
            }

            if (actual.compareTo(target) >= 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
            } else if (Objects.nonNull(cure) && actual.compareTo(cure) >= 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.PASS);
            } else {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.BAD);
            }
            return kpiComparisonDTO;
        }

        /**
         * 获取对比结果 for 【Actual vs Target less】
         *
         * @param metricsDTO 指标 kpi DTO
         * @return HotelKpiComparisonDTO
         * @author zc
         * @date 2024/6/26
         */
        private HotelKpiComparisonDTO getKpiComparisonActualVSTargetLess(HotelKpiMetricsDTO metricsDTO) {
            BigDecimal actual = getEffectiveValue(metricsDTO.getActual());
            BigDecimal target = getEffectiveValue(metricsDTO.getTarget());
            BigDecimal cure = getEffectiveValue(metricsDTO.getCure());

            HotelKpiComparisonDTO kpiComparisonDTO = new HotelKpiComparisonDTO();
            if (Objects.isNull(target)) {
                return kpiComparisonDTO;
            }
            if (Objects.isNull(actual)) {
                kpiComparisonDTO.setJudgment(getJudgmentWhenActualIsNull(metricsDTO.getMetricsType(),
                        metricsDTO.getActual().getCalculationFactor()));
                return kpiComparisonDTO;
            }

            // 针对 Loss of Revenue (by Adjustment) 指标特殊处理
            if (metricsDTO.getMetricsType() == HotelKpiMetricsEnum.LOSS_OF_REVENUE_ADJUSTMENT.getMetricsType()) {
                BigDecimal draft = getDraftForLossOfRevenueAdjustment(metricsDTO.getActual()
                        .getCalculationFactor());
                if (actual.compareTo(target) > 0) {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.BAD);
                } else if (draft.add(actual).compareTo(target) > 0) {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.PASS);
                } else {
                    kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
                }
                return kpiComparisonDTO;
            }

            if (actual.compareTo(target) <= 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
            } else if (Objects.nonNull(cure) && actual.compareTo(cure) <= 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.PASS);
            } else {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.BAD);
            }
            return kpiComparisonDTO;
        }

        /**
         * 获取判断结果 when actual is null
         *
         * @param metricsType       指标类型
         * @param calculationFactor 计算因子
         * @return java.lang.String
         * @author zc
         * @date 2024/12/19
         */
        private String getJudgmentWhenActualIsNull(int metricsType, List<HotelKpiCalculationFactorDTO> calculationFactor) {
            HotelKpiCalculationFactorDTO calculationFactorDTO = calculationFactor.stream()
                    .filter(item -> item.getIndex() == 2).findFirst().orElse(null);
            if (Objects.nonNull(calculationFactorDTO) && Objects.nonNull(calculationFactorDTO.getCrawl())) {
                if (calculationFactorDTO.getCrawl().compareTo(BigDecimal.ZERO) == 0) {
                    List<Integer> meetTypeList = Lists.newArrayList(HotelKpiMetricsEnum.QUORE_COMPLAINT.getMetricsType(),
                            HotelKpiMetricsEnum.QUORE_GR_PM.getMetricsType(), HotelKpiMetricsEnum.QUORE_DEEP_CLEAN.getMetricsType());
                    if (meetTypeList.contains(metricsType)) {
                        return HotelKpiJudgmentCons.GOOD;
                    }
                }
            }
            return null;
        }

        /**
         * 获取 draft for Loss of Revenue (by Adjustment)
         *
         * @param calculationFactor 计算因子
         * @return java.math.BigDecimal
         * @author zc
         * @date 2025/6/7
         */
        private BigDecimal getDraftForLossOfRevenueAdjustment(List<HotelKpiCalculationFactorDTO> calculationFactor) {
            HotelKpiCalculationFactorDTO calculationFactorDTO = calculationFactor.stream()
                    .filter(item -> item.getIndex() == 1).findFirst().orElse(null);
            if (Objects.nonNull(calculationFactorDTO)) {
                HotelKpiCrawlValueIndexDTO crawlValueIndexDTO = calculationFactorDTO.getCrawlOrigin().stream()
                        .filter(item -> item.getIndex() == 2).findFirst().orElse(null);
                if (Objects.nonNull(crawlValueIndexDTO) && Objects.nonNull(crawlValueIndexDTO.getValue())) {
                    return crawlValueIndexDTO.getValue();
                }
            }
            return BigDecimal.ZERO;
        }

        /**
         * 获取对比结果 for 【Actual vs Last greater】
         *
         * @param currentMetricsDTO 当前周期的 kpi DTO
         * @param lastMetricsDTO    上个周期的 kpi DTO
         * @return HotelKpiComparisonDTO
         * @author zc
         * @date 2024/6/26
         */
        private HotelKpiComparisonDTO getKpiComparisonActualVSLastGreater(HotelKpiMetricsDTO currentMetricsDTO, HotelKpiMetricsDTO lastMetricsDTO) {
            BigDecimal currentActual = getEffectiveValue(currentMetricsDTO.getActual());
            BigDecimal lastActual = getEffectiveValue(lastMetricsDTO.getActual());

            HotelKpiComparisonDTO kpiComparisonDTO = new HotelKpiComparisonDTO();
            if (Objects.isNull(currentActual) || Objects.isNull(lastActual)) {
                return kpiComparisonDTO;
            }

            if (currentActual.compareTo(lastActual) > 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
                kpiComparisonDTO.setTrend(HotelKpiTrendCons.INCREASE);
            } else if (currentActual.compareTo(lastActual) == 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
                kpiComparisonDTO.setTrend(HotelKpiTrendCons.STABLE);
            } else {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.BAD);
                kpiComparisonDTO.setTrend(HotelKpiTrendCons.DECREASE);
            }
            return kpiComparisonDTO;
        }

        /**
         * 获取对比结果 for 【Actual vs Last less】
         *
         * @param currentMetricsDTO 当前周期的 kpi DTO
         * @param lastMetricsDTO    上个周期的 kpi DTO
         * @return HotelKpiComparisonDTO
         * @author zc
         * @date 2024/7/25
         */
        private HotelKpiComparisonDTO getKpiComparisonActualVSLastLess(HotelKpiMetricsDTO currentMetricsDTO, HotelKpiMetricsDTO lastMetricsDTO) {
            BigDecimal currentActual = getEffectiveValue(currentMetricsDTO.getActual());
            BigDecimal lastActual = getEffectiveValue(lastMetricsDTO.getActual());

            HotelKpiComparisonDTO kpiComparisonDTO = new HotelKpiComparisonDTO();
            if (Objects.isNull(currentActual) || Objects.isNull(lastActual)) {
                return kpiComparisonDTO;
            }

            if (currentActual.compareTo(lastActual) < 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
                kpiComparisonDTO.setTrend(HotelKpiTrendCons.DECREASE);
            } else if (currentActual.compareTo(lastActual) == 0) {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.GOOD);
                kpiComparisonDTO.setTrend(HotelKpiTrendCons.STABLE);
            } else {
                kpiComparisonDTO.setJudgment(HotelKpiJudgmentCons.BAD);
                kpiComparisonDTO.setTrend(HotelKpiTrendCons.INCREASE);
            }
            return kpiComparisonDTO;
        }

        /**
         * 提取操作人名称
         *
         * @param tenantId       租户 id
         * @param metricsDTOList kpi 指标数据
         * @return 操作人名称
         * @author zc
         * @date 2024/6/25
         */
        @Override
        public Map<Integer, String> extractOperatorName(int tenantId, List<HotelKpiMetricsDTO> metricsDTOList) {
            if (CollectionUtils.isEmpty(metricsDTOList)) {
                return Collections.emptyMap();
            }
            Set<Integer> userIds = metricsDTOList.stream().flatMap(dto ->
                    dto.summaryUserIds().stream()).collect(Collectors.toSet());
            return userInternalService.getNameMapByIds(tenantId, userIds);
        }

        /**
         * 提取操作人名称
         *
         * @param tenantId       租户 id
         * @param metricsDTOList kpi 指标数据
         * @return 操作人名称
         * @author zc
         * @date 2025/8/19
         */
        @Override
        public Map<Integer, String> extractOperatorNameV2(int tenantId, List<HotelKpiV2MetricsDTO> metricsDTOList) {
            if (CollectionUtils.isEmpty(metricsDTOList)) {
                return Collections.emptyMap();
            }
            Set<Integer> userIds = metricsDTOList.stream().flatMap(dto ->
                    dto.summaryUserIds().stream()).collect(Collectors.toSet());
            return userInternalService.getNameMapByIds(tenantId, userIds);
        }

        /**
         * 生成指标 KPI 数据
         *
         * @param propertyDTO                   资产
         * @param userId2Name                   操作人名称
         * @param metricsConfigs                指标配置
         * @param metricsType2KpiMetricsDTO     当前 KPI 数据
         * @param metricsType2LastKpiMetricsDTO 上一 KPI 数据
         * @param metricsType2WarningCount      警示备注数量数据
         * @param currentDate                   当前日期
         * @param lastDate                      上一日期
         * @param dateType                      日期类型（1:week 2:month 3:quarter 4:year）
         * @return HotelKpiDetailDTO 集合
         * @author zc
         * @date 2024/6/26
         */
        @Override
        public List<HotelKpiDetailDTO> generateKpiDetailDTOList(PropertyDTO propertyDTO, Map<Integer, String> userId2Name,
                                                                List<HotelKpiMetricsConfig> metricsConfigs,
                                                                Map<Integer, List<HotelKpiMetricsConfig>> metricsParentId2Configs,
                                                                Map<Integer, HotelKpiMetricsDTO> metricsType2KpiMetricsDTO,
                                                                Map<Integer, HotelKpiMetricsDTO> metricsType2LastKpiMetricsDTO,
                                                                Map<Integer, Map<Integer, Long>> metricsType2WarningCount,
                                                                long currentDate, long lastDate, int dateType) {
            return metricsConfigs.stream()
                    .filter(metrics -> metrics.getParentId() == 0)
                    .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                    .map(parent0Metrics -> {
                        List<HotelKpiItemDTO> items = metricsParentId2Configs.getOrDefault(parent0Metrics.getId(), Collections.emptyList()).stream()
                                .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                                .map(parent1Metrics -> {
                                    List<HotelKpiMetricsDTO> metrics = metricsParentId2Configs.getOrDefault(parent1Metrics.getId(), Collections.emptyList()).stream()
                                            .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                                            .map(childMetrics -> {
                                                HotelKpiMetricsDTO currentMetricsDTO = metricsType2KpiMetricsDTO.getOrDefault(childMetrics.getMetricsType(), new HotelKpiMetricsDTO());
                                                // 组装指标 DTO
                                                packageMetricsDTO(propertyDTO, currentDate, dateType, childMetrics, currentMetricsDTO);

                                                HotelKpiMetricsDTO lastMetricsDTO = metricsType2LastKpiMetricsDTO.getOrDefault(childMetrics.getMetricsType(), new HotelKpiMetricsDTO());
                                                // 组装指标 DTO
                                                packageMetricsDTO(propertyDTO, lastDate, dateType, childMetrics, lastMetricsDTO);

                                                // 组装警示备注数量
                                                long warningCount = metricsType2WarningCount.getOrDefault(childMetrics.getMetricsType(), Collections.emptyMap())
                                                        .getOrDefault(HotelKpiValueCategoryCons.CATEGORY_1, 0L);
                                                currentMetricsDTO.setWarningCount(warningCount);
                                                // 当警示备注数量为0时，才需要设置对比结果
                                                if (warningCount == 0) {
                                                    currentMetricsDTO.setComparison(getKpiComparison(currentMetricsDTO, lastMetricsDTO, childMetrics.getComparisonRule()));
                                                }

                                                // 组装操作人名称
                                                currentMetricsDTO.getActual().getCalculationFactor().forEach(item ->
                                                        item.setUpdatedName(userId2Name.get(item.getUpdatedBy())));
                                                currentMetricsDTO.getTarget().getCalculationFactor().forEach(item ->
                                                        item.setUpdatedName(userId2Name.get(item.getUpdatedBy())));
                                                currentMetricsDTO.getCure().getCalculationFactor().forEach(item ->
                                                        item.setUpdatedName(userId2Name.get(item.getUpdatedBy())));

                                                return currentMetricsDTO;
                                            }).toList();
                                    return HotelKpiItemDTO.builder()
                                            .id(parent1Metrics.getId())
                                            .title(parent1Metrics.getTitle())
                                            .metrics(metrics)
                                            .build();
                                }).toList();
                        return HotelKpiDetailDTO.builder()
                                .id(parent0Metrics.getId())
                                .title(parent0Metrics.getTitle())
                                .items(items).build();
                    }).toList();
        }

        /**
         * 组装指标 DTO
         *
         * @param propertyDTO   资产
         * @param date          日期
         * @param dateType      日期类型（1:week 2:month 3:quarter 4:year）
         * @param metricsConfig 指标配置
         * @param metricsDTO    待组装 DTO
         * @author zc
         * @date 2024/12/14
         */
        private void packageMetricsDTO(PropertyDTO propertyDTO, long date, int dateType,
                                       HotelKpiMetricsConfig metricsConfig, HotelKpiMetricsDTO metricsDTO) {
            metricsDTO.setDate(date);
            metricsDTO.setMetricsType(metricsConfig.getMetricsType());
            metricsDTO.setTitle(metricsConfig.getTitle());
            metricsDTO.setDescription(metricsConfig.getDescription());
            metricsDTO.setDisabled(metricsDisabled(dateType, date, metricsConfig.getDisabledDate()));
            metricsDTO.setUnit(metricsConfig.getUnit());
            // 组装指标数值
            packageMetricsValue(propertyDTO, metricsConfig, metricsDTO);
        }

        /**
         * 根据日期类型校验指标是否禁用
         *
         * @param dateType            日期类型（1:week 2:month 3:quarter 4:year）
         * @param date                日期(yyyyMMdd,yyyyMM,yyyy)
         * @param metricsDisabledDate 指标禁用日期（yyyyMMdd）
         * @return java.lang.Boolean
         * @author zc
         * @date 2024/7/4
         */
        private Boolean metricsDisabled(int dateType, long date, long metricsDisabledDate) {
            if (HotelKpiDateTypeCons.WEEKLY == dateType) {
                return date > metricsDisabledDate;
            } else if (HotelKpiDateTypeCons.MONTHLY == dateType
                    || HotelKpiDateTypeCons.QUARTERLY == dateType) {
                long dateLong = Long.parseLong(DateTimeUtils.localDateToStr(YearMonthUtils.getYearMonth(String.valueOf(date)).atDay(1),
                        BaseConstant.DATE_FORMAT_YYYYMMDD));
                return dateLong > metricsDisabledDate;
            } else {
                long dateLong = Long.parseLong(DateTimeUtils.localDateToStr(YearMonth.of((int) date, 1)
                        .atDay(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
                return dateLong > metricsDisabledDate;
            }
        }

        /**
         * 组装指标数值
         *
         * @param propertyDTO   资产
         * @param metricsConfig 指标配置
         * @param metricsDTO    指标数值
         * @author zc
         * @date 2024/12/14
         */
        @Override
        public void packageMetricsValue(PropertyDTO propertyDTO, HotelKpiMetricsConfig metricsConfig, HotelKpiMetricsDTO metricsDTO) {
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            // 计算因子配置
            HotelKpiCalculationFactorConfigDTO configDTO = JsonUtils.convertStringToObject(objectMapper,
                    metricsConfig.getCalculationFactorConfig(), HotelKpiCalculationFactorConfigDTO.class);

            // 计算并设置 Actual 系统采集值、手动录入值的计算结果
            setCrawlAndManualValue(configDTO.getActual(), metricsDTO.getActual(), roundingMode);
            // 计算并设置 Target 系统采集值、手动录入值的计算结果
            setCrawlAndManualValue(configDTO.getTarget(), metricsDTO.getTarget(), roundingMode);
            // 计算并设置 Cure 系统采集值、手动录入值的计算结果
            setCrawlAndManualValue(configDTO.getTarget(), metricsDTO.getCure(), roundingMode);

            HotelKpiExplainMainDTO explainMainDTO = metricsDTO.getExplain();
            // 设置指标 Actual Explain Value
            packageKpiExplainValue(configDTO.getActual(), metricsDTO.getActual(), explainMainDTO.getActual());
            // 设置指标 Target Explain Value
            packageKpiExplainValue(configDTO.getTarget(), metricsDTO.getTarget(), explainMainDTO.getTarget());
        }

        /**
         * 设置采集值、手动录入值
         *
         * @param factorConfigItemDTO 计算因子配置
         * @param hotelKpiValueDTO    指标值
         * @param roundingMode        舍入模式
         * @author zc
         * @date 2025/1/14
         */
        private void setCrawlAndManualValue(HotelKpiCalculationFactorConfigItemDTO factorConfigItemDTO,
                                            HotelKpiValueDTO hotelKpiValueDTO, RoundingMode roundingMode) {
            List<HotelKpiCalculationFactorDTO> calculationFactorDTOList = hotelKpiValueDTO.getCalculationFactor();
            BigDecimal crawlValue;
            BigDecimal manualValue = null;

            BigDecimal index1ValueByCrawl = calculationFactorDTOList.stream().filter(item ->
                    item.getIndex() == 1).findFirst().orElse(new HotelKpiCalculationFactorDTO()).getCrawl();
            BigDecimal index1ValueByManual = calculationFactorDTOList.stream().filter(item ->
                    item.getIndex() == 1).findFirst().orElse(new HotelKpiCalculationFactorDTO()).getManual();
            if (factorConfigItemDTO.getCalculationFactor().size() == 1) {
                crawlValue = index1ValueByCrawl;
                manualValue = index1ValueByManual;
            } else {
                boolean percent = factorConfigItemDTO.getPercent();

                BigDecimal index2ValueByCrawl = calculationFactorDTOList.stream().filter(item ->
                        item.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO()).getCrawl();
                // 采集值计算结果 = 采集值分子 / 采集值分母
                crawlValue = calculateByPercent(percent, index1ValueByCrawl, index2ValueByCrawl, roundingMode);

                BigDecimal index2ValueByManual = calculationFactorDTOList.stream().filter(item ->
                        item.getIndex() == 2).findFirst().orElse(new HotelKpiCalculationFactorDTO()).getManual();
                if (Objects.nonNull(index1ValueByManual) || Objects.nonNull(index2ValueByManual)) {
                    if (Objects.isNull(index2ValueByManual)) {
                        // 若手动录入值分母为 null，手动录入值计算结果 = 手动录入值分子 / 采集值分母
                        manualValue = calculateByPercent(percent, index1ValueByManual, index2ValueByCrawl, roundingMode);
                    } else if (Objects.isNull(index1ValueByManual)) {
                        // 若手动录入值分子为 null，手动录入值计算结果 = 采集值分子 / 手动录入值分母
                        manualValue = calculateByPercent(percent, index1ValueByCrawl, index2ValueByManual, roundingMode);
                    } else {
                        // 手动录入值计算结果 = 手动录入值分子 / 手动录入值分母
                        manualValue = calculateByPercent(percent, index1ValueByManual, index2ValueByManual, roundingMode);
                    }
                }
            }
            hotelKpiValueDTO.setCrawlValue(crawlValue);
            hotelKpiValueDTO.setManualValue(manualValue);
        }

        /**
         * 根据是否为百分比进行计算
         *
         * @param percent      计算结果是否是百分比: true:是 false:否
         * @param molecular    分子
         * @param denominator  分母
         * @param roundingMode 舍入模式
         * @return java.math.BigDecimal
         * @author zc
         * @date 2025/4/3
         */
        private BigDecimal calculateByPercent(boolean percent, BigDecimal molecular, BigDecimal denominator, RoundingMode roundingMode) {
            if (Objects.isNull(molecular) || Objects.isNull(denominator) || denominator.compareTo(BigDecimal.ZERO) == 0) {
                return null;
            }
            if (percent) {
                return molecular.multiply(new BigDecimal("100")).divide(denominator, BaseConstant.DATA_PRECISION, roundingMode);
            }
            return molecular.divide(denominator, BaseConstant.DATA_PRECISION, roundingMode);
        }

        /**
         * 组装 KPI Explain Value
         *
         * @param factorConfigItemDTO 计算因子配置
         * @param hotelKpiValueDTO    指标值
         * @param explainValueDTO     待组装DTO
         * @author zc
         * @date 2024/12/24
         */
        private void packageKpiExplainValue(HotelKpiCalculationFactorConfigItemDTO factorConfigItemDTO, HotelKpiValueDTO hotelKpiValueDTO,
                                            HotelKpiExplainValueDTO explainValueDTO) {
            List<HotelKpiCalculationFactorConfigIndexDTO> calculationFactorList = factorConfigItemDTO.getCalculationFactor();
            if (calculationFactorList.size() != 2) {
                return;
            }
            // 计算因子 Title
            Map<Integer, HotelKpiCalculationFactorConfigIndexDTO> index2FactorIndex = calculationFactorList.stream()
                    .collect(Collectors.toMap(HotelKpiCalculationFactorConfigIndexDTO::getIndex, Function.identity()));

            // 计算因子数值
            Map<Integer, HotelKpiCalculationFactorDTO> index2CalculationDTO = hotelKpiValueDTO.getCalculationFactor().stream()
                    .collect(Collectors.toMap(HotelKpiCalculationFactorDTO::getIndex, Function.identity()));

            List<HotelKpiCalculationFactorValueDTO> calculationFactor = calculationFactorList.stream().map(item -> {
                HotelKpiCalculationFactorDTO calculationFactorDTO = index2CalculationDTO.getOrDefault(item.getIndex(),
                        new HotelKpiCalculationFactorDTO());

                HotelKpiCalculationFactorConfigIndexDTO factorIndexDTO = index2FactorIndex.getOrDefault(item.getIndex(),
                        new HotelKpiCalculationFactorConfigIndexDTO());

                return HotelKpiCalculationFactorValueDTO.builder()
                        .index(item.getIndex())
                        .title(factorIndexDTO.getTitle())
                        .value(getEffectiveValue(calculationFactorDTO))
                        .build();
            }).collect(Collectors.toList());

            // 获取当计算因子分母等于 0 时，显示的内容
            String valueContent = getActualValueContent(calculationFactor, factorConfigItemDTO.getValueContent());

            explainValueDTO.setCalculationFactor(calculationFactor);
            explainValueDTO.setValueContent(valueContent);
            explainValueDTO.setFormula(HotelKpiFormulaActualCons.MOLECULE_DIVIDE_DENOMINATOR);
        }

        /**
         * 获取 Actual 值内容
         *
         * <p>备注: 当 Actual 由两个计算因子计算得来时，若除数等于 0，则需要展示设置的 Actual 内容来代替计算的数值</p>
         *
         * @param calculationFactor  Actual 计算因子
         * @param actualValueContent Actual 内容
         * @return Actual 值内容
         * @author zc
         * @date 2024/12/14
         */
        private String getActualValueContent(List<HotelKpiCalculationFactorValueDTO> calculationFactor, String actualValueContent) {
            HotelKpiCalculationFactorValueDTO calculationFactorDTO = calculationFactor.stream()
                    .filter(item -> item.getIndex() == 2).findFirst().orElse(null);
            if (Objects.nonNull(calculationFactorDTO) && Objects.nonNull(calculationFactorDTO.getValue())) {
                if (calculationFactorDTO.getValue().compareTo(BigDecimal.ZERO) == 0) {
                    return actualValueContent;
                }
            }
            return null;
        }

        /**
         * 生成指标 KPI 数据 V2
         *
         * @param propertyDTO                   资产
         * @param userId2Name                   操作人名称
         * @param metricsConfigs                指标配置
         * @param metricsType2KpiMetricsDTO     当前 KPI 数据
         * @param metricsType2LastKpiMetricsDTO 上一 KPI 数据
         * @param metricsType2WarningCount      警示备注数量数据
         * @param currentDate                   当前日期
         * @param lastDate                      上一日期
         * @param dateType                      日期类型（1:week 2:month 3:quarter 4:year）
         * @return HotelKpiV2DetailDTO 集合
         * @author zc
         * @date 2025/8/18
         */
        @Override
        public List<HotelKpiV2DetailDTO> generateKpiDetailDTOListV2(PropertyDTO propertyDTO, Map<Integer, String> userId2Name,
                                                                    List<HotelKpiMetricsConfig> metricsConfigs,
                                                                    Map<Integer, List<HotelKpiMetricsConfig>> metricsParentId2Configs,
                                                                    Map<Integer, HotelKpiV2MetricsDTO> metricsType2KpiMetricsDTO,
                                                                    Map<Integer, HotelKpiV2MetricsDTO> metricsType2LastKpiMetricsDTO,
                                                                    Map<Integer, Map<Integer, Long>> metricsType2WarningCount,
                                                                    long currentDate, long lastDate, int dateType) {
            return metricsConfigs.stream()
                    .filter(metrics -> metrics.getParentId() == 0)
                    .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                    .map(parent0MetricsConfig -> {
                        List<HotelKpiV2ItemDTO> items = metricsParentId2Configs.getOrDefault(parent0MetricsConfig.getId(), Collections.emptyList()).stream()
                                .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                                .map(parent1MetricsConfig -> {
                                    List<HotelKpiV2MetricsDTO> metrics = metricsParentId2Configs.getOrDefault(parent1MetricsConfig.getId(), Collections.emptyList()).stream()
                                            .sorted(Comparator.comparing(HotelKpiMetricsConfig::getSort))
                                            .map(childMetricsConfig -> {
                                                HotelKpiV2FactorConfigDTO factorConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                                                        childMetricsConfig.getCalculationFactorConfig(), HotelKpiV2FactorConfigDTO.class);

                                                HotelKpiV2MetricsDTO currentMetricsDTO = metricsType2KpiMetricsDTO.getOrDefault(childMetricsConfig.getMetricsType(),
                                                        new HotelKpiV2MetricsDTO(dateType, factorConfigDTO));
                                                // 组装指标 DTO
                                                packageMetricsDTOV2(childMetricsConfig, currentMetricsDTO);

                                                HotelKpiV2MetricsDTO lastMetricsDTO = metricsType2LastKpiMetricsDTO.getOrDefault(childMetricsConfig.getMetricsType(),
                                                        new HotelKpiV2MetricsDTO(dateType, factorConfigDTO));
                                                // 组装指标 DTO
                                                packageMetricsDTOV2(childMetricsConfig, lastMetricsDTO);

                                                Map<Integer, HotelKpiV2MetricsCategoryDTO> category2CategoryDTO = lastMetricsDTO.getList().stream()
                                                        .collect(Collectors.toMap(HotelKpiV2MetricsCategoryDTO::getCategory, Function.identity()));

                                                // 组装指标是否可编辑标识
                                                setKpiCanEditFlagV2(currentMetricsDTO, childMetricsConfig, dateType);

                                                Map<Integer, Long> category2WarningCount = metricsType2WarningCount.getOrDefault(childMetricsConfig.getMetricsType(), Collections.emptyMap());

                                                // 组装操作人名称
                                                currentMetricsDTO.getList().forEach(categoryDTO -> {
                                                    // 组装警示备注数量
                                                    categoryDTO.setWarningCount(category2WarningCount.getOrDefault(categoryDTO.getCategory(), 0L));
                                                    // 当警示备注数量为0时，才需要设置对比结果
                                                    if (categoryDTO.getWarningCount() == 0) {
                                                        HotelKpiV2ComparisonRuleDTO ruleDTO = JsonUtils.convertStringToObject(objectMapper,
                                                                childMetricsConfig.getComparisonRule(), HotelKpiV2ComparisonRuleDTO.class);
                                                        // 若比较规则关闭，则不参与比较
                                                        if (ruleDTO.getOn()) {
                                                            HotelKpiV2MetricsCategoryDTO lastCategoryDTO = category2CategoryDTO.getOrDefault(categoryDTO.getCategory(),
                                                                    new HotelKpiV2MetricsCategoryDTO());
                                                            setKpiComparisonV2(categoryDTO, lastCategoryDTO, ruleDTO);
                                                        }
                                                    }

                                                    categoryDTO.getActual().getCalculationFactor().forEach(calculationFactor ->
                                                            calculationFactor.setUpdatedName(userId2Name.get(calculationFactor.getUpdatedBy())));
                                                    categoryDTO.getBudget().getCalculationFactor().forEach(calculationFactor ->
                                                            calculationFactor.setUpdatedName(userId2Name.get(calculationFactor.getUpdatedBy())));
                                                    categoryDTO.getLy().getCalculationFactor().forEach(calculationFactor ->
                                                            calculationFactor.setUpdatedName(userId2Name.get(calculationFactor.getUpdatedBy())));
                                                });

                                                return currentMetricsDTO;
                                            }).toList();
                                    return HotelKpiV2ItemDTO.builder()
                                            .id(parent1MetricsConfig.getId())
                                            .title(parent1MetricsConfig.getTitle())
                                            .metrics(metrics)
                                            .build();
                                }).toList();
                        return HotelKpiV2DetailDTO.builder()
                                .id(parent0MetricsConfig.getId())
                                .title(parent0MetricsConfig.getTitle())
                                .items(items).build();
                    }).toList();
        }

        /**
         * 组装指标 DTO V2
         *
         * @param metricsConfig 指标配置
         * @param metricsDTO    待组装 DTO
         * @author zc
         * @date 2025/8/19
         */
        private void packageMetricsDTOV2(HotelKpiMetricsConfig metricsConfig, HotelKpiV2MetricsDTO metricsDTO) {
            metricsDTO.setMetricsType(metricsConfig.getMetricsType());
            metricsDTO.setTitle(metricsConfig.getTitle());
            metricsDTO.setDescription(metricsConfig.getDescription());
            metricsDTO.setUnit(metricsConfig.getUnit());
        }

        /**
         * 设置 KPI V2 是否可编辑标识
         *
         * @param metricsDTO    kpi DTO
         * @param metricsConfig 指标配置
         * @param dateType      日期类型（1:week 2:month 3:quarter 4:year）
         * @author zc
         * @date 2025/8/19
         */
        private void setKpiCanEditFlagV2(HotelKpiV2MetricsDTO metricsDTO, HotelKpiMetricsConfig metricsConfig, int dateType) {
            HotelKpiV2EditConfigDTO editConfigDTO = JsonUtils.convertStringToObject(objectMapper,
                    metricsConfig.getEditConfig(), HotelKpiV2EditConfigDTO.class);

            List<HotelKpiV2EditConfigDetailDTO> configDetailDTOList;
            if (HotelKpiDateTypeCons.WEEKLY == dateType) {
                configDetailDTOList = editConfigDTO.getWeekly();
            } else if (HotelKpiDateTypeCons.MONTHLY == dateType) {
                configDetailDTOList = editConfigDTO.getMonthly();
            } else if (HotelKpiDateTypeCons.QUARTERLY == dateType) {
                configDetailDTOList = editConfigDTO.getQuarterly();
            } else {
                configDetailDTOList = editConfigDTO.getYearly();
            }
            Map<Integer, HotelKpiV2EditConfigDetailDTO> category2ConfigDetailDTO = configDetailDTOList.stream()
                    .collect(Collectors.toMap(HotelKpiV2EditConfigDetailDTO::getCategory, Function.identity()));

            metricsDTO.getList().forEach(categoryDTO -> {
                HotelKpiV2EditConfigDetailDTO configDetailDTO = category2ConfigDetailDTO.getOrDefault(categoryDTO.getCategory(),
                        new HotelKpiV2EditConfigDetailDTO());
                categoryDTO.getActual().setCanEdit(configDetailDTO.getActual());
                categoryDTO.getBudget().setCanEdit(configDetailDTO.getBudget());
                categoryDTO.getLy().setCanEdit(configDetailDTO.getLy());
                categoryDTO.getBudgetChg().setCanCalculate(configDetailDTO.getBudgetChg());
                categoryDTO.getLyChg().setCanCalculate(configDetailDTO.getLyChg());
            });
        }

        /**
         * 设置 KPI V2 对比结果
         *
         * @param current 当前周期的 kpi DTO
         * @param last    上个周期的 kpi DTO
         * @param ruleDTO 对比规则
         * @author zc
         * @date 2025/8/19
         */
        private void setKpiComparisonV2(HotelKpiV2MetricsCategoryDTO current, HotelKpiV2MetricsCategoryDTO last, HotelKpiV2ComparisonRuleDTO ruleDTO) {
            String actualGreaterLastRule = ruleDTO.getActualGreaterLast();
            HotelKpiV2ComparisonRuleDetailDTO budgetChgRule = ruleDTO.getBudgetChg();
            HotelKpiV2ComparisonRuleDetailDTO lyChgRule = ruleDTO.getLyChg();

            // 设置 Actual/OTB 对比 上一周期 Actual/OTB 的对比结果
            setComparisonLast(current.getActual(), last.getActual(),
                    actualGreaterLastRule);
            // 设置 Budget % Chg 的对比结果
            setComparisonChg(current.getBudgetChg(), budgetChgRule);
            // 设置 LY % Chg 的对比结果
            setComparisonChg(current.getLyChg(), lyChgRule);
        }

        /**
         * 设置 KPI V2 % Chg 的对比结果
         *
         * @param chgDTO  当前周期的 kpi DTO
         * @param chgRule 比较规则
         * @author zc
         * @date 2025/8/19
         */
        private void setComparisonChg(HotelKpiV2ValueChgDTO chgDTO, HotelKpiV2ComparisonRuleDetailDTO chgRule) {
            if (Objects.isNull(chgRule)) {
                return;
            }
            BigDecimal chgValue = chgDTO.getValue();
            if (Objects.isNull(chgValue)) {
                return;
            }
            BigDecimal threshold = chgRule.getThreshold();
            String rule = chgRule.getGreaterJudgment();
            String judgment;
            if (HotelKpiJudgmentCons.GOOD.equals(rule)) {
                judgment = (chgValue.compareTo(threshold) >= 0) ? HotelKpiJudgmentCons.GOOD : HotelKpiJudgmentCons.BAD;
            } else {
                judgment = (chgValue.compareTo(threshold) <= 0) ? HotelKpiJudgmentCons.GOOD : HotelKpiJudgmentCons.BAD;
            }
            chgDTO.setComparisonJudgment(judgment);
        }

        /**
         * 设置 KPI V2 Actual/OTB 对比上一周期 Actual/OTB 的对比结果
         *
         * @param currentDTO 当前周期的 kpi DTO
         * @param lastDTO    上个周期的 kpi DTO
         * @param rule       比较规则
         * @author zc
         * @date 2025/8/19
         */
        private void setComparisonLast(HotelKpiV2ValueDTO currentDTO, HotelKpiV2ValueDTO lastDTO, String rule) {
            if (StringUtils.isBlank(rule)) {
                return;
            }
            BigDecimal current = getEffectiveValueV2(currentDTO);
            BigDecimal last = getEffectiveValueV2(lastDTO);
            if (Objects.isNull(current) || Objects.isNull(last)) {
                return;
            }
            String judgment;
            if (HotelKpiJudgmentCons.GOOD.equals(rule)) {
                judgment = (current.compareTo(last) >= 0) ? HotelKpiJudgmentCons.GOOD : HotelKpiJudgmentCons.BAD;
            } else {
                judgment = (current.compareTo(last) <= 0) ? HotelKpiJudgmentCons.GOOD : HotelKpiJudgmentCons.BAD;
            }
            String trend;
            if (current.compareTo(last) > 0) {
                trend = HotelKpiTrendCons.INCREASE;
            } else if (current.compareTo(last) < 0) {
                trend = HotelKpiTrendCons.DECREASE;
            } else {
                trend = HotelKpiTrendCons.STABLE;
            }
            HotelKpiV2ComparisonDTO comparison = new HotelKpiV2ComparisonDTO();
            comparison.setJudgment(judgment);
            comparison.setTrend(trend);
            currentDTO.setComparison(comparison);
        }

        /**
         * 组装指标数值
         *
         * @param metricsList  指标数值
         * @param roundingMode 舍入模式
         * @author zc
         * @date 2025/8/19
         */
        @Override
        public void packageMetricsValueV2(List<HotelKpiV2MetricsCategoryDTO> metricsList, RoundingMode roundingMode) {
            // 设置系统采集值、手动录入值
            metricsList.forEach(categoryDTO -> {
                setCrawlAndManualValueV2(categoryDTO.getActual());
                setCrawlAndManualValueV2(categoryDTO.getBudget());
                setCrawlAndManualValueV2(categoryDTO.getLy());

                categoryDTO.getBudgetChg().setValue(calculatePercentChg(categoryDTO.getActual(), categoryDTO.getBudget(), roundingMode));
                categoryDTO.getLyChg().setValue(calculatePercentChg(categoryDTO.getActual(), categoryDTO.getLy(), roundingMode));
            });
        }

        /**
         * 设置采集值、手动录入值 V2
         *
         * @param valueDTO 指标值
         * @author zc
         * @date 2025/8/19
         */
        private void setCrawlAndManualValueV2(HotelKpiV2ValueDTO valueDTO) {
            HotelKpiV2CalculationFactorDTO factorDTO = valueDTO.getCalculationFactor().stream()
                    .filter(item -> item.getIndex() == 1)
                    .findFirst().orElse(new HotelKpiV2CalculationFactorDTO());
            valueDTO.setCrawlValue(factorDTO.getCrawl());
            valueDTO.setManualValue(factorDTO.getManual());
        }

        /**
         * 计算 % Chg
         *
         * @param first        计算值 1
         * @param second       计算值 2
         * @param roundingMode 舍入模式
         * @return % Chg
         * @author zc
         * @date 2025/8/20
         */
        private BigDecimal calculatePercentChg(HotelKpiV2ValueDTO first, HotelKpiV2ValueDTO second, RoundingMode roundingMode) {
            BigDecimal firstValue = getEffectiveValueV2(first);
            BigDecimal secondValue = getEffectiveValueV2(second);
            if (Objects.isNull(firstValue) || Objects.isNull(secondValue) || secondValue.compareTo(BigDecimal.ZERO) == 0) {
                return null;
            }
            return firstValue.subtract(secondValue).multiply(new BigDecimal("100"))
                    .divide(secondValue, BaseConstant.DATA_PRECISION, roundingMode);
        }
    }