# HotelKpiWeeklyServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.YearMonthWeekUtils;
    import cloud.wconnect.workperformance.cons.hotelkpi.old.HotelKpiDateTypeCons;
    import cloud.wconnect.workperformance.cons.hotelkpi.old.HotelKpiSettingCons;
    import cloud.wconnect.workperformance.entity.hotelkpi.old.HotelKpiMetricsConfig;
    import cloud.wconnect.workperformance.entity.hotelkpi.old.HotelKpiWarningComment;
    import cloud.wconnect.workperformance.entity.hotelkpi.old.HotelKpiWeekly;
    import cloud.wconnect.workperformance.mapper.hotelkpi.old.HotelKpiWeeklyMapper;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.old.kpi.*;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.old.kpi.explain.HotelKpiPreviousWeeksCalculationFactorDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.old.kpi.explain.HotelKpiPreviousWeeksCalculationFactorValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.old.originkpi.HotelKpiOriginDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.old.originkpi.HotelKpiOriginValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.old.setting.actual.HotelKpiMetricsSettingActualDTO;
    import cloud.wconnect.workperformance.pojo.dto.hotelkpi.old.setting.target.HotelKpiMetricsSettingTargetDTO;
    import cloud.wconnect.workperformance.pojo.query.hotelkpi.old.HotelKpiQuery;
    import cloud.wconnect.workperformance.service.hotelkpi.old.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.stereotype.Service;

    import jakarta.annotation.Resource;
    import java.math.BigDecimal;
    import java.time.LocalDate;
    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 Weekly 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-06-24
     */
    @Service("hotelKpiWeeklyServiceImpl_old")
    @DS("work_performance")
    public class HotelKpiWeeklyServiceImpl extends ServiceImpl<HotelKpiWeeklyMapper, HotelKpiWeekly> implements IHotelKpiWeeklyService {

        @Resource
        private IHotelKpiService hotelKpiService;
        @Resource
        private IHotelKpiMonthlyService monthlyService;
        @Resource
        private IHotelKpiMetricsSettingTargetService metricsSettingTargetService;
        @Resource
        private IHotelKpiMetricsSettingActualService metricsSettingActualService;
        @Resource
        private IHotelKpiMetricsConfigService metricsConfigService;
        @Resource
        private IHotelKpiMetricsSettingService metricsSettingService;
        @Resource
        private IHotelKpiWarningCommentService warningCommentService;
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        private BeanMapper beanMapper;

        /**
         * [for update] 根据日期范围、指标类型集合 获取 KPI 原始数据
         *
         * @param propertyId   资产 id
         * @param startDate    开始日期(yyyyMMdd)
         * @param endDate      结束日期(yyyyMMdd)
         * @param metricsTypes 指标类型集合
         * @return HotelKpiWeekly 集合
         * @author zc
         * @date 2024/6/25
         */
        @Override
        public List<HotelKpiWeekly> listByDateRangeAndMetricsTypes(int propertyId, long startDate, long endDate, List<Integer> metricsTypes) {
            if (CollectionUtils.isEmpty(metricsTypes)) {
                return Collections.emptyList();
            }
            return list(new LambdaQueryWrapper<HotelKpiWeekly>()
                    .between(HotelKpiWeekly::getWeek, startDate, endDate)
                    .in(HotelKpiWeekly::getMetricsType, metricsTypes)
                    .eq(HotelKpiWeekly::getPropertyId, propertyId)
                    .last("for update"));
        }

        /**
         * 获取 Hotel KPI 数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @return Hotel KPI 数据
         * @author zc
         * @date 2024/6/25
         */
        @Override
        public List<HotelKpiDTO> listHotelKpi(PropertyDTO propertyDTO, HotelKpiQuery query) {
            // 获取有效的指标
            List<HotelKpiMetricsConfig> effectiveMetrics = metricsConfigService.listEffectiveMetricsOfYear(propertyDTO.getId(),
                    query.getDateType(), query.getStartDateLong(), query.getEndDateLong(), Boolean.TRUE);
            Map<Integer, List<HotelKpiMetricsConfig>> metricsParentId2Configs = effectiveMetrics.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsConfig::getParentId));

            // 获取年初第一周的开始日期：年初到结束日期的数据用于计算【递减剩余】类型的指标数值
            long firstWeekOfCurrentYear = YearMonthWeekUtils.getFirstWeekOfCurrentYear(query.getStartDateLong());

            // 提取非父级数据的指标配置
            List<Integer> metricsTypes = effectiveMetrics.stream().map(HotelKpiMetricsConfig::getMetricsType)
                    .filter(metricsType -> metricsType > 0).toList();

            // 获取 Weekly KPI 原始数据
            List<HotelKpiMetricsDTO> metricsDTOList = listKpiMetricsDTOList(propertyDTO.getId(), firstWeekOfCurrentYear,
                    query.getEndDateLong(), metricsTypes);

            // 获取 Monthly KPI 原始数据
            List<HotelKpiMetricsDTO> metricsDTOMonthlyList = listKpiMetricsDTOMonthlyList(propertyDTO.getId(), query.getStartDateLong(),
                    query.getEndDateLong(), metricsTypes);

            // 组装 Metrics Setting
            packageSettingValue(propertyDTO.getId(), metricsDTOList, metricsDTOMonthlyList, metricsTypes, firstWeekOfCurrentYear, query.getEndDateLong());

            // 获取操作人姓名
            Map<Integer, String> userId2Name = hotelKpiService.extractOperatorName(propertyDTO.getTenantId(), metricsDTOList);

            Map<Long, Map<Integer, HotelKpiMetricsDTO>> metricsType2KpiMetricsDTOByDate = metricsDTOList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsDTO::getDate,
                            Collectors.toMap(HotelKpiMetricsDTO::getMetricsType, Function.identity())));

            Map<Integer, Map<Long, HotelKpiMetricsDTO>> date2MetricsDTOByMetricsType = metricsDTOList.stream()
                    .collect(Collectors.groupingBy(HotelKpiMetricsDTO::getMetricsType,
                            Collectors.toMap(HotelKpiMetricsDTO::getDate, Function.identity())));

            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(query.getStartDateLong()), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(query.getEndDateLong()), BaseConstant.DATE_FORMAT_YYYYMMDD);

            List<HotelKpiDTO> hotelKpiDTOList = Lists.newArrayList();

            // 获取指定日期警示备注数量
            Map<Long, Map<Integer, Long>> metricsType2WarningCountByDate = warningCommentService.listByDateRangeAndMetricsTypes(propertyDTO.getId(),
                            query.getDateType(), query.getStartDateLong(), query.getEndDateLong(), metricsTypes).stream()
                    .collect(Collectors.groupingBy(HotelKpiWarningComment::getDate,
                            Collectors.groupingBy(HotelKpiWarningComment::getMetricsType, Collectors.counting())));

            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = (indexLocalDate.getDayOfMonth() >= 22) ?
                    indexLocalDate.plusMonths(1).withDayOfMonth(1) : indexLocalDate.plusDays(7)) {
                // 获取当前周期的 KPI 原始数据
                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                Map<Integer, HotelKpiMetricsDTO> metricsType2KpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(indexDate, Collections.emptyMap());

                // 获取上一周期的 KPI 原始数据
                long lastIndexDate = YearMonthWeekUtils.getLastWeekDate(indexDate);
                Map<Integer, HotelKpiMetricsDTO> metricsType2LastKpiMetricsDTO = metricsType2KpiMetricsDTOByDate.getOrDefault(lastIndexDate, Collections.emptyMap());

                Map<Integer, Long> metricsType2WarningCount = metricsType2WarningCountByDate.getOrDefault(indexDate, Collections.emptyMap());

                // 生成 KPI 详情数据
                List<HotelKpiDetailDTO> kpiDetailDTOList = hotelKpiService.generateKpiDetailDTOList(propertyDTO, userId2Name, effectiveMetrics,
                        metricsParentId2Configs, metricsType2KpiMetricsDTO, metricsType2LastKpiMetricsDTO, metricsType2WarningCount,
                        indexDate, lastIndexDate, HotelKpiDateTypeCons.WEEKLY);

                // 组装当前周期的计算因子详情数据
                packagePreviousWeeksCalculationFactor(kpiDetailDTOList, indexLocalDate, date2MetricsDTOByMetricsType);

                hotelKpiDTOList.add(HotelKpiDTO.builder()
                        .date(indexDate)
                        .details(kpiDetailDTOList).build());
            }
            return hotelKpiDTOList;
        }

        /**
         * 组装当前周期的计算因子详情数据
         *
         * @param kpiDetailDTOList             待组装的 KPI 详情数据
         * @param indexLocalDate               日期
         * @param date2MetricsDTOByMetricsType 历史 KPI 数据
         * @author zc
         * @date 2024/12/23
         */
        private void packagePreviousWeeksCalculationFactor(List<HotelKpiDetailDTO> kpiDetailDTOList, LocalDate indexLocalDate,
                                                           Map<Integer, Map<Long, HotelKpiMetricsDTO>> date2MetricsDTOByMetricsType) {
            for (HotelKpiDetailDTO kpiDetailDTO : kpiDetailDTOList) {
                for (HotelKpiItemDTO item : kpiDetailDTO.getItems()) {
                    for (HotelKpiMetricsDTO metric : item.getMetrics()) {
                        List<HotelKpiCalculationFactorDTO> actualFactors = metric.getActual().getCalculationFactor();
                        // 若计算因子数量不为 2，则跳过该指标
                        if (actualFactors.size() != 2) {
                            continue;
                        }
                        HotelKpiCalculationFactorDTO actualFactorIndex2DTO = actualFactors.stream()
                                .filter(factor -> factor.getIndex() == 2).findFirst().orElse(null);
                        if (Objects.isNull(actualFactorIndex2DTO)) {
                            continue;
                        }
                        // 若计算因子除数不是递减剩余，则跳过该指标
                        if (!Objects.equals(actualFactorIndex2DTO.getSettingCategory(), HotelKpiSettingCons.DIMINISHING_SURPLUS)) {
                            continue;
                        }
                        Map<Long, HotelKpiMetricsDTO> date2MetricsDTO = date2MetricsDTOByMetricsType.get(metric.getMetricsType());
                        List<HotelKpiPreviousWeeksCalculationFactorDTO> calculationFactorDetailList =
                                generateCalculationFactorValue(indexLocalDate, date2MetricsDTO);
                        metric.getExplain().getActual().setCalculationFactorDetail(calculationFactorDetailList);
                    }
                }
            }
        }

        /**
         * 生成 Actual 计算因子详情数据
         *
         * @param indexLocalDate  日期
         * @param date2MetricsDTO 历史 KPI 数据
         * @return Actual 计算因子详情数据
         * @author zc
         * @date 2024/12/24
         */
        private List<HotelKpiPreviousWeeksCalculationFactorDTO> generateCalculationFactorValue(LocalDate indexLocalDate, Map<Long, HotelKpiMetricsDTO> date2MetricsDTO) {
            List<HotelKpiPreviousWeeksCalculationFactorDTO> calculationFactorDetailList = Lists.newArrayList();

            LocalDate currentLocalDate = indexLocalDate;
            LocalDate firstWeekOfMonth = currentLocalDate.withDayOfMonth(1);
            while (!currentLocalDate.isBefore(firstWeekOfMonth)) {
                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(currentLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                HotelKpiMetricsDTO indexMetricsDTO = date2MetricsDTO.getOrDefault(indexDate, new HotelKpiMetricsDTO());
                // 获取前一周的日期
                LocalDate lastLocalDate = currentLocalDate.minusDays(7);

                List<HotelKpiPreviousWeeksCalculationFactorValueDTO> calculationFactor = Lists.newArrayList();
                if (currentLocalDate.equals(firstWeekOfMonth)) {
                    calculationFactor.addAll(beanMapper.mapList(indexMetricsDTO.getActual().getCalculationFactor(),
                            HotelKpiPreviousWeeksCalculationFactorValueDTO.class, (dto1, dto2) -> dto2.setValue(dto1.getCrawl())));
                } else {
                    long lastIndexDate = Long.parseLong(DateTimeUtils.localDateToStr(lastLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                    HotelKpiMetricsDTO lastMetricsDTO = date2MetricsDTO.getOrDefault(lastIndexDate, new HotelKpiMetricsDTO());

                    Map<Integer, HotelKpiCalculationFactorDTO> index2ValueDTOForCurrent = indexMetricsDTO.getActual().getCalculationFactor().stream()
                            .collect(Collectors.toMap(HotelKpiCalculationFactorDTO::getIndex, Function.identity()));
                    Map<Integer, HotelKpiCalculationFactorDTO> lastIndex2ValueDTOForCurrent = lastMetricsDTO.getActual().getCalculationFactor().stream()
                            .collect(Collectors.toMap(HotelKpiCalculationFactorDTO::getIndex, Function.identity()));

                    for (int i = 1; i <= 2; i++) {
                        BigDecimal indexDateValue = index2ValueDTOForCurrent.getOrDefault(i, new HotelKpiCalculationFactorDTO()).getCrawl();
                        BigDecimal lastDateValue = lastIndex2ValueDTOForCurrent.getOrDefault(i, new HotelKpiCalculationFactorDTO()).getCrawl();
                        calculationFactor.add(HotelKpiPreviousWeeksCalculationFactorValueDTO.builder()
                                .index(i)
                                .value(Objects.isNull(indexDateValue) || Objects.isNull(lastDateValue) ?
                                        null : indexDateValue.subtract(lastDateValue))
                                .build());
                    }
                }
                calculationFactorDetailList.add(HotelKpiPreviousWeeksCalculationFactorDTO.builder()
                        .date(indexDate)
                        .calculationFactor(calculationFactor)
                        .build());

                // 往前推一周
                currentLocalDate = lastLocalDate;
            }
            return calculationFactorDetailList;
        }

        /**
         * 获取月 KPI 原始数据
         *
         * @param propertyId   资产 id
         * @param startDate    开始日期(yyyyMMdd)
         * @param endDate      结束日期(yyyyMMdd)
         * @param metricsTypes 指标类型集合
         * @return HotelKpiMetricsDTO 集合
         * @author zc
         * @date 2024/8/13
         */
        private List<HotelKpiMetricsDTO> listKpiMetricsDTOMonthlyList(int propertyId, long startDate, long endDate, List<Integer> metricsTypes) {
            long startMonth = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.strToLocalDate(String.valueOf(startDate),
                    BaseConstant.DATE_FORMAT_YYYYMMDD).withMonth(1), BaseConstant.DATE_FORMAT_YYYYMM));
            long endMonth = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.strToLocalDate(String.valueOf(endDate),
                    BaseConstant.DATE_FORMAT_YYYYMMDD), BaseConstant.DATE_FORMAT_YYYYMM));
            return monthlyService.listKpiMetricsDTOList(propertyId, startMonth, endMonth, metricsTypes);
        }

        /**
         * 获取 KPI 原始数据
         *
         * @param propertyId   资产 id
         * @param startDate    开始日期(yyyyMMdd)
         * @param endDate      结束日期(yyyyMMdd)
         * @param metricsTypes 指标类型
         * @return HotelKpiMetricsDTO 集合
         * @author zc
         * @date 2024/6/26
         */
        @Override
        public List<HotelKpiMetricsDTO> listKpiMetricsDTOList(int propertyId, long startDate, long endDate, List<Integer> metricsTypes) {
            List<HotelKpiWeekly> kpiWeeklyList = listByDateRangeAndMetricsTypes(propertyId, startDate, endDate, metricsTypes);
            Map<Long, Map<Integer, HotelKpiWeekly>> metricsType2KpiByDate = kpiWeeklyList.stream()
                    .collect(Collectors.groupingBy(HotelKpiWeekly::getWeek,
                            Collectors.toMap(HotelKpiWeekly::getMetricsType, Function.identity())));

            LocalDate startLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(startDate), BaseConstant.DATE_FORMAT_YYYYMMDD);
            LocalDate endLocalDate = DateTimeUtils.strToLocalDate(String.valueOf(endDate), BaseConstant.DATE_FORMAT_YYYYMMDD);

            List<HotelKpiMetricsDTO> kpiMetricsDTOList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = (indexLocalDate.getDayOfMonth() >= 22) ?
                    indexLocalDate.plusMonths(1).withDayOfMonth(1) : indexLocalDate.plusDays(7)) {
                long indexDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                Map<Integer, HotelKpiWeekly> metricsType2Kpi = metricsType2KpiByDate.getOrDefault(indexDate, Collections.emptyMap());
                for (Integer metricsType : metricsTypes) {
                    HotelKpiMetricsDTO kpiMetricsDTO = new HotelKpiMetricsDTO();
                    HotelKpiWeekly kpiWeekly = metricsType2Kpi.get(metricsType);
                    if (Objects.isNull(kpiWeekly)) {
                        kpiMetricsDTO.setDate(indexDate);
                        kpiMetricsDTO.setMetricsType(metricsType);
                    } else {
                        HotelKpiOriginDetailDTO originDetailDTO = JsonUtils.convertStringToObject(objectMapper,
                                kpiWeekly.getDetail(), HotelKpiOriginDetailDTO.class);
                        kpiMetricsDTO = beanMapper.map(kpiWeekly, HotelKpiMetricsDTO.class, dto -> {
                            dto.setDate(kpiWeekly.getWeek());
                            HotelKpiOriginValueDTO actual = originDetailDTO.getActual();
                            dto.setActual(beanMapper.map(actual, HotelKpiValueDTO.class, item -> item.setSystem(actual.getCrawl())));
                            HotelKpiOriginValueDTO target = originDetailDTO.getTarget();
                            dto.setTarget(beanMapper.map(target, HotelKpiValueDTO.class, item -> item.setSystem(target.getCrawl())));
                            HotelKpiOriginValueDTO cure = originDetailDTO.getCure();
                            dto.setCure(beanMapper.map(cure, HotelKpiValueDTO.class, item -> item.setSystem(cure.getCrawl())));
                        });
                    }
                    kpiMetricsDTOList.add(kpiMetricsDTO);
                }
            }
            return kpiMetricsDTOList;
        }

        /**
         * 组装 Target, Cure setting 数据
         *
         * @param propertyId            资产 id
         * @param metricsDTOList        Weekly 指标数据 DTO 容器
         * @param metricsDTOMonthlyList Monthly 指标数据
         * @param metricsTypes          指标类型集合
         * @param startDate             开始日期
         * @param endDate               结束日期
         * @author zc
         * @date 2024/6/26
         */
        private void packageSettingValue(int propertyId, List<HotelKpiMetricsDTO> metricsDTOList,
                                         List<HotelKpiMetricsDTO> metricsDTOMonthlyList,
                                         List<Integer> metricsTypes, long startDate, long endDate) {
            if (CollectionUtils.isEmpty(metricsDTOList)) {
                return;
            }
            // 获取 Actual 的设置数据
            List<HotelKpiMetricsSettingActualDTO> metricsSettingActualDTOS =
                    metricsSettingActualService.listWeeklySettingByDateRange(propertyId, metricsTypes, startDate,
                            endDate, metricsDTOMonthlyList);
            // 获取 Target、Cure 设置数据
            List<HotelKpiMetricsSettingTargetDTO> metricsSettingTargetDTOS =
                    metricsSettingTargetService.listWeeklySettingByDateRange(propertyId, metricsTypes, startDate, endDate);
            // 组装 Metrics Setting
            metricsSettingService.packageMetricsSetting(metricsDTOList, metricsSettingActualDTOS, metricsSettingTargetDTOS);
        }

    }