# NotifyShiftReportInfoTask.java

    package cloud.wconnect.frontoffice.businesstask.endofshift;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.FunctionIdConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.query.OpenFunctionPropertyQuery;
    import cloud.wconnect.commonlib.service.MailInternalService;
    import cloud.wconnect.commonlib.service.PropertyInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.frontoffice.cons.endofshift.EndOfShiftNotifyConfigTypeEnum;
    import cloud.wconnect.frontoffice.entity.endofshift.EosEmailConfig;
    import cloud.wconnect.frontoffice.entity.endofshift.EosEmailTarget;
    import cloud.wconnect.frontoffice.entity.endofshift.EosShiftConfig;
    import cloud.wconnect.frontoffice.entity.endofshift.ShiftReportInfo;
    import cloud.wconnect.frontoffice.pojo.dto.EosShiftDetailDTO;
    import cloud.wconnect.frontoffice.pojo.dto.endofshift.notify.ShiftReportNotifyDTO;
    import cloud.wconnect.frontoffice.service.endofshift.*;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    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.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;

    import java.time.*;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.concurrent.TimeUnit;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * @author lichen
     * @description 每小时检测上一个班次是否发送通知，如果没有则进行通知。规则：班次结束1个小时以后才发送通知。
     * @date 2025/7/17
     */
    @Slf4j
    @Component
    public class NotifyShiftReportInfoTask {

        @Autowired
        private IShiftReportInfoService shiftReportInfoService;
        @Autowired
        private MailInternalService mailInternalService;
        @Autowired
        private IEosEmailConfigService emailConfigService;
        @Autowired
        private IEosEmailTargetService emailTargetService;
        @Autowired
        private IEndOfShiftService shiftReportService;
        @Autowired
        private IShiftReportInfoService reportInfoService;
        @Autowired
        private IEosShiftConfigService shiftConfigService;
        @Autowired
        private PropertyInternalService propertyInternalService;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        private static final String prefixKey = "wconnect:task:end-of-shift-notify:";

        /**
         * 每小时检查上一个排班是否发送通知，如果没有则进行通知
         *
         * @return void
         * @author lichen
         * @date 2025/6/25
         */
        @Scheduled(cron = "${schedule.cron.notify.end-of-shfit-check-prev}", zone = "${default.timezone}")
        public void autoNotify() {
            String currentHour = LocalDateTime.now().format(DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYYMMDDHH));
            if (!preventConcurrency(prefixKey + currentHour)) {
                return;
            }
            log.info("EndOfShiftReportNotify.Start auto notify end of shirt report");
            String alertEmailSubject = "[Warning] - Auto Notify End Of Shirt Report";
            StringBuilder alertEmailContent = new StringBuilder("<p>There is something wrong when auto notify end of shirt report.</p>");
            try {
                //查询开通End Of Shift功能的资产
                List<PropertyDTO> propertyDTOS = propertyInternalService.getOpenFunctionProperty(OpenFunctionPropertyQuery.builder()
                                .openFunctionIds(Lists.newArrayList(FunctionIdConstant.END_OF_SHIFT)).build())
                        .stream().filter(PropertyDTO::getStatus).toList();

                for (PropertyDTO propertyDTO : propertyDTOS) {
                    //获取邮件通知配置
                    EosEmailConfig emailConfig = emailConfigService.getByPropertyIdAndType(propertyDTO.getId(),
                            EndOfShiftNotifyConfigTypeEnum.END_OF_SHIFT_DETAIL.getType());
                    if (Objects.isNull(emailConfig)) {
                        log.warn("NotifyShiftReportInfoTask.end, email config is empty, propertyId:{},type:{}",
                                propertyDTO.getId(), EndOfShiftNotifyConfigTypeEnum.END_OF_SHIFT_DETAIL.getType());
                        continue;
                    }
                    if (!emailConfig.getStatus()) {
                        log.warn("NotifyShiftReportInfoTask.end, email config status is false, propertyId:{},type:{}",
                                propertyDTO.getId(), EndOfShiftNotifyConfigTypeEnum.END_OF_SHIFT_DETAIL.getType());
                        continue;
                    }
                    List<EosEmailTarget> emailTargets = emailTargetService.listByConfigId(Lists.newArrayList(emailConfig.getId()));
                    if (CollectionUtils.isEmpty(emailTargets)) {
                        log.warn("NotifyShiftReportInfoTask.end, email target is empty, propertyId:{},type:{}",
                                propertyDTO.getId(), EndOfShiftNotifyConfigTypeEnum.END_OF_SHIFT_DETAIL.getType());
                        continue;
                    }

                    //获取当前日期的上一个排班数据
                    ShiftReportInfo shiftReportInfo = getCurrentTimePrevShiftReportInfo(propertyDTO);
                    if (Objects.isNull(shiftReportInfo)) {
                        log.warn("NotifyShiftReportInfoTask.end, prev shift report info is empty, propertyId:{}",
                                propertyDTO.getId());
                        continue;
                    }
                    if (!shiftReportInfo.getCanSend()) {
                        log.warn("NotifyShiftReportInfoTask.end, prev shift report info can not send, propertyId:{}",
                                propertyDTO.getId());
                        continue;
                    }

                    Map<Integer, ShiftReportNotifyDTO> shiftId2EmailDTO = shiftReportService.convertEmailDTO(propertyDTO, Lists.newArrayList(shiftReportInfo));

                    //获取排班数据对应的排班配置
                    EosShiftConfig shiftConfig = shiftConfigService.getById(shiftReportInfo.getShiftConfigId());
                    //获取排班数据对应的班次信息
                    Map<Integer, EosShiftDetailDTO> shift2ShiftDetailDTO = JsonUtils.convertStringToList(objectMapper, shiftConfig.getShiftConfig(), EosShiftDetailDTO.class)
                            .stream().collect(Collectors.toMap(EosShiftDetailDTO::getShift, Function.identity()));
                    EosShiftDetailDTO shiftDetailDTO = shift2ShiftDetailDTO.get(shiftReportInfo.getShift());

                    //发送通知
                    shiftReportService.doSendNotify(propertyDTO, emailConfig, emailTargets, shiftReportInfo,
                            shiftId2EmailDTO.get(shiftReportInfo.getId()), shiftDetailDTO);
                    //更新为不可发送
                    reportInfoService.updateById(new ShiftReportInfo()
                            .setId(shiftReportInfo.getId())
                            .setSendBy(0)
                            .setSendTime(DateTimeUtils.nowTimestamp())
                            .setCanSend(false)
                            .setSendTimes(shiftReportInfo.getSendTimes() + 1));
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                alertEmailContent.append("<p>").append("Exception occurred when auto notify end of shirt report.")
                        .append(e.getMessage()).append("</p>");
                mailInternalService.sendAlertEmail(alertEmailSubject, alertEmailContent.toString());
            }
            log.info("********EndOfShiftReportNotify  End auto notify end of shirt report");
        }

        /**
         * 获取当前日期的上一个排班数据
         *
         * @param propertyDTO 资产DTO
         * @return java.util.Map<java.lang.String, java.lang.Object>
         * @author lichen
         * @date 2025/6/25
         */
        public ShiftReportInfo getCurrentTimePrevShiftReportInfo(PropertyDTO propertyDTO) {
            LocalDateTime currentLocalDateTime = LocalDateTime.now(ZoneId.of(propertyDTO.getTimezone()));
            //当天（yyyyMMdd）
            Integer currentDate = Integer.valueOf(DateTimeUtils.localDateTimeToStr(currentLocalDateTime, BaseConstant.DATE_FORMAT_YYYYMMDD));
            //昨天（yyyyMMdd）
            Integer prevDate = Integer.valueOf(DateTimeUtils.localDateTimeToStr(currentLocalDateTime.minusDays(1), BaseConstant.DATE_FORMAT_YYYYMMDD));

            //按照开始日期倒序查询所有的排班配置
            List<EosShiftConfig> shiftConfigs = shiftConfigService.list(new LambdaQueryWrapper<EosShiftConfig>()
                    .eq(EosShiftConfig::getPropertyId, propertyDTO.getId())
                    .orderByDesc(EosShiftConfig::getStartDate));
            //资产没有配置时，无需通知
            if (CollectionUtils.isEmpty(shiftConfigs)) {
                log.warn("NotifyShiftReportInfoTask.end, property not set shift config, propertyId:{}",
                        propertyDTO.getId());
                return null;
            }

            //当天没有命中排班配置，无需通知
            //例如：资产新增了未来时间的排班配置，当前时间没有对应的排班配置，即没有对应的排班数据
            EosShiftConfig targetDateHitShiftConfig = shiftConfigService.getTargetDateHitShiftConfig(currentDate, shiftConfigs);
            if (Objects.isNull(targetDateHitShiftConfig)) {
                log.warn("NotifyShiftReportInfoTask.end, the current date has not hit the shift config, propertyId:{}, targetDate:{}",
                        propertyDTO.getId(), currentDate);
                return null;
            }

            //班次集合
            List<EosShiftDetailDTO> shiftDetailDTOS = JsonUtils.convertStringToList(objectMapper,
                    targetDateHitShiftConfig.getShiftConfig(), EosShiftDetailDTO.class);

            //当前时刻（小时：分钟）命中的班次
            EosShiftDetailDTO hitShiftConfigDetail = shiftConfigService.getSpecifiedTimeHitShiftConfigDetail(
                    currentLocalDateTime.getHour(), currentLocalDateTime.getMinute(), shiftDetailDTOS);

            //通知的上一个班次可能是当天或昨天的班次，所以获取前3天的数据作为起始条件
            Long minHitShiftSign = ShiftReportInfo.generateShiftSign(
                    Integer.valueOf(DateTimeUtils.localDateTimeToStr(currentLocalDateTime.minusDays(3), BaseConstant.DATE_FORMAT_YYYYMMDD)),
                    "0000");

            long hitShiftSign;
            if (Objects.nonNull(hitShiftConfigDetail)) {
                String startTime = hitShiftConfigDetail.getStartTime();
                String endTime = hitShiftConfigDetail.getEndTime();
                //判断命中的班次是否跨日
                if (ShiftTimeUtil.isCrossDay(startTime, endTime)) {
                    int hitCrossDayIntervalsIndex = getHitCrossDayIntervalsIndex(startTime, endTime, currentLocalDateTime);
                    //命中跨日班次的前半段，例如：[23:00,07:00)，命中了23:00-24:00
                    if (hitCrossDayIntervalsIndex == 0) {
                        hitShiftSign = ShiftReportInfo.generateShiftSign(currentDate, hitShiftConfigDetail.getStartTime());
                    } else {
                        //命中跨日班次的后半段，例如：[23:00,07:00)，命中了00:00-07:00
                        //签名需要用前一天来生成
                        hitShiftSign = ShiftReportInfo.generateShiftSign(prevDate, hitShiftConfigDetail.getStartTime());
                    }
                } else {
                    hitShiftSign = ShiftReportInfo.generateShiftSign(currentDate, hitShiftConfigDetail.getStartTime());
                }
            } else {
                hitShiftSign = Long.parseLong(DateTimeUtils.localDateTimeToStr(currentLocalDateTime, BaseConstant.DATE_FORMAT_YYYYMMDDHHmm));
            }

            //获取命中排班数据的上一个班次
            List<ShiftReportInfo> shiftReportInfoList = shiftReportInfoService.list(new LambdaQueryWrapper<ShiftReportInfo>()
                    .eq(ShiftReportInfo::getPropertyId, propertyDTO.getId())
                    .ge(ShiftReportInfo::getShiftSign, minHitShiftSign)
                    .lt(ShiftReportInfo::getShiftSign, hitShiftSign)
                    .orderByDesc(ShiftReportInfo::getShiftSign));
            if (CollectionUtils.isEmpty(shiftReportInfoList)) {
                log.warn("NotifyShiftReportInfoTask query param, minHitShiftSign:{}", minHitShiftSign);
                log.warn("NotifyShiftReportInfoTask query param, hitShiftSign:{}", hitShiftSign);
                log.warn("NotifyShiftReportInfoTask.end, the current time has not hit the shift config, propertyId:{}, targetDate:{}, hour:{}, minute:{}",
                        propertyDTO.getId(), currentDate, currentLocalDateTime.getHour(), currentLocalDateTime.getMinute());
                return null;
            }

            //待通知的排班
            ShiftReportInfo prevShiftReportInfo = shiftReportInfoList.get(0);
            //获取上一个排班对应的班次配置
            EosShiftDetailDTO prevShiftReportInfoShiftDetailDTO;
            if (prevShiftReportInfo.getTargetDate().equals(currentDate)) {
                Map<Integer, EosShiftDetailDTO> shift2ShiftDetailDTO = shiftDetailDTOS.stream().collect(Collectors.toMap(EosShiftDetailDTO::getShift, Function.identity()));
                prevShiftReportInfoShiftDetailDTO = shift2ShiftDetailDTO.get(prevShiftReportInfo.getShift());
            } else {
                EosShiftConfig prevTargetDateHitShiftConfig = shiftConfigService.getTargetDateHitShiftConfig(prevShiftReportInfo.getTargetDate(), shiftConfigs);
                if (Objects.isNull(prevTargetDateHitShiftConfig)) {
                    log.warn("NotifyShiftReportInfoTask.end, the prevShiftReportInfo has not hit the shift config, propertyId:{}, targetDate:{}",
                            propertyDTO.getId(), prevShiftReportInfo.getTargetDate());
                    return null;
                }
                Map<Integer, EosShiftDetailDTO> shift2PrevShiftDetailDTO = JsonUtils.convertStringToList(objectMapper, prevTargetDateHitShiftConfig.getShiftConfig(), EosShiftDetailDTO.class)
                        .stream().collect(Collectors.toMap(EosShiftDetailDTO::getShift, Function.identity()));
                prevShiftReportInfoShiftDetailDTO = shift2PrevShiftDetailDTO.get(prevShiftReportInfo.getShift());
            }
            //获取上一个班次的实际结束时间
            LocalDateTime prevEndLocalDateTime = getEndTime(prevShiftReportInfo.getTargetDate(), prevShiftReportInfoShiftDetailDTO);
            // 计算时间差
            Duration duration = Duration.between(prevEndLocalDateTime, currentLocalDateTime);
            // 转换为总分钟数（包含小数）
            long minutes = duration.toMinutes();
            if (minutes >= 60) {
                log.info("NotifyShiftReportInfoTask.end, prev shift report info, propertyId:{}, targetDate:{}, shift:{}", prevShiftReportInfo.getPropertyId(),
                        prevShiftReportInfo.getTargetDate(), prevShiftReportInfo.getShift());
                return prevShiftReportInfo;
            } else {
                log.info("NotifyShiftReportInfoTask.end, The end time of the previous shift was not one hour later, propertyId:{}, targetDate:{}, shift:{}", prevShiftReportInfo.getPropertyId(),
                        prevShiftReportInfo.getTargetDate(), prevShiftReportInfo.getShift());
                return null;
            }
        }

        /**
         * 获取排班的实际结束时间
         *
         * @param targetDate                        排班日期（yyyyMMdd）
         * @param prevShiftReportInfoShiftDetailDTO 排班命中的班次配置
         * @return java.time.LocalDateTime
         * @author lichen
         * @date 2025/7/7
         */
        private LocalDateTime getEndTime(Integer targetDate, EosShiftDetailDTO prevShiftReportInfoShiftDetailDTO) {
            LocalDate localDate = DateTimeUtils.strToLocalDate(targetDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);

            String startTime = prevShiftReportInfoShiftDetailDTO.getStartTime();
            String endTime = prevShiftReportInfoShiftDetailDTO.getEndTime();

            //班次的结束时间
            String[] split = endTime.split(":");
            int hour = Integer.parseInt(split[0]);
            int minute = Integer.parseInt(split[1]);
            LocalTime endLocalTime = LocalTime.of(hour, minute);

            //判断命中的班次是否跨日
            if (ShiftTimeUtil.isCrossDay(startTime, endTime)) {
                //排班的实际结束日期=targetDate + 1
                return LocalDateTime.of(localDate.plusDays(1), endLocalTime);
            } else {
                return LocalDateTime.of(localDate, endLocalTime);
            }
        }

        /**
         * 获取命中跨天区间的索引
         *
         * @param startTime            开始时间，例如：07:00
         * @param endTime              结束时间，例如：03:00
         * @param currentLocalDateTime 当前时刻
         * @return int
         * @author lichen
         * @date 2025/6/25
         */
        private int getHitCrossDayIntervalsIndex(String startTime, String endTime, LocalDateTime currentLocalDateTime) {
            //获取时间区间，单位分钟
            int[] minuteInterval = ShiftTimeUtil.timeToInterval(startTime, endTime);
            //将时间区间拆分为不跨天的子区间
            List<int[]> subIntervals = ShiftTimeUtil.splitInterval(minuteInterval[0], minuteInterval[1]);

            //当前时间的分钟数
            int currentMinute = currentLocalDateTime.getHour() * 60 + currentLocalDateTime.getMinute();

            int index = 0;
            for (int i = 0; i < subIntervals.size(); i++) {
                if (currentMinute >= subIntervals.get(i)[0] && currentMinute < subIntervals.get(i)[1]) {
                    index = i;
                    break;
                }
            }
            return index;
        }

        /**
         * 防止并发操作
         *
         * @param key 并发控制key
         * @return java.lang.Boolean
         * @author lichen
         * @date 2025/7/7
         */
        private Boolean preventConcurrency(String key) {
            return redisTemplate.opsForValue().setIfAbsent(key, 0, 1, TimeUnit.DAYS);
        }

    }