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,支持加签校验) /// 报文:msg_type=post 富文本,用 at 标签(user_id=open_id)真正 @ 人;加签时附 timestamp(秒)+sign(算法见 ImWebhookSigner.FeishuSign) /// public class FeishuNotifier : IAlertNotifier { private readonly HttpClient _httpClient; private readonly ILogger _logger; public FeishuNotifier(HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; } public NotifyChannelTypeEnum ChannelType => NotifyChannelTypeEnum.Feishu; public async Task SendAsync(AlertNotifyMessage message, string webhookUrl, string? secret, CancellationToken ct = default) { try { // 飞书 text 消息不支持结构化 @,必须用 post 富文本 + at 标签才能真正 @ 到人。 // at 标签的 user_id 为飞书 open_id(ou_ 开头);如需 @ 所有人,接收人填 "all"。 var contentLines = new List>>(); foreach (var raw in (message.Content ?? string.Empty).Split('\n')) { var line = raw.TrimEnd('\r'); if (line.Length == 0) continue; contentLines.Add(new List> { new Dictionary { ["tag"] = "text", ["text"] = line } }); } if (message.AtMobiles.Count > 0) { var atLine = new List> { new Dictionary { ["tag"] = "text", ["text"] = "关注人: " } }; foreach (var openId in message.AtMobiles) { if (string.IsNullOrWhiteSpace(openId)) continue; atLine.Add(new Dictionary { ["tag"] = "at", ["user_id"] = openId.Trim() }); } contentLines.Add(atLine); } var body = new Dictionary { ["msg_type"] = "post", ["content"] = new Dictionary { ["post"] = new Dictionary { ["zh_cn"] = new Dictionary { ["title"] = message.Title, ["content"] = contentLines } } } }; // 加签:timestamp(秒) + sign if (!string.IsNullOrWhiteSpace(secret)) { long timestampSec = DateTimeOffset.Now.ToUnixTimeSeconds(); body["timestamp"] = timestampSec.ToString(); body["sign"] = ImWebhookSigner.FeishuSign(timestampSec, secret); } string json = JsonSerializer.Serialize(body); _logger.LogInformation("飞书推送(post富文本) @open_id=[{At}]", string.Join(",", message.AtMobiles)); using var content = new StringContent(json, Encoding.UTF8, "application/json"); using var response = await _httpClient.PostAsync(webhookUrl, content, ct); string respText = await response.Content.ReadAsStringAsync(ct); if (!response.IsSuccessStatusCode) { return NotifyResult.Fail($"HTTP {(int)response.StatusCode}: {Truncate(respText)}"); } // 飞书返回 {"code":0,...}(旧版为 {"StatusCode":0,...}) using var doc = JsonDocument.Parse(respText); var root = doc.RootElement; bool ok = (root.TryGetProperty("code", out var code) && code.GetInt32() == 0) || (root.TryGetProperty("StatusCode", out var sc) && sc.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) + "..."; } }