diff --git a/ruoyi-extend/ruoyi-monitor-admin/pom.xml b/ruoyi-extend/ruoyi-monitor-admin/pom.xml index 77c9eb71a..4c1ca9497 100644 --- a/ruoyi-extend/ruoyi-monitor-admin/pom.xml +++ b/ruoyi-extend/ruoyi-monitor-admin/pom.xml @@ -50,6 +50,11 @@ lombok + + org.dromara + ruoyi-common-mail + + 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: | + #### **{}** + - **服务名称**: {} + - **实例编号**: {} + - **服务状态**: {}({}) + - **服务地址**: {} + - **发送时间**: {}