TkEndpointManager.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. using dodohold.core;
  2. using Google.Protobuf.WellKnownTypes;
  3. using molilian.core;
  4. using System;
  5. using System.Collections.Concurrent;
  6. using System.Linq;
  7. public partial class TkEndpointManager
  8. {
  9. // 存储每个账号的端点状态(Key: accountId)
  10. private static readonly ConcurrentDictionary<int, AccountEndpointState> accountStates = new();
  11. // 记录每个账号最后一次挂起操作的时间
  12. private static readonly ConcurrentDictionary<int, DateTime> lastSuspendTimes = new();
  13. // 每个账号的挂起锁对象
  14. private static readonly ConcurrentDictionary<int, object> accountLocks = new();
  15. // 挂起冷却时间(3秒)
  16. private static readonly TimeSpan SuspendCooldown = TimeSpan.FromSeconds(3);
  17. // 替换原来的 ConcurrentDictionary<int, object>
  18. private static readonly ConcurrentDictionary<int, SemaphoreSlim> accountSemaphores = new();
  19. public static void Refresh()
  20. {
  21. accountStates.Clear();
  22. lastSuspendTimes.Clear();
  23. accountLocks.Clear();
  24. accountSemaphores.Clear();
  25. }
  26. // 从数据库获取所有可用的API端点
  27. private static async Task<Dictionary<int, string>> GetAllAvailableApisAsync(int accountId)
  28. {
  29. var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId);
  30. return endpoints?
  31. .Where(e => e.status)
  32. .ToDictionary(e => e.ep_id, e => e.endpoint) ?? new Dictionary<int, string>();
  33. }
  34. // 从数据库获取端点的挂起时间配置
  35. private static async Task<ConcurrentDictionary<string, int>> GetEndpointHoldMinutesAsync(int accountId)
  36. {
  37. var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId);
  38. var dict = new ConcurrentDictionary<string, int>();
  39. if (endpoints != null)
  40. {
  41. foreach (var endpoint in endpoints)
  42. {
  43. dict.TryAdd(endpoint.endpoint, endpoint.suspend_duration);
  44. }
  45. }
  46. return dict;
  47. }
  48. public static bool IsEndpointSuspended(int accountId, string endpoint)
  49. {
  50. if (!accountStates.TryGetValue(accountId, out var state))
  51. return false;
  52. return state.IsEndpointSuspended(endpoint);
  53. }
  54. // 新增:直接接收端点对象的版本
  55. public static bool IsEndpointSuspended(TkEndpointConfigDTO endpoint)
  56. {
  57. if (endpoint == null) return true;
  58. if (!accountStates.TryGetValue(endpoint.tk_pool_id, out var state))
  59. return false;
  60. return state.IsEndpointSuspended(endpoint.endpoint);
  61. }
  62. /// <summary>
  63. /// 检查指定账号是否至少拥有一个可用的endpoint
  64. /// </summary>
  65. /// <param name="accountId">账号ID</param>
  66. /// <returns>true如果至少有一个可用endpoint,否则false</returns>
  67. public static async Task<bool> HasAvailableEndpointAsync(int accountId)
  68. {
  69. var availableApis = await GetAllAvailableApisAsync(accountId);
  70. if (availableApis.Count == 0) return false;
  71. if (!accountStates.TryGetValue(accountId, out var state))
  72. {
  73. return true; // 如果账号没有状态记录,所有节点都是正常的
  74. }
  75. return availableApis.Any(api => !state.IsEndpointSuspended(api.Value));
  76. }
  77. /// <summary>
  78. /// 获取多个账号的节点状态信息
  79. /// </summary>
  80. public static async Task<Dictionary<int, Dictionary<string, string>>> GetStatus(IEnumerable<int> accountIds)
  81. {
  82. var result = new Dictionary<int, Dictionary<string, string>>();
  83. foreach (var accountId in accountIds.Distinct())
  84. {
  85. var availableApis = await GetAllAvailableApisAsync(accountId);
  86. result[accountId] = await GetStatus(accountId, availableApis);
  87. }
  88. return result;
  89. }
  90. /// <summary>
  91. /// 获取指定账号的节点状态信息
  92. /// </summary>
  93. public static async Task<Dictionary<string, string>> GetStatus(int accountId, Dictionary<int, string> availableApis)
  94. {
  95. var status = new Dictionary<string, string>();
  96. if (availableApis.Count == 0)
  97. {
  98. return status;
  99. }
  100. if (!accountStates.TryGetValue(accountId, out var state))
  101. {
  102. // 如果账号没有状态记录,所有节点都是正常的
  103. foreach ((var api_id, var api) in availableApis)
  104. {
  105. status[api] = "正常";
  106. }
  107. return status;
  108. }
  109. foreach ((var api_id, var api) in availableApis)
  110. {
  111. if (state.IsEndpointSuspended(api))
  112. {
  113. var suspendUntil = state.GetSuspendTime(api);
  114. var remainingTime = suspendUntil - DateTime.Now;
  115. status[api] = $"挂起 (剩余时间: {remainingTime:mm\\:ss})";
  116. }
  117. else
  118. {
  119. status[api] = "正常";
  120. }
  121. }
  122. return status;
  123. }
  124. public static async Task<(int, string)> GetConvertApiAsync(int accountId, string parseEndpoint)
  125. {
  126. var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId);
  127. if (endpoints == null || !endpoints.Any(e => e.status)) return (0, string.Empty);
  128. // 1. 过滤可用端点(状态正常)
  129. var availableApis = endpoints.Where(e => e.status);
  130. // 2. 如果指定了 parseEndpoint,按 ID 进一步过滤
  131. if (!string.IsNullOrEmpty(parseEndpoint))
  132. {
  133. var endpointIds = parseEndpoint.Split(',')
  134. .Select(idStr => int.TryParse(idStr.Trim(), out var id) ? id : (int?)null)
  135. .Where(id => id.HasValue)
  136. .Select(id => id.Value)
  137. .ToHashSet();
  138. // 添加日志,看看过滤前后的端点
  139. var beforeCount = availableApis.Count();
  140. availableApis = availableApis.Where(e => endpointIds.Contains(e.ep_id));
  141. var afterCount = availableApis.Count();
  142. if (!availableApis.Any())
  143. return (0, string.Empty);
  144. }
  145. var state = accountStates.GetOrAdd(accountId, _ => new AccountEndpointState());
  146. var now = DateTime.Now;
  147. // 3. 先找出所有未被挂起、未达限制的端点
  148. var availableEndpoints = availableApis
  149. .Where(e => !state.IsEndpointSuspended(e.endpoint))
  150. .Where(e => e.hourly_calls_limit <= 0 || e.current_hourly_calls < e.hourly_calls_limit)
  151. .Where(e => e.daily_calls_limit <= 0 || e.current_daily_calls < e.daily_calls_limit)
  152. .ToArray();
  153. // 4. 如果存在可用端点,直接返回
  154. if (availableEndpoints.Length > 0)
  155. {
  156. // 生成唯一的轮询key,确保每种可用端点组合都有独立索引
  157. string roundRobinKey = $"{accountId}:{string.Join(",", availableEndpoints.Select(e => e.ep_id).OrderBy(x => x))}";
  158. var currentIndex = state.GetOrAddAccountIndex(roundRobinKey);
  159. var newIndex = (currentIndex + 1) % availableEndpoints.Length;
  160. state.UpdateAccountIndex(roundRobinKey, newIndex);
  161. var selectedEndpoint = availableEndpoints[newIndex];
  162. // 异步更新调用统计(不阻塞当前请求)
  163. _ = UpdateEndpointStatsAsync(accountId, selectedEndpoint, now);
  164. return (selectedEndpoint.ep_id, selectedEndpoint.endpoint);
  165. }
  166. // 5. 检查是否仅因时间间隔限制
  167. var intervalLimitedEndpoints = availableApis
  168. .Where(e => !state.IsEndpointSuspended(e.endpoint))
  169. .Where(e => e.hourly_calls_limit <= 0 || e.current_hourly_calls < e.hourly_calls_limit)
  170. .Where(e => e.daily_calls_limit <= 0 || e.current_daily_calls < e.daily_calls_limit)
  171. .Where(e => e.interval_seconds > 0 &&
  172. e.last_call_time != null &&
  173. (now - e.last_call_time).TotalSeconds < e.interval_seconds)
  174. .ToArray();
  175. if (intervalLimitedEndpoints.Length > 0)
  176. {
  177. // 如果是时间间隔限制,返回空字符串
  178. return (0, string.Empty);
  179. }
  180. // 6. 检查所有端点的状态
  181. var allEndpointsSuspended = availableApis.All(e => state.IsEndpointSuspended(e.endpoint));
  182. var allEndpointsLimited = availableApis.All(e =>
  183. (e.hourly_calls_limit > 0 && e.current_hourly_calls >= e.hourly_calls_limit) ||
  184. (e.daily_calls_limit > 0 && e.current_daily_calls >= e.daily_calls_limit));
  185. // 如果所有端点都被挂起或达到限制,返回 "ALL"
  186. if (allEndpointsSuspended || allEndpointsLimited)
  187. {
  188. return (0, "ALL");
  189. }
  190. // 7. 其他情况(理论上不应该到达这里)
  191. return (0, string.Empty);
  192. }
  193. // 辅助方法:异步更新调用统计(原逻辑,仅拆分以保持清晰)
  194. private static async Task UpdateEndpointStatsAsync(int accountId, TkEndpointConfigDTO selectedEndpoint, DateTime now)
  195. {
  196. var currentHour = now.ToString("yyyyMMddHH");
  197. var currentDay = now.ToString("yyyyMMdd");
  198. // 更新调用计数
  199. selectedEndpoint.current_hourly_calls++;
  200. selectedEndpoint.current_daily_calls++;
  201. selectedEndpoint.last_call_time = now;
  202. // 保存到 Redis
  203. await RiskControlCore.SetTkEndpointCallsAsync(accountId, selectedEndpoint.id, currentHour, selectedEndpoint.current_hourly_calls);
  204. await RiskControlCore.SetTkEndpointCallsAsync(accountId, selectedEndpoint.id, currentDay, selectedEndpoint.current_daily_calls);
  205. // 检查是否需要挂起(原逻辑)
  206. if (selectedEndpoint.hourly_calls_limit > 0 && selectedEndpoint.current_hourly_calls >= selectedEndpoint.hourly_calls_limit)
  207. {
  208. _ = SuspendAsync(accountId, selectedEndpoint.endpoint, SuspendReason.HourlyLimit, selectedEndpoint.suspend_duration);
  209. }
  210. if (selectedEndpoint.daily_calls_limit > 0 && selectedEndpoint.current_daily_calls >= selectedEndpoint.daily_calls_limit)
  211. {
  212. _ = SuspendAsync(accountId, selectedEndpoint.endpoint, SuspendReason.DailyLimit, selectedEndpoint.suspend_duration);
  213. }
  214. }
  215. public static async Task SuspendAsync(int accountId, string endpoint, SuspendReason reason = SuspendReason.Passive, int? customHoldMinutes = null)
  216. {
  217. // 获取或创建账号特定的信号量
  218. var semaphore = accountSemaphores.GetOrAdd(accountId, _ => new SemaphoreSlim(1, 1));
  219. await semaphore.WaitAsync();
  220. try
  221. {
  222. // 在锁外部获取所需数据(避免在锁内await)
  223. var holdMinutes = await GetEndpointHoldMinutesAsync(accountId);
  224. // 如果是被动暂停,检查冷却时间
  225. if (lastSuspendTimes.TryGetValue(accountId, out var lastSuspendTime) &&
  226. DateTime.UtcNow - lastSuspendTime < SuspendCooldown)
  227. {
  228. return;
  229. }
  230. // 确定挂起时长
  231. TimeSpan suspendDuration;
  232. if (customHoldMinutes.HasValue)
  233. {
  234. suspendDuration = TimeSpan.FromMinutes(customHoldMinutes.Value);
  235. }
  236. else
  237. {
  238. suspendDuration = reason switch
  239. {
  240. SuspendReason.HourlyLimit => CalculateHourlySuspendDuration(),
  241. SuspendReason.DailyLimit => CalculateDailySuspendDuration(),
  242. _ => TimeSpan.FromMinutes(holdMinutes.GetValueOrDefault(endpoint, 240))
  243. };
  244. }
  245. // 执行挂起操作
  246. var state = accountStates.GetOrAdd(accountId, _ => new AccountEndpointState());
  247. state.SuspendEndpoint(endpoint, suspendDuration);
  248. lastSuspendTimes[accountId] = DateTime.UtcNow;
  249. _ = new LoggerLibrary("转链接口风控", $"{accountId}").Info($"{accountId}_{endpoint}", reason.ToString()).SaveAsync();
  250. _ = NotifyCore.NotifyAsync($"【转链接口风控】{accountId}_{endpoint} ({reason})");
  251. }
  252. finally
  253. {
  254. semaphore.Release();
  255. }
  256. }
  257. /// <summary>
  258. /// 主动释放指定账号的挂起状态
  259. /// </summary>
  260. /// <param name="accountId">账号ID</param>
  261. /// <param name="endpoint">要释放的端点名称,如果为null则释放所有端点的挂起状态</param>
  262. public static void ReleaseSuspend(int accountId, string endpoint = null)
  263. {
  264. // 获取或创建账号特定的锁对象
  265. var accountLock = accountLocks.GetOrAdd(accountId, _ => new object());
  266. lock (accountLock)
  267. {
  268. if (accountStates.TryGetValue(accountId, out var state))
  269. {
  270. if (endpoint == null)
  271. {
  272. // 释放所有端点的挂起状态
  273. state.suspendedEndpoints.Clear();
  274. }
  275. else
  276. {
  277. // 释放指定端点的挂起状态
  278. state.suspendedEndpoints.TryRemove(endpoint, out _);
  279. }
  280. // 记录日志
  281. var action = endpoint == null ? "释放所有挂起" : $"释放挂起({endpoint})";
  282. _ = new LoggerLibrary("转链接口风控", $"{accountId}").Info($"{accountId}", action).SaveAsync();
  283. _ = NotifyCore.NotifyAsync($"【转链接口风控】{accountId} {action}");
  284. }
  285. }
  286. }
  287. private static TimeSpan CalculateHourlySuspendDuration()
  288. {
  289. var now = DateTime.UtcNow;
  290. var nextHour = now.AddHours(1).Date.AddHours(now.Hour + 1); // 下一个整点(如 14:30 → 15:00)
  291. return nextHour - now;
  292. }
  293. private static TimeSpan CalculateDailySuspendDuration()
  294. {
  295. var now = DateTime.UtcNow;
  296. var tomorrow = now.Date.AddDays(1); // 次日零点
  297. return tomorrow - now;
  298. }
  299. public enum SuspendReason
  300. {
  301. /// <summary>
  302. /// 主动暂停(手动操作)
  303. /// </summary>
  304. Active,
  305. /// <summary>
  306. /// 风控导致的被动暂停
  307. /// </summary>
  308. Passive,
  309. /// <summary>
  310. /// 每小时总量限制触发的暂停
  311. /// </summary>
  312. HourlyLimit,
  313. /// <summary>
  314. /// 每日总量限制触发的暂停
  315. /// </summary>
  316. DailyLimit
  317. }
  318. private class AccountEndpointState
  319. {
  320. internal readonly ConcurrentDictionary<string, DateTime> suspendedEndpoints = new();
  321. // 为每个账号和 parseEndpoint 组合维护独立的索引
  322. private readonly ConcurrentDictionary<string, int> roundRobinIndices = new();
  323. public bool IsEndpointSuspended(string endpoint)
  324. {
  325. if (!suspendedEndpoints.TryGetValue(endpoint, out var suspendUntil))
  326. return false;
  327. if (DateTime.Now >= suspendUntil)
  328. {
  329. suspendedEndpoints.TryRemove(endpoint, out _);
  330. return false;
  331. }
  332. return true;
  333. }
  334. public DateTime? GetSuspendTime(string endpoint)
  335. {
  336. if (suspendedEndpoints.TryGetValue(endpoint, out var suspendUntil))
  337. {
  338. return suspendUntil;
  339. }
  340. return null;
  341. }
  342. public void SuspendEndpoint(string endpoint, TimeSpan duration)
  343. {
  344. suspendedEndpoints[endpoint] = DateTime.Now.Add(duration);
  345. }
  346. public int GetOrAddAccountIndex(string key) => roundRobinIndices.GetOrAdd(key, -1);
  347. public void UpdateAccountIndex(string key, int newIndex) => roundRobinIndices[key] = newIndex;
  348. }
  349. }