diff --git a/novel-crawl/pom.xml b/novel-crawl/pom.xml index 3cfe319..c8e78ca 100644 --- a/novel-crawl/pom.xml +++ b/novel-crawl/pom.xml @@ -37,6 +37,10 @@ jackson-databind + + org.springframework.boot + spring-boot-starter-mail + diff --git a/novel-crawl/src/main/java/com/java2nb/novel/core/bean/DiskInfo.java b/novel-crawl/src/main/java/com/java2nb/novel/core/bean/DiskInfo.java new file mode 100644 index 0000000..416ecbd --- /dev/null +++ b/novel-crawl/src/main/java/com/java2nb/novel/core/bean/DiskInfo.java @@ -0,0 +1,28 @@ +package com.java2nb.novel.core.bean; + +import lombok.Data; + +/** + * @author xiongxiaoyang + * @date 2025/10/24 + */ +@Data +public class DiskInfo { + + private String path; + private double totalGB; + private double freeGB; + private double usage; + + public DiskInfo(String path, long totalBytes, long freeBytes, double usage) { + this.path = path; + this.totalGB = bytesToGB(totalBytes); + this.freeGB = bytesToGB(freeBytes); + this.usage = usage; + } + + private double bytesToGB(long bytes) { + return bytes / (1024.0 * 1024.0 * 1024.0); + } + +} diff --git a/novel-crawl/src/main/java/com/java2nb/novel/core/config/DiskMonitorProperties.java b/novel-crawl/src/main/java/com/java2nb/novel/core/config/DiskMonitorProperties.java new file mode 100644 index 0000000..36a5fa9 --- /dev/null +++ b/novel-crawl/src/main/java/com/java2nb/novel/core/config/DiskMonitorProperties.java @@ -0,0 +1,25 @@ +package com.java2nb.novel.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author xiongxiaoyang + * @date 2025/10/24 + */ +@Component +@ConfigurationProperties(prefix = "disk-monitor") +@Data +public class DiskMonitorProperties { + + private boolean enabled; + private int intervalMinutes; + private double warningThreshold; + private double severeThreshold; + private double criticalThreshold; + private List recipients; + +} diff --git a/novel-crawl/src/main/java/com/java2nb/novel/core/schedule/DiskMonitorSchedule.java b/novel-crawl/src/main/java/com/java2nb/novel/core/schedule/DiskMonitorSchedule.java new file mode 100644 index 0000000..c7207fa --- /dev/null +++ b/novel-crawl/src/main/java/com/java2nb/novel/core/schedule/DiskMonitorSchedule.java @@ -0,0 +1,138 @@ +package com.java2nb.novel.core.schedule; + +import com.java2nb.novel.core.bean.DiskInfo; +import com.java2nb.novel.core.config.DiskMonitorProperties; +import com.java2nb.novel.service.EmailService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author xiongxiaoyang + * @date 2025/10/24 + */ +@ConditionalOnProperty(prefix = "disk-monitor", name = "enabled", havingValue = "true") +@Service +@RequiredArgsConstructor +@Slf4j +public class DiskMonitorSchedule { + + private final DiskMonitorProperties properties; + + private final EmailService emailService; + + private final AtomicBoolean criticalAlertSent = new AtomicBoolean(false); + + @Scheduled(fixedDelayString = "#{1000 * 60 * ${disk-monitor.interval-minutes}}") + public void checkDiskUsage() { + log.info("🔍 开始检查磁盘使用情况..."); + + File[] roots = File.listRoots(); + List diskInfos = new ArrayList<>(); + boolean criticalDetected = false; + + for (File root : roots) { + String path = root.getAbsolutePath().trim(); + if (path.isEmpty()) continue; + long total = root.getTotalSpace(); + if (total == 0) continue; + long free = root.getFreeSpace(); + double usage = (double)(total - free) / total * 100; + diskInfos.add(new DiskInfo(path, total, free, usage)); + if (usage >= properties.getCriticalThreshold()) { + criticalDetected = true; + } + } + + if (criticalDetected) { + if (criticalAlertSent.compareAndSet(false, true)) { + sendAlertEmail("CRITICAL", diskInfos); + log.error("🚨 磁盘使用率 ≥ 95%,10秒后终止进程..."); + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + System.exit(1); + } + } else { + criticalAlertSent.set(false); + double maxUsage = diskInfos.stream().mapToDouble(DiskInfo::getUsage).max().orElse(0); + if (maxUsage >= properties.getSevereThreshold() && maxUsage < properties.getCriticalThreshold()) { + sendAlertEmail("WARNING", diskInfos); + } else if (maxUsage >= properties.getWarningThreshold() && maxUsage < properties.getSevereThreshold()) { + sendAlertEmail("INFO", diskInfos); + } + } + } + + private void sendAlertEmail(String level, List diskInfos) { + Map model = new HashMap<>(); + model.put("diskInfos", diskInfos); + model.put("alertLevel", getAlertLevelText(level)); + model.put("icon", getIcon(level)); + model.put("actionText", getActionText(level)); + model.put("levelColor", getLevelColor(level)); + + String subject = getSubject(level); + emailService.sendHtmlEmail(subject, "disk_alert", model, properties.getRecipients()); + } + + private String getAlertLevelText(String level) { + return switch (level) { + case "CRITICAL" -> "严重"; + case "WARNING" -> "警告"; + case "INFO" -> "提示"; + default -> "通知"; + }; + } + + private String getIcon(String level) { + return switch (level) { + case "CRITICAL" -> "🚨"; + case "WARNING" -> "⚠️"; + case "INFO" -> "💡"; + default -> "📢"; + }; + } + + private String getActionText(String level) { + return switch (level) { + case "CRITICAL" -> + "⚠️ 检测到任一磁盘使用率 ≥ 95%,为防止系统崩溃,系统已自动关闭爬虫程序。"; + case "WARNING" -> + "📌 建议:请立即暂停爬虫程序,防止磁盘写满导致服务中断。"; + case "INFO" -> + "📌 提示:磁盘使用率已较高,请关注爬虫数据写入情况。"; + default -> ""; + }; + } + + private String getLevelColor(String level) { + return switch (level) { + case "CRITICAL" -> "#d32f2f"; + case "WARNING" -> "#f57c00"; + case "INFO" -> "#388e3c"; + default -> "#1976d2"; + }; + } + + private String getSubject(String level) { + return switch (level) { + case "CRITICAL" -> "🚨 严重告警:磁盘使用率超 95%,爬虫已关闭"; + case "WARNING" -> "⚠️ 警告:磁盘使用率超 90%,请暂停爬虫"; + case "INFO" -> "💡 提示:磁盘使用率超 85%"; + default -> "磁盘使用率告警"; + }; + } + +} diff --git a/novel-crawl/src/main/java/com/java2nb/novel/service/EmailService.java b/novel-crawl/src/main/java/com/java2nb/novel/service/EmailService.java new file mode 100644 index 0000000..474da6d --- /dev/null +++ b/novel-crawl/src/main/java/com/java2nb/novel/service/EmailService.java @@ -0,0 +1,14 @@ +package com.java2nb.novel.service; + +import java.util.List; +import java.util.Map; + +/** + * @author xiongxiaoyang + * @date 2025/10/24 + */ +public interface EmailService { + + void sendHtmlEmail(String subject, String templateName, Map templateModel, List to); + +} diff --git a/novel-crawl/src/main/java/com/java2nb/novel/service/impl/EmailServiceImpl.java b/novel-crawl/src/main/java/com/java2nb/novel/service/impl/EmailServiceImpl.java new file mode 100644 index 0000000..c3bf15e --- /dev/null +++ b/novel-crawl/src/main/java/com/java2nb/novel/service/impl/EmailServiceImpl.java @@ -0,0 +1,62 @@ +package com.java2nb.novel.service.impl; + +import com.java2nb.novel.service.EmailService; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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 org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +/** + * @author xiongxiaoyang + * @date 2025/10/24 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class EmailServiceImpl implements EmailService { + + private final JavaMailSender javaMailSender; + private final TemplateEngine templateEngine; + + @Value("${spring.mail.username}") + private String fromEmail; + + @Value("${spring.mail.nickname}") + private String nickName; + + @Override + public void sendHtmlEmail(String subject, String templateName, Map templateModel, List to) { + try { + MimeMessage message = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper( + message, + MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, + StandardCharsets.UTF_8.name()); + + Context context = new Context(); + context.setVariables(templateModel); + + String htmlBody = templateEngine.process(templateName, context); + + helper.setFrom(new InternetAddress(fromEmail, nickName, "UTF-8")); + helper.setTo(to.toArray(new String[0])); + helper.setSubject(subject); + helper.setText(htmlBody, true); + + javaMailSender.send(message); + } catch (Exception e) { + log.error("发送邮件失败"); + log.error(e.getMessage(), e); + } + } +} diff --git a/novel-crawl/src/main/resources/application.yml b/novel-crawl/src/main/resources/application.yml index b4908fe..de787fb 100644 --- a/novel-crawl/src/main/resources/application.yml +++ b/novel-crawl/src/main/resources/application.yml @@ -28,6 +28,41 @@ crawl: max: 500 - +--- + +#邮箱服务器 +spring: + mail: + host: smtp.163.com + #发件人昵称 + nickname: novel-plus + #邮箱账户 + username: xxyopen@163.com + #邮箱第三方授权码 + password: novel123456 + #编码类型 + default-encoding: UTF-8 + port: 465 + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: rue + socketFactory: + port: 465 + class: javax.net.ssl.SSLSocketFactory + fallback: false + +# 磁盘监控配置 +disk-monitor: + enabled: false + interval-minutes: 5 + warning-threshold: 85.0 + severe-threshold: 90.0 + critical-threshold: 95.0 + # 告警邮件接收人列表(支持多个邮箱) + recipients: xxyopen@foxmail.com,12345678@qq.com diff --git a/novel-crawl/src/main/resources/templates/disk_alert.html b/novel-crawl/src/main/resources/templates/disk_alert.html new file mode 100644 index 0000000..9629139 --- /dev/null +++ b/novel-crawl/src/main/resources/templates/disk_alert.html @@ -0,0 +1,44 @@ + + + + + + + +
+
+

+
+ +

时间:

+ +
+ +

📊 磁盘使用详情

+ + + + + + + + + + +
磁盘路径总空间 (GB)空闲空间 (GB)使用率
+
+ + +
+ + \ No newline at end of file