# WeeklyMonthlyRevenueRoomServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.RoomsConfigDTO;
    import cloud.wconnect.commonlib.service.RoomsConfigInternalService;
    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.rateshop.cons.PrecisionCons;
    import cloud.wconnect.rateshop.cons.rateshop.RateTypeEnum;
    import cloud.wconnect.rateshop.cons.revenue.RevenueCons;
    import cloud.wconnect.rateshop.cons.weeklymonthlyrevenue.MonthlyRevenueCons;
    import cloud.wconnect.rateshop.entity.rateshop.Setting;
    import cloud.wconnect.rateshop.entity.revenue.RevenueBudgetOtb;
    import cloud.wconnect.rateshop.entity.revenue.RevenueOtbRevenueLastYear;
    import cloud.wconnect.rateshop.entity.weeklymonthlyrevenue.*;
    import cloud.wconnect.rateshop.mapper.weeklymonthlyrevenue.WeeklyMonthlyRevenueRoomMapper;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.LatestHotelPriceDTO;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.RateConfigDTO;
    import cloud.wconnect.rateshop.pojo.dto.weeklymonthlyrevenue.*;
    import cloud.wconnect.rateshop.pojo.query.rateshop.LatestHotelPriceQuery;
    import cloud.wconnect.rateshop.pojo.vo.monthlyrevenue.export.MonthlyRevenueExportVO;
    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.IRevenueBudgetOtbService;
    import cloud.wconnect.rateshop.service.revenue.IRevenueOtbRevenueLastYearService;
    import cloud.wconnect.rateshop.service.weeklymonthlyrevenue.*;
    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 jakarta.annotation.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.context.i18n.LocaleContextHolder;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.LocalDate;
    import java.time.YearMonth;
    import java.time.ZoneId;
    import java.time.format.TextStyle;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * Weekly Monthly Revenue Room 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-05-13
     */
    @Service
    @DS("rate_shop")
    public class WeeklyMonthlyRevenueRoomServiceImpl extends ServiceImpl<WeeklyMonthlyRevenueRoomMapper, WeeklyMonthlyRevenueRoom> implements IWeeklyMonthlyRevenueRoomService {

        @Resource
        private IWeeklyMonthlyRevenueSegmentService revenueSegmentService;
        @Resource
        private IWeeklyMonthlyRevenueRoomTypeService revenueRoomTypeService;
        @Resource
        private IWeeklyMonthlyRevenueBudgetService revenueBudgetService;
        @Resource
        private ISettingService settingService;
        @Resource
        private ILatestBARRateService latestBARRateService;
        @Resource
        private IPropertyHotelService propertyHotelService;
        @Resource
        private IRevenueBudgetOtbService revenueBudgetOtbService;
        @Resource
        private IRevenueOtbRevenueLastYearService revenueOtbRevenueLastYearService;
        @Resource
        private RoomsConfigInternalService roomsConfigInternalService;
        @Resource
        private IMonthlyRevenueCommonService monthlyRevenueCommonService;
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private ObjectMapper objectMapper;

        // 每周天数
        private static final int DAYS_PER_WEEK = 7;
        // 每月最小天数
        private static final int MIN_DAYS_PER_MONTH = 28;

        private static final String PERCENT_SYMBOL = "%";

        /**
         * 获取指定年月的 Room Revenue
         *
         * @param propertyDTO 资产
         * @param theMonth    年月（yyyyMM）
         * @return WeeklyMonthlyRevenueRoomDTO
         * @author zc
         * @date 2024/5/15
         */
        @Override
        public WeeklyMonthlyRevenueRoomDTO getRoomRevenue(PropertyDTO propertyDTO, long theMonth) {
            YearMonth targetYearMonth = YearMonthUtils.getYearMonth(String.valueOf(theMonth));
            LocalDate startLocalDate = targetYearMonth.atDay(1);
            // 由于当天还未结束，需要往前推一天作为 MTD 的结束日期
            LocalDate yesterdayLocalDate = LocalDate.now(ZoneId.of(propertyDTO.getTimezone())).minusDays(1);
            LocalDate endLocalDate = targetYearMonth.atEndOfMonth();

            long startDateCurrent = Long.parseLong(DateTimeUtils.localDateToStr(startLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDateCurrent = Long.parseLong(DateTimeUtils.localDateToStr(endLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            // 获取当前月范围内的 Room Revenue Daily
            List<WeeklyMonthlyRevenueRoom> roomRevenueDailyList = listRoomRevenueDaily(propertyDTO.getId(),
                    startDateCurrent, endDateCurrent);
            // 获取当前月范围内的 Room Revenue Segment
            List<WeeklyMonthlyRevenueSegment> roomRevenueSegmentList = revenueSegmentService.listByDateRange(propertyDTO.getId(),
                    startDateCurrent, endDateCurrent);

            long startDateLastYear = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.getSameDateLastYear(startLocalDate),
                    BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDateLastYear = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.getSameDateLastYear(endLocalDate),
                    BaseConstant.DATE_FORMAT_YYYYMMDD));
            // 获取去年同期日期范围内的 Room Revenue Daily
            List<WeeklyMonthlyRevenueRoom> roomRevenueDailyListLastYear = listRoomRevenueDaily(propertyDTO.getId(),
                    startDateLastYear, endDateLastYear);
            // 获取去年同期日期范围内的 Room Revenue Segment
            List<WeeklyMonthlyRevenueSegment> roomRevenueSegmentListLastYear = revenueSegmentService.listByDateRange(propertyDTO.getId(),
                    startDateLastYear, endDateLastYear);

            WeeklyMonthlyRevenueRoomDTO revenueRoomDTO = new WeeklyMonthlyRevenueRoomDTO();

            // 获取 Room type
            WeeklyMonthlyRevenueRoomType revenueRoomType = revenueRoomTypeService.getByMonth(propertyDTO.getId(), theMonth);
            revenueRoomDTO.setRoomType(Objects.isNull(revenueRoomType) ? null : revenueRoomType.getRoomType());
            revenueRoomDTO.setRoomRevenues(getRoomRevenues(propertyDTO, roomRevenueDailyList, roomRevenueSegmentList,
                    roomRevenueDailyListLastYear, roomRevenueSegmentListLastYear, startLocalDate, endLocalDate));

            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            // 若目标年月大于当前年月，则 MTD、LY 的数值设为空
            if (targetYearMonth.isAfter(YearMonth.from(yesterdayLocalDate))) {
                WeeklyMonthlyRevenueRoomRevenueStatisticsDTO statisticsDTO = WeeklyMonthlyRevenueRoomRevenueStatisticsDTO.builder()
                        .overall(new WeeklyMonthlyRevenueOverallDTO())
                        .transients(new WeeklyMonthlyRevenueTransientDTO())
                        .group(new WeeklyMonthlyRevenueGroupDTO())
                        .contract(new WeeklyMonthlyRevenueContractDTO()).build();

                // 组装 MTD
                revenueRoomDTO.setMtd(statisticsDTO);

                // 组装 LY
                revenueRoomDTO.setLy(statisticsDTO);
            } else if (targetYearMonth.isBefore(YearMonth.from(yesterdayLocalDate))) {
                // 若目标年月小于当前年月，则 MTD、LY 的数值为整月
                // 组装 MTD
                revenueRoomDTO.setMtd(getStatistics(roomRevenueDailyList, roomRevenueSegmentList, startDateCurrent, endDateCurrent, roundingMode));

                // 组装 LY
                revenueRoomDTO.setLy(getStatistics(roomRevenueDailyListLastYear, roomRevenueSegmentListLastYear,
                        startDateLastYear, endDateLastYear, roundingMode));
            } else {
                // 组装 MTD
                long nowDate = Long.parseLong(DateTimeUtils.localDateToStr(yesterdayLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                revenueRoomDTO.setMtd(getStatistics(roomRevenueDailyList, roomRevenueSegmentList, startDateCurrent, nowDate, roundingMode));

                // 组装 LY
                long nowDateLastYear = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.getSameDateLastYear(yesterdayLocalDate),
                        BaseConstant.DATE_FORMAT_YYYYMMDD));
                revenueRoomDTO.setLy(getStatistics(roomRevenueDailyListLastYear, roomRevenueSegmentListLastYear,
                        startDateLastYear, nowDateLastYear, roundingMode));
            }

            // 组装 Total
            revenueRoomDTO.setTotal(getStatistics(roomRevenueDailyList, roomRevenueSegmentList, startDateCurrent, endDateCurrent, roundingMode));

            RevenueBudgetOtb budgetOtb = revenueBudgetOtbService.getBudget(propertyDTO.getId(), targetYearMonth.getYear(), targetYearMonth.getMonthValue());
            revenueRoomDTO.setBudget(Objects.isNull(budgetOtb) ? new RevenueInfoDTO() :
                    new RevenueInfoDTO(budgetOtb.getRoomRevenue(), budgetOtb.getOccPercent(), budgetOtb.getAdr(), budgetOtb.getRevPar()));

            RevenueOtbRevenueLastYear lastYear = revenueOtbRevenueLastYearService.getLastYear(propertyDTO.getId(),
                    targetYearMonth.getYear() - 1, targetYearMonth.getMonthValue());
            revenueRoomDTO.setLastYear(Objects.isNull(lastYear) ? new RevenueInfoDTO() :
                    new RevenueInfoDTO(lastYear.getOtbRevenue(), lastYear.getOtbOccPercent(), lastYear.getOtbAdr(), lastYear.getOtbRevpar()));

            return revenueRoomDTO;
        }

        /**
         * 获取 Room Revenues
         *
         * @param propertyDTO                    资产
         * @param roomRevenueDailyList           当前月范围内的 Room Revenue Daily
         * @param roomRevenueSegmentList         当前月范围内的 Room Revenue Segment
         * @param roomRevenueDailyListLastYear   去年同期日期范围内的 Room Revenue Daily
         * @param roomRevenueSegmentListLastYear 去年同期日期范围内的 Room Revenue Segment
         * @param startLocalDate                 开始日期
         * @param endLocalDate                   结束日期
         * @return WeeklyMonthlyRevenueRoomRevenueDTO 集合
         * @author zc
         * @date 2024/5/14
         */
        private List<WeeklyMonthlyRevenueRoomRevenueDTO> getRoomRevenues(PropertyDTO propertyDTO,
                                                                         List<WeeklyMonthlyRevenueRoom> roomRevenueDailyList,
                                                                         List<WeeklyMonthlyRevenueSegment> roomRevenueSegmentList,
                                                                         List<WeeklyMonthlyRevenueRoom> roomRevenueDailyListLastYear,
                                                                         List<WeeklyMonthlyRevenueSegment> roomRevenueSegmentListLastYear,
                                                                         LocalDate startLocalDate, LocalDate endLocalDate) {
            Map<Long, WeeklyMonthlyRevenueRoom> theDate2RoomRevenue = roomRevenueDailyList.stream()
                    .collect(Collectors.toMap(WeeklyMonthlyRevenueRoom::getTheDate, Function.identity()));

            Map<Long, WeeklyMonthlyRevenueSegment> theDate2RevenueSegment = roomRevenueSegmentList.stream()
                    .collect(Collectors.toMap(WeeklyMonthlyRevenueSegment::getTheDate, Function.identity()));

            // 获取房间数配置
            RoomsConfigDTO roomsConfigDTO = roomsConfigInternalService.getRoomsConfig(propertyDTO.getId());
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            // 用于判断插入 Weekly、Weekly Last Year 统计数据
            int days = 1;
            List<WeeklyMonthlyRevenueRoomRevenueDTO> roomRevenueDTOList = Lists.newArrayList();
            for (LocalDate currentLocalDate = startLocalDate; !currentLocalDate.isAfter(endLocalDate); currentLocalDate = currentLocalDate.plusDays(1)) {
                long currentDate = Long.parseLong(DateTimeUtils.localDateToStr(currentLocalDate,
                        BaseConstant.DATE_FORMAT_YYYYMMDD));

                WeeklyMonthlyRevenueRoomRevenueDTO roomRevenueDTO = new WeeklyMonthlyRevenueRoomRevenueDTO();
                roomRevenueDTO.setTheDate(currentDate);
                roomRevenueDTO.setType(WeeklyMonthlyRevenueRoomRevenueDTO.DAY_TYPE);
                // 组装 Overall
                WeeklyMonthlyRevenueRoom revenueRoom = theDate2RoomRevenue.getOrDefault(currentDate, new WeeklyMonthlyRevenueRoom());
                roomRevenueDTO.setOverall(generateDailyOverall(revenueRoom, roomsConfigDTO.getOccRule(), roundingMode));
                // 组装 Segment
                WeeklyMonthlyRevenueSegment revenueSegment = theDate2RevenueSegment.getOrDefault(currentDate, new WeeklyMonthlyRevenueSegment());
                roomRevenueDTO.setTransients(generateDailyTransient(revenueSegment, roundingMode));
                roomRevenueDTO.setGroup(generateDailyGroup(revenueSegment, roundingMode));
                roomRevenueDTO.setContract(generateDailyContract(revenueSegment, roundingMode));

                // 添加 Daily Room Revenue
                roomRevenueDTOList.add(roomRevenueDTO);

                if ((days % DAYS_PER_WEEK == 0) || DateTimeUtils.isLastDayOfThisMonth(currentLocalDate)) {
                    LocalDate firstDayOfWeekLocalDate;
                    if (days % DAYS_PER_WEEK == 0) {
                        firstDayOfWeekLocalDate = currentLocalDate.minusDays(DAYS_PER_WEEK - 1);
                    } else {
                        firstDayOfWeekLocalDate = currentLocalDate.minusDays(YearMonth.from(currentLocalDate).lengthOfMonth()
                                - MIN_DAYS_PER_MONTH - 1);
                    }
                    // 获取 Weekly 第一天
                    long firstDayOfWeek = Long.parseLong(DateTimeUtils.localDateToStr(firstDayOfWeekLocalDate,
                            BaseConstant.DATE_FORMAT_YYYYMMDD));
                    // 组装 Weekly
                    roomRevenueDTOList.add(getStatisticsWeekly(WeeklyMonthlyRevenueRoomRevenueDTO.WEEK_TYPE,
                            firstDayOfWeek, currentDate, roomRevenueDailyList, roomRevenueSegmentList, roundingMode));

                    // 获取去年同期的 Weekly 第一天
                    long firstDayOfWeekLastYear = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.getSameDateLastYear(firstDayOfWeekLocalDate),
                            BaseConstant.DATE_FORMAT_YYYYMMDD));
                    // 获取去年同期的 Weekly 最后一天
                    long lastDayOfWeekLastYear = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.getSameDateLastYear(currentLocalDate),
                            BaseConstant.DATE_FORMAT_YYYYMMDD));
                    // 组装 Weekly Last Year
                    roomRevenueDTOList.add(getStatisticsWeekly(WeeklyMonthlyRevenueRoomRevenueDTO.LY_TYPE,
                            firstDayOfWeekLastYear, lastDayOfWeekLastYear, roomRevenueDailyListLastYear,
                            roomRevenueSegmentListLastYear, roundingMode));
                }
                days++;
            }

            return roomRevenueDTOList;
        }

        /**
         * 生成每日的 Room Revenue Overall
         *
         * @param revenueRoom  Room Revenue Daily
         * @param occRule      入住率计算规则配置 1:用房间数计算 ; 2:用报告中的入住率
         * @param roundingMode 舍入模式
         * @return WeeklyMonthlyRevenueOverallDTO
         * @author zc
         * @date 2024/5/14
         */
        private WeeklyMonthlyRevenueOverallDTO generateDailyOverall(WeeklyMonthlyRevenueRoom revenueRoom, Integer occRule, RoundingMode roundingMode) {
            return beanMapper.map(revenueRoom, WeeklyMonthlyRevenueOverallDTO.class, dto -> {
                dto.setPcOcc(getPcOccPercent(revenueRoom, occRule, roundingMode));
                Integer sellableCapacity = monthlyRevenueCommonService.getSellableCapacity(revenueRoom.getPhysicalCapacity(),
                        revenueRoom.getOoo());
                dto.setSellableCapacity(sellableCapacity);
                dto.setScOcc(monthlyRevenueCommonService.getOcc(revenueRoom.getRoomsSold(), sellableCapacity, roundingMode));
                dto.setRevPar(monthlyRevenueCommonService.getRevPar(revenueRoom.getRoomRevenue(),
                        revenueRoom.getPhysicalCapacity(), roundingMode));
                dto.setRoomLeftToSell(monthlyRevenueCommonService.getRoomLeftToSell(revenueRoom.getPhysicalCapacity(),
                        revenueRoom.getRoomsSold(), revenueRoom.getOoo()));
            });
        }

        /**
         * 计算 SC OCC %
         *
         * <p>PC OCC %：如果配置了房间数，则根据 pc occ 计算规则判断：1.使用配置的房间数计算 pc occ，2.直接使用文件中的 occ 作为 pc occ</>
         *
         * @param revenueRoom  Revenue Room
         * @param occRule      入住率计算规则配置 1:用房间数计算; 2:用报告中的入住率
         * @param roundingMode 舍去模式
         * @return java.math.BigDecimal
         * @author zc
         * @date 2024/5/18
         */
        private BigDecimal getPcOccPercent(WeeklyMonthlyRevenueRoom revenueRoom, Integer occRule, RoundingMode roundingMode) {
            if (Objects.nonNull(occRule) && occRule == RevenueCons.USE_ROOMS_TO_CALCULATE) {
                return monthlyRevenueCommonService.getOcc(revenueRoom.getRoomsSold(),
                        revenueRoom.getPhysicalCapacity(), roundingMode);
            }
            return revenueRoom.getOcc();
        }

        /**
         * 生成每日的 Room Revenue Transient Segment
         *
         * @param revenueSegment Room Revenue Segment
         * @param roundingMode   舍入模式
         * @return WeeklyMonthlyRevenueTransientDTO
         * @author zc
         * @date 2024/5/14
         */
        private WeeklyMonthlyRevenueTransientDTO generateDailyTransient(WeeklyMonthlyRevenueSegment revenueSegment, RoundingMode roundingMode) {
            WeeklyMonthlyRevenueTransientDTO transientDTO = new WeeklyMonthlyRevenueTransientDTO();
            transientDTO.setRevenue(revenueSegment.getTransientRevenue());
            transientDTO.setSold(revenueSegment.getTransientSold());
            transientDTO.setAdr(monthlyRevenueCommonService.getAdr(revenueSegment.getTransientRevenue(),
                    revenueSegment.getTransientSold(), roundingMode));
            transientDTO.setStatus(revenueSegment.getStatus());
            return transientDTO;
        }

        /**
         * 生成每日的 Room Revenue Group Segment
         *
         * @param revenueSegment Room Revenue Segment
         * @param roundingMode   舍入模式
         * @return WeeklyMonthlyRevenueGroupDTO
         * @author zc
         * @date 2024/5/14
         */
        private WeeklyMonthlyRevenueGroupDTO generateDailyGroup(WeeklyMonthlyRevenueSegment revenueSegment, RoundingMode roundingMode) {
            WeeklyMonthlyRevenueGroupDTO groupDTO = new WeeklyMonthlyRevenueGroupDTO();
            groupDTO.setContracted(revenueSegment.getGroupContracted());
            groupDTO.setCancelled(revenueSegment.getGroupCancelled());
            groupDTO.setRevenue(revenueSegment.getGroupRevenue());
            groupDTO.setSold(revenueSegment.getGroupSold());
            groupDTO.setAdr(monthlyRevenueCommonService.getAdr(revenueSegment.getGroupRevenue(),
                    revenueSegment.getGroupSold(), roundingMode));
            groupDTO.setStatus(revenueSegment.getStatus());
            groupDTO.setContractedStatus(revenueSegment.getGroupContractedStatus());
            if (Objects.nonNull(revenueSegment.getGroupCancelled())) {
                groupDTO.setCancelledStatus(MonthlyRevenueCons.HISTORY);
            }
            return groupDTO;
        }

        /**
         * 生成每日的 Room Revenue Contract Segment
         *
         * @param revenueSegment Room Revenue Segment
         * @param roundingMode   舍入模式
         * @return WeeklyMonthlyRevenueContractDTO
         * @author zc
         * @date 2024/5/14
         */
        private WeeklyMonthlyRevenueContractDTO generateDailyContract(WeeklyMonthlyRevenueSegment revenueSegment, RoundingMode roundingMode) {
            WeeklyMonthlyRevenueContractDTO contractDTO = new WeeklyMonthlyRevenueContractDTO();
            contractDTO.setRevenue(revenueSegment.getContractRevenue());
            contractDTO.setSold(revenueSegment.getContractSold());
            contractDTO.setAdr(monthlyRevenueCommonService.getAdr(revenueSegment.getContractRevenue(),
                    revenueSegment.getContractSold(), roundingMode));
            contractDTO.setStatus(revenueSegment.getStatus());
            return contractDTO;
        }

        /**
         * 获取周统计的 Room Revenue Statistics
         *
         * @param type                   数据类型（1:day; 2:week; 3:ly）
         * @param startDate              开始日期(yyyyMMdd)
         * @param endDate                结束日期(yyyyMMdd)
         * @param roomRevenueDailyList   Room Revenue Daily
         * @param roomRevenueSegmentList Room Revenue Segment
         * @param roundingMode           舍入模式
         * @return WeeklyMonthlyRevenueRoomRevenueStatisticsDTO
         * @author zc
         * @date 2024/5/14
         */
        private WeeklyMonthlyRevenueRoomRevenueDTO getStatisticsWeekly(int type, long startDate, long endDate,
                                                                       List<WeeklyMonthlyRevenueRoom> roomRevenueDailyList,
                                                                       List<WeeklyMonthlyRevenueSegment> roomRevenueSegmentList,
                                                                       RoundingMode roundingMode) {
            WeeklyMonthlyRevenueRoomRevenueDTO weeklyRoomRevenueDTO = new WeeklyMonthlyRevenueRoomRevenueDTO();
            weeklyRoomRevenueDTO.setTheDate(startDate);
            weeklyRoomRevenueDTO.setType(type);

            // 组装 Weekly Statistics Overall
            List<WeeklyMonthlyRevenueRoom> filterRoomRevenueDailyList = roomRevenueDailyList.stream().filter(item ->
                    (item.getTheDate() >= startDate) && (item.getTheDate() <= endDate)).toList();
            weeklyRoomRevenueDTO.setOverall(generateStatisticsOverall(filterRoomRevenueDailyList, roundingMode));

            // 组装 Weekly Statistics Segment
            List<WeeklyMonthlyRevenueSegment> filterRoomRevenueSegmentList = roomRevenueSegmentList.stream().filter(item ->
                    (item.getTheDate() >= startDate && (item.getTheDate() <= endDate))).toList();
            weeklyRoomRevenueDTO.setTransients(generateStatisticsTransient(filterRoomRevenueSegmentList, roundingMode));
            weeklyRoomRevenueDTO.setGroup(generateStatisticsGroup(filterRoomRevenueSegmentList, roundingMode));
            weeklyRoomRevenueDTO.setContract(generateStatisticsContract(filterRoomRevenueSegmentList, roundingMode));

            return weeklyRoomRevenueDTO;
        }

        /**
         * 获取统计的 Room Revenue Statistics
         *
         * @param roomRevenueDailyList   Room Revenue Daily
         * @param roomRevenueSegmentList Room Revenue Segment
         * @param startDate              开始日期(yyyyMMdd)
         * @param endDate                结束日期(yyyyMMdd)
         * @param roundingMode           舍入模式
         * @return WeeklyMonthlyRevenueRoomRevenueStatisticsDTO
         * @author zc
         * @date 2024/5/14
         */
        private WeeklyMonthlyRevenueRoomRevenueStatisticsDTO getStatistics(List<WeeklyMonthlyRevenueRoom> roomRevenueDailyList,
                                                                           List<WeeklyMonthlyRevenueSegment> roomRevenueSegmentList,
                                                                           long startDate, long endDate, RoundingMode roundingMode) {
            WeeklyMonthlyRevenueRoomRevenueStatisticsDTO revenueStatisticsDTO = new WeeklyMonthlyRevenueRoomRevenueStatisticsDTO();
            revenueStatisticsDTO.setStartDate(startDate);
            revenueStatisticsDTO.setEndDate(endDate);

            // 组装 Statistics Overall
            List<WeeklyMonthlyRevenueRoom> filterRoomRevenueDailyList = roomRevenueDailyList.stream()
                    .filter(roomRevenueDaily -> (roomRevenueDaily.getTheDate() >= startDate)
                            && (roomRevenueDaily.getTheDate() <= endDate)).toList();
            revenueStatisticsDTO.setOverall(generateStatisticsOverall(filterRoomRevenueDailyList, roundingMode));

            // 组装 Statistics Segment
            List<WeeklyMonthlyRevenueSegment> filterRoomRevenueSegmentList = roomRevenueSegmentList.stream()
                    .filter(roomRevenueSegment -> (roomRevenueSegment.getTheDate() >= startDate
                            && (roomRevenueSegment.getTheDate() <= endDate))).toList();
            revenueStatisticsDTO.setTransients(generateStatisticsTransient(filterRoomRevenueSegmentList, roundingMode));
            revenueStatisticsDTO.setGroup(generateStatisticsGroup(filterRoomRevenueSegmentList, roundingMode));
            revenueStatisticsDTO.setContract(generateStatisticsContract(filterRoomRevenueSegmentList, roundingMode));

            return revenueStatisticsDTO;
        }

        /**
         * 生成统计的 Room Revenue Overall
         *
         * @param roomRevenueDailyList Room Revenue Daily 集合
         * @param roundingMode         舍入模式
         * @return WeeklyMonthlyRevenueOverallDTO
         * @author zc
         * @date 2024/5/14
         */
        private WeeklyMonthlyRevenueOverallDTO generateStatisticsOverall(List<WeeklyMonthlyRevenueRoom> roomRevenueDailyList, RoundingMode roundingMode) {
            WeeklyMonthlyRevenueOverallDTO overallDTO = new WeeklyMonthlyRevenueOverallDTO();

            Integer roomsSold = roomRevenueDailyList.stream().map(WeeklyMonthlyRevenueRoom::getRoomsSold)
                    .filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
            overallDTO.setRoomsSold(roomsSold);

            BigDecimal roomRevenue = roomRevenueDailyList.stream().map(WeeklyMonthlyRevenueRoom::getRoomRevenue)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
            overallDTO.setRoomRevenue(roomRevenue);

            Integer physicalCapacity = roomRevenueDailyList.stream().map(WeeklyMonthlyRevenueRoom::getPhysicalCapacity)
                    .filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
            overallDTO.setPhysicalCapacity(physicalCapacity);

            Integer ooo = roomRevenueDailyList.stream().map(WeeklyMonthlyRevenueRoom::getOoo)
                    .filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
            overallDTO.setOoo(ooo);

            Integer complimentaryRooms = roomRevenueDailyList.stream().map(WeeklyMonthlyRevenueRoom::getComplimentaryRooms)
                    .filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
            overallDTO.setComplimentaryRooms(complimentaryRooms);

            Integer noShowRooms = roomRevenueDailyList.stream().map(WeeklyMonthlyRevenueRoom::getNoShowRooms)
                    .filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
            overallDTO.setNoShowRooms(noShowRooms);

            List<BigDecimal> bfrNotNullList = roomRevenueDailyList.stream().map(WeeklyMonthlyRevenueRoom::getBfr)
                    .filter(Objects::nonNull).toList();
            BigDecimal bfrTotal = bfrNotNullList.stream().reduce(BigDecimal::add).orElse(null);
            BigDecimal bfrAverage = bfrNotNullList.isEmpty() ? null :
                    bfrTotal.divide(new BigDecimal(bfrNotNullList.size()), BaseConstant.DATA_PRECISION, roundingMode);
            overallDTO.setBfr(bfrAverage);

            overallDTO.setSellableCapacity(monthlyRevenueCommonService.getSellableCapacity(physicalCapacity, ooo));
            overallDTO.setPcOcc(monthlyRevenueCommonService.getOcc(roomsSold, physicalCapacity, roundingMode));
            overallDTO.setScOcc(monthlyRevenueCommonService.getOcc(roomsSold, overallDTO.getSellableCapacity(), roundingMode));
            overallDTO.setAdr(monthlyRevenueCommonService.getAdr(roomRevenue, roomsSold, roundingMode));
            overallDTO.setRevPar(monthlyRevenueCommonService.getRevPar(roomRevenue, physicalCapacity, roundingMode));
            overallDTO.setRoomLeftToSell(monthlyRevenueCommonService.getRoomLeftToSell(physicalCapacity, roomsSold, ooo));

            return overallDTO;
        }

        /**
         * 生成统计的 Room Revenue Transient Segment
         *
         * @param roomRevenueSegmentList Room Revenue Segment 集合
         * @param roundingMode           舍入模式
         * @return WeeklyMonthlyRevenueTransientDTO
         * @author zc
         * @date 2024/5/14
         */
        private WeeklyMonthlyRevenueTransientDTO generateStatisticsTransient(List<WeeklyMonthlyRevenueSegment> roomRevenueSegmentList,
                                                                             RoundingMode roundingMode) {
            WeeklyMonthlyRevenueTransientDTO transientDTO = new WeeklyMonthlyRevenueTransientDTO();
            Integer sold = roomRevenueSegmentList.stream().map(WeeklyMonthlyRevenueSegment::getTransientSold)
                    .filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
            transientDTO.setSold(sold);
            BigDecimal revenue = roomRevenueSegmentList.stream().map(WeeklyMonthlyRevenueSegment::getTransientRevenue)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
            transientDTO.setRevenue(revenue);
            transientDTO.setAdr(monthlyRevenueCommonService.getAdr(revenue, sold, roundingMode));

            return transientDTO;
        }

        /**
         * 生成统计的 Room Revenue Group Segment
         *
         * @param roomRevenueSegmentList Room Revenue Segment 集合
         * @param roundingMode           舍入模式
         * @return WeeklyMonthlyRevenueGroupDTO
         * @author zc
         * @date 2024/5/14
         */
        private WeeklyMonthlyRevenueGroupDTO generateStatisticsGroup(List<WeeklyMonthlyRevenueSegment> roomRevenueSegmentList,
                                                                     RoundingMode roundingMode) {
            WeeklyMonthlyRevenueGroupDTO groupDTO = new WeeklyMonthlyRevenueGroupDTO();
            Integer contracted = roomRevenueSegmentList.stream().map(WeeklyMonthlyRevenueSegment::getGroupContracted)
                    .filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
            groupDTO.setContracted(contracted);

            Integer cancelled = roomRevenueSegmentList.stream().map(WeeklyMonthlyRevenueSegment::getGroupCancelled)
                    .filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
            groupDTO.setCancelled(cancelled);

            Integer sold = roomRevenueSegmentList.stream().map(WeeklyMonthlyRevenueSegment::getGroupSold)
                    .filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
            groupDTO.setSold(sold);

            BigDecimal revenue = roomRevenueSegmentList.stream().map(WeeklyMonthlyRevenueSegment::getGroupRevenue)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
            groupDTO.setRevenue(revenue);
            groupDTO.setAdr(monthlyRevenueCommonService.getAdr(revenue, sold, roundingMode));

            return groupDTO;
        }

        /**
         * 生成统计的 Room Revenue Contract Segment
         *
         * @param roomRevenueSegmentList Room Revenue Segment 集合
         * @param roundingMode           舍入模式
         * @return WeeklyMonthlyRevenueContractDTO
         * @author zc
         * @date 2024/5/14
         */
        private WeeklyMonthlyRevenueContractDTO generateStatisticsContract(List<WeeklyMonthlyRevenueSegment> roomRevenueSegmentList,
                                                                           RoundingMode roundingMode) {
            WeeklyMonthlyRevenueContractDTO contractDTO = new WeeklyMonthlyRevenueContractDTO();
            Integer sold = roomRevenueSegmentList.stream().map(WeeklyMonthlyRevenueSegment::getContractSold)
                    .filter(Objects::nonNull).reduce(Integer::sum).orElse(null);
            contractDTO.setSold(sold);
            BigDecimal revenue = roomRevenueSegmentList.stream().map(WeeklyMonthlyRevenueSegment::getContractRevenue)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null);
            contractDTO.setRevenue(revenue);
            contractDTO.setAdr(monthlyRevenueCommonService.getAdr(revenue, sold, roundingMode));

            return contractDTO;
        }

        /**
         * 获取指定日期范围的 Room Revenue
         *
         * @param propertyId 资产ID
         * @param startDate  开始日期（yyyyMMdd）
         * @param endDate    结束日期（yyyyMMdd）
         * @return List<WeeklyMonthlyRevenueRoom>
         * @author zc
         * @date 2024/5/14
         */
        @Override
        public List<WeeklyMonthlyRevenueRoom> listRoomRevenueDaily(int propertyId, long startDate, long endDate) {
            // 查询 BAR 房价
            Setting setting = settingService.getByPropertyId(propertyId);
            Map<Long, LatestHotelPriceDTO> theDate2HotelPrice = Collections.emptyMap();
            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())) {
                    // 查询资产自身酒店
                    int hotelId = propertyHotelService.getPropertySelfHotel(propertyId).getHotelId();
                    // 构造查询条件
                    LatestHotelPriceQuery latestHotelBARPriceQuery = LatestHotelPriceQuery.builder()
                            .fromDate(startDate).toDate(endDate).datasourceId(null).platformId(null)
                            .roomIds(Collections.emptyList()).build();
                    // 获取指定平台的最低 BAR 房价
                    theDate2HotelPrice = latestBARRateService.listLowestBARHotelPrice(Collections.singletonList(hotelId), setting.getCurrencyId(),
                                    setting.getContainTaxFlag(), latestHotelBARPriceQuery, rateConfigDTOS, setting.getPos()).stream()
                            .collect(Collectors.toMap(item -> {
                                LocalDate localDate = DateTimeUtils.strToLocalDate(item.getDate(), BaseConstant.DATE_FORMAT_YYYY_MM_DD);
                                return Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                            }, Function.identity()));
                }
            }

            Map<Long, WeeklyMonthlyRevenueRoom> theDate2MonthlyRevenue = list(new LambdaQueryWrapper<WeeklyMonthlyRevenueRoom>()
                    .between(WeeklyMonthlyRevenueRoom::getTheDate, startDate, endDate)
                    .eq(WeeklyMonthlyRevenueRoom::getPropertyId, propertyId)).stream()
                    .collect(Collectors.toMap(WeeklyMonthlyRevenueRoom::getTheDate, 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<WeeklyMonthlyRevenueRoom> resultList = Lists.newArrayList();
            for (LocalDate indexLocalDate = startLocalDate; !indexLocalDate.isAfter(endLocalDate); indexLocalDate = indexLocalDate.plusDays(1)) {
                long indexTheDate = Long.parseLong(DateTimeUtils.localDateToStr(indexLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                LatestHotelPriceDTO latestHotelPriceDTO = theDate2HotelPrice.get(indexTheDate);

                WeeklyMonthlyRevenueRoom monthlyRevenueRoom = theDate2MonthlyRevenue.get(indexTheDate);
                if (Objects.isNull(monthlyRevenueRoom)) {
                    monthlyRevenueRoom = new WeeklyMonthlyRevenueRoom();
                    monthlyRevenueRoom.setPropertyId(propertyId);
                    monthlyRevenueRoom.setTheDate(indexTheDate);
                }
                monthlyRevenueRoom.setBfr(Objects.isNull(latestHotelPriceDTO) ? null : latestHotelPriceDTO.getPrice());
                resultList.add(monthlyRevenueRoom);
            }
            return resultList;
        }

        /**
         * 保存 Physical capacity
         *
         * @param propertyId              资产 id
         * @param physicalCapacityDTOList Physical capacity DTO 集合
         * @param operatorId              操作人 id
         * @author zc
         * @date 2024/5/14
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void savePhysicalCapacity(int propertyId, List<WeeklyMonthlyRevenuePhysicalCapacityDTO> physicalCapacityDTOList, int operatorId) {
            if (CollectionUtils.isEmpty(physicalCapacityDTOList)) {
                return;
            }
            List<WeeklyMonthlyRevenueRoom> revenueRooms = beanMapper.mapList(physicalCapacityDTOList,
                    WeeklyMonthlyRevenueRoom.class, (dto, entity) -> {
                        entity.setPropertyId(propertyId);
                        entity.setPcUpdatedBy(operatorId);
                        entity.setPcUpdatedTime(DateTimeUtils.nowTimestamp());
                    });
            // 按唯一索引字段排序，避免死锁
            revenueRooms.sort(Comparator.comparing(WeeklyMonthlyRevenueRoom::getPropertyId)
                    .thenComparing(WeeklyMonthlyRevenueRoom::getTheDate));

            baseMapper.insertOrUpdatePhysicalCapacityBatchByUnique(revenueRooms);
        }

        /**
         * 保存 Overall
         *
         * @param revenueRooms Revenue Room 集合
         * @author zc
         * @date 2024/5/27
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void saveOverallBatch(List<WeeklyMonthlyRevenueRoom> revenueRooms) {
            if (CollectionUtils.isEmpty(revenueRooms)) {
                return;
            }
            // 按唯一索引字段排序，避免死锁
            revenueRooms.sort(Comparator.comparing(WeeklyMonthlyRevenueRoom::getPropertyId)
                    .thenComparing(WeeklyMonthlyRevenueRoom::getTheDate));

            baseMapper.insertOrUpdateOverallBatchByUnique(revenueRooms);
        }

        /**
         * 获取指定资产指定时间段内的数据
         *
         * @param propertyId 资产 id
         * @param startDate  开始时间（yyyyMMdd）
         * @param endDate    结束时间（yyyyMMdd）
         * @return List<WeeklyMonthlyRevenueRoom>
         * @author zc
         * @date 2025/5/23
         */
        @Override
        public List<WeeklyMonthlyRevenueRoom> listByDateRange(int propertyId, long startDate, long endDate) {
            return list(new LambdaQueryWrapper<WeeklyMonthlyRevenueRoom>()
                    .between(WeeklyMonthlyRevenueRoom::getTheDate, startDate, endDate)
                    .eq(WeeklyMonthlyRevenueRoom::getPropertyId, propertyId));
        }

        /**
         * 生成导出报告的上下文
         *
         * @param propertyDTO 资产
         * @param yearMonth   年月（yyyyMM）
         * @param loginUserId 登录用户 id
         * @return HashMap<String, Object>
         * @author zc
         * @date 2025/10/21
         */
        @Override
        public HashMap<String, Object> generateContext(PropertyDTO propertyDTO, long yearMonth, int loginUserId) {
            YearMonth targetYearMonth = YearMonthUtils.getYearMonth(String.valueOf(yearMonth));
            LocalDate startLocalDate = targetYearMonth.atDay(1);
            // 由于当天还未结束，需要往前推一天作为 MTD 的结束日期
            LocalDate yesterdayLocalDate = LocalDate.now(ZoneId.of(propertyDTO.getTimezone())).minusDays(1);
            LocalDate endLocalDate = targetYearMonth.atEndOfMonth();

            long startDate = Long.parseLong(DateTimeUtils.localDateToStr(startLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            long endDate = Long.parseLong(DateTimeUtils.localDateToStr(endLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
            // 获取当前月范围内的 Room Revenue Daily
            List<WeeklyMonthlyRevenueRoom> overallList = listRoomRevenueDaily(propertyDTO.getId(),
                    startDate, endDate);
            // 获取当前月范围内的 Room Revenue Segment
            List<WeeklyMonthlyRevenueSegment> segmentList = revenueSegmentService.listByDateRange(propertyDTO.getId(),
                    startDate, endDate);

            long lyStartDate = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.getSameDateLastYear(startLocalDate),
                    BaseConstant.DATE_FORMAT_YYYYMMDD));
            long lyEndDate = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.getSameDateLastYear(endLocalDate),
                    BaseConstant.DATE_FORMAT_YYYYMMDD));
            // 获取去年同期日期范围内的 Room Revenue Daily
            List<WeeklyMonthlyRevenueRoom> lyOverallList = listRoomRevenueDaily(propertyDTO.getId(),
                    lyStartDate, lyEndDate);
            // 获取去年同期日期范围内的 Room Revenue Segment
            List<WeeklyMonthlyRevenueSegment> lySegmentList = revenueSegmentService.listByDateRange(propertyDTO.getId(),
                    lyStartDate, lyEndDate);

            HashMap<String, Object> data = new HashMap<>();
            data.put("propertyName", propertyDTO.getName());

            String shortDateFormat = propertyDTO.getInternationalization().getDateTime().getShortDate();
            data.put("yearMonth", InternationalizationUtils.formatYearMonth((int) yearMonth, shortDateFormat, propertyDTO.getLanguage()));

            WeeklyMonthlyRevenueRoomType roomType = revenueRoomTypeService.getByMonth(propertyDTO.getId(), yearMonth);
            if (Objects.nonNull(roomType)) {
                data.put("roomType", roomType.getRoomType());
            }

            MonthlyRevenueExportVO otbTotalVO = getMtdVO(propertyDTO, overallList, segmentList, startDate, endDate);
            data.put("total", otbTotalVO);

            // 若目标年月大于当前年月，则 MTD、LY 的数值设为空
            if (targetYearMonth.isAfter(YearMonth.from(yesterdayLocalDate))) {
                data.put("mtd", new MonthlyRevenueExportVO());
                data.put("ly", new MonthlyRevenueExportVO());
            } else if (targetYearMonth.isBefore(YearMonth.from(yesterdayLocalDate))) {
                // 若目标年月小于当前年月，则 MTD、LY 的数值为整月
                data.put("mtd", otbTotalVO);
                data.put("ly", getMtdVO(propertyDTO, lyOverallList, lySegmentList, lyStartDate, lyEndDate));
            } else {
                long yesterdayDate = Long.parseLong(DateTimeUtils.localDateToStr(yesterdayLocalDate, BaseConstant.DATE_FORMAT_YYYYMMDD));
                data.put("mtd", getMtdVO(propertyDTO, overallList, segmentList, startDate, yesterdayDate));
                long lyYesterdayDate = Long.parseLong(DateTimeUtils.localDateToStr(DateTimeUtils.getSameDateLastYear(yesterdayLocalDate),
                        BaseConstant.DATE_FORMAT_YYYYMMDD));
                data.put("ly", getMtdVO(propertyDTO, lyOverallList, lySegmentList, lyStartDate, lyYesterdayDate));
            }
            data.put("budget", getBudgetVO(propertyDTO, yearMonth));
            data.put("dataList", listRoomRevenueVO(propertyDTO, overallList, segmentList, startLocalDate, endLocalDate));
            data.put("dRevBudget", getDailyRevenueBudgetVO(propertyDTO, targetYearMonth));
            data.put("dRevLy", getDailyRevenueLyVO(propertyDTO, targetYearMonth));

            return data;
        }

        /**
         * 格式化 VO 的值
         *
         * @param value           值
         * @param unit            单位
         * @param amount          是否金额
         * @param calculationRule 计算规则
         * @return String
         * @author zc
         * @date 2025/10/21
         */
        private String formatVoValue(Object value, String unit, boolean amount, String calculationRule) {
            String valueStr;
            if (amount) {
                valueStr = InternationalizationUtils.formatAmountViewValue(value, calculationRule, unit);
            } else {
                valueStr = InternationalizationUtils.formatViewValue(value,
                        PrecisionCons.MONTHLY_REVENUE_PRECISION, calculationRule, unit);
            }
            return StringUtils.isBlank(valueStr) ? "--" : valueStr;
        }

        /**
         * 获取 MTD VO
         *
         * @param propertyDTO 资产
         * @param overallList Room Revenue Daily
         * @param segmentList Room Revenue Segment
         * @param startDate   开始时间（yyyyMMdd）
         * @param endDate     结束时间（yyyyMMdd）
         * @return MonthlyRevenueExportVO
         * @author zc
         * @date 2025/10/21
         */
        private MonthlyRevenueExportVO getMtdVO(PropertyDTO propertyDTO, List<WeeklyMonthlyRevenueRoom> overallList,
                                                List<WeeklyMonthlyRevenueSegment> segmentList, long startDate, long endDate) {
            String currency = propertyDTO.getInternationalization().getNumber().getCurrency();
            String calculationRule = propertyDTO.getInternationalization().getNumber().getCalculationRule();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(calculationRule);

            List<WeeklyMonthlyRevenueRoom> effectiveOverallList = overallList.stream().filter(overall ->
                    overall.getTheDate() >= startDate && overall.getTheDate() <= endDate).toList();

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

            String startDateStr = DateTimeUtils.localDateToStr(startLocalDate, BaseConstant.DATE_FORMAT_SLASH_MM_D);
            String endDateStr = DateTimeUtils.localDateToStr(endLocalDate, BaseConstant.DATE_FORMAT_SLASH_MM_D);
            vo.setDateStr(startDateStr + " - " + endDateStr);
            WeeklyMonthlyRevenueOverallDTO overallDTO = generateStatisticsOverall(effectiveOverallList, roundingMode);
            vo.setWeek(String.valueOf(DateTimeUtils.numberOfDays(startLocalDate, endLocalDate) + 1));
            vo.setPcOcc(formatVoValue(overallDTO.getPcOcc(), PERCENT_SYMBOL, false, calculationRule));
            vo.setScOcc(formatVoValue(overallDTO.getScOcc(), PERCENT_SYMBOL, false, calculationRule));
            vo.setRoomsSold(formatVoValue(overallDTO.getRoomsSold(), null, false, calculationRule));
            vo.setAdr(formatVoValue(overallDTO.getAdr(), currency, true, calculationRule));
            vo.setRevPar(formatVoValue(overallDTO.getRevPar(), currency, true, calculationRule));
            vo.setRoomRevenue(formatVoValue(overallDTO.getRoomRevenue(), currency, true, calculationRule));
            vo.setBfr(formatVoValue(overallDTO.getBfr(), currency, true, calculationRule));
            vo.setPhysicalCapacity(formatVoValue(overallDTO.getPhysicalCapacity(), null, false, calculationRule));
            vo.setSellableCapacity(formatVoValue(overallDTO.getSellableCapacity(), null, false, calculationRule));
            vo.setOoo(formatVoValue(overallDTO.getOoo(), null, false, calculationRule));
            vo.setComplimentaryRooms(formatVoValue(overallDTO.getComplimentaryRooms(), null, false, calculationRule));
            vo.setRoomLeftToSell(formatVoValue(overallDTO.getRoomLeftToSell(), null, false, calculationRule));
            vo.setNoShowRooms(formatVoValue(overallDTO.getNoShowRooms(), null, false, calculationRule));

            List<WeeklyMonthlyRevenueSegment> effectiveSegmentList = segmentList.stream().filter(segment ->
                    segment.getTheDate() >= startDate && segment.getTheDate() <= endDate).toList();

            WeeklyMonthlyRevenueTransientDTO transientDTO = generateStatisticsTransient(effectiveSegmentList, roundingMode);
            vo.setTransientSold(formatVoValue(transientDTO.getSold(), null, false, calculationRule));
            vo.setTransientRevenue(formatVoValue(transientDTO.getRevenue(), currency, true, calculationRule));
            vo.setTransientAdr(formatVoValue(transientDTO.getAdr(), currency, true, calculationRule));

            WeeklyMonthlyRevenueGroupDTO groupDTO = generateStatisticsGroup(effectiveSegmentList, roundingMode);
            vo.setGroupSold(formatVoValue(groupDTO.getSold(), null, false, calculationRule));
            vo.setGroupRevenue(formatVoValue(groupDTO.getRevenue(), currency, true, calculationRule));
            vo.setGroupAdr(formatVoValue(groupDTO.getAdr(), currency, true, calculationRule));
            vo.setGroupCancelled(formatVoValue(groupDTO.getCancelled(), null, false, calculationRule));
            vo.setGroupContracted(formatVoValue(groupDTO.getContracted(), null, false, calculationRule));

            WeeklyMonthlyRevenueContractDTO contractDTO = generateStatisticsContract(effectiveSegmentList, roundingMode);
            vo.setContractSold(formatVoValue(contractDTO.getSold(), null, false, calculationRule));
            vo.setContractRevenue(formatVoValue(contractDTO.getRevenue(), currency, true, calculationRule));
            vo.setContractAdr(formatVoValue(contractDTO.getAdr(), currency, true, calculationRule));

            return vo;
        }

        /**
         * 获取预算 VO
         *
         * @param propertyDTO 资产
         * @param yearMonth   年月
         * @return MonthlyRevenueExportVO
         * @author zc
         * @date 2025/10/21
         */
        private MonthlyRevenueExportVO getBudgetVO(PropertyDTO propertyDTO, long yearMonth) {
            String currency = propertyDTO.getInternationalization().getNumber().getCurrency();
            String calculationRule = propertyDTO.getInternationalization().getNumber().getCalculationRule();

            WeeklyMonthlyRevenueBudget budget = revenueBudgetService.getByDate(propertyDTO.getId(), yearMonth);

            MonthlyRevenueExportVO vo = new MonthlyRevenueExportVO();
            int lengthOfMonth = YearMonthUtils.getYearMonth(String.valueOf(yearMonth)).lengthOfMonth();
            vo.setWeek(String.valueOf(lengthOfMonth));
            if (Objects.nonNull(budget)) {
                vo.setPcOcc(formatVoValue(budget.getPcOcc(), PERCENT_SYMBOL, false, calculationRule));
                vo.setScOcc(formatVoValue(budget.getScOcc(), PERCENT_SYMBOL, false, calculationRule));
                vo.setRoomsSold(formatVoValue(budget.getRoomsSold(), null, false, calculationRule));
                vo.setAdr(formatVoValue(budget.getAdr(), currency, true, calculationRule));
                vo.setRevPar(formatVoValue(budget.getRevPar(), currency, true, calculationRule));
                vo.setRoomRevenue(formatVoValue(budget.getRoomRevenue(), currency, true, calculationRule));
                vo.setBfr(formatVoValue(budget.getBfr(), currency, true, calculationRule));
                vo.setPhysicalCapacity(formatVoValue(budget.getPhysicalCapacity(), null, false, calculationRule));
                vo.setSellableCapacity(formatVoValue(budget.getSellableCapacity(), null, false, calculationRule));
                vo.setOoo(formatVoValue(budget.getOoo(), null, false, calculationRule));
                vo.setComplimentaryRooms(formatVoValue(budget.getComplimentaryRooms(), null, false, calculationRule));
                vo.setRoomLeftToSell(formatVoValue(budget.getRoomLeftToSell(), null, false, calculationRule));
                vo.setNoShowRooms(formatVoValue(budget.getNoShowRooms(), null, false, calculationRule));

                vo.setTransientSold(formatVoValue(budget.getTransientSold(), null, false, calculationRule));
                vo.setTransientRevenue(formatVoValue(budget.getTransientRevenue(), currency, true, calculationRule));
                vo.setTransientAdr(formatVoValue(budget.getTransientAdr(), currency, true, calculationRule));

                vo.setGroupSold(formatVoValue(budget.getGroupSold(), null, false, calculationRule));
                vo.setGroupRevenue(formatVoValue(budget.getGroupRevenue(), currency, true, calculationRule));
                vo.setGroupAdr(formatVoValue(budget.getGroupAdr(), currency, true, calculationRule));
                vo.setGroupCancelled(formatVoValue(budget.getGroupCancelled(), null, false, calculationRule));
                vo.setGroupContracted(formatVoValue(budget.getGroupContracted(), null, false, calculationRule));

                vo.setContractSold(formatVoValue(budget.getContractSold(), null, false, calculationRule));
                vo.setContractRevenue(formatVoValue(budget.getContractRevenue(), currency, true, calculationRule));
                vo.setContractAdr(formatVoValue(budget.getContractAdr(), currency, true, calculationRule));
            } else {
                int rooms = roomsConfigInternalService.getRoomsConfig(propertyDTO.getId()).getRooms();
                vo.setPhysicalCapacity(formatVoValue(lengthOfMonth * rooms, null, false, calculationRule));
            }
            return vo;
        }

        /**
         * 获取 DAILY REVENUE BUDGET VO
         *
         * @param propertyDTO     资产
         * @param targetYearMonth 目标年月
         * @return MonthlyRevenueExportVO
         * @author zc
         * @date 2025/10/21
         */
        private MonthlyRevenueExportVO getDailyRevenueBudgetVO(PropertyDTO propertyDTO, YearMonth targetYearMonth) {
            RevenueBudgetOtb budgetOtb = revenueBudgetOtbService.getBudget(propertyDTO.getId(),
                    targetYearMonth.getYear(), targetYearMonth.getMonthValue());
            MonthlyRevenueExportVO budgetVO = new MonthlyRevenueExportVO();
            budgetVO.setWeek(String.valueOf(targetYearMonth.lengthOfMonth()));
            if (Objects.nonNull(budgetOtb)) {
                String currency = propertyDTO.getInternationalization().getNumber().getCurrency();
                String calculationRule = propertyDTO.getInternationalization().getNumber().getCalculationRule();

                budgetVO.setPcOcc(formatVoValue(budgetOtb.getOccPercent(), PERCENT_SYMBOL, false, calculationRule));
                budgetVO.setAdr(formatVoValue(budgetOtb.getAdr(), currency, true, calculationRule));
                budgetVO.setRevPar(formatVoValue(budgetOtb.getRevPar(), currency, true, calculationRule));
                budgetVO.setRoomRevenue(formatVoValue(budgetOtb.getRoomRevenue(), currency, true, calculationRule));
            }
            return budgetVO;
        }

        /**
         * 获取 DAILY REVENUE LY VO
         *
         * @param propertyDTO     资产
         * @param targetYearMonth 目标年月
         * @return MonthlyRevenueExportVO
         * @author zc
         * @date 2025/10/21
         */
        private MonthlyRevenueExportVO getDailyRevenueLyVO(PropertyDTO propertyDTO, YearMonth targetYearMonth) {
            RevenueOtbRevenueLastYear lastYear = revenueOtbRevenueLastYearService.getLastYear(propertyDTO.getId(),
                    targetYearMonth.getYear() - 1, targetYearMonth.getMonthValue());
            MonthlyRevenueExportVO lyVO = new MonthlyRevenueExportVO();
            lyVO.setWeek(String.valueOf(targetYearMonth.lengthOfMonth()));
            if (Objects.nonNull(lastYear)) {
                String currency = propertyDTO.getInternationalization().getNumber().getCurrency();
                String calculationRule = propertyDTO.getInternationalization().getNumber().getCalculationRule();

                lyVO.setPcOcc(formatVoValue(lastYear.getOtbOccPercent(), PERCENT_SYMBOL, false, calculationRule));
                lyVO.setAdr(formatVoValue(lastYear.getOtbAdr(), currency, true, calculationRule));
                lyVO.setRevPar(formatVoValue(lastYear.getOtbRevpar(), currency, true, calculationRule));
                lyVO.setRoomRevenue(formatVoValue(lastYear.getOtbRevenue(), currency, true, calculationRule));
            }
            return lyVO;
        }

        /**
         * 获取每日房间收入 VO
         *
         * @param propertyDTO    资产
         * @param overallList    Room Revenue Daily
         * @param segmentList    Room Revenue Segment
         * @param startLocalDate 开始时间
         * @param endLocalDate   结束时间
         * @return List<MonthlyRevenueExportVO>
         * @author zc
         * @date 2025/10/21
         */
        private List<MonthlyRevenueExportVO> listRoomRevenueVO(PropertyDTO propertyDTO,
                                                               List<WeeklyMonthlyRevenueRoom> overallList,
                                                               List<WeeklyMonthlyRevenueSegment> segmentList,
                                                               LocalDate startLocalDate, LocalDate endLocalDate) {
            String currency = propertyDTO.getInternationalization().getNumber().getCurrency();
            String calculationRule = propertyDTO.getInternationalization().getNumber().getCalculationRule();
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());
            String longDateFormat = propertyDTO.getInternationalization().getDateTime().getLongDate();
            Locale locale = LocaleContextHolder.getLocale();

            Map<Long, WeeklyMonthlyRevenueRoom> date2Overall = overallList.stream()
                    .collect(Collectors.toMap(WeeklyMonthlyRevenueRoom::getTheDate, Function.identity()));

            Map<Long, WeeklyMonthlyRevenueSegment> date2Segment = segmentList.stream()
                    .collect(Collectors.toMap(WeeklyMonthlyRevenueSegment::getTheDate, Function.identity()));

            // 获取房间数配置
            RoomsConfigDTO roomsConfigDTO = roomsConfigInternalService.getRoomsConfig(propertyDTO.getId());

            List<MonthlyRevenueExportVO> voList = Lists.newArrayList();
            for (LocalDate currentLocalDate = startLocalDate; !currentLocalDate.isAfter(endLocalDate); currentLocalDate = currentLocalDate.plusDays(1)) {
                MonthlyRevenueExportVO vo = new MonthlyRevenueExportVO();
                long currentDate = Long.parseLong(DateTimeUtils.localDateToStr(currentLocalDate,
                        BaseConstant.DATE_FORMAT_YYYYMMDD));
                vo.setDateStr(InternationalizationUtils.formatLocalDate(currentLocalDate, longDateFormat, propertyDTO.getLanguage()));
                vo.setWeek(currentLocalDate.getDayOfWeek().getDisplayName(TextStyle.SHORT, locale));

                WeeklyMonthlyRevenueRoom overall = date2Overall.getOrDefault(currentDate, new WeeklyMonthlyRevenueRoom());
                WeeklyMonthlyRevenueOverallDTO overallDTO = generateDailyOverall(overall, roomsConfigDTO.getOccRule(), roundingMode);
                vo.setPcOcc(formatVoValue(overallDTO.getPcOcc(), PERCENT_SYMBOL, false, calculationRule));
                vo.setScOcc(formatVoValue(overallDTO.getScOcc(), PERCENT_SYMBOL, false, calculationRule));
                vo.setRoomsSold(formatVoValue(overallDTO.getRoomsSold(), null, false, calculationRule));
                vo.setAdr(formatVoValue(overallDTO.getAdr(), currency, true, calculationRule));
                vo.setRevPar(formatVoValue(overallDTO.getRevPar(), currency, true, calculationRule));
                vo.setRoomRevenue(formatVoValue(overallDTO.getRoomRevenue(), currency, true, calculationRule));
                vo.setBfr(formatVoValue(overallDTO.getBfr(), currency, true, calculationRule));
                vo.setPhysicalCapacity(formatVoValue(overallDTO.getPhysicalCapacity(), null, false, calculationRule));
                vo.setSellableCapacity(formatVoValue(overallDTO.getSellableCapacity(), null, false, calculationRule));
                vo.setOoo(formatVoValue(overallDTO.getOoo(), null, false, calculationRule));
                vo.setComplimentaryRooms(formatVoValue(overallDTO.getComplimentaryRooms(), null, false, calculationRule));
                vo.setRoomLeftToSell(formatVoValue(overallDTO.getRoomLeftToSell(), null, false, calculationRule));
                vo.setNoShowRooms(formatVoValue(overallDTO.getNoShowRooms(), null, false, calculationRule));
                //设置overall背景色
                if (Objects.nonNull(overallDTO.getStatus())) {
                    if (overallDTO.getStatus() == MonthlyRevenueCons.HISTORY) {
                        vo.setOverallBackground(MonthlyRevenueCons.HISTORY_COLOR);
                    } else {
                        vo.setOverallBackground(MonthlyRevenueCons.FORECAST_COLOR);
                    }
                }

                WeeklyMonthlyRevenueSegment segment = date2Segment.getOrDefault(currentDate, new WeeklyMonthlyRevenueSegment());
                WeeklyMonthlyRevenueTransientDTO transientDTO = generateDailyTransient(segment, roundingMode);
                vo.setTransientSold(formatVoValue(transientDTO.getSold(), null, false, calculationRule));
                vo.setTransientRevenue(formatVoValue(transientDTO.getRevenue(), currency, true, calculationRule));
                vo.setTransientAdr(formatVoValue(transientDTO.getAdr(), currency, true, calculationRule));
                //设置transient背景色
                if (Objects.nonNull(transientDTO.getStatus())) {
                    if (transientDTO.getStatus() == MonthlyRevenueCons.HISTORY) {
                        vo.setTransientsBackground(MonthlyRevenueCons.HISTORY_COLOR);
                    } else {
                        vo.setTransientsBackground(MonthlyRevenueCons.FORECAST_COLOR);
                    }
                }

                WeeklyMonthlyRevenueGroupDTO groupDTO = generateDailyGroup(segment, roundingMode);
                vo.setGroupSold(formatVoValue(groupDTO.getSold(), null, false, calculationRule));
                vo.setGroupRevenue(formatVoValue(groupDTO.getRevenue(), currency, true, calculationRule));
                vo.setGroupAdr(formatVoValue(groupDTO.getAdr(), currency, true, calculationRule));
                vo.setGroupCancelled(formatVoValue(groupDTO.getCancelled(), null, false, calculationRule));
                vo.setGroupContracted(formatVoValue(groupDTO.getContracted(), null, false, calculationRule));
                //设置group背景色
                if (Objects.nonNull(groupDTO.getStatus())) {
                    if (groupDTO.getStatus() == MonthlyRevenueCons.HISTORY) {
                        vo.setGroupBackground(MonthlyRevenueCons.HISTORY_COLOR);
                    } else {
                        vo.setGroupBackground(MonthlyRevenueCons.FORECAST_COLOR);
                    }
                }

                //设置groupContracted背景色
                if (Objects.nonNull(groupDTO.getContractedStatus())) {
                    if (groupDTO.getContractedStatus() == MonthlyRevenueCons.HISTORY) {
                        vo.setGroupContractedBackground(MonthlyRevenueCons.HISTORY_COLOR);
                    } else {
                        vo.setGroupContractedBackground(MonthlyRevenueCons.FORECAST_COLOR);
                    }
                }
                //设置groupCancelled背景色
                if (Objects.nonNull(groupDTO.getCancelledStatus())) {
                    if (groupDTO.getCancelledStatus() == MonthlyRevenueCons.HISTORY) {
                        vo.setGroupCancelledBackground(MonthlyRevenueCons.HISTORY_COLOR);
                    } else {
                        vo.setGroupCancelledBackground(MonthlyRevenueCons.FORECAST_COLOR);
                    }
                }

                WeeklyMonthlyRevenueContractDTO contractDTO = generateDailyContract(segment, roundingMode);
                vo.setContractSold(formatVoValue(contractDTO.getSold(), null, false, calculationRule));
                vo.setContractRevenue(formatVoValue(contractDTO.getRevenue(), currency, true, calculationRule));
                vo.setContractAdr(formatVoValue(contractDTO.getAdr(), currency, true, calculationRule));
                //设置contract背景色
                if (Objects.nonNull(contractDTO.getStatus())) {
                    if (contractDTO.getStatus() == MonthlyRevenueCons.HISTORY) {
                        vo.setContractBackground(MonthlyRevenueCons.HISTORY_COLOR);
                    } else {
                        vo.setContractBackground(MonthlyRevenueCons.FORECAST_COLOR);
                    }
                }
                voList.add(vo);
            }
            return voList;
        }
    }