# HotelKpiMetricsSettingActualServiceImpl.java

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

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiDateTypeCons;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiSettingCategoryEnum;
    import cloud.wconnect.workperformance.cons.hotelkpi.HotelKpiValueCategoryCons;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiMetricsSetting;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiCalculationFactorDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiMetricsDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpi.HotelKpiStageValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.HotelKpiSettingCalculationDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.HotelKpiSettingCalculationFactorDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.HotelKpiSettingCalculationFactorSettingDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.actual.HotelKpiMetricsSettingActualDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.actual.HotelKpiMetricsSettingConstantActualDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.setting.actual.HotelKpiMetricsSettingDiminishingSurplusActualDTO;
    import cloud.wconnect.workperformance.service.hotelkpi.IHotelKpiMetricsSettingActualService;
    import cloud.wconnect.workperformance.service.hotelkpi.IHotelKpiMetricsSettingService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Maps;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.stereotype.Service;

    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.DayOfWeek;
    import java.time.LocalDate;
    import java.time.YearMonth;
    import java.time.temporal.TemporalAdjusters;
    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * Hotel KPI Metrics Setting Actual 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-08-06
     */
    @Service
    @DS("work_performance")
    public class HotelKpiMetricsSettingActualServiceImpl implements IHotelKpiMetricsSettingActualService {
        @Resource
        private IHotelKpiMetricsSettingService metricsSettingService;
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        private I18nMessageUtils i18nMessageUtils;

        private static final int VALUE_CATEGORY = HotelKpiValueCategoryCons.ACTUAL;

        private static final List<Integer> largeMonths = Lists.newArrayList(1, 3, 5, 7, 8, 10, 12);

        /**
         * 根据日期范围获取周设置
         *
         * @param propertyId            资产 id
         * @param metricsTypes          指标类型
         * @param startDateLong         开始日期
         * @param endDateLong           结束日期
         * @param metricsDTOMonthlyList Monthly 指标数据集合
         * @param dayOfWeek             周几
         * @return HotelKpiMetricsSettingActualDTO 集合
         * @author zc
         * @date 2024/8/6
         */
        @Override
        public List<HotelKpiMetricsSettingActualDTO> listWeeklySettingByDateRange(int propertyId, List<Integer> metricsTypes, long startDateLong, long endDateLong,
                                                                                  List<HotelKpiMetricsDTO> metricsDTOMonthlyList, DayOfWeek dayOfWeek) {
            if (CollectionUtils.isEmpty(metricsTypes)) {
                return Collections.emptyList();
            }
            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(startDateLong), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(endDateLong), BaseConstant.DATE_FORMAT_YYYYMMDD);

            List<HotelKpiMetricsSetting> settingList = metricsSettingService.listByYearRangeAndMetricsTypes(propertyId,
                    metricsTypes, startLocalDate.getYear(), endLocalDate.getYear(), VALUE_CATEGORY);
            if (CollectionUtils.isEmpty(settingList)) {
                return Collections.emptyList();
            }
            Map<Long, List<HotelKpiMetricsSetting>> year2SettingList = settingList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsSetting::getYear));

            Map<Integer, List<HotelKpiMetricsDTO>> metricsType2MetricsMonthlyList = metricsDTOMonthlyList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsDTO::getMetricsType));

            List<HotelKpiMetricsSettingActualDTO> settingValueDTOList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = indexLocalDate.plusDays(7)) {
                List<HotelKpiMetricsSetting> settingListByYear = year2SettingList.get((long) indexLocalDate.getYear());
                if (CollectionUtils.isEmpty(settingListByYear)) {
                    continue;
                }
                for (HotelKpiMetricsSetting metricsSetting : settingListByYear) {
                    List<HotelKpiMetricsDTO> metricsMonthlyList = metricsType2MetricsMonthlyList.getOrDefault(metricsSetting.getMetricsType(), Collections.emptyList());
                    settingValueDTOList.add(convertToSettingValueDTO(HotelKpiDateTypeCons.WEEKLY, indexLocalDate, metricsSetting,
                            Collections.emptyList(), metricsMonthlyList, dayOfWeek));
                }
            }
            return settingValueDTOList;
        }

        /**
         * 根据日期范围获取月设置
         *
         * @param propertyId     资产 id
         * @param metricsTypes   指标类型
         * @param startDateLong  开始日期
         * @param endDateLong    结束日期
         * @param metricsDTOList 指标数据
         * @param dayOfWeek      周几
         * @return HotelKpiMetricsSettingActualDTO 集合
         * @author zc
         * @date 2024/8/6
         */
        @Override
        public List<HotelKpiMetricsSettingActualDTO> listMonthlySettingByDateRange(int propertyId, List<Integer> metricsTypes, long startDateLong, long endDateLong,
                                                                                   List<HotelKpiMetricsDTO> metricsDTOList, DayOfWeek dayOfWeek) {
            if (CollectionUtils.isEmpty(metricsTypes)) {
                return Collections.emptyList();
            }
            LocalDate startLocalDate = YearMonthUtils.getYearMonth(String.valueOf(startDateLong)).atDay(1);
            LocalDate endLocalDate = YearMonthUtils.getYearMonth(String.valueOf(endDateLong)).atDay(1);

            List<HotelKpiMetricsSetting> settingList = metricsSettingService.listByYearRangeAndMetricsTypes(propertyId,
                    metricsTypes, startLocalDate.getYear(), endLocalDate.getYear(), VALUE_CATEGORY);
            if (CollectionUtils.isEmpty(settingList)) {
                return Collections.emptyList();
            }
            Map<Long, List<HotelKpiMetricsSetting>> year2SettingList = settingList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsSetting::getYear));

            Map<Integer, List<HotelKpiMetricsDTO>> metricsType2MetricsList = metricsDTOList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsDTO::getMetricsType));

            List<HotelKpiMetricsSettingActualDTO> settingValueDTOList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = indexLocalDate.plusMonths(1)) {
                List<HotelKpiMetricsSetting> settingListByYear = year2SettingList.get((long) indexLocalDate.getYear());
                if (CollectionUtils.isEmpty(settingListByYear)) {
                    continue;
                }
                for (HotelKpiMetricsSetting metricsSetting : settingListByYear) {
                    List<HotelKpiMetricsDTO> metricsList = metricsType2MetricsList.getOrDefault(metricsSetting.getMetricsType(), Collections.emptyList());
                    settingValueDTOList.add(convertToSettingValueDTO(HotelKpiDateTypeCons.MONTHLY, indexLocalDate, metricsSetting,
                            metricsList, Collections.emptyList(), dayOfWeek));
                }
            }
            return settingValueDTOList;
        }

        /**
         * 根据日期范围获取季设置
         *
         * @param propertyId     资产 id
         * @param metricsTypes   指标类型
         * @param startDateLong  开始日期
         * @param endDateLong    结束日期
         * @param metricsDTOList 指标数据
         * @param dayOfWeek      周几
         * @return HotelKpiMetricsSettingActualDTO 集合
         * @author zc
         * @date 2024/8/6
         */
        @Override
        public List<HotelKpiMetricsSettingActualDTO> listQuarterlySettingByDateRange(int propertyId, List<Integer> metricsTypes, long startDateLong, long endDateLong,
                                                                                     List<HotelKpiMetricsDTO> metricsDTOList, DayOfWeek dayOfWeek) {
            if (CollectionUtils.isEmpty(metricsTypes)) {
                return Collections.emptyList();
            }
            LocalDate startLocalDate = YearMonthUtils.getYearMonth(String.valueOf(startDateLong)).atDay(1);
            LocalDate endLocalDate = YearMonthUtils.getYearMonth(String.valueOf(endDateLong)).atDay(1);

            List<HotelKpiMetricsSetting> settingList = metricsSettingService.listByYearRangeAndMetricsTypes(propertyId,
                    metricsTypes, startLocalDate.getYear(), endLocalDate.getYear(), VALUE_CATEGORY);
            if (CollectionUtils.isEmpty(settingList)) {
                return Collections.emptyList();
            }
            Map<Long, List<HotelKpiMetricsSetting>> year2SettingList = settingList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsSetting::getYear));

            Map<Integer, List<HotelKpiMetricsDTO>> metricsType2MetricsList = metricsDTOList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsDTO::getMetricsType));

            List<HotelKpiMetricsSettingActualDTO> settingValueDTOList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = indexLocalDate.plusMonths(3)) {
                List<HotelKpiMetricsSetting> settingListByYear = year2SettingList.get((long) indexLocalDate.getYear());
                if (CollectionUtils.isEmpty(settingListByYear)) {
                    continue;
                }
                for (HotelKpiMetricsSetting metricsSetting : settingListByYear) {
                    List<HotelKpiMetricsDTO> metricsList = metricsType2MetricsList.getOrDefault(metricsSetting.getMetricsType(), Collections.emptyList());
                    settingValueDTOList.add(convertToSettingValueDTO(HotelKpiDateTypeCons.QUARTERLY, indexLocalDate, metricsSetting,
                            metricsList, Collections.emptyList(), dayOfWeek));
                }
            }
            return settingValueDTOList;
        }

        /**
         * 根据日期范围获取年设置
         *
         * @param propertyId     资产 id
         * @param metricsTypes   指标类型
         * @param startDateLong  开始日期
         * @param endDateLong    结束日期
         * @param metricsDTOList 指标数据
         * @param dayOfWeek      周几
         * @return HotelKpiMetricsSettingActualDTO 集合
         * @author zc
         * @date 2024/8/6
         */
        @Override
        public List<HotelKpiMetricsSettingActualDTO> listYearlySettingByDateRange(int propertyId, List<Integer> metricsTypes, long startDateLong, long endDateLong,
                                                                                  List<HotelKpiMetricsDTO> metricsDTOList, DayOfWeek dayOfWeek) {
            if (CollectionUtils.isEmpty(metricsTypes)) {
                return Collections.emptyList();
            }
            LocalDate startLocalDate = YearMonth.of((int) startDateLong, 1).atDay(1);
            LocalDate endLocalDate = YearMonth.of((int) endDateLong, 1).atDay(1);

            List<HotelKpiMetricsSetting> settingList = metricsSettingService.listByYearRangeAndMetricsTypes(propertyId,
                    metricsTypes, startLocalDate.getYear(), endLocalDate.getYear(), VALUE_CATEGORY);
            if (CollectionUtils.isEmpty(settingList)) {
                return Collections.emptyList();
            }
            Map<Long, List<HotelKpiMetricsSetting>> year2SettingList = settingList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsSetting::getYear));

            Map<Integer, List<HotelKpiMetricsDTO>> metricsType2MetricsList = metricsDTOList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsDTO::getMetricsType));

            List<HotelKpiMetricsSettingActualDTO> settingValueDTOList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = indexLocalDate.plusYears(1)) {
                List<HotelKpiMetricsSetting> settingListByYear = year2SettingList.get((long) indexLocalDate.getYear());
                if (CollectionUtils.isEmpty(settingListByYear)) {
                    continue;
                }
                for (HotelKpiMetricsSetting targetSetting : settingListByYear) {
                    List<HotelKpiMetricsDTO> metricsList = metricsType2MetricsList.getOrDefault(targetSetting.getMetricsType(), Collections.emptyList());
                    settingValueDTOList.add(convertToSettingValueDTO(HotelKpiDateTypeCons.YEARLY, indexLocalDate, targetSetting,
                            metricsList, Collections.emptyList(), dayOfWeek));
                }
            }
            return settingValueDTOList;
        }

        /**
         * 转换设置值
         *
         * @param dateType              日期类型
         * @param localDate             当前日期
         * @param setting               设置
         * @param metricsDTOList        指标数值集合
         * @param monthlyMetricsDTOList Monthly 指标数值集合
         * @param dayOfWeek             周几
         * @return HotelKpiMetricsSettingActualDTO
         * @author zc
         * @date 2024/6/26
         */
        private HotelKpiMetricsSettingActualDTO convertToSettingValueDTO(int dateType, LocalDate localDate, HotelKpiMetricsSetting setting,
                                                                         List<HotelKpiMetricsDTO> metricsDTOList,
                                                                         List<HotelKpiMetricsDTO> monthlyMetricsDTOList, DayOfWeek dayOfWeek) {
            int category = setting.getSettingCategory();
            if (HotelKpiSettingCategoryEnum.DYNAMIC.getCategory() == category) {
                return extractionForDynamic(dateType, localDate, setting);
            } else {
                return extractionForDiminishingSurplus(dateType, localDate, setting, metricsDTOList,
                        monthlyMetricsDTOList, dayOfWeek);
            }
        }

        /**
         * 提取设置值 for Dynamic（动态）
         *
         * @param setting 设置值
         * @return HotelKpiMetricsSettingActualDTO
         * @author zc
         * @date 2024/6/28
         */
        private HotelKpiMetricsSettingActualDTO extractionForDynamic(int dateType, LocalDate localDate, HotelKpiMetricsSetting setting) {
            long date;
            if (HotelKpiDateTypeCons.WEEKLY == dateType) {
                date = Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            } else if (HotelKpiDateTypeCons.MONTHLY == dateType) {
                date = Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMM));
            } else if (HotelKpiDateTypeCons.QUARTERLY == dateType) {
                date = Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMM));
            } else {
                date = localDate.getYear();
            }
            return HotelKpiMetricsSettingActualDTO.builder()
                    .date(date)
                    .metricsType(setting.getMetricsType())
                    .index(setting.getIndex())
                    .settingCategory(setting.getSettingCategory())
                    .build();
        }

        /**
         * 提取设置值 for DiminishingSurplus（递减剩余）
         *
         * @param dateType              日期类型
         * @param localDate             日期
         * @param setting               设置值
         * @param metricsDTOList        指标数据
         * @param monthlyMetricsDTOList Monthly 指标数值集合
         * @param dayOfWeek             周几
         * @return HotelKpiMetricsSettingActualDTO
         * @author zc
         * @date 2024/8/1
         */
        private HotelKpiMetricsSettingActualDTO extractionForDiminishingSurplus(int dateType, LocalDate localDate, HotelKpiMetricsSetting setting,
                                                                                List<HotelKpiMetricsDTO> metricsDTOList, List<HotelKpiMetricsDTO> monthlyMetricsDTOList,
                                                                                DayOfWeek dayOfWeek) {
            HotelKpiMetricsSettingDiminishingSurplusActualDTO settingDiminishingSurplusDTO = JsonUtils.convertStringToObject(objectMapper,
                    setting.getDetail(), HotelKpiMetricsSettingDiminishingSurplusActualDTO.class);

            Integer monthRange = settingDiminishingSurplusDTO.getRange();
            BigDecimal totalValue = settingDiminishingSurplusDTO.getValue();
            if (Objects.isNull(monthRange) || Objects.isNull(totalValue)) {
                return new HotelKpiMetricsSettingActualDTO();
            }
            int totalTargetPerCycle = totalValue.intValue();

            long date;
            BigDecimal calculationTarget;
            BigDecimal stageTotalValue = totalValue;
            Map<Long, BigDecimal> date2TargetValue = Maps.newHashMap();
            if (HotelKpiDateTypeCons.WEEKLY == dateType) {
                date = Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMMDD));

                Map<Long, HotelKpiMetricsDTO> month2MetricsDTO = monthlyMetricsDTOList.stream()
                        .collect(Collectors.toMap(HotelKpiMetricsDTO::getDate, Function.identity()));
                // 计算当前月份的计算因子 Target
                long monthly = Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMM));
                BigDecimal monthTargetValue = calculateMonthTargetValue(localDate, totalTargetPerCycle, monthRange, month2MetricsDTO).get(monthly);
                // 计算当前周期的计算因子 Target
                date2TargetValue = calculateWeekTargetValue(localDate, monthTargetValue, dayOfWeek);
                calculationTarget = date2TargetValue.get(Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMMDD)));
                // 以月份目标值作为周的目标总值
                stageTotalValue = monthTargetValue;
            } else if (HotelKpiDateTypeCons.MONTHLY == dateType) {
                date = Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMM));

                Map<Long, HotelKpiMetricsDTO> month2MetricsDTO = metricsDTOList.stream()
                        .collect(Collectors.toMap(HotelKpiMetricsDTO::getDate, Function.identity()));
                // 计算当前月份的计算因子 Target
                date2TargetValue = calculateMonthTargetValue(localDate, totalTargetPerCycle, monthRange, month2MetricsDTO);
                calculationTarget = date2TargetValue.get(Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMM)));
            } else if (HotelKpiDateTypeCons.QUARTERLY == dateType) {
                date = Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMM));

                Map<Long, HotelKpiMetricsDTO> month2MetricsDTO = metricsDTOList.stream()
                        .collect(Collectors.toMap(HotelKpiMetricsDTO::getDate, Function.identity()));
                // 计算当前月份的计算因子 Target
                date2TargetValue = calculateQuarterTargetValue(localDate, totalTargetPerCycle, monthRange, month2MetricsDTO);
                calculationTarget = date2TargetValue.get(Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMM)));
            } else {
                date = localDate.getYear();
                calculationTarget = new BigDecimal(String.valueOf(totalTargetPerCycle * (12 / monthRange)));
                date2TargetValue.put(date, calculationTarget);
            }
            List<HotelKpiStageValueDTO> stageValue = Lists.newArrayList();
            date2TargetValue.forEach((key, value) -> {
                HotelKpiStageValueDTO stageValueDTO = new HotelKpiStageValueDTO();
                stageValueDTO.setDate(key);
                stageValueDTO.setValue(value);
                stageValue.add(stageValueDTO);
            });
            return HotelKpiMetricsSettingActualDTO.builder()
                    .date(date)
                    .metricsType(setting.getMetricsType())
                    .index(setting.getIndex())
                    .settingCategory(setting.getSettingCategory())
                    // 若小于0，则置为0
                    .value(calculationTarget.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : calculationTarget)
                    .stageValue(stageValue)
                    .stageTotalValue(stageTotalValue)
                    .build();
        }

        /**
         * 计算指定周期的目标值 for DiminishingSurplus（递减剩余）
         *
         * @param localDate        当前日期
         * @param monthTargetValue 对应月份的目标值
         * @param dayOfWeek        周几
         * @return 日期 -> 目标值
         * @author zc
         * @date 2025/2/27
         */
        private Map<Long, BigDecimal> calculateWeekTargetValue(LocalDate localDate, BigDecimal monthTargetValue, DayOfWeek dayOfWeek) {
            // 月末日期
            LocalDate lastDateInMonth = localDate.with(TemporalAdjusters.lastDayOfMonth());
            // 当月的天数
            BigDecimal totalOfDayInMonth = new BigDecimal(String.valueOf(lastDateInMonth.getDayOfMonth()));
            // 当月第一天
            LocalDate firstDateInMonth = localDate.with(TemporalAdjusters.firstDayOfMonth());
            // 当月第一周期的第一天
            LocalDate startDateInWeek = firstDateInMonth.with(TemporalAdjusters.nextOrSame(dayOfWeek));
            Map<Long, BigDecimal> weekly2TargetValue = Maps.newHashMap();
            for (LocalDate indexDateInWeek = startDateInWeek; !indexDateInWeek.isAfter(lastDateInMonth); indexDateInWeek =
                    indexDateInWeek.plusDays(7)) {
                LocalDate endDateInWeek = indexDateInWeek.plusDays(6);
                if (indexDateInWeek.getMonthValue() != endDateInWeek.getMonthValue()) {
                    endDateInWeek = lastDateInMonth;
                }
                // 当前循环周期的MTD总天数
                BigDecimal totalEndDayInMonth = new BigDecimal(String.valueOf(endDateInWeek.getDayOfMonth()));

                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexDateInWeek, BaseConstant.DATE_FORMAT_YYYYMMDD));
                // 向上取整
                BigDecimal indexTargetValue = (monthTargetValue.compareTo(BigDecimal.ZERO) == 0) ? BigDecimal.ZERO :
                        monthTargetValue.multiply(totalEndDayInMonth).divide(totalOfDayInMonth, 0, RoundingMode.UP);
                weekly2TargetValue.put(indexDate, indexTargetValue);
            }
            return weekly2TargetValue;
        }

        /**
         * 计算指定月份的目标值 for DiminishingSurplus（递减剩余）
         *
         * @param localDate           当前日期
         * @param totalTargetPerCycle 周期的总目标值
         * @param cycleLength         周期大小
         * @param month2MetricsDTO    指标数值
         * @return 月份 -> 目标值
         * @author zc
         * @date 2025/2/26
         */
        private Map<Long, BigDecimal> calculateMonthTargetValue(LocalDate localDate, int totalTargetPerCycle, int cycleLength,
                                                                Map<Long, HotelKpiMetricsDTO> month2MetricsDTO) {
            int year = localDate.getYear();
            int month = localDate.getMonthValue();
            int startMonth = ((month - 1) / cycleLength) * cycleLength + 1;

            int avgTarget = totalTargetPerCycle / cycleLength;
            Map<Integer, Integer> monthly2TargetValue = Maps.newHashMap();
            for (int m = startMonth; m < startMonth + cycleLength; m++) {
                monthly2TargetValue.put(m, avgTarget);
            }
            int remainder = totalTargetPerCycle % cycleLength;
            while (remainder > 0) {
                for (int m = startMonth; m < startMonth + cycleLength; m++) {
                    if (remainder == 0) {
                        break;
                    }
                    int targetValue = monthly2TargetValue.get(m);
                    // 均分到大月份上
                    if (largeMonths.contains(m)) {
                        targetValue++;
                        remainder--;
                    }
                    monthly2TargetValue.put(m, targetValue);
                }
            }
            for (int m = startMonth; m < month; m++) {
                int nextM = m + 1;
                Integer nextMonthTargetValue = monthly2TargetValue.get(nextM);
                if (Objects.isNull(nextMonthTargetValue)) {
                    break;
                }
                long monthly = YearMonthUtils.parseYearMonthToLong(YearMonth.of(year, m));
                HotelKpiMetricsDTO metricsDTO = month2MetricsDTO.getOrDefault(monthly, new HotelKpiMetricsDTO());
                int currentActualValue = getCalculationFactorIndex1(metricsDTO).intValue();
                int currentTargetValue = monthly2TargetValue.get(m);
                nextMonthTargetValue += (currentTargetValue - currentActualValue);
                monthly2TargetValue.put(nextM, nextMonthTargetValue);
            }
            Map<Long, BigDecimal> resultMap = Maps.newHashMap();
            for (Map.Entry<Integer, Integer> entry : monthly2TargetValue.entrySet()) {
                resultMap.put(YearMonthUtils.parseYearMonthToLong(YearMonth.of(year, entry.getKey())),
                        new BigDecimal(entry.getValue().toString()));
            }
            return resultMap;
        }

        /**
         * 计算指定季度的目标值 for DiminishingSurplus（递减剩余）
         *
         * @param localDate           当前日期
         * @param totalTargetPerCycle 周期的总目标值
         * @param cycleLength         周期大小
         * @param month2MetricsDTO    指标数值
         * @return 季度 -> 目标值
         * @author zc
         * @date 2025/2/26
         */
        private Map<Long, BigDecimal> calculateQuarterTargetValue(LocalDate localDate, int totalTargetPerCycle, int cycleLength,
                                                                  Map<Long, HotelKpiMetricsDTO> month2MetricsDTO) {
            Map<Long, BigDecimal> resultMap = Maps.newHashMap();
            int year = localDate.getYear();
            int month = localDate.getMonthValue();
            int target;
            if (cycleLength > 3) {
                int quarterCycleLength = cycleLength / 3;
                int quarterIndexOfYear = YearMonthWeekUtils.calculateQuarterOfYear(localDate);
                int startQuarter = ((quarterIndexOfYear - 1) / quarterCycleLength) * quarterCycleLength + 1;

                // 计算出每个季度的平均值
                int avgTarget = totalTargetPerCycle / quarterCycleLength;
                Map<Integer, Integer> quarterly2TargetValue = Maps.newHashMap();
                for (int q = startQuarter; q < startQuarter + quarterCycleLength; q++) {
                    int quarterly = (q - 1) * 3 + 1;
                    quarterly2TargetValue.put(quarterly, avgTarget);
                }
                int remainder = totalTargetPerCycle % quarterCycleLength;
                // 分配余数到各季度中
                while (remainder > 0) {
                    for (int q = startQuarter; q < startQuarter + quarterCycleLength; q++) {
                        if (remainder == 0) {
                            break;
                        }
                        int quarterly = (q - 1) * 3 + 1;
                        int targetValue = quarterly2TargetValue.get(quarterly);
                        targetValue++;
                        remainder--;
                        quarterly2TargetValue.put(quarterly, targetValue);
                    }
                }
                for (int q = startQuarter; q < month; q++) {
                    int currentQ = (q - 1) * 3 + 1;
                    int nextQ = currentQ + 3;
                    Integer nextQuarterTargetValue = quarterly2TargetValue.get(nextQ);
                    if (Objects.isNull(nextQuarterTargetValue)) {
                        break;
                    }
                    long quarterly = YearMonthUtils.parseYearMonthToLong(YearMonth.of(year, currentQ));
                    HotelKpiMetricsDTO metricsDTO = month2MetricsDTO.getOrDefault(quarterly, new HotelKpiMetricsDTO());
                    int currentActualValue = getCalculationFactorIndex1(metricsDTO).intValue();
                    int currentTargetValue = quarterly2TargetValue.get(currentQ);
                    nextQuarterTargetValue += (currentTargetValue - currentActualValue);
                    quarterly2TargetValue.put(nextQ, nextQuarterTargetValue);
                }
                for (Map.Entry<Integer, Integer> entry : quarterly2TargetValue.entrySet()) {
                    resultMap.put(YearMonthUtils.parseYearMonthToLong(YearMonth.of(year, entry.getKey())),
                            new BigDecimal(entry.getValue().toString()));
                }
            } else {
                target = totalTargetPerCycle * (3 / cycleLength);
                resultMap.put(YearMonthUtils.parseYearMonthToLong(YearMonth.of(year, month)),
                        new BigDecimal(String.valueOf(target)));
            }
            return resultMap;
        }

        /**
         * 获取计算因子 index 1
         *
         * @param metricsDTO 指标数据
         * @return java.math.BigDecimal
         * @author zc
         * @date 2024/8/11
         */
        private BigDecimal getCalculationFactorIndex1(HotelKpiMetricsDTO metricsDTO) {
            return metricsDTO.getActual().getCalculationFactor().stream().filter(item -> item.getIndex() == 1).findFirst()
                    .map(HotelKpiCalculationFactorDTO::getCrawl).orElse(BigDecimal.ZERO);
        }

        /**
         * 校验 settingValue 的格式
         *
         * @param settingDTO 指标 setting DTO
         * @author zc
         * @date 2024/8/7
         */
        @Override
        public void checkSettingValueFormat(HotelKpiSettingCalculationDTO settingDTO) {
            List<HotelKpiSettingCalculationFactorDTO> calculationFactor = settingDTO.getCalculationFactor();
            try {
                for (HotelKpiSettingCalculationFactorDTO factor : calculationFactor) {
                    HotelKpiSettingCalculationFactorSettingDTO valueSetting = factor.getValueSetting();

                    if (HotelKpiSettingCategoryEnum.DYNAMIC.getCategory() == valueSetting.getCategory()) {
                        JsonUtils.convertStringToObject(objectMapper, valueSetting.getSettingValue(),
                                HotelKpiMetricsSettingConstantActualDTO.class);
                    } else {
                        JsonUtils.convertStringToObject(objectMapper, valueSetting.getSettingValue(),
                                HotelKpiMetricsSettingDiminishingSurplusActualDTO.class);
                    }
                }
            } catch (Exception e) {
                throw new BadRequestException(CommonErrorI18nConstant.FORMAT_ERROR,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("settingValue"),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The settingValue structure is wrong format, exception:" + e.getMessage());
            }
        }
    }