# HotelKpiMetricsFbRevenueService.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.workperformance.cons.hotelkpi.*;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiMetricsConfig;
    import cloud.wconnect.workperformance.entity.hotelkpi.HotelKpiWeekly;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.budgetdb.HotelKpiBudgetDatabaseDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.m3.HotelKpiM3DailyDTO;
    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.apache.commons.lang3.StringUtils;
    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.temporal.TemporalAdjusters;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.function.Function;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

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

        @Resource
        private IHotelKpiService hotelKpiService;
        @Resource
        private IHotelKpiWeeklyService weeklyService;
        @Resource
        private IHotelKpiM3DailyService m3DailyService;
        @Resource
        private IHotelKpiWeekStartDayService weekStartDayService;
        @Resource
        private IHotelKpiBudgetDatabaseService budgetDatabaseService;
        @Resource
        private IHotelKpiMetricsConfigService metricsConfigService;
        @Resource
        private ObjectMapper objectMapper;

        private static final int METRICS_TYPE = HotelKpiMetricsEnum.FB_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);
        }

        /**
         * 初始化 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);
        }

        /**
         * 触发设置数据
         *
         * @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.M3_DAILY == triggerType) {
                LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(startDate),
                        BaseConstant.DATE_FORMAT_YYYYMMDD);
                LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(endDate),
                        BaseConstant.DATE_FORMAT_YYYYMMDD);

                setWeekly(propertyDTO, startLocalDate, endLocalDate, 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);
            }
        }

        /**
         * 设置数据
         *
         * @param propertyDTO    属性信息
         * @param startLocalDate 开始日期
         * @param endLocalDate   结束日期
         * @param dateType       日期类型
         * @author zc
         * @date 2025/8/20
         */
        @Override
        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);
            }
        }

        /**
         * 设置数据 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);

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

            // 获取 M3 Daily 数据
            List<HotelKpiM3DailyDTO> m3DailyList = m3DailyService.listByDateRange(propertyId,
                    Long.parseLong(DateTimeUtils.localDateToStr(startWeekLastLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD)),
                    Long.parseLong(DateTimeUtils.localDateToStr(endWeekLastLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD)));

            // 获取原始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.getOrDefault(indexDate, null);
                List<HotelKpiV2OriginDetailDTO> originDetailDTOList = hotelKpiService.convert2V2OriginDetailDTOList(originDetail, dateType, factorConfigStr);

                // 获取周期最后一天的日期
                LocalDate indexWeekLastLocalDate = indexLocalDate.plusDays(6);

                // 截取 Budget Database 数据
                int year = indexLocalDate.getYear();
                HotelKpiBudgetDatabaseDTO budgetDTO = budgetList.stream().filter(item -> item.getYear() == year)
                        .findFirst().orElse(new HotelKpiBudgetDatabaseDTO());
                // 获取周期最后一天所处的年月
                long indexMonth = Long.parseLong(DateTimeUtils.localDateToStr(indexWeekLastLocalDate, BaseConstant.DATE_FORMAT_YYYYMM));
                BigDecimal totalBudgetValue = budgetDatabaseService.getFBRevenueBudget(budgetDTO, indexMonth, indexMonth, null);
                // 设置 total.budget
                setMetricsValue(originDetailDTOList, totalBudgetValue, null, HotelKpiValueCategoryCons.CATEGORY_1,
                        HotelKpiValueCategoryCons.BUDGET, roundingMode);

                // 截取 M3 Daily 数据
                long indexWeekLastDate = Long.parseLong(DateTimeUtils.localDateToStr(indexWeekLastLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                HotelKpiM3DailyDTO m3Daily = m3DailyList.stream().filter(item ->
                        item.getDate() == indexWeekLastDate).findFirst().orElse(new HotelKpiM3DailyDTO());
                setMetricsValue(originDetailDTOList, m3Daily.getFbRevenue().getMtd(), m3Daily.getFileUrl(),
                        HotelKpiValueCategoryCons.CATEGORY_2, HotelKpiValueCategoryCons.ACTUAL, roundingMode);
                setMetricsValue(originDetailDTOList, m3Daily.getFbRevenue().getMtdBudget(), m3Daily.getFileUrl(),
                        HotelKpiValueCategoryCons.CATEGORY_2, HotelKpiValueCategoryCons.BUDGET, roundingMode);
                setMetricsValue(originDetailDTOList, m3Daily.getFbRevenue().getMtdLastYear(), m3Daily.getFileUrl(),
                        HotelKpiValueCategoryCons.CATEGORY_2, HotelKpiValueCategoryCons.LY, roundingMode);

                setMetricsValue(originDetailDTOList, m3Daily.getBanquetRevenue().getMtd(), m3Daily.getFileUrl(),
                        HotelKpiValueCategoryCons.CATEGORY_3, HotelKpiValueCategoryCons.ACTUAL, roundingMode);
                setMetricsValue(originDetailDTOList, m3Daily.getBanquetRevenue().getMtdBudget(), m3Daily.getFileUrl(),
                        HotelKpiValueCategoryCons.CATEGORY_3, HotelKpiValueCategoryCons.BUDGET, roundingMode);
                setMetricsValue(originDetailDTOList, m3Daily.getBanquetRevenue().getMtdLastYear(), m3Daily.getFileUrl(),
                        HotelKpiValueCategoryCons.CATEGORY_3, HotelKpiValueCategoryCons.LY, roundingMode);

                // 设置 total.actual, total.ly
                resetOtherCategory(originDetailDTOList, roundingMode);

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

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

        /**
         * 重新计算其他类别
         *
         * @param detailDTOList 源数据
         * @param roundingMode  舍入模式
         * @author zc
         * @date 2025/8/20
         */
        private void resetOtherCategory(List<HotelKpiV2OriginDetailDTO> detailDTOList, RoundingMode roundingMode) {
            Map<Integer, HotelKpiV2OriginDetailDTO> category2OriginDetailDTO = detailDTOList.stream()
                    .collect(Collectors.toMap(HotelKpiV2OriginDetailDTO::getCategory, Function.identity()));

            HotelKpiV2OriginDetailDTO transientDTO = category2OriginDetailDTO.get(HotelKpiValueCategoryCons.CATEGORY_2);

            HotelKpiOriginCalculationFactorDTO transientActualFactorDTO = transientDTO.getActual().getCalculationFactor().stream()
                    .findFirst().orElse(new HotelKpiOriginCalculationFactorDTO());
            BigDecimal transientActual = hotelKpiService.getEffectiveOriginValue(transientActualFactorDTO);

            HotelKpiV2OriginDetailDTO groupDTO = category2OriginDetailDTO.get(HotelKpiValueCategoryCons.CATEGORY_3);
            HotelKpiOriginCalculationFactorDTO groupActualFactorDTO = groupDTO.getActual().getCalculationFactor().stream()
                    .findFirst().orElse(new HotelKpiOriginCalculationFactorDTO());
            BigDecimal groupActual = hotelKpiService.getEffectiveOriginValue(groupActualFactorDTO);

            // 设置 total.actual
            BigDecimal actualValue = Stream.of(transientActual, groupActual).filter(Objects::nonNull)
                    .reduce(BigDecimal::add).orElse(null);
            setMetricsValue(detailDTOList, actualValue, null, HotelKpiValueCategoryCons.CATEGORY_1,
                    HotelKpiValueCategoryCons.ACTUAL, roundingMode);

            HotelKpiOriginCalculationFactorDTO transientLyFactorDTO = transientDTO.getLy().getCalculationFactor().stream()
                    .findFirst().orElse(new HotelKpiOriginCalculationFactorDTO());
            BigDecimal transientLy = hotelKpiService.getEffectiveOriginValue(transientLyFactorDTO);

            HotelKpiOriginCalculationFactorDTO groupLyFactorDTO = groupDTO.getLy().getCalculationFactor().stream()
                    .findFirst().orElse(new HotelKpiOriginCalculationFactorDTO());
            BigDecimal groupLy = hotelKpiService.getEffectiveOriginValue(groupLyFactorDTO);

            // 设置 total.ly
            BigDecimal lyValue = Stream.of(transientLy, groupLy).filter(Objects::nonNull)
                    .reduce(BigDecimal::add).orElse(null);
            setMetricsValue(detailDTOList, lyValue, null, HotelKpiValueCategoryCons.CATEGORY_1,
                    HotelKpiValueCategoryCons.LY, roundingMode);
        }

        /**
         * 设置指标值
         *
         * @param originDetailDTOList 原始数值
         * @param value               指标值
         * @param fileUrl             报告地址
         * @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, String fileUrl,
                                     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) {
                        if (StringUtils.isNotBlank(fileUrl)) {
                            originDetailDTO.getActual().setFileUrl(fileUrl);
                        }
                        hotelKpiService.replaceOriginFactorList(originDetailDTO.getActual().getCalculationFactor(), factorIndex1);
                    } else if (HotelKpiValueCategoryCons.BUDGET == titleIndex) {
                        if (StringUtils.isNotBlank(fileUrl)) {
                            originDetailDTO.getBudget().setFileUrl(fileUrl);
                        }
                        hotelKpiService.replaceOriginFactorList(originDetailDTO.getBudget().getCalculationFactor(), factorIndex1);
                    } else {
                        if (StringUtils.isNotBlank(fileUrl)) {
                            originDetailDTO.getLy().setFileUrl(fileUrl);
                        }
                        hotelKpiService.replaceOriginFactorList(originDetailDTO.getLy().getCalculationFactor(), factorIndex1);
                    }
                    return;
                }
            }
        }
    }