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 { /// /// 钉钉群机器人通知发送器(自定义机器人 Webhook,支持加签校验) /// 报文:msgtype=markdown,at.atMobiles 支持 @手机号; /// 加签:URL 追加 timestamp(毫秒)+sign(算法见 ImWebhookSigner.DingTalkSign) /// public class DingTalkNotifier : IAlertNotifier { private readonly HttpClient _httpClient; private readonly ILogger _logger; public DingTalkNotifier(HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; } public NotifyChannelTypeEnum ChannelType => NotifyChannelTypeEnum.DingTalk; public async Task 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 { ["msgtype"] = "markdown", ["markdown"] = new Dictionary { ["title"] = message.Title, ["text"] = text.ToString() }, ["at"] = new Dictionary { ["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) + "..."; } }