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

148 lines
6.7 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 Microsoft.Extensions.Logging;
using Service.Interface;
using System;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Service.Implement
{
/// <summary>
/// 飞书通讯录服务实现:手机号 → open_id
/// 依赖飞书自建应用凭证(FeishuConfig)tenant_access_token 与 open_id 结果均做进程级内存缓存,
/// 避免每条告警都调用飞书 APItoken 有效期约 7200sopen_id 基本不变)。
/// </summary>
public class FeishuContactService : IFeishuContactService
{
private const string TokenUrl = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
private const string BatchGetIdUrl = "https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id?user_id_type=open_id";
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<FeishuContactService> _logger;
// tenant_access_token 进程级缓存(提前 5 分钟刷新,避免边界失效)
private static string? _token;
private static DateTime _tokenExpireAt = DateTime.MinValue;
private static readonly SemaphoreSlim _tokenLock = new(1, 1);
// 手机号 → open_id 结果缓存(换不到的也缓存 null 避免反复调用;重启进程可重置)
private static readonly ConcurrentDictionary<string, string?> _openIdCache = new();
public FeishuContactService(IHttpClientFactory httpClientFactory, ILogger<FeishuContactService> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
/// <summary>
/// 用手机号换取飞书 open_id;未配置凭证、查无此人或无权限时返回 null
/// </summary>
public async Task<string?> GetOpenIdByMobileAsync(string mobile)
{
if (string.IsNullOrWhiteSpace(mobile)) return null;
mobile = mobile.Trim();
if (_openIdCache.TryGetValue(mobile, out var cached)) return cached;
if (!FeishuConfig.IsConfigured)
{
_logger.LogWarning("飞书应用凭证未配置(AppId/AppSecret),无法把手机号 {Mobile} 转 open_id;请在 appsettings.json 的 Feishu 节配置", mobile);
return null;
}
try
{
var token = await GetTenantAccessTokenAsync();
if (string.IsNullOrEmpty(token)) return null;
var http = _httpClientFactory.CreateClient();
string body = JsonSerializer.Serialize(new { mobiles = new[] { mobile } });
using var req = new HttpRequestMessage(HttpMethod.Post, BatchGetIdUrl);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var resp = await http.SendAsync(req);
string respText = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(respText);
var root = doc.RootElement;
int code = root.TryGetProperty("code", out var c) ? c.GetInt32() : -1;
if (code != 0)
{
string msg = root.TryGetProperty("msg", out var m) ? m.GetString() ?? "" : "";
_logger.LogWarning("飞书手机号换 open_id 失败 mobile={Mobile} code={Code} msg={Msg}(多为权限未开通或通讯录范围不含该成员)", mobile, code, msg);
_openIdCache[mobile] = null;
return null;
}
string? openId = null;
if (root.TryGetProperty("data", out var data) &&
data.TryGetProperty("user_list", out var list) &&
list.ValueKind == JsonValueKind.Array && list.GetArrayLength() > 0 &&
list[0].TryGetProperty("user_id", out var uid))
{
openId = uid.GetString();
}
if (string.IsNullOrEmpty(openId))
_logger.LogWarning("飞书手机号 {Mobile} 未查到 open_id(该手机号可能不在应用通讯录范围内)", mobile);
_openIdCache[mobile] = openId;
return openId;
}
catch (Exception ex)
{
_logger.LogError("飞书手机号换 open_id 异常 mobile={Mobile}: {Err}", mobile, NotifyError.Describe(ex));
return null;
}
}
/// <summary>
/// 获取 tenant_access_token(进程级缓存,提前 5 分钟刷新)
/// </summary>
private async Task<string?> GetTenantAccessTokenAsync()
{
if (!string.IsNullOrEmpty(_token) && DateTime.Now < _tokenExpireAt) return _token;
await _tokenLock.WaitAsync();
try
{
if (!string.IsNullOrEmpty(_token) && DateTime.Now < _tokenExpireAt) return _token;
var http = _httpClientFactory.CreateClient();
string body = JsonSerializer.Serialize(new { app_id = FeishuConfig.AppId, app_secret = FeishuConfig.AppSecret });
using var content = new StringContent(body, Encoding.UTF8, "application/json");
using var resp = await http.PostAsync(TokenUrl, content);
string respText = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(respText);
var root = doc.RootElement;
int code = root.TryGetProperty("code", out var c) ? c.GetInt32() : -1;
if (code != 0)
{
string msg = root.TryGetProperty("msg", out var m) ? m.GetString() ?? "" : "";
_logger.LogError("飞书获取 tenant_access_token 失败 code={Code} msg={Msg}(检查 AppId/AppSecret", code, msg);
return null;
}
_token = root.TryGetProperty("tenant_access_token", out var t) ? t.GetString() : null;
int expire = root.TryGetProperty("expire", out var e) ? e.GetInt32() : 7200;
_tokenExpireAt = DateTime.Now.AddSeconds(Math.Max(60, expire - 300));
return _token;
}
catch (Exception ex)
{
_logger.LogError("飞书获取 tenant_access_token 异常: {Err}", NotifyError.Describe(ex));
return null;
}
finally
{
_tokenLock.Release();
}
}
}
}