diff --git a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/properties/NotifyProperties.java b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/properties/NotifyProperties.java
new file mode 100644
index 000000000..8dba895be
--- /dev/null
+++ b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/config/properties/NotifyProperties.java
@@ -0,0 +1,90 @@
+package org.dromara.monitor.admin.config.properties;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 通知配置
+ *
+ * @author AprilWind
+ */
+@Data
+@ConfigurationProperties(prefix = "notify")
+@Configuration
+public class NotifyProperties {
+
+ /**
+ * 邮件设置
+ */
+ private Mail mail;
+
+ /**
+ * WebHooks 设置
+ */
+ private WebHook webHook;
+
+ @Data
+ @NoArgsConstructor
+ public static class Mail {
+
+ /**
+ * 发送开关
+ */
+ private Boolean enabled = false;
+
+ /**
+ * 收件人
+ */
+ private String to;
+
+ /**
+ * 主题
+ */
+ private String subject;
+
+ /**
+ * 邮件模版
+ */
+ private String template;
+
+ }
+
+ @Data
+ @NoArgsConstructor
+ public static class WebHook {
+
+ /**
+ * 发送开关
+ */
+ private Boolean enabled = false;
+
+ /**
+ * 0默认 1密码 2签名密钥
+ */
+ private String type = "0";
+
+ /**
+ * 签名密钥
+ */
+ private String secret;
+
+ /**
+ * 关键词
+ */
+ private String keywords;
+
+ /**
+ * Post地址
+ */
+ private String url;
+
+ /**
+ * WebHook发送模版
+ */
+ private String template;
+
+ }
+
+}
diff --git a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/event/NotifierEvent.java b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/event/NotifierEvent.java
new file mode 100644
index 000000000..7df53e460
--- /dev/null
+++ b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/event/NotifierEvent.java
@@ -0,0 +1,44 @@
+package org.dromara.monitor.admin.event;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 通知事件
+ *
+ * @author AprilWind
+ */
+@Data
+public class NotifierEvent implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 实例注册名称
+ */
+ private String registName;
+
+ /**
+ * 实例状态名称
+ */
+ private String statusName;
+
+ /**
+ * 实例ID
+ */
+ private String instanceId;
+
+ /**
+ * 实例状态
+ */
+ private String status;
+
+ /**
+ * 服务URL
+ */
+ private String serviceUrl;
+
+}
diff --git a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/notifier/CustomNotifier.java b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/notifier/CustomNotifier.java
index 838eefc47..e2b529449 100644
--- a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/notifier/CustomNotifier.java
+++ b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/notifier/CustomNotifier.java
@@ -6,6 +6,8 @@ import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent;
import de.codecentric.boot.admin.server.notify.AbstractEventNotifier;
import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.monitor.admin.event.NotifierEvent;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@@ -49,6 +51,13 @@ public class CustomNotifier extends AbstractEventNotifier {
};
log.info("Instance Status Change: 状态名称【{}】, 注册名称【{}】, 实例ID【{}】, 状态【{}】, 服务URL【{}】",
statusName, registName, instanceId, status, serviceUrl);
+ NotifierEvent notifier = new NotifierEvent();
+ notifier.setRegistName(registName);
+ notifier.setStatusName(statusName);
+ notifier.setInstanceId(instanceId);
+ notifier.setStatus(status);
+ notifier.setServiceUrl(serviceUrl);
+ SpringUtils.context().publishEvent(notifier);
}
});
}
diff --git a/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/notifier/InfoNotifier.java b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/notifier/InfoNotifier.java
new file mode 100644
index 000000000..d4e7232bd
--- /dev/null
+++ b/ruoyi-extend/ruoyi-monitor-admin/src/main/java/org/dromara/monitor/admin/notifier/InfoNotifier.java
@@ -0,0 +1,206 @@
+package org.dromara.monitor.admin.notifier;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.DateUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mail.utils.MailUtils;
+import org.dromara.monitor.admin.config.properties.NotifyProperties;
+import org.dromara.monitor.admin.event.NotifierEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 信息通知
+ *
+ * @author AprilWind
+ */
+@RequiredArgsConstructor
+@Slf4j
+@Service
+public class InfoNotifier {
+ private final NotifyProperties notifyProperties;
+
+ /**
+ * 处理服务通知事件
+ *
+ * 此方法会在服务状态发生变化时触发,并且会异步地发送 WebHook 通知
+ * 适用于服务的正常状态变更,不包括异常情况
+ *
+ * @param notifier 通知事件对象,包含服务的状态和其他相关信息
+ */
+ @Async
+ @EventListener
+ public void infoNotification(NotifierEvent notifier) {
+ sendWebHook(notifier);
+ }
+
+ /**
+ * 处理服务下线,离线或异常的通知事件
+ *
+ * 当服务的状态为“OFFLINE”、“DOWN”或“UNKNOWN”时,会触发此方法
+ * 该方法会异步发送邮件通知,以便及时处理服务问题
+ *
+ * @param notifier 通知事件对象,包含服务的状态和其他相关信息
+ */
+ @Async
+ @EventListener(condition = "#notifier.status == 'OFFLINE' || #notifier.status == 'DOWN' || #notifier.status == 'UNKNOWN'")
+ public void errNotification(NotifierEvent notifier) {
+ sendMail(notifier);
+ }
+
+ /**
+ * 发送邮件通知
+ *
+ * @param notifier 包含通知信息的对象
+ */
+ public void sendMail(NotifierEvent notifier) {
+ NotifyProperties.Mail mail = notifyProperties.getMail();
+ if (mail.getEnabled()) {
+ String message = StringUtils.format(mail.getTemplate(), notifier.getRegistName(), notifier.getInstanceId(),
+ notifier.getStatusName(), notifier.getStatus(), notifier.getServiceUrl(), DateUtils.getTime());
+ try {
+ MailUtils.sendHtml(mail.getTo(), notifier.getRegistName() + notifier.getStatusName(), message);
+ log.info("邮件已发送至: {}", mail.getTo());
+ } catch (Exception e) {
+ log.error("邮件发送失败: ", e);
+ }
+ }
+ }
+
+ /**
+ * 发送WebHook通知
+ *
+ * @param notifier 包含通知信息的对象
+ */
+ public void sendWebHook(NotifierEvent notifier) {
+ NotifyProperties.WebHook webHook = notifyProperties.getWebHook();
+ if (webHook.getEnabled()) {
+ String title = notifier.getRegistName() + notifier.getStatusName();
+ String message = StringUtils.format(webHook.getTemplate(), title, notifier.getRegistName(),
+ notifier.getInstanceId(), notifier.getStatusName(), notifier.getStatus(), notifier.getServiceUrl(), DateUtils.getTime());
+ try {
+ sendWebHookMessage(webHook, title, message);
+ log.info("WebHook消息已发送至: {}", webHook.getUrl());
+ } catch (Exception e) {
+ log.error("WebHook消息发送失败: ", e);
+ }
+ }
+ }
+
+ /**
+ * 发送WebHook消息
+ *
+ * @param webHook WebHook配置信息
+ * @param title 消息标题
+ * @param markdownMessage 消息内容
+ * @throws Exception 处理过程中可能抛出的异常
+ */
+ private void sendWebHookMessage(NotifyProperties.WebHook webHook, String title, String markdownMessage) throws Exception {
+ String jsonMessage = createJsonMessage(title, markdownMessage);
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest request = createHttpRequest(webHook, jsonMessage);
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ handleResponse(response);
+ }
+
+ /**
+ * 创建消息体的JSON字符串
+ *
+ * @param title 消息标题
+ * @param markdownMessage 消息内容
+ * @return JSON格式的消息体
+ * @throws Exception 处理过程中可能抛出的异常
+ */
+ private String createJsonMessage(String title, String markdownMessage) throws Exception {
+ Map messageBody = new HashMap<>();
+ messageBody.put("msgtype", "markdown");
+ Map markdownContent = new HashMap<>();
+ markdownContent.put("title", title + "通知");
+ markdownContent.put("text", markdownMessage);
+ messageBody.put("markdown", markdownContent);
+ ObjectMapper objectMapper = new ObjectMapper();
+ return objectMapper.writeValueAsString(messageBody);
+ }
+
+ /**
+ * 根据WebHook配置创建HTTP请求
+ *
+ * @param webHook WebHook配置信息
+ * @param jsonMessage JSON格式的消息体
+ * @return 创建的HTTP请求
+ * @throws Exception 处理过程中可能抛出的异常
+ */
+ private HttpRequest createHttpRequest(NotifyProperties.WebHook webHook, String jsonMessage) throws Exception {
+ HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(jsonMessage));
+
+ if ("2".equals(webHook.getType())) {
+ String signedUrl = generateSignedUrl(webHook);
+ requestBuilder.uri(new URI(signedUrl));
+ } else {
+ requestBuilder.uri(new URI(webHook.getUrl()));
+ }
+
+ return requestBuilder.build();
+ }
+
+ /**
+ * 生成带签名的URL
+ *
+ * @param webHook WebHook配置信息
+ * @return 带签名的URL
+ * @throws Exception 处理过程中可能抛出的异常
+ */
+ private String generateSignedUrl(NotifyProperties.WebHook webHook) throws Exception {
+ long timestamp = System.currentTimeMillis();
+ String secret = webHook.getSecret();
+ String stringToSign = timestamp + "\n" + secret;
+
+ Mac mac = Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
+ byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
+ String sign = URLEncoder.encode(Base64.getEncoder().encodeToString(signData), StandardCharsets.UTF_8);
+
+ return String.format("%s×tamp=%d&sign=%s", webHook.getUrl(), timestamp, sign);
+ }
+
+ /**
+ * 处理HTTP响应
+ *
+ * @param response HTTP响应
+ */
+ private void handleResponse(HttpResponse response) {
+ try {
+ // 解析响应体
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode jsonNode = objectMapper.readTree(response.body());
+ int errcode = jsonNode.has("errcode") ? jsonNode.get("errcode").asInt() : -1;
+ String errmsg = jsonNode.has("errmsg") ? jsonNode.get("errmsg").asText() : "Unknown error";
+ if (errcode == 0) {
+ log.info("WebHook消息发送成功");
+ } else {
+ log.error("WebHook消息发送失败: errcode={}, errmsg={}", errcode, errmsg);
+ }
+ } catch (Exception e) {
+ log.error("处理WebHook响应时发生异常: {}", e.getMessage());
+ }
+ }
+
+}
diff --git a/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml b/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml
index beee58775..66f293282 100644
--- a/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml
+++ b/ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml
@@ -46,3 +46,51 @@ spring.boot.admin.client:
userpassword: ${spring.boot.admin.client.password}
username: ruoyi
password: 123456
+
+--- # mail 邮件发送
+mail:
+ enabled: false
+ host: smtp.163.com
+ port: 465
+ # 是否需要用户名密码验证
+ auth: true
+ # 发送方,遵循RFC-822标准
+ from: xxx@163.com
+ # 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
+ user: xxx@163.com
+ # 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
+ pass: xxxxxxxxxx
+ # 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
+ starttlsEnable: true
+ # 使用SSL安全连接
+ sslEnable: true
+ # SMTP超时时长,单位毫秒,缺省值不超时
+ timeout: 0
+ # Socket连接超时值,单位毫秒,缺省值不超时
+ connectionTimeout: 0
+
+# 服务通知
+notify:
+ mail:
+ enabled: false
+ to:
+ subject: 系统通知
+ template: "
+ 服务名称: {}
+ 实例编号: {}
+ 服务状态: {}({})
+ 服务地址: {}
+ 发送时间: {}
+ "
+ web-hook:
+ enabled: true
+ type: 2
+ secret: SEC4d035df24951f00445ea16022f0db2d442fdc83b34c6ebe15a427572264f6798
+ url: https://oapi.dingtalk.com/robot/send?access_token=81a122dbbfbcbe237acdd9e20e421059fc2e0d80bf83ded4099b0fef9f9800ef
+ template: |
+ #### **{}**
+ - **服务名称**: {}
+ - **实例编号**: {}
+ - **服务状态**: {}({})
+ - **服务地址**: {}
+ - **发送时间**: {}