|
@@ -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("&", "&")
|
|
|
|
|
+ .replace("<", "<")
|
|
|
|
|
+ .replace(">", ">")
|
|
|
|
|
+ .replace("\"", """)
|
|
|
|
|
+ .replace("'", "'");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|