| @ -0,0 +1,338 @@ | |||
| package com.inspect.calender.service; | |||
| import com.github.benmanes.caffeine.cache.Caffeine; | |||
| import com.github.benmanes.caffeine.cache.LoadingCache; | |||
| import com.inspect.base.core.enums.ExecStatus; | |||
| import com.inspect.base.core.enums.IntervalType; | |||
| import com.inspect.base.core.utils.StringUtils; | |||
| import com.inspect.calender.domain.DailyTaskStatsDTO; | |||
| import com.inspect.calender.domain.MonthlyTaskStatsDTO; | |||
| import com.inspect.calender.enums.TaskStateEnum; | |||
| import com.inspect.task.domain.PatrolTask; | |||
| import com.inspect.task.mapper.PatrolTaskMapper; | |||
| import com.inspect.taskstatus.mapper.PatrolTaskStatusMapper; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.springframework.scheduling.annotation.Scheduled; | |||
| import org.springframework.stereotype.Service; | |||
| import javax.annotation.Resource; | |||
| import java.time.*; | |||
| import java.time.temporal.TemporalAdjusters; | |||
| import java.util.*; | |||
| import java.util.concurrent.TimeUnit; | |||
| import java.util.stream.Collectors; | |||
| /** | |||
| * 未来任务生成器 | |||
| */ | |||
| @Service | |||
| @Slf4j | |||
| public class TaskCalendarGenerator { | |||
| /* | |||
| * 未来任务缓存的过期时间 | |||
| * 预生成未来1个月计划(本月和下一个月) | |||
| */ | |||
| private static final Integer FUTURE_CALENDAR = 60; | |||
| @Resource | |||
| private PatrolTaskMapper patrolTaskMapper; | |||
| private final LoadingCache<Integer, Map<LocalDate, List<DailyTaskStatsDTO>>> futureCalendarCache = | |||
| Caffeine.newBuilder() | |||
| .maximumSize(5) | |||
| .expireAfterWrite(12, TimeUnit.HOURS) | |||
| .build(this::generate); | |||
| @Resource | |||
| private PatrolTaskStatusMapper patrolTaskStatusMapper; | |||
| // 定时任务:每天凌晨或任务变更后主动刷新缓存 | |||
| @Scheduled(cron = "0 50 0 * * ?") | |||
| public void refreshCalendarCache() { | |||
| log.info("[任务执行日历]定时刷新未来任务日历缓存"); | |||
| futureCalendarCache.invalidateAll(); | |||
| futureCalendarCache.refresh(FUTURE_CALENDAR); | |||
| } | |||
| /** | |||
| * 任务信息变更时,主动刷新日历缓存 | |||
| */ | |||
| public void onTaskChange() { | |||
| log.info("[任务执行日历]任务信息变化,日历缓存失效"); | |||
| futureCalendarCache.invalidateAll(); | |||
| } | |||
| public List<MonthlyTaskStatsDTO> getMonthlyStatsByYear(int year) { | |||
| List<MonthlyTaskStatsDTO> existingStats = patrolTaskStatusMapper.countMonthlyStatsByYear(year); | |||
| // 获取未来预生成的计划任务(缓存) | |||
| Map<LocalDate, List<DailyTaskStatsDTO>> futureCalendarMap = futureCalendarCache.get(FUTURE_CALENDAR); | |||
| // 验证缓存数据是否为空 | |||
| if (futureCalendarMap == null) { | |||
| return existingStats; | |||
| } | |||
| LocalDateTime currentTime = LocalDateTime.now(); | |||
| LocalDate currentDate = currentTime.toLocalDate(); | |||
| for (Map.Entry<LocalDate, List<DailyTaskStatsDTO>> entry : futureCalendarMap.entrySet()) { | |||
| LocalDate date = entry.getKey(); | |||
| List<DailyTaskStatsDTO> futureTasks = entry.getValue(); | |||
| // 检查当前日期是否在指定年份范围内 | |||
| if (date.getYear() == year) { | |||
| long count = 0; | |||
| if (date.equals(currentDate)) { | |||
| if (futureTasks != null) { | |||
| count = futureTasks.stream() | |||
| .filter(dto -> dto.getStartTime() != null) | |||
| .filter(dto -> dto.getStartTime().isAfter(currentTime)) | |||
| .count(); | |||
| } | |||
| } else if (date.isAfter(currentDate)) { | |||
| if (futureTasks != null) { | |||
| count = futureTasks.size(); | |||
| } | |||
| } | |||
| // 确保月份索引在有效范围内 | |||
| int monthIndex = date.getMonthValue() - 1; | |||
| if (monthIndex < existingStats.size()) { | |||
| MonthlyTaskStatsDTO monthlyStats = existingStats.get(date.getMonthValue() - 1); | |||
| monthlyStats.setPending(monthlyStats.getPending() + count); | |||
| monthlyStats.setTotal(monthlyStats.getTotal() + count); | |||
| } | |||
| } | |||
| } | |||
| return existingStats; | |||
| } | |||
| public List<DailyTaskStatsDTO> getFullCalendar(int year, int month) { | |||
| return getFullCalendar(year, month, 0); | |||
| } | |||
| public List<DailyTaskStatsDTO> getFullCalendar(int year, int month, int day) { | |||
| List<DailyTaskStatsDTO> existingList; | |||
| if (day == 0) { | |||
| existingList = patrolTaskStatusMapper.selectDailyStatsByYearAndMonth(year, month); | |||
| } else { | |||
| existingList = patrolTaskStatusMapper.selectDailyStatsByYearAndMonthAndDay(year, month, day); | |||
| } | |||
| existingList.forEach(DailyTaskStatsDTO::formatTimeAndStatus); | |||
| // 将已生成任务按日期分组 | |||
| Map<LocalDate, List<DailyTaskStatsDTO>> fullCalendarMap = existingList.stream() | |||
| .collect(Collectors.groupingBy(dto -> dto.getStartTime().toLocalDate(), TreeMap::new, Collectors.toList())); | |||
| // 2. 获取未来预生成的计划任务(缓存) | |||
| Map<LocalDate, List<DailyTaskStatsDTO>> futureCalendarMap = futureCalendarCache.get(FUTURE_CALENDAR); | |||
| if (day != 0) { | |||
| LocalDate targetDate = LocalDate.of(year, month, day); | |||
| Map<LocalDate, List<DailyTaskStatsDTO>> tempMap = new TreeMap<>(); | |||
| tempMap.computeIfAbsent(targetDate, k -> new ArrayList<>()).addAll(futureCalendarMap.getOrDefault(targetDate, new ArrayList<>())); | |||
| futureCalendarMap = tempMap; | |||
| } | |||
| LocalDateTime currentTime = LocalDateTime.now(); | |||
| LocalDate currentDate = currentTime.toLocalDate(); | |||
| for (Map.Entry<LocalDate, List<DailyTaskStatsDTO>> entry : futureCalendarMap.entrySet()) { | |||
| LocalDate date = entry.getKey(); | |||
| List<DailyTaskStatsDTO> futureTasks = entry.getValue(); | |||
| // 检查当前日期是否在指定年月范围内 | |||
| if (date.getYear() == year && date.getMonthValue() == month) { | |||
| List<DailyTaskStatsDTO> existingTasks = fullCalendarMap.computeIfAbsent(date, k -> new ArrayList<>()); | |||
| // 将未来计划任务添加到当前日期的任务列表中(如果该任务尚未存在) | |||
| for (DailyTaskStatsDTO futureTask : futureTasks) { | |||
| if (date.equals(currentDate)) { | |||
| // 获取 startTime 最晚的任务 | |||
| if (currentTime.isBefore(futureTask.getStartTime())) { | |||
| existingTasks.add(futureTask); | |||
| } | |||
| } else { | |||
| existingTasks.add(futureTask); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // 对每一天的任务按开始时间排序 | |||
| fullCalendarMap.values().forEach(list -> | |||
| list.sort(Comparator.comparing(DailyTaskStatsDTO::getStartTime))); | |||
| return fullCalendarMap.values() | |||
| .stream() | |||
| .flatMap(List::stream) | |||
| .collect(Collectors.toList()); | |||
| } | |||
| /** | |||
| * 获取未来 1 个月天的任务执行日历 | |||
| * (本月和下个月) | |||
| */ | |||
| public Map<LocalDate, List<DailyTaskStatsDTO>> generate(Integer days) { | |||
| Map<LocalDate, List<DailyTaskStatsDTO>> calendarMap = new TreeMap<>(); | |||
| LocalDateTime beginTime = LocalDate.now().atTime(0, 0, 0); | |||
| LocalDateTime endTime = LocalDate.now().plusMonths(1) | |||
| .with(TemporalAdjusters.lastDayOfMonth()) | |||
| .atTime(23, 59, 59); | |||
| // 获取所有启用的任务 | |||
| List<PatrolTask> patrolTaskList = patrolTaskMapper.selectPatrolTaskList(PatrolTask.builder().isEnable("0").build()); | |||
| log.info("查询所有启用任务计划,数量:{}", patrolTaskList.size()); | |||
| // 逐个任务解析,生成未来任务 | |||
| for (PatrolTask task : patrolTaskList) { | |||
| if (StringUtils.isEmpty(task.getDevType())) { | |||
| continue; | |||
| } | |||
| try { | |||
| // 1.定期执行 | |||
| if (ExecStatus.REGULAR.getCode().equals(task.getExecutionStatus())) { | |||
| if (task.getFixedStartTime() != null) { | |||
| LocalDateTime startTime = task.getFixedStartTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); | |||
| // 判断开始时间是否在范围内 | |||
| if (startTime.isAfter(beginTime) && startTime.isBefore(endTime)) { | |||
| DailyTaskStatsDTO dto = DailyTaskStatsDTO.builder() | |||
| .id(task.getTaskCode()) | |||
| .name(task.getTaskName()) | |||
| .taskState(TaskStateEnum.PENDING.getCode()) | |||
| .startTime(startTime) | |||
| .build(); | |||
| dto.formatTimeAndStatus(); | |||
| calendarMap.computeIfAbsent(startTime.toLocalDate(), k -> new ArrayList<>()).add(dto); | |||
| } | |||
| log.info("定时任务{}【{}】生成完毕", task.getTaskId(), task.getTaskName()); | |||
| } | |||
| } | |||
| // 2.周期执行 | |||
| else if (ExecStatus.PERIODIC.getCode().equals(task.getExecutionStatus())) { | |||
| LocalDateTime cycleStartDateTime = task.getCycleStartTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); | |||
| LocalDateTime cycleEndTime = task.getCycleEndTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); | |||
| LocalDateTime startLdt = beginTime.isBefore(cycleStartDateTime) ? cycleStartDateTime : beginTime; | |||
| LocalDateTime endLdt = endTime.isBefore(cycleEndTime) ? endTime : cycleEndTime; | |||
| String[] executeTimes = task.getCycleExecuteTime().split(StringUtils.COMMA); | |||
| // 按周 | |||
| if (!StringUtils.isEmpty(task.getCycleWeek())) { | |||
| String[] weeks = task.getCycleWeek().split(StringUtils.COMMA); | |||
| // 解析星期列表 | |||
| List<DayOfWeek> weekDays = Arrays.stream(weeks).map(this::parseDayOfWeek).collect(Collectors.toList()); | |||
| log.info("周期任务(周){}【{}】生成开始,开始时间:{},结束时间:{},执行时间:{}", task.getTaskId(), task.getTaskName(), startLdt, endLdt, executeTimes); | |||
| // 判断范围内的每一天的星期是否为目标星期 | |||
| for (LocalDateTime date = startLdt; date.isBefore(endLdt); date = date.plusDays(1)) { | |||
| // 判断是否为目标星期 | |||
| for (int i = 0; i < weekDays.size(); i++) { | |||
| DayOfWeek weekDay = weekDays.get(i); | |||
| if (date.getDayOfWeek().equals(weekDay)) { | |||
| LocalTime executeTime = LocalTime.parse(executeTimes[i]); | |||
| LocalDateTime startTime = LocalDateTime.of(date.toLocalDate(), executeTime); | |||
| DailyTaskStatsDTO dto = DailyTaskStatsDTO.builder() | |||
| .id(task.getTaskCode()) | |||
| .name(task.getTaskName()) | |||
| .taskState(TaskStateEnum.PENDING.getCode()) | |||
| .startTime(startTime) | |||
| .build(); | |||
| dto.formatTimeAndStatus(); | |||
| calendarMap.computeIfAbsent(startTime.toLocalDate(), k -> new ArrayList<>()).add(dto); | |||
| } | |||
| } | |||
| } | |||
| log.info("周期任务(周){}【{}】生成完毕", task.getTaskId(), task.getTaskName()); | |||
| } | |||
| // 按月 | |||
| else if (!StringUtils.isEmpty(task.getCycleMonth())) { | |||
| String[] monthDays = task.getCycleMonth().split(StringUtils.COMMA); | |||
| List<Integer> targetDays = Arrays.stream(monthDays).map(Integer::parseInt).collect(Collectors.toList()); | |||
| log.info("周期任务(月){}【{}】生成开始,开始时间:{},结束时间:{},执行时间:{}", task.getTaskId(), task.getTaskName(), startLdt, endLdt, executeTimes); | |||
| for (LocalDateTime date = startLdt; date.isBefore(endLdt); date = date.plusDays(1)) { | |||
| // 判断是否为目标日期 | |||
| for (int i = 0; i < targetDays.size(); i++) { | |||
| int targetDay = targetDays.get(i); | |||
| if (date.getDayOfMonth() == targetDay) { | |||
| LocalTime executeTime = LocalTime.parse(executeTimes[i]); | |||
| LocalDateTime startTime = LocalDateTime.of(date.toLocalDate(), executeTime); | |||
| DailyTaskStatsDTO dto = DailyTaskStatsDTO.builder() | |||
| .id(task.getTaskCode()) | |||
| .name(task.getTaskName()) | |||
| .taskState(TaskStateEnum.PENDING.getCode()) | |||
| .startTime(startTime) | |||
| .build(); | |||
| dto.formatTimeAndStatus(); | |||
| calendarMap.computeIfAbsent(startTime.toLocalDate(), k -> new ArrayList<>()).add(dto); | |||
| } | |||
| } | |||
| } | |||
| log.info("周期任务(月){}【{}】生成完毕", task.getTaskId(), task.getTaskName()); | |||
| } | |||
| } | |||
| // 3.间隔执行 | |||
| else if (ExecStatus.INTERVAL.getCode().equals(task.getExecutionStatus())) { | |||
| // 小时间隔 | |||
| if (IntervalType.BY_HOUR.getCode().equals(task.getIntervalType())) { | |||
| // 按小时执行 | |||
| int intervalNumber = task.getIntervalNumber(); | |||
| if (intervalNumber > 0) { | |||
| LocalDateTime intervalStartTime = task.getIntervalStartTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); | |||
| LocalDateTime intervalEndTime = task.getIntervalEndTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); | |||
| LocalTime executeTime = LocalTime.parse(task.getIntervalExecuteTime()); | |||
| LocalDateTime startLdt = beginTime.isBefore(intervalStartTime) ? intervalStartTime : beginTime; | |||
| LocalDateTime endLdt = endTime.isAfter(intervalEndTime) ? intervalEndTime : endTime; | |||
| startLdt = LocalDateTime.of(startLdt.toLocalDate(), executeTime); | |||
| for (LocalDateTime date = startLdt; date.isBefore(endLdt); date = date.plusHours(intervalNumber)) { | |||
| // 判断是否为目标星期 | |||
| DailyTaskStatsDTO dto = DailyTaskStatsDTO.builder() | |||
| .name(task.getTaskName()) | |||
| .taskState(TaskStateEnum.PENDING.getCode()) | |||
| .startTime(date) | |||
| .build(); | |||
| dto.formatTimeAndStatus(); | |||
| calendarMap.computeIfAbsent(date.toLocalDate(), k -> new ArrayList<>()).add(dto); | |||
| } | |||
| } | |||
| } | |||
| log.info("间隔任务(月){}【{}】生成完毕", task.getTaskId(), task.getTaskName()); | |||
| } | |||
| } catch (Exception e) { | |||
| // 无效任务,跳过该任务 | |||
| log.error("任务异常,解析失败,taskId:{}", task.getTaskId(), e); | |||
| } | |||
| } | |||
| int totalCount = calendarMap.values().stream() | |||
| .mapToInt(List::size) | |||
| .sum(); | |||
| log.info("未来任务生成完毕,总任务数量:{}", totalCount); | |||
| // 将每一天的任务按开始时间排序 | |||
| calendarMap.values().forEach(list -> list.sort(Comparator.comparing(DailyTaskStatsDTO::getStartTime))); | |||
| return calendarMap; | |||
| } | |||
| /** | |||
| * 将中文星期转换为DayOfWeek枚举 | |||
| */ | |||
| private DayOfWeek parseDayOfWeek(String week) { | |||
| switch (week) { | |||
| case "星期一": | |||
| return DayOfWeek.MONDAY; | |||
| case "星期二": | |||
| return DayOfWeek.TUESDAY; | |||
| case "星期三": | |||
| return DayOfWeek.WEDNESDAY; | |||
| case "星期四": | |||
| return DayOfWeek.THURSDAY; | |||
| case "星期五": | |||
| return DayOfWeek.FRIDAY; | |||
| case "星期六": | |||
| return DayOfWeek.SATURDAY; | |||
| case "星期日": | |||
| return DayOfWeek.SUNDAY; | |||
| default: | |||
| throw new IllegalArgumentException("不支持的星期: " + week); | |||
| } | |||
| } | |||
| } | |||