# HotelKpiMetricsRmRevenueService.java

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

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.workperformance.cons.hotelkpi.*;
    import cloud.wconnect.workperformance.entity.hotelkpi.*;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.budgetdb.HotelKpiBudgetDatabaseDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.kpiv2.HotelKpiMonthlyReportFieldConfigDTO;
    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.originkpiv2.HotelKpiV2OriginDetailDTO;
    import cloud.wconnect.workperformance.service.hotelkpi.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Resource;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

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

    /**
     * @author zc
     * @date 2025/08/20
     */
    @Service
    @DS("work_performance")
    public class HotelKpiMetricsRmRevenueService implements IHotelKpiMetricsService {

        @Resource
        private IHotelKpiService hotelKpiService;
        @Resource
        private IHotelKpiWeeklyService weeklyService;
        @Resource
        private IHotelKpiMonthlyService monthlyService;
        @Resource
        private IHotelKpiQuarterlyService quarterlyService;
        @Resource
        private IHotelKpiYearlyService yearlyService;
        @Resource
        private IHotelKpiBudgetDatabaseService budgetDatabaseService;
        @Resource
        private IHotelKpiWconnectMonthlyReportService monthlyReportService;
        @Resource
        private IHotelKpiMonthlyReportFieldConfigService monthlyReportFieldConfigService;
        @Resource
        private IHotelKpiMetricsConfigService metricsConfigService;
        @Resource
        private IHotelKpiWeekStartDayService weekStartDayService;
        @Resource
        private IHotelKpiWconnectMonthlyRevenueService monthlyRevenueService;
        @Resource
        private IHotelKpiManagerFlashService managerFlashService;
        @Resource
        private ObjectMapper objectMapper;

        private static final int METRICS_TYPE = HotelKpiMetricsEnum.RM_REVENUE.getMetricsType();

        /**
         * 支持的指标类型
         *
         * @param metricsType 指标类型
         * @return boolean
         * @author zc
         * @date 2025/8/20
         */
        @Override
        public boolean supports(int metricsType) {
            return METRICS_TYPE == metricsType;
        }

        /**
         * 初始化
         *
         * @param propertyDTO  资产
         * @param nowLocalDate 当前时间
         * @author zc
         * @date 2025/9/11
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void initData(PropertyDTO propertyDTO, LocalDate nowLocalDate) {
            HotelKpiMetricsConfig metricsConfig = metricsConfigService.getMetricsConfigByTypeForUpdate(propertyDTO.getId(), METRICS_TYPE);
            if (Objects.isNull(metricsConfig)) {
                return;
            }
            initWeekly(propertyDTO, nowLocalDate, metricsConfig);
            initMonthly(propertyDTO, nowLocalDate, metricsConfig);
            initQuarterly(propertyDTO, nowLocalDate, metricsConfig);
            initYearly(propertyDTO, nowLocalDate, metricsConfig);
        }

        /**
         * 初始化 for Weekly
         *
         * @param propertyDTO   资产
         * @param nowLocalDate  当前时间
         * @param metricsConfig 指标配置
         * @author zc
         * @date 2025/9/11
         */
        private void initWeekly(PropertyDTO propertyDTO, LocalDate nowLocalDate, HotelKpiMetricsConfig metricsConfig) {
            // 当前日期若是周起始日，则生成上一周期的数据
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyDTO.getId());
            if (nowLocalDate.getDayOfWeek().compareTo(dayOfWeek) != 0) {
                return;
            }
            // 获取上一周期的起始日期
            LocalDate lastWeekStartLocalDate = nowLocalDate.minusDays(7);
            // 设置数据
            setWeekly(propertyDTO, lastWeekStartLocalDate, lastWeekStartLocalDate, metricsConfig);
        }

        /**
         * 初始化 for Monthly
         *
         * @param propertyDTO   资产
         * @param nowLocalDate  当前时间
         * @param metricsConfig 指标配置
         * @author zc
         * @date 2025/9/11
         */
        private void initMonthly(PropertyDTO propertyDTO, LocalDate nowLocalDate, HotelKpiMetricsConfig metricsConfig) {
            // 获取当前日期，如果不是当月第一天，则不处理
            if (nowLocalDate.getDayOfMonth() != 1) {
                return;
            }
            // 当前时间生成上一周期数据，需要减去一个月
            LocalDate lastMonthLocalDate = nowLocalDate.minusMonths(1);
            // 设置数据
            setMonthly(propertyDTO, lastMonthLocalDate, lastMonthLocalDate, metricsConfig);
        }

        /**
         * 初始化 for Quarterly
         *
         * @param propertyDTO   资产
         * @param nowLocalDate  当前时间
         * @param metricsConfig 指标配置
         * @author zc
         * @date 2025/9/11
         */
        private void initQuarterly(PropertyDTO propertyDTO, LocalDate nowLocalDate, HotelKpiMetricsConfig metricsConfig) {
            LocalDate firstLocalDateOfQuarter = LocalDate.of(nowLocalDate.getYear(),
                    nowLocalDate.getMonth().firstMonthOfQuarter(), 1);
            // 获取当前日期，若不是季度的第一天，则不处理
            if (!firstLocalDateOfQuarter.isEqual(nowLocalDate)) {
                return;
            }
            // 当前时间生成上一周期数据，需要减去3个月
            LocalDate lastQuarterLocalDate = nowLocalDate.minusMonths(3);
            // 设置数据
            setQuarterly(propertyDTO, lastQuarterLocalDate, lastQuarterLocalDate, metricsConfig);
        }

        /**
         * 初始化 for Yearly
         *
         * @param propertyDTO   资产
         * @param nowLocalDate  当前时间
         * @param metricsConfig 指标配置
         * @author zc
         * @date 2025/9/11
         */
        private void initYearly(PropertyDTO propertyDTO, LocalDate nowLocalDate, HotelKpiMetricsConfig metricsConfig) {
            // 获取当前日期，如果不是当月第一天，则不处理
            if (nowLocalDate.getDayOfMonth() != 1) {
                return;
            }
            // 当前时间生成截至到上个月的 YTD 数据，需要减去一个月
            LocalDate lastMonthLocalDate = nowLocalDate.minusMonths(1);
            // 设置数据
            setYearly(propertyDTO, lastMonthLocalDate, lastMonthLocalDate, metricsConfig);
        }

        /**
         * 触发设置数据
         *
         * @param propertyDTO 资产
         * @param triggerType 触发类型，不同类型的日期格式不同
         * @param startDate   开始日期(yyyyMMdd,yyyyMM,yyyy)
         * @param endDate     结束日期(yyyyMMdd,yyyyMM,yyyy)
         * @author zc
         * @date 2025/9/12
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void triggerSetData(PropertyDTO propertyDTO, int triggerType, long startDate, long endDate) {
            HotelKpiMetricsConfig metricsConfig = metricsConfigService.getMetricsConfigByTypeForUpdate(propertyDTO.getId(), METRICS_TYPE);
            if (Objects.isNull(metricsConfig)) {
                return;
            }
            if (HotelKpiTriggerTypeCons.MONTHLY_REVENUE == triggerType) {
                LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(startDate),
                        BaseConstant.DATE_FORMAT_YYYYMMDD);
                // 指标计算最小时间维度是 MTD，统计维度有重叠，结束日期需设置为该月份的月底
                LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(endDate),
                        BaseConstant.DATE_FORMAT_YYYYMMDD).with(TemporalAdjusters.lastDayOfMonth());

                setWeekly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);

                // 触发设置次年数据
                setWeekly(propertyDTO, startLocalDate.plusYears(1), endLocalDate.plusYears(1), metricsConfig);
            } else if (HotelKpiTriggerTypeCons.BUDGET_DATABASE == triggerType) {
                LocalDate startLocalDate = YearMonth.of((int) startDate, 1).atDay(1);
                LocalDate endLocalDate = YearMonth.of((int) endDate, 12).atDay(1);

                setWeekly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
                setMonthly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
                setQuarterly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
                setYearly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
            } else if (HotelKpiTriggerTypeCons.MANAGER_FLASH == triggerType) {
                LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(startDate),
                        BaseConstant.DATE_FORMAT_YYYYMMDD);
                LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(endDate),
                        BaseConstant.DATE_FORMAT_YYYYMMDD);

                setMonthly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
                setQuarterly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
                setYearly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
            } else if (HotelKpiTriggerTypeCons.MONTHLY_REPORT == triggerType) {
                LocalDate startLocalDate = YearMonthUtils.getYearMonth(String.valueOf(startDate))
                        .plusYears(1).atDay(1);
                LocalDate endLocalDate = YearMonthUtils.getYearMonth(String.valueOf(endDate))
                        .plusYears(1).atDay(1);

                setMonthly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
                setQuarterly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
                setYearly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
            }
        }

        /**
         * 设置数据
         *
         * @param propertyDTO    资产
         * @param dateType       日期类型
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @author zc
         * @date 2025/9/11
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void setData(PropertyDTO propertyDTO, int dateType, LocalDate startLocalDate, LocalDate endLocalDate) {
            HotelKpiMetricsConfig metricsConfig = metricsConfigService.getMetricsConfigByTypeForUpdate(propertyDTO.getId(), METRICS_TYPE);
            if (Objects.isNull(metricsConfig)) {
                return;
            }
            if (HotelKpiDateTypeCons.WEEKLY == dateType) {
                setWeekly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
            } else if (HotelKpiDateTypeCons.MONTHLY == dateType) {
                setMonthly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
                setQuarterly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
                setYearly(propertyDTO, startLocalDate, endLocalDate, metricsConfig);
            }
        }

        /**
         * 设置数据 for Weekly
         *
         * @param propertyDTO    资产
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @param metricsConfig  指标配置
         * @author zc
         * @date 2025/9/11
         */
        private void setWeekly(PropertyDTO propertyDTO, LocalDate startLocalDate, LocalDate endLocalDate, HotelKpiMetricsConfig metricsConfig) {
            int dateType = HotelKpiDateTypeCons.WEEKLY;
            int propertyId = propertyDTO.getId();
            String factorConfigStr = metricsConfig.getCalculationFactorConfig();
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyId);
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            // 转换为周的开始日期
            LocalDate startWeekLocalDate = startLocalDate.with(TemporalAdjusters.previousOrSame(dayOfWeek));
            LocalDate endWeekLocalDate = endLocalDate.with(TemporalAdjusters.previousOrSame(dayOfWeek));

            // 获取第一周的结束日期
            LocalDate startWeekLastLocalDate = startWeekLocalDate.plusDays(6);
            // 获取最后一周的结束日期
            LocalDate endWeekLastLocalDate = endWeekLocalDate.plusDays(6);

            LocalDate firstDayOfMonth = startWeekLastLocalDate.with(TemporalAdjusters.firstDayOfMonth());
            LocalDate lastDayOfMonth = endWeekLastLocalDate.with(TemporalAdjusters.lastDayOfMonth());
            // 获取 Monthly Revenue 数据
            long startDayOfMonth = Long.parseLong(DateTimeUtils.localDateToStr(firstDayOfMonth, BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDayOfMonth = Long.parseLong(DateTimeUtils.localDateToStr(lastDayOfMonth, BaseConstant.DATE_FORMAT_YYYYMMDD));
            List<HotelKpiWconnectMonthlyRevenue> monthlyRevenueList = monthlyRevenueService.listByDateRange(propertyId,
                    startDayOfMonth, endDayOfMonth);

            // 获取去年同月数据 Monthly Revenue 数据
            long startDateLy = Long.parseLong(DateTimeUtils.localDateToStr(firstDayOfMonth
                    .minusYears(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDateLy = Long.parseLong(DateTimeUtils.localDateToStr(lastDayOfMonth
                    .minusYears(1).with(TemporalAdjusters.lastDayOfMonth()), BaseConstant.DATE_FORMAT_YYYYMMDD));
            List<HotelKpiWconnectMonthlyRevenue> lyMonthlyRevenueList = monthlyRevenueService.listByDateRange(propertyId,
                    startDateLy, endDateLy);

            // 获取 Budget Database 数据
            List<HotelKpiBudgetDatabaseDTO> budgetList = budgetDatabaseService.listByYearRange(propertyId,
                    startWeekLastLocalDate.getYear(), endWeekLastLocalDate.getYear());

            // 获取原始KPI数据
            long startWeekDate = Long.parseLong(DateTimeUtils.localDateToStr(startWeekLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endWeekDate = Long.parseLong(DateTimeUtils.localDateToStr(endWeekLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            Map<Long, String> date2OriginDetail = weeklyService.listByDateRangeAndMetricsType(propertyId,
                            startWeekDate, endWeekDate, METRICS_TYPE).stream()
                    .collect(Collectors.toMap(HotelKpiWeekly::getWeek, HotelKpiWeekly::getDetail));

            List<HotelKpiWeekly> saveList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startWeekLocalDate; !indexLocalDate.isAfter(endWeekLocalDate); indexLocalDate = indexLocalDate.plusWeeks(1)) {
                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));

                String originDetail = date2OriginDetail.get(indexDate);
                List<HotelKpiV2OriginDetailDTO> originDetailDTOList = hotelKpiService.convert2V2OriginDetailDTOList(originDetail, dateType, factorConfigStr);

                LocalDate endIndexLocalDate = indexLocalDate.plusDays(6);

                LocalDate indexFirstDayOfMonth = endIndexLocalDate.withDayOfMonth(1);
                LocalDate indexEndDayOfMonth = endIndexLocalDate.with(TemporalAdjusters.lastDayOfMonth());
                long indexStartDate = Long.parseLong(DateTimeUtils.localDateToStr(indexFirstDayOfMonth, BaseConstant.DATE_FORMAT_YYYYMMDD));
                long indexEndDate = Long.parseLong(DateTimeUtils.localDateToStr(indexEndDayOfMonth, BaseConstant.DATE_FORMAT_YYYYMMDD));

                // 截取 Monthly Revenue 数据
                List<HotelKpiWconnectMonthlyRevenue> effectiveMonthlyRevList = monthlyRevenueList.stream().filter(item ->
                        item.getDate() >= indexStartDate && item.getDate() <= indexEndDate).toList();
                // 截取 LY Monthly Revenue 数据
                long indexLyStartDate = Long.parseLong(DateTimeUtils.localDateToStr(indexFirstDayOfMonth
                        .minusYears(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
                long indexLyEndDate = Long.parseLong(DateTimeUtils.localDateToStr(indexEndDayOfMonth
                        .minusYears(1).with(TemporalAdjusters.lastDayOfMonth()), BaseConstant.DATE_FORMAT_YYYYMMDD));
                List<HotelKpiWconnectMonthlyRevenue> lyEffectiveMonthlyRevList = lyMonthlyRevenueList.stream().filter(item ->
                        item.getDate() >= indexLyStartDate && item.getDate() <= indexLyEndDate).toList();

                // 设置 total.actual
                BigDecimal totalActualValue = calculateForMonthlyRevenue(effectiveMonthlyRevList,
                        HotelKpiValueCategoryCons.CATEGORY_1);
                setMetricsValue(originDetailDTOList, totalActualValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.ACTUAL, roundingMode);

                // 设置 total.ly
                BigDecimal totalLyValue = calculateForMonthlyRevenue(lyEffectiveMonthlyRevList,
                        HotelKpiValueCategoryCons.CATEGORY_1);
                setMetricsValue(originDetailDTOList, totalLyValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.LY, roundingMode);

                // 设置 transient.actual
                BigDecimal transientActualValue = calculateForMonthlyRevenue(effectiveMonthlyRevList,
                        HotelKpiValueCategoryCons.CATEGORY_2);
                setMetricsValue(originDetailDTOList, transientActualValue, HotelKpiValueCategoryCons.CATEGORY_2,
                        HotelKpiValueCategoryCons.ACTUAL, roundingMode);

                // 设置 transient.ly
                BigDecimal transientLyValue = calculateForMonthlyRevenue(lyEffectiveMonthlyRevList,
                        HotelKpiValueCategoryCons.CATEGORY_2);
                setMetricsValue(originDetailDTOList, transientLyValue, HotelKpiValueCategoryCons.CATEGORY_2,
                        HotelKpiValueCategoryCons.LY, roundingMode);

                // 设置 group.actual
                BigDecimal groupActualValue = calculateForMonthlyRevenue(effectiveMonthlyRevList,
                        HotelKpiValueCategoryCons.CATEGORY_3);
                setMetricsValue(originDetailDTOList, groupActualValue, HotelKpiValueCategoryCons.CATEGORY_3,
                        HotelKpiValueCategoryCons.ACTUAL, roundingMode);

                // 设置 group.ly
                BigDecimal groupLyValue = calculateForMonthlyRevenue(lyEffectiveMonthlyRevList,
                        HotelKpiValueCategoryCons.CATEGORY_3);
                setMetricsValue(originDetailDTOList, groupLyValue, HotelKpiValueCategoryCons.CATEGORY_3,
                        HotelKpiValueCategoryCons.LY, roundingMode);

                // 设置 contract.actual
                BigDecimal contractActualValue = calculateForMonthlyRevenue(effectiveMonthlyRevList,
                        HotelKpiValueCategoryCons.CATEGORY_4);
                setMetricsValue(originDetailDTOList, contractActualValue, HotelKpiValueCategoryCons.CATEGORY_4,
                        HotelKpiValueCategoryCons.ACTUAL, roundingMode);

                // 设置 contract.ly
                BigDecimal contractLyValue = calculateForMonthlyRevenue(lyEffectiveMonthlyRevList,
                        HotelKpiValueCategoryCons.CATEGORY_4);
                setMetricsValue(originDetailDTOList, contractLyValue, HotelKpiValueCategoryCons.CATEGORY_4,
                        HotelKpiValueCategoryCons.LY, roundingMode);

                // 截取 Budget Database 数据
                HotelKpiBudgetDatabaseDTO budgetDTO = budgetList.stream().filter(item ->
                        item.getYear() == endIndexLocalDate.getYear()).findFirst().orElse(new HotelKpiBudgetDatabaseDTO());
                long indexMonth = Long.parseLong(DateTimeUtils.localDateToStr(endIndexLocalDate, BaseConstant.DATE_FORMAT_YYYYMM));
                // 设置 total.budget
                BigDecimal totalBudgetValue = budgetDatabaseService.getRoomRevenueBudget(budgetDTO, indexMonth, indexMonth, null);
                setMetricsValue(originDetailDTOList, totalBudgetValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.BUDGET, roundingMode);
                // 设置 transient.budget
                BigDecimal transientBudgetValue = budgetDatabaseService.getRoomRevenueBudget(budgetDTO, indexMonth, indexMonth,
                        HotelKpiBudgetDatabaseCons.ROOM_TRANSIENT);
                setMetricsValue(originDetailDTOList, transientBudgetValue, HotelKpiValueCategoryCons.CATEGORY_2,
                        HotelKpiValueCategoryCons.BUDGET, roundingMode);
                // 设置 group.budget
                BigDecimal groupBudgetValue = budgetDatabaseService.getRoomRevenueBudget(budgetDTO, indexMonth, indexMonth,
                        HotelKpiBudgetDatabaseCons.ROOM_GROUP);
                setMetricsValue(originDetailDTOList, groupBudgetValue, HotelKpiValueCategoryCons.CATEGORY_3,
                        HotelKpiValueCategoryCons.BUDGET, roundingMode);
                // 设置 contract.budget
                BigDecimal contractBudgetValue = budgetDatabaseService.getRoomRevenueBudget(budgetDTO, indexMonth, indexMonth,
                        HotelKpiBudgetDatabaseCons.ROOM_CONTRACT);
                setMetricsValue(originDetailDTOList, contractBudgetValue, HotelKpiValueCategoryCons.CATEGORY_4,
                        HotelKpiValueCategoryCons.BUDGET, roundingMode);

                String detail = JsonUtils.convertObjListToString(objectMapper, originDetailDTOList);

                saveList.add(HotelKpiWeekly.builder()
                        .propertyId(propertyId)
                        .week(indexDate)
                        .metricsType(METRICS_TYPE)
                        .detail(detail)
                        .build());
            }
            weeklyService.insertOrUpdateBatch(saveList);
        }

        /**
         * 设置数据 for Monthly
         *
         * @param propertyDTO    资产
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @param metricsConfig  指标配置
         * @author zc
         * @date 2025/9/11
         */
        private void setMonthly(PropertyDTO propertyDTO, LocalDate startLocalDate, LocalDate endLocalDate, HotelKpiMetricsConfig metricsConfig) {
            int dateType = HotelKpiDateTypeCons.MONTHLY;
            int propertyId = propertyDTO.getId();
            String factorConfigStr = metricsConfig.getCalculationFactorConfig();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());
            // 获取 W-connect - Monthly Report 字段映射配置
            HotelKpiMonthlyReportFieldConfigDTO fieldConfigDTO = monthlyReportFieldConfigService.get(propertyId);

            YearMonth startYearMonth = YearMonth.of(startLocalDate.getYear(), startLocalDate.getMonthValue());
            YearMonth endYearMonth = YearMonth.of(endLocalDate.getYear(), endLocalDate.getMonthValue());

            // 获取 Manager Flash 数据
            List<HotelKpiManagerFlash> managerFlashList = listManagerFlash(propertyId, startYearMonth, endYearMonth);

            // 获取 Budget Database 数据
            List<HotelKpiBudgetDatabaseDTO> budgetList = budgetDatabaseService.listByYearRange(propertyId,
                    startLocalDate.getYear(), endLocalDate.getYear());

            // 获取去年的 W-connect - Monthly Report 数据
            long lyStartMonth = YearMonthUtils.parseYearMonthToLong(startYearMonth.minusYears(1));
            long lyEndMonth = YearMonthUtils.parseYearMonthToLong(endYearMonth.minusYears(1));
            List<HotelKpiWconnectMonthlyReport> monthlyReportList = monthlyReportService.listByDateRange(propertyId,
                    HotelKpiMonthlyReportCons.ROOM_REVENUE, lyStartMonth, lyEndMonth);

            // 获取原始KPI数据
            long startMonth = YearMonthUtils.parseYearMonthToLong(startYearMonth);
            long endMonth = YearMonthUtils.parseYearMonthToLong(endYearMonth);
            Map<Long, String> date2OriginDetail = monthlyService.listByDateRangeAndMetricsType(propertyId,
                    startMonth, endMonth, METRICS_TYPE).stream().collect(Collectors.toMap(HotelKpiMonthly::getMonth, HotelKpiMonthly::getDetail));

            List<HotelKpiMonthly> saveList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonth; !indexYearMonth.isAfter(endYearMonth); indexYearMonth = indexYearMonth.plusMonths(1)) {
                long indexMonth = YearMonthUtils.parseYearMonthToLong(indexYearMonth);

                String originDetail = date2OriginDetail.getOrDefault(indexMonth, null);
                List<HotelKpiV2OriginDetailDTO> originDetailDTOList = hotelKpiService.convert2V2OriginDetailDTOList(originDetail, dateType, factorConfigStr);

                // 截取 Manager Flash 数据
                long indexEndOfMonthDate = Long.parseLong(DateTimeUtils.localDateToStr(indexYearMonth.atEndOfMonth(), BaseConstant.DATE_FORMAT_YYYYMMDD));
                Map<String, HotelKpiManagerFlash> category2ManagerFlash = managerFlashList.stream()
                        .filter(item -> item.getDate().equals(indexEndOfMonthDate))
                        .collect(Collectors.toMap(HotelKpiManagerFlash::getCategory, Function.identity()));
                BigDecimal totalActualValue = category2ManagerFlash.getOrDefault(HotelKpiManagerFlashCons.ROOM_REVENUE, new HotelKpiManagerFlash()).getMtd();
                // 设置 total.actual
                setMetricsValue(originDetailDTOList, totalActualValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.ACTUAL, roundingMode);

                // 截取 Budget Database 数据
                int year = indexYearMonth.getYear();
                HotelKpiBudgetDatabaseDTO budgetDTO = budgetList.stream().filter(item -> item.getYear() == year)
                        .findFirst().orElse(new HotelKpiBudgetDatabaseDTO());
                // 设置 total.budget
                BigDecimal totalBudgetValue = budgetDatabaseService.getRoomRevenueBudget(budgetDTO, indexMonth, indexMonth, null);
                setMetricsValue(originDetailDTOList, totalBudgetValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.BUDGET, roundingMode);

                // 截取去年同月的 W-connect - Monthly Report 数据
                long lyMonth = YearMonthUtils.parseYearMonthToLong(indexYearMonth.minusYears(1));
                Map<String, HotelKpiWconnectMonthlyReport> filedName2MonthlyReport = monthlyReportList.stream()
                        .filter(item -> item.getMonth().equals(lyMonth))
                        .collect(Collectors.toMap(HotelKpiWconnectMonthlyReport::getName, Function.identity()));
                // 设置 total.ly
                BigDecimal totalLyValue = filedName2MonthlyReport.getOrDefault(fieldConfigDTO.getRoomRevenue(),
                        new HotelKpiWconnectMonthlyReport()).getActual();
                setMetricsValue(originDetailDTOList, totalLyValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.LY, roundingMode);

                String detail = JsonUtils.convertObjListToString(objectMapper, originDetailDTOList);

                saveList.add(HotelKpiMonthly.builder()
                        .propertyId(propertyId)
                        .month(indexMonth)
                        .metricsType(METRICS_TYPE)
                        .detail(detail)
                        .build());
            }
            monthlyService.insertOrUpdateBatch(saveList);
        }

        /**
         * 设置数据 for Quarterly
         *
         * @param propertyDTO    资产
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @param metricsConfig  指标配置
         * @author zc
         * @date 2025/9/11
         */
        private void setQuarterly(PropertyDTO propertyDTO, LocalDate startLocalDate, LocalDate endLocalDate, HotelKpiMetricsConfig metricsConfig) {
            int dateType = HotelKpiDateTypeCons.QUARTERLY;
            int propertyId = propertyDTO.getId();
            String factorConfigStr = metricsConfig.getCalculationFactorConfig();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            YearMonth startYearMonthOfQuarter = YearMonth.of(startLocalDate.getYear(), startLocalDate.getMonth().firstMonthOfQuarter());
            YearMonth endYearMonthOfQuarter = YearMonth.of(endLocalDate.getYear(), endLocalDate.getMonth().firstMonthOfQuarter());

            // 获取 Monthly KPI 数据
            long startMonth = YearMonthUtils.parseYearMonthToLong(startYearMonthOfQuarter);
            long endMonth = YearMonthUtils.parseYearMonthToLong(startYearMonthOfQuarter.plusMonths(2));
            List<HotelKpiMonthly> monthlyList = monthlyService.listByDateRangeAndMetricsType(propertyId, startMonth, endMonth, METRICS_TYPE);

            // 获取原始KPI数据
            long startQuarterDate = YearMonthUtils.parseYearMonthToLong(startYearMonthOfQuarter);
            long endQuarterDate = YearMonthUtils.parseYearMonthToLong(endYearMonthOfQuarter);
            Map<Long, String> month2OriginDetail = quarterlyService.listByDateRangeAndMetricsType(propertyId,
                            startQuarterDate, endQuarterDate, METRICS_TYPE).stream()
                    .collect(Collectors.toMap(HotelKpiQuarterly::getMonth, HotelKpiQuarterly::getDetail));

            List<HotelKpiQuarterly> saveList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonthOfQuarter; !indexYearMonth.isAfter(endYearMonthOfQuarter); indexYearMonth = indexYearMonth.plusMonths(3)) {
                long indexStartMonth = YearMonthUtils.parseYearMonthToLong(indexYearMonth);

                String originDetail = month2OriginDetail.get(indexStartMonth);
                List<HotelKpiV2OriginDetailDTO> originDetailDTOList = hotelKpiService.convert2V2OriginDetailDTOList(originDetail, dateType, factorConfigStr);

                // 截取数据
                long indexEndMonth = YearMonthUtils.parseYearMonthToLong(indexYearMonth.plusMonths(2));
                List<HotelKpiMonthly> effectiveMonthlyList = monthlyList.stream().filter(metricsKpi ->
                        metricsKpi.getMonth() >= startMonth && metricsKpi.getMonth() <= indexEndMonth).toList();

                BigDecimal totalActualValue = effectiveMonthlyList.stream().map(item ->
                                hotelKpiService.getEffectiveValueForKpiDetailV2(item.getDetail(),
                                        HotelKpiValueCategoryCons.CATEGORY_1, HotelKpiValueCategoryCons.ACTUAL))
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                // 设置 total.actual
                setMetricsValue(originDetailDTOList, totalActualValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.ACTUAL, roundingMode);

                BigDecimal totalBudgetValue = effectiveMonthlyList.stream().map(item ->
                                hotelKpiService.getEffectiveValueForKpiDetailV2(item.getDetail(),
                                        HotelKpiValueCategoryCons.CATEGORY_1, HotelKpiValueCategoryCons.BUDGET))
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                // 设置 total.budget
                setMetricsValue(originDetailDTOList, totalBudgetValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.BUDGET, roundingMode);

                BigDecimal totalLyValue = effectiveMonthlyList.stream().map(item ->
                                hotelKpiService.getEffectiveValueForKpiDetailV2(item.getDetail(),
                                        HotelKpiValueCategoryCons.CATEGORY_1, HotelKpiValueCategoryCons.LY))
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                // 设置 total.ly
                setMetricsValue(originDetailDTOList, totalLyValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.LY, roundingMode);

                String detail = JsonUtils.convertObjListToString(objectMapper, originDetailDTOList);

                saveList.add(HotelKpiQuarterly.builder()
                        .propertyId(propertyId)
                        .month(indexStartMonth)
                        .metricsType(METRICS_TYPE)
                        .detail(detail)
                        .build());
            }
            quarterlyService.insertOrUpdateBatch(saveList);
        }

        /**
         * 设置数据 for Yearly
         *
         * @param propertyDTO    资产
         * @param startLocalDate 起始日期
         * @param endLocalDate   结束日期
         * @param metricsConfig  指标配置
         * @author zc
         * @date 2025/9/11
         */
        private void setYearly(PropertyDTO propertyDTO, LocalDate startLocalDate, LocalDate endLocalDate, HotelKpiMetricsConfig metricsConfig) {
            int dateType = HotelKpiDateTypeCons.YEARLY;
            int propertyId = propertyDTO.getId();
            String factorConfigStr = metricsConfig.getCalculationFactorConfig();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());
            // 获取上个月的日期
            YearMonth lastMonthYearMonth = YearMonth.now(ZoneId.of(propertyDTO.getTimezone())).minusMonths(1);

            int startYear = startLocalDate.getYear();
            int endYear = endLocalDate.getYear();

            YearMonth startYearMonth = YearMonth.of(startYear, 1);
            YearMonth endYearMonth = YearMonth.of(endYear, 12);

            // 获取 Monthly KPI 数据
            long startMonth = YearMonthUtils.parseYearMonthToLong(startYearMonth);
            long endMonth = YearMonthUtils.parseYearMonthToLong(endYearMonth);
            List<HotelKpiMonthly> monthlyList = monthlyService.listByDateRangeAndMetricsType(propertyId, startMonth, endMonth, METRICS_TYPE);

            // 获取原始KPI数据
            Map<Long, String> year2OriginDetail = yearlyService.listByDateRangeAndMetricsType(propertyId,
                            startYear, endYear, METRICS_TYPE).stream()
                    .collect(Collectors.toMap(HotelKpiYearly::getYear, HotelKpiYearly::getDetail));

            List<HotelKpiYearly> saveList = Lists.newArrayList();
            for (int indexYear = startYear; indexYear <= endYear; indexYear++) {
                // 若年份大于当前上个月的年份，则跳过
                if (indexYear > lastMonthYearMonth.getYear()) {
                    continue;
                }
                String originDetail = year2OriginDetail.get((long) indexYear);
                List<HotelKpiV2OriginDetailDTO> originDetailDTOList = hotelKpiService.convert2V2OriginDetailDTOList(originDetail, dateType, factorConfigStr);

                int asOfMonth = indexYear < lastMonthYearMonth.getYear() ? 12 : lastMonthYearMonth.getMonthValue();
                long indexStartMonth = YearMonthUtils.parseYearMonthToLong(YearMonth.of(indexYear, 1));
                long indexEndMonth = YearMonthUtils.parseYearMonthToLong(YearMonth.of(indexYear, asOfMonth));

                // 截取数据
                List<HotelKpiMonthly> effectiveMonthlyList = monthlyList.stream().filter(metricsKpi ->
                        metricsKpi.getMonth() >= indexStartMonth && metricsKpi.getMonth() <= indexEndMonth).toList();

                BigDecimal totalActualValue = effectiveMonthlyList.stream().map(item ->
                                hotelKpiService.getEffectiveValueForKpiDetailV2(item.getDetail(),
                                        HotelKpiValueCategoryCons.CATEGORY_1, HotelKpiValueCategoryCons.ACTUAL))
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                // 设置 total.actual
                setMetricsValue(originDetailDTOList, totalActualValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.ACTUAL, roundingMode);

                BigDecimal totalBudgetValue = effectiveMonthlyList.stream().map(item ->
                                hotelKpiService.getEffectiveValueForKpiDetailV2(item.getDetail(),
                                        HotelKpiValueCategoryCons.CATEGORY_1, HotelKpiValueCategoryCons.BUDGET))
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                // 设置 total.budget
                setMetricsValue(originDetailDTOList, totalBudgetValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.BUDGET, roundingMode);

                BigDecimal totalLyValue = effectiveMonthlyList.stream().map(item ->
                                hotelKpiService.getEffectiveValueForKpiDetailV2(item.getDetail(),
                                        HotelKpiValueCategoryCons.CATEGORY_1, HotelKpiValueCategoryCons.LY))
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
                // 设置 total.ly
                setMetricsValue(originDetailDTOList, totalLyValue, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.LY, roundingMode);

                String detail = JsonUtils.convertObjListToString(objectMapper, originDetailDTOList);

                saveList.add(HotelKpiYearly.builder()
                        .propertyId(propertyId)
                        .year((long) indexYear)
                        .metricsType(METRICS_TYPE)
                        .detail(detail)
                        .build());
            }
            yearlyService.insertOrUpdateBatch(saveList);
        }

        /**
         * 根据日期范围获取 Manager Flash 数据
         *
         * @param propertyId     资产 id
         * @param startYearMonth 开始日期
         * @param endYearMonth   结束日期
         * @return HotelKpiManagerFlash 集合
         * @author zc
         * @date 2025/8/29
         */
        private List<HotelKpiManagerFlash> listManagerFlash(int propertyId, YearMonth startYearMonth, YearMonth endYearMonth) {
            return managerFlashService.listMonthEnd(propertyId, startYearMonth, endYearMonth,
                    List.of(HotelKpiManagerFlashCons.ROOM_REVENUE));
        }

        /**
         * 获取计算结果 for Monthly Revenue
         *
         * @param dataList 报告数据
         * @param category 分类
         * @return java.math.BigDecimal
         * @author zc
         * @date 2025/8/25
         */
        private BigDecimal calculateForMonthlyRevenue(List<HotelKpiWconnectMonthlyRevenue> dataList, int category) {
            if (category == HotelKpiValueCategoryCons.CATEGORY_1) {
                return dataList.stream().map(HotelKpiWconnectMonthlyRevenue::getRoomRevenue)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
            } else if (category == HotelKpiValueCategoryCons.CATEGORY_2) {
                return dataList.stream().map(HotelKpiWconnectMonthlyRevenue::getTransientRevenue)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
            } else if (category == HotelKpiValueCategoryCons.CATEGORY_3) {
                return dataList.stream().map(HotelKpiWconnectMonthlyRevenue::getGroupRevenue)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
            } else {
                return dataList.stream().map(HotelKpiWconnectMonthlyRevenue::getContractRevenue)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
            }
        }

        /**
         * 设置指标值
         *
         * @param originDetailDTOList 原始数值
         * @param value               指标值
         * @param category            指标类别(1:Total 2:Transient 3:Group)
         * @param titleIndex          标题坐标(1:Actual 2:Budget 3:LY)
         * @param roundingMode        舍入模式
         * @author zc
         * @date 2025/8/25
         */
        private void setMetricsValue(List<HotelKpiV2OriginDetailDTO> originDetailDTOList, BigDecimal value, int category,
                                     int titleIndex, RoundingMode roundingMode) {
            if (Objects.nonNull(value)) {
                value = value.setScale(BaseConstant.DATA_PRECISION, roundingMode);
            }
            for (HotelKpiV2OriginDetailDTO originDetailDTO : originDetailDTOList) {
                if (Objects.equals(originDetailDTO.getCategory(), category)) {
                    // 组装新的计算因子 1
                    HotelKpiOriginCalculationFactorDTO factorIndex1 = new HotelKpiOriginCalculationFactorDTO()
                            .setIndex(1).setCrawl(value)
                            .setCrawlOrigin(List.of(HotelKpiOriginCrawlValueIndexDTO.builder().index(1).value(value).build()));

                    if (HotelKpiValueCategoryCons.ACTUAL == titleIndex) {
                        hotelKpiService.replaceOriginFactorList(originDetailDTO.getActual().getCalculationFactor(), factorIndex1);
                    } else if (HotelKpiValueCategoryCons.BUDGET == titleIndex) {
                        hotelKpiService.replaceOriginFactorList(originDetailDTO.getBudget().getCalculationFactor(), factorIndex1);
                    } else {
                        hotelKpiService.replaceOriginFactorList(originDetailDTO.getLy().getCalculationFactor(), factorIndex1);
                    }
                    return;
                }
            }
        }
    }