Explorar el Código

Merge branch 'feat-20260918-dailyBriefling' of yyc3/yyc-quartz-service into master

malei hace 1 mes
padre
commit
f0cc57afb7

+ 6 - 0
pom.xml

@@ -144,6 +144,12 @@
             <version>1.0.0-SNAPSHOT</version>
         </dependency>
 
+        <!-- Spring Boot Mail (替换 Hutool MailUtil) -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-mail</artifactId>
+        </dependency>
+
         <!-- Hutool 工具库 -->
         <dependency>
             <groupId>cn.hutool</groupId>

+ 26 - 29
src/main/java/net/yyc/quartz/config/QuartzInitJob.java

@@ -17,12 +17,12 @@
 
 package net.yyc.quartz.config;
 
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import net.yyc.quartz.constants.QuartzEnum;
 import net.yyc.quartz.entity.SysJob;
 import net.yyc.quartz.service.SysJobService;
 import net.yyc.quartz.util.TaskUtil;
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
 import org.quartz.Scheduler;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -39,35 +39,32 @@ import java.util.List;
 @AllArgsConstructor
 public class QuartzInitJob {
 
-	private final SysJobService sysJobService;
+    private final SysJobService sysJobService;
 
-	private final TaskUtil taskUtil;
+    private final TaskUtil taskUtil;
 
-	private final Scheduler scheduler;
+    private final Scheduler scheduler;
 
-	@Bean
-	public void customize() {
-		List<SysJob> jobList = sysJobService.list();
-		log.info("========== 开始加载定时任务,共 {} 条 ==========", jobList.size());
-		jobList.forEach(sysjob -> {
-			if (QuartzEnum.JOB_STATUS_RELEASE.getType().equals(sysjob.getJobStatus())) {
-				log.info("[QUARTZ] [INIT] 移除 | jobId={} jobName={} 状态=已发布", sysjob.getJobId(), sysjob.getJobName());
-				taskUtil.removeJob(sysjob, scheduler);
-			}
-			else if (QuartzEnum.JOB_STATUS_RUNNING.getType().equals(sysjob.getJobStatus())) {
-				log.info("[QUARTZ] [INIT] 调度 | jobId={} jobName={}", sysjob.getJobId(), sysjob.getJobName());
-				taskUtil.addOrUpdateJob(sysjob, scheduler);
-			}
-			else if (QuartzEnum.JOB_STATUS_NOT_RUNNING.getType().equals(sysjob.getJobStatus())) {
-				log.info("[QUARTZ] [INIT] 暂停 | jobId={} jobName={}", sysjob.getJobId(), sysjob.getJobName());
-				taskUtil.addOrUpdateJob(sysjob, scheduler);
-			}
-			else {
-				log.info("[QUARTZ] [INIT] 移除 | jobId={} jobName={} 状态={}", sysjob.getJobId(), sysjob.getJobName(), sysjob.getJobStatus());
-				taskUtil.removeJob(sysjob, scheduler);
-			}
-		});
-		log.info("========== 定时任务加载完成 ==========");
-	}
+    @Bean
+    public void customize() {
+        List<SysJob> jobList = sysJobService.list();
+        log.info("========== 开始加载定时任务,共 {} 条 ==========", jobList.size());
+        jobList.forEach(sysjob -> {
+            if (QuartzEnum.JOB_STATUS_RELEASE.getType().equals(sysjob.getJobStatus())) {
+                log.info("[QUARTZ] [INIT] 移除 | jobId={} jobName={} 状态=已发布", sysjob.getJobId(), sysjob.getJobName());
+                taskUtil.removeJob(sysjob, scheduler);
+            } else if (QuartzEnum.JOB_STATUS_RUNNING.getType().equals(sysjob.getJobStatus())) {
+                log.info("[QUARTZ] [INIT] 调度 | jobId={} jobName={}", sysjob.getJobId(), sysjob.getJobName());
+                taskUtil.resumeJob(sysjob, scheduler);
+            } else if (QuartzEnum.JOB_STATUS_NOT_RUNNING.getType().equals(sysjob.getJobStatus())) {
+                log.info("[QUARTZ] [INIT] 暂停 | jobId={} jobName={}", sysjob.getJobId(), sysjob.getJobName());
+                taskUtil.pauseJob(sysjob, scheduler);
+            } else {
+                log.info("[QUARTZ] [INIT] 移除 | jobId={} jobName={} 状态={}", sysjob.getJobId(), sysjob.getJobName(), sysjob.getJobStatus());
+                taskUtil.removeJob(sysjob, scheduler);
+            }
+        });
+        log.info("========== 定时任务加载完成 ==========");
+    }
 
 }

+ 122 - 0
src/main/java/net/yyc/quartz/entity/JobBriefing.java

@@ -0,0 +1,122 @@
+package net.yyc.quartz.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 每日简报数据模型
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class JobBriefing {
+
+    /**
+     * 报告日期
+     */
+    private LocalDate reportDate;
+
+    /**
+     * 任务统计列表
+     */
+    private List<JobStatInfo> jobStats;
+
+    /**
+     * 错过执行的任务列表
+     */
+    private List<MissedJobInfo> missedJobs;
+
+    /**
+     * 生成时间
+     */
+    private LocalDateTime generatedAt;
+
+    @Data
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class JobStatInfo {
+        /**
+         * 任务ID
+         */
+        private Integer jobId;
+
+        /**
+         * 任务名称
+         */
+        private String jobName;
+
+        /**
+         * 任务组
+         */
+        private String jobGroup;
+
+        /**
+         * 任务状态(1已发布 2运行中 3暂停)
+         */
+        private String jobStatus;
+
+        /**
+         * 下次执行时间
+         */
+        private String nextExecuteTime;
+
+        /**
+         * 执行次数
+         */
+        private int execCount;
+
+        /**
+         * 成功次数
+         */
+        private int successCount;
+
+        /**
+         * 失败次数
+         */
+        private int failCount;
+
+        /**
+         * 成功率
+         */
+        private double successRate;
+
+        /**
+         * 平均执行时间(ms)
+         */
+        private long avgExecuteTime;
+
+        /**
+         * 最后执行时间
+         */
+        private String lastExecuteTime;
+
+        /**
+         * 最后执行状态
+         */
+        private String lastExecStatus;
+
+        /**
+         * 异常信息(如果有)
+         */
+        private String exceptionInfo;
+    }
+
+    @Data
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class MissedJobInfo {
+        private String jobName;
+        private String jobGroup;
+        private String nextExecuteTime;
+        private String remark;
+    }
+}

+ 0 - 1
src/main/java/net/yyc/quartz/event/SysJobLogListener.java

@@ -40,7 +40,6 @@ public class SysJobLogListener {
 	public void saveSysJobLog(SysJobLogEvent event) {
 		SysJobLog sysJobLog = event.getSysJobLog();
 		sysJobLogService.save(sysJobLog);
-		log.info("执行定时任务日志");
 	}
 
 }

+ 202 - 0
src/main/java/net/yyc/quartz/executor/JobBriefingExecutor.java

@@ -0,0 +1,202 @@
+package net.yyc.quartz.executor;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import lombok.extern.slf4j.Slf4j;
+import net.yyc.quartz.entity.JobBriefing;
+import net.yyc.quartz.service.EmailService;
+import net.yyc.quartz.service.JobBriefingService;
+import org.springframework.stereotype.Component;
+
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 每日简报执行器
+ * 每天早上9点执行,生成昨日任务执行简报并发送邮件
+ */
+@Slf4j
+@Component(value = "jobBriefingExecutor")
+@RequiredArgsConstructor
+public class JobBriefingExecutor {
+    private final JobBriefingService jobBriefingService;
+    private final EmailService emailService;
+
+    @Value("${spring.profiles.active:unknown}")
+    private String activeProfile;
+
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    /**
+     * 生成并发送每日简报
+     *
+     * @return 执行结果(成功返回"success",失败返回异常信息)
+     */
+    public String sendDailyBriefing() {
+        try {
+            log.info("[简报] 开始生成每日简报...");
+            JobBriefing briefing = jobBriefingService.generateYesterdayBriefing();
+
+            log.info("[简报] 开始构建简报内容");
+            String content = buildEmailContent(briefing);
+            String subject = String.format("【%s-定时任务简报】%s", activeProfile, briefing.getReportDate().format(DATE_FORMATTER));
+
+            log.info("[简报] 准备发送邮件");
+            boolean success = emailService.sendBriefing(content, subject);
+            if (success) {
+                log.info("[简报] 每日简报发送成功 | 日期={} | 任务数={} | 错过执行={}",
+                        briefing.getReportDate(), briefing.getJobStats().size(), briefing.getMissedJobs().size());
+                return "success";
+            } else {
+                log.warn("[简报] 每日简报发送失败");
+                return "send failed";
+            }
+        } catch (Exception e) {
+            log.error("[简报] 生成每日简报异常", e);
+            throw new RuntimeException("生成每日简报异常: " + e.getMessage(), e);
+        }
+    }
+
+    private String buildEmailContent(JobBriefing briefing) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("<html><body style='font-family: Arial, sans-serif; font-size: 14px;'>");
+
+        // 标题
+        sb.append("<h2 style='color: #333; margin-bottom: 20px;'>定时任务每日简报</h2>");
+        sb.append(String.format("<p><b>报告日期:</b>%s</p>",
+                briefing.getReportDate().format(DATE_FORMATTER)));
+
+        // 错过执行告警
+        if (!briefing.getMissedJobs().isEmpty()) {
+            sb.append("<div style='background-color: #fff3cd; border: 1px solid #ffc107; padding: 15px; margin-bottom: 20px; border-radius: 5px;'>");
+            sb.append("<h3 style='color: #856404; margin-top: 0;'>⚠️ 错过执行</h3>");
+            sb.append("<table style='border-collapse: collapse; width: 100%;'>");
+            sb.append("<tr style='background-color: #fff8e1;'>");
+            sb.append("<th style='padding: 8px; border: 1px solid #ffc107; text-align: left;'>任务名称</th>");
+            sb.append("<th style='padding: 8px; border: 1px solid #ffc107; text-align: left;'>任务组</th>");
+            sb.append("<th style='padding: 8px; border: 1px solid #ffc107; text-align: left;'>下次执行时间</th>");
+            sb.append("<th style='padding: 8px; border: 1px solid #ffc107; text-align: left;'>说明</th>");
+            sb.append("</tr>");
+            for (JobBriefing.MissedJobInfo job : briefing.getMissedJobs()) {
+                sb.append("<tr>");
+                sb.append(String.format("<td style='padding: 8px; border: 1px solid #ffc107;'>%s</td>",
+                        escapeHtml(job.getJobName())));
+                sb.append(String.format("<td style='padding: 8px; border: 1px solid #ffc107;'>%s</td>",
+                        escapeHtml(job.getJobGroup())));
+                sb.append(String.format("<td style='padding: 8px; border: 1px solid #ffc107;'>%s</td>",
+                        escapeHtml(job.getNextExecuteTime())));
+                sb.append(String.format("<td style='padding: 8px; border: 1px solid #ffc107; color: #856404;'>%s</td>",
+                        escapeHtml(job.getRemark())));
+                sb.append("</tr>");
+            }
+            sb.append("</table>");
+            sb.append("</div>");
+        }
+
+        // 任务统计列表
+        sb.append("<h3 style='color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px;'>任务执行情况</h3>");
+        sb.append("<table style='border-collapse: collapse; width: 100%; font-size: 12px;'>");
+        sb.append("<tr style='background-color: #f5f5f5;'>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: left;'>任务名称</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: left;'>任务组</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: center;'>状态</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: center;'>执行次数</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: center;'>成功</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: center;'>失败</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: center;'>成功率</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: center;'>平均耗时</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: center;'>下次执行</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: center;'>最后执行</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: center;'>末次结果</th>");
+        sb.append("<th style='padding: 8px; border: 1px solid #ddd; text-align: left;'>异常信息</th>");
+        sb.append("</tr>");
+
+        for (JobBriefing.JobStatInfo job : briefing.getJobStats()) {
+            // 根据失败次数设置行背景色
+            String rowBgColor = "";
+            if (job.getFailCount() > 0) {
+                rowBgColor = "background-color: #ffebee;"; // 红色背景(失败)
+            } else if (job.getSuccessRate() < 90 && job.getExecCount() > 0) {
+                rowBgColor = "background-color: #fff3e0;"; // 橙色背景(成功率低)
+            } else if (job.getExecCount() == 0) {
+                rowBgColor = "background-color: #e3f2fd;"; // 蓝色背景(无执行)
+            }
+
+            // 格式化任务状态文字
+            String jobStatusText = "";
+            if ("1".equals(job.getJobStatus())) jobStatusText = "已发布";
+            else if ("2".equals(job.getJobStatus())) jobStatusText = "运行中";
+            else if ("3".equals(job.getJobStatus())) jobStatusText = "暂停";
+
+            // 格式化平均耗时
+            String avgTimeText = formatDuration(job.getAvgExecuteTime());
+
+            // 格式化最后执行时间(只显示月日时分的后19位)
+            String lastExecText = "";
+            if (job.getLastExecuteTime() != null && job.getLastExecuteTime().length() >= 16) {
+                lastExecText = job.getLastExecuteTime().substring(5, 16); // MM-DD HH:mm
+            }
+
+            sb.append(String.format("<tr style='%s'>", rowBgColor));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd;'><b>%s</b></td>",
+                    escapeHtml(job.getJobName())));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd;'>%s</td>",
+                    escapeHtml(job.getJobGroup())));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd; text-align: center;'>%s</td>",
+                    jobStatusText));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd; text-align: center;'>%d</td>",
+                    job.getExecCount()));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd; text-align: center; color: green;'>%d</td>",
+                    job.getSuccessCount()));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd; text-align: center; %s'>%d</td>",
+                    job.getFailCount() > 0 ? "color: red; font-weight: bold;" : "",
+                    job.getFailCount()));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd; text-align: center; %s'>%.2f%%</td>",
+                    job.getSuccessRate() < 90 ? "color: orange; font-weight: bold;" : "",
+                    job.getSuccessRate()));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd; text-align: center;'>%s</td>",
+                    avgTimeText));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd;'>%s</td>",
+                    job.getNextExecuteTime() != null ? job.getNextExecuteTime() : "-"));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd;'>%s</td>",
+                    lastExecText));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd; text-align: center; %s'>%s</td>",
+                    "失败".equals(job.getLastExecStatus()) ? "color: red; font-weight: bold;" : "color: green;",
+                    job.getLastExecStatus() != null ? job.getLastExecStatus() : "-"));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd; font-size: 11px; color: #666;'>%s</td>",
+                    escapeHtml(job.getExceptionInfo())));
+            sb.append("</tr>");
+        }
+        sb.append("</table>");
+
+        // 页脚
+        sb.append(String.format("<p style='color: #999; font-size: 12px; margin-top: 20px;'>生成时间:%s</p>",
+                briefing.getGeneratedAt()));
+        sb.append("</body></html>");
+
+        return sb.toString();
+    }
+
+    private String escapeHtml(String text) {
+        if (StrUtil.isBlank(text) || "null".equals(text)) {
+            return "";
+        }
+        return text.replace("&", "&amp;")
+                .replace("<", "&lt;")
+                .replace(">", "&gt;")
+                .replace("\"", "&quot;")
+                .replace("'", "&#39;");
+    }
+
+    /**
+     * 格式化时长,便于阅读
+     */
+    private String formatDuration(long ms) {
+        if (ms < 0) return "-";
+        if (ms < 1000) return ms + "ms";
+        if (ms < 60000) return String.format("%.1fs", ms / 1000.0);
+        long minutes = ms / 60000;
+        long seconds = (ms % 60000) / 1000;
+        return String.format("%dm%ds", minutes, seconds);
+    }
+}

+ 33 - 0
src/main/java/net/yyc/quartz/mapper/JobBriefingMapper.java

@@ -0,0 +1,33 @@
+package net.yyc.quartz.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 简报数据访问接口
+ */
+@Mapper
+public interface JobBriefingMapper {
+
+    /**
+     * 按任务ID统计执行情况
+     */
+    List<Map<String, Object>> selectJobStatSummary(@Param("startTime") LocalDateTime startTime,
+                                                    @Param("endTime") LocalDateTime endTime);
+
+    /**
+     * 查询指定日期有执行记录的任务ID列表
+     */
+    List<Integer> selectExecutedJobIds(@Param("startTime") LocalDateTime startTime,
+                                        @Param("endTime") LocalDateTime endTime);
+
+    /**
+     * 查询任务最新一条执行记录
+     */
+    Map<String, Object> selectLastExecLog(@Param("jobId") Integer jobId,
+                                           @Param("endTime") LocalDateTime endTime);
+}

+ 17 - 0
src/main/java/net/yyc/quartz/service/EmailService.java

@@ -0,0 +1,17 @@
+package net.yyc.quartz.service;
+
+/**
+ * 邮件发送服务接口
+ * 后续自行实现邮件发送逻辑
+ */
+public interface EmailService {
+
+    /**
+     * 发送简报邮件
+     *
+     * @param content 邮件内容(HTML格式)
+     * @param subject 邮件主题
+     * @return 是否发送成功
+     */
+    boolean sendBriefing(String content, String subject);
+}

+ 26 - 0
src/main/java/net/yyc/quartz/service/JobBriefingService.java

@@ -0,0 +1,26 @@
+package net.yyc.quartz.service;
+
+import net.yyc.quartz.entity.JobBriefing;
+
+import java.time.LocalDate;
+
+/**
+ * 每日简报服务接口
+ */
+public interface JobBriefingService {
+
+    /**
+     * 生成指定日期的每日简报
+     *
+     * @param date 报告日期
+     * @return 简报数据
+     */
+    JobBriefing generateBriefing(LocalDate date);
+
+    /**
+     * 生成昨日的每日简报
+     *
+     * @return 简报数据
+     */
+    JobBriefing generateYesterdayBriefing();
+}

+ 42 - 0
src/main/java/net/yyc/quartz/service/impl/EmailServiceImpl.java

@@ -0,0 +1,42 @@
+package net.yyc.quartz.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.yyc.quartz.service.EmailService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import javax.mail.internet.MimeMessage;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class EmailServiceImpl implements EmailService {
+    private static final String MAIL_TO = "malei@yaoyi.net";
+
+    @Resource
+    private JavaMailSender mailSender;
+    @Value("${spring.mail.username:noreply@yaoyi.net}")
+    private String mailFrom;
+
+    @Override
+    public boolean sendBriefing(String content, String subject) {
+        try {
+            MimeMessage message = mailSender.createMimeMessage();
+            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
+            helper.setTo(MAIL_TO);
+            helper.setFrom(mailFrom);
+            helper.setSubject(subject);
+            helper.setText(content, true);
+            mailSender.send(message);
+            log.info("[邮件] 发送成功 | subject={}", subject);
+            return true;
+        } catch (Exception e) {
+            log.error("[邮件] 发送失败 | subject={}", subject, e);
+            return false;
+        }
+    }
+}

+ 169 - 0
src/main/java/net/yyc/quartz/service/impl/JobBriefingServiceImpl.java

@@ -0,0 +1,169 @@
+package net.yyc.quartz.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.yyc.quartz.entity.JobBriefing;
+import net.yyc.quartz.entity.SysJob;
+import net.yyc.quartz.mapper.JobBriefingMapper;
+import net.yyc.quartz.service.JobBriefingService;
+import net.yyc.quartz.service.SysJobService;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 每日简报服务实现
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class JobBriefingServiceImpl implements JobBriefingService {
+
+    private final JobBriefingMapper jobBriefingMapper;
+    private final SysJobService sysJobService;
+
+    private static final DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Override
+    public JobBriefing generateBriefing(LocalDate date) {
+        LocalDateTime startTime = date.atStartOfDay();
+        LocalDateTime endTime = date.atTime(LocalTime.MAX);
+
+        // 1. 获取昨日执行的任务列表
+        List<Integer> executedJobIds = jobBriefingMapper.selectExecutedJobIds(startTime, endTime);
+
+        // 2. 获取非删除且非暂停的任务(1已发布 2运行中)
+        LambdaQueryWrapper<SysJob> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(SysJob::getJobStatus, "1", "2");
+        List<SysJob> runningJobList = sysJobService.list(queryWrapper);
+        if (runningJobList == null) {
+            runningJobList = Collections.emptyList();
+        }
+
+        // 3. 任务ID列表
+        Set<Integer> allJobIds = runningJobList.stream()
+                .map(SysJob::getJobId)
+                .collect(Collectors.toSet());
+
+        // 5. 查询昨日执行统计
+        List<Map<String, Object>> statSummary = jobBriefingMapper.selectJobStatSummary(startTime, endTime);
+        Map<Integer, Map<String, Object>> statMap = new HashMap<>();
+        for (Map<String, Object> stat : statSummary) {
+            Integer jobId = ((Number) stat.get("job_id")).intValue();
+            statMap.put(jobId, stat);
+        }
+
+        // 6. 构建任务统计列表
+        List<JobBriefing.JobStatInfo> jobStats = new ArrayList<>();
+        for (SysJob sysJob : runningJobList) {
+            Map<String, Object> stat = statMap.get(sysJob.getJobId());
+
+            String jobName = sysJob.getJobName();
+            String jobGroup = sysJob.getJobGroup();
+
+            int execCount = 0, successCount = 0, failCount = 0;
+            long avgExecuteTime = 0;
+            String lastExecStatus = null;
+            String exceptionInfo = null;
+
+            if (stat != null) {
+                execCount = ((Number) stat.get("execCount")).intValue();
+                successCount = ((Number) stat.get("successCount")).intValue();
+                failCount = ((Number) stat.get("failCount")).intValue();
+                Object avgObj = stat.get("avgExecuteTime");
+                if (avgObj != null) {
+                    avgExecuteTime = ((Number) avgObj).longValue();
+                }
+            }
+
+            // 查询最新执行记录
+            Map<String, Object> lastLog = jobBriefingMapper.selectLastExecLog(sysJob.getJobId(), LocalDateTime.now());
+            if (lastLog != null) {
+                lastExecStatus = "1".equals(String.valueOf(lastLog.get("job_log_status"))) ? "失败" : "成功";
+                Object exInfo = lastLog.get("exception_info");
+                if (exInfo != null && !"null".equals(String.valueOf(exInfo))) {
+                    exceptionInfo = String.valueOf(exInfo);
+                    if (exceptionInfo.length() > 200) {
+                        exceptionInfo = exceptionInfo.substring(0, 200) + "...";
+                    }
+                }
+            }
+
+            double successRate = execCount > 0 ? (double) successCount / execCount * 100 : 0;
+
+            jobStats.add(JobBriefing.JobStatInfo.builder()
+                    .jobId(sysJob.getJobId())
+                    .jobName(jobName)
+                    .jobGroup(jobGroup)
+                    .jobStatus(sysJob.getJobStatus())
+                    .nextExecuteTime(sysJob.getNextTime() != null
+                            ? sysJob.getNextTime().format(defaultDateTimeFormatter) : null)
+                    .execCount(execCount)
+                    .successCount(successCount)
+                    .failCount(failCount)
+                    .successRate(Math.round(successRate * 100) / 100.0)
+                    .avgExecuteTime(avgExecuteTime)
+                    .lastExecuteTime(sysJob.getPreviousTime() != null ? sysJob.getPreviousTime().format(defaultDateTimeFormatter) : "")
+                    .lastExecStatus(lastExecStatus)
+                    .exceptionInfo(exceptionInfo)
+                    .build());
+        }
+
+        // 按任务名称排序
+        jobStats.sort(Comparator.comparing(JobBriefing.JobStatInfo::getJobName));
+
+        // 7. 检测错过执行的任务
+        List<JobBriefing.MissedJobInfo> missedJobs = detectMissedJobs(date, startTime, endTime, executedJobIds, runningJobList);
+
+        return JobBriefing.builder()
+                .reportDate(date)
+                .jobStats(jobStats)
+                .missedJobs(missedJobs)
+                .generatedAt(LocalDateTime.now())
+                .build();
+    }
+
+    @Override
+    public JobBriefing generateYesterdayBriefing() {
+        return generateBriefing(LocalDate.now().minusDays(1));
+    }
+
+    /**
+     * 检测错过执行的任务
+     * 条件:任务状态=RUNNING(2) 且 next_time已过 且 昨日无执行记录
+     */
+    private List<JobBriefing.MissedJobInfo> detectMissedJobs(LocalDate date,
+                                                             LocalDateTime startTime,
+                                                             LocalDateTime endTime,
+                                                             List<Integer> executedJobIds,
+                                                             List<SysJob> runningJobList) {
+        List<JobBriefing.MissedJobInfo> missed = new ArrayList<>();
+        Set<Integer> executedSet = new HashSet<>(executedJobIds);
+        LocalDateTime now = LocalDateTime.now();
+
+        for (SysJob job : runningJobList) {
+            // 检查昨日是否有执行记录
+            if (executedSet.contains(job.getJobId())) {
+                continue;
+            }
+
+            // 检查 next_time 是否已过
+            if (job.getNextTime() != null && job.getNextTime().isBefore(now)) {
+                missed.add(JobBriefing.MissedJobInfo.builder()
+                        .jobName(job.getJobName())
+                        .jobGroup(job.getJobGroup())
+                        .nextExecuteTime(job.getNextTime() != null ? job.getNextTime().toString() : null)
+                        .remark("下次执行时间已过,请检查任务调度")
+                        .build());
+            }
+        }
+
+        return missed;
+    }
+}

+ 5 - 1
src/main/java/net/yyc/quartz/util/SpringBeanTaskInvoke.java

@@ -23,6 +23,7 @@ import net.yyc.common.core.util.SpringContextHolder;
 import net.yyc.quartz.constants.QuartzEnum;
 import net.yyc.quartz.entity.SysJob;
 import net.yyc.quartz.exception.TaskException;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.stereotype.Component;
 import org.springframework.util.ReflectionUtils;
 
@@ -44,8 +45,8 @@ public class SpringBeanTaskInvoke implements ITaskInvoke {
         Method method;
         Object returnValue;
         // 通过Spring上下文去找 也有可能找不到
-        target = SpringContextHolder.getBean(sysJob.getClassName());
         try {
+            target = SpringContextHolder.getBean(sysJob.getClassName());
             if (StrUtil.isNotEmpty(sysJob.getMethodParamsValue())) {
                 method = target.getClass().getDeclaredMethod(sysJob.getMethodName(), String.class);
                 ReflectionUtils.makeAccessible(method);
@@ -65,6 +66,9 @@ public class SpringBeanTaskInvoke implements ITaskInvoke {
                 log.error("[QUARTZ] SpringBean任务执行失败 | bean={} method={}", sysJob.getClassName(), sysJob.getMethodName());
                 throw new TaskException("定时任务springBeanTaskInvoke业务执行失败,任务:" + sysJob.getClassName());
             }
+        } catch (NoSuchBeanDefinitionException e) {
+            log.error("[QUARTZ] Bean未找到 | bean={} 请检查类名", sysJob.getClassName(), e);
+            throw new TaskException("Bean未找到, bean=" + sysJob.getClassName());
         } catch (NoSuchMethodException e) {
             log.error("[QUARTZ] Bean方法未找到 | bean={} method={} 请检查方法名和参数是否匹配", sysJob.getClassName(), sysJob.getMethodName(), e);
             throw new TaskException("Bean方法未找到, bean=" + sysJob.getClassName() + ", method=" + sysJob.getMethodName());

+ 0 - 49
src/main/java/net/yyc/quartz/util/SpringContextHolder.java

@@ -1,49 +0,0 @@
-package net.yyc.quartz.util;
-
-import org.springframework.beans.BeansException;
-import org.springframework.context.ApplicationContext;
-import org.springframework.context.ApplicationContextAware;
-import org.springframework.stereotype.Component;
-
-/**
- * Spring 上下文持有者
- */
-@Component
-public class SpringContextHolder implements ApplicationContextAware {
-
-    private static ApplicationContext applicationContext;
-
-    @Override
-    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
-        SpringContextHolder.applicationContext = applicationContext;
-    }
-
-    /**
-     * 获取 Spring 应用上下文
-     */
-    public static ApplicationContext getApplicationContext() {
-        return applicationContext;
-    }
-
-    /**
-     * 获取指定类型的 Bean
-     *
-     * @param name Bean 名称
-     * @return Bean 实例
-     */
-    @SuppressWarnings("unchecked")
-    public static <T> T getBean(String name) {
-        return (T) applicationContext.getBean(name);
-    }
-
-    /**
-     * 获取指定类型的 Bean
-     *
-     * @param clazz Bean 类型
-     * @return Bean 实例
-     */
-    public static <T> T getBean(Class<T> clazz) {
-        return applicationContext.getBean(clazz);
-    }
-
-}

+ 2 - 9
src/main/java/net/yyc/quartz/util/TaskInvokFactory.java → src/main/java/net/yyc/quartz/util/TaskInvokeFactory.java

@@ -9,20 +9,13 @@ import lombok.extern.slf4j.Slf4j;
 /**
  * @author Hccake
  * @version 1.0
- * @date 2019/8/8 15:40 TaskInvok工厂类
  */
 @Slf4j
-public class TaskInvokFactory {
+public class TaskInvokeFactory {
 
-	/**
-	 * 根据对应jobType获取对应 invoker
-	 * @param jobType
-	 * @return
-	 * @throws TaskException
-	 */
 	public static ITaskInvoke getInvoker(String jobType) throws TaskException {
 		if (StrUtil.isBlank(jobType)) {
-			log.info("获取TaskInvok传递参数有误,jobType:{}", jobType);
+			log.info("获取TaskInvoke传递参数有误,jobType:{}", jobType);
 			throw new TaskException("");
 		}
 

+ 1 - 1
src/main/java/net/yyc/quartz/util/TaskInvokeUtil.java

@@ -69,7 +69,7 @@ public class TaskInvokeUtil {
         sysJobLog.setTenantId(sysJob.getTenantId());
         try {
             // 执行任务
-            ITaskInvoke iTaskInvoke = TaskInvokFactory.getInvoker(sysJob.getJobType());
+            ITaskInvoke iTaskInvoke = TaskInvokeFactory.getInvoker(sysJob.getJobType());
             // 设置uuid
             sysJob.setUuid(executionId);
             // 直接执行任务

+ 11 - 13
src/main/java/net/yyc/quartz/util/TaskUtil.java

@@ -60,27 +60,17 @@ public class TaskUtil {
             trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
             // 判断触发器是否存在(如果存在说明之前运行过但是在当前被禁用了,如果不存在说明一次都没运行过)
             if (trigger == null) {
-                // 新建一个工作任务 指定任务类型为串接进行的
                 JobDetail jobDetail = JobBuilder.newJob(QuartzFactory.class).withIdentity(jobKey).build();
-                // 将任务信息添加到任务信息中
                 jobDetail.getJobDataMap().put(SCHEDULE_JOB_KEY.getType(), sysjob);
-                // 将cron表达式进行转换
                 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(sysjob.getCronExpression());
                 cronScheduleBuilder = this.handleCronScheduleMisfirePolicy(sysjob, cronScheduleBuilder);
-                // 创建触发器并将cron表达式对象给塞入
-                trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder)
-                        .build();
-                // 在调度器中将触发器和任务进行组合
+                trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
                 scheduler.scheduleJob(jobDetail, trigger);
             } else {
                 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(sysjob.getCronExpression());
                 cronScheduleBuilder = this.handleCronScheduleMisfirePolicy(sysjob, cronScheduleBuilder);
-                // 按照新的规则进行
-                trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder)
-                        .build();
-                // 将任务信息更新到任务信息中
+                trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
                 trigger.getJobDataMap().put(SCHEDULE_JOB_KEY.getType(), sysjob);
-                // 重启
                 scheduler.rescheduleJob(triggerKey, trigger);
             }
             // 如任务状态为暂停
@@ -128,8 +118,16 @@ public class TaskUtil {
      * 恢复定时任务
      */
     public void resumeJob(SysJob sysjob, Scheduler scheduler) {
+        if (scheduler == null) {
+            return;
+        }
+
         try {
-            if (scheduler != null) {
+            TriggerKey triggerKey = getTriggerKey(sysjob);
+            Trigger trigger = scheduler.getTrigger(triggerKey);
+            if (trigger == null) {
+                addOrUpdateJob(sysjob, scheduler);
+            } else {
                 scheduler.resumeJob(getJobKey(sysjob));
             }
         } catch (SchedulerException e) {

+ 42 - 0
src/main/resources/mapper/JobBriefingMapper.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="net.yyc.quartz.mapper.JobBriefingMapper">
+
+    <!-- 按任务ID统计执行情况 -->
+    <select id="selectJobStatSummary" resultType="map">
+        SELECT
+            job_id,
+            job_name,
+            job_group,
+            COUNT(*) as execCount,
+            SUM(CASE WHEN job_log_status = '0' THEN 1 ELSE 0 END) as successCount,
+            SUM(CASE WHEN job_log_status = '1' THEN 1 ELSE 0 END) as failCount,
+            AVG(CAST(execute_time AS SIGNED)) as avgExecuteTime,
+            MAX(create_time) as lastExecuteTime
+        FROM sys_job_log
+        WHERE create_time BETWEEN #{startTime} AND #{endTime}
+        GROUP BY job_id, job_name, job_group
+    </select>
+
+    <!-- 查询指定日期有执行记录的任务ID -->
+    <select id="selectExecutedJobIds" resultType="java.lang.Integer">
+        SELECT DISTINCT job_id
+        FROM sys_job_log
+        WHERE create_time BETWEEN #{startTime} AND #{endTime}
+    </select>
+
+    <!-- 查询任务最新一条执行记录 -->
+    <select id="selectLastExecLog" resultType="map">
+        SELECT
+            job_log_status,
+            exception_info,
+            create_time
+        FROM sys_job_log
+        WHERE job_id = #{jobId}
+          AND create_time &lt;= #{endTime}
+        ORDER BY create_time DESC
+        LIMIT 1
+    </select>
+
+</mapper>