Files
IOT_API/Service/Implement/Inspection/Notify/FeishuNotifier.cs
T

114 lines
4.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) + "...";
}
}