malei05 1 ngày trước cách đây
mục cha
commit
10a5279ec5

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

@@ -0,0 +1,170 @@
+package net.yyc.quartz.executor;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.RequiredArgsConstructor;
+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("JobBriefingExecutor")
+@RequiredArgsConstructor
+public class JobBriefingExecutor {
+    private final JobBriefingService jobBriefingService;
+    private final EmailService emailService;
+
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    /**
+     * 生成并发送每日简报
+     *
+     * @return 执行结果(成功返回"success",失败返回异常信息)
+     */
+    public String sendDailyBriefing() {
+        try {
+            log.info("[简报] 开始生成每日简报...");
+
+            // 生成昨日简报
+            JobBriefing briefing = jobBriefingService.generateYesterdayBriefing();
+
+            // 构建邮件内容
+            String content = buildEmailContent(briefing);
+            String subject = String.format("【定时任务简报】%s", briefing.getReportDate().format(DATE_FORMATTER));
+
+            // 发送邮件
+            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;'>平均耗时(ms)</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;"; // 蓝色背景(无执行)
+            }
+
+            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;'>%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;'>%d</td>",
+                    job.getAvgExecuteTime()));
+            sb.append(String.format("<td style='padding: 8px; border: 1px solid #ddd;'>%s</td>",
+                    job.getLastExecuteTime() != null ? job.getLastExecuteTime().substring(0, Math.min(19, job.getLastExecuteTime().length())) : ""));
+            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;");
+    }
+}

+ 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();
+}

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

@@ -0,0 +1,25 @@
+package net.yyc.quartz.service.impl;
+
+import cn.hutool.extra.mail.MailUtil;
+import lombok.extern.slf4j.Slf4j;
+import net.yyc.quartz.service.EmailService;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+public class EmailServiceImpl implements EmailService {
+
+    private static final String mailTo = "malei@yaoyi.net";
+
+    @Override
+    public boolean sendBriefing(String content, String subject) {
+        try {
+            MailUtil.send(mailTo, subject, content, true);
+            log.info("[邮件] 发送成功 | subject={}", subject);
+            return true;
+        } catch (Exception e) {
+            log.error("[邮件] 发送失败 | subject={}", subject, e);
+            return false;
+        }
+    }
+}

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

@@ -0,0 +1,180 @@
+package net.yyc.quartz.service.impl;
+
+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.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 每日简报服务实现
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class JobBriefingServiceImpl implements JobBriefingService {
+
+    private final JobBriefingMapper jobBriefingMapper;
+    private final SysJobService sysJobService;
+
+    @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. 获取当前运行中的任务
+        List<SysJob> runningJobs = sysJobService.list(null);
+        if (runningJobs == null) {
+            runningJobs = Collections.emptyList();
+        }
+        List<SysJob> runningJobList = runningJobs.stream()
+                .filter(job -> "2".equals(job.getJobStatus()))
+                .collect(Collectors.toList());
+
+        // 3. 计算任务列表并集(昨日执行 + 当前运行中)
+        Set<Integer> allJobIds = new HashSet<>();
+        allJobIds.addAll(executedJobIds);
+        runningJobList.forEach(job -> allJobIds.add(job.getJobId()));
+
+        // 4. 获取任务ID到任务的映射
+        Map<Integer, SysJob> jobMap = new HashMap<>();
+        for (SysJob job : runningJobList) {
+            jobMap.put(job.getJobId(), job);
+        }
+
+        // 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 (Integer jobId : allJobIds) {
+            SysJob sysJob = jobMap.get(jobId);
+            Map<String, Object> stat = statMap.get(jobId);
+
+            String jobName = sysJob != null ? sysJob.getJobName() : String.valueOf(stat != null ? stat.get("job_name") : "未知");
+            String jobGroup = sysJob != null ? sysJob.getJobGroup() : String.valueOf(stat != null ? stat.get("job_group") : "未知");
+
+            int execCount = 0, successCount = 0, failCount = 0;
+            long avgExecuteTime = 0;
+            String lastExecuteTime = null;
+            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();
+                }
+                lastExecuteTime = stat.get("lastExecuteTime") != null
+                        ? String.valueOf(stat.get("lastExecuteTime")) : null;
+            }
+
+            // 查询最新执行记录
+            if (endTime != null) {
+                Map<String, Object> lastLog = jobBriefingMapper.selectLastExecLog(jobId, 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(jobId)
+                    .jobName(jobName)
+                    .jobGroup(jobGroup)
+                    .execCount(execCount)
+                    .successCount(successCount)
+                    .failCount(failCount)
+                    .successRate(Math.round(successRate * 100) / 100.0)
+                    .avgExecuteTime(avgExecuteTime)
+                    .lastExecuteTime(lastExecuteTime)
+                    .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 ("4".equals(job.getJobStatus())) {
+                continue;
+            }
+
+            // 检查昨日是否有执行记录
+            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;
+    }
+}

+ 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);
             // 直接执行任务