# RevenueWidgetServiceImpl.java

    package cloud.wconnect.rateshop.service.revenue.impl;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.StatisticsUnitCons;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.rateshop.cons.rateshop.RateTypeEnum;
    import cloud.wconnect.rateshop.entity.rateshop.Setting;
    import cloud.wconnect.rateshop.entity.revenue.RevenueBudgetOtb;
    import cloud.wconnect.rateshop.entity.revenue.RevenueOtbRevenue;
    import cloud.wconnect.rateshop.entity.revenue.RevenueOtbRevenueFuture;
    import cloud.wconnect.rateshop.entity.revenue.RevenueTransientGroup;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.LatestHotelPriceDTO;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.RateConfigDTO;
    import cloud.wconnect.rateshop.pojo.dto.revenue.*;
    import cloud.wconnect.rateshop.pojo.query.rateshop.LatestHotelPriceQuery;
    import cloud.wconnect.rateshop.pojo.query.revenue.OtbRevenueQuery;
    import cloud.wconnect.rateshop.service.rateshop.ILatestBARRateService;
    import cloud.wconnect.rateshop.service.rateshop.IPropertyHotelService;
    import cloud.wconnect.rateshop.service.rateshop.ISettingService;
    import cloud.wconnect.rateshop.service.revenue.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    import java.math.BigDecimal;
    import java.time.LocalDate;
    import java.time.ZoneId;
    import java.time.ZonedDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.stream.Collectors;

    /**
     * @author yyt
     * 2023/12/14
     */
    @Service
    @DS("rate_shop")
    public class RevenueWidgetServiceImpl implements IRevenueWidgetService {

        @Autowired
        private IRevenueBudgetOtbService revenueBudgetOtbService;
        @Autowired
        private IRevenueOtbRevenueService revenueOtbRevenueService;
        @Autowired
        private IRevenueOtbRevenueFutureService revenueOtbRevenueFutureService;
        @Autowired
        private IRevenueTransientGroupService revenueTransientGroupService;
        @Autowired
        private ISettingService settingService;
        @Autowired
        private ILatestBARRateService latestBARRateService;
        @Autowired
        private IPropertyHotelService propertyHotelService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private ObjectMapper objectMapper;

        /**
         * 获取不同资产下当天的 Revenue 和 Occ% 完成度
         *
         * @param propertyDTOS 选择资产
         * @return Revenue 和 Occ% 完成度
         * @author yyt
         * @date 2023/12/14
         */
        @Override
        public List<RevenueCompletionDTO> listRevenueCompletions(List<PropertyDTO> propertyDTOS) {
            List<RevenueCompletionDTO> revenueCompletionDTOList = Lists.newArrayListWithExpectedSize(propertyDTOS.size());

            propertyDTOS.forEach(propertyBO -> {
                Integer propertyId = propertyBO.getId();

                // 获取指定日期 otb revenue
                ZonedDateTime dateNow = DateTimeUtils.dateNow(propertyBO.getTimezone());
                RevenueOtbRevenue otbRevenue = revenueOtbRevenueService.getOne(new QueryWrapper<RevenueOtbRevenue>()
                        .lambda().eq(RevenueOtbRevenue::getPropertyId, propertyId)
                        .eq(RevenueOtbRevenue::getTheDate, dateNow.toEpochSecond()));
                BigDecimal revenue = BigDecimal.ZERO;
                BigDecimal occ = BigDecimal.ZERO;
                if (Objects.nonNull(otbRevenue)) {
                    if (Objects.nonNull(otbRevenue.getOtbRevenue())) {
                        revenue = otbRevenue.getOtbRevenue();
                    }
                    if (Objects.nonNull(otbRevenue.getOtbOccPercent())) {
                        occ = otbRevenue.getOtbOccPercent();
                    }
                }

                // 获取指定日期 budget
                RevenueBudgetOtb budget = revenueBudgetOtbService.getOne(new QueryWrapper<RevenueBudgetOtb>().lambda()
                        .eq(RevenueBudgetOtb::getPropertyId, propertyId).eq(RevenueBudgetOtb::getTheYear, dateNow.getYear())
                        .eq(RevenueBudgetOtb::getTheMonth, dateNow.getMonthValue()));
                BigDecimal budgetRevenue = BigDecimal.ZERO;
                BigDecimal budgetOcc = BigDecimal.ZERO;
                if (Objects.nonNull(budget)) {
                    if (Objects.nonNull(budget.getRoomRevenue())) {
                        budgetRevenue = budget.getRoomRevenue();
                    }
                    if (Objects.nonNull(budget.getOccPercent())) {
                        budgetOcc = budget.getOccPercent();
                    }
                }

                RevenueCompletionDTO revenueCompletionDTO = new RevenueCompletionDTO();
                revenueCompletionDTO.setPropertyId(propertyId);
                revenueCompletionDTO.setPropertyName(propertyBO.getAbbr());
                revenueCompletionDTO.setRevenue(revenue);
                revenueCompletionDTO.setBudgetRevenue(budgetRevenue);
                revenueCompletionDTO.setOcc(occ);
                revenueCompletionDTO.setBudgetOcc(budgetOcc);
                revenueCompletionDTOList.add(revenueCompletionDTO);
            });
            return revenueCompletionDTOList;
        }

        /**
         * 获取不同资产下 OTB Revenue 的走势
         *
         * @param propertyDTOS 选择资产
         * @param query        检索条件
         * @return OTB Revenue
         * @author yyt
         * @date 2023/12/14
         */
        @Override
        public List<OtbRevenueTrendsDTO> listOtbRevenue(List<PropertyDTO> propertyDTOS, OtbRevenueQuery query) {
            //根据统计单位计算统计日期
            List<LocalDate> statisticDate = DateTimeUtils.calculateStatisticsDate(query.getUnit(), query.getSpan());
            if (CollectionUtils.isEmpty(statisticDate)) {
                return Collections.emptyList();
            }

            LocalDate startLocalDate = statisticDate.get(0);
            LocalDate endLocalDate = statisticDate.get(1);

            List<OtbRevenueTrendsDTO> otbRevenueTrendsDTOList = Lists.newArrayListWithCapacity(propertyDTOS.size());

            propertyDTOS.forEach(propertyDTO -> {
                Integer propertyId = propertyDTO.getId();
                String timezone = propertyDTO.getTimezone();

                // 资产时区下的开始日期
                long startDate = DateTimeUtils.localDateToLong(startLocalDate, timezone);
                // 资产时区下的结束日期
                long endDate = DateTimeUtils.localDateToLong(endLocalDate, timezone);

                List<RevenueOtbRevenue> otbRevenues = revenueOtbRevenueService.list(new QueryWrapper<RevenueOtbRevenue>()
                        .lambda().eq(RevenueOtbRevenue::getPropertyId, propertyId)
                        .between(RevenueOtbRevenue::getTheDate, startDate, endDate)
                        .select(RevenueOtbRevenue::getId, RevenueOtbRevenue::getPropertyId, RevenueOtbRevenue::getTheDate, RevenueOtbRevenue::getOtbRevenue));

                // 查询未来月份的数据
                ZonedDateTime zonedDateTime = DateTimeUtils.dateNow(timezone).withDayOfMonth(1);
                List<RevenueOtbRevenueFuture> otbRevenueFutures =
                        revenueOtbRevenueFutureService.list(new QueryWrapper<RevenueOtbRevenueFuture>().lambda()
                        .eq(RevenueOtbRevenueFuture::getPropertyId, propertyId)
                        .eq(RevenueOtbRevenueFuture::getTheYear, zonedDateTime.getYear())
                        .gt(RevenueOtbRevenueFuture::getTheMonth, zonedDateTime.getMonthValue()));

                OtbRevenueTrendsDTO otbRevenueTrendsDTO = new OtbRevenueTrendsDTO();
                otbRevenueTrendsDTO.setPropertyId(propertyId);
                otbRevenueTrendsDTO.setPropertyName(propertyDTO.getAbbr());

                // 根据统计单位和统计时间生成 otb revenue 趋势集合
                List<OtbRevenueTrendDTO> otbRevenueTrendDTOList = generateOtbRevenueTrend(propertyDTO, otbRevenues, otbRevenueFutures, query);
                otbRevenueTrendDTOList.sort(Comparator.comparing(OtbRevenueTrendDTO::getDate));
                otbRevenueTrendsDTO.setData(otbRevenueTrendDTOList);
                otbRevenueTrendsDTOList.add(otbRevenueTrendsDTO);
            });

            return otbRevenueTrendsDTOList;
        }

        /**
         * 生成 otb revenue 趋势集合
         *
         * @param propertyDTO       资产
         * @param otbRevenues       otb revenue
         * @param otbRevenueFutures otb revenue future
         * @param query             检索条件
         * @return otb revenue 趋势集合
         * @author yyt
         * @date 2023/12/14
         */
        private List<OtbRevenueTrendDTO> generateOtbRevenueTrend(PropertyDTO propertyDTO, List<RevenueOtbRevenue> otbRevenues,
                                                                 List<RevenueOtbRevenueFuture> otbRevenueFutures, OtbRevenueQuery query) {
            // 处理 revenue 数据，每个月保留最后一条数据即可，因为每天的 revenue 是累计的
            String timezone = propertyDTO.getTimezone();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYY_MM);
            Map<String, RevenueOtbRevenue> revenueByYearMonth = otbRevenues.stream()
                    .peek(item -> item.setYearMonth(DateTimeUtils.toZonedDateTime(item.getTheDate(), timezone).format(formatter)))
                    .collect(Collectors.groupingBy(RevenueOtbRevenue::getYearMonth, Collectors.collectingAndThen(Collectors.toList(),
                            revenues -> revenues.stream().max(Comparator.comparing(RevenueOtbRevenue::getTheDate))
                                    .orElseGet(RevenueOtbRevenue::new)
                    )));
            otbRevenues = new ArrayList<>(revenueByYearMonth.values());

            long startOfCurrentMonth = DateTimeUtils.dateNow(timezone).withDayOfMonth(1).toEpochSecond();

            // 未来月数据 以年、月 分组
            Map<Integer, Map<Integer, BigDecimal>> revenueByYear = otbRevenueFutures.stream()
                    .filter(item -> Objects.nonNull(item.getOtbRevenue()))
                    .collect(Collectors.groupingBy(RevenueOtbRevenueFuture::getTheYear,
                            Collectors.toMap(RevenueOtbRevenueFuture::getTheMonth, RevenueOtbRevenueFuture::getOtbRevenue)));

            Integer span = query.getSpan();
            Integer unit = query.getUnit();
            List<OtbRevenueTrendDTO> otbRevenueTrendDTOList = Lists.newArrayListWithCapacity(span);
            LocalDate localDate = LocalDate.now(ZoneId.of(timezone));
            if (StatisticsUnitCons.MONTH == unit) {
                for (int i = 1; i <= span; i++) {
                    // 获取日期,例如：2023-05
                    String date = localDate.format(formatter);

                    // 当月的开始日期
                    long startDate = DateTimeUtils.localDateToLong(localDate.withDayOfMonth(1), timezone);
                    // 当月的结束日期
                    long endDate = DateTimeUtils.localDateToLong(localDate.withDayOfMonth(localDate.lengthOfMonth()), timezone);

                    //计算符合条件的 otb revenue
                    List<RevenueOtbRevenue> matchList = otbRevenues.stream()
                            .filter(item -> item.getTheDate() >= startDate && item.getTheDate() <= endDate).toList();
                    BigDecimal totalRevenue = matchList.stream().map(RevenueOtbRevenue::getOtbRevenue)
                            .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
                    otbRevenueTrendDTOList.add(new OtbRevenueTrendDTO(date, totalRevenue));

                    //往前推一个月
                    localDate = localDate.minusMonths(1);
                }
            } else if(StatisticsUnitCons.QUARTER == unit) {
                for (int i = 1; i <= span; i++) {
                    int year = localDate.getYear();
                    // 当前日期的所属季度
                    int quarter = (localDate.getMonthValue() - 1) / 3 + 1;

                    BigDecimal totalRevenue = BigDecimal.ZERO;

                    // 季度的开始日期
                    LocalDate startDate = LocalDate.of(year, quarter * 3 - 2, 1);
                    // 当前季度（i == 1）的未来月份的 db 数据，也要参与统计
                    // 历史季度，只统计当月 db 的数据就行
                    if(i == 1){
                        // 循环一个季度
                        for (int month = 0; month < 3; month++) {
                            // 计算符合条件的 otb revenue
                            // 若当前循环的月份为未来月，则使用未来月的 db 的数据
                            totalRevenue = totalRevenue.add(statisticsRevenue(startDate.plusMonths(month), startOfCurrentMonth,
                                    revenueByYear, otbRevenues, timezone));
                        }
                    } else {
                        List<RevenueOtbRevenue> matchList = otbRevenues.stream()
                                .filter(item -> item.getTheDate() >= DateTimeUtils.localDateToLong(startDate, timezone)
                                        && item.getTheDate() <= DateTimeUtils.localDateToLong(startDate.plusMonths(3)
                                        .minusDays(1), timezone)).toList();
                        totalRevenue = matchList.stream().map(RevenueOtbRevenue::getOtbRevenue)
                                .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
                    }

                    // 季度的日期格式为：2023-1
                    String date = year + "-" + quarter;
                    otbRevenueTrendDTOList.add(new OtbRevenueTrendDTO(date, totalRevenue));

                    // 往前推一个季度
                    localDate = localDate.minusMonths(3);
                }
            } else {
                localDate = localDate.withDayOfYear(1);
                for (int i = 1; i <= span; i++) {

                    // 当前年（i == 1）的未来月份的 db 数据，也要参与统计
                    // 历史年度，只统计当月 db 的数据就行
                    if(i == 1){
                        BigDecimal totalRevenue = BigDecimal.ZERO;

                        // 循环一年
                        for (int month = 0; month < 12; month++) {
                            // 计算符合条件的 otb revenue
                            // 若当前循环的月份为未来月，则使用未来月的 db 的数据
                            totalRevenue = totalRevenue.add(statisticsRevenue(localDate.plusMonths(month), startOfCurrentMonth,
                                    revenueByYear, otbRevenues, timezone));
                        }

                        otbRevenueTrendDTOList.add(new OtbRevenueTrendDTO(String.valueOf(localDate.getYear()), totalRevenue));
                    } else {
                        // 当前年的开始日期
                        long startDate = DateTimeUtils.localDateToLong(localDate, timezone);
                        // 当前年的结束日期
                        long endDate = DateTimeUtils.localDateToLong(localDate.plusYears(1).minusDays(1), timezone);

                        //计算符合条件的 otb revenue
                        List<RevenueOtbRevenue> matchList = otbRevenues.stream()
                                .filter(item -> item.getTheDate() >= startDate && item.getTheDate() <= endDate).toList();
                        BigDecimal totalRevenue = matchList.stream().map(RevenueOtbRevenue::getOtbRevenue)
                                .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
                        otbRevenueTrendDTOList.add(new OtbRevenueTrendDTO(String.valueOf(localDate.getYear()), totalRevenue));
                    }

                    // 往前推一年
                    localDate = localDate.minusYears(1);
                }
            }
            return otbRevenueTrendDTOList;
        }

        /**
         * 统计 revenue
         *
         * @param currentLoopStartMonthDate 当前循环的月
         * @param startOfCurrentMonth       当前月份
         * @param revenueByYear             otb revenue future
         * @param otbRevenues               otb revenue
         * @param timezone                  时区
         * @return revenue
         * @author yyt
         * @date 2024/5/23
         **/
        private BigDecimal statisticsRevenue(LocalDate currentLoopStartMonthDate, long startOfCurrentMonth,
                                             Map<Integer, Map<Integer, BigDecimal>> revenueByYear, List<RevenueOtbRevenue> otbRevenues,
                                             String timezone) {
            long currentLoopStartMonthTime = DateTimeUtils.localDateToLong(currentLoopStartMonthDate, timezone);
            if (currentLoopStartMonthTime > startOfCurrentMonth) {
                return revenueByYear.getOrDefault(currentLoopStartMonthDate.getYear(), Collections.emptyMap())
                        .getOrDefault(currentLoopStartMonthDate.getMonthValue(), BigDecimal.ZERO);
            } else {
                // 若不是，则使用当前月的 db 的数据
                long currentLoopEndMonthTime = DateTimeUtils.localDateToLong(currentLoopStartMonthDate.plusMonths(1)
                        .minusDays(1), timezone);
                List<RevenueOtbRevenue> matchList = otbRevenues.stream()
                        .filter(item -> item.getTheDate() >= currentLoopStartMonthTime && item.getTheDate() <= currentLoopEndMonthTime).toList();
                return matchList.stream().map(RevenueOtbRevenue::getOtbRevenue)
                        .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
            }

        }

        /**
         * 获取未来14天的入住信息（occ%, Room available, 自身官网最低房价）
         *
         * @param propertyId   资产 id
         * @param datasourceId 数据源 id
         * @param platformId   平台 id
         * @return 未来14天的入住信息
         * @author yyt
         * @date 2026/1/19
         */
        @Override
        public FutureStayInformationDTO listFutureStayInformation(int propertyId, Integer datasourceId, Integer platformId) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            String timezone = propertyDTO.getTimezone();
            Long targetDate = DateTimeUtils.dateNowTimestamp(timezone);
            ZonedDateTime currentDay = DateTimeUtils.toZonedDateTime(targetDate, timezone);
            ZonedDateTime nextDayDate = currentDay.plusDays(13);
            List<RevenueTransientGroup> transientGroups = revenueTransientGroupService.list(propertyId, targetDate, targetDate, nextDayDate.toEpochSecond());
            Map<Long, RevenueTransientGroup> transientGroupByTargetDate = transientGroups.stream()
                    .collect(Collectors.toMap(RevenueTransientGroup::getTargetDate, v -> v));

            Map<Long, BigDecimal> rateByDate = new HashMap<>();
            Setting setting = settingService.getByPropertyId(propertyId);
            if (Objects.nonNull(setting)) {
                //配置的房价类型参数
                List<RateConfigDTO> rateConfigDTOS = JsonUtils.convertStringToList(objectMapper, setting.getRateConfig(), RateConfigDTO.class);
                //房价类型
                Set<Integer> rateTypes = rateConfigDTOS.stream().map(RateConfigDTO::getRateType).collect(Collectors.toSet());
                if (rateTypes.contains(RateTypeEnum.BEST_AVAILABLE_RATE.getRateType())) {
                    //查询资产自身酒店
                    Integer hotelId = propertyHotelService.getPropertySelfHotel(propertyId).getHotelId();
                    //查询日期
                    Long startDate = Long.valueOf(currentDay.format(DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYYMMDD)));
                    Long endDate = Long.valueOf(nextDayDate.format(DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYYMMDD)));
                    //构造查询条件
                    LatestHotelPriceQuery latestHotelBARPriceQuery = LatestHotelPriceQuery.builder()
                            .fromDate(startDate)
                            .toDate(endDate)
                            .datasourceId(datasourceId)
                            .platformId(platformId)
                            .roomIds(Collections.emptyList())
                            .build();
                    ZoneId zoneId = ZoneId.of(timezone);
                    //获取指定数据源、指定平台的最低BAR房价
                    List<LatestHotelPriceDTO> latestHotelPriceDTOS = latestBARRateService.listLowestBARHotelPrice(Collections.singletonList(hotelId), setting.getCurrencyId(), setting.getContainTaxFlag(),
                            latestHotelBARPriceQuery, rateConfigDTOS, setting.getPos());
                    rateByDate = latestHotelPriceDTOS.stream()
                            .filter(item -> Objects.nonNull(item.getPrice()))
                            .collect(Collectors.toMap(
                                    item -> LocalDate.parse(item.getDate(), DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYY_MM_DD))
                                            .atStartOfDay(zoneId).toEpochSecond(),
                                    LatestHotelPriceDTO::getPrice
                            ));
                }
            }

            DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYY_MM_DD);

            List<HotelOperationDTO> hotelOperationDTOList = Lists.newArrayListWithExpectedSize(14);
            for (ZonedDateTime date = currentDay; !date.isAfter(nextDayDate); date = date.plusDays(1)) {
                long dateSecond = date.toEpochSecond();
                RevenueTransientGroup transientGroup = transientGroupByTargetDate.getOrDefault(dateSecond,
                        new RevenueTransientGroup().setTheDate(targetDate).setTargetDate(dateSecond));

                HotelOperationDTO hotelOperationDTO = new HotelOperationDTO();
                hotelOperationDTO.setDate(date.format(dateTimeFormatter2));
                hotelOperationDTO.setOcc(Objects.isNull(transientGroup.getOccPercent()) ? BigDecimal.ZERO : transientGroup.getOccPercent());
                int rooms = 0;
                if (Objects.nonNull(transientGroup.getTransientRoomNum())) {
                    rooms += transientGroup.getTransientRoomNum();
                }
                if (Objects.nonNull(transientGroup.getGroupRoomNum())) {
                    rooms += transientGroup.getGroupRoomNum();
                }
                hotelOperationDTO.setRooms(rooms);
                hotelOperationDTO.setRate(rateByDate.getOrDefault(dateSecond, BigDecimal.ZERO));
                hotelOperationDTOList.add(hotelOperationDTO);
            }

            return new FutureStayInformationDTO(hotelOperationDTOList, setting.getContainTaxFlag());
        }

    }