Procházet zdrojové kódy

feat(隐藏号): 隐藏号专用限制改为 config 配置,所有隐藏号共用

- tk_config 新增 hide_ignore_city、hide_limit_per_ip_24h、hide_limit_per_oaid_24h
- 同 IP/OAID 每天转成功次数按所有隐藏号合计,淘宝/拼多多分开计数,0 不限
- 转链开始占用名额,失败归还,成功同步到其他节点
- 拦截文案与全局限制一致(地区控制/IP控制/OAID控制)
- 替换原先写死的"每个隐藏号同 ip/oaid 每天一次"

需执行:
ALTER TABLE tk_config
  ADD COLUMN hide_ignore_city VARCHAR(1000) NOT NULL DEFAULT '' AFTER tk_limit_per_oaid_24h,
  ADD COLUMN hide_limit_per_ip_24h INT NOT NULL DEFAULT 0 AFTER hide_ignore_city,
  ADD COLUMN hide_limit_per_oaid_24h INT NOT NULL DEFAULT 0 AFTER hide_limit_per_ip_24h;
dodo hold před 1 týdnem
rodič
revize
b7468d1ac6

+ 86 - 30
molilian.core/Core/HiddenAccountCore.cs

@@ -1,4 +1,3 @@
-using CSRedis;
 using dodohold.core;
 
 namespace molilian.core
@@ -8,10 +7,7 @@ namespace molilian.core
     /// </summary>
     public static class HiddenAccountCore
     {
-        // 同一隐藏号当天同 ip / oaid 只转成功一次
-        private const int OnceDoneExpireSeconds = 86400;
-        // 转链进行中的占位,防止同一用户并发请求重复转成功
-        private const int OncePendingExpireSeconds = 120;
+        private const int LimitExpireSeconds = 86400;
 
         public static Task<bool> IsHiddenAsync(TkChannelEnum channel, int accountId)
         {
@@ -37,55 +33,89 @@ namespace molilian.core
         }
 
         /// <summary>
-        /// 占用隐藏号当天的 ip / oaid 名额。当天已转成功或正在转链时返回 false。
+        /// 隐藏号专用限制(config 配置,所有隐藏号共用,淘宝/拼多多分开计数):
+        /// 城市过滤 + 同 IP / OAID 每天转成功次数。通过时占用名额,转链结束后调用 CompleteLimitAsync。
         /// </summary>
-        public static bool TryReserveOnce(TkChannelEnum channel, int accountId, string ip, string oaid)
+        public static bool TryReserveLimit(TkChannelEnum channel, string ip, string oaid, out HiddenLimitTicket ticket, out string reason)
         {
-            var keys = BuildOnceKeys(channel, accountId, ip, oaid);
-            var reserved = new List<string>();
-            foreach (var key in keys)
+            ticket = new HiddenLimitTicket();
+            reason = string.Empty;
+            if (TestParseCore.InWhitelist(ip, oaid)) return true;
+
+            try
             {
-                if (!RedisHelper.Set(key, "pending", OncePendingExpireSeconds, RedisExistence.Nx))
+                var config = TkConfigCore.Get();
+                if (config == null) return true;
+
+                if (!string.IsNullOrEmpty(config.hide_ignore_city))
                 {
-                    if (reserved.Count > 0) RedisHelper.Del(reserved.ToArray());
+                    string? ipInfo = IP2RegionPlus.Search(ip);
+                    if (AlimamaPlus.IgnoreRegionIncluded(config.hide_ignore_city, ipInfo, out string regionInfo))
+                    {
+                        reason = $"地区控制:{regionInfo}";
+                        return false;
+                    }
+                }
+
+                string date = DateTime.Now.ToString("yyyyMMdd");
+                if (!TryReserveCount(BuildLimitKey(channel, date, "ip", ip), config.hide_limit_per_ip_24h, ticket))
+                {
+                    ReleaseReserved(ticket);
+                    reason = "IP控制";
                     return false;
                 }
-                reserved.Add(key);
+                if (!TryReserveCount(BuildLimitKey(channel, date, "oaid", oaid), config.hide_limit_per_oaid_24h, ticket))
+                {
+                    ReleaseReserved(ticket);
+                    reason = "OAID控制";
+                    return false;
+                }
+                return true;
+            }
+            catch (Exception ex)
+            {
+                // 与全局限制一致:异常时不拦截
+                _ = new LoggerLibrary("HiddenAccountCore", "TryReserveLimit")
+                    .Info(ex.Message, ex.StackTrace)
+                    .SaveAsync();
+                return true;
             }
-            return true;
         }
 
         /// <summary>
-        /// 转链结束:成功则标记当天已转(同步到所有节点),失败则释放占位
+        /// 转链结束:成功则计入当天次数(同步到其他节点),失败则归还占用的名额
         /// </summary>
-        public static async Task CompleteOnceAsync(TkChannelEnum channel, int accountId, string ip, string oaid, bool success)
+        public static async Task CompleteLimitAsync(HiddenLimitTicket ticket, bool success)
         {
-            var keys = BuildOnceKeys(channel, accountId, ip, oaid);
-            if (keys.Count == 0) return;
+            if (ticket == null || ticket.Keys.Count == 0) return;
 
             try
             {
                 if (!success)
                 {
-                    RedisHelper.Del(keys.ToArray());
+                    ReleaseReserved(ticket);
                     return;
                 }
 
-                foreach (var key in keys) RedisHelper.Set(key, "done", OnceDoneExpireSeconds);
-
+                // 本节点在占用时已计数,只同步到其他节点
                 await EndPointCore.ProcessEndPointNodesAsync(node =>
                 {
                     if (string.IsNullOrEmpty(node.redis_server)) return Task.CompletedTask;
                     if (!node.is_public_api) return Task.CompletedTask;
+                    if (string.Equals(node.name, EndPointCore.CurrentEndPoint, StringComparison.OrdinalIgnoreCase)) return Task.CompletedTask;
 
                     try
                     {
                         var redis = RedisClientManager.GetRedisClient(node.redis_server);
-                        foreach (var key in keys) redis.Set(key, "done", OnceDoneExpireSeconds);
+                        foreach (var key in ticket.Keys)
+                        {
+                            redis.IncrBy(key);
+                            redis.Expire(key, LimitExpireSeconds);
+                        }
                     }
                     catch (Exception ex)
                     {
-                        _ = new LoggerLibrary("HiddenAccountCore", "CompleteOnce")
+                        _ = new LoggerLibrary("HiddenAccountCore", "CompleteLimit")
                             .Info(node.name, ex.Message)
                             .SaveAsync();
                     }
@@ -94,19 +124,45 @@ namespace molilian.core
             }
             catch (Exception ex)
             {
-                _ = new LoggerLibrary("HiddenAccountCore", "CompleteOnce")
+                _ = new LoggerLibrary("HiddenAccountCore", "CompleteLimit")
                     .Info(ex.Message, ex.StackTrace)
                     .SaveAsync();
             }
         }
 
-        private static List<string> BuildOnceKeys(TkChannelEnum channel, int accountId, string ip, string oaid)
+        private static bool TryReserveCount(string key, int limit, HiddenLimitTicket ticket)
+        {
+            if (limit <= 0 || string.IsNullOrEmpty(key)) return true;
+
+            long count = RedisHelper.IncrBy(key);
+            RedisHelper.Expire(key, LimitExpireSeconds);
+            if (count > limit)
+            {
+                RedisHelper.IncrBy(key, -1);
+                return false;
+            }
+            ticket.Keys.Add(key);
+            return true;
+        }
+
+        private static void ReleaseReserved(HiddenLimitTicket ticket)
         {
-            string prefix = $":hide_once:{channel}_{accountId}:{DateTime.Now:yyyyMMdd}";
-            var keys = new List<string>();
-            if (!string.IsNullOrEmpty(ip)) keys.Add($"{prefix}:ip:{ip}");
-            if (!string.IsNullOrEmpty(oaid)) keys.Add($"{prefix}:oaid:{oaid}");
-            return keys;
+            foreach (var key in ticket.Keys) RedisHelper.IncrBy(key, -1);
+            ticket.Keys.Clear();
         }
+
+        private static string BuildLimitKey(TkChannelEnum channel, string date, string type, string value)
+        {
+            if (string.IsNullOrEmpty(value)) return string.Empty;
+            return $":hide_limit:{channel}:{date}:{type}:{value}";
+        }
+    }
+
+    /// <summary>
+    /// 隐藏号限制占用的名额(Redis 计数 key)
+    /// </summary>
+    public sealed class HiddenLimitTicket
+    {
+        public List<string> Keys { get; } = [];
     }
 }

+ 21 - 24
molilian.core/Core/taoke/UnionParseCore/UnionParseCore.cs

@@ -452,7 +452,7 @@ namespace molilian.core
             bool other_aff = false;
             AlimamaPlus alimama = null;
             TkDataDTO? hiddenAccountFailure = null;
-            TkPoolDTO? hiddenOnceAccount = null;
+            HiddenLimitTicket? hiddenLimitTicket = null;
             //content = content.UrlDecode();
             var result = AlimamaPlus.GetFormattedObject(request);
 
@@ -769,15 +769,15 @@ namespace molilian.core
 
                 result.hidden_account = account.is_hide;
 
-                //============================== 放弃转链-隐藏号同 ip/oaid 当天只转一次 ==============================
+                //============================== 放弃转链-隐藏号专用限制(城市、IP/OAID 每日次数,所有隐藏号共用) ==============================
                 if (account.is_hide)
                 {
-                    if (!HiddenAccountCore.TryReserveOnce(TkChannelEnum.tb, account.id, request.Ip, request.Oaid))
+                    if (!HiddenAccountCore.TryReserveLimit(TkChannelEnum.tb, request.Ip, request.Oaid, out hiddenLimitTicket, out string hiddenLimitReason))
                     {
                         if (request.AccountId <= 0) TkPoolCore.ReleaseQps(account, qpsAcquiredSinceTicks);
                         result.success = false;
                         result.message = "放弃转链";
-                        result.reason = "频次限制";
+                        result.reason = hiddenLimitReason;
                         result.subCode = TkSubCodeEnum.TrafficCtrl;
                         result.itemName = "点击打开淘宝APP";
 
@@ -798,7 +798,6 @@ namespace molilian.core
                             result.deeplink_url,
                         });
                     }
-                    hiddenOnceAccount = account;
                 }
 
 
@@ -872,10 +871,10 @@ namespace molilian.core
                     result.itemName = "点击打开淘宝APP";
                 }
             }
-            // 隐藏号:转链成功则当天该 ip/oaid 不再转,失败则释放占位
-            if (hiddenOnceAccount != null)
+            // 隐藏号:转链成功计入当天 ip/oaid 次数,失败则归还占用的名额
+            if (hiddenLimitTicket != null)
             {
-                _ = HiddenAccountCore.CompleteOnceAsync(TkChannelEnum.tb, hiddenOnceAccount.id, request.Ip, request.Oaid, result.success);
+                _ = HiddenAccountCore.CompleteLimitAsync(hiddenLimitTicket, result.success);
             }
             _ = TkLogCore.ParseLogAsync(result, alimama);
 
@@ -1448,7 +1447,7 @@ namespace molilian.core
         {
             var result = PddUnionPlus.GetFormattedObject(request);
             PddDataDTO? hiddenAccountFailure = null;
-            PddPoolDTO? hiddenOnceAccount = null;
+            HiddenLimitTicket? hiddenLimitTicket = null;
 
             var overrideContent = await OverrideRuleCore.ProcessAsync(request);
             if (overrideContent != null)
@@ -1597,21 +1596,20 @@ namespace molilian.core
 
                 result.hidden_account = account.is_hide;
 
-                //============================== 放弃转链-隐藏号同 ip/oaid 当天只转一次 ==============================
+                //============================== 放弃转链-隐藏号专用限制(城市、IP/OAID 每日次数,所有隐藏号共用) ==============================
                 if (account.is_hide)
                 {
-                    if (!HiddenAccountCore.TryReserveOnce(TkChannelEnum.pdd, account.id, request.Ip, request.Oaid))
+                    if (!HiddenAccountCore.TryReserveLimit(TkChannelEnum.pdd, request.Ip, request.Oaid, out hiddenLimitTicket, out string hiddenLimitReason))
                     {
                         result.success = false;
                         result.message = "放弃转链";
-                        result.reason = "频次限制";
+                        result.reason = hiddenLimitReason;
                         result.subCode = (int)TkSubCodeEnum.TrafficCtrl;
                         result.deeplink_url = string.IsNullOrEmpty(result.shortLinkurl) ? string.Empty : PddUnionPlus.GetDeeplink(result.shortLinkurl);
                         _ = TkLogCore.ParseLogAsync(result);
                         // 传入隐藏号失败对象仅用于不输出埋点
                         return PddParseOutput(result, APIResultCodeEnum.OK, CreatePddFailureResult(result));
                     }
-                    hiddenOnceAccount = account;
                 }
 
                 // 更新当前日使用统计
@@ -1648,10 +1646,10 @@ namespace molilian.core
                     result.reason = "转链接口异常";
                 }
             }
-            // 隐藏号:转链成功则当天该 ip/oaid 不再转,失败则释放占位
-            if (hiddenOnceAccount != null)
+            // 隐藏号:转链成功计入当天 ip/oaid 次数,失败则归还占用的名额
+            if (hiddenLimitTicket != null)
             {
-                _ = HiddenAccountCore.CompleteOnceAsync(TkChannelEnum.pdd, hiddenOnceAccount.id, request.Ip, request.Oaid, result.success);
+                _ = HiddenAccountCore.CompleteLimitAsync(hiddenLimitTicket, result.success);
             }
             _ = TkLogCore.ParseLogAsync(result);
             return PddParseOutput(result, IfExceptional ? APIResultCodeEnum.NotAcceptable : APIResultCodeEnum.OK, hiddenAccountFailure);
@@ -1661,7 +1659,7 @@ namespace molilian.core
         {
             var result = PddUnionPlus.GetFormattedObject(request);
             PddDataDTO? hiddenAccountFailure = null;
-            PddPoolDTO? hiddenOnceAccount = null;
+            HiddenLimitTicket? hiddenLimitTicket = null;
 
             string content = request.Content;
             content = content.UrlDecode();
@@ -1823,21 +1821,20 @@ namespace molilian.core
 
                 result.hidden_account = account.is_hide;
 
-                //============================== 放弃转链-隐藏号同 ip/oaid 当天只转一次 ==============================
+                //============================== 放弃转链-隐藏号专用限制(城市、IP/OAID 每日次数,所有隐藏号共用) ==============================
                 if (account.is_hide)
                 {
-                    if (!HiddenAccountCore.TryReserveOnce(TkChannelEnum.pdd, account.id, request.Ip, request.Oaid))
+                    if (!HiddenAccountCore.TryReserveLimit(TkChannelEnum.pdd, request.Ip, request.Oaid, out hiddenLimitTicket, out string hiddenLimitReason))
                     {
                         result.success = false;
                         result.message = "放弃转链";
-                        result.reason = "频次限制";
+                        result.reason = hiddenLimitReason;
                         result.subCode = (int)TkSubCodeEnum.TrafficCtrl;
                         result.deeplink_url = string.IsNullOrEmpty(result.shortLinkurl) ? string.Empty : PddUnionPlus.GetDeeplink(result.shortLinkurl);
                         _ = TkLogCore.ParseLogAsync(result);
                         // 传入隐藏号失败对象仅用于不输出埋点
                         return PddParseOutput(result, APIResultCodeEnum.OK, CreatePddFailureResult(result));
                     }
-                    hiddenOnceAccount = account;
                 }
 
                 // 更新当前日使用统计
@@ -1878,10 +1875,10 @@ namespace molilian.core
                     result.reason = "转链接口异常";
                 }
             }
-            // 隐藏号:转链成功则当天该 ip/oaid 不再转,失败则释放占位
-            if (hiddenOnceAccount != null)
+            // 隐藏号:转链成功计入当天 ip/oaid 次数,失败则归还占用的名额
+            if (hiddenLimitTicket != null)
             {
-                _ = HiddenAccountCore.CompleteOnceAsync(TkChannelEnum.pdd, hiddenOnceAccount.id, request.Ip, request.Oaid, result.success);
+                _ = HiddenAccountCore.CompleteLimitAsync(hiddenLimitTicket, result.success);
             }
             _ = TkLogCore.ParseLogAsync(result);
             return PddParseOutput(result, IfExceptional ? APIResultCodeEnum.NotAcceptable : APIResultCodeEnum.OK, hiddenAccountFailure);

+ 14 - 16
molilian.core/Core/taoke/UnionParseCore/dp2dp.cs

@@ -21,7 +21,7 @@ namespace molilian.core
             bool other_aff = false;
             AlimamaPlus alimama = null;
             TkDataDTO? hiddenAccountFailure = null;
-            TkPoolDTO? hiddenOnceAccount = null;
+            HiddenLimitTicket? hiddenLimitTicket = null;
             var result = AlimamaPlus.GetFormattedObject(request);
 
             result.riskStrategy = request.RiskStrategy;
@@ -418,15 +418,15 @@ namespace molilian.core
 
                 result.hidden_account = account.is_hide;
 
-                //============================== 放弃转链-隐藏号同 ip/oaid 当天只转一次 ==============================
+                //============================== 放弃转链-隐藏号专用限制(城市、IP/OAID 每日次数,所有隐藏号共用) ==============================
                 if (account.is_hide)
                 {
-                    if (!HiddenAccountCore.TryReserveOnce(TkChannelEnum.tb, account.id, request.Ip, request.Oaid))
+                    if (!HiddenAccountCore.TryReserveLimit(TkChannelEnum.tb, request.Ip, request.Oaid, out hiddenLimitTicket, out string hiddenLimitReason))
                     {
                         if (request.AccountId <= 0) TkPoolCore.ReleaseQps(account, qpsAcquiredSinceTicks);
                         result.success = false;
                         result.message = "放弃转链";
-                        result.reason = "频次限制";
+                        result.reason = hiddenLimitReason;
                         result.subCode = TkSubCodeEnum.TrafficCtrl;
                         result.itemName = "点击打开淘宝APP";
                         result.deeplink_url = string.Empty;
@@ -448,7 +448,6 @@ namespace molilian.core
                             result.deeplink_url,
                         });
                     }
-                    hiddenOnceAccount = account;
                 }
 
                 alimama = new AlimamaPlus(account);
@@ -522,10 +521,10 @@ namespace molilian.core
                 }
             }
 
-            // 隐藏号:转链成功则当天该 ip/oaid 不再转,失败则释放占位
-            if (hiddenOnceAccount != null)
+            // 隐藏号:转链成功计入当天 ip/oaid 次数,失败则归还占用的名额
+            if (hiddenLimitTicket != null)
             {
-                _ = HiddenAccountCore.CompleteOnceAsync(TkChannelEnum.tb, hiddenOnceAccount.id, request.Ip, request.Oaid, result.success);
+                _ = HiddenAccountCore.CompleteLimitAsync(hiddenLimitTicket, result.success);
             }
 
             if (!result.success)
@@ -764,7 +763,7 @@ namespace molilian.core
         {
             var result = PddUnionPlus.GetFormattedObject(request);
             PddDataDTO? hiddenAccountFailure = null;
-            PddPoolDTO? hiddenOnceAccount = null;
+            HiddenLimitTicket? hiddenLimitTicket = null;
             var content = request.Content.UrlDecode();
             result.rawContent = content;
 
@@ -893,21 +892,20 @@ namespace molilian.core
 
                 result.hidden_account = account.is_hide;
 
-                //============================== 放弃转链-隐藏号同 ip/oaid 当天只转一次 ==============================
+                //============================== 放弃转链-隐藏号专用限制(城市、IP/OAID 每日次数,所有隐藏号共用) ==============================
                 if (account.is_hide)
                 {
-                    if (!HiddenAccountCore.TryReserveOnce(TkChannelEnum.pdd, account.id, request.Ip, request.Oaid))
+                    if (!HiddenAccountCore.TryReserveLimit(TkChannelEnum.pdd, request.Ip, request.Oaid, out hiddenLimitTicket, out string hiddenLimitReason))
                     {
                         result.success = false;
                         result.message = "放弃转链";
-                        result.reason = "频次限制";
+                        result.reason = hiddenLimitReason;
                         result.subCode = (int)TkSubCodeEnum.TrafficCtrl;
                         result.deeplink_url = PddUnionPlus.GetDeeplink(result.shortLinkurl);
                         _ = TkLogCore.ParseLogAsync(result);
                         // 传入隐藏号失败对象仅用于不输出埋点
                         return PddParseOutput(result, APIResultCodeEnum.OK, CreatePddFailureResult(result));
                     }
-                    hiddenOnceAccount = account;
                 }
 
                 // 更新当前日使用统计
@@ -945,10 +943,10 @@ namespace molilian.core
                     result.reason = "转链接口异常";
                 }
             }
-            // 隐藏号:转链成功则当天该 ip/oaid 不再转,失败则释放占位
-            if (hiddenOnceAccount != null)
+            // 隐藏号:转链成功计入当天 ip/oaid 次数,失败则归还占用的名额
+            if (hiddenLimitTicket != null)
             {
-                _ = HiddenAccountCore.CompleteOnceAsync(TkChannelEnum.pdd, hiddenOnceAccount.id, request.Ip, request.Oaid, result.success);
+                _ = HiddenAccountCore.CompleteLimitAsync(hiddenLimitTicket, result.success);
             }
             _ = TkLogCore.ParseLogAsync(result);
             return PddParseOutput(result, IfExceptional ? APIResultCodeEnum.NotAcceptable : APIResultCodeEnum.OK, hiddenAccountFailure);

+ 13 - 0
molilian.core/DTO/alimama/TkConfigDTO.cs

@@ -39,6 +39,19 @@ namespace molilian.core
 
         public int tk_limit_per_ip_24h { get; set; } = 0;
         public int tk_limit_per_oaid_24h { get; set; } = 0;
+
+        /// <summary>
+        /// 隐藏号专用:放弃转链的城市(所有隐藏号共用,写法同 ignorePercentageCity)
+        /// </summary>
+        public string hide_ignore_city { get; set; } = string.Empty;
+        /// <summary>
+        /// 隐藏号专用:同一 IP 每天最多转成功次数(所有隐藏号合计,淘宝/拼多多分开计数),0 不限
+        /// </summary>
+        public int hide_limit_per_ip_24h { get; set; } = 0;
+        /// <summary>
+        /// 隐藏号专用:同一 OAID 每天最多转成功次数(所有隐藏号合计,淘宝/拼多多分开计数),0 不限
+        /// </summary>
+        public int hide_limit_per_oaid_24h { get; set; } = 0;
         public int tk_parse_retry_count { get; set; } = 0;
         public string tk_parse_default_icon { get; set; } = string.Empty;