# RevenueRateReviewTask.java

    package cloud.wconnect.rateshop.businesstask.revenue;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.FunctionCons;
    import cloud.wconnect.commonlib.cons.FunctionPrecisionEnum;
    import cloud.wconnect.commonlib.exception.InternalServerErrorException;
    import cloud.wconnect.commonlib.pojo.dto.InlineResourceDTO;
    import cloud.wconnect.commonlib.pojo.dto.NotifyDTO;
    import cloud.wconnect.commonlib.pojo.dto.NotifyIdentityDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationDateTimeDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.MailInternalService;
    import cloud.wconnect.commonlib.service.NotifyInternalService;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.rateshop.cons.FunctionPointEnum;
    import cloud.wconnect.rateshop.cons.S3BucketFunctionCons;
    import cloud.wconnect.rateshop.cons.rateshop.DatasourceEnum;
    import cloud.wconnect.rateshop.cons.rateshop.PlatformEnum;
    import cloud.wconnect.rateshop.cons.revenue.RevenueCons;
    import cloud.wconnect.rateshop.entity.rateshop.Hotel;
    import cloud.wconnect.rateshop.entity.rateshop.PropertyHotel;
    import cloud.wconnect.rateshop.entity.rateshop.Setting;
    import cloud.wconnect.rateshop.entity.revenue.*;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.hotel.PropertyHotelDTO;
    import cloud.wconnect.rateshop.pojo.dto.revenue.Next14DaysMEPSGroupDTO;
    import cloud.wconnect.rateshop.pojo.dto.revenue.RevenueDTO;
    import cloud.wconnect.rateshop.pojo.dto.revenue.TransientAndGroupDTO;
    import cloud.wconnect.rateshop.pojo.query.revenue.ExportRevenueQuery;
    import cloud.wconnect.rateshop.pojo.query.revenue.HourHotelPriceQuery;
    import cloud.wconnect.rateshop.pojo.query.revenue.RevenueDetailQuery;
    import cloud.wconnect.rateshop.pojo.vo.revenue.*;
    import cloud.wconnect.rateshop.service.AmazonS3Service;
    import cloud.wconnect.rateshop.service.rateshop.IHotelService;
    import cloud.wconnect.rateshop.service.rateshop.IPropertyHotelService;
    import cloud.wconnect.rateshop.service.rateshop.ISettingService;
    import cloud.wconnect.rateshop.service.revenue.*;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.google.common.collect.Lists;
    import gui.ava.html.Html2Image;
    import gui.ava.html.renderer.ImageRenderer;
    import jakarta.annotation.Resource;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Service;
    import org.thymeleaf.TemplateEngine;
    import org.thymeleaf.context.Context;
    import org.xhtmlrenderer.util.XRLog;

    import java.io.File;
    import java.math.BigDecimal;
    import java.time.DayOfWeek;
    import java.time.ZoneId;
    import java.time.ZonedDateTime;
    import java.time.format.DateTimeFormatter;
    import java.time.format.TextStyle;
    import java.util.*;
    import java.util.stream.Collectors;

    /**
     * Task: Revenue Rate Review
     *
     * @author: yyt
     * 2025-02-12
     */
    @Slf4j
    @Service
    public class RevenueRateReviewTask {

        static {
            // 禁用 xhtmlrenderer 的日志输出，避免并发时报NPE
            XRLog.setLoggingEnabled(false);
        }

        @Autowired
        private MailInternalService mailInternalService;
        @Autowired
        private IRevenueEmailConfigService revenueEmailConfigService;
        @Autowired
        private IRevenueEmailTargetService revenueEmailTargetService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private IRevenueRateReviewService revenueRateReviewService;
        @Autowired
        private IRevenueRateReviewNotifyLogService revenueRateReviewNotifyLogService;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private NotifyInternalService notifyInternalService;
        @Resource
        private IRevenueHandlerService revenueHandlerCurrentDataServiceImpl;
        @Autowired
        private IRevenueRoomTypeRoomsService revenueRoomTypeRoomsService;
        @Autowired
        private IPropertyHotelService propertyHotelService;
        @Autowired
        private IHotelService hotelService;
        @Autowired
        private IRevenueDailySummaryService revenueDailySummaryService;
        @Autowired
        private IRevenueService revenueService;
        @Autowired
        private IRevenueImportLogService revenueImportLogService;
        @Autowired
        private IRevenueMepsGroupConfigService revenueMepsGroupConfigService;
        @Autowired
        private IRevenueRateFilterConfigService revenueRateFilterConfigService;
        @Autowired
        private ISettingService settingService;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private AmazonS3Service amazonS3Service;
        @Autowired
        private TemplateEngine templateEngine;
        @Autowired
        private BeanMapper beanMapper;

        @Value("${here.link.revenue}")
        private String hereLink;

        /**
         * Rate Review Notify
         *
         * @author yyt
         * @date 2025/2/12
         **/
        @Async
        public void rateReviewNotify(RevenueSchedulerJob schedulerJob) {
            log.info("RevenueRateReviewTask.rateReviewNotify start...");

            String alertTitle = "[Warning] - Exceptions About Rate Review Notify";
            StringBuilder warningEmail = new StringBuilder("<p>There is something wrong when auto notify rate review.</p>");

            try {
                Integer propertyId = schedulerJob.getPropertyId();

                // 查询邮件配置
                RevenueEmailConfig config = revenueEmailConfigService.getEmailConfig(propertyId, schedulerJob.getCategory(), true);
                if (Objects.isNull(config)) {
                    log.warn("RevenueRateReviewTask... no enabled mail config was found");
                    log.info("RevenueRateReviewTask... end");
                    return;
                }

                // 过滤有效酒店
                PropertyDTO property = propertyI18nRedisService.get(propertyId);
                if (!property.getStatus()) {
                    log.warn("RevenueRateReviewTask... the property is closed, no processing required");
                    log.info("RevenueRateReviewTask... end");
                    return;
                }

                List<RevenueEmailTarget> emailTargets = revenueEmailTargetService.listByConfigId(config.getId());
                if (emailTargets.isEmpty()) {
                    log.warn("RevenueRateReviewTask... email target is not set");
                    log.info("RevenueRateReviewTask... end");
                    return;
                }

                // 发送通知
                doNotify(property, config, emailTargets);

            } catch (Exception e) {
                log.warn("RevenueRateReviewTask... exception occurs:", e);
                warningEmail.append("<p>Exception:").append(e).append("</p>");
                mailInternalService.sendAlertEmail(alertTitle, warningEmail.toString());
            }

            log.info("RevenueRateReviewTask.rateReviewNotify end...");
        }

        /**
         * 发送通知
         *
         * @param propertyDTO  资产
         * @param emailConfig  邮件配置
         * @param emailTargets 邮件通知人集合
         * @author yyt
         * @date 2024/10/4
         */
        private void doNotify(PropertyDTO propertyDTO, RevenueEmailConfig emailConfig, List<RevenueEmailTarget> emailTargets) {
            Integer propertyId = propertyDTO.getId();
            String timezone = propertyDTO.getTimezone();
            // 查询确认记录
            ZonedDateTime currentDate = DateTimeUtils.dateNow(timezone);
            RevenueRateReview revenueRateReview = revenueRateReviewService.get(propertyId, currentDate.toEpochSecond());

            String propertyName = propertyDTO.getName();
            String language = propertyDTO.getLanguage();
            String functionName = i18nMessageUtils.message(FunctionCons.REVENUE, language);
            Locale locale = new Locale(language.split("_")[0], language.split("_")[1]);
            String defaultSubject = revenueEmailConfigService.getDefaultSubjectForRateReviewNotify(propertyDTO, currentDate, locale);

            // 通知记录
            RevenueRateReviewNotifyLog revenueRateReviewNotifyLog = new RevenueRateReviewNotifyLog();
            revenueRateReviewNotifyLog.setPropertyId(propertyId);
            revenueRateReviewNotifyLog.setTheDate(DateTimeUtils.dateNowTimestamp(timezone));
            revenueRateReviewNotifyLog.setConfirmBy(Objects.isNull(revenueRateReview) ? BaseConstant.ZERO : revenueRateReview.getUpdatedBy());
            revenueRateReviewNotifyLog.setConfirmTime(Objects.isNull(revenueRateReview) ? BaseConstant.LONG_ZERO : revenueRateReview.getUpdatedTime());
            revenueRateReviewNotifyLog.setNotifyTime(DateTimeUtils.nowTimestamp());

            // 邮件内容
            String finalHereLink = hereLink + "?property=" + propertyDTO.getAbbr() + "&tenant=" + propertyDTO.getTenantSymbol();
            Context context = new Context();
            context.setVariable("propertyName", propertyName);
            context.setVariable("functionName", functionName);
            context.setVariable("hereLink", finalHereLink);
            String emailContent = EmailUtil.getFullEmailContent(defaultSubject, templateEngine.process("dailyrevenue/rate-review-notify", context), language);

            NotifyDTO notifyDTO = new NotifyDTO();
            notifyDTO.setPropertyId(propertyDTO.getId());
            notifyDTO.setFunctionPoint(FunctionPointEnum.REVENUE_RATE_REVIEW_NOTIFY.getFunctionPoint());
            notifyDTO.setSubject(StringUtils.isNotBlank(emailConfig.getSubject()) ? emailConfig.getSubject() : defaultSubject);
            notifyDTO.setIdentityList(beanMapper.mapList(emailTargets, NotifyIdentityDTO.class));
            notifyDTO.setEmailContent(emailContent);
            notifyDTO.setLineContent(null);
            notifyDTO.setResources(listResource(propertyDTO, revenueRateReview, revenueRateReviewNotifyLog));

            //通知
            notifyInternalService.sendNotify(notifyDTO);

            // 保存通知记录
            revenueRateReviewNotifyLogService.save(revenueRateReviewNotifyLog);

            log.info("RevenueRateReviewTask... send the email request; property:{}", propertyDTO.getAbbr());
        }

        /**
         * 封装截图信息
         *
         * @param propertyDTO                资产
         * @param revenueRateReview          revenueRateReview
         * @param revenueRateReviewNotifyLog revenueRateReviewNotifyLog
         * @return 截图信息
         * @author yyt
         * @date 2025/2/12
         **/
        private List<InlineResourceDTO> listResource(PropertyDTO propertyDTO, RevenueRateReview revenueRateReview, RevenueRateReviewNotifyLog revenueRateReviewNotifyLog) {
            List<InlineResourceDTO> resourceDTOS = Lists.newArrayListWithExpectedSize(2);

            Integer propertyId = propertyDTO.getId();
            RevenueRateFilterConfig rateFilterConfig = revenueRateFilterConfigService.get(propertyId);
            Setting setting = settingService.getByPropertyId(propertyId);

            // Daily Revenue 截图
            resourceDTOS.add(packageDailyRevenueImg(propertyDTO, revenueRateReview, setting, rateFilterConfig));

            // Daily Summary 截图
            resourceDTOS.add(packageDailySummaryImg(propertyDTO, revenueRateReview, setting, rateFilterConfig, revenueRateReviewNotifyLog));

            return resourceDTOS;
        }

        /**
         * 封装 Daily Revenue 截图
         *
         * @param propertyDTO       资产
         * @param revenueRateReview revenueRateReview
         * @param setting           资产配置
         * @param rateFilterConfig  房价筛选配置
         * @return Daily Revenue 截图
         * @author yyt
         * @date 2025/2/12
         **/
        private InlineResourceDTO packageDailyRevenueImg(PropertyDTO propertyDTO, RevenueRateReview revenueRateReview, Setting setting, RevenueRateFilterConfig rateFilterConfig) {
            InlineResourceDTO inlineResourceDTO = new InlineResourceDTO();
            inlineResourceDTO.setCid("dailyRevenueImg");
            inlineResourceDTO.setBucket(S3BucketFunctionCons.REVENUE_IMPORT_RECORD);
            inlineResourceDTO.setKey(generateImgKey(propertyDTO, "daily-revenue", packageDailyRevenueTemplate(propertyDTO, revenueRateReview, setting, rateFilterConfig)));
            return inlineResourceDTO;
        }

        /**
         * 封装 daily revenue 模版
         *
         * @param propertyDTO       资产
         * @param revenueRateReview rate review 确认记录
         * @param setting           资产配置
         * @param rateFilterConfig  房价筛选配置
         * @return daily revenue 模版
         * @author yyt
         * @date 2025/3/14
         **/
        private String packageDailyRevenueTemplate(PropertyDTO propertyDTO, RevenueRateReview revenueRateReview, Setting setting, RevenueRateFilterConfig rateFilterConfig) {
            String timezone = propertyDTO.getTimezone();
            String language = propertyDTO.getLanguage();
            InternationalizationDTO internationalization = propertyDTO.getInternationalization();
            InternationalizationDateTimeDTO dateTime = internationalization.getDateTime();
            String longDate = dateTime.getLongDate();
            String hourClock = dateTime.getHourClock();

            // 去年同期的查询条件-数据源
            Integer datasourceIdBySameTimeLastYear = revenueRateFilterConfigService.checkDatasourceIdLastYearEffectiveForNotify(setting, rateFilterConfig) ? rateFilterConfig.getDatasourceIdLastYear() : null;
            // 去年同期的查询条件-平台
            Integer platformIdBySameTimeLastYear = revenueRateFilterConfigService.checkPlatformIdLastYearEffectiveForNotify(setting, rateFilterConfig) ? rateFilterConfig.getPlatformIdLastYear() : null;
            // 未来14天标准房价的查询条件-数据源
            Integer datasourceIdByNext14DaysBAR = revenueRateFilterConfigService.checkDatasourceIdNextDayBarEffectiveForNotify(setting, rateFilterConfig) ? rateFilterConfig.getDatasourceIdNextDayBar() : null;
            // 未来14天标准房价的查询条件-平台
            Integer platformIdByNext14DaysBAR = revenueRateFilterConfigService.checkPlatformIdNextDayBarEffectiveForNotify(setting, rateFilterConfig) ? rateFilterConfig.getPlatformIdNextDayBar() : null;
            // 未来14天最低房价的查询条件-数据源
            Integer datasourceIdByNext14DaysEBR = revenueRateFilterConfigService.checkDatasourceIdNextDayEbrEffectiveForNotify(setting, rateFilterConfig) ? rateFilterConfig.getDatasourceIdNextDayEbr() : null;
            // 未来14天最低房价的查询条件-平台
            Integer platformIdByNext14DaysEBR = revenueRateFilterConfigService.checkPlatformIdNextDayEbrEffectiveForNotify(setting, rateFilterConfig) ? rateFilterConfig.getPlatformIdNextDayEbr() : null;

            Long theDate = Objects.isNull(revenueRateReview) ? DateTimeUtils.dateNowTimestamp(timezone) : revenueRateReview.getTheDate();
            RevenueDTO detail = revenueHandlerCurrentDataServiceImpl.getDetail(propertyDTO, theDate,
                    new RevenueDetailQuery(datasourceIdBySameTimeLastYear, platformIdBySameTimeLastYear));

            Context context = new Context();
            context.setVariable("propertyId", propertyDTO.getId());
            String currentDate = InternationalizationUtils.formatDateTime(theDate, internationalization.getDateTime().getLongDate(), null,
                    timezone, language);
            context.setVariable("currentDate", currentDate);
            // 最新 报告导入记录 时间
            RevenueImportLog lastImportLog = revenueImportLogService.getOne(new QueryWrapper<RevenueImportLog>().lambda().orderByDesc(RevenueImportLog::getId)
                    .last("limit 1"));
            context.setVariable("reportUpdateTime", Objects.isNull(lastImportLog) ? "--" : InternationalizationUtils.formatDateTime(lastImportLog.getCreatedTime(), longDate, hourClock, timezone, language));

            // OTB Revenue
            context.setVariable("otb", revenueService.packageOtbRevenue(detail.getOtb(), internationalization));
            // Budget
            context.setVariable("budget", revenueService.packageBudget(detail.getBudget(), internationalization));
            // Last Year OTB Revenue
            context.setVariable("lastYearOtb", revenueService.packageLastYearOtb(detail.getLastYearOtb(), internationalization));
            // Customers
            context.setVariable("customer", revenueService.packageCustomer(detail.getCustomers(), internationalization));

            // Segment
            Map<String, Object> result = revenueService.packageSegment(detail.getSegments(), internationalization);
            context.setVariable("segments", result.get(RevenueCons.SEGMENT));
            context.setVariable("totalRevenue", result.get(RevenueCons.TOTAL_REVENUE));
            context.setVariable("totalBudget", result.get(RevenueCons.TOTAL_BUDGET));

            // Next 14 days + Same Time Last Year
            // 所有酒店
            List<PropertyHotelDTO> propertyHotelDTOs = propertyHotelService.listSortDTOByPropertyId(propertyDTO.getId());
            context.setVariable("next14days", packageNext14days(propertyDTO, theDate, detail, internationalization,
                   datasourceIdBySameTimeLastYear, platformIdBySameTimeLastYear,  datasourceIdByNext14DaysBAR, platformIdByNext14DaysBAR,
                    datasourceIdByNext14DaysEBR, platformIdByNext14DaysEBR, propertyHotelDTOs));

            context.setVariable("hasMepsConfig", revenueMepsGroupConfigService.hasConfig(propertyDTO.getId()));

            // Detailed Availability
            context.setVariable("detailedAvailability", packageDetailedAvailability(propertyDTO, theDate, internationalization));

            // 设置数据源和平台翻译内容
            setRateSourceAndPlatform(context, propertyDTO, datasourceIdBySameTimeLastYear, platformIdBySameTimeLastYear,
                    datasourceIdByNext14DaysBAR, platformIdByNext14DaysBAR, datasourceIdByNext14DaysEBR, platformIdByNext14DaysEBR);

            return templateEngine.process("dailyrevenue/daily-revenue_" + language, context);
        }

        /**
         * 设置数据源和平台
         *
         * @param context                        上下文
         * @param propertyDTO                    资产
         * @param datasourceIdBySameTimeLastYear 历史年同期数据源
         * @param platformIdBySameTimeLastYear   历史同期平台
         * @param datasourceIdByNext14DaysBAR    未来14天标准房价的数据源
         * @param platformIdByNext14DaysBAR      未来14天标准房价的平台
         * @param datasourceIdByNext14DaysEBR    未来14天最低房价的数据源
         * @param platformIdByNext14DaysEBR      未来14天最低房价的平台
         * @author yyt
         * @date 2025/8/28
         **/
        private void setRateSourceAndPlatform(Context context, PropertyDTO propertyDTO, Integer datasourceIdBySameTimeLastYear, Integer platformIdBySameTimeLastYear,
                                                   Integer datasourceIdByNext14DaysBAR, Integer platformIdByNext14DaysBAR, Integer datasourceIdByNext14DaysEBR, Integer platformIdByNext14DaysEBR) {
            //当前上下文语言
            String language = propertyDTO.getLanguage();
            Locale currentThreadLocale = new Locale(language.split("_")[0], language.split("_")[1]);
            String i18nAllDatasource = i18nMessageUtils.message(RevenueCons.I18N_ALL_DATASOURCE, currentThreadLocale);
            String i18nAllPlatform = i18nMessageUtils.message(RevenueCons.I18N_ALL_PLATFORM, currentThreadLocale);

            if (Objects.isNull(datasourceIdBySameTimeLastYear)) {
                context.setVariable("datasourceBySameTimeLastYear", i18nAllDatasource);
            } else {
                context.setVariable("datasourceBySameTimeLastYear",
                        i18nMessageUtils.message(DatasourceEnum.getI18nCode(datasourceIdBySameTimeLastYear), currentThreadLocale));
            }
            if (Objects.isNull(platformIdBySameTimeLastYear)) {
                context.setVariable("platformBySameTimeLastYear", i18nAllPlatform);
            } else {
                context.setVariable("platformBySameTimeLastYear",
                        i18nMessageUtils.message(PlatformEnum.getI18nCode(platformIdBySameTimeLastYear), currentThreadLocale));
            }
            if (Objects.isNull(datasourceIdByNext14DaysBAR)) {
                context.setVariable("datasourceByNext14DaysBAR", i18nAllDatasource);
            } else {
                context.setVariable("datasourceByNext14DaysBAR",
                        i18nMessageUtils.message(DatasourceEnum.getI18nCode(datasourceIdByNext14DaysBAR), currentThreadLocale));
            }
            if (Objects.isNull(platformIdByNext14DaysBAR)) {
                context.setVariable("platformByNext14DaysBAR", i18nAllPlatform);
            } else {
                context.setVariable("platformByNext14DaysBAR",
                        i18nMessageUtils.message(PlatformEnum.getI18nCode(platformIdByNext14DaysBAR), currentThreadLocale));
            }
            if (Objects.isNull(datasourceIdByNext14DaysEBR)) {
                context.setVariable("datasourceByNext14DaysEBR", i18nAllDatasource);
            } else {
                context.setVariable("datasourceByNext14DaysEBR",
                        i18nMessageUtils.message(DatasourceEnum.getI18nCode(datasourceIdByNext14DaysEBR), currentThreadLocale));
            }
            if (Objects.isNull(platformIdByNext14DaysEBR)) {
                context.setVariable("platformByNext14DaysEBR", i18nAllPlatform);
            } else {
                context.setVariable("platformByNext14DaysEBR",
                        i18nMessageUtils.message(PlatformEnum.getI18nCode(platformIdByNext14DaysEBR), currentThreadLocale));
            }

            // 数据源
            context.setVariable("dataSource", i18nMessageUtils.message(RevenueCons.DATA_SOURCE, currentThreadLocale));
            // 平台
            context.setVariable("platform", i18nMessageUtils.message(RevenueCons.PLATFORM, currentThreadLocale));
        }

        /**
         * 封装 Next 14 days
         *
         * @param propertyDTO          资产
         * @param theDate              指定日期
         * @param detail               revenue 详情
         * @param internationalization 国际化
         * @param datasourceIdBySameTimeLastYear 历史年同期数据源
         * @param platformIdBySameTimeLastYear   历史同期平台
         * @param datasourceIdByNext14DaysBAR    未来14天标准房价的数据源
         * @param platformIdByNext14DaysBAR      未来14天标准房价的平台
         * @param datasourceIdByNext14DaysEBR    未来14天最低房价的数据源
         * @param platformIdByNext14DaysEBR      未来14天最低房价的平台
         * @param propertyHotelDTOs    所有酒店
         * @return Next 14 days
         * @author yyt
         * @date 2025/3/14
         **/
        private Next14daysVO packageNext14days(PropertyDTO propertyDTO, Long theDate, RevenueDTO detail,
                                               InternationalizationDTO internationalization,
                                               Integer datasourceIdBySameTimeLastYear, Integer platformIdBySameTimeLastYear,
                                               Integer datasourceIdByNext14DaysBAR, Integer platformIdByNext14DaysBAR,
                                               Integer datasourceIdByNext14DaysEBR, Integer platformIdByNext14DaysEBR,
                                               List<PropertyHotelDTO> propertyHotelDTOs) {
            ZonedDateTime theDateZoned = DateTimeUtils.toZonedDateTime(theDate, propertyDTO.getTimezone());
            List<Long> next14Days = DateTimeUtils.listNextDays(theDateZoned, 14);
            List<TransientAndGroupVO> transientAndGroupVOs = Lists.newArrayListWithExpectedSize(14);
            Map<Long, TransientAndGroupDTO> transientAndGroupByDate = detail.getTransientAndGroup().stream()
                    .collect(Collectors.toMap(TransientAndGroupDTO::getTargetDate, v -> v));

            Map<Long, Next14DaysMEPSGroupDTO> mepsGroupDTOByDate = revenueMepsGroupConfigService.listNext14DaysMEPSGroup(propertyDTO, theDate)
                    .stream().collect(Collectors.toMap(Next14DaysMEPSGroupDTO::getDate, v -> v));

            ExportRevenueQuery exportRevenueQuery = new ExportRevenueQuery();
            exportRevenueQuery.setDatasourceIdBySameTimeLastYear(datasourceIdBySameTimeLastYear);
            exportRevenueQuery.setPlatformIdBySameTimeLastYear(platformIdBySameTimeLastYear);

            exportRevenueQuery.setDatasourceIdByNext14DaysBAR(datasourceIdByNext14DaysBAR);
            exportRevenueQuery.setPlatformIdByNext14DaysBAR(platformIdByNext14DaysBAR);

            exportRevenueQuery.setDatasourceIdByNext14DaysEBR(datasourceIdByNext14DaysEBR);
            exportRevenueQuery.setPlatformIdByNext14DaysEBR(platformIdByNext14DaysEBR);

            String timezone = propertyDTO.getTimezone();
            ZoneId zoneId = ZoneId.of(timezone);
            String language = propertyDTO.getLanguage();
            Locale locale = new Locale(language.split("_")[0], language.split("_")[1]);

            List<RateVO> rates = revenueService.getRate(propertyDTO, theDate, exportRevenueQuery, locale);
            Map<Long, List<RatesVO>> ratesByDatee = rates.stream().flatMap(item -> item.getRates().stream())
                    .collect(Collectors.groupingBy(item -> DateTimeUtils.dateStrToEpochSecond(item.getDate(), BaseConstant.DATE_FORMAT_YYYY_MM_DD, timezone)));

            Map<Long, TransientAndGroupLyVO> transientAndGroupLyVOByLastYearTargetDate = revenueService.packageSameTimeLastYear(propertyDTO, theDate, detail, internationalization, locale)
                    .stream().filter(item -> Objects.nonNull(item.getTargetDate())).collect(Collectors.toMap(TransientAndGroupLyVO::getTargetDate, v -> v));

            for (Long day : next14Days) {
                TransientAndGroupVO transientAndGroupVO = new TransientAndGroupVO();
                // 设置资产运营信息
                revenueService.setPropertyOperationInfo(propertyDTO, internationalization, day, transientAndGroupVO, transientAndGroupByDate,
                        mepsGroupDTOByDate.getOrDefault(day, new Next14DaysMEPSGroupDTO()), locale);

                // 设置所有酒店当天的房价
                Map<Integer, RatesVO> rateByHotelId = ratesByDatee.getOrDefault(day, Collections.emptyList()).stream().collect(Collectors.toMap(RatesVO::getHotelId, v -> v));
                transientAndGroupVO.setRates(propertyHotelDTOs.stream().map(item -> rateByHotelId.getOrDefault(item.getHotelId(), new RatesVO())).collect(Collectors.toList()));

                // 去年同期
                ZonedDateTime lastYearDateTime = DateTimeUtils.toZonedDateTime(day, propertyDTO.getTimezone());
                long lastYearTimeStamp = DateTimeUtils.getSameDateLastYear(lastYearDateTime.toLocalDate()).atStartOfDay(zoneId).toEpochSecond();
                TransientAndGroupLyVO transientAndGroupLyVO = transientAndGroupLyVOByLastYearTargetDate.getOrDefault(lastYearTimeStamp, new TransientAndGroupLyVO());

                // 设置颜色
                if (Objects.nonNull(transientAndGroupLyVO.getOccPercent()) && Objects.nonNull(transientAndGroupVO.getOccPercent())) {
                    transientAndGroupVO.setShowColor(transientAndGroupLyVO.getOccPercent().subtract(transientAndGroupVO.getOccPercent()).abs()
                            .compareTo(BigDecimal.valueOf(25)) > 0);
                }

                transientAndGroupVO.setTransientAndGroupLy(transientAndGroupLyVO);

                transientAndGroupVOs.add(transientAndGroupVO);
            }

            Next14daysVO next14daysVO = new Next14daysVO();
            next14daysVO.setTransientAndGroups(transientAndGroupVOs);
            next14daysVO.setHotelNames(propertyHotelDTOs.stream().map(PropertyHotelDTO::getName).toList());
            return next14daysVO;
        }

        /**
         * 封装 detailed availability
         *
         * @param propertyDTO          资产
         * @param theDate              指定日期
         * @param internationalization 国际化
         * @return detailed availability
         * @author yyt
         * @date 2025/3/14
         **/
        private DetailedAvailabilityVO packageDetailedAvailability(PropertyDTO propertyDTO, Long theDate, InternationalizationDTO internationalization) {
            InternationalizationDateTimeDTO dateTime = internationalization.getDateTime();
            String longDate = dateTime.getLongDate();
            String timezone = propertyDTO.getTimezone();
            String language = propertyDTO.getLanguage();
            Locale locale = new Locale(language.split("_")[0], language.split("_")[1]);

            DetailedAvailabilityVO detailedAvailabilityVO = new DetailedAvailabilityVO();

            List<RevenueRoomTypeRooms> roomTypeRooms = revenueRoomTypeRoomsService.list(propertyDTO.getId(), theDate);
            if (CollectionUtils.isEmpty(roomTypeRooms)) return detailedAvailabilityVO;

            List<String> allRoomTypes = roomTypeRooms.stream().map(RevenueRoomTypeRooms::getRoomType).distinct().toList();
            detailedAvailabilityVO.setRoomTypes(allRoomTypes);

            List<DayOfWeek> showColorDayOfWeeks = Lists.newArrayList(DayOfWeek.FRIDAY, DayOfWeek.SATURDAY);

            List<RoomsVO> roomsVOs = Lists.newArrayListWithExpectedSize(14);
            List<Long> next14Days = DateTimeUtils.listNextDays(DateTimeUtils.toZonedDateTime(theDate, timezone), 14);
            Map<Long, Map<String, Integer>> roomsByRoomDate = roomTypeRooms.stream().collect(Collectors.groupingBy(RevenueRoomTypeRooms::getTargetDate,
                    Collectors.toMap(RevenueRoomTypeRooms::getRoomType, RevenueRoomTypeRooms::getRooms)));

            for (Long day : next14Days) {
                RoomsVO roomsVO = new RoomsVO();
                roomsVO.setDate(InternationalizationUtils.formatDateTime(day, longDate, null, timezone, locale));

                DayOfWeek dayOfWeek = DateTimeUtils.toZonedDateTime(day, timezone).getDayOfWeek();
                roomsVO.setWeek(dayOfWeek.getDisplayName(TextStyle.SHORT_STANDALONE, locale));
                roomsVO.setShowColor(showColorDayOfWeeks.contains(dayOfWeek));

                Map<String, Integer> roomsByRoomType = roomsByRoomDate.getOrDefault(day, Collections.emptyMap());
                roomsVO.setRooms(allRoomTypes.stream().map(item -> {
                    Integer rooms = roomsByRoomType.getOrDefault(item, null);
                    return Objects.isNull(rooms) ? "--" : rooms.toString();
                }).toList());
                roomsVOs.add(roomsVO);
            }
            detailedAvailabilityVO.setRooms(roomsVOs);

            return detailedAvailabilityVO;
        }

        /**
         * 封装 Daily Summary 截图
         *
         * @param propertyDTO                资产
         * @param revenueRateReview          revenueRateReview 确认记录
         * @param setting                    资产配置
         * @param rateFilterConfig           房价筛选配置
         * @param revenueRateReviewNotifyLog revenueRateReviewNotifyLog
         * @return Daily Revenue 截图
         * @author yyt
         * @date 2025/2/12
         **/
        private InlineResourceDTO packageDailySummaryImg(PropertyDTO propertyDTO, RevenueRateReview revenueRateReview,
                                                         Setting setting, RevenueRateFilterConfig rateFilterConfig,
                                                         RevenueRateReviewNotifyLog revenueRateReviewNotifyLog) {
            InlineResourceDTO inlineResourceDTO = new InlineResourceDTO();
            inlineResourceDTO.setCid("dailySummaryImg");
            inlineResourceDTO.setBucket(S3BucketFunctionCons.REVENUE_IMPORT_RECORD);
            inlineResourceDTO.setKey(generateImgKey(propertyDTO, "daily-summary",
                    packageDailySummaryTemplate(propertyDTO, revenueRateReview, setting, rateFilterConfig, revenueRateReviewNotifyLog)));
            return inlineResourceDTO;
        }

        /**
         * 封装 Daily Summary Template
         *
         * @param propertyDTO                资产
         * @param revenueRateReview          revenueRateReview 确认记录
         * @param setting                    资产配置
         * @param rateFilterConfig           房价筛选配置
         * @param revenueRateReviewNotifyLog revenueRateReviewNotifyLog
         * @return DailySummaryImgVO
         * @author yyt
         * @date 2025/2/13
         **/
        private String packageDailySummaryTemplate(PropertyDTO propertyDTO, RevenueRateReview revenueRateReview,
                                                   Setting setting, RevenueRateFilterConfig rateFilterConfig,
                                                   RevenueRateReviewNotifyLog revenueRateReviewNotifyLog) {
            InternationalizationDTO internationalization = propertyDTO.getInternationalization();
            InternationalizationNumberDTO number = internationalization.getNumber();
            // 非金额的显示精度
            int precision = FunctionPrecisionEnum.DAILY_REVENUE.getPrecision();
            String calculationRule = number.getCalculationRule();
            String currency = number.getCurrency();
            InternationalizationDateTimeDTO dateTime = internationalization.getDateTime();
            String longDate = dateTime.getLongDate();
            String hourClock = dateTime.getHourClock();

            String timezone = propertyDTO.getTimezone();
            ZonedDateTime now = DateTimeUtils.dateNow(timezone);
            String language = propertyDTO.getLanguage();
            Locale locale = new Locale(language.split("_")[0], language.split("_")[1]);
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_HHMMA).withLocale(locale);

            // Daily Revenue 页面检索日期 - 一天
            ZonedDateTime zonedDateTime = DateTimeUtils.toZonedDateTime(Objects.isNull(revenueRateReview) ? DateTimeUtils.dateNowTimestamp(timezone) : revenueRateReview.getTheDate(), timezone);
            long finalDate = zonedDateTime.withHour(5).toEpochSecond();
            long theDate = zonedDateTime.minusDays(1).toEpochSecond();

            // 查询酒店运营信息（房间出售数量、入住率等）
            Map<Integer, DailySummaryVO> dailySummaryVOByHour = revenueDailySummaryService.listDailySummary(propertyDTO, theDate)
                    .stream().collect(Collectors.toMap(DailySummaryVO::getHour, v -> v));

            // 查询 bar 房价
            PropertyHotel propertySelfHotel = propertyHotelService.getPropertySelfHotel(propertyDTO.getId());
            Hotel hotel = hotelService.getById(propertySelfHotel.getHotelId());
            // 格式：20250212
            Integer theDateInteger = Integer.parseInt(DateTimeUtils.localDateToStr(DateTimeUtils.longToLocalDate(theDate, propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD));
            Integer datasourceIdDailySummary = revenueRateFilterConfigService.checkDatasourceIdDailySummaryEffectiveForNotify(setting, rateFilterConfig) ? rateFilterConfig.getDatasourceIdDailySummary() : null;
            Integer platformIdDailySummary = revenueRateFilterConfigService.checkPlatformIdDailySummaryEffectiveForNotify(setting, rateFilterConfig) ? rateFilterConfig.getPlatformIdDailySummary() : null;
            HourHotelPriceQuery query = new HourHotelPriceQuery(datasourceIdDailySummary, platformIdDailySummary);
            Map<Integer, BigDecimal> priceByHour = revenueDailySummaryService.listPropertySelfHourBARRate(theDateInteger, propertyDTO, hotel, query)
                    .stream().collect(Collectors.toMap(DailySummaryHotelPriceVO::getHour, DailySummaryHotelPriceVO::getPrice));

            // 页面显示小时
            List<DailySummaryImgVO> summaryImgVOS = Lists.newArrayList(6, 10, 15, 19, 23, RevenueDailySummary.FINAL_HOUR).stream()
                    .map(hour -> {
                        DailySummaryImgVO dailySummaryImgVO = new DailySummaryImgVO();
                        dailySummaryImgVO.setHour(hour);
                        DailySummaryVO dailySummaryVO = dailySummaryVOByHour.getOrDefault(hour, new DailySummaryVO());
                        dailySummaryImgVO.setTotalRoomsSoldStr(Objects.isNull(dailySummaryVO.getTotalRoomsSold()) ? "--" : dailySummaryVO.getTotalRoomsSold().toString());
                        dailySummaryImgVO.setTransientPickUpStr(Objects.isNull(dailySummaryVO.getTransientPickUp()) ? "--" : dailySummaryVO.getTransientPickUp().toString());
                        dailySummaryImgVO.setGroupPickUpStr(Objects.isNull(dailySummaryVO.getGroupPickUp()) ? "--" : dailySummaryVO.getGroupPickUp().toString());
                        dailySummaryImgVO.setOccStr(Objects.isNull(dailySummaryVO.getOcc()) ? "--" : InternationalizationUtils.formatViewValue(dailySummaryVO.getOcc(), precision, calculationRule, null) + "%");
                        dailySummaryImgVO.setRoomsAvailableStr(Objects.isNull(dailySummaryVO.getRoomsAvailable()) ? "--" : dailySummaryVO.getRoomsAvailable().toString());
                        dailySummaryImgVO.setOooRoomsStr(Objects.isNull(dailySummaryVO.getOooRooms()) ? "--" : dailySummaryVO.getOooRooms().toString());
                        dailySummaryImgVO.setTaStr(Objects.isNull(dailySummaryVO.getTa()) ? "--" : dailySummaryVO.getTa().toString());
                        dailySummaryImgVO.setAdrStr(Objects.isNull(dailySummaryVO.getAdr()) ? "--" : InternationalizationUtils.formatAmountViewValue(dailySummaryVO.getAdr(), calculationRule, currency));
                        dailySummaryImgVO.setMepsGroupTotalStr(Objects.isNull(dailySummaryVO.getMepsGroupTotal()) ? "--" : dailySummaryVO.getMepsGroupTotal().toString());
                        dailySummaryImgVO.setNoShowStr(Objects.isNull(dailySummaryVO.getNoShow()) ? "--" : dailySummaryVO.getNoShow().toString());
                        dailySummaryImgVO.setCancelledStr(Objects.isNull(dailySummaryVO.getCancelled()) ? "--" : dailySummaryVO.getCancelled().toString());

                        // 为空 时 显示 no data
                        // 为0 时显示 sold out
                        // 不为0 时显示价格
                        String priceStr;
                        BigDecimal price = priceByHour.getOrDefault(hour, null);
                        if (Objects.isNull(price)) {
                            priceStr = i18nMessageUtils.message(RevenueCons.I18N_NO_DATA, language);
                        } else if (price.compareTo(BigDecimal.ZERO) == 0) {
                            priceStr = i18nMessageUtils.message(RevenueCons.I18N_SOLD_OUT, language);
                        } else {
                            priceStr = InternationalizationUtils.formatAmountViewValue(price, calculationRule, currency);
                        }
                        dailySummaryImgVO.setPriceStr(priceStr);

                        if (hour == RevenueDailySummary.FINAL_HOUR) {
                            dailySummaryImgVO.setHourStr(i18nMessageUtils.message(RevenueCons.FINAL, language));
                        } else {
                            dailySummaryImgVO.setHourStr(now.withHour(hour).format(formatter));
                        }

                        return dailySummaryImgVO;
                    }).toList();

            Context context = new Context();
            context.setVariable("propertyName", propertyDTO.getName());
            context.setVariable("date", InternationalizationUtils.formatDateTime(theDate, longDate, null, timezone, language));
            context.setVariable("confirmDate", Objects.isNull(revenueRateReview) ? null : InternationalizationUtils.formatDateTime(revenueRateReview.getUpdatedTime(), longDate, hourClock, timezone, language));
            context.setVariable("notifyDate", InternationalizationUtils.formatDateTime(revenueRateReviewNotifyLog.getNotifyTime(), longDate, hourClock, timezone, language));
            context.setVariable("confirmUserName", Objects.isNull(revenueRateReview) ? null : userInternalService.getOne(propertyDTO.getTenantId(), revenueRateReview.getUpdatedBy()).getName());
            context.setVariable("dataSourceName", Objects.isNull(datasourceIdDailySummary) ? i18nMessageUtils.message(RevenueCons.I18N_ALL_DATASOURCE, language)
                    : i18nMessageUtils.message(DatasourceEnum.getI18nCode(datasourceIdDailySummary), language));
            context.setVariable("platformName", Objects.isNull(platformIdDailySummary) ? i18nMessageUtils.message(RevenueCons.I18N_ALL_PLATFORM, language)
                    : i18nMessageUtils.message(PlatformEnum.getI18nCode(platformIdDailySummary), language));
            // 数据源
            context.setVariable("dataSource", i18nMessageUtils.message(RevenueCons.DATA_SOURCE, locale));
            // 平台
            context.setVariable("platform", i18nMessageUtils.message(RevenueCons.PLATFORM, locale));
            context.setVariable("dailySummaryImgVOs", summaryImgVOS);
            context.setVariable("propertyLogo", propertyDTO.getLogo());
            context.setVariable("hasMepsConfig", revenueMepsGroupConfigService.hasConfig(propertyDTO.getId()));
            context.setVariable("finalDate", InternationalizationUtils.formatDateTime(finalDate, longDate, hourClock, timezone, language));
            return templateEngine.process("dailyrevenue/daily-summary_" + language, context);
        }

        /**
         * 生成图片 s3 key
         *
         * @param propertyDTO  资产
         * @param business     业务
         * @param emailContent 邮件内容
         * @return s3 key
         * @author yyt
         * @date 2025/3/14
         **/
        private String generateImgKey(PropertyDTO propertyDTO, String business, String emailContent) {
            try {
                // 生成图片
                Html2Image html2Image = Html2Image.fromHtml(emailContent);
                ImageRenderer imageRenderer = html2Image.getImageRenderer();
                imageRenderer.setWidth(1800); // 设置更高的宽度

                String imgSuffix = ".png";
                File file = File.createTempFile(business + propertyDTO.getAbbr() + "-", imgSuffix);
                imageRenderer.saveImage(file);

                String key = S3Utils.generateS3Key(propertyDTO.getTenantSymbol(), propertyDTO.getAbbr(), business + imgSuffix, business);
                amazonS3Service.uploadFile(S3BucketFunctionCons.REVENUE_IMPORT_RECORD, file, key);

                file.delete();

                return key;
            } catch (Exception e) {
                throw new InternalServerErrorException("Failed to generate mage file", e);
            }
        }

    }