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