Ver código fonte

引入转链端点结构

dodo hold 1 ano atrás
pai
commit
7a8a623c5d
37 arquivos alterados com 2276 adições e 1055 exclusões
  1. 4 0
      molilian.api/Controllers/admin/JdUnionController.cs
  2. 0 2
      molilian.api/Controllers/public/ApiController.cs
  3. 1 1
      molilian.api/Controllers/public/TaskController.cs
  4. 2 3
      molilian.api/Controllers/public/TkController.cs
  5. 72 0
      molilian.api/Controllers/public/TkEndpointController.cs
  6. 0 0
      molilian.api/Properties/PublishProfiles/latest.pubxml.user
  7. 1 1
      molilian.api/molilian.api.csproj
  8. 75 17
      molilian.core/Core/EndPointCore.cs
  9. 55 12
      molilian.core/Core/NotifyCore.cs
  10. 1 13
      molilian.core/Core/ProxyNodesCore.cs
  11. 5 2
      molilian.core/Core/log/taobao.cs
  12. 6 0
      molilian.core/Core/log/第三方旧接口.cs
  13. 32 13
      molilian.core/Core/taoke/OrderTrackingCore.cs
  14. 52 35
      molilian.core/Core/taoke/RiskControlCore.cs
  15. 125 0
      molilian.core/Core/taoke/TkEndpointCore.cs
  16. 419 0
      molilian.core/Core/taoke/TkEndpointManager.cs
  17. 88 48
      molilian.core/Core/taoke/TkPoolCore.cs
  18. 1 1
      molilian.core/Core/taoke/UnionParseCore.cs
  19. 0 7
      molilian.core/Core/tool/DeeplinkParseCore.cs
  20. 0 6
      molilian.core/Core/tool/DeeplinkParseRuleCore.cs
  21. 1 0
      molilian.core/DTO/VeapiPoolDTO.cs
  22. 1 0
      molilian.core/DTO/alimama/TkDataDTO.cs
  23. 35 0
      molilian.core/DTO/alimama/TkEndpointConfigDTO.cs
  24. 3 1
      molilian.core/DTO/alimama/TkPoolDTO.cs
  25. 0 1
      molilian.core/DTO/deeplink/DeeplinkParseRuleDTO.cs
  26. 0 6
      molilian.core/Plus/Alimama/RefundOrders.cs
  27. 0 3
      molilian.core/Plus/Alimama/bills.cs
  28. 0 7
      molilian.core/Plus/Alimama/orders.cs
  29. 78 44
      molilian.core/Plus/Alimama/parse.cs
  30. 3 800
      molilian.core/Plus/Alimama/parse_2.cs
  31. 327 0
      molilian.core/Plus/Alimama/parse_endpoint/moose_share.cs
  32. 567 0
      molilian.core/Plus/Alimama/parse_endpoint/promo_goods.cs
  33. 286 0
      molilian.core/Plus/Alimama/parse_endpoint/tool_links.cs
  34. 2 1
      molilian.core/Plus/JDUnion/H5stHack.cs
  35. 32 24
      molilian.core/Plus/VeapiPlus.cs
  36. 1 6
      molilian.core/Plus/ks/KsUnionPlus.cs
  37. 1 1
      molilian.core/molilian.core.csproj

+ 4 - 0
molilian.api/Controllers/admin/JdUnionController.cs

@@ -365,7 +365,11 @@ namespace molilian.api.Controllers
             cookie = cookie.Replace("\r", "").Replace("\n", "").Trim();
             if (!cookie.EndsWith(";")) cookie += ";";
 
+            string[] parts = h5st.Split(';');
+            h5st = string.Join(";", parts.Take(8));
             h5st = h5st.Trim();
+
+
             user_agent = user_agent.Trim();
             var accountId = JdPoolCore.UpdateCookies(cookie, user_agent, h5st);
             bool success = accountId > 0;

+ 0 - 2
molilian.api/Controllers/public/ApiController.cs

@@ -159,7 +159,5 @@ namespace molilian.api.Controllers
 
 
 
-
-
     }
 }

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

@@ -959,7 +959,6 @@ namespace molilian.api.Controllers
             //ElePoolCore.Refresh();
             //MeituanPoolCore.Refresh();
             CpsPoolCore.Refresh();
-
             return new APIResult(new
             {
                 success = true,
@@ -1019,5 +1018,6 @@ namespace molilian.api.Controllers
             });
         }
 
+
     }
 }

+ 2 - 3
molilian.api/Controllers/public/TkController.cs

@@ -213,7 +213,6 @@ namespace molilian.api.Controllers
                         result.deeplink_url,
                     });
                 }
-
                 var account = await TkPoolCore.GetOneAsync(TkPoolCore.TkAction.parse);
                 if (account == null)
                 {
@@ -254,9 +253,9 @@ namespace molilian.api.Controllers
                         result.shortLinkurl,
                         result.deeplink_url,
                     });
-
                 }
-                result = await alimama.UnionParseAsync(content, 0, result);
+
+                result = await alimama.UnionParseAsync(ip, oaid, content, 0, result);
             }
             catch (Exception ex)
             {

+ 72 - 0
molilian.api/Controllers/public/TkEndpointController.cs

@@ -0,0 +1,72 @@
+using molilian.core;
+using dodohold.core;
+using Microsoft.AspNetCore.Mvc;
+using Org.BouncyCastle.Ocsp;
+using System.Text.Json;
+using System.Runtime.InteropServices;
+using System.Net;
+using System.Security.Cryptography;
+using Microsoft.AspNetCore.Authentication;
+using TencentCloud.Ecm.V20190719.Models;
+using COSXML.Network;
+using System.Linq;
+using static dodohold.core.ZTOExpress.CreateOrderArgs;
+using TencentCloud.Lighthouse.V20200324.Models;
+using TencentCloud.Ame.V20190916.Models;
+using TencentCloud.Omics.V20221128.Models;
+using System.Threading.Channels;
+
+namespace molilian.api.Controllers
+{
+    [ApiController]
+    [Route("api/[controller]/[action]")]
+    public class TkEndpointController : ControllerBase
+    {
+        protected IHttpContextAccessor _accessor;
+        TaokeOpenCore core;
+        public TkEndpointController(IHttpContextAccessor accessor)
+        {
+            _accessor = accessor;
+            core = new TaokeOpenCore();
+        }
+
+        [HttpPost]
+        public async Task<ActionResult> ChangeSuspend([FromForm] JsonElement body)
+        {
+            int accountId = body.Read<int>("accountId");
+            string endpoint = body.Read<string>("accountId");
+            bool release = body.Read<bool>("release");
+
+            if (release)
+            {
+                TkEndpointManager.ReleaseSuspend(accountId, endpoint);
+            }
+            else
+            {
+                await TkEndpointManager.SuspendAsync(accountId, endpoint, TkEndpointManager.SuspendReason.Active);
+            }
+
+            return new APIResult(new
+            {
+                success = true,
+                action = release ? "ReleaseSuspend" : "Suspend",
+            });
+        }
+
+
+        [HttpPost]
+        public ActionResult GetStatus([FromForm] JsonElement body)
+        {
+            var ids = body.PathReadArray<int>("accountId[]");
+            var result = TkEndpointManager.GetStatus(ids);
+
+            return new APIResult(new
+            {
+                success = true,
+                data = result
+            });
+        }
+
+
+    }
+}

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
molilian.api/Properties/PublishProfiles/latest.pubxml.user


+ 1 - 1
molilian.api/molilian.api.csproj

@@ -12,7 +12,7 @@
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'" />
 
   <ItemGroup>
-    <PackageReference Include="dodohold.core" Version="1.0.1.18" />
+    <PackageReference Include="dodohold.core" Version="1.0.1.27" />
     <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.0-Preview.1" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
   </ItemGroup>

+ 75 - 17
molilian.core/Core/EndPointCore.cs

@@ -1,17 +1,4 @@
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Controllers;
-using Microsoft.AspNetCore.Mvc.Filters;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using dodohold.core;
-using Spire.Pdf.Exporting.XPS.Schema;
-using System.Xml.Linq;
-using System.Net;
-using TencentCloud.Mrs.V20200910.Models;
-using static molilian.core.TkPoolCore;
-using Org.BouncyCastle.Tls;
+using dodohold.core;
 using MySql.Data.MySqlClient;
 using System.Data;
 
@@ -69,7 +56,7 @@ namespace molilian.core
 
 
 #if DEBUG
-           // item.db_server = "Server=rm-2ze74506m3gfsqe7mco.rwlb.rds.aliyuncs.com; Port=3306; Database=coupon; Uid=coupon; Pwd=67ktWBmw5G4yMs4J;SslMode=None;CharSet=utf8mb4;ConnectionTimeout=60;";
+            // item.db_server = "Server=rm-2ze74506m3gfsqe7mco.rwlb.rds.aliyuncs.com; Port=3306; Database=coupon; Uid=coupon; Pwd=67ktWBmw5G4yMs4J;SslMode=None;CharSet=utf8mb4;ConnectionTimeout=60;";
 #endif
             /*   
 Server=rm-2zey1jqxnoqy9mcc0zo.rwlb.rds.aliyuncs.com; Port=3306; Database=taoke; Uid=taoke; Pwd=67ktWBmw5G4yMs4J;SslMode=None;CharSet=utf8mb4;ConnectionTimeout=60
@@ -182,8 +169,6 @@ Server=rm-2ze74506m3gfsqe7mco.rwlb.rds.aliyuncs.com; Port=3306; Database=coupon;
             });
         }
 
-
-
         public static List<(string, string)> NotifyCheckDeepUrl(int accountid, CancellationToken cancellationToken = default)
         {
             var result = ProcessEndPointNodes<(string, string)>(node =>
@@ -220,5 +205,78 @@ Server=rm-2ze74506m3gfsqe7mco.rwlb.rds.aliyuncs.com; Port=3306; Database=coupon;
             });
             return result;
         }
+
+
+        public static async Task NotifyChangeSuspend(int accountId, string endpoint, bool release = false, CancellationToken cancellationToken = default)
+        {
+            await ProcessEndPointNodesAsync(async node =>
+            {
+                string post = string.Empty;
+                try
+                {
+                    string url = $"{node.api_server}api/TkEndpoint/ChangeSuspend";
+                    post = new
+                    {
+                        accountId,
+                        endpoint,
+                        release
+                    }.Convert2Json();
+                    await new WebClientUtility().Post(post).RequestAsync(url, "POST", cancellationToken);
+                }
+                catch (Exception ex)
+                {
+                    _ = new LoggerLibrary("TkEndpoint", "ChangeSuspend_error")
+                    .Info(node.Convert2Json(), post)
+                    .Info(ex.Message, ex.StackTrace)
+                    .SaveAsync();
+
+                    NotifyCore.Notify(new NifyMessage
+                    {
+                        message = $"【TkEndpoint异常】ChangeSuspend\t{node.description}\n{post}\n{ex.Message}\n{ex.StackTrace}",
+                        priority = NifyMessagePriority.high,
+                        tags = ["red_circle"]
+                    });
+                }
+            });
+        }
+
+
+        public static List<(string, string)> TkEndpointGetStatus(int accountid, CancellationToken cancellationToken = default)
+        {
+            var result = ProcessEndPointNodes<(string, string)>(node =>
+            {
+                try
+                {
+                    if (!node.is_public_api) return (node.name, "break");
+                    if (!node.status) return (node.name, "账号下线");
+
+                    string url = $"{node.api_server}api/TkEndpoint/GetStatus";
+                    var result = new WebClientUtility().Request(url, "POST");
+                    string body = result.Body();
+                    var root = body.Convert2JsonElement();
+                    var message = root.Read<string>("message", string.Empty);
+                    return (node.name, message);
+                }
+                catch (Exception ex)
+                {
+                    _ = new LoggerLibrary("GetStatus", "error")
+                    .Info(node.Convert2Json(), $"accountid:\t{accountid}")
+                    .Info(ex.Message, ex.StackTrace)
+                    .SaveAsync();
+
+                    NotifyCore.Notify(new NifyMessage
+                    {
+                        message = $"【TkEndpoint异常】GetStatus\t{node.description}\n{ex.Message}\n{ex.StackTrace}",
+                        priority = NifyMessagePriority.high,
+                        tags = ["red_circle"]
+                    });
+                    return (node.name, ex.Message);
+                }
+            });
+            return result;
+        }
+
+
+
     }
 }

+ 55 - 12
molilian.core/Core/NotifyCore.cs

@@ -14,6 +14,7 @@ namespace molilian.core
     {
         static string _base_url = string.Empty;
         static string _anpush_url = string.Empty;
+
         static NotifyCore()
         {
             _anpush_url = Environment.GetEnvironmentVariable("ANPush");
@@ -25,6 +26,7 @@ namespace molilian.core
             }
         }
 
+        // 同步版本保持不变
         public static void Notify(string message)
         {
             if (!string.IsNullOrEmpty(_base_url))
@@ -33,6 +35,16 @@ namespace molilian.core
             }
         }
 
+        // 新增异步版本
+        public static async Task NotifyAsync(string message)
+        {
+            if (!string.IsNullOrEmpty(_base_url))
+            {
+                await NifyPlus.SendAsync(_base_url, new NifyMessage { message = message });
+            }
+        }
+
+        // 同步版本保持不变
         public static void Notify(NifyMessage message)
         {
             if (!string.IsNullOrEmpty(_base_url))
@@ -40,24 +52,55 @@ namespace molilian.core
                 NifyPlus.Send(_base_url, message);
             }
         }
+
+        // 新增异步版本
+        public static async Task NotifyAsync(NifyMessage message)
+        {
+            if (!string.IsNullOrEmpty(_base_url))
+            {
+                await NifyPlus.SendAsync(_base_url, message);
+            }
+        }
+
+        // 同步版本保持不变
         public static void AnPushNotify(string title, string content)
         {
 #if DEBUG
 #else
-            if (!string.IsNullOrEmpty(_anpush_url))
+        if (!string.IsNullOrEmpty(_anpush_url))
+        {
+            string url = _anpush_url;
+            url = url.Replace("{title}", title?.UrlEncode());
+            url = url.Replace("{content}", content?.UrlEncode());
+
+            try
             {
-                string url = _anpush_url;
-                url = url.Replace("{title}", title?.UrlEncode());
-                url = url.Replace("{content}", content?.UrlEncode());
-
-                try
-                {
-                    url.HttpRequest();
-                }
-                catch (Exception ex) { }
+                url.HttpRequest();
             }
+            catch (Exception ex) { }
+        }
 #endif
         }
-    }
 
-}
+        // 新增异步版本
+        public static async Task AnPushNotifyAsync(string title, string content)
+        {
+#if DEBUG
+            await Task.CompletedTask;
+#else
+        if (!string.IsNullOrEmpty(_anpush_url))
+        {
+            string url = _anpush_url;
+            url = url.Replace("{title}", title?.UrlEncode());
+            url = url.Replace("{content}", content?.UrlEncode());
+
+            try
+            {
+                await Task.Run(() => url.HttpRequest());
+            }
+            catch (Exception ex) { }
+        }
+#endif
+        }
+    }
+}

+ 1 - 13
molilian.core/Core/ProxyNodesCore.cs

@@ -1,17 +1,5 @@
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Controllers;
-using Microsoft.AspNetCore.Mvc.Filters;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using dodohold.core;
-using Spire.Pdf.Exporting.XPS.Schema;
-using System.Xml.Linq;
+using dodohold.core;
 using System.Net;
-using TencentCloud.Mrs.V20200910.Models;
-using static molilian.core.TkPoolCore;
-using TencentCloud.Ecm.V20190719.Models;
 
 
 namespace molilian.core

+ 5 - 2
molilian.core/Core/log/taobao.cs

@@ -196,10 +196,12 @@ namespace molilian.core
                             break;
                     }
                 }
-                if (response.subCode == TkSubCodeEnum.ParseDeny || "接口风控".Equals(response.reason))
+                if (response.subCode == TkSubCodeEnum.ParseDeny && "接口风控ALL".Equals(response.reason))
                 {
-                    TkPoolCore.SwitchBackup(response.end_point, response.accountId, response.accountName, "接口风控");
+                    TkPoolCore.Disabled(response.accountId, response.accountName, "接口风控NONE");
+                    //todo  计数器+1
                 }
+
                 if (response.subCode == TkSubCodeEnum.Captcha || "霸下验证码".Equals(response.reason))
                 {
                     TkPoolCore.Suspend(response.end_point, response.accountId, response.accountName, "霸下验证码");
@@ -223,6 +225,7 @@ namespace molilian.core
                 .Add("accountId", data.accountId)
                 .Add("accountName", data.accountName)
                 .Add("proxy_node", data.proxy_node)
+                .Add("tkEndpoint", data.tkEndpoint)
                 .Add("rawContent", data.rawContent)
                 .Add("success", data.success)
                 .Add("message", data.message)

+ 6 - 0
molilian.core/Core/log/第三方旧接口.cs

@@ -129,6 +129,12 @@ namespace molilian.core
                             break;
                     }
                 }
+
+                if ("接口风控ALL".Equals(response.reason))
+                {
+                    TkPoolCore.Disabled(response.accountId, response.accountName, $"{response.rawContent}\n{response.rawContent2}");
+                }
+
                 if ("没有匹配账号".Equals(response.reason))
                 {
                     TkPoolCore.AccountExhausted();

+ 32 - 13
molilian.core/Core/taoke/OrderTrackingCore.cs

@@ -59,14 +59,26 @@ namespace molilian.core
         }
 
 
-        public static async Task<TkDataDTO> GetLinkSummaryAsync(int accountId, string itemId, string mktId, string itemTitle)
+        public static async Task<TkDataDTO> GetLinkSummaryAsync(TkPoolDTO account, string itemId, string mktId, string itemTitle)
         {
             try
             {
+                int accountId = account.id;
                 mktId = !string.IsNullOrEmpty(mktId) && mktId.Contains('-')
                     ? mktId.Split('-')[^1]
                     : mktId;
 
+
+                // 获取所有关联账户ID(包括当前账户)
+                var relatedAccountIds = new List<int> { accountId };
+                if (!string.IsNullOrEmpty(account.related_account_ids))
+                {
+                    relatedAccountIds.AddRange(account.related_account_ids
+                        .Split(',')
+                        .Where(id => !string.IsNullOrWhiteSpace(id))
+                        .Select(int.Parse));
+                }
+
                 var tasks = EndPointCore.List()
                     .Where(node => node.is_public_api && !string.IsNullOrEmpty(node.redis_server))
                     .Select(async node =>
@@ -83,21 +95,26 @@ namespace molilian.core
                             // 按优先级依次查询不同的缓存key
                             TkDataDTO result = null;
 
-                            if (!string.IsNullOrEmpty(mktId))
-                            {
-                                result = await redis.GetAsync<TkDataDTO>($":cache:order_summary:{accountId}:mktId:{mktId}");
-                                if (result != null) return result;
-                            }
-
-                            if (!string.IsNullOrEmpty(itemId))
+           
+                            // 遍历所有关联账户ID
+                            foreach (var relatedId in relatedAccountIds)
                             {
-                                result = await redis.GetAsync<TkDataDTO>($":cache:order_summary:{accountId}:{itemId}");
-                                if (result != null) return result;
+                                if (!string.IsNullOrEmpty(mktId))
+                                {
+                                    result = await redis.GetAsync<TkDataDTO>($":cache:order_summary:{relatedId}:mktId:{mktId}");
+                                    if (result != null) return result;
+                                }
 
-                                if (!string.IsNullOrEmpty(itemTitle))
+                                if (!string.IsNullOrEmpty(itemId))
                                 {
-                                    result = await redis.GetAsync<TkDataDTO>($":cache:order_summary:{accountId}:{itemTitle}");
+                                    result = await redis.GetAsync<TkDataDTO>($":cache:order_summary:{relatedId}:{itemId}");
                                     if (result != null) return result;
+
+                                    if (!string.IsNullOrEmpty(itemTitle))
+                                    {
+                                        result = await redis.GetAsync<TkDataDTO>($":cache:order_summary:{relatedId}:{itemTitle}");
+                                        if (result != null) return result;
+                                    }
                                 }
                             }
                         }
@@ -183,7 +200,7 @@ namespace molilian.core
         {
             if (item.tbPaidTime < DateTime.Now.AddDays(-1)) return false;
 
-            var summary = await GetLinkSummaryAsync(item.accountId, item.itemId, item.mktId, item.itemTitle);
+            var summary = await GetLinkSummaryAsync(account, item.itemId, item.mktId, item.itemTitle);
             if (summary == null) return false;
 
 
@@ -197,6 +214,8 @@ namespace molilian.core
             DateTime paidTime = item.tbPaidTime;
             if (paidTime < summary.create_time) return false;
 
+            // todo 改成入库时间
+
             int minutes = paidTime.Minute / 10 * 10;
             DateTime rounded = new(paidTime.Year, paidTime.Month, paidTime.Day, paidTime.Hour, minutes, 0);
             string batchId = rounded.ToString("yyyyMMddHHmm");

+ 52 - 35
molilian.core/Core/taoke/RiskControlCore.cs

@@ -82,42 +82,9 @@ namespace molilian.core
                 _calls[key] = val;
             }
             string cache_key = $"RiskControl:{key}:calls:{flag}";
-            RedisHelper.IncrByAsync(cache_key);
-            RedisHelper.ExpireAsync(cache_key, 3 * 86400);
+            await RedisHelper.IncrByAsync(cache_key);
+            await RedisHelper.ExpireAsync(cache_key, 3 * 86400);
         }
-
-        //        public static int GetAllNodesCalls(TkChannelEnum channel, int accountId, string flag)
-        //        {
-        //            string key = $"{channel}:{accountId}:{flag}";
-        //            var result = EndPointCore.ProcessEndPointNodes<int>(node =>
-        //            {
-        //                if (!node.is_public_api) return 0;
-        //                if (string.IsNullOrEmpty(node.redis_server)) return 0;
-        //#if DEBUG
-        //                switch (node.name)
-        //                {
-
-        //                    case "bj":
-        //                        node.redis_server = "101.200.46.46:6379,password=pKBiS4ka2IpXayIdcx00,defaultDatabase=0,idleTimeout=20000,preheat=3,tryit=2,ssl=false,prefix=webhook";
-        //                        break;
-        //                    case "gz":
-        //                        node.redis_server = "8.138.110.158:6379,password=pKBiS4ka2IpXayIdcx00,defaultDatabase=0,idleTimeout=20000,preheat=3,tryit=2,ssl=false,prefix=webhook";
-        //                        break;
-        //                    case "coupon1":
-        //                        node.redis_server = "123.56.185.166:6379,password=pKBiS4ka2IpXayIdcx00,defaultDatabase=0,idleTimeout=20000,preheat=3,tryit=2,ssl=false,prefix=coupon";
-        //                        break;
-        //                    default: return 0;
-        //                }
-        //#endif
-
-        //                string cache_key = $"RiskControl:{key}:calls:{flag}";
-        //                var redis = RedisClientManager.GetRedisClient(node.redis_server);
-        //                return redis.Get<int>(cache_key);
-        //            });
-        //            int num = result.Sum();
-        //            _calls.TryAdd(key, num);
-        //            return num;
-        //        }
         public static async Task<int> GetAllNodesCallsAsync(TkChannelEnum channel, int accountId, string flag)
         {
             var key = $"{channel}:{accountId}:{flag}";
@@ -153,6 +120,56 @@ namespace molilian.core
         }
 
 
+        internal static async Task SetTkEndpointCallsAsync(int accountId, int ep_id, string flag, int val)
+        {
+            string key = $"tk_endpoint:{accountId}:{ep_id}:{flag}";
+            if (_calls.ContainsKey(key))
+            {
+                _calls[key] = val;
+            }
+            else
+            {
+                _calls[key] = val;
+            }
+            string cache_key = $"RiskControl:{key}:calls:{flag}";
+            await RedisHelper.IncrByAsync(cache_key);
+            await RedisHelper.ExpireAsync(cache_key, 3 * 86400);
+        }
+
+         
+        public static async Task<int> GetAllNodesTkEndpointCallsAsync(int accountId, int ep_id, string flag)
+        {
+            string key = $"tk_endpoint:{accountId}:{ep_id}:{flag}";
+            try
+            {
+                var tasks = EndPointCore.List()
+                    .Where(node => node.is_public_api && !string.IsNullOrEmpty(node.redis_server))
+                    .Select(async node =>
+                    {
+                        try
+                        {
+                            var redisServer = EndPointCore.GetRedisServer(node);
+                            if (string.IsNullOrEmpty(redisServer)) return 0;
+
+                            string cacheKey = $"RiskControl:{key}:calls:{flag}";
+                            using var scope = await RedisClientFactory.CreateScopeAsync(redisServer);
+                            return await scope.Client.GetAsync<int>(cacheKey);
+                        }
+                        catch (Exception ex) { }
+                        return 0;
+                    });
+
+                var results = await Task.WhenAll(tasks);
+                var totalCalls = results.Sum();
+                _calls.TryAdd(key, totalCalls);
+                return totalCalls;
+            }
+            catch (Exception)
+            {
+                // TODO: 添加日志
+                return 0;
+            }
+        }
 
         public static int GetCalls(TkChannelEnum channel, int accountId, string flag)
         {

+ 125 - 0
molilian.core/Core/taoke/TkEndpointCore.cs

@@ -0,0 +1,125 @@
+using dodohold.core;
+using molilian.core;
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using YunhuiKit;
+using static molilian.core.TkPoolCore;
+
+public partial class TkEndpointCore
+{
+    private static SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
+
+    private static IEnumerable<TkEndpointConfigDTO> _cached;
+
+    // 添加到 TkEndpointCore 类中
+    private static readonly ConcurrentDictionary<int, List<TkEndpointConfigDTO>> _accountEndpointCache = new();
+
+
+    private static async Task<IEnumerable<TkEndpointConfigDTO>> ListAsync(bool force = false)
+    {
+        // 内存缓存检查
+        if (!force && _cached != null) return _cached;
+        string cache_key = "cache:tk_endpoint_config";
+
+        if (!force)
+        {
+            var cachedList = await RedisHelper.GetAsync<IEnumerable<TkEndpointConfigDTO>>(cache_key);
+            if (cachedList != null)
+            {
+                _cached = cachedList;
+                return cachedList;
+            }
+        }
+        // 获取新数据
+        await _semaphore.WaitAsync();
+        try
+        {
+            // 双重检查,防止并发情况下重复加载
+            if (!force)
+            {
+                var cachedList = await RedisHelper.GetAsync<IEnumerable<TkEndpointConfigDTO>>(cache_key);
+                if (cachedList != null)
+                {
+                    _cached = cachedList;
+                    return cachedList;
+                }
+            }
+
+            // 从数据库加载数据
+            var list = new DBContext.Table("tk_endpoint_config")
+                .Where("status=@status", new { status = 1 })
+                .Select<TkEndpointConfigDTO>();
+
+            if (list == null) return default;
+
+            // 更新调用次数
+            //foreach (var item in list)
+            //{
+            //    item.current_daily_calls = await RiskControlCore.GetAllNodesTkEndpointCallsAsync(item.id, DateTime.Now.ToString("yyyyMMdd"));
+            //    item.current_hourly_calls = await RiskControlCore.GetAllNodesTkEndpointCallsAsync(item.id, DateTime.Now.ToString("yyyyMMddHH"));
+            //}
+
+            // 更新缓存
+            await RedisHelper.SetAsync(cache_key, list, 30 * 86400);
+            _cached = list;
+            _accountEndpointCache.Clear();
+            return list;
+        }
+        finally
+        {
+            _semaphore.Release();
+        }
+    }
+
+
+    public static async Task<List<TkEndpointConfigDTO>> GetEndpointsByAccountAsync(int accountId)
+    {
+        // 1. 从缓存读取(直接返回副本)
+        if (_accountEndpointCache.TryGetValue(accountId, out var cached))
+            return cached.Select(x => x.DeepCopy()).ToList();
+
+        // 2. 获取所有端点(包含全局和专属)
+        var allEndpoints = (await ListAsync())?.Where(e => e.status).ToList() ?? [];
+
+        // 3. 合并逻辑:专属配置覆盖全局配置(关键!)
+        var mergedEndpoints = allEndpoints
+            .GroupBy(e => e.endpoint)
+            .Select(g =>
+            {
+                // 优先取专属配置
+                var accountSpecific = g.FirstOrDefault(e => e.tk_pool_id == accountId);
+                if (accountSpecific != null)
+                    return accountSpecific.DeepCopy(); // 深拷贝专属配置
+
+                // 否则取全局配置(tk_pool_id=0)
+                var global = g.FirstOrDefault(e => e.tk_pool_id == 0);
+                return global?.DeepCopy(); // 深拷贝全局配置
+            })
+            .Where(e => e != null)
+            .ToList();
+
+        // 4. 存入缓存(存储深拷贝后的组合配置)
+        _accountEndpointCache.TryAdd(accountId, mergedEndpoints.Select(x => x.DeepCopy()).ToList());
+
+        return mergedEndpoints;
+    }
+
+
+
+    // 添加缓存清理方法
+    public static void ClearEndpointCache(int accountId)
+    {
+        _accountEndpointCache.TryRemove(accountId, out _);
+    }
+
+
+
+    public static void Refresh()
+    {
+        _ = AllListAsync(true);
+        _ = ListAsync(true);
+        _accountEndpointCache.Clear();
+    }
+
+}

+ 419 - 0
molilian.core/Core/taoke/TkEndpointManager.cs

@@ -0,0 +1,419 @@
+using dodohold.core;
+using Google.Protobuf.WellKnownTypes;
+using molilian.core;
+using System;
+using System.Collections.Concurrent;
+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)
+    {
+        var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId);
+        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)
+    {
+        var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId);
+        var dict = new ConcurrentDictionary<string, int>();
+
+        if (endpoints != null)
+        {
+            foreach (var endpoint in endpoints)
+            {
+                dict.TryAdd(endpoint.endpoint, endpoint.suspend_duration);
+            }
+        }
+
+        return dict;
+    }
+    public static bool IsEndpointSuspended(int accountId, string endpoint)
+    {
+        if (!accountStates.TryGetValue(accountId, out var state))
+            return false;
+
+        return state.IsEndpointSuspended(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>
+    /// <param name="accountId">账号ID</param>
+    /// <returns>true如果至少有一个可用endpoint,否则false</returns>
+    public static async Task<bool> HasAvailableEndpointAsync(int accountId)
+    {
+        var availableApis = await GetAllAvailableApisAsync(accountId);
+        if (availableApis.Count == 0) return false;
+
+        if (!accountStates.TryGetValue(accountId, out var state))
+        {
+            return true; // 如果账号没有状态记录,所有节点都是正常的
+        }
+
+        return availableApis.Any(api => !state.IsEndpointSuspended(api.Value));
+    }
+
+    /// <summary>
+    /// 获取多个账号的节点状态信息
+    /// </summary>
+    public static async Task<Dictionary<int, Dictionary<string, string>>> GetStatus(IEnumerable<int> accountIds)
+    {
+        var result = new Dictionary<int, Dictionary<string, string>>();
+
+        foreach (var accountId in accountIds.Distinct())
+        {
+            var availableApis = await GetAllAvailableApisAsync(accountId);
+            result[accountId] = await GetStatus(accountId, availableApis);
+        }
+
+        return result;
+    }
+
+    /// <summary>
+    /// 获取指定账号的节点状态信息
+    /// </summary>
+    public static async Task<Dictionary<string, string>> GetStatus(int accountId, Dictionary<int, string> availableApis)
+    {
+        var status = new Dictionary<string, string>();
+        if (availableApis.Count == 0)
+        {
+            return status;
+        }
+
+        if (!accountStates.TryGetValue(accountId, out var state))
+        {
+            // 如果账号没有状态记录,所有节点都是正常的
+            foreach ((var api_id, var api) in availableApis)
+            {
+                status[api] = "正常";
+            }
+            return status;
+        }
+
+        foreach ((var api_id, var api) in availableApis)
+        {
+            if (state.IsEndpointSuspended(api))
+            {
+                var suspendUntil = state.GetSuspendTime(api);
+                var remainingTime = suspendUntil - DateTime.Now;
+                status[api] = $"挂起 (剩余时间: {remainingTime:mm\\:ss})";
+            }
+            else
+            {
+                status[api] = "正常";
+            }
+        }
+        return status;
+    }
+
+    public static async Task<(int, string)> GetConvertApiAsync(int accountId, string parseEndpoint)
+    {
+        var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(accountId);
+        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. 先找出所有未被挂起、未达限制的端点
+        var availableEndpoints = availableApis
+            .Where(e => !state.IsEndpointSuspended(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. 如果存在可用端点,直接返回
+        if (availableEndpoints.Length > 0)
+        {
+            // 生成唯一的轮询key,确保每种可用端点组合都有独立索引
+            string roundRobinKey = $"{accountId}:{string.Join(",", availableEndpoints.Select(e => e.ep_id).OrderBy(x => x))}";
+            var currentIndex = state.GetOrAddAccountIndex(roundRobinKey);
+            var newIndex = (currentIndex + 1) % availableEndpoints.Length;
+            state.UpdateAccountIndex(roundRobinKey, newIndex);
+            var selectedEndpoint = availableEndpoints[newIndex];
+
+            // 异步更新调用统计(不阻塞当前请求)
+            _ = UpdateEndpointStatsAsync(accountId, selectedEndpoint, now);
+            return (selectedEndpoint.ep_id, selectedEndpoint.endpoint);
+        }
+
+        // 5. 检查是否仅因时间间隔限制
+        var intervalLimitedEndpoints = availableApis
+            .Where(e => !state.IsEndpointSuspended(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 &&
+                       e.last_call_time != null &&
+                       (now - e.last_call_time).TotalSeconds < e.interval_seconds)
+            .ToArray();
+
+        if (intervalLimitedEndpoints.Length > 0)
+        {
+            // 如果是时间间隔限制,返回空字符串
+            return (0, string.Empty);
+        }
+
+        // 6. 检查所有端点的状态
+        var allEndpointsSuspended = availableApis.All(e => state.IsEndpointSuspended(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));
+
+        // 如果所有端点都被挂起或达到限制,返回 "ALL"
+        if (allEndpointsSuspended || allEndpointsLimited)
+        {
+            return (0, "ALL");
+        }
+
+        // 7. 其他情况(理论上不应该到达这里)
+        return (0, string.Empty);
+    }
+
+    // 辅助方法:异步更新调用统计(原逻辑,仅拆分以保持清晰)
+    private static async Task UpdateEndpointStatsAsync(int accountId, TkEndpointConfigDTO selectedEndpoint, DateTime now)
+    {
+        var currentHour = now.ToString("yyyyMMddHH");
+        var currentDay = now.ToString("yyyyMMdd");
+
+        // 更新调用计数
+        selectedEndpoint.current_hourly_calls++;
+        selectedEndpoint.current_daily_calls++;
+        selectedEndpoint.last_call_time = now;
+
+        // 保存到 Redis
+        await RiskControlCore.SetTkEndpointCallsAsync(accountId, selectedEndpoint.id, currentHour, selectedEndpoint.current_hourly_calls);
+        await RiskControlCore.SetTkEndpointCallsAsync(accountId, selectedEndpoint.id, currentDay, selectedEndpoint.current_daily_calls);
+
+        // 检查是否需要挂起(原逻辑)
+        if (selectedEndpoint.hourly_calls_limit > 0 && selectedEndpoint.current_hourly_calls >= selectedEndpoint.hourly_calls_limit)
+        {
+            _ = SuspendAsync(accountId, selectedEndpoint.endpoint, SuspendReason.HourlyLimit, selectedEndpoint.suspend_duration);
+        }
+        if (selectedEndpoint.daily_calls_limit > 0 && selectedEndpoint.current_daily_calls >= selectedEndpoint.daily_calls_limit)
+        {
+            _ = SuspendAsync(accountId, selectedEndpoint.endpoint, SuspendReason.DailyLimit, selectedEndpoint.suspend_duration);
+        }
+    }
+
+    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
+        {
+            // 在锁外部获取所需数据(避免在锁内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})");
+        }
+        finally
+        {
+            semaphore.Release();
+        }
+    }
+
+
+    /// <summary>
+    /// 主动释放指定账号的挂起状态
+    /// </summary>
+    /// <param name="accountId">账号ID</param>
+    /// <param name="endpoint">要释放的端点名称,如果为null则释放所有端点的挂起状态</param>
+    public static void ReleaseSuspend(int accountId, string endpoint = null)
+    {
+        // 获取或创建账号特定的锁对象
+        var accountLock = accountLocks.GetOrAdd(accountId, _ => new object());
+
+        lock (accountLock)
+        {
+            if (accountStates.TryGetValue(accountId, out var state))
+            {
+                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}");
+            }
+        }
+    }
+
+
+    private static TimeSpan CalculateHourlySuspendDuration()
+    {
+        var now = DateTime.UtcNow;
+        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 tomorrow = now.Date.AddDays(1); // 次日零点
+        return tomorrow - now;
+    }
+
+    public enum SuspendReason
+    {
+        /// <summary>
+        /// 主动暂停(手动操作)
+        /// </summary>
+        Active,
+
+        /// <summary>
+        /// 风控导致的被动暂停
+        /// </summary>
+        Passive,
+
+        /// <summary>
+        /// 每小时总量限制触发的暂停
+        /// </summary>
+        HourlyLimit,
+
+        /// <summary>
+        /// 每日总量限制触发的暂停
+        /// </summary>
+        DailyLimit
+    }
+
+    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;
+    }
+
+
+}

+ 88 - 48
molilian.core/Core/taoke/TkPoolCore.cs

@@ -1,15 +1,4 @@
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Controllers;
-using Microsoft.AspNetCore.Mvc.Filters;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using dodohold.core;
-using Spire.Pdf.Exporting.XPS.Schema;
-using System.Xml.Linq;
-using System.Net;
-using TencentCloud.Mrs.V20200910.Models;
+using dodohold.core;
 using YunhuiKit;
 
 
@@ -35,20 +24,95 @@ namespace molilian.core
 
         private static IEnumerable<TkPoolDTO> _cached;
         private static IEnumerable<TkPoolDTO> _all_cached;
-        public static async Task<TkPoolDTO> GetOneAsync(TkAction action)
+
+        public static async Task<TkPoolDTO> GetOneAsync(int id)
         {
             var list = await ListAsync();
             if (!list.Any()) return null;
-            return list.Where(e => FilterNodes(e, action)).OrderBy(l => Guid.NewGuid()).FirstOrDefault();
+            return list.Where(e => e.id == id).FirstOrDefault();
         }
-        public static async Task<TkPoolDTO> GetOneAsync(int id)
+
+        public static async Task<TkPoolDTO> GetOneAsync2(TkAction action)
         {
-            var list = await ListAsync();
+            var list = await ListAsync().ConfigureAwait(false);
             if (!list.Any()) return null;
-            return list.Where(e => e.id == id).FirstOrDefault();
+
+            // 线程安全的随机排序
+            var random = new Random(Guid.NewGuid().GetHashCode());
+            var shuffled = list.OrderBy(_ => random.Next()).ToList();
+
+            foreach (var item in shuffled)
+            {
+                if (await FilterNodesAsync(item, action).ConfigureAwait(false))
+                    return item;
+            }
+            return null;
+        }
+        public static async Task<TkPoolDTO> GetOneAsync(TkAction action)
+        {
+            var list = await ListAsync().ConfigureAwait(false);
+            if (!list.Any())
+                return null;
+
+            // 1. 筛选出有可用端点的账号池,并计算权重
+            var weightedAccounts = new List<(TkPoolDTO account, int weight)>();
+            var random = new Random(Guid.NewGuid().GetHashCode()); // 避免重复种子问题
+
+            foreach (var item in list)
+            {
+                // 获取账号关联的所有端点配置
+                var endpoints = await TkEndpointCore.GetEndpointsByAccountAsync(item.id);
+                if (endpoints == null || endpoints.Count == 0)
+                    continue;
+
+                // 过滤可用端点(状态正常且未达限制)
+                var availableEndpoints = endpoints
+                    .Where(e => e.status)
+                    .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 => !TkEndpointManager.IsEndpointSuspended(e)) // 直接检查端点对象
+                    .ToList();
+
+                if (availableEndpoints.Count == 0) continue;
+
+                // 计算账号权重(基于剩余调用量)
+                int weight = availableEndpoints.Sum(e =>
+                    e.hourly_calls_limit <= 0 ? 3600 : e.hourly_calls_limit - e.current_hourly_calls);
+
+                weightedAccounts.Add((item, weight));
+            }
+
+            if (weightedAccounts.Count == 0)  return null;
+
+            // 2. 加权随机选择
+            int totalWeight = weightedAccounts.Sum(x => x.weight);
+            int randomNumber = random.Next(0, totalWeight);
+
+            foreach (var (account, weight) in weightedAccounts)
+            {
+                if (randomNumber < weight)
+                {
+                    if (await FilterNodesAsync(account, action).ConfigureAwait(false))
+                        return account;
+                    break; // 如果过滤失败,跳出当前循环
+                }
+                randomNumber -= weight;
+            }
+
+            // 3. 如果加权选择失败,回退到简单随机选择
+            var fallbackCandidates = weightedAccounts
+                .OrderBy(_ => random.Next())
+                .Select(x => x.account);
+
+            foreach (var account in fallbackCandidates)
+            {
+                if (await FilterNodesAsync(account, action).ConfigureAwait(false))
+                    return account;
+            }
+            return null;
         }
 
-        private static bool FilterNodes(TkPoolDTO item, TkAction action)
+        private static async Task<bool> FilterNodesAsync(TkPoolDTO item, TkAction action)
         {
             if (!string.IsNullOrEmpty(item.suspended_endpoint) && item.suspended_endpoint.Contains($"{_end_point}|")) return false;
 
@@ -70,6 +134,10 @@ namespace molilian.core
                 int hourly_num = RiskControlCore.GetCalls(TkChannelEnum.tb, item.id, DateTime.Now.ToString("yyyyMMddHH"));
                 if (hourly_num >= item.hourly_calls_limit) return false;
             }
+            var endpointCheck = await TkEndpointManager.HasAvailableEndpointAsync(item.id).ConfigureAwait(false);
+
+            if (!endpointCheck) return false;
+
 
             switch (action)
             {
@@ -90,7 +158,6 @@ namespace molilian.core
         }
 
 
-
         public static async Task<IEnumerable<TkPoolDTO>> AllListAsync(bool force = false)
         {
             if (!force && _all_cached != default) return _all_cached;
@@ -180,11 +247,12 @@ namespace molilian.core
             }
         }
 
-
         public static void Refresh()
         {
             _ = AllListAsync(true);
             _ = ListAsync(true);
+            TkEndpointCore.Refresh();
+            TkEndpointManager.Refresh();
         }
         public static void UpdateDrawBalance(string name, decimal amout)
         {
@@ -223,34 +291,6 @@ namespace molilian.core
             EndPointCore.NotifyReload(true);
         }
 
-
-        public static bool IsNeedSwitchBackup(int accountId)
-        {
-            string cache_key = $"cache:tk_pool:{accountId}:switchBackup";
-            long count = RedisHelper.Get<int>(cache_key);
-            return count != 0;
-        }
-
-
-        public static void SwitchBackup(string endpoint, int accountId, string name, string content)
-        {
-            string cache_key = $"cache:tk_pool:{accountId}:switchBackup";
-            long count = RedisHelper.IncrBy(cache_key);
-            RedisHelper.Expire(cache_key, 3600 * 4);
-            if (count > 1) return;
-
-            //_ = ListAsync(true);
-            NotifyCore.Notify(new NifyMessage
-            {
-                message = $"【淘客{accountId}:{name}】{endpoint} 风控,切换备用接口",
-                priority = NifyMessagePriority.high,
-                tags = ["red_circle"]
-            });
-            NotifyCore.AnPushNotify("切换接口", $"【淘客{accountId}:{name}】{endpoint} 切换备用接口");
-            EndPointCore.NotifyReload(true);
-        }
-
-
         public static void Disabled(int accountId, string name, string content)
         {
 #if DEBUG

+ 1 - 1
molilian.core/Core/taoke/UnionParseCore.cs

@@ -373,7 +373,7 @@ namespace molilian.core
                 using var cts = new CancellationTokenSource();
                 cts.CancelAfter(timeout);
 
-                var requestTask = Task.Run(() => alimama.UnionParse2Async(content, commerceType, result, cts.Token, swData), cts.Token);
+                var requestTask = Task.Run(() => alimama.UnionParseAsync(ip, oaid, content, commerceType, result, cts.Token, swData), cts.Token);
                 var delayTask = Task.Delay(timeout, cts.Token);
                 var completedTask = await Task.WhenAny(requestTask, delayTask);
                 if (completedTask == requestTask)

+ 0 - 7
molilian.core/Core/tool/DeeplinkParseCore.cs

@@ -1,17 +1,10 @@
 
-using COSXML.Network;
 using dodohold.core;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.FileSystemGlobbing.Internal;
 using Sayaka.Common;
-using Spire.Pdf.Annotations;
-using Spire.Pdf.Exporting.XPS.Schema;
-using System.Security.Cryptography;
 using System.Text.Json;
 using System.Text.RegularExpressions;
 using System.Web;
-using System.Xml.Linq;
-using TencentCloud.Tcss.V20201101.Models;
 namespace molilian.core
 {
     public partial class DeeplinkParseCore

+ 0 - 6
molilian.core/Core/tool/DeeplinkParseRuleCore.cs

@@ -1,11 +1,5 @@
 
 using dodohold.core;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.FileSystemGlobbing.Internal;
-using Spire.Pdf.Annotations;
-using System.Text.RegularExpressions;
-using System.Threading.Channels;
-using System.Xml.Linq;
 namespace molilian.core
 {
 

+ 1 - 0
molilian.core/DTO/VeapiPoolDTO.cs

@@ -9,6 +9,7 @@
         public DateTime create_time { get; set; }
         public DateTime last_time { get; set; }
         public bool status { get; set; }
+        public string refpid { get; set; } = string.Empty;
 
     }
 }

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

@@ -80,6 +80,7 @@ namespace molilian.core
         public int accountId { get; set; } = 0;
         public string accountName { get; set; } = string.Empty;
         public string proxy_node { get; set; } = string.Empty;
+        public int tkEndpoint { get; set; } = 0;
         public bool success { get; set; } = true;
         public bool commercial { get; set; } = false;
         public string message { get; set; } = string.Empty;

+ 35 - 0
molilian.core/DTO/alimama/TkEndpointConfigDTO.cs

@@ -0,0 +1,35 @@
+using dodohold.core;
+using System.Xml.Linq;
+
+namespace molilian.core
+{
+    [Table("tk_endpoint_config")]
+    public class TkEndpointConfigDTO
+    {
+        [Key]
+        public int id { get; set; }
+        public int tk_pool_id { get; set; }
+        public int ep_id { get; set; }
+
+        public string name { get; set; } = string.Empty;
+
+        public string endpoint { get; set; } = string.Empty;
+
+        public bool enable_coupon { get; set; } = false;
+
+        public int daily_calls_limit { get; set; } = 0;
+
+        public int current_daily_calls { get; set; } = 0;
+
+        public int hourly_calls_limit { get; set; } = 0;
+
+        public int current_hourly_calls { get; set; } = 0;
+
+        public int interval_seconds { get; set; } = 0;
+
+        public bool status { get; set; } = false;
+        public int suspend_duration { get; set; } = 0;
+
+        public DateTime last_call_time { get; set; } = DateTime.MinValue;
+    }
+}

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

@@ -58,5 +58,7 @@
         public bool is_banned { get; set; } = false;
         public bool is_hide { get; set; } = true;
         public bool enable_call_stats { get; set; } = true;
+        public string parseEndpoint { get; set; } = string.Empty;
+        public string related_account_ids { get; set; } = string.Empty;
     }
-}
+}

+ 0 - 1
molilian.core/DTO/deeplink/DeeplinkParseRuleDTO.cs

@@ -1,5 +1,4 @@
 using dodohold.core;
-using Spire.Pdf.Exporting.XPS.Schema;
 
 namespace molilian.core
 {

+ 0 - 6
molilian.core/Plus/Alimama/RefundOrders.cs

@@ -1,11 +1,5 @@
 using dodohold.core;
-using Spire.Pdf.Xmp;
-using System.Data;
-using System.Drawing.Printing;
-using System.Runtime.InteropServices;
 using System.Text.Json;
-using System.Text.RegularExpressions;
-using static System.Runtime.InteropServices.JavaScript.JSType;
 
 namespace molilian.core
 {

+ 0 - 3
molilian.core/Plus/Alimama/bills.cs

@@ -1,7 +1,4 @@
 using dodohold.core;
-using Spire.Pdf.Xmp;
-using System.Data;
-using System.Drawing.Printing;
 using System.Text.Json;
 
 namespace molilian.core

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

@@ -1,12 +1,5 @@
 using dodohold.core;
-using Spire.Pdf.Xmp;
-using System.Data;
-using System.Drawing.Printing;
-using System.Runtime.InteropServices;
 using System.Text.Json;
-using System.Text.RegularExpressions;
-using System.Xml.Linq;
-using static System.Runtime.InteropServices.JavaScript.JSType;
 
 namespace molilian.core
 {

+ 78 - 44
molilian.core/Plus/Alimama/parse.cs

@@ -27,7 +27,7 @@ namespace molilian.core
         private int _api_id = 0;
         public TkConfigDTO _config;
         private static string _end_point;
-        private static TkPoolDTO _account;
+        private TkPoolDTO _account;
         static Random _random = new Random();
 
         public static string _url_pattern = @"https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]";
@@ -69,37 +69,7 @@ namespace molilian.core
             return result.link_type == LinkTypeEnum.other_aff;
         }
 
-        public TkDataDTO UnionParse(string content, ref TkDataDTO result)
-        {
-            InternalTextProcessing(content, ref result);
-            //(result.success, result.content, result.link_type, result.shortLinkurl) = InternalTextProcessing(content,ref result);
-            if (result.link_type == LinkTypeEnum.other_aff)
-            {
-                result.channel = TkChannelEnum.tb;
-                result.channel_type = ChannelTypeEnum.tb;
-                result.link_type = LinkTypeEnum.unknown;
-                result.success = false;
-                result.message = "放弃转链";
-                result.reason = "其他推广链接";
-                result.subCode = TkSubCodeEnum.OtherPromo;
-                result.accountName = string.Empty;
-                result.content = content;
-                //result.deeplink_url = GetDeeplink(null);
-                result.itemName = GetTitle(content);
-            }
-            else
-            {
-                //处理过后的正文 用于转链
-                if (result.success) content = result.content;
-                result = alimamaParse(content, result);
-            }
-
-            result.content = content;
-            result.link_type = result.link_type;
-            return result;
-        }
-
-        public async Task<TkDataDTO> UnionParseAsync(string content, int commerceType, TkDataDTO result,
+        public async Task<TkDataDTO> UnionParseAsync(string ip, string oaid, string content, int commerceType, TkDataDTO result,
             CancellationToken cancellationToken = default,
             Dictionary<string, long> swData = null)
         {
@@ -157,7 +127,7 @@ namespace molilian.core
             }
 
             //============================== 放弃转链-地区过滤 ==============================
-            bool is_ignore2 = FlowControlIgnoreRequest(out string reason);
+            bool is_ignore2 = FlowControlIgnoreRequest(ip, oaid, out string reason);
             if (is_ignore2)
             {
                 result.success = false;
@@ -178,31 +148,93 @@ namespace molilian.core
                     return result;
             }
 
-
             //2025-02-06 增加重试机制
             int try_count = 0;
 #if DEBUG
             _config.tk_parse_retry_count = 3;
+
 #endif
             while (try_count == 0 || try_count <= _config.tk_parse_retry_count)
             {
                 if (try_count >= 3) break;
 
-                result = await alimamaParseAsync2(result.content, result, cancellationToken);
-
-                //if (TkPoolCore.IsNeedSwitchBackup(_accountId))
-                //{
-                //    result = await alimamaParseAsync2(result.content, result, cancellationToken);
-                //}
-                //else
+                //if (!string.IsNullOrEmpty(_api) && _api_id > 0)
                 //{
-                //    result = await alimamaParseAsync(result.content, result, cancellationToken);
+                //    switch (_api)
+                //    {
+                //        case "veapi":
+                //            var account = VeapiPoolCore.Get(_api_id);
+                //            if (account != null)
+                //            {
+                //                var core = new VeapiPlus(account.name, account.key, account.sessionKey);
+                //                result = await core.UnionParseAsync(content, _refpid, result, cancellationToken);
+                //                result.tkEndpoint = 4;
+                //                return result;
+                //            }
+                //            break;
+                //        case "dataoke":
+                //            break;
+                //        default:
+                //            break;
+                //    }
                 //}
 
+                (int ep_id, string tkEndpoint) = await TkEndpointManager.GetConvertApiAsync(_accountId, _account.parseEndpoint);
+                result.tkEndpoint = ep_id;
+                switch (tkEndpoint)
+                {
+                    case "moose_share":
+                        result = await endpoint_moose_share(result.content, result, cancellationToken);
+                        break;
+                    case "promo_goods":
+                        result = await endpoint_promo_goods(result.content, result, cancellationToken);
+                        break;
+                    case "tool_links":
+                        result = await endpoint_tool_links(result.content, result, cancellationToken);
+                        break;
+                    case "veapi":
+                        if (!string.IsNullOrEmpty(_api) && _api_id > 0)
+                        {
+                            var account = VeapiPoolCore.Get(_api_id);
+                            if (account != null)
+                            {
+                                var core = new VeapiPlus(account.name, account.key, account.sessionKey);
+                                string refpid = string.IsNullOrEmpty(account.refpid) ? _refpid : account.refpid;
+                                result = await core.UnionParseAsync(content, refpid, result, cancellationToken);
+                                result.tkEndpoint = 4;
+                                return result;
+                            }
+                        }
+                        else
+                        {
+                            result.link_type = LinkTypeEnum.unknown;
+                            result.subCode = TkSubCodeEnum.Other;
+                            result.success = false;
+                            result.message = "放弃转链";
+                            result.reason = "接口配置失败";
+                            return result;
+                        }
+                        break;
+                    case "ALL":
+                        //没有可以用的接口了
+                        result.link_type = LinkTypeEnum.unknown;
+                        result.subCode = TkSubCodeEnum.ParseDeny;
+                        result.success = false;
+                        result.message = "放弃转链";
+                        result.reason = "接口风控ALL";
+                        return result;
+                    default:
+                        result.link_type = LinkTypeEnum.unknown;
+                        result.subCode = TkSubCodeEnum.ParseDeny;
+                        result.success = false;
+                        result.message = "放弃转链";
+                        result.reason = $"接口风控{tkEndpoint}";
+                        break;
+                }
+
                 if (!"该链接不支持转化,请更换链接尝试".Equals(result.message)) break;
                 try_count++;
             }
-            //result = await alimamaParseAsync(result.content, result, cancellationToken);
             result.retry_count = try_count;
             result.commercial = result.success;
 
@@ -552,9 +584,11 @@ namespace molilian.core
             return false;
         }
 
-        public static bool FlowControlIgnoreRequest(out string reason)
+        public static bool FlowControlIgnoreRequest(string ip, string oaid, out string reason)
         {
             reason = string.Empty;
+            if (ip.StartsWith("127.0.0")) return false;
+
             try
             {
                 // IP和流量控制

+ 3 - 800
molilian.core/Plus/Alimama/parse_2.cs

@@ -10,6 +10,8 @@ using System.Threading;
 using TencentCloud.Tcm.V20210413.Models;
 using COSXML.Network;
 using System.Net;
+using TencentCloud.Cdb.V20170320.Models;
+using System.Security.Cryptography;
 
 namespace molilian.core
 {
@@ -35,39 +37,6 @@ namespace molilian.core
             return result;
         }
 
-        public void UnionParse2(string content, ref TkDataDTO result)
-        {
-            if ("veapi".Equals(_api) && _api_id > 0)
-            {
-                var account = VeapiPoolCore.Get(_api_id);
-                if (account != null)
-                {
-                    var core = new VeapiPlus(account.name, account.key, account.sessionKey);
-                    core.UnionParse(content, ref result);
-                    return;
-                }
-            }
-            UnionParse(content, ref result);
-        }
-
-        public async Task<TkDataDTO> UnionParse2Async(string content, int commerceType, TkDataDTO result,
-            CancellationToken cancellationToken = default,
-            Dictionary<string, long> swData = null)
-        {
-            if ("veapi".Equals(_api) && _api_id > 0)
-            {
-                var account = VeapiPoolCore.Get(_api_id);
-                if (account != null)
-                {
-                    var core = new VeapiPlus(account.name, account.key, account.sessionKey);
-                    result = await core.UnionParseAsync(content, result, cancellationToken);
-                    return result;
-                }
-            }
-            result = await UnionParseAsync(content, commerceType, result, cancellationToken, swData);
-            return result;
-        }
-
         /// <summary>
         /// 提取url里面的id参数
         /// </summary>
@@ -82,7 +51,7 @@ namespace molilian.core
             }
             return null;
         }
-        
+
         /// <summary>
         /// 是否淘宝链接,如果是商品链接就格式化成https://item.taobao.com/item.htm?id=****
         /// </summary>
@@ -618,772 +587,6 @@ namespace molilian.core
             return LinkTypeEnum.unknown;
         }
 
-        public async Task<TkDataDTO> alimamaParseAsync2(string content, TkDataDTO result, CancellationToken cancellationToken = default)
-        {
-            string t = $"{DateTime.Now.Convert2UnixTimestamp(true)}";
-
-            Random random = new();
-            double randomNumber = random.NextDouble();
-            string randomString = randomNumber.ToString()[2..];
-
-            string cna = _cookies.GetContentPart("cna=", ";");
-            string firstFiveCharacters = cna[..Math.Min(5, cna.Length)];
-
-            //var variableMap = new
-            //{
-            //    url = content,
-            //    union_lens = $"b_pvid:a219t._portal_v2_tool_links_page_home_index_htm_{t}_{randomString}_{firstFiveCharacters}",
-            //    lensScene = "PUB",
-            //    spmB = "_portal_v2_tool_links_page_home_index_htm"
-            //}.Convert2Json(true).UrlEncode();
-
-            string item_id = content.GetContentPart("item.htm?id=", "");
-            var variableMap = new
-            {
-                itemId = item_id
-            }.Convert2Json(true).UrlEncode();
-
-
-            variableMap = variableMap.Replace(" ", "%20");
-            _floorId = "102359";
-            string url = "https://pub.alimama.com/openapi/param2/1/gateway.unionpub/xt.entry.json?" +
-                $"t={t}&_tb_token_={_tb_token}&floorId={_floorId}&refpid={_refpid}&variableMap={variableMap}";
-
-
-
-            Stopwatch stopwatch = Stopwatch.StartNew();
-            stopwatch.Start();
-            var client = new WebClientUtility().SetContentType("application/json;charset=utf-8")
-                        .AddHeaders("X-Requested-With", "XMLHttpRequest")
-                        .AddHeaders("Cookie", _cookies);
-            client.Proxy = _proxy;
-#if DEBUG
-            client.Proxy = null;
-#endif
-            if (!string.IsNullOrEmpty(_user_agent)) client.UserAgent = _user_agent;
-            client.Timeout = GetRequestTimeout2(result.elapsedTime2);
-            //var response = client.Request(url);
-
-#if DEBUG
-            var response = await client.RequestAsync(url, "GET");
-#else
-            var response = await client.RequestAsync(url, "GET", cancellationToken);
-#endif
-
-            stopwatch.Stop();
-            result.elapsedTime3 = (int)stopwatch.ElapsedMilliseconds;
-
-
-
-            var body = string.Empty;
-            try
-            {
-                if (!response.Successed)
-                {
-                    result.channel = TkChannelEnum.tb;
-                    result.link_type = LinkTypeEnum.unknown;
-                    result.success = false;
-                    result.message = "fail";
-
-                    if (response.ResponseException != null)
-                    {
-                        _ = new LoggerLibrary("api_error", "fail")
-                            .Info(response.ResponseException.Message, response.ResponseException.StackTrace)
-                            .SaveAsync();
-                        throw response.ResponseException;
-                    }
-
-                    return result;
-                }
-                if (response.ResponseMessage.StatusCode == System.Net.HttpStatusCode.Found)
-                {
-                    _ = new LoggerLibrary("api_error", "fail")
-                        .Info($"302:{response.ResponseMessage.Headers.Location}")
-                        .SaveAsync();
-                    throw new Exception("302 Found, was canceled");
-                }
-                JsonElement root;
-                try
-                {
-                    body = response.Body();
-                    //{"success":false,"message":"result empty"}
-
-
-                    root = body.Convert2JsonElement();
-                }
-                catch (Exception ex)
-                {
-                    _ = new LoggerLibrary("api_error", "fail")
-                        .Info(ex.Message, ex.StackTrace)
-                        .SaveAsync();
-
-                    if (body.Contains("https://g.alicdn.com/sd/punish/waf_block.html"))
-                    {
-
-                        result.channel = TkChannelEnum.tb;
-                        result.link_type = LinkTypeEnum.unknown;
-                        result.success = false;
-                        result.message = "waf_block";
-                        result.reason = "接口风控";
-                        return result;
-                    }
-                    throw new Exception(body);
-                }
-
-
-                bool success = root.Read<bool>("success", false);
-                string message = root.Read("message", string.Empty);
-                string info_message = root.PathRead("info.message", string.Empty);
-                if (!string.IsNullOrEmpty(info_message)) message = info_message;
-
-                if ("result empty".Equals(message))
-                {
-                    result.subCode = TkSubCodeEnum.NoConvert;
-
-                }
-                JsonElement item = default;
-
-                if (success)
-                {
-                    var data = root.ElementRead("data");
-                    var resultList = data.ElementRead("resultList");
-                    if (resultList.ValueKind != JsonValueKind.Array)
-                    {
-                        _ = new LoggerLibrary("api_error", "fail")
-                            .Info("resultList Undefined", body)
-                            .SaveAsync();
-                        throw new Exception("resultList Undefined");
-                    }
-                    item = resultList[0];
-                }
-
-
-
-                string taoToken = item.Read("taoToken", string.Empty);
-                string shortLinkurl = item.Read("shortLinkurl", string.Empty);
-
-                string couponLinkTaoToken = item.Read("couponLinkTaoToken", string.Empty);
-                string couponShortLinkUrl = item.Read("couponShortLinkUrl", string.Empty);
-                decimal couponAmount = item.Read<decimal>("couponAmount", 0);
-                string couponEffectiveEndTime = item.Read("couponEffectiveEndTime", string.Empty);
-                string couponEffectiveStartTime = item.Read("couponEffectiveStartTime", string.Empty);
-
-                string itemId = item.Read("itemId", string.Empty);
-                string mktId = item.Read("outputMktId", string.Empty);
-                string itemName = item.Read("itemName", string.Empty);
-                decimal promotionPrice = item.Read<decimal>("promotionPrice", 0);
-                string sellerNickName = item.Read("sellerNickName", string.Empty);
-                string shopTitle = item.Read("shopTitle", string.Empty);
-                string pic = item.Read("data.pic", string.Empty);
-                if (pic.StartsWith("//")) pic = "https:" + pic;
-
-                string qrCodeUrl = item.Read("qrCodeUrl", string.Empty);
-
-
-                if (response.ResponseMessage != null)
-                {
-
-                    switch (response.ResponseMessage.StatusCode)
-                    {
-                        case System.Net.HttpStatusCode.OK:
-                            {
-                                if (couponAmount > 0 && !string.IsNullOrEmpty(couponLinkTaoToken) && !string.IsNullOrEmpty(couponShortLinkUrl))
-                                {
-                                    taoToken = couponLinkTaoToken;
-                                    shortLinkurl = couponShortLinkUrl;
-                                }
-                                string shortLink = GetLink(taoToken);
-                                content = ReplaceUrls(content, shortLink);
-                            }
-                            break;
-                        case System.Net.HttpStatusCode.Found:
-                            string location = response.ResponseMessage.Headers.Location.OriginalString;
-                            if (!string.IsNullOrEmpty(location) && location.Contains("www.alimama.com/member/login.htm"))
-                            {
-                                success = false;
-                                message = "nologin";
-                            }
-                            break;
-                        default:
-                            {
-                                success = false;
-                                message = "other";
-                            }
-                            break;
-                    }
-                }
-                if (!success)
-                {
-                    shortLinkurl = GetLink(content);
-                    switch (message)
-                    {
-                        case "该商品已经下架或未加入淘宝客":
-                        case "该链接不支持转化,请更换链接尝试":
-                        case "result empty":
-                            result.subCode = TkSubCodeEnum.NoConvert;
-                            break;
-                        case "网络错误":
-                            result.subCode = TkSubCodeEnum.NetError;
-                            break;
-                        default:
-                            var _headers = response.ResponseMessage.Headers;
-                            var headers = "";
-                            foreach (var h in _headers)
-                            {
-                                foreach (var Value in h.Value)
-                                {
-                                    headers += $"{h.Key}: {Value}\n";
-                                }
-                            }
-                            _ = new LoggerLibrary("api_error", "fail")
-                                .Info(message, content)
-                                .Info(headers, body)
-                                .SaveAsync();
-                            break;
-                    }
-                }
-
-                shortLinkurl = GetLink(taoToken);
-                string deeplink_url = GetDeeplink(shortLinkurl);
-
-                result.channel = TkChannelEnum.tb;
-
-                if (success)
-                {
-                    result.subCode = TkSubCodeEnum.Success;
-                    switch (result.link_type)
-                    {
-                        case LinkTypeEnum.profile:
-                        case LinkTypeEnum.goods:
-                        case LinkTypeEnum.video:
-                        case LinkTypeEnum.live:
-                            break;
-                        default:
-                            result.link_type = LinkTypeEnum.goods;
-                            break;
-                    }
-                }
-
-                result.success = success;
-                result.message = message;
-                result.reason = string.Empty;
-
-                result.content = content;
-                result.couponAmount = couponAmount;
-                result.couponEffectiveEndTime = couponEffectiveEndTime;
-                result.couponEffectiveStartTime = couponEffectiveStartTime;
-                result.taoToken = taoToken;
-                if (!string.IsNullOrEmpty(shortLinkurl))
-                {
-                    result.shortLinkurl = shortLinkurl;
-                    result.deeplink_url = deeplink_url;
-                }
-                result.mktId = mktId;
-
-                result.itemId = itemId;
-                result.itemName = itemName;
-                result.promotionPrice = promotionPrice;
-                result.sellerNickName = sellerNickName;
-                result.shopTitle = shopTitle;
-                result.pic = pic;
-                result.qrCodeUrl = qrCodeUrl;
-            }
-            catch (Exception ex)
-            {
-                if (response.ResponseMessage?.StatusCode == System.Net.HttpStatusCode.OK)
-                {
-                    if (body.Contains("<p>抱歉!页面无法访问……</p>"))
-                    {
-                        throw new APIException("抱歉!页面无法访问……");
-                    }
-                }
-                throw;
-            }
-            return result;
-        }
-
-        public async Task<TkDataDTO> alimamaParseAsync(string content, TkDataDTO result, CancellationToken cancellationToken = default)
-        {
-            string t = $"{DateTime.Now.Convert2UnixTimestamp(true)}";
-
-            Random random = new();
-            double randomNumber = random.NextDouble();
-            string randomString = randomNumber.ToString()[2..];
-
-            string cna = _cookies.GetContentPart("cna=", ";");
-            string firstFiveCharacters = cna[..Math.Min(5, cna.Length)];
-
-            var variableMap = new
-            {
-                url = content,
-                union_lens = $"b_pvid:a219t._portal_v2_tool_links_page_home_index_htm_{t}_{randomString}_{firstFiveCharacters}",
-                lensScene = "PUB",
-                spmB = "_portal_v2_tool_links_page_home_index_htm"
-            }.Convert2Json(true).UrlEncode();
-
-            variableMap = variableMap.Replace(" ", "%20");
-
-            string url = "https://pub.alimama.com/openapi/param2/1/gateway.unionpub/xt.entry.json?" +
-                $"t={t}&_tb_token_={_tb_token}&floorId={_floorId}&refpid={_refpid}&variableMap={variableMap}";
-
-
-
-            Stopwatch stopwatch = Stopwatch.StartNew();
-            stopwatch.Start();
-            var client = new WebClientUtility().SetContentType("application/json;charset=utf-8")
-                        .AddHeaders("X-Requested-With", "XMLHttpRequest")
-                        .AddHeaders("Cookie", _cookies);
-            client.Proxy = _proxy;
-#if DEBUG
-            client.Proxy = null;
-#endif
-            if (!string.IsNullOrEmpty(_user_agent)) client.UserAgent = _user_agent;
-            client.Timeout = GetRequestTimeout2(result.elapsedTime2);
-            //var response = client.Request(url);
-
-#if DEBUG
-            var response = await client.RequestAsync(url, "GET");
-#else
-            var response = await client.RequestAsync(url, "GET", cancellationToken);
-#endif
-
-            stopwatch.Stop();
-            result.elapsedTime3 = (int)stopwatch.ElapsedMilliseconds;
-
-
-
-            var body = string.Empty;
-            try
-            {
-                if (!response.Successed)
-                {
-                    result.channel = TkChannelEnum.tb;
-                    result.link_type = LinkTypeEnum.unknown;
-                    result.success = false;
-                    result.message = "fail";
-
-                    if (response.ResponseException != null)
-                    {
-                        _ = new LoggerLibrary("api_error", "fail")
-                            .Info(response.ResponseException.Message, response.ResponseException.StackTrace)
-                            .SaveAsync();
-                        throw response.ResponseException;
-                    }
-
-                    return result;
-                }
-                if (response.ResponseMessage.StatusCode == System.Net.HttpStatusCode.Found)
-                {
-                    _ = new LoggerLibrary("api_error", "fail")
-                        .Info($"302:{response.ResponseMessage.Headers.Location}")
-                        .SaveAsync();
-                    throw new Exception("302 Found, was canceled");
-                }
-                JsonElement root;
-                try
-                {
-                    body = response.Body();
-                    root = body.Convert2JsonElement();
-                }
-                catch (Exception ex)
-                {
-                    _ = new LoggerLibrary("api_error", "fail")
-                        .Info(ex.Message, ex.StackTrace)
-                        .SaveAsync();
-
-                    if (body.Contains("https://g.alicdn.com/sd/punish/waf_block.html"))
-                    {
-
-                        result.channel = TkChannelEnum.tb;
-                        result.link_type = LinkTypeEnum.unknown;
-                        result.success = false;
-                        result.message = "waf_block";
-                        return result;
-                    }
-
-                    if (body.Contains("https://bixi.alicdn.com") && body.Contains("\"action\": \"deny\""))
-                    {
-                        result.channel = TkChannelEnum.tb;
-                        result.link_type = LinkTypeEnum.unknown;
-                        result.subCode = TkSubCodeEnum.ParseDeny;
-                        result.success = false;
-                        result.message = "waf_block";
-                        result.reason = "接口风控";
-                        return result;
-                    }
-                    throw new Exception(body);
-                }
-
-                bool success = root.Read<bool>("success", false);
-                string message = root.Read("message", string.Empty);
-                string info_message = root.PathRead("info.message", string.Empty);
-                if (!string.IsNullOrEmpty(info_message)) message = info_message;
-
-                string taoToken = root.PathRead("data.taoToken", string.Empty);
-                string shortLinkurl = root.PathRead("data.shortLinkurl", string.Empty);
-
-                string couponLinkTaoToken = root.PathRead("data.couponLinkTaoToken", string.Empty);
-                string couponShortLinkUrl = root.PathRead("data.couponShortLinkUrl", string.Empty);
-                decimal couponAmount = root.PathRead<decimal>("data.couponAmount", 0);
-                string couponEffectiveEndTime = root.PathRead("data.couponEffectiveEndTime", string.Empty);
-                string couponEffectiveStartTime = root.PathRead("data.couponEffectiveStartTime", string.Empty);
-
-                string itemId = root.PathRead("data.itemId", string.Empty);
-                string mktId = root.PathRead("data.outputMktId", string.Empty);
-                string itemName = root.PathRead("data.itemName", string.Empty);
-                decimal promotionPrice = root.PathRead<decimal>("data.promotionPrice", 0);
-                string sellerNickName = root.PathRead("data.sellerNickName", string.Empty);
-                string shopTitle = root.PathRead("data.shopTitle", string.Empty);
-                string pic = root.PathRead("data.pic", string.Empty);
-                if (pic.StartsWith("//")) pic = "https:" + pic;
-
-                string qrCodeUrl = root.PathRead("data.qrCodeUrl", string.Empty);
-
-
-                if (response.ResponseMessage != null)
-                {
-
-                    switch (response.ResponseMessage.StatusCode)
-                    {
-                        case System.Net.HttpStatusCode.OK:
-                            {
-                                if (couponAmount > 0 && !string.IsNullOrEmpty(couponLinkTaoToken) && !string.IsNullOrEmpty(couponShortLinkUrl))
-                                {
-                                    taoToken = couponLinkTaoToken;
-                                    shortLinkurl = couponShortLinkUrl;
-                                }
-                                string shortLink = GetLink(taoToken);
-                                content = ReplaceUrls(content, shortLink);
-                            }
-                            break;
-                        case System.Net.HttpStatusCode.Found:
-                            string location = response.ResponseMessage.Headers.Location.OriginalString;
-                            if (!string.IsNullOrEmpty(location) && location.Contains("www.alimama.com/member/login.htm"))
-                            {
-                                success = false;
-                                message = "nologin";
-                            }
-                            break;
-                        default:
-                            {
-                                success = false;
-                                message = "other";
-                            }
-                            break;
-                    }
-                }
-                if (!success)
-                {
-                    shortLinkurl = GetLink(content);
-                    switch (message)
-                    {
-                        case "该商品已经下架或未加入淘宝客":
-                        case "该链接不支持转化,请更换链接尝试":
-                            result.subCode = TkSubCodeEnum.NoConvert;
-                            break;
-                        case "网络错误":
-                            result.subCode = TkSubCodeEnum.NetError;
-                            break;
-                        default:
-                            var _headers = response.ResponseMessage.Headers;
-                            var headers = "";
-                            foreach (var h in _headers)
-                            {
-                                foreach (var Value in h.Value)
-                                {
-                                    headers += $"{h.Key}: {Value}\n";
-                                }
-                            }
-                            _ = new LoggerLibrary("api_error", "fail")
-                                .Info(message, content)
-                                .Info(headers, body)
-                                .SaveAsync();
-                            break;
-                    }
-                }
-
-                shortLinkurl = GetLink(taoToken);
-                string deeplink_url = GetDeeplink(shortLinkurl);
-
-                result.channel = TkChannelEnum.tb;
-
-                if (success)
-                {
-                    result.subCode = TkSubCodeEnum.Success;
-                    switch (result.link_type)
-                    {
-                        case LinkTypeEnum.profile:
-                        case LinkTypeEnum.goods:
-                        case LinkTypeEnum.video:
-                        case LinkTypeEnum.live:
-                            break;
-                        default:
-                            result.link_type = LinkTypeEnum.goods;
-                            break;
-                    }
-                }
-
-                result.success = success;
-                result.message = message;
-                result.reason = string.Empty;
-
-                result.content = content;
-                result.couponAmount = couponAmount;
-                result.couponEffectiveEndTime = couponEffectiveEndTime;
-                result.couponEffectiveStartTime = couponEffectiveStartTime;
-                result.taoToken = taoToken;
-                if (!string.IsNullOrEmpty(shortLinkurl))
-                {
-                    result.shortLinkurl = shortLinkurl;
-                    result.deeplink_url = deeplink_url;
-                }
-                result.mktId = mktId;
-
-                result.itemId = itemId;
-                result.itemName = itemName;
-                result.promotionPrice = promotionPrice;
-                result.sellerNickName = sellerNickName;
-                result.shopTitle = shopTitle;
-                result.pic = pic;
-                result.qrCodeUrl = qrCodeUrl;
-            }
-            catch (Exception ex)
-            {
-                if (response.ResponseMessage?.StatusCode == System.Net.HttpStatusCode.OK)
-                {
-                    if (body.Contains("<p>抱歉!页面无法访问……</p>"))
-                    {
-                        throw new APIException("抱歉!页面无法访问……");
-                    }
-                }
-                throw;
-            }
-            return result;
-        }
-
-
-        private TkDataDTO alimamaParse(string content, TkDataDTO result)
-        {
-            string t = $"{DateTime.Now.Convert2UnixTimestamp(true)}";
-
-            Random random = new();
-            double randomNumber = random.NextDouble();
-            string randomString = randomNumber.ToString()[2..];
-
-            string cna = _cookies.GetContentPart("cna=", ";");
-            string firstFiveCharacters = cna[..Math.Min(5, cna.Length)];
-
-            var variableMap = new
-            {
-                url = content,
-                union_lens = $"b_pvid:a219t._portal_v2_tool_links_page_home_index_htm_{t}_{randomString}_{firstFiveCharacters}",
-                lensScene = "PUB",
-                spmB = "_portal_v2_tool_links_page_home_index_htm"
-            }.Convert2Json(true).UrlEncode();
-
-            variableMap = variableMap.Replace(" ", "%20");
-
-            string url = "https://pub.alimama.com/openapi/param2/1/gateway.unionpub/xt.entry.json?" +
-                $"t={t}&_tb_token_={_tb_token}&floorId={_floorId}&refpid={_refpid}&variableMap={variableMap}";
-
-            Stopwatch stopwatch = Stopwatch.StartNew();
-            stopwatch.Start();
-
-            var client = new WebClientUtility().SetContentType("application/json;charset=utf-8")
-                        .AddHeaders("X-Requested-With", "XMLHttpRequest")
-                        .AddHeaders("Cookie", _cookies);
-            client.Proxy = _proxy;
-            if (!string.IsNullOrEmpty(_user_agent)) client.UserAgent = _user_agent;
-#if DEBUG
-            client.Proxy = null;
-
-            url = "https://47.246.177.226/openapi/param2/1/gateway.unionpub/xt.entry.json?" +
-               $"t={t}&_tb_token_={_tb_token}&floorId={_floorId}&refpid={_refpid}&variableMap={variableMap}";
-            client.Headers["Host"] = "pub.alimama.com";
-#endif
-            client.Timeout = GetRequestTimeout2(result.elapsedTime2);
-            //var response = client.Request(url);
-            var response = client.Request(url);
-
-            stopwatch.Stop();
-            result.elapsedTime3 = (int)stopwatch.ElapsedMilliseconds;
-
-            var body = string.Empty;
-            try
-            {
-                if (!response.Successed)
-                {
-                    result.channel = TkChannelEnum.tb;
-                    result.accountId = _accountId;
-                    result.accountName = _accountName;
-                    result.link_type = LinkTypeEnum.unknown;
-                    result.success = false;
-                    result.message = "fail";
-
-                    if (response.ResponseException != null)
-                    {
-                        _ = new LoggerLibrary("api_error", "fail")
-                            .Info(response.ResponseException.Message, response.ResponseException.StackTrace)
-                            .SaveAsync();
-                        throw response.ResponseException;
-                    }
-                    return result;
-                }
-
-
-                body = response.Body();
-                //{"code":601,"info":{"ok":false,"message":"nologin"}}
-
-
-                JsonElement root = body.Convert2JsonElement();
-
-                bool success = root.Read<bool>("success", false);
-                string message = root.Read("message", string.Empty);
-                string info_message = root.PathRead("info.message", string.Empty);
-                if (!string.IsNullOrEmpty(info_message)) message = info_message;
-
-                string taoToken = root.PathRead("data.taoToken", string.Empty);
-                string shortLinkurl = root.PathRead("data.shortLinkurl", string.Empty);
-
-                string couponLinkTaoToken = root.PathRead("data.couponLinkTaoToken", string.Empty);
-                string couponShortLinkUrl = root.PathRead("data.couponShortLinkUrl", string.Empty);
-                decimal couponAmount = root.PathRead<decimal>("data.couponAmount", 0);
-                string couponEffectiveEndTime = root.PathRead("data.couponEffectiveEndTime", string.Empty);
-                string couponEffectiveStartTime = root.PathRead("data.couponEffectiveStartTime", string.Empty);
-
-                string itemId = root.PathRead("data.itemId", string.Empty);
-                string itemName = root.PathRead("data.itemName", string.Empty);
-                decimal promotionPrice = root.PathRead<decimal>("data.promotionPrice", 0);
-                string sellerNickName = root.PathRead("data.sellerNickName", string.Empty);
-                string shopTitle = root.PathRead("data.shopTitle", string.Empty);
-                string pic = root.PathRead("data.pic", string.Empty);
-                string qrCodeUrl = root.PathRead("data.qrCodeUrl", string.Empty);
-
-                if (response.ResponseMessage != null)
-                {
-
-                    switch (response.ResponseMessage.StatusCode)
-                    {
-                        case System.Net.HttpStatusCode.OK:
-                            {
-                                if (couponAmount > 0 && !string.IsNullOrEmpty(couponLinkTaoToken) && !string.IsNullOrEmpty(couponShortLinkUrl))
-                                {
-                                    taoToken = couponLinkTaoToken;
-                                    shortLinkurl = couponShortLinkUrl;
-                                }
-                                string shortLink = GetLink(taoToken);
-                                content = ReplaceUrls(content, shortLink);
-                            }
-                            break;
-                        case System.Net.HttpStatusCode.Found:
-                            string location = response.ResponseMessage.Headers.Location.OriginalString;
-                            if (!string.IsNullOrEmpty(location) && location.Contains("www.alimama.com/member/login.htm"))
-                            {
-                                success = false;
-                                message = "nologin";
-                            }
-                            break;
-                        default:
-                            {
-                                success = false;
-                                message = "other";
-                            }
-                            break;
-                    }
-                }
-                if (!success)
-                {
-                    shortLinkurl = GetLink(content);
-                    switch (message)
-                    {
-                        case "该商品已经下架或未加入淘宝客":
-                        case "该链接不支持转化,请更换链接尝试":
-                        case "网络错误":
-                            break;
-                        default:
-                            var _headers = response.ResponseMessage.Headers;
-                            var headers = "";
-                            foreach (var h in _headers)
-                            {
-                                foreach (var Value in h.Value)
-                                {
-                                    headers += $"{h.Key}: {Value}\n";
-                                }
-                            }
-                            _ = new LoggerLibrary("api_error", "fail")
-                                .Info(message, content)
-                                .Info(headers, body)
-                                .SaveAsync();
-                            break;
-                    }
-                }
-                if (success)
-                {
-                    if (string.IsNullOrEmpty(itemName))
-                    {
-                        itemName = GetTitle(taoToken);
-                    }
-                }
-                else
-                {
-                    itemName = GetTitle(content);
-                }
-
-                string deeplink_url = GetDeeplink(shortLinkurl);
-
-                result.channel = TkChannelEnum.tb;
-                result.accountId = _accountId;
-                result.accountName = _accountName;
-
-                if (success)
-                {
-                    switch (result.link_type)
-                    {
-                        case LinkTypeEnum.profile:
-                        case LinkTypeEnum.goods:
-                        case LinkTypeEnum.video:
-                        case LinkTypeEnum.live:
-                            break;
-                        default:
-                            result.link_type = LinkTypeEnum.goods;
-                            break;
-                    }
-                }
-
-                result.success = success;
-                result.message = message;
-                result.content = content;
-                result.couponAmount = couponAmount;
-
-                result.couponEffectiveEndTime = couponEffectiveEndTime;
-                result.couponEffectiveStartTime = couponEffectiveStartTime;
-
-                result.taoToken = taoToken;
-                result.shortLinkurl = shortLinkurl;
-                result.deeplink_url = deeplink_url;
-
-                result.itemId = itemId;
-                result.itemName = itemName;
-                result.promotionPrice = promotionPrice;
-                result.sellerNickName = sellerNickName;
-                result.shopTitle = shopTitle;
-                result.pic = pic;
-                result.qrCodeUrl = qrCodeUrl;
-            }
-            catch
-            {
-                if (response.ResponseMessage?.StatusCode == System.Net.HttpStatusCode.OK)
-                {
-                    if (body.Contains("<p>抱歉!页面无法访问……</p>"))
-                    {
-                        throw new APIException("抱歉!页面无法访问……");
-                    }
-                }
-                throw;
-            }
-            return result;
-        }
-
 
     }
 }

+ 327 - 0
molilian.core/Plus/Alimama/parse_endpoint/moose_share.cs

@@ -0,0 +1,327 @@
+using dodohold.core;
+using System.Diagnostics;
+using System.Text.Json;
+
+
+namespace molilian.core
+{
+    public partial class AlimamaPlus
+    {
+        public async Task<TkDataDTO> endpoint_moose_share(string content, TkDataDTO result, CancellationToken cancellationToken = default)
+        {
+            string t = $"{DateTime.Now.Convert2UnixTimestamp(true)}";
+
+            Random random = new();
+            double randomNumber = random.NextDouble();
+            string randomString = randomNumber.ToString()[2..];
+
+            string cna = _cookies.GetContentPart("cna=", ";");
+            string firstFiveCharacters = cna[..Math.Min(5, cna.Length)];
+
+            //var variableMap = new
+            //{
+            //    url = content,
+            //    union_lens = $"b_pvid:a219t._portal_v2_tool_links_page_home_index_htm_{t}_{randomString}_{firstFiveCharacters}",
+            //    lensScene = "PUB",
+            //    spmB = "_portal_v2_tool_links_page_home_index_htm"
+            //}.Convert2Json(true).UrlEncode();
+
+            string item_id = content.GetContentPart("item.htm?id=", "");
+            var bizParam = new
+            {
+                itemId = item_id,
+                pid = _account.refpid,
+                creativeType = "creativeAll"
+            }.Convert2Json(true).UrlEncode();
+
+
+            string url = "https://pub.alimama.com/openapi/param2/1/gateway.unionpub/union.moose.share.entry";
+            string post = $"bizType=share&bizParam={bizParam}";
+
+            Stopwatch stopwatch = Stopwatch.StartNew();
+            stopwatch.Start();
+            var client = new WebClientUtility().SetContentType("application/x-www-form-urlencoded")
+                .AddHeaders("Origin", "https://pub.alimama.com")
+                .AddHeaders("X-Requested-With", "XMLHttpRequest")
+                .AddHeaders("Cookie", _cookies);
+            client.Proxy = _proxy;
+#if DEBUG
+            client.Proxy = null;
+#endif
+            if (!string.IsNullOrEmpty(_user_agent)) client.UserAgent = _user_agent;
+            client.Timeout = GetRequestTimeout2(result.elapsedTime2);
+            //var response = client.Request(url);
+
+#if DEBUG
+            var response = await client.Post(post).RequestAsync(url, "POST");
+#else
+            var response = await client.Post(post).RequestAsync(url, "POST", cancellationToken);
+#endif
+
+            stopwatch.Stop();
+            result.elapsedTime3 = (int)stopwatch.ElapsedMilliseconds;
+
+
+
+            var body = string.Empty;
+            try
+            {
+                if (!response.Successed)
+                {
+                    result.channel = TkChannelEnum.tb;
+                    result.link_type = LinkTypeEnum.unknown;
+                    result.success = false;
+                    result.message = "fail";
+
+                    if (response.ResponseException != null)
+                    {
+                        _ = new LoggerLibrary("api_error", "fail")
+                            .Info(response.ResponseException.Message, response.ResponseException.StackTrace)
+                            .SaveAsync();
+                        throw response.ResponseException;
+                    }
+
+                    return result;
+                }
+                if (response.ResponseMessage.StatusCode == System.Net.HttpStatusCode.Found)
+                {
+                    _ = new LoggerLibrary("api_error", "fail")
+                        .Info($"302:{response.ResponseMessage.Headers.Location}")
+                        .SaveAsync();
+                    throw new Exception("302 Found, was canceled");
+                }
+                JsonElement root;
+                try
+                {
+                    body = response.Body();
+                    //{"success":false,"message":"result empty"}
+                    root = body.Convert2JsonElement();
+                }
+                catch (Exception ex)
+                {
+                    _ = new LoggerLibrary("api_error", "fail")
+                        .Info(ex.Message, ex.StackTrace)
+                        .SaveAsync();
+
+                    if (body.Contains("https://g.alicdn.com/sd/punish/waf_block.html"))
+                    {
+                        await TkEndpointManager.SuspendAsync(_accountId, "moose_share", TkEndpointManager.SuspendReason.Passive);
+
+                        result.channel = TkChannelEnum.tb;
+                        result.link_type = LinkTypeEnum.unknown;
+                        result.subCode = TkSubCodeEnum.ParseDeny;
+                        result.success = false;
+                        result.message = "waf_block";
+                        result.reason = "接口风控";
+                        return result;
+                    }
+                    throw new Exception(body);
+                }
+
+
+
+                bool success = root.Read<bool>("success", false);
+                string err_url = root.Read("url", string.Empty);
+                if (!success)
+                {
+                    //{"rgv587_flag": "sm", "url": "https://bixi.alicdn.com/punish/punish:resource:template:baba:default_35969158.html?qrcode=oPTbncSdl-B_dxCchZVrhw|aBW9wQ|MmZvng_0|BrNc-A&uuid=a0f4db9dc49d97e07f77109c85956b87&action=deny&origin=https%3A%2F%2Fpub.alimama.com%2Fopenapi%2Fparam2%2F1%2Fgateway.unionpub%2Funion.moose.share.entry&pureDenyWait="}
+                    if (err_url.Contains("bixi.alicdn.com"))
+                    {
+                        await TkEndpointManager.SuspendAsync(_accountId, "moose_share", TkEndpointManager.SuspendReason.Passive);
+
+                        result.channel = TkChannelEnum.tb;
+                        result.link_type = LinkTypeEnum.unknown;
+                        result.subCode = TkSubCodeEnum.ParseDeny;
+                        result.success = false;
+                        result.message = "waf_block";
+                        result.reason = "接口风控";
+                        return result;
+                    }
+                }
+
+                string message = root.Read("message", string.Empty);
+                string info_message = root.PathRead("msgInfo", string.Empty);
+                if (!string.IsNullOrEmpty(info_message)) message = info_message;
+
+                switch (message)
+                {
+                    case "detail result is empty itemId={}":
+                    case "itemId params error":
+                        message = "result empty";
+                        result.subCode = TkSubCodeEnum.NoConvert;
+                        break;
+
+                }
+
+                JsonElement item = default;
+                JsonElement creativityCopy = default;
+
+                if (success)
+                {
+                    var data = root.ElementRead("model");
+                    var resultList = data.ElementRead("resultList");
+                    if (resultList.ValueKind != JsonValueKind.Array)
+                    {
+                        _ = new LoggerLibrary("api_error", "fail")
+                            .Info("resultList Undefined", body)
+                            .SaveAsync();
+                        throw new Exception("resultList Undefined");
+                    }
+                    item = resultList[0];
+
+                    var creativityCopyArr = item.ElementRead("creativityCopy");
+                    creativityCopy = creativityCopyArr[0];
+
+                }
+
+
+                string cpsShortToken = creativityCopy.Read("cpsShortToken", string.Empty);
+                string cpsShortLinkUrl = creativityCopy.Read("cpsShortLinkUrl", string.Empty);
+                string itemName = creativityCopy.Read("itemName", string.Empty);
+                string taoToken = $"{cpsShortToken} {cpsShortLinkUrl} {itemName}";
+
+                string shortLinkurl = creativityCopy.Read("cpsShortLinkUrl", string.Empty);
+
+                string couponLinkTaoToken = item.Read("couponLinkTaoToken", string.Empty);
+                string couponShortLinkUrl = item.Read("couponShortLinkUrl", string.Empty);
+                decimal couponAmount = item.Read<decimal>("couponAmount", 0);
+                string couponEffectiveEndTime = item.Read("couponEffectiveEndTime", string.Empty);
+                string couponEffectiveStartTime = item.Read("couponEffectiveStartTime", string.Empty);
+
+                string itemId = item.Read("itemId", string.Empty);
+                itemId = item_id;//用数字id覆盖
+                string mktId = item.Read("outputMktId", string.Empty);
+                decimal promotionPrice = item.Read<decimal>("promotionPrice", 0);
+                string sellerNickName = item.Read("sellerNickName", string.Empty);
+                string shopTitle = item.Read("shopTitle", string.Empty);
+                string pic = item.Read("pic", string.Empty);
+                if (pic.StartsWith("//")) pic = "https:" + pic;
+
+                string qrCodeUrl = item.Read("qrCodeUrl", string.Empty);
+
+
+                if (response.ResponseMessage != null)
+                {
+
+                    switch (response.ResponseMessage.StatusCode)
+                    {
+                        case System.Net.HttpStatusCode.OK:
+                            {
+                                if (couponAmount > 0 && !string.IsNullOrEmpty(couponLinkTaoToken) && !string.IsNullOrEmpty(couponShortLinkUrl))
+                                {
+                                    taoToken = couponLinkTaoToken;
+                                    shortLinkurl = couponShortLinkUrl;
+                                }
+                                string shortLink = GetLink(taoToken);
+                                content = ReplaceUrls(content, shortLink);
+                            }
+                            break;
+                        case System.Net.HttpStatusCode.Found:
+                            string location = response.ResponseMessage.Headers.Location.OriginalString;
+                            if (!string.IsNullOrEmpty(location) && location.Contains("www.alimama.com/member/login.htm"))
+                            {
+                                success = false;
+                                message = "nologin";
+                            }
+                            break;
+                        default:
+                            {
+                                success = false;
+                                message = "other";
+                            }
+                            break;
+                    }
+                }
+                if (!success)
+                {
+                    shortLinkurl = GetLink(content);
+                    switch (message)
+                    {
+                        case "该商品已经下架或未加入淘宝客":
+                        case "该链接不支持转化,请更换链接尝试":
+                        case "result empty":
+                            result.subCode = TkSubCodeEnum.NoConvert;
+                            break;
+                        case "网络错误":
+                            result.subCode = TkSubCodeEnum.NetError;
+                            break;
+                        default:
+                            var _headers = response.ResponseMessage.Headers;
+                            var headers = "";
+                            foreach (var h in _headers)
+                            {
+                                foreach (var Value in h.Value)
+                                {
+                                    headers += $"{h.Key}: {Value}\n";
+                                }
+                            }
+                            _ = new LoggerLibrary("api_error", "fail")
+                                .Info(message, content)
+                                .Info(headers, body)
+                                .SaveAsync();
+                            break;
+                    }
+                }
+
+                shortLinkurl = GetLink(taoToken);
+                string deeplink_url = GetDeeplink(shortLinkurl);
+
+                result.channel = TkChannelEnum.tb;
+
+                if (success)
+                {
+                    result.subCode = TkSubCodeEnum.Success;
+                    switch (result.link_type)
+                    {
+                        case LinkTypeEnum.profile:
+                        case LinkTypeEnum.goods:
+                        case LinkTypeEnum.video:
+                        case LinkTypeEnum.live:
+                            break;
+                        default:
+                            result.link_type = LinkTypeEnum.goods;
+                            break;
+                    }
+                }
+
+                result.success = success;
+                result.message = message;
+                result.reason = string.Empty;
+
+                result.content = content;
+                result.couponAmount = couponAmount;
+                result.couponEffectiveEndTime = couponEffectiveEndTime;
+                result.couponEffectiveStartTime = couponEffectiveStartTime;
+                result.taoToken = taoToken;
+                if (!string.IsNullOrEmpty(shortLinkurl))
+                {
+                    result.shortLinkurl = shortLinkurl;
+                    result.deeplink_url = deeplink_url;
+                }
+                result.mktId = mktId;
+
+                result.itemId = itemId;
+                result.itemName = itemName;
+                result.promotionPrice = promotionPrice;
+                result.sellerNickName = sellerNickName;
+                result.shopTitle = shopTitle;
+                result.pic = pic;
+                result.qrCodeUrl = qrCodeUrl;
+            }
+            catch (Exception ex)
+            {
+                if (response.ResponseMessage?.StatusCode == System.Net.HttpStatusCode.OK)
+                {
+                    if (body.Contains("<p>抱歉!页面无法访问……</p>"))
+                    {
+                        throw new APIException("抱歉!页面无法访问……");
+                    }
+                }
+                throw;
+            }
+            return result;
+        }
+
+    }
+
+}

+ 567 - 0
molilian.core/Plus/Alimama/parse_endpoint/promo_goods.cs

@@ -0,0 +1,567 @@
+using dodohold.core;
+using StackExchange.Redis;
+using System.Diagnostics;
+using System.Text.Json;
+
+
+namespace molilian.core
+{
+    public partial class AlimamaPlus
+    {
+
+        public async Task<TkDataDTO> endpoint_promo_goods(string content, TkDataDTO result, CancellationToken cancellationToken = default)
+        {
+            string t = $"{DateTime.Now.Convert2UnixTimestamp(true)}";
+
+            Random random = new();
+            double randomNumber = random.NextDouble();
+            string randomString = randomNumber.ToString()[2..];
+
+            string cna = _cookies.GetContentPart("cna=", ";");
+            string firstFiveCharacters = cna[..Math.Min(5, cna.Length)];
+
+            //var variableMap = new
+            //{
+            //    url = content,
+            //    union_lens = $"b_pvid:a219t._portal_v2_tool_links_page_home_index_htm_{t}_{randomString}_{firstFiveCharacters}",
+            //    lensScene = "PUB",
+            //    spmB = "_portal_v2_tool_links_page_home_index_htm"
+            //}.Convert2Json(true).UrlEncode();
+
+            string item_id = content.GetContentPart("item.htm?id=", "");
+            var variableMap = new
+            {
+                itemId = item_id
+            }.Convert2Json(true).UrlEncode();
+
+
+            variableMap = variableMap.Replace(" ", "%20");
+            _floorId = "102359";
+            string url = "https://pub.alimama.com/openapi/param2/1/gateway.unionpub/xt.entry.json?" +
+                $"t={t}&_tb_token_={_tb_token}&floorId={_floorId}&refpid={_refpid}&variableMap={variableMap}";
+
+
+
+            Stopwatch stopwatch = Stopwatch.StartNew();
+            stopwatch.Start();
+            var client = new WebClientUtility().SetContentType("application/json;charset=utf-8")
+                        .AddHeaders("X-Requested-With", "XMLHttpRequest")
+                        .AddHeaders("Cookie", _cookies);
+            client.Proxy = _proxy;
+#if DEBUG
+            client.Proxy = null;
+#endif
+            if (!string.IsNullOrEmpty(_user_agent)) client.UserAgent = _user_agent;
+            client.Timeout = GetRequestTimeout2(result.elapsedTime2);
+            //var response = client.Request(url);
+
+#if DEBUG
+            var response = await client.RequestAsync(url, "GET");
+#else
+            var response = await client.RequestAsync(url, "GET", cancellationToken);
+#endif
+
+            stopwatch.Stop();
+            result.elapsedTime3 = (int)stopwatch.ElapsedMilliseconds;
+
+
+
+            var body = string.Empty;
+            try
+            {
+                if (!response.Successed)
+                {
+                    result.channel = TkChannelEnum.tb;
+                    result.link_type = LinkTypeEnum.unknown;
+                    result.success = false;
+                    result.message = "fail";
+
+                    if (response.ResponseException != null)
+                    {
+                        _ = new LoggerLibrary("api_error", "fail")
+                            .Info(response.ResponseException.Message, response.ResponseException.StackTrace)
+                            .SaveAsync();
+                        throw response.ResponseException;
+                    }
+
+                    return result;
+                }
+                if (response.ResponseMessage.StatusCode == System.Net.HttpStatusCode.Found)
+                {
+                    _ = new LoggerLibrary("api_error", "fail")
+                        .Info($"302:{response.ResponseMessage.Headers.Location}")
+                        .SaveAsync();
+                    throw new Exception("302 Found, was canceled");
+                }
+                JsonElement root;
+                try
+                {
+                    body = response.Body();
+                    //{"success":false,"message":"result empty"}
+
+                    if (body.Contains("bixi.alicdn.com"))
+                    {
+                        await TkEndpointManager.SuspendAsync(_accountId, "promo_goods", TkEndpointManager.SuspendReason.Passive);
+                        result.channel = TkChannelEnum.tb;
+                        result.link_type = LinkTypeEnum.unknown;
+                        result.subCode = TkSubCodeEnum.ParseDeny;
+                        result.success = false;
+                        result.message = "waf_block";
+                        result.reason = "接口风控";
+                        return result;
+                    }
+
+                    root = body.Convert2JsonElement();
+                }
+                catch (Exception ex)
+                {
+                    _ = new LoggerLibrary("api_error", "fail")
+                        .Info(ex.Message, ex.StackTrace)
+                        .SaveAsync();
+
+                    if (body.Contains("https://g.alicdn.com/sd/punish/waf_block.html"))
+                    {
+                        await TkEndpointManager.SuspendAsync(_accountId, "promo_goods", TkEndpointManager.SuspendReason.Passive);
+                        result.channel = TkChannelEnum.tb;
+                        result.link_type = LinkTypeEnum.unknown;
+                        result.success = false;
+                        result.message = "waf_block";
+                        result.reason = "接口风控";
+                        return result;
+                    }
+                    throw new Exception(body);
+                }
+
+
+                bool success = root.Read<bool>("success", false);
+                string message = root.Read("message", string.Empty);
+                string info_message = root.PathRead("info.message", string.Empty);
+                if (!string.IsNullOrEmpty(info_message)) message = info_message;
+
+                if ("result empty".Equals(message))
+                {
+                    result.subCode = TkSubCodeEnum.NoConvert;
+
+                }
+                JsonElement item = default;
+
+                if (success)
+                {
+                    var data = root.ElementRead("data");
+                    var resultList = data.ElementRead("resultList");
+                    if (resultList.ValueKind != JsonValueKind.Array)
+                    {
+                        _ = new LoggerLibrary("api_error", "fail")
+                            .Info("resultList Undefined", body)
+                            .SaveAsync();
+                        throw new Exception("resultList Undefined");
+                    }
+                    item = resultList[0];
+                }
+
+
+
+                string taoToken = item.Read("taoToken", string.Empty);
+                string shortLinkurl = item.Read("shortLinkurl", string.Empty);
+
+                string couponLinkTaoToken = item.Read("couponLinkTaoToken", string.Empty);
+                string couponShortLinkUrl = item.Read("couponShortLinkUrl", string.Empty);
+                decimal couponAmount = item.Read<decimal>("couponAmount", 0);
+                string couponEffectiveEndTime = item.Read("couponEffectiveEndTime", string.Empty);
+                string couponEffectiveStartTime = item.Read("couponEffectiveStartTime", string.Empty);
+
+                string itemId = item.Read("itemId", string.Empty);
+                string mktId = item.Read("outputMktId", string.Empty);
+                string itemName = item.Read("itemName", string.Empty);
+                decimal promotionPrice = item.Read<decimal>("promotionPrice", 0);
+                string sellerNickName = item.Read("sellerNickName", string.Empty);
+                string shopTitle = item.Read("shopTitle", string.Empty);
+                string pic = item.Read("data.pic", string.Empty);
+                if (pic.StartsWith("//")) pic = "https:" + pic;
+
+                string qrCodeUrl = item.Read("qrCodeUrl", string.Empty);
+
+
+                if (response.ResponseMessage != null)
+                {
+
+                    switch (response.ResponseMessage.StatusCode)
+                    {
+                        case System.Net.HttpStatusCode.OK:
+                            {
+                                if (couponAmount > 0 && !string.IsNullOrEmpty(couponLinkTaoToken) && !string.IsNullOrEmpty(couponShortLinkUrl))
+                                {
+                                    taoToken = couponLinkTaoToken;
+                                    shortLinkurl = couponShortLinkUrl;
+                                }
+                                string shortLink = GetLink(taoToken);
+                                content = ReplaceUrls(content, shortLink);
+                            }
+                            break;
+                        case System.Net.HttpStatusCode.Found:
+                            string location = response.ResponseMessage.Headers.Location.OriginalString;
+                            if (!string.IsNullOrEmpty(location) && location.Contains("www.alimama.com/member/login.htm"))
+                            {
+                                success = false;
+                                message = "nologin";
+                            }
+                            break;
+                        default:
+                            {
+                                success = false;
+                                message = "other";
+                            }
+                            break;
+                    }
+                }
+                if (!success)
+                {
+                    shortLinkurl = GetLink(content);
+                    switch (message)
+                    {
+                        case "该商品已经下架或未加入淘宝客":
+                        case "该链接不支持转化,请更换链接尝试":
+                        case "result empty":
+                            result.subCode = TkSubCodeEnum.NoConvert;
+                            break;
+                        case "网络错误":
+                            result.subCode = TkSubCodeEnum.NetError;
+                            break;
+                        default:
+                            var _headers = response.ResponseMessage.Headers;
+                            var headers = "";
+                            foreach (var h in _headers)
+                            {
+                                foreach (var Value in h.Value)
+                                {
+                                    headers += $"{h.Key}: {Value}\n";
+                                }
+                            }
+                            _ = new LoggerLibrary("api_error", "fail")
+                                .Info(message, content)
+                                .Info(headers, body)
+                                .SaveAsync();
+                            break;
+                    }
+                }
+
+                shortLinkurl = GetLink(taoToken);
+                string deeplink_url = GetDeeplink(shortLinkurl);
+
+                result.channel = TkChannelEnum.tb;
+
+                if (success)
+                {
+                    result.subCode = TkSubCodeEnum.Success;
+                    switch (result.link_type)
+                    {
+                        case LinkTypeEnum.profile:
+                        case LinkTypeEnum.goods:
+                        case LinkTypeEnum.video:
+                        case LinkTypeEnum.live:
+                            break;
+                        default:
+                            result.link_type = LinkTypeEnum.goods;
+                            break;
+                    }
+                }
+
+                result.success = success;
+                result.message = message;
+                result.reason = string.Empty;
+
+                result.content = content;
+                result.couponAmount = couponAmount;
+                result.couponEffectiveEndTime = couponEffectiveEndTime;
+                result.couponEffectiveStartTime = couponEffectiveStartTime;
+                result.taoToken = taoToken;
+                if (!string.IsNullOrEmpty(shortLinkurl))
+                {
+                    result.shortLinkurl = shortLinkurl;
+                    result.deeplink_url = deeplink_url;
+                }
+                result.mktId = mktId;
+
+                result.itemId = itemId;
+                result.itemName = itemName;
+                result.promotionPrice = promotionPrice;
+                result.sellerNickName = sellerNickName;
+                result.shopTitle = shopTitle;
+                result.pic = pic;
+                result.qrCodeUrl = qrCodeUrl;
+            }
+            catch (Exception ex)
+            {
+                if (response.ResponseMessage?.StatusCode == System.Net.HttpStatusCode.OK)
+                {
+                    if (body.Contains("<p>抱歉!页面无法访问……</p>"))
+                    {
+                        throw new APIException("抱歉!页面无法访问……");
+                    }
+                }
+                throw;
+            }
+            return result;
+        }
+
+        public async Task<TkDataDTO> alimamaParseAsync(string content, TkDataDTO result, CancellationToken cancellationToken = default)
+        {
+            string t = $"{DateTime.Now.Convert2UnixTimestamp(true)}";
+
+            Random random = new();
+            double randomNumber = random.NextDouble();
+            string randomString = randomNumber.ToString()[2..];
+
+            string cna = _cookies.GetContentPart("cna=", ";");
+            string firstFiveCharacters = cna[..Math.Min(5, cna.Length)];
+
+            var variableMap = new
+            {
+                url = content,
+                union_lens = $"b_pvid:a219t._portal_v2_tool_links_page_home_index_htm_{t}_{randomString}_{firstFiveCharacters}",
+                lensScene = "PUB",
+                spmB = "_portal_v2_tool_links_page_home_index_htm"
+            }.Convert2Json(true).UrlEncode();
+
+            variableMap = variableMap.Replace(" ", "%20");
+            string url = "https://pub.alimama.com/openapi/param2/1/gateway.unionpub/xt.entry.json?" +
+                $"t={t}&_tb_token_={_tb_token}&floorId={_floorId}&refpid={_refpid}&variableMap={variableMap}";
+
+
+
+            Stopwatch stopwatch = Stopwatch.StartNew();
+            stopwatch.Start();
+            var client = new WebClientUtility().SetContentType("application/json;charset=utf-8")
+                        .AddHeaders("X-Requested-With", "XMLHttpRequest")
+                        .AddHeaders("Cookie", _cookies);
+            client.Proxy = _proxy;
+#if DEBUG
+            client.Proxy = null;
+#endif
+            if (!string.IsNullOrEmpty(_user_agent)) client.UserAgent = _user_agent;
+            client.Timeout = GetRequestTimeout2(result.elapsedTime2);
+            //var response = client.Request(url);
+
+#if DEBUG
+            var response = await client.RequestAsync(url, "GET");
+#else
+            var response = await client.RequestAsync(url, "GET", cancellationToken);
+#endif
+
+            stopwatch.Stop();
+            result.elapsedTime3 = (int)stopwatch.ElapsedMilliseconds;
+
+
+
+            var body = string.Empty;
+            try
+            {
+                if (!response.Successed)
+                {
+                    result.channel = TkChannelEnum.tb;
+                    result.link_type = LinkTypeEnum.unknown;
+                    result.success = false;
+                    result.message = "fail";
+
+                    if (response.ResponseException != null)
+                    {
+                        _ = new LoggerLibrary("api_error", "fail")
+                            .Info(response.ResponseException.Message, response.ResponseException.StackTrace)
+                            .SaveAsync();
+                        throw response.ResponseException;
+                    }
+
+                    return result;
+                }
+                if (response.ResponseMessage.StatusCode == System.Net.HttpStatusCode.Found)
+                {
+                    _ = new LoggerLibrary("api_error", "fail")
+                        .Info($"302:{response.ResponseMessage.Headers.Location}")
+                        .SaveAsync();
+                    throw new Exception("302 Found, was canceled");
+                }
+                JsonElement root;
+                try
+                {
+                    body = response.Body();
+                    root = body.Convert2JsonElement();
+                }
+                catch (Exception ex)
+                {
+                    _ = new LoggerLibrary("api_error", "fail")
+                        .Info(ex.Message, ex.StackTrace)
+                        .SaveAsync();
+
+                    if (body.Contains("https://g.alicdn.com/sd/punish/waf_block.html"))
+                    {
+
+                        result.channel = TkChannelEnum.tb;
+                        result.link_type = LinkTypeEnum.unknown;
+                        result.success = false;
+                        result.message = "waf_block";
+                        return result;
+                    }
+
+                    if (body.Contains("https://bixi.alicdn.com") && body.Contains("\"action\": \"deny\""))
+                    {
+                        result.channel = TkChannelEnum.tb;
+                        result.link_type = LinkTypeEnum.unknown;
+                        result.subCode = TkSubCodeEnum.ParseDeny;
+                        result.success = false;
+                        result.message = "waf_block";
+                        result.reason = "接口风控";
+                        return result;
+                    }
+                    throw new Exception(body);
+                }
+
+                bool success = root.Read<bool>("success", false);
+                string message = root.Read("message", string.Empty);
+                string info_message = root.PathRead("info.message", string.Empty);
+                if (!string.IsNullOrEmpty(info_message)) message = info_message;
+
+                string taoToken = root.PathRead("data.taoToken", string.Empty);
+                string shortLinkurl = root.PathRead("data.shortLinkurl", string.Empty);
+
+                string couponLinkTaoToken = root.PathRead("data.couponLinkTaoToken", string.Empty);
+                string couponShortLinkUrl = root.PathRead("data.couponShortLinkUrl", string.Empty);
+                decimal couponAmount = root.PathRead<decimal>("data.couponAmount", 0);
+                string couponEffectiveEndTime = root.PathRead("data.couponEffectiveEndTime", string.Empty);
+                string couponEffectiveStartTime = root.PathRead("data.couponEffectiveStartTime", string.Empty);
+
+                string itemId = root.PathRead("data.itemId", string.Empty);
+                string mktId = root.PathRead("data.outputMktId", string.Empty);
+                string itemName = root.PathRead("data.itemName", string.Empty);
+                decimal promotionPrice = root.PathRead<decimal>("data.promotionPrice", 0);
+                string sellerNickName = root.PathRead("data.sellerNickName", string.Empty);
+                string shopTitle = root.PathRead("data.shopTitle", string.Empty);
+                string pic = root.PathRead("data.pic", string.Empty);
+                if (pic.StartsWith("//")) pic = "https:" + pic;
+
+                string qrCodeUrl = root.PathRead("data.qrCodeUrl", string.Empty);
+
+
+                if (response.ResponseMessage != null)
+                {
+
+                    switch (response.ResponseMessage.StatusCode)
+                    {
+                        case System.Net.HttpStatusCode.OK:
+                            {
+                                if (couponAmount > 0 && !string.IsNullOrEmpty(couponLinkTaoToken) && !string.IsNullOrEmpty(couponShortLinkUrl))
+                                {
+                                    taoToken = couponLinkTaoToken;
+                                    shortLinkurl = couponShortLinkUrl;
+                                }
+                                string shortLink = GetLink(taoToken);
+                                content = ReplaceUrls(content, shortLink);
+                            }
+                            break;
+                        case System.Net.HttpStatusCode.Found:
+                            string location = response.ResponseMessage.Headers.Location.OriginalString;
+                            if (!string.IsNullOrEmpty(location) && location.Contains("www.alimama.com/member/login.htm"))
+                            {
+                                success = false;
+                                message = "nologin";
+                            }
+                            break;
+                        default:
+                            {
+                                success = false;
+                                message = "other";
+                            }
+                            break;
+                    }
+                }
+                if (!success)
+                {
+                    shortLinkurl = GetLink(content);
+                    switch (message)
+                    {
+                        case "该商品已经下架或未加入淘宝客":
+                        case "该链接不支持转化,请更换链接尝试":
+                            result.subCode = TkSubCodeEnum.NoConvert;
+                            break;
+                        case "网络错误":
+                            result.subCode = TkSubCodeEnum.NetError;
+                            break;
+                        default:
+                            var _headers = response.ResponseMessage.Headers;
+                            var headers = "";
+                            foreach (var h in _headers)
+                            {
+                                foreach (var Value in h.Value)
+                                {
+                                    headers += $"{h.Key}: {Value}\n";
+                                }
+                            }
+                            _ = new LoggerLibrary("api_error", "fail")
+                                .Info(message, content)
+                                .Info(headers, body)
+                                .SaveAsync();
+                            break;
+                    }
+                }
+
+                shortLinkurl = GetLink(taoToken);
+                string deeplink_url = GetDeeplink(shortLinkurl);
+
+                result.channel = TkChannelEnum.tb;
+
+                if (success)
+                {
+                    result.subCode = TkSubCodeEnum.Success;
+                    switch (result.link_type)
+                    {
+                        case LinkTypeEnum.profile:
+                        case LinkTypeEnum.goods:
+                        case LinkTypeEnum.video:
+                        case LinkTypeEnum.live:
+                            break;
+                        default:
+                            result.link_type = LinkTypeEnum.goods;
+                            break;
+                    }
+                }
+
+                result.success = success;
+                result.message = message;
+                result.reason = string.Empty;
+
+                result.content = content;
+                result.couponAmount = couponAmount;
+                result.couponEffectiveEndTime = couponEffectiveEndTime;
+                result.couponEffectiveStartTime = couponEffectiveStartTime;
+                result.taoToken = taoToken;
+                if (!string.IsNullOrEmpty(shortLinkurl))
+                {
+                    result.shortLinkurl = shortLinkurl;
+                    result.deeplink_url = deeplink_url;
+                }
+                result.mktId = mktId;
+
+                result.itemId = itemId;
+                result.itemName = itemName;
+                result.promotionPrice = promotionPrice;
+                result.sellerNickName = sellerNickName;
+                result.shopTitle = shopTitle;
+                result.pic = pic;
+                result.qrCodeUrl = qrCodeUrl;
+            }
+            catch (Exception ex)
+            {
+                if (response.ResponseMessage?.StatusCode == System.Net.HttpStatusCode.OK)
+                {
+                    if (body.Contains("<p>抱歉!页面无法访问……</p>"))
+                    {
+                        throw new APIException("抱歉!页面无法访问……");
+                    }
+                }
+                throw;
+            }
+            return result;
+        }
+
+    }
+
+}

+ 286 - 0
molilian.core/Plus/Alimama/parse_endpoint/tool_links.cs

@@ -0,0 +1,286 @@
+using dodohold.core;
+using System.Diagnostics;
+using System.Text.Json;
+
+
+namespace molilian.core
+{
+    public partial class AlimamaPlus
+    {
+        public async Task<TkDataDTO> endpoint_tool_links(string content, TkDataDTO result, CancellationToken cancellationToken = default)
+        {
+            string t = $"{DateTime.Now.Convert2UnixTimestamp(true)}";
+
+            Random random = new();
+            double randomNumber = random.NextDouble();
+            string randomString = randomNumber.ToString()[2..];
+
+            string cna = _cookies.GetContentPart("cna=", ";");
+            string firstFiveCharacters = cna[..Math.Min(5, cna.Length)];
+
+            var variableMap = new
+            {
+                url = content,
+                union_lens = $"b_pvid:a219t._portal_v2_home_plus_index_htm_{t}_{randomString}_{firstFiveCharacters}_mmWuH",
+                lensScene = "PUB",
+                spmB = "_portal_v2_tool_links_page_home_index_htm"
+            }.Convert2Json(true).UrlEncode();
+
+            variableMap = variableMap.Replace(" ", "%20");
+            _floorId = "61446";
+            string url = "https://pub.alimama.com/openapi/param2/1/gateway.unionpub/xt.entry.json?" +
+                $"t={t}&_tb_token_={_tb_token}&floorId={_floorId}&refpid={_refpid}&variableMap={variableMap}";
+
+
+            Stopwatch stopwatch = Stopwatch.StartNew();
+            stopwatch.Start();
+            var client = new WebClientUtility().SetContentType("application/json;charset=utf-8")
+                        .AddHeaders("X-Requested-With", "XMLHttpRequest")
+                        .AddHeaders("Cookie", _cookies);
+            client.Proxy = _proxy;
+#if DEBUG
+            client.Proxy = null;
+#endif
+            if (!string.IsNullOrEmpty(_user_agent)) client.UserAgent = _user_agent;
+            client.Timeout = GetRequestTimeout2(result.elapsedTime2);
+            //var response = client.Request(url);
+
+#if DEBUG
+            var response = await client.RequestAsync(url, "GET");
+#else
+            var response = await client.RequestAsync(url, "GET", cancellationToken);
+#endif
+
+            stopwatch.Stop();
+            result.elapsedTime3 = (int)stopwatch.ElapsedMilliseconds;
+
+
+
+            var body = string.Empty;
+            try
+            {
+                if (!response.Successed)
+                {
+                    result.channel = TkChannelEnum.tb;
+                    result.link_type = LinkTypeEnum.unknown;
+                    result.success = false;
+                    result.message = "fail";
+
+                    if (response.ResponseException != null)
+                    {
+                        _ = new LoggerLibrary("api_error", "fail")
+                            .Info(response.ResponseException.Message, response.ResponseException.StackTrace)
+                            .SaveAsync();
+                        throw response.ResponseException;
+                    }
+
+                    return result;
+                }
+                if (response.ResponseMessage.StatusCode == System.Net.HttpStatusCode.Found)
+                {
+                    _ = new LoggerLibrary("api_error", "fail")
+                        .Info($"302:{response.ResponseMessage.Headers.Location}")
+                        .SaveAsync();
+                    throw new Exception("302 Found, was canceled");
+                }
+                JsonElement root;
+                try
+                {
+                    body = response.Body();
+                    root = body.Convert2JsonElement();
+                }
+                catch (Exception ex)
+                {
+                    _ = new LoggerLibrary("api_error", "fail")
+                        .Info(ex.Message, ex.StackTrace)
+                        .SaveAsync();
+
+                    if (body.Contains("https://g.alicdn.com/sd/punish/waf_block.html"))
+                    {
+                        await TkEndpointManager.SuspendAsync(_accountId, "tool_links", TkEndpointManager.SuspendReason.Passive);
+                        result.channel = TkChannelEnum.tb;
+                        result.link_type = LinkTypeEnum.unknown;
+                        result.success = false;
+                        result.message = "waf_block";
+                        return result;
+                    }
+
+                    if (body.Contains("https://bixi.alicdn.com") && body.Contains("\"action\": \"deny\""))
+                    {
+                        await TkEndpointManager.SuspendAsync(_accountId, "tool_links", TkEndpointManager.SuspendReason.Passive);
+                        result.channel = TkChannelEnum.tb;
+                        result.link_type = LinkTypeEnum.unknown;
+                        result.subCode = TkSubCodeEnum.ParseDeny;
+                        result.success = false;
+                        result.message = "waf_block";
+                        result.reason = "接口风控";
+                        return result;
+                    }
+                    throw new Exception(body);
+                }
+                
+
+                if (body.Contains("https://bixi.alicdn.com") && body.Contains("\"action\": \"deny\""))
+                {
+                    await TkEndpointManager.SuspendAsync(_accountId, "tool_links", TkEndpointManager.SuspendReason.Passive);
+                    result.channel = TkChannelEnum.tb;
+                    result.link_type = LinkTypeEnum.unknown;
+                    result.subCode = TkSubCodeEnum.ParseDeny;
+                    result.success = false;
+                    result.message = "waf_block";
+                    result.reason = "接口风控";
+                    return result;
+                }
+
+                bool success = root.Read<bool>("success", false);
+
+
+                string message = root.Read("message", string.Empty);
+                string info_message = root.PathRead("info.message", string.Empty);
+                if (!string.IsNullOrEmpty(info_message)) message = info_message;
+
+                string taoToken = root.PathRead("data.taoToken", string.Empty);
+                string shortLinkurl = root.PathRead("data.shortLinkurl", string.Empty);
+
+                string couponLinkTaoToken = root.PathRead("data.couponLinkTaoToken", string.Empty);
+                string couponShortLinkUrl = root.PathRead("data.couponShortLinkUrl", string.Empty);
+                decimal couponAmount = root.PathRead<decimal>("data.couponAmount", 0);
+                string couponEffectiveEndTime = root.PathRead("data.couponEffectiveEndTime", string.Empty);
+                string couponEffectiveStartTime = root.PathRead("data.couponEffectiveStartTime", string.Empty);
+
+                string itemId = root.PathRead("data.itemId", string.Empty);
+                string mktId = root.PathRead("data.outputMktId", string.Empty);
+                string itemName = root.PathRead("data.itemName", string.Empty);
+                decimal promotionPrice = root.PathRead<decimal>("data.promotionPrice", 0);
+                string sellerNickName = root.PathRead("data.sellerNickName", string.Empty);
+                string shopTitle = root.PathRead("data.shopTitle", string.Empty);
+                string pic = root.PathRead("data.pic", string.Empty);
+                if (pic.StartsWith("//")) pic = "https:" + pic;
+
+                string qrCodeUrl = root.PathRead("data.qrCodeUrl", string.Empty);
+
+
+                if (response.ResponseMessage != null)
+                {
+
+                    switch (response.ResponseMessage.StatusCode)
+                    {
+                        case System.Net.HttpStatusCode.OK:
+                            {
+                                if (couponAmount > 0 && !string.IsNullOrEmpty(couponLinkTaoToken) && !string.IsNullOrEmpty(couponShortLinkUrl))
+                                {
+                                    taoToken = couponLinkTaoToken;
+                                    shortLinkurl = couponShortLinkUrl;
+                                }
+                                string shortLink = GetLink(taoToken);
+                                content = ReplaceUrls(content, shortLink);
+                            }
+                            break;
+                        case System.Net.HttpStatusCode.Found:
+                            string location = response.ResponseMessage.Headers.Location.OriginalString;
+                            if (!string.IsNullOrEmpty(location) && location.Contains("www.alimama.com/member/login.htm"))
+                            {
+                                success = false;
+                                message = "nologin";
+                            }
+                            break;
+                        default:
+                            {
+                                success = false;
+                                message = "other";
+                            }
+                            break;
+                    }
+                }
+                if (!success)
+                {
+                    shortLinkurl = GetLink(content);
+                    switch (message)
+                    {
+                        case "该商品已经下架或未加入淘宝客":
+                        case "该链接不支持转化,请更换链接尝试":
+                            result.subCode = TkSubCodeEnum.NoConvert;
+                            break;
+                        case "网络错误":
+                            result.subCode = TkSubCodeEnum.NetError;
+                            break;
+                        default:
+                            var _headers = response.ResponseMessage.Headers;
+                            var headers = "";
+                            foreach (var h in _headers)
+                            {
+                                foreach (var Value in h.Value)
+                                {
+                                    headers += $"{h.Key}: {Value}\n";
+                                }
+                            }
+                            _ = new LoggerLibrary("api_error", "fail")
+                                .Info(message, content)
+                                .Info(headers, body)
+                                .SaveAsync();
+                            break;
+                    }
+                }
+
+                shortLinkurl = GetLink(taoToken);
+                string deeplink_url = GetDeeplink(shortLinkurl);
+
+                result.channel = TkChannelEnum.tb;
+
+                if (success)
+                {
+                    result.subCode = TkSubCodeEnum.Success;
+                    switch (result.link_type)
+                    {
+                        case LinkTypeEnum.profile:
+                        case LinkTypeEnum.goods:
+                        case LinkTypeEnum.video:
+                        case LinkTypeEnum.live:
+                            break;
+                        default:
+                            result.link_type = LinkTypeEnum.goods;
+                            break;
+                    }
+                }
+
+                result.success = success;
+                result.message = message;
+                result.reason = string.Empty;
+
+                result.content = content;
+                result.couponAmount = couponAmount;
+                result.couponEffectiveEndTime = couponEffectiveEndTime;
+                result.couponEffectiveStartTime = couponEffectiveStartTime;
+                result.taoToken = taoToken;
+                if (!string.IsNullOrEmpty(shortLinkurl))
+                {
+                    result.shortLinkurl = shortLinkurl;
+                    result.deeplink_url = deeplink_url;
+                }
+                result.mktId = mktId;
+
+                result.itemId = itemId;
+                result.itemName = itemName;
+                result.promotionPrice = promotionPrice;
+                result.sellerNickName = sellerNickName;
+                result.shopTitle = shopTitle;
+                result.pic = pic;
+                result.qrCodeUrl = qrCodeUrl;
+            }
+            catch (Exception ex)
+            {
+                if (response.ResponseMessage?.StatusCode == System.Net.HttpStatusCode.OK)
+                {
+                    if (body.Contains("<p>抱歉!页面无法访问……</p>"))
+                    {
+                        throw new APIException("抱歉!页面无法访问……");
+                    }
+                }
+                throw;
+            }
+            return result;
+        }
+
+    }
+
+}

+ 2 - 1
molilian.core/Plus/JDUnion/H5stHack.cs

@@ -21,7 +21,8 @@ namespace molilian.core
                 body = querystring,
                 pin = _account.pin,
                 ua = _account.user_agent,
-                h5st = _account.h5st,
+                //h5st = _account.h5st,
+                appId= "586ae",
             };
 
 

+ 32 - 24
molilian.core/Plus/VeapiPlus.cs

@@ -1,4 +1,6 @@
 using dodohold.core;
+using Org.BouncyCastle.Ocsp;
+using System.Text.RegularExpressions;
 
 
 namespace molilian.core
@@ -9,6 +11,7 @@ namespace molilian.core
         private string _accountName;
         private string _key;
         private string _sessionKey;
+        private string _refpid;
         public VeapiPlus(string accountName, string key, string sessionKey)
         {
             _accountName = accountName;
@@ -197,11 +200,10 @@ namespace molilian.core
 
         }
 
-        public async Task<TkDataDTO> UnionParseAsync(string content, TkDataDTO result, CancellationToken cancellationToken)
+        public async Task<TkDataDTO> UnionParseAsync(string content, string pid, TkDataDTO result, CancellationToken cancellationToken)
         {
             string para = content.UrlEncode();
-            string url = $"http://api.veapi.cn/tbk/hcapi?vekey={_key}&sessionkey={_sessionKey}&para={para}";
-
+            string url = $"http://api.veapi.cn/tbk/generalconvert?vekey={_key}&para={para}&pid={pid}";
             var response = await new WebClientUtility().RequestAsync(url, "GET", cancellationToken);
 
             var body = response.Body();
@@ -211,32 +213,38 @@ namespace molilian.core
             bool success = error == 0;
             string message = root.Read("msg", string.Empty);
 
-            string taoToken = root.PathRead("data.ios_tbk_pwd", string.Empty);
-            string num_iid = root.PathRead("data.num_iid", string.Empty);
-            decimal couponAmount = root.PathRead<decimal>("data.coupon_amount", 0);
-
-            string shortLinkurl = root.PathRead("data.coupon_short_url", string.Empty);
+            var data = root.ElementRead("data");
+            result.channel = TkChannelEnum.tb;
+            result.accountName = _accountName;
 
-            string shortLink = AlimamaPlus.GetLink(taoToken);
-            content = AlimamaPlus.ReplaceUrls(content, shortLink);
+            result.success = success;
+            result.message = message;
 
-            if (!success)
+            if (success)
             {
-                shortLinkurl = AlimamaPlus.GetLink(content);
-            }
-            string deeplink_url = AlimamaPlus.GetDeeplink(shortLinkurl);
+                result.content = content;
+                result.message = string.Empty;
+                result.commercial = true;
 
+                result.couponAmount = data.Read<decimal>("coupon_amount", 0);
+                result.mktId = data.Read<string>("isv_mktid", string.Empty);
+
+                result.itemId = data.Read<string>("item_id", string.Empty);
+                result.itemId = content.GetContentPart("item.htm?id=", "");
+                // veapi 缺少商品pic 和 promotionPrice
+
+                result.taoToken = data.Read<string>("cps_full_tpwd", string.Empty);
+                string coupon_full_tpwd = data.Read<string>("coupon_full_tpwd", string.Empty);
+                if (!string.IsNullOrEmpty(coupon_full_tpwd)) result.taoToken = coupon_full_tpwd;
+
+                result.shortLinkurl = AlimamaPlus.GetLink(result.taoToken);
+                result.itemName = result.taoToken.GetContentPart(result.shortLinkurl, "").Trim();
+
+
+                result.deeplink_url = AlimamaPlus.GetDeeplink(result.shortLinkurl);
+                return result;
+            }
 
-            result.channel = TkChannelEnum.tb;
-            result.accountName = _accountName;
-            result.success = success;
-            result.message = message;
-            result.content = content;
-            result.couponAmount = couponAmount;
-            result.taoToken = taoToken;
-            result.shortLinkurl = shortLinkurl;
-            result.deeplink_url = deeplink_url;
-            result.num_iid = num_iid;
             return result;
 
         }

+ 1 - 6
molilian.core/Plus/ks/KsUnionPlus.cs

@@ -1,11 +1,6 @@
-using COSXML.Network;
-using dodohold.core;
-using Spire.Pdf.Graphics;
+using dodohold.core;
 using System.Diagnostics;
-using System.Net;
 using System.Text.RegularExpressions;
-using TencentCloud.Apm.V20210622.Models;
-using static dodohold.core.ZTOExpress.CreateOrderArgs;
 
 namespace molilian.core
 {

+ 1 - 1
molilian.core/molilian.core.csproj

@@ -20,7 +20,7 @@
     <PackageReference Include="AlibabaCloud.SDK.Vpc20160428" Version="6.10.4" />
     <PackageReference Include="AlibabaCloud.TeaConsole" Version="0.1.0" />
     <PackageReference Include="AlibabaCloud.TeaUtil" Version="0.1.19" />
-    <PackageReference Include="dodohold.core" Version="1.0.1.17" />
+    <PackageReference Include="dodohold.core" Version="1.0.1.27" />
     <PackageReference Include="FakeUserAgent" Version="1.0.2" />
     <PackageReference Include="IP2Region.Net" Version="2.0.2" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff