# RevenueOccJumpAlertTask.java

    package cloud.wconnect.rateshop.businesstask.revenue;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.SchedulerCons;
    import cloud.wconnect.commonlib.pojo.dto.NotifyDTO;
    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.pojo.dto.scheduler.JobParameterDTO;
    import cloud.wconnect.commonlib.service.NotifyInternalService;
    import cloud.wconnect.commonlib.service.PropertyInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JobParameterUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.rateshop.cons.PrecisionCons;
    import cloud.wconnect.rateshop.entity.revenue.OccForecast;
    import cloud.wconnect.rateshop.entity.revenue.OccJumpAlertFilterConfig;
    import cloud.wconnect.rateshop.entity.revenue.RevenueSchedulerJob;
    import cloud.wconnect.rateshop.pojo.dto.revenue.OccForecastValuesDTO;
    import cloud.wconnect.rateshop.pojo.vo.revenue.OccJumpAlertVO;
    import cloud.wconnect.rateshop.service.revenue.IOccForecastService;
    import cloud.wconnect.rateshop.service.revenue.IOccJumpAlertFilterConfigService;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.JsonNode;
    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.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Service;

    import jakarta.annotation.Resource;
    import java.math.BigDecimal;
    import java.time.ZonedDateTime;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * Task: Occ Jump Alert
     *
     * @author zc
     * @date 2024/04/17
     */
    @Slf4j
    @Service
    public class RevenueOccJumpAlertTask {
        @Resource
        private IOccForecastService occForecastService;
        @Resource
        private IOccJumpAlertFilterConfigService filterConfigService;
        @Resource
        private PackageNotifyDTOUtil packageNotifyDTOUtil;
        @Resource
        private NotifyInternalService notifyInternalService;
        @Resource
        private PropertyInternalService propertyInternalService;
        @Resource
        private ObjectMapper objectMapper;

        /**
         * 检查数据是否按时处理
         *
         * @param schedulerJob 调度数据
         * @author zc
         * @date 2024/4/17
         */
        @Async
        public void autoNotify(RevenueSchedulerJob schedulerJob) {
            log.info("RevenueOccJumpAlertTask.autoNotify start...");

            int propertyId = schedulerJob.getPropertyId();
            PropertyDTO propertyDTO = propertyInternalService.getOne(propertyId);
            if (!propertyDTO.getStatus()) {
                log.warn("RevenueOccJumpAlertTask.autoNotify end... the property is closed, no processing required");
                return;
            }

            JobParameterDTO jobParameterDTO = JsonUtils.convertStringToObject(objectMapper,
                    schedulerJob.getParameters(), JobParameterDTO.class);
            JsonNode customBusinessNode = parseCustomBusinessNode(jobParameterDTO);
            if (customBusinessNode == null) {
                return;
            }

            ZonedDateTime zonedDateTimeNow = DateTimeUtils.dateTimeNow(propertyDTO.getTimezone());
            ZonedDateTime zonedDateTimeLast = calculateLastZonedDateTime(zonedDateTimeNow, customBusinessNode);

            // 当前时刻 key
            String nowKey = DateTimeUtils.parseToLong(zonedDateTimeNow, BaseConstant.DATE_FORMAT_YYYYMMDDHH).toString();
            // 比对时刻 key
            String lastKey = DateTimeUtils.parseToLong(zonedDateTimeLast, BaseConstant.DATE_FORMAT_YYYYMMDDHH).toString();

            // 数据日期范围
            Map<String, Long> dateRange = JobParameterUtils.parseDateRange(jobParameterDTO.getDateRange(),
                    jobParameterDTO.getDateRangeUnit(), propertyDTO.getTimezone(), null);
            long startDate = DateTimeUtils.parseToLong(DateTimeUtils.toZonedDateTime(dateRange.get(JobParameterDTO.START_DATE),
                    propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD);
            long endDate = DateTimeUtils.parseToLong(DateTimeUtils.toZonedDateTime(dateRange.get(JobParameterDTO.END_DATE),
                    propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD);

            // 获取 Occ Jump Alert 过滤配置
            OccJumpAlertFilterConfig filterConfig = filterConfigService.getFilterConfig(propertyDTO.getId());
            BigDecimal filterMinValue = Objects.isNull(filterConfig) ? BigDecimal.ZERO : filterConfig.getMinValue();
            BigDecimal filterMaxValue = Objects.isNull(filterConfig) ? BigDecimal.ZERO : filterConfig.getMaxValue();

            // 生成 OccJumpAlertVO 集合
            List<OccJumpAlertVO> occJumpAlertVOList = generateOccJumpAlertVOList(propertyDTO, filterMinValue, filterMaxValue,
                    startDate, endDate, nowKey, lastKey);
            if (CollectionUtils.isEmpty(occJumpAlertVOList)) {
                log.warn("RevenueOccJumpAlertTask.autoNotify end... no data to process");
                return;
            }

            // 生成通知对象
            NotifyDTO notifyDTO = createNotifyDTO(propertyDTO, schedulerJob, zonedDateTimeNow, zonedDateTimeLast,
                    filterMinValue, filterMaxValue, occJumpAlertVOList);
            if (Objects.isNull(notifyDTO)) {
                log.info("RevenueOccJumpAlertTask.autoNotify end... notifyDTO is null");
                return;
            }

            // 发送通知
            notifyInternalService.sendNotify(notifyDTO);

            log.info("RevenueOccJumpAlertTask.autoNotify end...");
        }

        /**
         * 解析自定义业务参数
         *
         * @param jobParameterDTO 调度任务参数
         * @return JsonNode
         * @author zc
         * @date 2024/11/15
         */
        private JsonNode parseCustomBusinessNode(JobParameterDTO jobParameterDTO) {
            try {
                return objectMapper.readTree(jobParameterDTO.getCustomBusiness());
            } catch (JsonProcessingException e) {
                log.error("RevenueOccJumpAlertTask.autoNotify end... Failed to parse customBusiness JSON", e);
                return null;
            }
        }

        /**
         * 计算比对时间
         *
         * @param zonedDateTimeNow   当前时间
         * @param customBusinessNode 自定义业务参数
         * @return 比对时间
         * @author zc
         * @date 2024/11/15
         */
        private ZonedDateTime calculateLastZonedDateTime(ZonedDateTime zonedDateTimeNow, JsonNode customBusinessNode) {
            int hourLast = customBusinessNode.get(SchedulerCons.HOUR).asInt();
            return (hourLast >= zonedDateTimeNow.getHour()) ?
                    zonedDateTimeNow.minusDays(1).withHour(hourLast) : zonedDateTimeNow.withHour(hourLast);
        }

        /**
         * 创建通知对象
         *
         * @param propertyDTO        资产
         * @param schedulerJob       调度任务
         * @param zonedDateTimeNow   当前时间
         * @param zonedDateTimeLast  比对时间
         * @param filterMinValue     过滤配置最小值
         * @param filterMaxValue     过滤配置最大值
         * @param occJumpAlertVOList 通知数据集合
         * @return NotifyDTO
         * @author zc
         * @date 2024/11/15
         */
        private NotifyDTO createNotifyDTO(PropertyDTO propertyDTO, RevenueSchedulerJob schedulerJob,
                                          ZonedDateTime zonedDateTimeNow, ZonedDateTime zonedDateTimeLast,
                                          BigDecimal filterMinValue, BigDecimal filterMaxValue,
                                          List<OccJumpAlertVO> occJumpAlertVOList) {
            InternationalizationDateTimeDTO dateTimeDTO = propertyDTO.getInternationalization().getDateTime();
            String lastMoment = InternationalizationUtils.formatDateTime(zonedDateTimeLast.withMinute(0).toEpochSecond(),
                    dateTimeDTO.getLongDate(), dateTimeDTO.getHourClock(), propertyDTO.getTimezone(), propertyDTO.getLanguage());
            String nowMoment = InternationalizationUtils.formatDateTime(zonedDateTimeNow.withMinute(0).toEpochSecond(),
                    dateTimeDTO.getLongDate(), dateTimeDTO.getHourClock(), propertyDTO.getTimezone(), propertyDTO.getLanguage());

            InternationalizationNumberDTO numberDTO = propertyDTO.getInternationalization().getNumber();
            String thresholdMin = InternationalizationUtils.formatViewValue(filterMinValue, PrecisionCons.DAILY_REVENUE_PRECISION,
                    numberDTO.getCalculationRule(), null);
            String thresholdMax = InternationalizationUtils.formatViewValue(filterMaxValue, PrecisionCons.DAILY_REVENUE_PRECISION,
                    numberDTO.getCalculationRule(), null);
            return packageNotifyDTOUtil.getNotifyDTOForOccJumpAlert(propertyDTO, schedulerJob.getCategory(),
                    occJumpAlertVOList, lastMoment, nowMoment, thresholdMin, thresholdMax);
        }

        /**
         * 生成 OccJumpAlertVO 集合
         *
         * @param propertyDTO    资产
         * @param filterMinValue 过滤配置最小值
         * @param filterMaxValue 过滤配置最大值
         * @param startDate      数据日期，开始日期
         * @param endDate        数据日期，结束日期
         * @param nowKey         当前时刻 key
         * @param lastKey        比对时刻 key
         * @return OccJumpAlertVO 集合
         * @author zc
         * @date 2024/4/18
         */
        private List<OccJumpAlertVO> generateOccJumpAlertVOList(PropertyDTO propertyDTO, BigDecimal filterMinValue, BigDecimal filterMaxValue,
                                                                long startDate, long endDate, String nowKey, String lastKey) {
            // 获取国际化配置
            String language = propertyDTO.getLanguage();
            InternationalizationDTO internationalization = propertyDTO.getInternationalization();
            InternationalizationNumberDTO numberDTO = internationalization.getNumber();
            int scale = PrecisionCons.DAILY_REVENUE_PRECISION;
            String calculationRule = numberDTO.getCalculationRule();

            InternationalizationDateTimeDTO dateTimeDTO = internationalization.getDateTime();
            String longDate = dateTimeDTO.getLongDate();

            // 获取指定日期范围内的 OccForecast 数据
            List<OccForecast> occForecastList = occForecastService.listByDateRange(propertyDTO.getId(), startDate, endDate);

            List<OccJumpAlertVO> resultList = Lists.newArrayListWithExpectedSize(91);
            for (OccForecast occForecast : occForecastList) {
                Map<String, OccForecastValuesDTO> key2ValuesDTO = JsonUtils.convertStringToList(objectMapper,
                                occForecast.getValues(), OccForecastValuesDTO.class).stream()
                        .collect(Collectors.toMap(OccForecastValuesDTO::getKey, Function.identity()));

                BigDecimal pcOccNow = null;
                BigDecimal scOccNow = null;
                OccForecastValuesDTO valuesDTONow = key2ValuesDTO.get(nowKey);
                if (Objects.nonNull(valuesDTONow)) {
                    pcOccNow = valuesDTONow.getValue().getPcOcc();
                    scOccNow = valuesDTONow.getValue().getScOcc();
                }

                BigDecimal scOccLast = null;
                BigDecimal pcOccLast = null;
                OccForecastValuesDTO valuesDTOLast = key2ValuesDTO.get(lastKey);
                if (Objects.nonNull(valuesDTOLast)) {
                    scOccLast = valuesDTOLast.getValue().getPcOcc();
                    pcOccLast = valuesDTOLast.getValue().getScOcc();
                }

                if (Objects.isNull(pcOccNow) || Objects.isNull(pcOccLast)
                        || Objects.isNull(scOccNow) || Objects.isNull(scOccLast)) {
                    continue;
                }

                // PC Occ 差值
                BigDecimal subtractPcOcc = pcOccNow.subtract(pcOccLast);
                if (subtractPcOcc.compareTo(filterMinValue) > 0 && subtractPcOcc.compareTo(filterMaxValue) < 0) {
                    continue;
                }

                // SC Occ 差值
                BigDecimal subtractScOcc = scOccNow.subtract(scOccLast);

                OccJumpAlertVO occJumpAlertVO = new OccJumpAlertVO();
                occJumpAlertVO.setTheDate(InternationalizationUtils.formatYearMonthDate(occForecast.getTheDate(), longDate, language));
                occJumpAlertVO.setPcOccLast(InternationalizationUtils.formatViewValue(pcOccLast, scale, calculationRule, null));
                occJumpAlertVO.setPcOccNow(InternationalizationUtils.formatViewValue(pcOccNow, scale, calculationRule, null));
                occJumpAlertVO.setPcOccVariances(InternationalizationUtils.formatViewValue(subtractPcOcc, scale, calculationRule, null));
                occJumpAlertVO.setPcOccIsUp(subtractPcOcc.compareTo(BigDecimal.ZERO) > 0);

                occJumpAlertVO.setScOccLast(InternationalizationUtils.formatViewValue(scOccLast, scale, calculationRule, null));
                occJumpAlertVO.setScOccNow(InternationalizationUtils.formatViewValue(scOccNow, scale, calculationRule, null));
                occJumpAlertVO.setScOccVariances(InternationalizationUtils.formatViewValue(subtractScOcc, scale, calculationRule, null));
                occJumpAlertVO.setScOccIsUp(subtractScOcc.compareTo(BigDecimal.ZERO) > 0);
                resultList.add(occJumpAlertVO);
            }
            return resultList;
        }
    }