Преглед изворни кода

feat(隐藏号): QPS 只给真正提交到 endpoint 的转链使用

- 转链提交到 endpoint 时标记 endpoint_called
- 隐藏号未提交 endpoint(前置放弃)时归还分配账号占用的 QPS 名额
- SpecialBusinessRateLimiter 增加 Release,超出窗口的占用不处理
dodo hold пре 1 дан
родитељ
комит
fdedc8184b

+ 13 - 0
molilian.core/Core/taoke/TkPoolCore.cs

@@ -193,6 +193,19 @@ namespace molilian.core
                 $"tk_pool_qps:{account.id}",
                 account.qps_limit,
                 TimeSpan.FromSeconds(1));
+        }
+
+        /// <summary>
+        /// 归还分配账号时占用的 QPS 名额(转链未提交到 endpoint 时调用)
+        /// </summary>
+        public static void ReleaseQps(TkPoolDTO account, long acquiredSinceTicks)
+        {
+            if (account == null || account.qps_limit <= 0) return;
+
+            SpecialBusinessRateLimiter.Release(
+                $"tk_pool_qps:{account.id}",
+                TimeSpan.FromSeconds(1),
+                acquiredSinceTicks);
         }
 
         private static async Task<bool> FilterNodesAsync(TkPoolDTO item, TkAction action)

+ 7 - 0
molilian.core/Core/taoke/UnionParseCore/UnionParseCore.cs

@@ -721,6 +721,7 @@ namespace molilian.core
                 //request.AccountId = 133;
 #endif
 
+                long qpsAcquiredSinceTicks = DateTime.UtcNow.Ticks;
                 TkPoolDTO? account = request.AccountId > 0 ?
                     await TkPoolCore.GetOneAsync(request.AccountId) :
                     await TkPoolCore.GetOneAsync(TkPoolCore.TkAction.parse, isTaobaoUrl, request.RiskStrategy, request.LaunchScene, request.Type);
@@ -782,6 +783,12 @@ namespace molilian.core
                 if (completedTask == requestTask)
                 {
                     result = await requestTask;
+
+                    // 隐藏号的 QPS 只给真正提交到 endpoint 的请求使用,前置放弃的归还名额
+                    if (account.is_hide && request.AccountId <= 0 && !result.endpoint_called)
+                    {
+                        TkPoolCore.ReleaseQps(account, qpsAcquiredSinceTicks);
+                    }
                 }
                 else
                 {

+ 7 - 0
molilian.core/Core/taoke/UnionParseCore/dp2dp.cs

@@ -378,6 +378,7 @@ namespace molilian.core
                 (bool isTaobaoUrl, string url) = AlimamaPlus.IsTaobaoUrl(content);
 
                 //============================== 放弃转链-没有匹配账号 ==============================
+                long qpsAcquiredSinceTicks = DateTime.UtcNow.Ticks;
                 TkPoolDTO? account = request.AccountId > 0 ?
                     await TkPoolCore.GetOneAsync(request.AccountId) :
                     await TkPoolCore.GetOneAsync(TkPoolCore.TkAction.parse, isTaobaoUrl, request.RiskStrategy, request.LaunchScene, "dp");
@@ -431,6 +432,12 @@ namespace molilian.core
                 if (completedTask == requestTask)
                 {
                     result = await requestTask;
+
+                    // 隐藏号的 QPS 只给真正提交到 endpoint 的请求使用,前置放弃的归还名额
+                    if (account.is_hide && request.AccountId <= 0 && !result.endpoint_called)
+                    {
+                        TkPoolCore.ReleaseQps(account, qpsAcquiredSinceTicks);
+                    }
                 }
                 else
                 {

+ 6 - 0
molilian.core/DTO/alimama/TkDataDTO.cs

@@ -136,6 +136,12 @@ namespace molilian.core
         public string item_deeplink_url { get; set; } = string.Empty;
         public string remote_response { get; set; } = string.Empty;
 
+        /// <summary>
+        /// 是否已提交到转链 endpoint(隐藏号据此归还未使用的 QPS 名额)
+        /// </summary>
+        [IgnoreInsert, IgnoreUpdate]
+        public bool endpoint_called { get; set; } = false;
+
 
         /// <summary>
         /// 搜同款时可能会用到

+ 7 - 0
molilian.core/Plus/Alimama/parse.cs

@@ -162,21 +162,27 @@ namespace molilian.core
                 switch (tkEndpoint)
                 {
                     case "moose_share":
+                        result.endpoint_called = true;
                         result = await endpoint_moose_share(result.content, result, cancellationToken);
                         break;
                     case "promo_goods":
+                        result.endpoint_called = true;
                         result = await endpoint_promo_goods(result.content, result, cancellationToken);
                         break;
                     case "tool_links":
+                        result.endpoint_called = true;
                         result = await endpoint_tool_links(result.content, result, cancellationToken);
                         break;
                     case "aliyun":
+                        result.endpoint_called = true;
                         result = await endpoint_aliyun(result.content, result, cancellationToken);
                         break;
                     case "tbk_dg":
+                        result.endpoint_called = true;
                         result = await endpoint_tbk_dg(result.content, result, cancellationToken);
                         break;
                     case "aiopen":
+                        result.endpoint_called = true;
                         result = await endpoint_aiopen(result.content, result, cancellationToken);
                         break;
                     case "veapi":
@@ -189,6 +195,7 @@ namespace molilian.core
                             {
                                 var core = new OtherApiPlus(account.name, account.key, account.sessionKey, tkEndpoint, account.api_url);
                                 string refpid = string.IsNullOrEmpty(account.refpid) ? _refpid : account.refpid;
+                                result.endpoint_called = true;
                                 result = await core.UnionParseAsync(result.content, refpid, result, cancellationToken);
                                 return result;
                             }

+ 24 - 0
molilian.core/Plus/SpecialBusinessRateLimiter.cs

@@ -32,6 +32,30 @@ namespace molilian.core
             }
         }
 
+        /// <summary>
+        /// 归还 acquiredSinceTicks 之后占用的一个名额(请求未真正发生时使用)。
+        /// 超过窗口时间的占用已自然释放,不再处理,避免误删其他请求的名额。
+        /// </summary>
+        public static void Release(string bucketKey, TimeSpan window, long acquiredSinceTicks)
+        {
+            if (string.IsNullOrWhiteSpace(bucketKey)) return;
+            if (!States.TryGetValue(bucketKey, out var state)) return;
+            if (DateTime.UtcNow.Ticks - acquiredSinceTicks >= window.Ticks) return;
+
+            lock (state.SyncRoot)
+            {
+                var timestamps = state.Timestamps.ToArray();
+                int index = Array.FindLastIndex(timestamps, t => t >= acquiredSinceTicks);
+                if (index < 0) return;
+
+                state.Timestamps.Clear();
+                for (int i = 0; i < timestamps.Length; i++)
+                {
+                    if (i != index) state.Timestamps.Enqueue(timestamps[i]);
+                }
+            }
+        }
+
         private sealed class SlidingWindowState
         {
             public object SyncRoot { get; } = new();