dodo hold 4 месяцев назад
Родитель
Сommit
f0f6499a73

+ 2 - 1
molilian.api/Controllers/admin/ConfigController.cs

@@ -81,6 +81,7 @@ namespace molilian.api.Controllers
 
             int current = form.Read("current", 1);
             int pageSize = form.Read("pageSize", 20);
+            string channels = form.Read("channels", string.Empty);
             string bucketType = form.Read("bucket_type", "day");
             string start = form.Read("start", string.Empty);
             string end = form.Read("end", string.Empty);
@@ -88,7 +89,7 @@ namespace molilian.api.Controllers
             DateTime? startTime = DateTime.TryParse(start, out var parsedStart) ? parsedStart : null;
             DateTime? endTime = DateTime.TryParse(end, out var parsedEnd) ? parsedEnd : null;
 
-            var data = await XhsZlongForwardCore.QueryReportAsync(bucketType, startTime, endTime, current, pageSize);
+            var data = await ZlongForwardCore.QueryReportAsync(channels, bucketType, startTime, endTime, current, pageSize);
             return new APIResult(new { data });
         }
 

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
molilian.api/Properties/PublishProfiles/latest.pubxml.user


+ 1 - 1
molilian.core/Core/log/taobao.cs

@@ -102,7 +102,7 @@ namespace molilian.core
                 response.elapsedTime = (int)ts.TotalMilliseconds;
                 _ = RedisKit.RPushAsync(queue_parse_tb_key, response);
 
-                if (response.success || response.message.Equals("转链失败"))
+                if (response.success || response.message.Equals("转链失败") || response.message.Equals("放弃转链"))
                 {
                     await RiskControlCore.CallsIncrByAsync(response.channel, response.accountId);
                     await saveClientRequestTotalAsync(response.channel, response.ip, response.oaid);

+ 10 - 0
molilian.core/Core/pdd/PddPoolCore.cs

@@ -81,6 +81,11 @@ namespace molilian.core
         private static PddPoolDTO? SmoothWeightedRoundRobin(List<PddPoolDTO> candidates, string parse_type)
         {
             if (!candidates.Any()) return null;
+#if DEBUG
+
+            var single = candidates[0];
+            return single;
+#else
             if (candidates.Count == 1)
             {
                 var single = candidates[0];
@@ -91,6 +96,7 @@ namespace molilian.core
                 }
                 return null; // 预留失败,说明账号已被其他请求占用
             }
+#endif
 
             try
             {
@@ -622,6 +628,9 @@ namespace molilian.core
             // cis_limit 检查已移至 SmoothWeightedRoundRobin 方法中基于时间间隔判断
             // 这里不再需要检查 Redis 锁
 
+
+#if DEBUG
+#else
             if (item.rpm_limit > 0)
             {
                 int rpm_num = RiskControlCore.GetCalls(TkChannelEnum.pdd, item.id, DateTime.Now.ToString("yyyyMMddHHmm"));
@@ -637,6 +646,7 @@ namespace molilian.core
                 int hourly_num = RiskControlCore.GetCalls(TkChannelEnum.pdd, item.id, DateTime.Now.ToString("yyyyMMddHH"));
                 if (hourly_num >= item.hourly_calls_limit) return false;
             }
+#endif
             return true;
         }
 

+ 22 - 10
molilian.core/Core/taoke/UnionParseCore/UnionParseCore.cs

@@ -35,11 +35,12 @@ namespace molilian.core
                         "pdd" => await DeeplinkPddParseAsync(request),
                         "tb" => await DeeplinkTaobaoParseAsync(request),
                     };
-                case "ym":
-                    return request.Channel switch
-                    {
-                        "tb" => await DeepleaperParseAsync(request),
-                    };
+                //case "ym":
+                //    return request.Channel switch
+                //    {
+                //        "tb" => await DeeplinkTaobaoParseAsync(request),
+                //        //"tb" => await DeepleaperParseAsync(request),
+                //    };
                 case "goods":
                     return request.Channel switch
                     {
@@ -48,10 +49,21 @@ namespace molilian.core
                     };
             }
 
-            var config = TkConfigCore.Get();
-            if ("xhs".Equals(request.Channel) && config.webook_xhs_zlong == 1 && !string.IsNullOrEmpty(request.Oaid))
+            var config = TkConfigCore.Get();
+
+            string[] enabledForwardChannels = $"{config.webook_forward_zlong}".Split(',', StringSplitOptions.RemoveEmptyEntries);
+
+            switch (request.Channel)
             {
-                _ = XhsZlongForwardCore.ForwardAsync(request.Oaid);
+                case "xhs":
+                case "ks":
+                    if (Array.Exists(enabledForwardChannels, item =>
+                            string.Equals(item.Trim(), request.Channel, StringComparison.OrdinalIgnoreCase))
+                        && !string.IsNullOrEmpty(request.Oaid))
+                    {
+                        _ = ZlongForwardCore.ForwardAsync(request.Channel, request.Oaid);
+                    }
+                    break;
             }
 
             return request.Channel switch
@@ -601,12 +613,12 @@ namespace molilian.core
 
                 //============================== 放弃转链-没有匹配账号 ==============================
 #if DEBUG
-                request.AccountId = 133;
+                //request.AccountId = 133;
 #endif
 
                 TkPoolDTO? account = request.AccountId > 0 ?
                     await TkPoolCore.GetOneAsync(request.AccountId) :
-                    await TkPoolCore.GetOneAsync(TkPoolCore.TkAction.parse, isTaobaoUrl, request.RiskStrategy, request.LaunchScene);
+                    await TkPoolCore.GetOneAsync(TkPoolCore.TkAction.parse, isTaobaoUrl, request.RiskStrategy, request.LaunchScene, request.Type);
                 if (account == null)
                 {
                     result.success = false;

+ 0 - 315
molilian.core/Core/taoke/UnionParseCore/XhsZlongForwardCore.cs

@@ -1,315 +0,0 @@
-using dodohold.core;
-using System.Net;
-using System.Globalization;
-using YunhuiKit;
-
-namespace molilian.core
-{
-    public static class XhsZlongForwardCore
-    {
-        public const string RedisPrefix = ":union_parse:xhs_zlong_forward";
-        public const string ExceptionRedisPrefix = $"{RedisPrefix}:exception";
-        private const string ExceptionAggregateType = "all";
-        private const string ForwardUrl = "https://tracker.z-long.cn/ad/tracker/special/liutiyun/xhs";
-        private static readonly HttpClient HttpClient = new()
-        {
-            Timeout = TimeSpan.FromSeconds(10)
-        };
-
-        public static async Task ForwardAsync(string oaid)
-        {
-            string requestUrl = string.Empty;
-            try
-            {
-                long unixTimeMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
-                requestUrl = $"{ForwardUrl}?oaid={WebUtility.UrlEncode(oaid ?? string.Empty)}&datetime={unixTimeMilliseconds}";
-            }
-            catch (Exception ex)
-            {
-                _ = new LoggerLibrary("xhs_zlong_forward")
-                    .Info(requestUrl)
-                    .Info(ex.Message, ex.StackTrace)
-                    .SaveAsync();
-                return;
-            }
-
-            try
-            {
-                SaveCount();
-            }
-            catch (Exception ex)
-            {
-                _ = new LoggerLibrary("xhs_zlong_forward")
-                    .Info(requestUrl)
-                    .Info(ex.Message, ex.StackTrace)
-                    .SaveAsync();
-            }
-
-            try
-            {
-                using var response = await HttpClient.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead);
-                if (response.IsSuccessStatusCode) return;
-
-                SaveExceptionCount($"status_{(int)response.StatusCode}");
-            }
-            catch (TaskCanceledException)
-            {
-                SaveExceptionCount("timeout");
-            }
-            catch (Exception)
-            {
-                SaveExceptionCount("request_error");
-            }
-        }
-
-        private static void SaveCount()
-        {
-            DateTime now = DateTime.Now;
-            var metrics = GetBucketMetrics(now);
-
-            foreach (var metric in metrics)
-            {
-                string key = GetCountKey(metric.bucketType, metric.bucketValue);
-                SaveMetricCount(key, GetIndexKey(metric.bucketType), metric.expireSeconds);
-            }
-        }
-
-        public static async Task<XhsZlongForwardReportResult> QueryReportAsync(string bucketType, DateTime? start, DateTime? end, int page, int size)
-        {
-            string normalizedBucketType = NormalizeBucketType(bucketType);
-            page = Math.Max(page, 1);
-            size = size <= 0 ? 20 : size;
-
-            var list = new List<XhsZlongForwardReportItem>();
-            string[] keys = await TkLogCore.GetTotalKeysAsync(GetIndexKey(normalizedBucketType));
-
-            DateTime? startTime = start?.Date;
-            DateTime? endTime = end?.Date.AddDays(1).AddTicks(-1);
-
-            foreach (var key in keys)
-            {
-                if (!TryParseKey(key, out var item)) continue;
-                if (!string.Equals(item.bucket_type, normalizedBucketType, StringComparison.OrdinalIgnoreCase)) continue;
-                if (startTime.HasValue && item.report_time < startTime.Value) continue;
-                if (endTime.HasValue && item.report_time > endTime.Value) continue;
-
-                list.Add(item);
-            }
-
-            var totalTasks = list.Select(async item =>
-            {
-                var totalTask = TkLogCore.GetTotalAsync(item.redis_key);
-                var exceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(ExceptionAggregateType, item.bucket_type, item.bucket_value));
-                await Task.WhenAll(totalTask, exceptionTask);
-
-                item.total = totalTask.Result;
-                item.exception_count = exceptionTask.Result;
-                return item;
-            });
-
-            list = (await Task.WhenAll(totalTasks))
-                .Where(x => x.total > 0)
-                .ToList();
-
-            list = list
-                .OrderByDescending(x => x.report_time)
-                .ThenByDescending(x => x.bucket_value)
-                .ToList();
-
-            int count = list.Count;
-            int skip = (page - 1) * size;
-            int pageCount = count == 0 ? 0 : (int)Math.Ceiling(count / (double)size);
-
-            return new XhsZlongForwardReportResult
-            {
-                list = list.Skip(skip).Take(size).ToList(),
-                page = page,
-                size = size,
-                count = count,
-                pageCount = pageCount,
-                summary = await GetSummaryAsync(),
-            };
-        }
-
-        public static async Task<XhsZlongForwardReportSummary> GetSummaryAsync()
-        {
-            DateTime now = DateTime.Now;
-            var hourTask = TkLogCore.GetTotalAsync(GetCountKey("hour", now.ToString("yyyyMMddHH")));
-            var dayTask = TkLogCore.GetTotalAsync(GetCountKey("day", now.ToString("yyyyMMdd")));
-            var monthTask = TkLogCore.GetTotalAsync(GetCountKey("month", now.ToString("yyyyMM")));
-            var yearTask = TkLogCore.GetTotalAsync(GetCountKey("year", now.ToString("yyyy")));
-
-            var hourExceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(ExceptionAggregateType, "hour", now.ToString("yyyyMMddHH")));
-            var dayExceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(ExceptionAggregateType, "day", now.ToString("yyyyMMdd")));
-            var monthExceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(ExceptionAggregateType, "month", now.ToString("yyyyMM")));
-            var yearExceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(ExceptionAggregateType, "year", now.ToString("yyyy")));
-
-            await Task.WhenAll(hourTask, dayTask, monthTask, yearTask, hourExceptionTask, dayExceptionTask, monthExceptionTask, yearExceptionTask);
-
-            return new XhsZlongForwardReportSummary
-            {
-                hour = hourTask.Result,
-                day = dayTask.Result,
-                month = monthTask.Result,
-                year = yearTask.Result,
-                hour_exception = hourExceptionTask.Result,
-                day_exception = dayExceptionTask.Result,
-                month_exception = monthExceptionTask.Result,
-                year_exception = yearExceptionTask.Result,
-            };
-        }
-
-        private static string NormalizeBucketType(string bucketType)
-        {
-            return bucketType?.Trim().ToLowerInvariant() switch
-            {
-                "hour" => "hour",
-                "month" => "month",
-                "year" => "year",
-                _ => "day",
-            };
-        }
-
-        private static string GetIndexKey(string bucketType)
-        {
-            return $"{RedisPrefix}:index:{bucketType}";
-        }
-
-        private static string GetCountKey(string bucketType, string bucketValue)
-        {
-            return $"{RedisPrefix}:{bucketType}:{bucketValue}";
-        }
-
-        private static string GetExceptionCountKey(string exceptionType, string bucketType, string bucketValue)
-        {
-            return $"{ExceptionRedisPrefix}:{exceptionType}:{bucketType}:{bucketValue}";
-        }
-
-        private static string GetExceptionIndexKey(string exceptionType, string bucketType)
-        {
-            return $"{ExceptionRedisPrefix}:index:{exceptionType}:{bucketType}";
-        }
-
-        private static (string bucketType, string bucketValue, int expireSeconds)[] GetBucketMetrics(DateTime now)
-        {
-            return
-            [
-                ("hour", now.ToString("yyyyMMddHH"), 86400 * 90),
-                ("day", now.ToString("yyyyMMdd"), 86400 * 366),
-                ("month", now.ToString("yyyyMM"), 86400 * 3660),
-                ("year", now.ToString("yyyy"), 86400 * 3660),
-            ];
-        }
-
-        private static void SaveMetricCount(string key, string indexKey, int expireSeconds)
-        {
-            RedisHelper.IncrBy(key);
-            RedisHelper.Expire(key, expireSeconds);
-            RedisHelper.SAdd(indexKey, key);
-            RedisHelper.Expire(indexKey, expireSeconds);
-        }
-
-        private static void SaveExceptionCount(string exceptionType)
-        {
-            DateTime now = DateTime.Now;
-            var metrics = GetBucketMetrics(now);
-
-            foreach (var metric in metrics)
-            {
-                string key = GetExceptionCountKey(exceptionType, metric.bucketType, metric.bucketValue);
-                SaveMetricCount(key, GetExceptionIndexKey(exceptionType, metric.bucketType), metric.expireSeconds);
-
-                string aggregateKey = GetExceptionCountKey(ExceptionAggregateType, metric.bucketType, metric.bucketValue);
-                SaveMetricCount(aggregateKey, GetExceptionIndexKey(ExceptionAggregateType, metric.bucketType), metric.expireSeconds);
-            }
-        }
-
-        private static bool TryParseKey(string key, out XhsZlongForwardReportItem item)
-        {
-            item = new XhsZlongForwardReportItem();
-            if (string.IsNullOrWhiteSpace(key)) return false;
-
-            var parts = key.Split(':', StringSplitOptions.RemoveEmptyEntries);
-            if (parts.Length < 4) return false;
-
-            string bucketType = parts[^2];
-            string bucketValue = parts[^1];
-            if (!TryParseBucket(bucketType, bucketValue, out var reportTime, out var displayTime)) return false;
-
-            item.bucket_type = bucketType;
-            item.bucket_value = bucketValue;
-            item.report_time = reportTime;
-            item.display_time = displayTime;
-            item.redis_key = key;
-            return true;
-        }
-
-        private static bool TryParseBucket(string bucketType, string bucketValue, out DateTime reportTime, out string displayTime)
-        {
-            reportTime = DateTime.MinValue;
-            displayTime = string.Empty;
-
-            string format = bucketType switch
-            {
-                "hour" => "yyyyMMddHH",
-                "day" => "yyyyMMdd",
-                "month" => "yyyyMM",
-                "year" => "yyyy",
-                _ => string.Empty,
-            };
-            if (string.IsNullOrEmpty(format)) return false;
-
-            bool success = DateTime.TryParseExact(
-                bucketValue,
-                format,
-                CultureInfo.InvariantCulture,
-                DateTimeStyles.None,
-                out reportTime);
-
-            if (!success) return false;
-
-            displayTime = bucketType switch
-            {
-                "hour" => reportTime.ToString("yyyy-MM-dd HH:00"),
-                "day" => reportTime.ToString("yyyy-MM-dd"),
-                "month" => reportTime.ToString("yyyy-MM"),
-                "year" => reportTime.ToString("yyyy"),
-                _ => bucketValue,
-            };
-            return true;
-        }
-    }
-
-    public class XhsZlongForwardReportSummary
-    {
-        public long hour { get; set; }
-        public long day { get; set; }
-        public long month { get; set; }
-        public long year { get; set; }
-        public long hour_exception { get; set; }
-        public long day_exception { get; set; }
-        public long month_exception { get; set; }
-        public long year_exception { get; set; }
-    }
-
-    public class XhsZlongForwardReportItem
-    {
-        public string bucket_type { get; set; } = string.Empty;
-        public string bucket_value { get; set; } = string.Empty;
-        public DateTime report_time { get; set; } = DateTime.MinValue;
-        public string display_time { get; set; } = string.Empty;
-        public long total { get; set; }
-        public long exception_count { get; set; }
-        public string redis_key { get; set; } = string.Empty;
-    }
-
-    public class XhsZlongForwardReportResult
-    {
-        public int page { get; set; }
-        public int pageCount { get; set; }
-        public int size { get; set; }
-        public int count { get; set; }
-        public List<XhsZlongForwardReportItem> list { get; set; } = [];
-        public XhsZlongForwardReportSummary summary { get; set; } = new();
-    }
-}

+ 467 - 0
molilian.core/Core/taoke/UnionParseCore/ZlongForwardCore.cs

@@ -0,0 +1,467 @@
+using dodohold.core;
+using System.Net;
+using System.Globalization;
+using YunhuiKit;
+
+namespace molilian.core
+{
+    public static class ZlongForwardCore
+    {
+        public const string RedisPrefix = ":union_parse:xhs_zlong_forward";
+        public const string ExceptionRedisPrefix = $"{RedisPrefix}:exception";
+        private const string ExceptionAggregateType = "all";
+        private const string ForwardUrlTemplate = "https://tracker.z-long.cn/ad/tracker/special/liutiyun/{0}";
+        private static readonly string[] SupportedChannels = new[] { "xhs", "ks" };
+        private static readonly HttpClient HttpClient = new()
+        {
+            Timeout = TimeSpan.FromSeconds(10)
+        };
+
+        public static async Task ForwardAsync(string channel, string oaid)
+        {
+            string normalizedChannel = NormalizeChannel(channel);
+            if (string.IsNullOrEmpty(normalizedChannel)) return;
+
+            string requestUrl = string.Empty;
+            try
+            {
+                long unixTimeMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                string forwardUrl = string.Format(CultureInfo.InvariantCulture, ForwardUrlTemplate, normalizedChannel);
+                requestUrl = $"{forwardUrl}?oaid={WebUtility.UrlEncode(oaid ?? string.Empty)}&datetime={unixTimeMilliseconds}";
+            }
+            catch (Exception ex)
+            {
+                _ = new LoggerLibrary("zlong_forward")
+                    .Info(normalizedChannel, requestUrl)
+                    .Info(ex.Message, ex.StackTrace)
+                    .SaveAsync();
+                return;
+            }
+
+            try
+            {
+                SaveCount(normalizedChannel);
+            }
+            catch (Exception ex)
+            {
+                _ = new LoggerLibrary("zlong_forward")
+                    .Info(normalizedChannel, requestUrl)
+                    .Info(ex.Message, ex.StackTrace)
+                    .SaveAsync();
+            }
+
+            try
+            {
+                using var response = await HttpClient.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead);
+                if (response.IsSuccessStatusCode) return;
+
+                SaveExceptionCount(normalizedChannel, $"status_{(int)response.StatusCode}");
+            }
+            catch (TaskCanceledException)
+            {
+                SaveExceptionCount(normalizedChannel, "timeout");
+            }
+            catch (Exception)
+            {
+                SaveExceptionCount(normalizedChannel, "request_error");
+            }
+        }
+
+        private static void SaveCount(string channel)
+        {
+            DateTime now = DateTime.Now;
+            var metrics = GetBucketMetrics(now);
+
+            foreach (var metric in metrics)
+            {
+                string key = GetCountKey(channel, metric.bucketType, metric.bucketValue);
+                SaveMetricCount(key, GetIndexKey(channel, metric.bucketType), metric.expireSeconds);
+            }
+        }
+
+        public static async Task<ZlongForwardReportResult> QueryReportAsync(string channels, string bucketType, DateTime? start, DateTime? end, int page, int size)
+        {
+            string[] selectedChannels = NormalizeChannels(channels);
+            string normalizedBucketType = NormalizeBucketType(bucketType);
+            page = Math.Max(page, 1);
+            size = size <= 0 ? 20 : size;
+
+            var list = new List<ZlongForwardReportItem>();
+            DateTime? startTime = start?.Date;
+            DateTime? endTime = end?.Date.AddDays(1).AddTicks(-1);
+
+            foreach (var channel in selectedChannels)
+            {
+                string[] keys = await TkLogCore.GetTotalKeysAsync(GetIndexKey(channel, normalizedBucketType)) ?? Array.Empty<string>();
+                foreach (var key in keys)
+                {
+                    if (!TryParseKey(key, out var item)) continue;
+                    if (!IsMatched(item, normalizedBucketType, startTime, endTime)) continue;
+
+                    list.Add(item);
+                }
+
+                if (!string.Equals(channel, "xhs", StringComparison.OrdinalIgnoreCase)) continue;
+
+                string[] legacyKeys = await TkLogCore.GetTotalKeysAsync(GetLegacyIndexKey(normalizedBucketType)) ?? Array.Empty<string>();
+                foreach (var key in legacyKeys)
+                {
+                    if (!TryParseKey(key, out var item)) continue;
+                    if (!IsMatched(item, normalizedBucketType, startTime, endTime)) continue;
+
+                    list.Add(item);
+                }
+            }
+
+            var totalTasks = list.Select(async item =>
+            {
+                var totalTask = TkLogCore.GetTotalAsync(item.redis_key);
+                var exceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(item.redis_key, item.channel, item.bucket_type, item.bucket_value));
+                await Task.WhenAll(totalTask, exceptionTask);
+
+                item.total = totalTask.Result;
+                item.exception_count = exceptionTask.Result;
+                return item;
+            });
+
+            list = MergeItems(await Task.WhenAll(totalTasks))
+                .Where(x => x.total > 0 || x.exception_count > 0)
+                .ToList();
+
+            list = list
+                .OrderByDescending(x => x.report_time)
+                .ThenBy(x => GetChannelSortOrder(x.channel))
+                .ThenByDescending(x => x.bucket_value)
+                .ToList();
+
+            int count = list.Count;
+            int skip = (page - 1) * size;
+            int pageCount = count == 0 ? 0 : (int)Math.Ceiling(count / (double)size);
+
+            return new ZlongForwardReportResult
+            {
+                list = list.Skip(skip).Take(size).ToList(),
+                page = page,
+                size = size,
+                count = count,
+                pageCount = pageCount,
+                summary = await GetSummaryAsync(selectedChannels),
+            };
+        }
+
+        public static async Task<ZlongForwardReportSummary> GetSummaryAsync(string channels)
+        {
+            return await GetSummaryAsync(NormalizeChannels(channels));
+        }
+
+        private static async Task<ZlongForwardReportSummary> GetSummaryAsync(string[] channels)
+        {
+            DateTime now = DateTime.Now;
+            var hourTask = GetMetricTotalAsync(channels, "hour", now.ToString("yyyyMMddHH"));
+            var dayTask = GetMetricTotalAsync(channels, "day", now.ToString("yyyyMMdd"));
+            var monthTask = GetMetricTotalAsync(channels, "month", now.ToString("yyyyMM"));
+            var yearTask = GetMetricTotalAsync(channels, "year", now.ToString("yyyy"));
+
+            var hourExceptionTask = GetMetricTotalAsync(channels, "hour", now.ToString("yyyyMMddHH"), true);
+            var dayExceptionTask = GetMetricTotalAsync(channels, "day", now.ToString("yyyyMMdd"), true);
+            var monthExceptionTask = GetMetricTotalAsync(channels, "month", now.ToString("yyyyMM"), true);
+            var yearExceptionTask = GetMetricTotalAsync(channels, "year", now.ToString("yyyy"), true);
+
+            await Task.WhenAll(hourTask, dayTask, monthTask, yearTask, hourExceptionTask, dayExceptionTask, monthExceptionTask, yearExceptionTask);
+
+            return new ZlongForwardReportSummary
+            {
+                hour = hourTask.Result,
+                day = dayTask.Result,
+                month = monthTask.Result,
+                year = yearTask.Result,
+                hour_exception = hourExceptionTask.Result,
+                day_exception = dayExceptionTask.Result,
+                month_exception = monthExceptionTask.Result,
+                year_exception = yearExceptionTask.Result,
+            };
+        }
+
+        private static async Task<int> GetMetricTotalAsync(string[] channels, string bucketType, string bucketValue, bool isException = false)
+        {
+            var tasks = new List<Task<int>>();
+
+            foreach (var channel in channels)
+            {
+                string key = isException
+                    ? GetChannelExceptionCountKey(channel, ExceptionAggregateType, bucketType, bucketValue)
+                    : GetCountKey(channel, bucketType, bucketValue);
+
+                tasks.Add(TkLogCore.GetTotalAsync(key));
+
+                if (!string.Equals(channel, "xhs", StringComparison.OrdinalIgnoreCase)) continue;
+
+                string legacyKey = isException
+                    ? GetLegacyExceptionCountKey(ExceptionAggregateType, bucketType, bucketValue)
+                    : GetLegacyCountKey(bucketType, bucketValue);
+
+                tasks.Add(TkLogCore.GetTotalAsync(legacyKey));
+            }
+
+            await Task.WhenAll(tasks);
+            return tasks.Sum(task => task.Result);
+        }
+
+        private static bool IsMatched(ZlongForwardReportItem item, string bucketType, DateTime? startTime, DateTime? endTime)
+        {
+            if (!string.Equals(item.bucket_type, bucketType, StringComparison.OrdinalIgnoreCase)) return false;
+            if (startTime.HasValue && item.report_time < startTime.Value) return false;
+            if (endTime.HasValue && item.report_time > endTime.Value) return false;
+
+            return true;
+        }
+
+        private static List<ZlongForwardReportItem> MergeItems(IEnumerable<ZlongForwardReportItem> items)
+        {
+            return items
+                .GroupBy(item => $"{item.channel}:{item.bucket_type}:{item.bucket_value}", StringComparer.OrdinalIgnoreCase)
+                .Select(group =>
+                {
+                    var first = group.First();
+                    return new ZlongForwardReportItem
+                    {
+                        channel = first.channel,
+                        bucket_type = first.bucket_type,
+                        bucket_value = first.bucket_value,
+                        report_time = first.report_time,
+                        display_time = first.display_time,
+                        total = group.Sum(item => item.total),
+                        exception_count = group.Sum(item => item.exception_count),
+                        redis_key = $"{first.channel}:{first.bucket_type}:{first.bucket_value}",
+                    };
+                })
+                .ToList();
+        }
+
+        private static string NormalizeBucketType(string bucketType)
+        {
+            return bucketType?.Trim().ToLowerInvariant() switch
+            {
+                "hour" => "hour",
+                "month" => "month",
+                "year" => "year",
+                _ => "day",
+            };
+        }
+
+        private static string[] NormalizeChannels(string channels)
+        {
+            if (string.IsNullOrWhiteSpace(channels)) return SupportedChannels;
+
+            var list = new List<string>();
+            foreach (var item in channels.Split(',', StringSplitOptions.RemoveEmptyEntries))
+            {
+                string normalized = item.Trim().ToLowerInvariant();
+                if (string.Equals(normalized, "all", StringComparison.OrdinalIgnoreCase))
+                {
+                    return SupportedChannels;
+                }
+
+                normalized = NormalizeChannel(normalized);
+                if (string.IsNullOrEmpty(normalized) || list.Contains(normalized)) continue;
+
+                list.Add(normalized);
+            }
+
+            return list.Count > 0 ? list.ToArray() : SupportedChannels;
+        }
+
+        private static string NormalizeChannel(string channel)
+        {
+            if (string.IsNullOrWhiteSpace(channel)) return string.Empty;
+
+            string normalized = channel.Trim().ToLowerInvariant();
+            return SupportedChannels.Contains(normalized) ? normalized : string.Empty;
+        }
+
+        private static int GetChannelSortOrder(string channel)
+        {
+            int index = Array.IndexOf(SupportedChannels, channel);
+            return index >= 0 ? index : SupportedChannels.Length;
+        }
+
+        private static string GetIndexKey(string channel, string bucketType)
+        {
+            return $"{RedisPrefix}:index:{channel}:{bucketType}";
+        }
+
+        private static string GetLegacyIndexKey(string bucketType)
+        {
+            return $"{RedisPrefix}:index:{bucketType}";
+        }
+
+        private static string GetCountKey(string channel, string bucketType, string bucketValue)
+        {
+            return $"{RedisPrefix}:{channel}:{bucketType}:{bucketValue}";
+        }
+
+        private static string GetLegacyCountKey(string bucketType, string bucketValue)
+        {
+            return $"{RedisPrefix}:{bucketType}:{bucketValue}";
+        }
+
+        private static string GetExceptionCountKey(string redisKey, string channel, string bucketType, string bucketValue)
+        {
+            return IsLegacyCountKey(redisKey)
+                ? GetLegacyExceptionCountKey(ExceptionAggregateType, bucketType, bucketValue)
+                : GetChannelExceptionCountKey(channel, ExceptionAggregateType, bucketType, bucketValue);
+        }
+
+        private static string GetChannelExceptionCountKey(string channel, string exceptionType, string bucketType, string bucketValue)
+        {
+            return $"{ExceptionRedisPrefix}:{channel}:{exceptionType}:{bucketType}:{bucketValue}";
+        }
+
+        private static string GetLegacyExceptionCountKey(string exceptionType, string bucketType, string bucketValue)
+        {
+            return $"{ExceptionRedisPrefix}:{exceptionType}:{bucketType}:{bucketValue}";
+        }
+
+        private static bool IsLegacyCountKey(string key)
+        {
+            var parts = key.Split(':', StringSplitOptions.RemoveEmptyEntries);
+            return parts.Length == 4;
+        }
+
+        private static string GetExceptionIndexKey(string channel, string exceptionType, string bucketType)
+        {
+            return $"{ExceptionRedisPrefix}:index:{channel}:{exceptionType}:{bucketType}";
+        }
+
+        private static (string bucketType, string bucketValue, int expireSeconds)[] GetBucketMetrics(DateTime now)
+        {
+            return
+            [
+                ("hour", now.ToString("yyyyMMddHH"), 86400 * 90),
+                ("day", now.ToString("yyyyMMdd"), 86400 * 366),
+                ("month", now.ToString("yyyyMM"), 86400 * 3660),
+                ("year", now.ToString("yyyy"), 86400 * 3660),
+            ];
+        }
+
+        private static void SaveMetricCount(string key, string indexKey, int expireSeconds)
+        {
+            RedisHelper.IncrBy(key);
+            RedisHelper.Expire(key, expireSeconds);
+            RedisHelper.SAdd(indexKey, key);
+            RedisHelper.Expire(indexKey, expireSeconds);
+        }
+
+        private static void SaveExceptionCount(string channel, string exceptionType)
+        {
+            DateTime now = DateTime.Now;
+            var metrics = GetBucketMetrics(now);
+
+            foreach (var metric in metrics)
+            {
+                string key = GetChannelExceptionCountKey(channel, exceptionType, metric.bucketType, metric.bucketValue);
+                SaveMetricCount(key, GetExceptionIndexKey(channel, exceptionType, metric.bucketType), metric.expireSeconds);
+
+                string aggregateKey = GetChannelExceptionCountKey(channel, ExceptionAggregateType, metric.bucketType, metric.bucketValue);
+                SaveMetricCount(aggregateKey, GetExceptionIndexKey(channel, ExceptionAggregateType, metric.bucketType), metric.expireSeconds);
+            }
+        }
+
+        private static bool TryParseKey(string key, out ZlongForwardReportItem item)
+        {
+            item = new ZlongForwardReportItem();
+            if (string.IsNullOrWhiteSpace(key)) return false;
+
+            var parts = key.Split(':', StringSplitOptions.RemoveEmptyEntries);
+            if (parts.Length < 4) return false;
+
+            string channel = "xhs";
+            string bucketType = parts[^2];
+            string bucketValue = parts[^1];
+            if (parts.Length >= 5)
+            {
+                channel = NormalizeChannel(parts[^3]);
+                if (string.IsNullOrEmpty(channel)) return false;
+            }
+
+            if (!TryParseBucket(bucketType, bucketValue, out var reportTime, out var displayTime)) return false;
+
+            item.channel = channel;
+            item.bucket_type = bucketType;
+            item.bucket_value = bucketValue;
+            item.report_time = reportTime;
+            item.display_time = displayTime;
+            item.redis_key = key;
+            return true;
+        }
+
+        private static bool TryParseBucket(string bucketType, string bucketValue, out DateTime reportTime, out string displayTime)
+        {
+            reportTime = DateTime.MinValue;
+            displayTime = string.Empty;
+
+            string format = bucketType switch
+            {
+                "hour" => "yyyyMMddHH",
+                "day" => "yyyyMMdd",
+                "month" => "yyyyMM",
+                "year" => "yyyy",
+                _ => string.Empty,
+            };
+            if (string.IsNullOrEmpty(format)) return false;
+
+            bool success = DateTime.TryParseExact(
+                bucketValue,
+                format,
+                CultureInfo.InvariantCulture,
+                DateTimeStyles.None,
+                out reportTime);
+
+            if (!success) return false;
+
+            displayTime = bucketType switch
+            {
+                "hour" => reportTime.ToString("yyyy-MM-dd HH:00"),
+                "day" => reportTime.ToString("yyyy-MM-dd"),
+                "month" => reportTime.ToString("yyyy-MM"),
+                "year" => reportTime.ToString("yyyy"),
+                _ => bucketValue,
+            };
+            return true;
+        }
+    }
+
+    public class ZlongForwardReportSummary
+    {
+        public long hour { get; set; }
+        public long day { get; set; }
+        public long month { get; set; }
+        public long year { get; set; }
+        public long hour_exception { get; set; }
+        public long day_exception { get; set; }
+        public long month_exception { get; set; }
+        public long year_exception { get; set; }
+    }
+
+    public class ZlongForwardReportItem
+    {
+        public string channel { get; set; } = string.Empty;
+        public string bucket_type { get; set; } = string.Empty;
+        public string bucket_value { get; set; } = string.Empty;
+        public DateTime report_time { get; set; } = DateTime.MinValue;
+        public string display_time { get; set; } = string.Empty;
+        public long total { get; set; }
+        public long exception_count { get; set; }
+        public string redis_key { get; set; } = string.Empty;
+    }
+
+    public class ZlongForwardReportResult
+    {
+        public int page { get; set; }
+        public int pageCount { get; set; }
+        public int size { get; set; }
+        public int count { get; set; }
+        public List<ZlongForwardReportItem> list { get; set; } = [];
+        public ZlongForwardReportSummary summary { get; set; } = new();
+    }
+}

+ 1 - 2
molilian.core/DTO/alimama/TkConfigDTO.cs

@@ -64,8 +64,7 @@ namespace molilian.core
         public string dyIgnorePercentageCity { get; set; } = string.Empty;
         public int dy_limit_per_ip_24h { get; set; } = 0;
         public int dy_limit_per_oaid_24h { get; set; } = 0;
-        public int webook_xhs_zlong { get; set; } = 0;
-
+        public string webook_forward_zlong { get; set; } = string.Empty;
 
 
     }

+ 8 - 1
molilian.core/Plus/Alimama/parse.cs

@@ -134,11 +134,18 @@ namespace molilian.core
             }
 
             //============================== 放弃转链-地区过滤 ==============================
+
             bool is_ignore2 = FlowControlIgnoreRequest(ip, oaid, riskStrategy, launchScene, out string reason);
+            string message = "放弃转链";
             if (is_ignore2)
             {
+                if ("7".Equals(_account.parseEndpoint))
+                {
+                    message = "转链失败";
+                    result.reason = "fail2";
+                }
                 result.success = false;
-                result.message = "放弃转链";
+                result.message = message;
                 result.reason = reason;
                 result.subCode = TkSubCodeEnum.TrafficCtrl;
                 return result;

+ 0 - 2
molilian.core/Plus/Alimama/parse_endpoint/endpoint_tbk_dg.cs

@@ -59,9 +59,7 @@ namespace molilian.core
                 return result;
             }
 
-
             string deeplink = response?.Data?.PageDeeplink;
-
             result.deeplink_url = deeplink;
             string h5Url = deeplink.GetContentPart("h5Url=", "&").UrlDecode();
             result.shortLinkurl = h5Url;

+ 11 - 6
molilian.core/Plus/DeepleaperPlus.cs

@@ -135,7 +135,7 @@ namespace molilian.core
             //	"message": "淘宝口令解析服务异常: Cannot deserialize value of type `java.lang.Integer` from String \"该卖家ID暂不支持\": not a valid `java.lang.Integer` value\n at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: com.deepleaper.qianxun.model.taobao.QXTbGeneralLinkParseResponse[\"data\"]->com.deepleaper.qianxun.model.taobao.QXTbGeneralLinkParseResponse$Result[\"material_url_list\"]->java.util.ArrayList[0]->com.deepleaper.qianxun.model.taobao.QXTbGeneralLinkParseResponse$Material[\"msg\"])",
 
             result.reason = message;
-            result.message = "转链失败";
+            result.message = "fail";
             if (!success) return result;
             try
             {
@@ -149,19 +149,24 @@ namespace molilian.core
                 result.deeplink_url = shoppingInfoItems[0].PathRead<string>("detailUrl.deepLink.url");
                 result.with_middle_page = shoppingInfoItems[0].PathRead<int>("detailUrl.type");
                 result.reason = shoppingInfoItems[0].PathRead<string>("detailUrl.subMsg");
-                if (result.with_middle_page == 1) { result.commercial = true; }
-
-
+                if (result.with_middle_page == 1)
+                {
+                    result.message = "success";
+                    result.commercial = true;
+                }
+                else
+                {
+                    result.message = "转链失败";
+                }
                 result.channel = TkChannelEnum.tb;
                 result.success = success;
-                result.message = message;
                 //result.content = content;
                 return result;
             }
             catch (Exception ex)
             {
                 result.reason = "undefined";
-                result.message = "转链失败";
+                result.message = "fail";
                 return result;
             }
         }

+ 2 - 1
molilian.core/Plus/pdd/Crawler.cs

@@ -44,7 +44,8 @@ namespace molilian.core
             string cookies = _account.cookies.Trim();
 
 #if DEBUG
-            //cookies = "DDJB_PASS_ID=bedd7aa97aff1edfe392ad8d2c8f1dc5; DDJB_LOGIN_SCENE=0";
+            cookies = "_nano_fp=Xpm8nqXjl0djX5TYnC_i8vSD~GoSCs6EwSX~KQF3; api_uid=ZeIuHGnwfkePzgBjEGupAg==; jrpl=FYxQYhHDRjBKXQBaaFrEUyyNgiOx9FjB; njrpl=FYxQYhHDRjBKXQBaaFrEUyyNgiOx9FjB; dilx=u8i4bJTsT3tQaAKzP5jRQ; DDJB_PASS_ID=7752af5c1a8d94018b900b8c34d5a362; DDJB_LOGIN_SCENE=0";
+            _account.pid = "25613623_309553545";
 #endif
 
             string useragent = ProviderFakeUserAgent.RandomComputer;

Некоторые файлы не были показаны из-за большого количества измененных файлов