|
|
@@ -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("&", "&")
|
|
|
+ .replace("<", "<")
|
|
|
+ .replace(">", ">")
|
|
|
+ .replace("\"", """)
|
|
|
+ .replace("'", "'");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化时长,便于阅读
|
|
|
+ */
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+}
|