Jelajahi Sumber

feat(万能解析): 支持账号池及无可用账号时降级

dodo hold 3 hari lalu
induk
melakukan
dd43e952e3

+ 3 - 1
molilian.api/Controllers/admin/TaobaoController.cs

@@ -71,7 +71,9 @@ namespace molilian.api.Controllers
                 case "enable_coupon":
                 case "enable_sync_order":
                 case "enable_promotionQuery":
-                case "enable_deep_url":
+                case "enable_deep_url":
+                case "enable_general_link_parse_pool":
+                case "use_general_link_parse_pool":
                 case "status":
                     bool bVal = val.Equals("True");
                     result = new DBContext.Table("tk_pool")

+ 3 - 3
molilian.api/Controllers/public/TaskController.cs

@@ -1195,8 +1195,8 @@ namespace molilian.api.Controllers
                     message = "无效URL",
                 });
             }
-            WebProxy proxy = ProxyNodesCore.RandomOne(account.nodeName);
-            (success, string content) = await alimama.GetDesiredUrlAsync(proxy, url);
+            WebProxy proxy = account.use_general_link_parse_pool ? null : ProxyNodesCore.RandomOne(account.nodeName);
+            (success, string content) = await alimama.GetDesiredUrlAsync(proxy, url);
             if (content.Contains("霸下通用 web 页面-验证码"))
             {
                 message = "霸下验证码";
@@ -1214,4 +1214,4 @@ namespace molilian.api.Controllers
 
 
     }
-}
+}

+ 44 - 0
molilian.core/Core/taoke/TkPoolCore.GeneralLinkParse.cs

@@ -0,0 +1,44 @@
+namespace molilian.core
+{
+    public partial class TkPoolCore
+    {
+        /// <summary>
+        /// 选择万能解析账号。解析池独立于转链节点及 enable_parse 配置。
+        /// </summary>
+        public static async Task<TkPoolDTO?> GetGeneralLinkParseAccountAsync()
+        {
+            var accounts = await ListAsync().ConfigureAwait(false);
+            if (accounts == null) return null;
+
+            foreach (var account in accounts.OrderBy(_ => Random.Shared.Next()))
+            {
+                if (!account.status || account.is_banned || !account.enable_general_link_parse_pool ||
+                    !HasGeneralLinkParseCredentials(account) || !PassesFastAccountChecks(account, TkAction.all))
+                    continue;
+
+                DateTime now = DateTime.Now;
+                if (account.daily_calls_limit > 0 &&
+                    RiskControlCore.GetCalls(TkChannelEnum.tb, account.id, now.ToString("yyyyMMdd")) >= account.daily_calls_limit)
+                    continue;
+                if (account.hourly_calls_limit > 0 &&
+                    RiskControlCore.GetCalls(TkChannelEnum.tb, account.id, now.ToString("yyyyMMddHH")) >= account.hourly_calls_limit)
+                    continue;
+
+                if (TryAcquireQps(account)) return account;
+            }
+            return null;
+        }
+
+        private static bool HasGeneralLinkParseCredentials(TkPoolDTO account)
+        {
+            if (string.IsNullOrWhiteSpace(account.appkey) || string.IsNullOrWhiteSpace(account.appSecret) ||
+                string.IsNullOrWhiteSpace(account.open_pid))
+                return false;
+
+            string[] pidParts = account.open_pid.Split('_');
+            return pidParts.Length == 4 && pidParts[0] == "mm" &&
+                !string.IsNullOrWhiteSpace(pidParts[1]) && !string.IsNullOrWhiteSpace(pidParts[2]) &&
+                long.TryParse(pidParts[3], out long adzoneId) && adzoneId > 0;
+        }
+    }
+}

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

@@ -68,6 +68,11 @@ namespace molilian.core
         ParseDeny = 113,
         RemoteDailyLimit = 114,
 
+        /// <summary>
+        /// 万能解析池无可用账号,可降级为原短链解析逻辑。
+        /// </summary>
+        GeneralLinkParsePoolUnavailable = 115,
+
     }
 
 

+ 11 - 1
molilian.core/DTO/alimama/TkPoolDTO.cs

@@ -52,7 +52,17 @@
         public bool enable_coupon { get; set; } = false;
         public bool enable_sync_order { get; set; } = false;
         public bool enable_promotionQuery { get; set; } = false;
-        public bool enable_deep_url { get; set; } = false;
+        public bool enable_deep_url { get; set; } = false;
+
+        /// <summary>
+        /// 加入万能解析账号池,向其他账号提供解析能力。
+        /// </summary>
+        public bool enable_general_link_parse_pool { get; set; } = false;
+
+        /// <summary>
+        /// 使用万能解析账号池处理短链。
+        /// </summary>
+        public bool use_general_link_parse_pool { get; set; } = false;
 
 
         public string appkey { get; set; } = string.Empty;

+ 5 - 17
molilian.core/Plus/Alimama/parse.cs

@@ -79,10 +79,7 @@ namespace molilian.core
             Dictionary<string, long> swData = null)
         {
             result.itemName = "点击打开淘宝APP";
-            // 先保留短链,选定节点后再决定是否展开。
-            result = await InternalTextProcessingAsync(content, result, cancellationToken, swData, preserveShortLinks: true);
-            bool hasShortLink = result.link_type == LinkTypeEnum.baseDomain;
-            bool shortLinkExpanded = false;
+            result = await InternalTextProcessingAsync(content, result, cancellationToken, swData);
 
             bool ShouldStopAfterTextProcessing()
             {
@@ -112,8 +109,8 @@ namespace molilian.core
                             result.reason = string.Empty;
                             break;
                         default:
-                            result.reason = "非标准链接";
-                            result.subCode = TkSubCodeEnum.NonStdLink;
+                            if (string.IsNullOrWhiteSpace(result.reason)) result.reason = "非标准链接";
+                            if (result.subCode == TkSubCodeEnum.Other) result.subCode = TkSubCodeEnum.NonStdLink;
                             return true;
                     }
                 }
@@ -162,15 +159,6 @@ namespace molilian.core
                 (int ep_id, string tkEndpoint) = await TkEndpointManager.GetConvertApiAsync(_accountId, isTaobaoUrl, _account.parseEndpoint);
                 result.tkEndpoint = ep_id;
 
-                // 其他节点只展开一次;tbk_dg 始终使用原始短链。
-                if (hasShortLink && !shortLinkExpanded && tkEndpoint != "tbk_dg" &&
-                    !string.IsNullOrEmpty(tkEndpoint) && tkEndpoint != "ALL")
-                {
-                    result = await InternalTextProcessingAsync(content, result, cancellationToken, swData);
-                    shortLinkExpanded = true;
-                    if (ShouldStopAfterTextProcessing()) return result;
-                }
-
                 switch (tkEndpoint)
                 {
                     case "moose_share":
@@ -185,8 +173,8 @@ namespace molilian.core
                     case "aliyun":
                         result = await endpoint_aliyun(result.content, result, cancellationToken);
                         break;
-                    case "tbk_dg":
-                        result = await endpoint_tbk_dg(hasShortLink ? content : result.content, result, cancellationToken);
+                    case "tbk_dg":
+                        result = await endpoint_tbk_dg(result.content, result, cancellationToken);
                         break;
                     case "aiopen":
                         result = await endpoint_aiopen(result.content, result, cancellationToken);

+ 75 - 37
molilian.core/Plus/Alimama/parse_2.cs

@@ -170,14 +170,47 @@ namespace molilian.core
         /// </summary>
         /// <param name="url"></param>
         /// <returns></returns>
-        public async Task<(bool, string)> GetDesiredUrlAsync(WebProxy proxy, string url, CancellationToken cancellationToken = default)
-        {
-            string result;
-            try
-            {
-                if (!IsShortLink(url)) return (false, "不是淘宝短网址");
-
-                if (!_account.enable_deep_url) return (true, url);
+        public async Task<(bool, string)> GetDesiredUrlAsync(WebProxy proxy, string url, CancellationToken cancellationToken = default)
+        {
+            var result = await GetDesiredUrlResultAsync(proxy, url, cancellationToken);
+            return (result.Success, result.Content);
+        }
+
+        private readonly record struct DesiredUrlResult(bool Success, string Content, bool UsedGeneralLinkParse);
+
+        private async Task<DesiredUrlResult> GetDesiredUrlResultAsync(WebProxy proxy, string url, CancellationToken cancellationToken)
+        {
+            string result;
+            bool usedGeneralLinkParse = false;
+            try
+            {
+                if (!IsShortLink(url)) return new(false, "不是淘宝短网址", false);
+
+                if (_account.use_general_link_parse_pool)
+                {
+                    usedGeneralLinkParse = true;
+                    var parsed = await endpoint_tbk_dg_general_link_parse(url, new TkDataDTO { content = url }, cancellationToken);
+                    if (parsed.subCode != TkSubCodeEnum.GeneralLinkParsePoolUnavailable)
+                    {
+                        if (!parsed.success) return new(false, parsed.reason, true);
+
+                        string desiredUrl = IsDigitsOnly(parsed.itemId)
+                            ? $"https://item.taobao.com/item.htm?id={parsed.itemId}"
+                            : parsed.item_url;
+                        return new(true, desiredUrl, true);
+                    }
+
+                    // 账号池无可用账号时,继续按下探网址设置执行原逻辑。
+                    usedGeneralLinkParse = false;
+                }
+
+                cancellationToken.ThrowIfCancellationRequested();
+                if (!_account.enable_deep_url) return new(true, url, false);
+
+                if (_account.use_general_link_parse_pool)
+                {
+                    proxy ??= ProxyNodesCore.RandomOne(_account.nodeName);
+                }
 
                 string desiredUrlPattern = "var url = '(.*?)'";
                 WebClientUtility client = new()
@@ -202,8 +235,8 @@ namespace molilian.core
                 if (response.ResponseMessage?.StatusCode == HttpStatusCode.Found &&
                     !string.IsNullOrEmpty(response.ResponseMessage?.Headers?.Location?.OriginalString))
                 {
-                    string redirectUrl = response.ResponseMessage?.Headers?.Location?.OriginalString;
-                    return (true, redirectUrl);
+                    string redirectUrl = response.ResponseMessage?.Headers?.Location?.OriginalString;
+                    return new(true, redirectUrl, false);
                 }
                 var responseBody = response.Body();
                 result = responseBody;
@@ -217,10 +250,10 @@ namespace molilian.core
                         {
                             result = match.Groups[1].Value; // 返回匹配的 URL
 
-                            if (string.IsNullOrEmpty(result)) return (false, "没有匹配的URL");
+                            if (string.IsNullOrEmpty(result)) return new(false, "没有匹配的URL", false);
                             if (result.StartsWith("//"))
                                 result = "https:" + result;
-                            return (true, result);
+                            return new(true, result, false);
                         }
                     }
                     else
@@ -234,12 +267,16 @@ namespace molilian.core
 
                 }
             }
-            catch (Exception ex)
-            {
-                _ = new LoggerLibrary("api_error", "desiredUrl_error").Info(ex.Message, ex.StackTrace).SaveAsync();
+            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+            {
+                throw;
+            }
+            catch (Exception ex)
+            {
+                _ = new LoggerLibrary("api_error", "desiredUrl_error").Info(ex.Message, ex.StackTrace).SaveAsync();
                 result = "网络异常";
             }
-            return (false, result);
+            return new(false, result, usedGeneralLinkParse);
         }
 
         /// <summary>
@@ -455,10 +492,9 @@ namespace molilian.core
             result.shortLinkurl = null;
         }
 
-        public async Task<TkDataDTO> InternalTextProcessingAsync(string content, TkDataDTO result,
-            CancellationToken cancellationToken = default,
-            Dictionary<string, long> swData = null,
-            bool preserveShortLinks = false)
+        public async Task<TkDataDTO> InternalTextProcessingAsync(string content, TkDataDTO result,
+            CancellationToken cancellationToken = default,
+            Dictionary<string, long> swData = null)
         {
             bool success;
             string resultText;
@@ -493,30 +529,32 @@ namespace molilian.core
                 }
 
 
-                if (preserveShortLinks && IsShortLink(url))
-                {
-                    result.success = false;
-                    result.message = string.Empty;
-                    result.reason = string.Empty;
-                    result.content = content;
-                    result.link_type = LinkTypeEnum.baseDomain;
-                    result.elapsedTime2 = 0;
-                    return result;
-                }
-
-                Stopwatch sw = Stopwatch.StartNew();
-
-                var proxy = ProxyNodesCore.RandomOne(_account.nodeName);
+                Stopwatch sw = Stopwatch.StartNew();
+
+                var proxy = _account.use_general_link_parse_pool ? null : ProxyNodesCore.RandomOne(_account.nodeName);
                 if (_proxy != null && proxy != null && _proxy.Address != proxy.Address)
                 {
                     result.proxy_node = proxy?.Address?.Authority;
                 }
 
-                (success, resultText) = await GetDesiredUrlAsync(proxy, url, cancellationToken);
+                var desiredUrlResult = await GetDesiredUrlResultAsync(proxy, url, cancellationToken);
+                success = desiredUrlResult.Success;
+                resultText = desiredUrlResult.Content;
                 sw.Stop();
                 // 获取执行时间
-                result.elapsedTime2 = (int)sw.ElapsedMilliseconds;
-                swData?.Add("\tGetDesiredUrlAsync", sw.ElapsedMilliseconds);
+                result.elapsedTime2 = (int)sw.ElapsedMilliseconds;
+                swData?.Add("\tGetDesiredUrlAsync", sw.ElapsedMilliseconds);
+
+                if (desiredUrlResult.UsedGeneralLinkParse && !success)
+                {
+                    result.success = false;
+                    result.message = "解析失败";
+                    result.reason = resultText;
+                    result.subCode = TkSubCodeEnum.NoConvert;
+                    result.link_type = LinkTypeEnum.unknown;
+                    result.content = content;
+                    return result;
+                }
                 //============================== 放弃转链-请求超时 ==============================
 #if DEBUG
 #else

+ 5 - 12
molilian.core/Plus/Alimama/parse_endpoint/endpoint_tbk_dg.cs

@@ -6,7 +6,7 @@ namespace molilian.core
 {
     public partial class AlimamaPlus
     {
-        public async Task<TkDataDTO> endpoint_tbk_dg(string content, TkDataDTO result, CancellationToken cancellationToken = default)
+        public Task<TkDataDTO> endpoint_tbk_dg(string content, TkDataDTO result, CancellationToken cancellationToken = default)
         {
             cancellationToken.ThrowIfCancellationRequested();
 
@@ -14,13 +14,6 @@ namespace molilian.core
                 ? content
                 : Regex.Match(content ?? string.Empty, @"(?:\?|&)(?:id|shareDetailItemId|itemIds)=([0-9]+)(?=[&#\s]|$)").Groups[1].Value;
 
-            if (!IsDigitsOnly(item_id))
-            {
-                result = await endpoint_tbk_dg_general_link_parse(content, result, cancellationToken);
-                if (!result.success) return result;
-                item_id = result.itemId;
-            }
-
             if (!IsDigitsOnly(item_id))
             {
                 // _ = new LoggerLibrary("endpoint", "tbk_dg")
@@ -35,7 +28,7 @@ namespace molilian.core
                 result.message = "放弃转链";
                 result.reason = "无效商品id";
                 result.subCode = TkSubCodeEnum.NoConvert;
-                return result;
+                return Task.FromResult(result);
             }
 
             content = $"https://item.taobao.com/item.htm?id={item_id}";
@@ -77,7 +70,7 @@ namespace molilian.core
                 result.success = false;
                 result.message = "fail";
                 result.reason = "接口异常";
-                return result;
+                return Task.FromResult(result);
             }
             result.success = !response.IsError;
             if (!result.success)
@@ -92,7 +85,7 @@ namespace molilian.core
                 result.channel = TkChannelEnum.tb;
                 result.link_type = LinkTypeEnum.unknown;
                 result.success = false;
-                return result;
+                return Task.FromResult(result);
             }
 
             string deeplink = response.Data?.PageDeeplink ?? string.Empty;
@@ -100,7 +93,7 @@ namespace molilian.core
             string h5Url = Regex.Match(deeplink, @"(?:\?|&)h5Url=([^&]*)").Groups[1].Value.UrlDecode();
             result.shortLinkurl = h5Url;
             result.subCode = TkSubCodeEnum.Success;
-            return result;
+            return Task.FromResult(result);
         }
 
     }

+ 24 - 6
molilian.core/Plus/Alimama/parse_endpoint/endpoint_tbk_dg_general_link_parse.cs

@@ -5,14 +5,14 @@ namespace molilian.core
 {
     public partial class AlimamaPlus
     {
-        public Task<TkDataDTO> endpoint_tbk_dg_general_link_parse(string content, TkDataDTO result, CancellationToken cancellationToken = default)
+        public async Task<TkDataDTO> endpoint_tbk_dg_general_link_parse(string content, TkDataDTO result, CancellationToken cancellationToken = default)
         {
             cancellationToken.ThrowIfCancellationRequested();
             Stopwatch stopwatch = Stopwatch.StartNew();
             result.channel = TkChannelEnum.tb;
             result.commercial = false;
 
-            Task<TkDataDTO> Fail(string reason, TkSubCodeEnum subCode = TkSubCodeEnum.Other)
+            TkDataDTO Fail(string reason, TkSubCodeEnum subCode = TkSubCodeEnum.Other)
             {
                 result.link_type = LinkTypeEnum.unknown;
                 result.success = false;
@@ -23,7 +23,7 @@ namespace molilian.core
                 _ = new LoggerLibrary("endpoint", "tbk_dg_general_link_parse")
                     .Info(content, reason)
                     .SaveAsync();
-                return Task.FromResult(result);
+                return result;
             }
 
             try
@@ -33,7 +33,25 @@ namespace molilian.core
                     return Fail("待解析内容为空", TkSubCodeEnum.NoConvert);
                 }
 
-                TaobaoOpenPlus taobao = new(_account.appkey, _account.appSecret, _account.open_pid, false, null!);
+                var parseAccount = await TkPoolCore.GetGeneralLinkParseAccountAsync();
+                cancellationToken.ThrowIfCancellationRequested();
+                if (parseAccount == null)
+                {
+                    return Fail("万能解析账号池无可用账号", TkSubCodeEnum.GeneralLinkParsePoolUnavailable);
+                }
+
+                TaobaoOpenPlus taobao = new(parseAccount.appkey, parseAccount.appSecret, parseAccount.open_pid, false, null!);
+                try
+                {
+                    await RiskControlCore.CallsIncrByAsync(TkChannelEnum.tb, parseAccount.id);
+                }
+                catch (Exception ex)
+                {
+                    _ = new LoggerLibrary("endpoint", "tbk_dg_general_link_parse")
+                        .Info($"解析账号 {parseAccount.id} 调用计数失败", ex.Message)
+                        .SaveAsync();
+                }
+                cancellationToken.ThrowIfCancellationRequested();
                 var response = taobao.GeneralLinkParseRequest(content);
                 cancellationToken.ThrowIfCancellationRequested();
                 result.remote_response = response?.Body ?? string.Empty;
@@ -96,7 +114,7 @@ namespace molilian.core
                 result.deeplink_url = GetDeeplink(url);
                 result.item_url = url;
                 result.item_deeplink_url = result.deeplink_url;
-                return Task.FromResult(result);
+                return result;
             }
             finally
             {
@@ -105,4 +123,4 @@ namespace molilian.core
             }
         }
     }
-}
+}

+ 34 - 0
sql/20261103_add_tk_pool_general_link_parse_switches.sql

@@ -0,0 +1,34 @@
+-- 两个开关默认关闭,保留现有账号的短链处理方式。
+SET @column_exists := (
+    SELECT COUNT(*)
+    FROM information_schema.columns
+    WHERE table_schema = DATABASE()
+      AND table_name = 'tk_pool'
+      AND column_name = 'enable_general_link_parse_pool'
+);
+
+SET @sql := IF(
+    @column_exists = 0,
+    'ALTER TABLE tk_pool ADD COLUMN enable_general_link_parse_pool TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''加入万能解析账号池''',
+    'SELECT 1'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @column_exists := (
+    SELECT COUNT(*)
+    FROM information_schema.columns
+    WHERE table_schema = DATABASE()
+      AND table_name = 'tk_pool'
+      AND column_name = 'use_general_link_parse_pool'
+);
+
+SET @sql := IF(
+    @column_exists = 0,
+    'ALTER TABLE tk_pool ADD COLUMN use_general_link_parse_pool TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''使用万能解析账号池处理短链''',
+    'SELECT 1'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;