|
|
@@ -7,44 +7,33 @@ using System.Linq;
|
|
|
|
|
|
public partial class TkEndpointManager
|
|
|
{
|
|
|
- // 存储每个账号的端点状态(Key: accountId)
|
|
|
+ // 只保留轮询索引
|
|
|
private static readonly ConcurrentDictionary<int, AccountEndpointState> accountStates = new();
|
|
|
|
|
|
// 记录每个账号最后一次挂起操作的时间
|
|
|
private static readonly ConcurrentDictionary<int, DateTime> lastSuspendTimes = new();
|
|
|
|
|
|
- // 每个账号的挂起锁对象
|
|
|
- private static readonly ConcurrentDictionary<int, object> accountLocks = new();
|
|
|
-
|
|
|
- // 挂起冷却时间(3秒)
|
|
|
- private static readonly TimeSpan SuspendCooldown = TimeSpan.FromSeconds(3);
|
|
|
-
|
|
|
- // 替换原来的 ConcurrentDictionary<int, object>
|
|
|
- private static readonly ConcurrentDictionary<int, SemaphoreSlim> accountSemaphores = new();
|
|
|
-
|
|
|
|
|
|
public static void Refresh()
|
|
|
{
|
|
|
accountStates.Clear();
|
|
|
lastSuspendTimes.Clear();
|
|
|
- accountLocks.Clear();
|
|
|
- accountSemaphores.Clear();
|
|
|
}
|
|
|
|
|
|
|
|
|
// 从数据库获取所有可用的API端点
|
|
|
- private static async Task<Dictionary<int, string>> GetAllAvailableApisAsync(int accountId)
|
|
|
+ private static async Task<Dictionary<int, string>> GetAllAvailableApisAsync(int accountId, bool? isTaobaoUrl = null, string parseEndpoint = null)
|
|
|
{
|
|
|
- var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId);
|
|
|
+ var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId, isTaobaoUrl, parseEndpoint);
|
|
|
return endpoints?
|
|
|
.Where(e => e.status)
|
|
|
.ToDictionary(e => e.ep_id, e => e.endpoint) ?? new Dictionary<int, string>();
|
|
|
}
|
|
|
|
|
|
// 从数据库获取端点的挂起时间配置
|
|
|
- private static async Task<ConcurrentDictionary<string, int>> GetEndpointHoldMinutesAsync(int accountId)
|
|
|
+ private static async Task<ConcurrentDictionary<string, int>> GetEndpointHoldMinutesAsync(int accountId, bool? isTaobaoUrl = null, string parseEndpoint = null)
|
|
|
{
|
|
|
- var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId);
|
|
|
+ var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId, isTaobaoUrl, parseEndpoint);
|
|
|
var dict = new ConcurrentDictionary<string, int>();
|
|
|
|
|
|
if (endpoints != null)
|
|
|
@@ -59,23 +48,9 @@ public partial class TkEndpointManager
|
|
|
}
|
|
|
public static bool IsEndpointSuspended(int accountId, string endpoint)
|
|
|
{
|
|
|
- if (!accountStates.TryGetValue(accountId, out var state))
|
|
|
- return false;
|
|
|
-
|
|
|
- return state.IsEndpointSuspended(endpoint);
|
|
|
+ return RedisHelper.Exists($"tk_suspend:{accountId}:{endpoint}");
|
|
|
}
|
|
|
|
|
|
- // 新增:直接接收端点对象的版本
|
|
|
- public static bool IsEndpointSuspended(TkEndpointConfigDTO endpoint)
|
|
|
- {
|
|
|
- if (endpoint == null) return true;
|
|
|
- if (!accountStates.TryGetValue(endpoint.tk_pool_id, out var state))
|
|
|
- return false;
|
|
|
-
|
|
|
- return state.IsEndpointSuspended(endpoint.endpoint);
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
/// <summary>
|
|
|
/// 检查指定账号是否至少拥有一个可用的endpoint
|
|
|
/// </summary>
|
|
|
@@ -91,7 +66,7 @@ public partial class TkEndpointManager
|
|
|
return true; // 如果账号没有状态记录,所有节点都是正常的
|
|
|
}
|
|
|
|
|
|
- return availableApis.Any(api => !state.IsEndpointSuspended(api.Value));
|
|
|
+ return availableApis.Any(api => !IsEndpointSuspended(accountId, api.Value));
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -133,11 +108,10 @@ public partial class TkEndpointManager
|
|
|
|
|
|
foreach ((var api_id, var api) in availableApis)
|
|
|
{
|
|
|
- if (state.IsEndpointSuspended(api))
|
|
|
+ if (IsEndpointSuspended(accountId, api))
|
|
|
{
|
|
|
- var suspendUntil = state.GetSuspendTime(api);
|
|
|
- var remainingTime = suspendUntil - DateTime.Now;
|
|
|
- status[api] = $"挂起 (剩余时间: {remainingTime:mm\\:ss})";
|
|
|
+ // Redis没有挂起到期时间,展示"挂起"即可
|
|
|
+ status[api] = $"挂起";
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
@@ -147,43 +121,25 @@ public partial class TkEndpointManager
|
|
|
return status;
|
|
|
}
|
|
|
|
|
|
- public static async Task<(int, string)> GetConvertApiAsync(int accountId, string parseEndpoint)
|
|
|
+ public static async Task<(int, string)> GetConvertApiAsync(int accountId, bool isTaobaoUrl, string parseEndpoint)
|
|
|
{
|
|
|
- var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId);
|
|
|
+ var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId, isTaobaoUrl, parseEndpoint);
|
|
|
if (endpoints == null || !endpoints.Any(e => e.status)) return (0, string.Empty);
|
|
|
|
|
|
// 1. 过滤可用端点(状态正常)
|
|
|
var availableApis = endpoints.Where(e => e.status);
|
|
|
|
|
|
- // 2. 如果指定了 parseEndpoint,按 ID 进一步过滤
|
|
|
- if (!string.IsNullOrEmpty(parseEndpoint))
|
|
|
- {
|
|
|
- var endpointIds = parseEndpoint.Split(',')
|
|
|
- .Select(idStr => int.TryParse(idStr.Trim(), out var id) ? id : (int?)null)
|
|
|
- .Where(id => id.HasValue)
|
|
|
- .Select(id => id.Value)
|
|
|
- .ToHashSet();
|
|
|
-
|
|
|
- // 添加日志,看看过滤前后的端点
|
|
|
- var beforeCount = availableApis.Count();
|
|
|
- availableApis = availableApis.Where(e => endpointIds.Contains(e.ep_id));
|
|
|
- var afterCount = availableApis.Count();
|
|
|
-
|
|
|
- if (!availableApis.Any())
|
|
|
- return (0, string.Empty);
|
|
|
- }
|
|
|
-
|
|
|
var state = accountStates.GetOrAdd(accountId, _ => new AccountEndpointState());
|
|
|
var now = DateTime.Now;
|
|
|
|
|
|
- // 3. 先找出所有未被挂起、未达限制的端点
|
|
|
+ // 2. 先找出所有未被挂起、未达限制的端点
|
|
|
var availableEndpoints = availableApis
|
|
|
- .Where(e => !state.IsEndpointSuspended(e.endpoint))
|
|
|
+ .Where(e => !IsEndpointSuspended(accountId, e.endpoint))
|
|
|
.Where(e => e.hourly_calls_limit <= 0 || e.current_hourly_calls < e.hourly_calls_limit)
|
|
|
.Where(e => e.daily_calls_limit <= 0 || e.current_daily_calls < e.daily_calls_limit)
|
|
|
.ToArray();
|
|
|
|
|
|
- // 4. 如果存在可用端点,直接返回
|
|
|
+ // 3. 如果存在可用端点,直接返回
|
|
|
if (availableEndpoints.Length > 0)
|
|
|
{
|
|
|
// 生成唯一的轮询key,确保每种可用端点组合都有独立索引
|
|
|
@@ -198,9 +154,9 @@ public partial class TkEndpointManager
|
|
|
return (selectedEndpoint.ep_id, selectedEndpoint.endpoint);
|
|
|
}
|
|
|
|
|
|
- // 5. 检查是否仅因时间间隔限制
|
|
|
+ // 4. 检查是否仅因时间间隔限制
|
|
|
var intervalLimitedEndpoints = availableApis
|
|
|
- .Where(e => !state.IsEndpointSuspended(e.endpoint))
|
|
|
+ .Where(e => !IsEndpointSuspended(accountId, e.endpoint))
|
|
|
.Where(e => e.hourly_calls_limit <= 0 || e.current_hourly_calls < e.hourly_calls_limit)
|
|
|
.Where(e => e.daily_calls_limit <= 0 || e.current_daily_calls < e.daily_calls_limit)
|
|
|
.Where(e => e.interval_seconds > 0 &&
|
|
|
@@ -214,9 +170,9 @@ public partial class TkEndpointManager
|
|
|
return (0, string.Empty);
|
|
|
}
|
|
|
|
|
|
- // 6. 检查所有端点的状态
|
|
|
- var allEndpointsSuspended = availableApis.All(e => state.IsEndpointSuspended(e.endpoint));
|
|
|
- var allEndpointsLimited = availableApis.All(e =>
|
|
|
+ // 5. 检查所有端点的状态
|
|
|
+ var allEndpointsSuspended = availableApis.All(e => IsEndpointSuspended(accountId, e.endpoint));
|
|
|
+ var allEndpointsLimited = availableApis.All(e =>
|
|
|
(e.hourly_calls_limit > 0 && e.current_hourly_calls >= e.hourly_calls_limit) ||
|
|
|
(e.daily_calls_limit > 0 && e.current_daily_calls >= e.daily_calls_limit));
|
|
|
|
|
|
@@ -226,7 +182,7 @@ public partial class TkEndpointManager
|
|
|
return (0, "ALL");
|
|
|
}
|
|
|
|
|
|
- // 7. 其他情况(理论上不应该到达这里)
|
|
|
+ // 6. 其他情况(理论上不应该到达这里)
|
|
|
return (0, string.Empty);
|
|
|
}
|
|
|
|
|
|
@@ -258,51 +214,26 @@ public partial class TkEndpointManager
|
|
|
|
|
|
public static async Task SuspendAsync(int accountId, string endpoint, SuspendReason reason = SuspendReason.Passive, int? customHoldMinutes = null)
|
|
|
{
|
|
|
- // 获取或创建账号特定的信号量
|
|
|
- var semaphore = accountSemaphores.GetOrAdd(accountId, _ => new SemaphoreSlim(1, 1));
|
|
|
-
|
|
|
- await semaphore.WaitAsync();
|
|
|
- try
|
|
|
+ var holdMinutes = await GetEndpointHoldMinutesAsync(accountId);
|
|
|
+ TimeSpan suspendDuration;
|
|
|
+ if (customHoldMinutes.HasValue)
|
|
|
{
|
|
|
- // 在锁外部获取所需数据(避免在锁内await)
|
|
|
- var holdMinutes = await GetEndpointHoldMinutesAsync(accountId);
|
|
|
-
|
|
|
- // 如果是被动暂停,检查冷却时间
|
|
|
- if (lastSuspendTimes.TryGetValue(accountId, out var lastSuspendTime) &&
|
|
|
- DateTime.UtcNow - lastSuspendTime < SuspendCooldown)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 确定挂起时长
|
|
|
- TimeSpan suspendDuration;
|
|
|
- if (customHoldMinutes.HasValue)
|
|
|
- {
|
|
|
- suspendDuration = TimeSpan.FromMinutes(customHoldMinutes.Value);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- suspendDuration = reason switch
|
|
|
- {
|
|
|
- SuspendReason.HourlyLimit => CalculateHourlySuspendDuration(),
|
|
|
- SuspendReason.DailyLimit => CalculateDailySuspendDuration(),
|
|
|
- _ => TimeSpan.FromMinutes(holdMinutes.GetValueOrDefault(endpoint, 240))
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // 执行挂起操作
|
|
|
- var state = accountStates.GetOrAdd(accountId, _ => new AccountEndpointState());
|
|
|
- state.SuspendEndpoint(endpoint, suspendDuration);
|
|
|
-
|
|
|
- lastSuspendTimes[accountId] = DateTime.UtcNow;
|
|
|
-
|
|
|
- _ = new LoggerLibrary("转链接口风控", $"{accountId}").Info($"{accountId}_{endpoint}", reason.ToString()).SaveAsync();
|
|
|
- _ = NotifyCore.NotifyAsync($"【转链接口风控】{accountId}_{endpoint} ({reason})");
|
|
|
+ suspendDuration = TimeSpan.FromMinutes(customHoldMinutes.Value);
|
|
|
}
|
|
|
- finally
|
|
|
+ else
|
|
|
{
|
|
|
- semaphore.Release();
|
|
|
+ suspendDuration = reason switch
|
|
|
+ {
|
|
|
+ SuspendReason.HourlyLimit => CalculateHourlySuspendDuration(),
|
|
|
+ SuspendReason.DailyLimit => CalculateDailySuspendDuration(),
|
|
|
+ _ => TimeSpan.FromMinutes(holdMinutes.GetValueOrDefault(endpoint, 240))
|
|
|
+ };
|
|
|
}
|
|
|
+
|
|
|
+ RedisHelper.Set($"tk_suspend:{accountId}:{endpoint}", 1, (int)suspendDuration.TotalSeconds);
|
|
|
+
|
|
|
+ _ = new LoggerLibrary("转链接口风控", accountId.ToString()).Info($"{accountId}_{endpoint}", reason.ToString()).SaveAsync();
|
|
|
+ _ = NotifyCore.NotifyAsync($"【转链接口风控】{accountId}_{endpoint} ({reason})");
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -313,43 +244,36 @@ public partial class TkEndpointManager
|
|
|
/// <param name="endpoint">要释放的端点名称,如果为null则释放所有端点的挂起状态</param>
|
|
|
public static void ReleaseSuspend(int accountId, string endpoint = null)
|
|
|
{
|
|
|
- // 获取或创建账号特定的锁对象
|
|
|
- var accountLock = accountLocks.GetOrAdd(accountId, _ => new object());
|
|
|
-
|
|
|
- lock (accountLock)
|
|
|
+ if (endpoint == null)
|
|
|
{
|
|
|
- if (accountStates.TryGetValue(accountId, out var state))
|
|
|
+ var pattern = $"tk_suspend:{accountId}:*";
|
|
|
+ var keys = RedisHelper.Keys(pattern);
|
|
|
+ foreach (var key in keys)
|
|
|
{
|
|
|
- if (endpoint == null)
|
|
|
- {
|
|
|
- // 释放所有端点的挂起状态
|
|
|
- state.suspendedEndpoints.Clear();
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- // 释放指定端点的挂起状态
|
|
|
- state.suspendedEndpoints.TryRemove(endpoint, out _);
|
|
|
- }
|
|
|
-
|
|
|
- // 记录日志
|
|
|
- var action = endpoint == null ? "释放所有挂起" : $"释放挂起({endpoint})";
|
|
|
- _ = new LoggerLibrary("转链接口风控", $"{accountId}").Info($"{accountId}", action).SaveAsync();
|
|
|
- _ = NotifyCore.NotifyAsync($"【转链接口风控】{accountId} {action}");
|
|
|
+ RedisHelper.Del(key);
|
|
|
}
|
|
|
}
|
|
|
+ else
|
|
|
+ {
|
|
|
+ RedisHelper.Del($"tk_suspend:{accountId}:{endpoint}");
|
|
|
+ }
|
|
|
+
|
|
|
+ var action = endpoint == null ? "释放所有挂起" : $"释放挂起({endpoint})";
|
|
|
+ _ = new LoggerLibrary("转链接口风控", accountId.ToString()).Info($"{accountId}", action).SaveAsync();
|
|
|
+ _ = NotifyCore.NotifyAsync($"【转链接口风控】{accountId} {action}");
|
|
|
}
|
|
|
|
|
|
|
|
|
private static TimeSpan CalculateHourlySuspendDuration()
|
|
|
{
|
|
|
- var now = DateTime.UtcNow;
|
|
|
+ var now = DateTime.Now;
|
|
|
var nextHour = now.AddHours(1).Date.AddHours(now.Hour + 1); // 下一个整点(如 14:30 → 15:00)
|
|
|
return nextHour - now;
|
|
|
}
|
|
|
|
|
|
private static TimeSpan CalculateDailySuspendDuration()
|
|
|
{
|
|
|
- var now = DateTime.UtcNow;
|
|
|
+ var now = DateTime.Now;
|
|
|
var tomorrow = now.Date.AddDays(1); // 次日零点
|
|
|
return tomorrow - now;
|
|
|
}
|
|
|
@@ -379,38 +303,8 @@ public partial class TkEndpointManager
|
|
|
|
|
|
private class AccountEndpointState
|
|
|
{
|
|
|
- internal readonly ConcurrentDictionary<string, DateTime> suspendedEndpoints = new();
|
|
|
-
|
|
|
- // 为每个账号和 parseEndpoint 组合维护独立的索引
|
|
|
+ // 只保留轮询索引
|
|
|
private readonly ConcurrentDictionary<string, int> roundRobinIndices = new();
|
|
|
-
|
|
|
- public bool IsEndpointSuspended(string endpoint)
|
|
|
- {
|
|
|
- if (!suspendedEndpoints.TryGetValue(endpoint, out var suspendUntil))
|
|
|
- return false;
|
|
|
-
|
|
|
- if (DateTime.Now >= suspendUntil)
|
|
|
- {
|
|
|
- suspendedEndpoints.TryRemove(endpoint, out _);
|
|
|
- return false;
|
|
|
- }
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- public DateTime? GetSuspendTime(string endpoint)
|
|
|
- {
|
|
|
- if (suspendedEndpoints.TryGetValue(endpoint, out var suspendUntil))
|
|
|
- {
|
|
|
- return suspendUntil;
|
|
|
- }
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- public void SuspendEndpoint(string endpoint, TimeSpan duration)
|
|
|
- {
|
|
|
- suspendedEndpoints[endpoint] = DateTime.Now.Add(duration);
|
|
|
- }
|
|
|
-
|
|
|
public int GetOrAddAccountIndex(string key) => roundRobinIndices.GetOrAdd(key, -1);
|
|
|
public void UpdateAccountIndex(string key, int newIndex) => roundRobinIndices[key] = newIndex;
|
|
|
}
|