114 lines
4.9 KiB
C#
114 lines
4.9 KiB
C#
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,支持加签校验)
|
||
/// 报文:msg_type=post 富文本,用 at 标签(user_id=open_id)真正 @ 人;加签时附 timestamp(秒)+sign(算法见 ImWebhookSigner.FeishuSign)
|
||
/// </summary>
|
||
public class FeishuNotifier : IAlertNotifier
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
private readonly ILogger<FeishuNotifier> _logger;
|
||
|
||
public FeishuNotifier(HttpClient httpClient, ILogger<FeishuNotifier> logger)
|
||
{
|
||
_httpClient = httpClient;
|
||
_logger = logger;
|
||
}
|
||
|
||
public NotifyChannelTypeEnum ChannelType => NotifyChannelTypeEnum.Feishu;
|
||
|
||
public async Task<NotifyResult> 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<List<Dictionary<string, object>>>();
|
||
foreach (var raw in (message.Content ?? string.Empty).Split('\n'))
|
||
{
|
||
var line = raw.TrimEnd('\r');
|
||
if (line.Length == 0) continue;
|
||
contentLines.Add(new List<Dictionary<string, object>>
|
||
{
|
||
new Dictionary<string, object> { ["tag"] = "text", ["text"] = line }
|
||
});
|
||
}
|
||
if (message.AtMobiles.Count > 0)
|
||
{
|
||
var atLine = new List<Dictionary<string, object>>
|
||
{
|
||
new Dictionary<string, object> { ["tag"] = "text", ["text"] = "关注人: " }
|
||
};
|
||
foreach (var openId in message.AtMobiles)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(openId)) continue;
|
||
atLine.Add(new Dictionary<string, object> { ["tag"] = "at", ["user_id"] = openId.Trim() });
|
||
}
|
||
contentLines.Add(atLine);
|
||
}
|
||
|
||
var body = new Dictionary<string, object>
|
||
{
|
||
["msg_type"] = "post",
|
||
["content"] = new Dictionary<string, object>
|
||
{
|
||
["post"] = new Dictionary<string, object>
|
||
{
|
||
["zh_cn"] = new Dictionary<string, object>
|
||
{
|
||
["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) + "...";
|
||
}
|
||
}
|