搭建消息告警模块以及跨网推送服务,支持企微,飞书,钉钉通知

This commit is contained in:
2026-09-03 14:02:35 +08:00
parent cfa8e78d33
commit 3927b0c201
40 changed files with 4360 additions and 7 deletions
@@ -0,0 +1,95 @@
using Common.Notify;
using Model.Entity.Inspection;
using Service.Interface;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Service.Implement
{
/// <summary>
/// 钉钉群机器人通知发送器(自定义机器人 Webhook,支持加签校验)
/// 报文:msgtype=markdownat.atMobiles 支持 @手机号;
/// 加签:URL 追加 timestamp(毫秒)+sign(算法见 ImWebhookSigner.DingTalkSign
/// </summary>
public class DingTalkNotifier : IAlertNotifier
{
private readonly HttpClient _httpClient;
private readonly ILogger<DingTalkNotifier> _logger;
public DingTalkNotifier(HttpClient httpClient, ILogger<DingTalkNotifier> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public NotifyChannelTypeEnum ChannelType => NotifyChannelTypeEnum.DingTalk;
public async Task<NotifyResult> SendAsync(AlertNotifyMessage message, string webhookUrl, string? secret, CancellationToken ct = default)
{
try
{
string url = webhookUrl;
// 加签:URL 追加 timestamp 与 sign 参数
if (!string.IsNullOrWhiteSpace(secret))
{
long timestampMs = DateTimeOffset.Now.ToUnixTimeMilliseconds();
string sign = ImWebhookSigner.DingTalkSign(timestampMs, secret);
url += $"{(url.Contains('?') ? "&" : "?")}timestamp={timestampMs}&sign={sign}";
}
// markdown 正文中 @ 手机号需同时出现在 atMobiles 与正文里才会真正提醒
var text = new StringBuilder();
text.AppendLine($"### {message.Title}");
text.AppendLine(message.Content);
foreach (var mobile in message.AtMobiles)
{
text.Append($"@{mobile} ");
}
var body = new Dictionary<string, object>
{
["msgtype"] = "markdown",
["markdown"] = new Dictionary<string, object>
{
["title"] = message.Title,
["text"] = text.ToString()
},
["at"] = new Dictionary<string, object>
{
["atMobiles"] = message.AtMobiles,
["isAtAll"] = false
}
};
string json = JsonSerializer.Serialize(body);
_logger.LogInformation("钉钉推送 atMobiles=[{At}]", string.Join(",", message.AtMobiles));
using var content = new StringContent(json, Encoding.UTF8, "application/json");
using var response = await _httpClient.PostAsync(url, content, ct);
string respText = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
{
return NotifyResult.Fail($"HTTP {(int)response.StatusCode}: {Truncate(respText)}");
}
// 钉钉返回 {"errcode":0,"errmsg":"ok"}
using var doc = JsonDocument.Parse(respText);
bool ok = doc.RootElement.TryGetProperty("errcode", out var errcode) && errcode.GetInt32() == 0;
return ok ? NotifyResult.Ok() : NotifyResult.Fail(Truncate(respText));
}
catch (Exception ex)
{
return NotifyResult.Fail($"钉钉推送异常: {NotifyError.Describe(ex)}");
}
}
private static string Truncate(string s, int max = 200) =>
string.IsNullOrEmpty(s) || s.Length <= max ? s : s.Substring(0, max) + "...";
}
}