# NotifyAlertMessageTask.java

    package cloud.wconnect.rateshop.businesstask.rateshop;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.FunctionCons;
    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.InternationalizationDateTimeDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.pojo.dto.line.PublicFlexMsgParm;
    import cloud.wconnect.commonlib.service.NotifyInternalService;
    import cloud.wconnect.commonlib.service.PropertyInternalService;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.rateshop.cons.FunctionPointEnum;
    import cloud.wconnect.rateshop.cons.rateshop.*;
    import cloud.wconnect.rateshop.entity.rateshop.AlertMessage;
    import cloud.wconnect.rateshop.entity.rateshop.RateShopEmailConfig;
    import cloud.wconnect.rateshop.entity.rateshop.RateShopEmailTarget;
    import cloud.wconnect.rateshop.entity.rateshop.Setting;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.alertmessage.notify.*;
    import cloud.wconnect.rateshop.pojo.dto.rateshop.hotel.PropertyHotelDTO;
    import cloud.wconnect.rateshop.service.rateshop.*;
    import com.alibaba.fastjson.JSONObject;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    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.data.redis.core.RedisTemplate;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    import org.thymeleaf.TemplateEngine;
    import org.thymeleaf.context.Context;

    import java.math.BigDecimal;
    import java.text.MessageFormat;
    import java.time.LocalDate;
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.concurrent.TimeUnit;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    import static java.util.stream.Collectors.*;

    /**
     * 邮件通知rate-shop功能的alertMessage
     *
     * @author lichen
     * @date 2023/6/10
     */
    @Slf4j
    @Component
    public class NotifyAlertMessageTask {

        @Autowired
        private ISettingService settingService;

        @Autowired
        private PropertyInternalService propertyInternalService;
        @Autowired
        private IAlertMessageService alertMessageService;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private IPropertyHotelService propertyHotelService;
        @Autowired
        private TemplateEngine templateEngine;
        @Autowired
        private IRateShopEmailConfigService emailConfigService;
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        @Autowired
        private NotifyInternalService notifyInternalService;
        @Autowired
        private LineMessageUtil lineMessageUtil;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private IRateShopEmailTargetService emailTargetService;
        @Value("${here.link.rate-shop}")
        private String hereLink;
        @Autowired
        private BeanMapper beanMapper;
        /**
         * 定时任务key
         */
        private static final String PREFIX_KEY = "wconnect:task:rate-shop:notify-alert-message:";

        /**
         * 邮件通知alertMessage定时任务
         *
         * @return void
         * @author lichen
         * @date 2023/6/10
         */
        @Scheduled(cron = "${schedule.cron.notify-alert-message}", zone = "${default.timezone}")
        public void notifyAlertMessage() {
            String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYMMDDHH));
            if (!preventConcurrency(PREFIX_KEY + currentDate)) {
                return;
            }
            //查询总条数
            long count = settingService.count();
            if (count == 0) return;
            //每页大小
            int pageSize = 30;
            //计算总页数
            int totalPage = (int) Math.ceil((double) count / pageSize);

            for (int i = 1; i <= totalPage; i++) {
                //分页查询property setting信息
                Page<Setting> page = new Page<>(i, pageSize);
                Page<Setting> settingPage = settingService.page(page);
                //获取查询结果
                List<Setting> records = settingPage.getRecords();
                //setting查询结果为空时 停止操作
                if (CollectionUtils.isEmpty(records)) continue;

                //筛选出资产id
                List<Integer> propertyIds = records.stream()
                        .map(Setting::getPropertyId).collect(Collectors.toList());
                //过滤出开通的资产
                List<PropertyDTO> openPropertys = propertyInternalService.getProperty(propertyIds)
                        .stream()
                        .filter(PropertyDTO::getStatus)
                        .collect(Collectors.toList());
                if (CollectionUtils.isEmpty(openPropertys)) continue;
                List<Integer> openPropertyIds = openPropertys.stream().map(PropertyDTO::getId).toList();

                //查询资产设置的邮件配置信息
                List<RateShopEmailConfig> emailConfigs = emailConfigService.listByPropertyIdAndType(openPropertyIds, RateShopNotifyConfigTypeEnum.ALERT_MESSAGE.getType());
                if (CollectionUtils.isEmpty(emailConfigs)) continue;
                Map<Integer, RateShopEmailConfig> propertyId2EmailConfig = emailConfigs.stream()
                        .collect(Collectors.toMap(RateShopEmailConfig::getPropertyId, Function.identity()));
                Map<Integer, List<RateShopEmailTarget>> configId2EmailTargetList = emailTargetService.listByConfigIds(emailConfigs.stream().map(RateShopEmailConfig::getId).toList())
                        .stream().collect(groupingBy(RateShopEmailTarget::getConfigId));

                //查询资产未通知的alertMessage
                List<AlertMessage> notNotifyAlertMessages = alertMessageService.list(new LambdaQueryWrapper<AlertMessage>()
                        .in(AlertMessage::getPropertyId, openPropertyIds)
                        .eq(AlertMessage::getIsSent, false)
                        .orderByDesc(AlertMessage::getId));
                if (CollectionUtils.isEmpty(notNotifyAlertMessages)) continue;
                //List<AlertMessage> -> Map<Integer, List<AlertMessage>>
                Map<Integer, List<AlertMessage>> propertyId2AlertMessageList = notNotifyAlertMessages.stream().collect(groupingBy(AlertMessage::getPropertyId));

                //查询资产配置的hotel信息
                List<PropertyHotelDTO> allPropertyHotels = propertyHotelService.listByPropertyIds(openPropertyIds);
                //List<PropertyHotel> -> Map<Integer, List<PropertyHotel>>
                Map<Integer, List<PropertyHotelDTO>> propertyId2PropertyHotels = allPropertyHotels.stream().collect(groupingBy(PropertyHotelDTO::getPropertyId));

                //通知完成后，更新为已通知
                List<AlertMessage> updateAlertMessages = Lists.newArrayListWithCapacity(notNotifyAlertMessages.size());

                //循环处理
                for (PropertyDTO openProperty : openPropertys) {
                    //资产id
                    Integer openPropertyId = openProperty.getId();
                    //未通知的alertMessage
                    List<AlertMessage> alertMessages = propertyId2AlertMessageList.get(openPropertyId);
                    if (CollectionUtils.isEmpty(alertMessages)) continue;

                    //资产配置的邮件
                    RateShopEmailConfig emailConfig = propertyId2EmailConfig.get(openPropertyId);
                    if (Objects.isNull(emailConfig)) continue;
                    if (!emailConfig.getStatus()) continue;
                    List<RateShopEmailTarget> emailTargets = configId2EmailTargetList.get(emailConfig.getId());
                    if (CollectionUtils.isEmpty(emailTargets)) continue;

                    /**
                     * 判断待通知的message是否大于等于当前日期
                     */
                    //当前日期yyyyMMdd
                    String nowDateStr = DateTimeUtils.dateNowStr(openProperty.getTimezone(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                    long nowDate = Long.parseLong(nowDateStr);

                    List<AlertMessage> waitNotifyAlertMessages = alertMessages.stream()
                            .filter(item -> item.getCheckInDate().compareTo(nowDate) >= 0).collect(toList());
                    if (CollectionUtils.isEmpty(waitNotifyAlertMessages)) continue;

                    //资产配置的酒店集合
                    List<PropertyHotelDTO> propertyHotels = propertyId2PropertyHotels.get(openPropertyId);

                    List<EmailAlertMsgDTO> alertMsgDTOList = convert(waitNotifyAlertMessages, openProperty, propertyHotels);
                    //发送通知
                    doSendNotify(openProperty, emailConfig, emailTargets, alertMsgDTOList);
                    //把待通知的消息数据添加到已通知的集合中
                    updateAlertMessages.addAll(waitNotifyAlertMessages);
                }

                //更新为已通知
                updateAlertMessages.forEach(item -> item.setIsSent(true));
                alertMessageService.updateBatchById(updateAlertMessages);
            }
        }

        /**
         * 转化成DTO
         *
         * @param alertMessages  警报信息集合
         * @param openProperty   资产
         * @param propertyHotels 酒店集合
         * @return void
         * @author lichen
         * @date 2023/6/10
         */
        private List<EmailAlertMsgDTO> convert(List<AlertMessage> alertMessages, PropertyDTO openProperty, List<PropertyHotelDTO> propertyHotels) {
            //获取国际化配置
            String currency = openProperty.getInternationalization().getNumber().getCurrency();
            InternationalizationDateTimeDTO dateTime = openProperty.getInternationalization().getDateTime();
            String hourClock = dateTime.getHourClock();
            String longDate = dateTime.getLongDate();
            String timezone = openProperty.getTimezone();
            String language = openProperty.getLanguage();
            InternationalizationNumberDTO number = openProperty.getInternationalization().getNumber();
            String calculationRule = number.getCalculationRule();

            String ownI18nMsg = i18nMessageUtils.message(HotelTypeEnum.OWN.getI18nCode(), language);
            String competitorI18nMsg = i18nMessageUtils.message(HotelTypeEnum.COMPETITOR.getI18nCode(), language);
            String i18nRisenMsg = i18nMessageUtils.message(MarketDemandTrendEnum.RISEN.getI18nCode(), language);
            String i18nDroppedMsg = i18nMessageUtils.message(MarketDemandTrendEnum.DROPPED.getI18nCode(), language);

            //List<PropertyHotelDTO> -> Map<String, PropertyHotelDTO>
            Map<String, PropertyHotelDTO> symbol2PropertyHotel = propertyHotels.stream().collect(toMap(PropertyHotelDTO::getSymbol, Function.identity()));

            //去重并排序警报消息
            List<AlertMessage> waitAlertMessageList = distinctAlertMessages(alertMessages);

            List<EmailAlertMsgDTO> resultList = Lists.newArrayListWithCapacity(waitAlertMessageList.size());
            for (AlertMessage alertMessage : waitAlertMessageList) {
                //把yyyyMMdd的checkInDate转成LocalDate
                LocalDate checkInDateLocalDate = DateTimeUtils.strToLocalDate(alertMessage.getCheckInDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                //把localDate转成yyyy-MM-dd格式的字符串
                String checkInDateStr = InternationalizationUtils.formatDateTime(DateTimeUtils.localDateToLong(checkInDateLocalDate, timezone),
                        longDate, StringUtils.EMPTY, timezone, language);
                //triggerTime时间戳转化成ZoneDateTime
                String triggerTimeStr = InternationalizationUtils.formatDateTime(alertMessage.getTriggerTime(), longDate, hourClock, timezone, language);

                //构建EmailAlertMsgDTO对象
                EmailAlertMsgDTO emailAlertMsgDTO = EmailAlertMsgDTO.builder()
                        .id(alertMessage.getId())
                        .rule(alertMessage.getRule())
                        .checkInDateStr(checkInDateStr)
                        .triggerTimeStr(triggerTimeStr)
                        .build();

                //规则类型等于最低价格变化、sold out时
                if (alertMessage.getRule() == AlertRuleEnum.PRICE_CHANGE.getCode() || alertMessage.getRule() == AlertRuleEnum.SOLD_OUT.getCode()) {
                    //json字符串的message 转化为 EmailAlertPriceChangeDTO
                    EmailAlertPriceChangeDTO emailAlertPriceChangeDTO = JsonUtils.convertStringToObject(objectMapper, alertMessage.getMessage(), EmailAlertPriceChangeDTO.class);
                    emailAlertPriceChangeDTO.setI18nRateType(i18nMessageUtils.message(RateTypeEnum.getI18nCode(emailAlertPriceChangeDTO.getRateType()), language));
                    emailAlertPriceChangeDTO.setI18nDatasource(i18nMessageUtils.message(DatasourceEnum.getI18nCode(emailAlertPriceChangeDTO.getDatasourceId()), language));
                    emailAlertPriceChangeDTO.setPlatform(i18nMessageUtils.message(PlatformEnum.getI18nCode(emailAlertPriceChangeDTO.getPlatformId()), language));
                    //设置酒店名称和酒店类型
                    PropertyHotelDTO propertyHotelDTO = symbol2PropertyHotel.get(emailAlertPriceChangeDTO.getSymbol());
                    if (Objects.nonNull(propertyHotelDTO)) {
                        if (propertyHotelDTO.getType() == HotelTypeEnum.OWN.getType()) {
                            emailAlertPriceChangeDTO.setHotelType(ownI18nMsg);
                        } else {
                            emailAlertPriceChangeDTO.setHotelType(competitorI18nMsg);
                        }
                        emailAlertPriceChangeDTO.setHotelName(propertyHotelDTO.getName());
                    } else {
                        /**
                         * 当消息未邮件通知时，property更改了hotel配置
                         * 不通知此类消息
                         */
                        continue;
                    }
                    if (StringUtils.isNotEmpty(emailAlertPriceChangeDTO.getRate())) {
                        emailAlertPriceChangeDTO.setRate(InternationalizationUtils.formatAmountViewValue(new BigDecimal(emailAlertPriceChangeDTO.getRate()),
                                calculationRule, currency));
                    }
                    emailAlertMsgDTO.setPriceChangeContent(emailAlertPriceChangeDTO);
                } else if (alertMessage.getRule() == AlertRuleEnum.MARKET_DEMAND.getCode()) {
                    //json字符串的message 转化为 EmailAlertMarketDemandDTO
                    EmailAlertMarketDemandDTO emailAlertMarketDemandDTO = JsonUtils.convertStringToObject(objectMapper, alertMessage.getMessage(), EmailAlertMarketDemandDTO.class);
                    emailAlertMarketDemandDTO.setChangeTypeDesc(emailAlertMarketDemandDTO.getChangeType() == MarketDemandTrendEnum.RISEN.getTrend() ?
                            i18nRisenMsg : i18nDroppedMsg);
                    emailAlertMarketDemandDTO.setChangeAfterLevelDesc(i18nMessageUtils.message(MarketDemandLevelEnum.getI18nCode(emailAlertMarketDemandDTO.getChangeAfterLevel()), language));
                    emailAlertMsgDTO.setMarketDemand(emailAlertMarketDemandDTO);
                } else {
                    //json字符串的message 转化为 EmailIwcLevelChangeDTO
                    EmailIwcLevelChangeDTO emailIwcLevelChangeDTO = JsonUtils.convertStringToObject(objectMapper, alertMessage.getMessage(), EmailIwcLevelChangeDTO.class);
                    emailIwcLevelChangeDTO.setI18nRateType(i18nMessageUtils.message(RateTypeEnum.getI18nCode(emailIwcLevelChangeDTO.getRateType()), language));
                    emailIwcLevelChangeDTO.setI18nDatasource(i18nMessageUtils.message(DatasourceEnum.getI18nCode(emailIwcLevelChangeDTO.getDatasourceId()), language));
                    emailIwcLevelChangeDTO.setPlatform(i18nMessageUtils.message(PlatformEnum.getI18nCode(emailIwcLevelChangeDTO.getPlatformId()), language));
                    emailIwcLevelChangeDTO.setChangeTypeDesc(emailIwcLevelChangeDTO.getChangeType() == MarketDemandTrendEnum.RISEN.getTrend() ?
                            i18nRisenMsg : i18nDroppedMsg);
                    //变化后的级别描述
                    String changeAfterLevelDesc = i18nMessageUtils.message(MarketDemandLevelEnum.getI18nCode(emailIwcLevelChangeDTO.getChangeAfterLevel()), language).replace("%", "");
                    emailIwcLevelChangeDTO.setChangeAfterLevelDesc(changeAfterLevelDesc);
                    emailAlertMsgDTO.setIwcLevelChangeContent(emailIwcLevelChangeDTO);
                }
                resultList.add(emailAlertMsgDTO);
            }
            return resultList.stream().sorted(Comparator.comparing(EmailAlertMsgDTO::getId).reversed()).toList();
        }

        /**
         * 去重警报消息
         *
         * @param alertMessages 警报消息集合
         * @return java.util.List<cloud.wconnect.rateshop.entity.rateshop.AlertMessage>
         * @author lichen
         * @date 2025/4/7
         */
        private List<AlertMessage> distinctAlertMessages(List<AlertMessage> alertMessages) {
            ArrayList<AlertMessage> resultList = Lists.newArrayListWithCapacity(alertMessages.size());
            //根据rule分组
            Map<Integer, List<AlertMessage>> rule2AlertMessages = alertMessages.stream().collect(groupingBy(AlertMessage::getRule));
            for (Map.Entry<Integer, List<AlertMessage>> entry : rule2AlertMessages.entrySet()) {
                Integer rule = entry.getKey();
                List<AlertMessage> list = entry.getValue();
                if (AlertRuleEnum.MARKET_DEMAND.getCode() == rule) {
                    //市场需求趋势的alert：同一个入住日期，保留最新的数据
                    Collection<AlertMessage> distinctList = list.stream().collect(Collectors.toMap(
                                    message -> message.getCheckInDate() + "_" + message.getRule(),
                                    Function.identity(),
                                    (v1, v2) -> v1.getTriggerTime().compareTo(v2.getTriggerTime()) >= 0 ? v1 : v2))
                            .values();
                    resultList.addAll(distinctList);
                } else {
                    //市场需求趋势的alert：同一个入住日期，保留最新的数据
                    Collection<AlertMessage> distinctList = list.stream().collect(Collectors.toMap(
                                    message -> {
                                        DistinctAlertMsgDTO distinctAlertMsgDTO = JsonUtils.convertStringToObject(objectMapper, message.getMessage(), DistinctAlertMsgDTO.class);
                                        distinctAlertMsgDTO.setCheckInDate(message.getCheckInDate());
                                        return distinctAlertMsgDTO.generateKey();
                                    },
                                    Function.identity(),
                                    (v1, v2) -> v1.getTriggerTime().compareTo(v2.getTriggerTime()) >= 0 ? v1 : v2))
                            .values();
                    resultList.addAll(distinctList);
                }
            }
            return resultList;
        }

        /**
         * 检测定时任务是否并发执行
         *
         * @param key 缓存key
         * @return java.lang.Boolean
         * @author lichen
         * @date 2023/9/29
         */
        private Boolean preventConcurrency(String key) {
            return redisTemplate.opsForValue().setIfAbsent(key, 0, 1, TimeUnit.DAYS);
        }

        /**
         * 发送通知
         *
         * @param propertyDTO     资产DTO
         * @param emailConfig     通知配置
         * @param emailTargets    通知对象集合
         * @param alertMsgDTOList 警报消息DTO集合
         * @return void
         * @author lichen
         * @date 2023/11/22
         */
        private void doSendNotify(PropertyDTO propertyDTO, RateShopEmailConfig emailConfig, List<RateShopEmailTarget> emailTargets, List<EmailAlertMsgDTO> alertMsgDTOList) {
            String link = hereLink + MessageFormat.format("?property={0}&tenant={1}", propertyDTO.getAbbr(), propertyDTO.getTenantSymbol());
            String functionName = i18nMessageUtils.message(FunctionCons.RATE_SHOP_FUNCTION, propertyDTO.getLanguage());

            //通知主题
            String defaultSubjectI18nCode = RateShopNotifyConfigTypeEnum.getDefaultSubjectI18nCode(emailConfig.getType());
            String defaultSubject = i18nMessageUtils.message(defaultSubjectI18nCode, propertyDTO.getLanguage(), functionName, propertyDTO.getName());
            //自定义主体优先，其次默认主题
            String subject = StringUtils.isNotBlank(emailConfig.getSubject()) ? emailConfig.getSubject() : defaultSubject;

            //生成邮件内容
            Context context = new Context();
            context.setVariable("emailAlertMsgs", alertMsgDTOList);
            context.setVariable("propertyName", propertyDTO.getName());
            context.setVariable("functionName", functionName);
            context.setVariable("hereLink", link);
            String mailBody = templateEngine.process("rateshop/rateShop-alertMessage_" + propertyDTO.getLanguage(), context);

            //生成line消息内容
            Map<Integer, Long> rule2Count = alertMsgDTOList.stream().collect(groupingBy(EmailAlertMsgDTO::getRule, counting()));
            LinkedHashMap<String, String> businessMap = new LinkedHashMap();
            for (Map.Entry<Integer, Long> entry : rule2Count.entrySet()) {
                businessMap.put(AlertRuleEnum.getDesc(entry.getKey()), entry.getValue() + " pieces");
            }
            PublicFlexMsgParm publicFlexMsgParm = PublicFlexMsgParm.builder()
                    .propertyId(propertyDTO.getId())
                    .propertyName(propertyDTO.getName())
                    .functionName(functionName)
                    .title("There are some alert messages to look at")
                    .link(link)
                    .businessMap(businessMap)
                    .build();
            JSONObject lineContent = lineMessageUtil.generatePublicFlexMsg(publicFlexMsgParm);

            NotifyDTO notifyDTO = new NotifyDTO();
            notifyDTO.setPropertyId(propertyDTO.getId());
            notifyDTO.setFunctionPoint(FunctionPointEnum.RATE_SHOP_ALERT.getFunctionPoint());
            notifyDTO.setSubject(subject);
            notifyDTO.setIdentityList(beanMapper.mapList(emailTargets, NotifyIdentityDTO.class));
            notifyDTO.setEmailContent(EmailUtil.getFullEmailContent(defaultSubject, mailBody, propertyDTO.getLanguage()));
            notifyDTO.setLineContent(Lists.newArrayList(lineContent));
            notifyInternalService.sendNotify(notifyDTO);
        }

    }