# HotelKpiQuoreServiceImpl.java

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

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.workperformance.entity.hotelkpi.*;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.internal.crawl.value.v1.HotelKpiCrawlDailyDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.internal.crawl.value.v1.HotelKpiCrawlDailyValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginCalculationFactorDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginCrawlValueIndexDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.originkpi.HotelKpiOriginDetailDTO;
    import cloud.wconnect.workperformance.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.collections4.CollectionUtils;
    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.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 Quore 服务类
     * </p>
     *
     * @author zc
     * @since 2025-03-24
     */
    @Service
    @DS("work_performance")
    public class HotelKpiQuoreServiceImpl implements IHotelKpiQuoreService {
        @Resource
        private IHotelKpiService hotelKpiService;
        @Resource
        private IHotelKpiDailyService dailyService;
        @Resource
        private IHotelKpiWeeklyService weeklyService;
        @Resource
        private IHotelKpiMonthlyService monthlyService;
        @Resource
        private IHotelKpiQuarterlyService quarterlyService;
        @Resource
        private IHotelKpiYearlyService yearlyService;
        @Resource
        private IHotelKpiMetricsConfigService metricsConfigService;
        @Resource
        private IHotelKpiWeekStartDayService weekStartDayService;
        @Resource
        private ObjectMapper objectMapper;

        /**
         * 处理采集的数据-每日
         *
         * @param propertyDTO 资产
         * @param dailyDTO    采集数据DTO
         * @author zc
         * @date 2025/3/21
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleCrawlValueDaily(PropertyDTO propertyDTO, HotelKpiCrawlDailyDTO dailyDTO) {
            // 获取指标配置
            List<HotelKpiMetricsConfig> metricsConfigList = metricsConfigService.listMetricsConfigByTypesForUpdate(propertyDTO.getId(),
                    Lists.newArrayList(dailyDTO.getMetricsType()));
            if (CollectionUtils.isEmpty(metricsConfigList)) {
                return;
            }

            List<Long> dateList = dailyDTO.getList().stream().map(HotelKpiCrawlDailyValueDTO::getDate).toList();
            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(Collections.min(dateList).toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(Collections.max(dateList).toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            DayOfWeek dayOfWeek = weekStartDayService.getWeekStartDay(propertyDTO.getId());
            // 获取全年数据
            List<Integer> metricsTypeList = metricsConfigList.stream().map(HotelKpiMetricsConfig::getMetricsType).toList();
            List<HotelKpiDaily> dailyList = listAllYearByDateRange(propertyDTO.getId(), metricsTypeList, startLocalDate, endLocalDate, dayOfWeek);

            saveWeeklyList(propertyDTO, metricsConfigList, dailyList, startLocalDate, endLocalDate, dayOfWeek);
            saveMonthlyList(propertyDTO, metricsConfigList, dailyList, startLocalDate, endLocalDate);
            saveQuarterlyList(propertyDTO, metricsConfigList, dailyList, startLocalDate, endLocalDate);
            saveYearlyList(propertyDTO, metricsConfigList, dailyList, startLocalDate, endLocalDate);
        }

        /**
         * 获取全年数据
         *
         * @param propertyId      资产
         * @param metricsTypeList 指标类型集合
         * @param startLocalDate  开始日期
         * @param endLocalDate    结束日期
         * @param dayOfWeek       周起始日
         * @return HotelKpiDaily 集合
         * @author zc
         * @date 2025/3/22
         */
        private List<HotelKpiDaily> listAllYearByDateRange(int propertyId, List<Integer> metricsTypeList, LocalDate startLocalDate,
                                                           LocalDate endLocalDate, DayOfWeek dayOfWeek) {
            // 多获取一部分数据，方便计算周数值
            long startDate = Long.parseLong(DateTimeUtils.localDateToStr(startLocalDate.withDayOfYear(1)
                    .with(TemporalAdjusters.previousOrSame(dayOfWeek)), BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDate = Long.parseLong(DateTimeUtils.localDateToStr(endLocalDate.with(TemporalAdjusters.lastDayOfYear())
                    .with(TemporalAdjusters.previousOrSame(dayOfWeek)).plusDays(6), BaseConstant.DATE_FORMAT_YYYYMMDD));
            return dailyService.listByDateRange(propertyId, metricsTypeList, startDate, endDate);
        }

        /**
         * 保存周数据
         *
         * @param propertyDTO       资产
         * @param metricsConfigList 指标配置
         * @param dailyList         指标每日数据
         * @param startLocalDate    开始日期
         * @param endLocalDate      结束日期
         * @param dayOfWeek         周起始日
         * @author zc
         * @date 2025/3/24
         */
        private void saveWeeklyList(PropertyDTO propertyDTO, List<HotelKpiMetricsConfig> metricsConfigList,
                                    List<HotelKpiDaily> dailyList, LocalDate startLocalDate, LocalDate endLocalDate, DayOfWeek dayOfWeek) {
            // 转换为周的开始日期
            LocalDate startWeekLocalDate = startLocalDate.with(TemporalAdjusters.previousOrSame(dayOfWeek));
            long startWeekDate = Long.parseLong(DateTimeUtils.localDateToStr(startWeekLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            LocalDate endWeekLocalDate = endLocalDate.with(TemporalAdjusters.previousOrSame(dayOfWeek));
            long endWeekDate = Long.parseLong(DateTimeUtils.localDateToStr(endWeekLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));

            List<Integer> metricsTypeList = metricsConfigList.stream().map(HotelKpiMetricsConfig::getMetricsType).toList();
            // 根据指定指标类型，获取 DB 中已存在的周数据
            Map<Long, Map<Integer, HotelKpiWeekly>> metricsType2KpiByDate = weeklyService.listByDateRangeAndMetricsTypes(propertyDTO.getId(),
                            startWeekDate, endWeekDate, metricsTypeList).stream()
                    .collect(Collectors.groupingBy(HotelKpiWeekly::getWeek,
                            Collectors.toMap(HotelKpiWeekly::getMetricsType, Function.identity())));

            List<HotelKpiWeekly> saveWeeklyList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startWeekLocalDate; !indexLocalDate.isAfter(endWeekLocalDate); indexLocalDate = indexLocalDate.plusDays(7)) {
                long startDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate.withDayOfMonth(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
                LocalDate endIndexLocalDate = indexLocalDate.plusDays(6);
                // MTD 取值规则，若跨月，则取当前月的最后一天
                if (endIndexLocalDate.getMonthValue() != indexLocalDate.getMonthValue()) {
                    endIndexLocalDate = indexLocalDate.with(TemporalAdjusters.lastDayOfMonth());
                }
                long endDate = Long.parseLong(DateTimeUtils.localDateToStr(endIndexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                // 截取指定日期范围的数据
                List<HotelKpiDaily> indexDailyList = dailyList.stream().filter(item ->
                        item.getDate() >= startDate && item.getDate() <= endDate).toList();

                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));

                Map<Integer, HotelKpiWeekly> metricsType2KpiWeekly = metricsType2KpiByDate.getOrDefault(indexDate, Collections.emptyMap());
                for (HotelKpiMetricsConfig metricsConfig : metricsConfigList) {
                    HotelKpiWeekly originKpiWeekly = metricsType2KpiWeekly.getOrDefault(metricsConfig.getMetricsType(), new HotelKpiWeekly());

                    saveWeeklyList.add(HotelKpiWeekly.builder()
                            .propertyId(propertyDTO.getId())
                            .week(indexDate)
                            .metricsType(metricsConfig.getMetricsType())
                            .detail(packageOriginDetail(propertyDTO, indexDailyList, originKpiWeekly.getDetail()))
                            .build());
                }
            }
            weeklyService.insertOrUpdateBatch(saveWeeklyList);
        }

        /**
         * 保存 Monthly 指标数据
         *
         * @param propertyDTO       资产
         * @param metricsConfigList 指标配置集合
         * @param dailyList         指标每日数据
         * @param startLocalDate    开始日期
         * @param endLocalDate      结束日期
         * @author zc
         * @date 2025/3/24
         */
        private void saveMonthlyList(PropertyDTO propertyDTO, List<HotelKpiMetricsConfig> metricsConfigList,
                                     List<HotelKpiDaily> dailyList, LocalDate startLocalDate, LocalDate endLocalDate) {
            YearMonth startYearMonth = YearMonth.of(startLocalDate.getYear(), startLocalDate.getMonthValue());
            long startMonthDate = YearMonthUtils.parseYearMonthToLong(startYearMonth);
            YearMonth endYearMonth = YearMonth.of(endLocalDate.getYear(), endLocalDate.getMonthValue());
            long endMonthDate = YearMonthUtils.parseYearMonthToLong(endYearMonth);

            List<Integer> metricsTypeList = metricsConfigList.stream().map(HotelKpiMetricsConfig::getMetricsType).toList();
            // 根据指定指标类型，获取 DB 中已存在的月数据
            Map<Long, Map<Integer, HotelKpiMonthly>> metricsType2KpiByDate = monthlyService.listByDateRangeAndMetricsTypes(propertyDTO.getId(),
                            startMonthDate, endMonthDate, metricsTypeList).stream()
                    .collect(Collectors.groupingBy(HotelKpiMonthly::getMonth,
                            Collectors.toMap(HotelKpiMonthly::getMetricsType, Function.identity())));

            List<HotelKpiMonthly> saveMonthlyList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonth; !indexYearMonth.isAfter(endYearMonth); indexYearMonth = indexYearMonth.plusMonths(1)) {
                long startDate = Long.parseLong(DateTimeUtils.localDateToStr(indexYearMonth.atDay(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
                long endDate = Long.parseLong(DateTimeUtils.localDateToStr(indexYearMonth.atEndOfMonth(), BaseConstant.DATE_FORMAT_YYYYMMDD));
                // 截取指定日期范围的数据
                List<HotelKpiDaily> indexDailyList = dailyList.stream().filter(item ->
                        item.getDate() >= startDate && item.getDate() <= endDate).toList();

                long indexDate = YearMonthUtils.parseYearMonthToLong(indexYearMonth);

                Map<Integer, HotelKpiMonthly> metricsType2KpiMonthly = metricsType2KpiByDate.getOrDefault(indexDate, Collections.emptyMap());
                for (HotelKpiMetricsConfig metricsConfig : metricsConfigList) {
                    HotelKpiMonthly originKpiMonthly = metricsType2KpiMonthly.getOrDefault(metricsConfig.getMetricsType(), new HotelKpiMonthly());

                    saveMonthlyList.add(HotelKpiMonthly.builder()
                            .propertyId(propertyDTO.getId())
                            .month(indexDate)
                            .metricsType(metricsConfig.getMetricsType())
                            .detail(packageOriginDetail(propertyDTO, indexDailyList, originKpiMonthly.getDetail()))
                            .build());
                }
            }
            monthlyService.insertOrUpdateBatch(saveMonthlyList);
        }

        /**
         * 保存 Quarterly 指标数据
         *
         * @param propertyDTO       资产
         * @param metricsConfigList 指标配置集合
         * @param dailyList         指标每日数据
         * @param startLocalDate    开始日期
         * @param endLocalDate      结束日期
         * @author zc
         * @date 2025/3/24
         */
        private void saveQuarterlyList(PropertyDTO propertyDTO, List<HotelKpiMetricsConfig> metricsConfigList,
                                       List<HotelKpiDaily> dailyList, LocalDate startLocalDate, LocalDate endLocalDate) {
            YearMonth startYearMonth = YearMonth.of(startLocalDate.getYear(), startLocalDate.getMonth().firstMonthOfQuarter().getValue());
            long startQuarterDate = YearMonthUtils.parseYearMonthToLong(startYearMonth);
            YearMonth endYearMonth = YearMonth.of(endLocalDate.getYear(), endLocalDate.getMonth().firstMonthOfQuarter().getValue());
            long endQuarterDate = YearMonthUtils.parseYearMonthToLong(endYearMonth);

            List<Integer> metricsTypeList = metricsConfigList.stream().map(HotelKpiMetricsConfig::getMetricsType).toList();
            // 根据指定指标类型，获取 DB 中已存在的季数据
            Map<Long, Map<Integer, HotelKpiQuarterly>> metricsType2KpiByDate = quarterlyService.listByDateRangeAndMetricsTypes(propertyDTO.getId(),
                            startQuarterDate, endQuarterDate, metricsTypeList).stream()
                    .collect(Collectors.groupingBy(HotelKpiQuarterly::getMonth,
                            Collectors.toMap(HotelKpiQuarterly::getMetricsType, Function.identity())));

            List<HotelKpiQuarterly> saveQuarterlyList = Lists.newArrayList();
            for (YearMonth indexYearMonth = startYearMonth; !indexYearMonth.isAfter(endYearMonth); indexYearMonth = indexYearMonth.plusMonths(3)) {
                long startDate = Long.parseLong(DateTimeUtils.localDateToStr(indexYearMonth.atDay(1), BaseConstant.DATE_FORMAT_YYYYMMDD));
                long endDate = Long.parseLong(DateTimeUtils.localDateToStr(indexYearMonth.plusMonths(2).atEndOfMonth(), BaseConstant.DATE_FORMAT_YYYYMMDD));

                // 截取指定日期范围的数据
                List<HotelKpiDaily> indexDailyList = dailyList.stream().filter(item ->
                        item.getDate() >= startDate && item.getDate() <= endDate).toList();

                long indexDate = YearMonthUtils.parseYearMonthToLong(indexYearMonth);

                Map<Integer, HotelKpiQuarterly> metricsType2KpiQuarterly = metricsType2KpiByDate.getOrDefault(indexDate, Collections.emptyMap());
                for (HotelKpiMetricsConfig metricsConfig : metricsConfigList) {
                    HotelKpiQuarterly originKpiQuarterly = metricsType2KpiQuarterly.getOrDefault(metricsConfig.getMetricsType(), new HotelKpiQuarterly());

                    saveQuarterlyList.add(HotelKpiQuarterly.builder()
                            .propertyId(propertyDTO.getId())
                            .month(indexDate)
                            .metricsType(metricsConfig.getMetricsType())
                            .detail(packageOriginDetail(propertyDTO, indexDailyList, originKpiQuarterly.getDetail()))
                            .build());
                }
            }
            quarterlyService.insertOrUpdateBatch(saveQuarterlyList);
        }

        /**
         * 保存 Yearly 指标数据
         *
         * @param propertyDTO       资产
         * @param metricsConfigList 指标配置集合
         * @param dailyList         指标每日数据
         * @param startLocalDate    开始日期
         * @param endLocalDate      结束日期
         * @author zc
         * @date 2025/3/24
         */
        private void saveYearlyList(PropertyDTO propertyDTO, List<HotelKpiMetricsConfig> metricsConfigList,
                                    List<HotelKpiDaily> dailyList, LocalDate startLocalDate, LocalDate endLocalDate) {
            // 获取上个月的日期
            YearMonth lastMonthYearMonth = YearMonth.now(ZoneId.of(propertyDTO.getTimezone())).minusMonths(1);

            List<Integer> metricsTypeList = metricsConfigList.stream().map(HotelKpiMetricsConfig::getMetricsType).toList();
            // 根据指定指标类型，获取 DB 中已存在的年数据
            Map<Long, Map<Integer, HotelKpiYearly>> metricsType2KpiByDate = yearlyService.listByDateRangeAndMetricsTypes(propertyDTO.getId(),
                            startLocalDate.getYear(), endLocalDate.getYear(), metricsTypeList).stream()
                    .collect(Collectors.groupingBy(HotelKpiYearly::getYear,
                            Collectors.toMap(HotelKpiYearly::getMetricsType, Function.identity())));

            List<HotelKpiYearly> saveYearlyList = Lists.newArrayList();
            for (int indexYear = startLocalDate.getYear(); indexYear <= endLocalDate.getYear(); indexYear++) {
                // 若年份大于当前上个月的年份，则跳过
                if (indexYear > lastMonthYearMonth.getYear()) {
                    continue;
                }
                long startDate = Long.parseLong(DateTimeUtils.localDateToStr(YearMonth.of(indexYear, 1)
                        .atDay(1), BaseConstant.DATE_FORMAT_YYYYMMDD));

                int asOfMonth = indexYear < lastMonthYearMonth.getYear() ? 12 : lastMonthYearMonth.getMonthValue();
                long endDate = Long.parseLong(DateTimeUtils.localDateToStr(YearMonth.of(indexYear, asOfMonth)
                        .atEndOfMonth(), BaseConstant.DATE_FORMAT_YYYYMMDD));

                // 截取指定日期范围的数据
                List<HotelKpiDaily> indexDailyList = dailyList.stream().filter(item ->
                        item.getDate() >= startDate && item.getDate() <= endDate).toList();

                Map<Integer, HotelKpiYearly> metricsType2KpiYearly = metricsType2KpiByDate.getOrDefault((long) indexYear, Collections.emptyMap());
                for (HotelKpiMetricsConfig metricsConfig : metricsConfigList) {
                    HotelKpiYearly originKpiYearly = metricsType2KpiYearly.getOrDefault(metricsConfig.getMetricsType(), new HotelKpiYearly());

                    saveYearlyList.add(HotelKpiYearly.builder()
                            .propertyId(propertyDTO.getId())
                            .year((long) indexYear)
                            .metricsType(metricsConfig.getMetricsType())
                            .detail(packageOriginDetail(propertyDTO, indexDailyList, originKpiYearly.getDetail()))
                            .build());
                }
            }
            yearlyService.insertOrUpdateBatch(saveYearlyList);
        }

        /**
         * 组装原始指标数据DTO
         *
         * @param propertyDTO  资产
         * @param dailyList    指标每日数据
         * @param originDetail 原始数据详情
         * @author zc
         * @date 2025/3/24
         */
        private String packageOriginDetail(PropertyDTO propertyDTO, List<HotelKpiDaily> dailyList, String originDetail) {
            HotelKpiOriginDetailDTO originDetailDTO = Objects.isNull(originDetail) ? new HotelKpiOriginDetailDTO() :
                    JsonUtils.convertStringToObject(objectMapper, originDetail, HotelKpiOriginDetailDTO.class);
            // 解析并设置指标数值
            parseValueForQuore(propertyDTO, dailyList, originDetailDTO);
            return JsonUtils.convertObjToString(objectMapper, originDetailDTO);
        }

        /**
         * 解析并设置指标数值
         *
         * @param propertyDTO     资产
         * @param dailyList       指标每日数据
         * @param originDetailDTO 原始数据详情
         * @author zc
         * @date 2025/3/24
         */
        private void parseValueForQuore(PropertyDTO propertyDTO, List<HotelKpiDaily> dailyList, HotelKpiOriginDetailDTO originDetailDTO) {
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            BigDecimal value = dailyList.stream().map(HotelKpiDaily::getValue).reduce(BigDecimal.ZERO, BigDecimal::add)
                    .setScale(BaseConstant.DATA_PRECISION, roundingMode);
            HotelKpiOriginCalculationFactorDTO newFactorDTO = new HotelKpiOriginCalculationFactorDTO().setIndex(1).setCrawl(value)
                    .setCrawlOrigin(List.of(HotelKpiOriginCrawlValueIndexDTO.builder().index(1).value(value).build()));

            // 查找并替换或新增新的指标计算因子原始数据
            hotelKpiService.replaceOriginFactorList(originDetailDTO.getActual().getCalculationFactor(), newFactorDTO);
        }
    }