Przeglądaj źródła

🐞 fix(链路报表): 调整展示数据

dodo hold 1 miesiąc temu
rodzic
commit
029e8e148f

+ 89 - 0
molilian.api/Controllers/admin/TracksAdminController.cs

@@ -233,6 +233,95 @@ HAVING SUM(r.event_count)>0";
             });
         }
 
+        [HttpPost]
+        public async Task<ActionResult> parse_metrics([FromBody] JsonElement form)
+        {
+            var token = provider.Get(_accessor.HttpContext);
+            int page = form.Read("current", 1);
+            int size = form.Read("pageSize", 10);
+            bool getTotal = form.Read("getTotal", true);
+            int accountId = form.Read("account_id", -1);
+            bool accountBreakdownOnly = form.Read("account_breakdown_only", false);
+            string platform = form.Read("platform", string.Empty);
+            string riskStrategy = form.Read("risk_strategy", string.Empty);
+            int launchSceneValue = form.Read("launch_scene", int.MinValue);
+            int? launchScene = launchSceneValue == int.MinValue ? null : launchSceneValue;
+            DateTime? start = form.Read<DateTime?>("start", null);
+            DateTime? end = form.Read<DateTime?>("end", null);
+
+            DateTime startDate = start?.Date ?? DateTime.Now.Date;
+            DateTime endDate = end?.Date ?? startDate;
+            var result = await TracksCore.GetParseMetricReportAsync(
+                startDate,
+                endDate,
+                page,
+                size,
+                getTotal,
+                accountId,
+                accountBreakdownOnly,
+                platform,
+                riskStrategy,
+                launchScene);
+
+            return new APIResult(new { data = result });
+        }
+
+        [HttpPost]
+        public async Task<ActionResult> parse_metric_hourly([FromBody] JsonElement form)
+        {
+            var token = provider.Get(_accessor.HttpContext);
+            string dateText = form.Read("date", string.Empty);
+            string platform = form.Read("platform", string.Empty);
+            string riskStrategy = form.Read("risk_strategy", string.Empty);
+            int launchSceneValue = form.Read("launch_scene", int.MinValue);
+            int? launchScene = launchSceneValue == int.MinValue ? null : launchSceneValue;
+            int accountId = form.Read("account_id", 0);
+            DateTime date = DateTime.TryParse(dateText, out var parsed)
+                ? parsed.Date
+                : DateTime.Now.Date;
+
+            var list = await TracksCore.GetParseMetricHourlyReportAsync(
+                date,
+                platform,
+                riskStrategy,
+                launchScene,
+                accountId);
+
+            return new APIResult(new
+            {
+                data = new
+                {
+                    list,
+                    count = list.Count
+                }
+            });
+        }
+
+        [HttpPost]
+        public async Task<ActionResult> backfill_parse_metrics([FromBody] JsonElement form)
+        {
+            var token = provider.Get(_accessor.HttpContext);
+            string reportDateText = form.Read("reportDate", string.Empty);
+            string platform = form.Read("platform", string.Empty);
+            string riskStrategy = form.Read("risk_strategy", string.Empty);
+            int launchSceneValue = form.Read("launch_scene", int.MinValue);
+            int? launchScene = launchSceneValue == int.MinValue ? null : launchSceneValue;
+            DateTime targetDate = DateTime.TryParse(reportDateText, out var parsed)
+                ? parsed.Date
+                : DateTime.Now.Date;
+
+            var result = await TracksCore.BackfillParseMetricCountersAsync(targetDate, platform, riskStrategy, launchScene);
+            return new APIResult(new
+            {
+                data = new
+                {
+                    success = true,
+                    msg = "重新计算完成",
+                    result
+                }
+            });
+        }
+
         [HttpPost]
         public async Task<ActionResult> create([FromBody] TrackLinkDTO data)
         {

+ 34 - 12
molilian.api/Controllers/public/TestController.cs

@@ -100,18 +100,40 @@ namespace molilian.api.Controllers
          
 
         [HttpGet]
-        public async Task<ActionResult> testReconnectionRedis()
-        {
-            string cacheKey = "test";
-            RedisKit.SetAsync(cacheKey, 1, 3600);
-            string val = await RedisKit.GetAsync<string>(cacheKey);
-            return new APIResult(new { success = "ok", val });
-
-        }
-
-        [HttpGet]
-        public async Task<ActionResult> ChangePublicIpByName(string nodeName)
-        {
+        public async Task<ActionResult> testReconnectionRedis()
+        {
+            string cacheKey = "test";
+            RedisKit.SetAsync(cacheKey, 1, 3600);
+            string val = await RedisKit.GetAsync<string>(cacheKey);
+            return new APIResult(new { success = "ok", val });
+
+        }
+
+        [HttpGet]
+        public async Task<ActionResult> backfill_track_parse_metrics([FromQuery] string reportDate = "")
+        {
+            DateTime targetDate = DateTime.Now.Date;
+            if (!string.IsNullOrWhiteSpace(reportDate) && !DateTime.TryParse(reportDate, out targetDate))
+            {
+                return new APIResult(new
+                {
+                    success = false,
+                    message = "reportDate格式错误,请使用 yyyy-MM-dd"
+                });
+            }
+
+            var result = await TracksCore.BackfillParseMetricCountersAsync(targetDate);
+            return new APIResult(new
+            {
+                success = true,
+                message = "ok",
+                data = result
+            });
+        }
+
+        [HttpGet]
+        public async Task<ActionResult> ChangePublicIpByName(string nodeName)
+        {
 
             var proxy_node = new DBContext.Table("proxy_nodes").Get<dynamic>("nodeName=@nodeName", new { nodeName });
             if (proxy_node == null) return new APIResult(new { success = false, msg = "没有匹配的 proxy_nodes 记录" });

+ 80 - 61
molilian.api/Controllers/public/TkController.cs

@@ -42,15 +42,32 @@ namespace molilian.api.Controllers
             var commerceType = form.Read<int>("commerceType", 0);
             var riskStrategy = form.Read("riskStrategy", string.Empty);
             var launchScene = form.Read<int>("launchScene", -1);
-            var special_text = form.Read<int>("special_text", -1);
-            var query_text = form.Read("query_text", string.Empty);
-            var pic = form.Read("pic", string.Empty);
-
-
-            //验证签名
-            if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(sign))
-            {
-                return new APIResult(new { success = false, message = "验证错误" }, APIResultCodeEnum.Unauthorized);
+            var special_text = form.Read<int>("special_text", -1);
+            var query_text = form.Read("query_text", string.Empty);
+            var pic = form.Read("pic", string.Empty);
+
+            var request = new UnionParseRequest
+            {
+                Content = content,
+                Channel = channel,
+                CommerceType = commerceType,
+                Ip = ip,
+                Oaid = oaid,
+                RiskStrategy = riskStrategy,
+                LaunchScene = launchScene,
+                AccountId = 0,
+                SpecialText = special_text,
+                QueryText = query_text,
+                ClickId = clickId,
+                Type = type,
+                Pic = pic,
+            };
+            _ = TracksCore.RecordParseRequestAsync(request);
+
+            //验证签名
+            if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(sign))
+            {
+                return new APIResult(new { success = false, message = "验证错误" }, APIResultCodeEnum.Unauthorized);
             }
 
             DateTime time1 = t.Convert2Datetime();
@@ -65,31 +82,15 @@ namespace molilian.api.Controllers
                 return new APIResult(new { success = false, message = "验证错误3" }, APIResultCodeEnum.Unauthorized);
             }
             string app_sign = $"{account.api_secret}@{t}".MD5();
-            if (sign != app_sign)
-            {
-                return new APIResult(new { success = false, message = "验证错误4" }, APIResultCodeEnum.Unauthorized);
-            }
-            var request = new UnionParseRequest
-            {
-                Content = content,
-                Channel = channel,
-                CommerceType = commerceType,
-                Ip = ip,
-                Oaid = oaid,
-                RiskStrategy = riskStrategy,
-                LaunchScene = launchScene,
-                AccountId = 0,
-                SpecialText = special_text,
-                QueryText = query_text,
-                ClickId = clickId,
-                Type = type,
-                Pic = pic,
-            };
-            return await UnionParseCore.UnionParseAsync(request);
-        }
-
-        [HttpPost]
-        public async Task<ActionResult> unsafeParse([FromBody] JsonElement form)
+            if (sign != app_sign)
+            {
+                return new APIResult(new { success = false, message = "验证错误4" }, APIResultCodeEnum.Unauthorized);
+            }
+            return await UnionParseCore.UnionParseAsync(request);
+        }
+
+        [HttpPost]
+        public async Task<ActionResult> unsafeParse([FromBody] JsonElement form)
         {
             var content = form.Read("s", string.Empty);
             var channel = form.Read("c", string.Empty);
@@ -101,16 +102,16 @@ namespace molilian.api.Controllers
             var commerceType = form.Read<int>("commerceType", 0);
             var riskStrategy = form.Read("riskStrategy", string.Empty);
             var launchScene = form.Read<int>("launchScene", -1);
-            var special_text = form.Read<int>("special_text", -1);
-            var query_text = form.Read("query_text", string.Empty);
-            var pic = form.Read("pic", string.Empty);
-
-#if DEBUG
-            //ip = "127.0.0.1";
-            //oaid = "test-oaid";
-#endif
-            var request = new UnionParseRequest
-            {
+            var special_text = form.Read<int>("special_text", -1);
+            var query_text = form.Read("query_text", string.Empty);
+            var pic = form.Read("pic", string.Empty);
+
+#if DEBUG
+            //ip = "127.0.0.1";
+            //oaid = "test-oaid";
+#endif
+            var request = new UnionParseRequest
+            {
                 Content = content,
                 Channel = channel,
                 CommerceType = commerceType,
@@ -122,11 +123,12 @@ namespace molilian.api.Controllers
                 SpecialText = special_text,
                 QueryText = query_text,
                 ClickId = clickId,
-                Type = type,
-                Pic = pic,
-            };
-            return await UnionParseCore.UnionParseAsync(request);
-        }
+                Type = type,
+                Pic = pic,
+            };
+            _ = TracksCore.RecordParseRequestAsync(request);
+            return await UnionParseCore.UnionParseAsync(request);
+        }
 
 
         [HttpPost]
@@ -144,18 +146,35 @@ namespace molilian.api.Controllers
             //入参参数。brw-浏览器,qapp-快应用
             var riskStrategy = form.Read("riskStrategy", string.Empty);
             var launchScene = form.Read<int>("launchScene", -1);
-            var special_text = form.Read<int>("special_text", -1);
-            var query_text = form.Read("query_text", string.Empty);
-            var pic = form.Read("pic", string.Empty);
-
-#if DEBUG
-            //ip = "127.0.0.1";
-            //oaid = "test-oaid";
-#endif
-            for (int i = 0; i < count; i++)
-            {
-                var request = new UnionParseRequest
-                {
+            var special_text = form.Read<int>("special_text", -1);
+            var query_text = form.Read("query_text", string.Empty);
+            var pic = form.Read("pic", string.Empty);
+
+#if DEBUG
+            //ip = "127.0.0.1";
+            //oaid = "test-oaid";
+#endif
+            _ = TracksCore.RecordParseRequestAsync(new UnionParseRequest
+            {
+                Content = content.FirstOrDefault() ?? string.Empty,
+                Channel = channel,
+                CommerceType = commerceType,
+                Ip = ip,
+                Oaid = oaid,
+                RiskStrategy = riskStrategy,
+                LaunchScene = launchScene,
+                AccountId = accountid,
+                SpecialText = special_text,
+                QueryText = query_text,
+                ClickId = clickId,
+                Type = type,
+                Pic = pic,
+            });
+
+            for (int i = 0; i < count; i++)
+            {
+                var request = new UnionParseRequest
+                {
                     Content = content[i],
                     Channel = channel,
                     CommerceType = commerceType,

Plik diff jest za duży
+ 0 - 0
molilian.api/Properties/PublishProfiles/latest.pubxml.user


+ 29 - 17
molilian.core/Core/TracksCore.cs

@@ -318,25 +318,27 @@ ON DUPLICATE KEY UPDATE
                 if (accountId == 0 && !string.IsNullOrEmpty(metricScene) && metricScene == linkScene)
                 {
                     total += await RedisHelper.GetAsync<int>(BuildHourlyCountKey(eventType, trackId, hourStr, string.Empty));
-                }
-                int callCount = await GetTrackHourlyCallCountAsync(link, hourStr, accountId);
-
-                result.Add(new TrackHourlyReportDTO
-                {
+                }
+                int callCount = await GetTrackHourlyCallCountAsync(link, hourStr, accountId);
+                int successCount = await GetTrackHourlySuccessCountAsync(link, hourStr, accountId);
+
+                result.Add(new TrackHourlyReportDTO
+                {
                     track_link_id = trackId,
                     event_type = eventType,
                     platform = link.platform ?? string.Empty,
                     typename = link.typename ?? string.Empty,
                     scene = metricScene,
                     unique_id = link.unique_id ?? string.Empty,
-                    account_id = accountId,
-                    report_date = targetDate.Date,
-                    hour = hour,
-                    event_count = total,
-                    call_count = callCount
-                });
-            }
-
+                    account_id = accountId,
+                    report_date = targetDate.Date,
+                    hour = hour,
+                    event_count = total,
+                    call_count = callCount,
+                    success_count = successCount
+                });
+            }
+
             return result;
         }
 
@@ -346,10 +348,20 @@ ON DUPLICATE KEY UPDATE
 
             string channelName = GetTrackCallChannelName(link);
             if (string.IsNullOrEmpty(channelName)) return 0;
-
-            return await TkLogCore.GetTotalAsync($":parse_total:{channelName}_{accountId}:{hourStr}");
-        }
-
+
+            return await TkLogCore.GetTotalAsync($":parse_total:{channelName}_{accountId}:{hourStr}");
+        }
+
+        private static async Task<int> GetTrackHourlySuccessCountAsync(TrackLinkDTO link, string hourStr, int accountId)
+        {
+            if (accountId <= 0) return 0;
+
+            string channelName = GetTrackCallChannelName(link);
+            if (string.IsNullOrEmpty(channelName)) return 0;
+
+            return await TkLogCore.GetTotalAsync($":parse_total:{channelName}_{accountId}:success:{hourStr}");
+        }
+
         private static string GetTrackCallChannelName(TrackLinkDTO link)
         {
             string platform = Normalize(link.platform).ToLowerInvariant();

+ 798 - 0
molilian.core/Core/TracksParseMetricCore.cs

@@ -0,0 +1,798 @@
+using System.Collections.Concurrent;
+using System.Data;
+using System.Threading;
+using Dapper;
+using dodohold.core;
+using YunhuiKit;
+
+namespace molilian.core
+{
+    public partial class TracksCore
+    {
+        private const string ParseMetricTypeName = "转链";
+        private const string ParseMetricRequest = "request";
+        private const string ParseMetricCall = "call";
+        private const string ParseMetricSuccess = "success";
+        private const int ParseMetricFlushIntervalMs = 1000;
+
+        private static readonly ConcurrentDictionary<ParseMetricCounterKey, ParseMetricCounter> PendingParseMetricCounters = new();
+        private static readonly object ParseMetricFlushTimerLock = new();
+        private static Timer? ParseMetricFlushTimer;
+        private static int ParseMetricFlushRunning;
+        private static int ParseMetricLifecycleHooked;
+
+        public static Task<bool> RecordParseRequestAsync(UnionParseRequest request)
+        {
+            var dimension = ResolveParseMetricDimension(request.Channel, request.RiskStrategy, request.LaunchScene);
+            return IncrementParseMetricAsync(ParseMetricRequest, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, request.AccountId);
+        }
+
+        public static Task<bool> RecordParseAccountRequestAsync(string channel, string riskStrategy, int launchScene, int accountId)
+        {
+            if (accountId <= 0) return Task.FromResult(false);
+
+            var dimension = ResolveParseMetricDimension(channel, riskStrategy, launchScene);
+            return IncrementParseAccountMetricAsync(ParseMetricRequest, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, accountId);
+        }
+
+        public static Task<bool> RecordParseResultAsync(string channel, string riskStrategy, int launchScene, int accountId, bool success)
+        {
+            var dimension = ResolveParseMetricDimension(channel, riskStrategy, launchScene);
+            return IncrementParseResultMetricAsync(dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, accountId, success);
+        }
+
+        public static async Task<TrackParseMetricReportResult> GetParseMetricReportAsync(
+            DateTime start,
+            DateTime end,
+            int page,
+            int size,
+            bool getTotal,
+            int accountId = -1,
+            bool accountBreakdownOnly = false,
+            string platform = "",
+            string riskStrategy = "",
+            int? launchScene = null)
+        {
+            start = start.Date;
+            end = end.Date;
+            if (end < start) end = start;
+            page = Math.Max(page, 1);
+            size = Math.Max(size, 1);
+
+            var list = new List<TrackParseMetricReportDTO>();
+            for (var date = start; date <= end; date = date.AddDays(1))
+            {
+                string dateStr = date.ToString("yyyyMMdd");
+                string indexKey = BuildParseMetricIndexKey("daily", dateStr);
+                string[] members = await TkLogCore.GetTotalKeysAsync(indexKey) ?? [];
+
+                foreach (var member in members)
+                {
+                    if (!TryParseParseMetricIndexValue(member, out var dimension)) continue;
+                    if (!IsMatchedParseMetricDimension(dimension, accountId, accountBreakdownOnly, platform, riskStrategy, launchScene)) continue;
+
+                    var requestTask = TkLogCore.GetTotalAsync(BuildParseMetricCountKey(ParseMetricRequest, "daily", dateStr, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, dimension.AccountId));
+                    var callTask = TkLogCore.GetTotalAsync(BuildParseMetricCountKey(ParseMetricCall, "daily", dateStr, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, dimension.AccountId));
+                    var successTask = TkLogCore.GetTotalAsync(BuildParseMetricCountKey(ParseMetricSuccess, "daily", dateStr, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, dimension.AccountId));
+                    await Task.WhenAll(requestTask, callTask, successTask);
+
+                    int requestCount = requestTask.Result;
+                    int callCount = callTask.Result;
+                    int successCount = successTask.Result;
+                    if (requestCount <= 0 && callCount <= 0 && successCount <= 0) continue;
+
+                    list.Add(new TrackParseMetricReportDTO
+                    {
+                        report_date = date,
+                        platform = dimension.Platform,
+                        typename = ParseMetricTypeName,
+                        risk_strategy = dimension.RiskStrategy,
+                        launch_scene = dimension.LaunchScene,
+                        account_id = dimension.AccountId,
+                        request_count = requestCount,
+                        call_count = callCount,
+                        success_count = successCount
+                    });
+                }
+            }
+
+            list = list
+                .OrderByDescending(item => item.report_date)
+                .ThenBy(item => item.platform)
+                .ThenBy(item => item.risk_strategy)
+                .ThenBy(item => item.launch_scene)
+                .ThenBy(item => item.account_id)
+                .ToList();
+
+            int count = list.Count;
+            list = list
+                .Skip((page - 1) * size)
+                .Take(size)
+                .ToList();
+
+            return new TrackParseMetricReportResult
+            {
+                list = list,
+                count = getTotal ? count : list.Count
+            };
+        }
+
+        public static async Task<List<TrackParseMetricHourlyDTO>> GetParseMetricHourlyReportAsync(
+            DateTime targetDate,
+            string platform,
+            string riskStrategy,
+            int? launchScene = null,
+            int accountId = 0)
+        {
+            targetDate = targetDate.Date;
+            platform = NormalizeParseMetricPlatform(platform);
+            riskStrategy = NormalizeReportScene(riskStrategy);
+            accountId = Math.Max(accountId, 0);
+
+            var list = new List<TrackParseMetricHourlyDTO>();
+            string dateStr = targetDate.ToString("yyyyMMdd");
+            for (int hour = 0; hour < 24; hour++)
+            {
+                string hourStr = $"{dateStr}{hour:00}";
+                string indexKey = BuildParseMetricIndexKey("hour", hourStr);
+                string[] members = await TkLogCore.GetTotalKeysAsync(indexKey) ?? [];
+                long requestCount = 0;
+                long callCount = 0;
+                long successCount = 0;
+
+                foreach (var member in members)
+                {
+                    if (!TryParseParseMetricIndexValue(member, out var dimension)) continue;
+                    if (!IsMatchedParseMetricDimension(dimension, accountId, false, platform, riskStrategy, launchScene)) continue;
+
+                    var requestTask = TkLogCore.GetTotalAsync(BuildParseMetricCountKey(ParseMetricRequest, "hour", hourStr, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, dimension.AccountId));
+                    var callTask = TkLogCore.GetTotalAsync(BuildParseMetricCountKey(ParseMetricCall, "hour", hourStr, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, dimension.AccountId));
+                    var successTask = TkLogCore.GetTotalAsync(BuildParseMetricCountKey(ParseMetricSuccess, "hour", hourStr, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, dimension.AccountId));
+                    await Task.WhenAll(requestTask, callTask, successTask);
+
+                    requestCount += requestTask.Result;
+                    callCount += callTask.Result;
+                    successCount += successTask.Result;
+                }
+
+                list.Add(new TrackParseMetricHourlyDTO
+                {
+                    report_date = targetDate,
+                    platform = platform,
+                    typename = ParseMetricTypeName,
+                    risk_strategy = riskStrategy,
+                    launch_scene = launchScene ?? -1,
+                    account_id = accountId,
+                    hour = hour,
+                    request_count = SafeInt(requestCount),
+                    call_count = SafeInt(callCount),
+                    success_count = SafeInt(successCount)
+                });
+            }
+
+            return list;
+        }
+
+        public static async Task<TrackParseMetricBackfillResult> BackfillParseMetricCountersAsync(
+            DateTime targetDate,
+            string platform = "",
+            string riskStrategy = "",
+            int? launchScene = null)
+        {
+            targetDate = targetDate.Date;
+            platform = NormalizeParseMetricPlatform(platform);
+            riskStrategy = NormalizeReportScene(riskStrategy);
+            bool hasPlatformFilter = !string.IsNullOrWhiteSpace(platform);
+            bool hasRiskStrategyFilter = !string.IsNullOrWhiteSpace(riskStrategy);
+            bool hasLaunchSceneFilter = launchScene.HasValue;
+            bool hasDimensionFilter = hasPlatformFilter || hasRiskStrategyFilter || hasLaunchSceneFilter;
+            var result = new TrackParseMetricBackfillResult
+            {
+                report_date = targetDate.ToString("yyyy-MM-dd")
+            };
+
+            var allSources = new[]
+            {
+                new ParseMetricBackfillSource("tb", $"tk_parse_logs_{targetDate:yyyyMMdd}"),
+                new ParseMetricBackfillSource("jd", $"jd_parse_logs_{targetDate:yyyyMMdd}"),
+                new ParseMetricBackfillSource("pdd", $"pdd_parse_logs_{targetDate:yyyyMMdd}")
+            };
+            var sources = hasDimensionFilter
+                ? allSources.Where(source => !hasPlatformFilter || string.Equals(source.Platform, platform, StringComparison.OrdinalIgnoreCase)).ToArray()
+                : allSources;
+
+            using var conn = DBContext.GetOpenConnection();
+            DateTime start = targetDate;
+            DateTime end = targetDate.AddDays(1);
+            result.redis_cleared_keys = hasDimensionFilter
+                ? await ClearParseMetricCountersForDimensionAsync(targetDate, platform, riskStrategy, launchScene)
+                : await ClearParseMetricCountersForDateAsync(targetDate);
+
+            foreach (var source in sources)
+            {
+                if (!TableExists(conn, source.TableName))
+                {
+                    result.details.Add(new { platform = source.Platform, table = source.TableName, exists = false, rows = 0, dimensions = 0 });
+                    continue;
+                }
+
+                const string riskStrategySql = "COALESCE(NULLIF(TRIM(riskStrategy), ''), '')";
+                const string rawLaunchSceneSql = "COALESCE(launchScene, -1)";
+                string launchSceneSql = $"CASE WHEN LOWER({riskStrategySql}) IN ('os','tbpush','brwsimilar','icon') AND {rawLaunchSceneSql}=-1 THEN 0 ELSE {rawLaunchSceneSql} END";
+                var dimensionWhereItems = new List<string>();
+                if (hasRiskStrategyFilter) dimensionWhereItems.Add($"{riskStrategySql}=@riskStrategy");
+                if (hasLaunchSceneFilter) dimensionWhereItems.Add($"{launchSceneSql}=@launchScene");
+                string dimensionWhere = dimensionWhereItems.Count > 0
+                    ? "\n    AND " + string.Join("\n    AND ", dimensionWhereItems)
+                    : string.Empty;
+                string sql = $@"
+SELECT
+    {riskStrategySql} AS risk_strategy,
+    {launchSceneSql} AS launch_scene,
+    COALESCE(accountId, 0) AS account_id,
+    DATE_FORMAT(create_time, '%Y%m%d%H') AS hour_key,
+    CAST(COUNT(1) AS SIGNED) AS call_count,
+    CAST(SUM(CASE WHEN success=1 THEN 1 ELSE 0 END) AS SIGNED) AS success_count
+FROM `{source.TableName}`
+WHERE create_time>=@start AND create_time<@end
+{dimensionWhere}
+GROUP BY
+    {riskStrategySql},
+    {launchSceneSql},
+    COALESCE(accountId, 0),
+    DATE_FORMAT(create_time, '%Y%m%d%H')";
+
+                var args = new DynamicParameters();
+                args.Add("start", start);
+                args.Add("end", end);
+                if (hasRiskStrategyFilter) args.Add("riskStrategy", riskStrategy);
+                if (hasLaunchSceneFilter) args.Add("launchScene", launchScene!.Value);
+
+                var rows = SqlMapper.Query<ParseMetricBackfillRow>(conn, sql, args).ToList();
+                int mysqlRows = rows.Sum(row => SafeInt(row.call_count));
+                int dimensions = ApplyParseMetricBackfillRows(source.Platform, targetDate, rows);
+
+                result.mysql_rows += mysqlRows;
+                result.redis_dimensions += dimensions;
+                result.details.Add(new { platform = source.Platform, table = source.TableName, exists = true, rows = mysqlRows, dimensions });
+            }
+
+            await Task.CompletedTask;
+            return result;
+        }
+
+        private static Task<bool> IncrementParseMetricAsync(string metric, string platform, string riskStrategy, int launchScene, int accountId = 0)
+        {
+            try
+            {
+                AddParseMetricCount(metric, DateTime.Now, platform, riskStrategy, launchScene, 0, 1);
+                if (accountId > 0)
+                {
+                    AddParseMetricCount(metric, DateTime.Now, platform, riskStrategy, launchScene, accountId, 1);
+                }
+                return Task.FromResult(true);
+            }
+            catch (Exception ex)
+            {
+                _ = new LoggerLibrary("TracksCore", "RecordParseMetric")
+                    .Info(ex.Message, ex.StackTrace)
+                    .SaveAsync();
+                return Task.FromResult(false);
+            }
+        }
+
+        private static Task<bool> IncrementParseAccountMetricAsync(string metric, string platform, string riskStrategy, int launchScene, int accountId)
+        {
+            try
+            {
+                accountId = Math.Max(accountId, 0);
+                if (accountId <= 0) return Task.FromResult(false);
+
+                AddParseMetricCount(metric, DateTime.Now, platform, riskStrategy, launchScene, accountId, 1);
+                return Task.FromResult(true);
+            }
+            catch (Exception ex)
+            {
+                _ = new LoggerLibrary("TracksCore", "RecordParseAccountMetric")
+                    .Info(ex.Message, ex.StackTrace)
+                    .SaveAsync();
+                return Task.FromResult(false);
+            }
+        }
+
+        private static Task<bool> IncrementParseResultMetricAsync(string platform, string riskStrategy, int launchScene, int accountId, bool success)
+        {
+            try
+            {
+                accountId = Math.Max(accountId, 0);
+
+                AddParseMetricCount(ParseMetricCall, DateTime.Now, platform, riskStrategy, launchScene, 0, 1);
+                if (success) AddParseMetricCount(ParseMetricSuccess, DateTime.Now, platform, riskStrategy, launchScene, 0, 1);
+
+                if (accountId > 0)
+                {
+                    AddParseMetricCount(ParseMetricCall, DateTime.Now, platform, riskStrategy, launchScene, accountId, 1);
+                    if (success) AddParseMetricCount(ParseMetricSuccess, DateTime.Now, platform, riskStrategy, launchScene, accountId, 1);
+                }
+
+                return Task.FromResult(true);
+            }
+            catch (Exception ex)
+            {
+                _ = new LoggerLibrary("TracksCore", "RecordParseResultMetric")
+                    .Info(ex.Message, ex.StackTrace)
+                    .SaveAsync();
+                return Task.FromResult(false);
+            }
+        }
+
+        private static int ApplyParseMetricBackfillRows(string platform, DateTime targetDate, List<ParseMetricBackfillRow> rows)
+        {
+            string dateStr = targetDate.ToString("yyyyMMdd");
+            int dimensions = 0;
+
+            var dailyGroups = rows.GroupBy(row => new
+            {
+                RiskStrategy = NormalizeReportScene(row.risk_strategy),
+                LaunchScene = NormalizeParseMetricLaunchScene(row.risk_strategy, row.launch_scene)
+            });
+
+            foreach (var group in dailyGroups)
+            {
+                int callCount = group.Sum(row => SafeInt(row.call_count));
+                int successCount = group.Sum(row => SafeInt(row.success_count));
+                SetParseMetricCount(ParseMetricCall, "daily", dateStr, platform, group.Key.RiskStrategy, group.Key.LaunchScene, 0, callCount);
+                SetParseMetricCount(ParseMetricSuccess, "daily", dateStr, platform, group.Key.RiskStrategy, group.Key.LaunchScene, 0, successCount);
+                dimensions++;
+            }
+
+            var dailyAccountGroups = rows
+                .Where(row => row.account_id > 0)
+                .GroupBy(row => new
+                {
+                    RiskStrategy = NormalizeReportScene(row.risk_strategy),
+                    launch_scene = NormalizeParseMetricLaunchScene(row.risk_strategy, row.launch_scene),
+                    row.account_id
+                });
+
+            foreach (var group in dailyAccountGroups)
+            {
+                int callCount = group.Sum(row => SafeInt(row.call_count));
+                int successCount = group.Sum(row => SafeInt(row.success_count));
+                SetParseMetricCount(ParseMetricRequest, "daily", dateStr, platform, group.Key.RiskStrategy, group.Key.launch_scene, group.Key.account_id, callCount);
+                SetParseMetricCount(ParseMetricCall, "daily", dateStr, platform, group.Key.RiskStrategy, group.Key.launch_scene, group.Key.account_id, callCount);
+                SetParseMetricCount(ParseMetricSuccess, "daily", dateStr, platform, group.Key.RiskStrategy, group.Key.launch_scene, group.Key.account_id, successCount);
+                dimensions++;
+            }
+
+            var hourlyGroups = rows.GroupBy(row => new
+            {
+                RiskStrategy = NormalizeReportScene(row.risk_strategy),
+                launch_scene = NormalizeParseMetricLaunchScene(row.risk_strategy, row.launch_scene),
+                row.hour_key
+            });
+
+            foreach (var group in hourlyGroups)
+            {
+                int callCount = group.Sum(row => SafeInt(row.call_count));
+                int successCount = group.Sum(row => SafeInt(row.success_count));
+                SetParseMetricCount(ParseMetricCall, "hour", group.Key.hour_key, platform, group.Key.RiskStrategy, group.Key.launch_scene, 0, callCount);
+                SetParseMetricCount(ParseMetricSuccess, "hour", group.Key.hour_key, platform, group.Key.RiskStrategy, group.Key.launch_scene, 0, successCount);
+            }
+
+            var hourlyAccountGroups = rows
+                .Where(row => row.account_id > 0)
+                .GroupBy(row => new
+                {
+                    RiskStrategy = NormalizeReportScene(row.risk_strategy),
+                    launch_scene = NormalizeParseMetricLaunchScene(row.risk_strategy, row.launch_scene),
+                    row.account_id,
+                    row.hour_key
+                });
+
+            foreach (var group in hourlyAccountGroups)
+            {
+                int callCount = group.Sum(row => SafeInt(row.call_count));
+                int successCount = group.Sum(row => SafeInt(row.success_count));
+                SetParseMetricCount(ParseMetricRequest, "hour", group.Key.hour_key, platform, group.Key.RiskStrategy, group.Key.launch_scene, group.Key.account_id, callCount);
+                SetParseMetricCount(ParseMetricCall, "hour", group.Key.hour_key, platform, group.Key.RiskStrategy, group.Key.launch_scene, group.Key.account_id, callCount);
+                SetParseMetricCount(ParseMetricSuccess, "hour", group.Key.hour_key, platform, group.Key.RiskStrategy, group.Key.launch_scene, group.Key.account_id, successCount);
+            }
+
+            return dimensions;
+        }
+
+        private static void AddParseMetricCount(string metric, DateTime metricTime, string platform, string riskStrategy, int launchScene, int accountId, int count)
+        {
+            if (count <= 0) return;
+
+            QueueParseMetricCount(metric, "daily", metricTime.ToString("yyyyMMdd"), platform, riskStrategy, launchScene, accountId, count);
+            QueueParseMetricCount(metric, "hour", metricTime.ToString("yyyyMMddHH"), platform, riskStrategy, launchScene, accountId, count);
+        }
+
+        private static void SetParseMetricCount(string metric, string bucketType, string bucketValue, string platform, string riskStrategy, int launchScene, int accountId, int count)
+        {
+            if (count < 0) return;
+            SetOrIncrementParseMetricCount(metric, bucketType, bucketValue, platform, riskStrategy, launchScene, accountId, count, increment: false);
+        }
+
+        private static void SetOrIncrementParseMetricCount(string metric, string bucketType, string bucketValue, string platform, string riskStrategy, int launchScene, int accountId, long count, bool increment)
+        {
+            platform = Normalize(platform).ToLowerInvariant();
+            riskStrategy = NormalizeReportScene(riskStrategy);
+            launchScene = NormalizeParseMetricLaunchScene(riskStrategy, launchScene);
+            accountId = Math.Max(accountId, 0);
+
+            string countKey = BuildParseMetricCountKey(metric, bucketType, bucketValue, platform, riskStrategy, launchScene, accountId);
+            string indexKey = BuildParseMetricIndexKey(bucketType, bucketValue);
+            string indexValue = BuildParseMetricIndexValue(platform, riskStrategy, launchScene, accountId);
+            int expireSeconds = bucketType == "hour" ? HourlyExpireSeconds : DailyExpireSeconds;
+
+            if (increment)
+            {
+                IncrementParseMetricRedisKey(countKey, count);
+            }
+            else
+            {
+                RedisHelper.Set(countKey, count, expireSeconds);
+            }
+            RedisHelper.Expire(countKey, expireSeconds);
+            RedisHelper.SAdd(indexKey, indexValue);
+            RedisHelper.Expire(indexKey, expireSeconds);
+        }
+
+        public static int FlushParseMetricCounters()
+        {
+            if (Interlocked.Exchange(ref ParseMetricFlushRunning, 1) == 1) return 0;
+
+            try
+            {
+                int flushed = 0;
+                foreach (var item in PendingParseMetricCounters.ToArray())
+                {
+                    long count = Interlocked.Exchange(ref item.Value.Count, 0);
+                    if (count <= 0) continue;
+
+                    try
+                    {
+                        SetOrIncrementParseMetricCount(
+                            item.Key.Metric,
+                            item.Key.BucketType,
+                            item.Key.BucketValue,
+                            item.Key.Platform,
+                            item.Key.RiskStrategy,
+                            item.Key.LaunchScene,
+                            item.Key.AccountId,
+                            count,
+                            increment: true);
+                        flushed++;
+                    }
+                    catch (Exception ex)
+                    {
+                        Interlocked.Add(ref item.Value.Count, count);
+                        _ = new LoggerLibrary("TracksCore", "FlushParseMetricCounters")
+                            .Info(ex.Message, ex.StackTrace)
+                            .SaveAsync();
+                    }
+                }
+                return flushed;
+            }
+            finally
+            {
+                Interlocked.Exchange(ref ParseMetricFlushRunning, 0);
+            }
+        }
+
+        private static void QueueParseMetricCount(string metric, string bucketType, string bucketValue, string platform, string riskStrategy, int launchScene, int accountId, int count)
+        {
+            if (count <= 0) return;
+
+            var key = new ParseMetricCounterKey(
+                metric,
+                bucketType,
+                bucketValue,
+                Normalize(platform).ToLowerInvariant(),
+                NormalizeReportScene(riskStrategy),
+                NormalizeParseMetricLaunchScene(riskStrategy, launchScene),
+                Math.Max(accountId, 0));
+            var counter = PendingParseMetricCounters.GetOrAdd(key, _ => new ParseMetricCounter());
+            Interlocked.Add(ref counter.Count, count);
+            EnsureParseMetricFlushTimer();
+        }
+
+        private static void EnsureParseMetricFlushTimer()
+        {
+            if (ParseMetricFlushTimer != null) return;
+
+            lock (ParseMetricFlushTimerLock)
+            {
+                if (ParseMetricFlushTimer != null) return;
+
+                ParseMetricFlushTimer = new Timer(
+                    _ => FlushParseMetricCounters(),
+                    null,
+                    ParseMetricFlushIntervalMs,
+                    ParseMetricFlushIntervalMs);
+
+                if (Interlocked.Exchange(ref ParseMetricLifecycleHooked, 1) == 0)
+                {
+                    AppDomain.CurrentDomain.ProcessExit += (_, _) => FlushParseMetricCounters();
+                    AppDomain.CurrentDomain.UnhandledException += (_, _) => FlushParseMetricCounters();
+                }
+            }
+        }
+
+        private static void IncrementParseMetricRedisKey(string key, long count)
+        {
+            while (count > 0)
+            {
+                int delta = count > int.MaxValue ? int.MaxValue : (int)count;
+                RedisHelper.IncrBy(key, delta);
+                count -= delta;
+            }
+        }
+
+        private static async Task<int> ClearParseMetricCountersForDateAsync(DateTime targetDate)
+        {
+            string dateStr = targetDate.ToString("yyyyMMdd");
+            int cleared = await ClearParseMetricBucketAsync("daily", dateStr);
+
+            for (int hour = 0; hour < 24; hour++)
+            {
+                cleared += await ClearParseMetricBucketAsync("hour", $"{dateStr}{hour:00}");
+            }
+
+            return cleared;
+        }
+
+        private static async Task<int> ClearParseMetricCountersForDimensionAsync(
+            DateTime targetDate,
+            string platform,
+            string riskStrategy,
+            int? launchScene)
+        {
+            string dateStr = targetDate.ToString("yyyyMMdd");
+            int cleared = await ClearParseMetricBucketAsync(
+                "daily",
+                dateStr,
+                dimension => IsSameParseMetricDimension(dimension, platform, riskStrategy, launchScene));
+
+            for (int hour = 0; hour < 24; hour++)
+            {
+                cleared += await ClearParseMetricBucketAsync(
+                    "hour",
+                    $"{dateStr}{hour:00}",
+                    dimension => IsSameParseMetricDimension(dimension, platform, riskStrategy, launchScene));
+            }
+
+            return cleared;
+        }
+
+        private static async Task<int> ClearParseMetricBucketAsync(
+            string bucketType,
+            string bucketValue,
+            Func<ParseMetricDimension, bool>? predicate = null)
+        {
+            string indexKey = BuildParseMetricIndexKey(bucketType, bucketValue);
+            string[] members = await TkLogCore.GetTotalKeysAsync(indexKey) ?? [];
+            int cleared = 0;
+
+            foreach (var member in members)
+            {
+                if (!TryParseParseMetricIndexValue(member, out var dimension)) continue;
+                if (predicate != null && !predicate(dimension)) continue;
+
+                RedisHelper.Del(BuildParseMetricCountKey(ParseMetricCall, bucketType, bucketValue, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, dimension.AccountId));
+                RedisHelper.Del(BuildParseMetricCountKey(ParseMetricSuccess, bucketType, bucketValue, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, dimension.AccountId));
+                cleared += 2;
+                if (dimension.AccountId > 0)
+                {
+                    RedisHelper.Del(BuildParseMetricCountKey(ParseMetricRequest, bucketType, bucketValue, dimension.Platform, dimension.RiskStrategy, dimension.LaunchScene, dimension.AccountId));
+                    cleared++;
+                }
+            }
+            return cleared;
+        }
+
+        private static string BuildParseMetricCountKey(string metric, string bucketType, string bucketValue, string platform, string riskStrategy, int launchScene, int accountId = 0)
+        {
+            string key = $"{RedisPrefix}:parse_metric:{bucketType}:{metric}:{bucketValue}:{EncodeIndexPart(platform)}:{EncodeIndexPart(riskStrategy)}:{launchScene}";
+            if (accountId > 0) key += $":account:{accountId}";
+            return key;
+        }
+
+        private static string BuildParseMetricIndexKey(string bucketType, string bucketValue)
+        {
+            return $"{RedisPrefix}:parse_metric:{bucketType}:index:{bucketValue}";
+        }
+
+        private static string BuildParseMetricIndexValue(string platform, string riskStrategy, int launchScene, int accountId = 0)
+        {
+            string value = $"{EncodeIndexPart(platform)}|{EncodeIndexPart(riskStrategy)}|{launchScene}";
+            if (accountId > 0) value += $"|{accountId}";
+            return value;
+        }
+
+        private static bool TryParseParseMetricIndexValue(string value, out ParseMetricDimension dimension)
+        {
+            dimension = new ParseMetricDimension();
+            var parts = (value ?? string.Empty).Split('|');
+            if (parts.Length < 3 || parts.Length > 4) return false;
+            if (!int.TryParse(parts[2], out int launchScene)) return false;
+
+            int accountId = 0;
+            if (parts.Length == 4 && (!int.TryParse(parts[3], out accountId) || accountId <= 0)) return false;
+
+            dimension = new ParseMetricDimension
+            {
+                Platform = DecodeIndexPart(parts[0]),
+                RiskStrategy = DecodeIndexPart(parts[1]),
+                LaunchScene = launchScene,
+                AccountId = accountId
+            };
+            return true;
+        }
+
+        private static bool IsMatchedParseMetricDimension(
+            ParseMetricDimension dimension,
+            int accountId,
+            bool accountBreakdownOnly,
+            string platform,
+            string riskStrategy,
+            int? launchScene)
+        {
+            if (accountBreakdownOnly)
+            {
+                if (dimension.AccountId <= 0) return false;
+            }
+            else if (accountId > 0)
+            {
+                if (dimension.AccountId != accountId) return false;
+            }
+            else if (dimension.AccountId > 0)
+            {
+                return false;
+            }
+
+            if (!string.IsNullOrWhiteSpace(platform) && !string.Equals(NormalizeParseMetricPlatform(platform), dimension.Platform, StringComparison.OrdinalIgnoreCase)) return false;
+            if (!string.IsNullOrWhiteSpace(riskStrategy) && !string.Equals(NormalizeReportScene(riskStrategy), dimension.RiskStrategy, StringComparison.OrdinalIgnoreCase)) return false;
+            if (launchScene.HasValue && NormalizeParseMetricLaunchScene(dimension.RiskStrategy, launchScene.Value) != NormalizeParseMetricLaunchScene(dimension.RiskStrategy, dimension.LaunchScene)) return false;
+
+            return true;
+        }
+
+        private static bool IsSameParseMetricDimension(
+            ParseMetricDimension dimension,
+            string platform,
+            string riskStrategy,
+            int? launchScene)
+        {
+            if (!string.IsNullOrWhiteSpace(platform)
+                && !string.Equals(NormalizeParseMetricPlatform(platform), dimension.Platform, StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            if (!string.IsNullOrWhiteSpace(riskStrategy)
+                && !string.Equals(NormalizeReportScene(riskStrategy), dimension.RiskStrategy, StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            if (launchScene.HasValue
+                && NormalizeParseMetricLaunchScene(dimension.RiskStrategy, launchScene.Value) != NormalizeParseMetricLaunchScene(dimension.RiskStrategy, dimension.LaunchScene))
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        private static ParseMetricDimension ResolveParseMetricDimension(string channel, string riskStrategy, int launchScene)
+        {
+            channel = Normalize(channel).ToLowerInvariant();
+            riskStrategy = NormalizeReportScene(riskStrategy);
+
+            switch (channel)
+            {
+                case "tbpush":
+                case "brwsimilar":
+                case "icon":
+                    riskStrategy = channel;
+                    launchScene = 0;
+                    channel = "tb";
+                    break;
+            }
+
+            switch (riskStrategy)
+            {
+                case "tbpush":
+                case "brwsimilar":
+                case "icon":
+                    if (launchScene == -1) launchScene = 0;
+                    break;
+            }
+            launchScene = NormalizeParseMetricLaunchScene(riskStrategy, launchScene);
+
+            return new ParseMetricDimension
+            {
+                Platform = channel,
+                RiskStrategy = riskStrategy,
+                LaunchScene = launchScene,
+                AccountId = 0
+            };
+        }
+
+        private static int NormalizeParseMetricLaunchScene(string riskStrategy, int launchScene)
+        {
+            if (launchScene != -1) return launchScene;
+
+            return NormalizeReportScene(riskStrategy).ToLowerInvariant() switch
+            {
+                "os" or "tbpush" or "brwsimilar" or "icon" => 0,
+                _ => launchScene
+            };
+        }
+
+        private static string NormalizeParseMetricPlatform(string platform)
+        {
+            string value = Normalize(platform).ToLowerInvariant();
+            return value switch
+            {
+                "淘宝" or "taobao" or "tb" or "1" => "tb",
+                "京东" or "jd" or "13" => "jd",
+                "拼多多" or "pdd" or "9" => "pdd",
+                _ => value
+            };
+        }
+
+        private static bool TableExists(IDbConnection conn, string tableName)
+        {
+            const string sql = @"
+SELECT COUNT(1)
+FROM information_schema.TABLES
+WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=@tableName";
+            return conn.ExecuteScalar<int>(sql, new { tableName }) > 0;
+        }
+
+        private static int SafeInt(long value)
+        {
+            if (value <= 0) return 0;
+            return value > int.MaxValue ? int.MaxValue : (int)value;
+        }
+
+        private sealed record ParseMetricCounterKey(
+            string Metric,
+            string BucketType,
+            string BucketValue,
+            string Platform,
+            string RiskStrategy,
+            int LaunchScene,
+            int AccountId);
+
+        private sealed class ParseMetricCounter
+        {
+            public long Count = 0;
+        }
+
+        private sealed class ParseMetricDimension
+        {
+            public string Platform { get; set; } = string.Empty;
+            public string RiskStrategy { get; set; } = string.Empty;
+            public int LaunchScene { get; set; } = -1;
+            public int AccountId { get; set; } = 0;
+        }
+
+        private sealed record ParseMetricBackfillSource(string Platform, string TableName);
+
+        private sealed class ParseMetricBackfillRow
+        {
+            public string risk_strategy { get; set; } = string.Empty;
+            public int launch_scene { get; set; } = -1;
+            public int account_id { get; set; } = 0;
+            public string hour_key { get; set; } = string.Empty;
+            public long call_count { get; set; } = 0;
+            public long success_count { get; set; } = 0;
+        }
+    }
+}

+ 9 - 8
molilian.core/Core/log/jd.cs

@@ -141,14 +141,15 @@ namespace molilian.core
                 response.elapsedTime = (int)ts.TotalMilliseconds;
                 _ = RedisKit.RPushAsync(queue_parse_jd_key, response);
 
-                if (!TestParseCore.InWhitelist(response.ip, response.oaid))
-                {
-                    await SaveParseCacheAsync(response.channel.ToString(), response.accountId,
-                        response.accountName, response.success, response.message, response.reason,
-                        response.deeplink_url);
-                }
-
-                if (response.success) await saveClientRequestTotalAsync(response.channel, response.ip, response.oaid);
+                if (!TestParseCore.InWhitelist(response.ip, response.oaid))
+                {
+                    await SaveParseCacheAsync(response.channel.ToString(), response.accountId,
+                        response.accountName, response.success, response.message, response.reason,
+                        response.deeplink_url);
+                    await TracksCore.RecordParseResultAsync(response.channel.ToString(), response.riskStrategy, response.launchScene, response.accountId, response.success);
+                }
+
+                if (response.success) await saveClientRequestTotalAsync(response.channel, response.ip, response.oaid);
                 if (response.success || response.message.Equals("转链失败"))
                 {
                     await RiskControlCore.CallsIncrByAsync(response.channel, response.accountId);

+ 8 - 7
molilian.core/Core/log/pdd.cs

@@ -123,13 +123,14 @@ namespace molilian.core
                 _ = RedisKit.RPushAsync(queue_parse_pdd_key, response);
 
                 if (!TestParseCore.InWhitelist(response.ip, response.oaid))
-                {
-                    await SaveParseCacheAsync(response.channel.ToString(), response.accountId,
-                          response.accountName, response.success, response.message, response.reason,
-                          response.deeplink_url);
-                }
-
-                //if (response.success) saveClientRequestTotal(response.channel, response.ip, response.oaid);
+                {
+                    await SaveParseCacheAsync(response.channel.ToString(), response.accountId,
+                          response.accountName, response.success, response.message, response.reason,
+                          response.deeplink_url);
+                    await TracksCore.RecordParseResultAsync(response.channel.ToString(), response.riskStrategy, response.launchScene, response.accountId, response.success);
+                }
+
+                //if (response.success) saveClientRequestTotal(response.channel, response.ip, response.oaid);
                 if (response.success || response.message.Equals("转链失败"))
                 {
                     await RiskControlCore.CallsIncrByAsync(response.channel, response.accountId);

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

@@ -116,6 +116,7 @@ namespace molilian.core
                     await SaveParseCacheAsync(response.channel.ToString(), response.accountId,
                         response.accountName, response.success, response.message, response.reason,
                         response.deeplink_url);
+                    await TracksCore.RecordParseResultAsync(response.channel.ToString(), response.riskStrategy, response.launchScene, response.accountId, response.success);
                 }
 
                 var account = await TkPoolCore.ALLGetOneAsync(response.accountId);

+ 74 - 34
molilian.core/Core/taoke/UnionParseCore/UnionParseCore.cs

@@ -712,12 +712,20 @@ namespace molilian.core
                         other_aff,
                         result.deeplink_url,
                     });
-                }
-                result.accountId = account.id;
-                result.accountName = account.company;
-
-
-                alimama = new AlimamaPlus(account);
+                }
+                result.accountId = account.id;
+                result.accountName = account.company;
+                if (request.AccountId <= 0)
+                {
+                    _ = TracksCore.RecordParseAccountRequestAsync(
+                        request.Channel,
+                        request.RiskStrategy,
+                        request.LaunchScene,
+                        result.accountId);
+                }
+
+
+                alimama = new AlimamaPlus(account);
 
 
                 int timeout = alimama._config.rt_max;
@@ -1008,13 +1016,21 @@ namespace molilian.core
 
                     _ = TkLogCore.ParseLogAsync(result, is_hide);
                     return JdParseOutput(result);
-                }
-                result.accountId = account.id;
-                result.accountName = account.name;
-                is_hide = account.is_hide;
-
-                var plus = new JdUnionPlus(account);
-                result.proxy_name = plus._proxyName;
+                }
+                result.accountId = account.id;
+                result.accountName = account.name;
+                is_hide = account.is_hide;
+                if (request.AccountId <= 0)
+                {
+                    _ = TracksCore.RecordParseAccountRequestAsync(
+                        request.Channel,
+                        request.RiskStrategy,
+                        request.LaunchScene,
+                        result.accountId);
+                }
+
+                var plus = new JdUnionPlus(account);
+                result.proxy_name = plus._proxyName;
                 result.proxy_node = plus._proxy?.Address?.Host;
 
                 result = await plus.JdParseAsync(content, request, result, cancellationToken);
@@ -1183,13 +1199,21 @@ namespace molilian.core
 
                     _ = TkLogCore.ParseLogAsync(result, is_hide);
                     return JdParseOutput(result);
-                }
-                result.accountId = account.id;
-                result.accountName = account.name;
-                is_hide = account.is_hide;
-
-                var plus = new JdUnionPlus(account);
-                result.proxy_name = plus._proxyName;
+                }
+                result.accountId = account.id;
+                result.accountName = account.name;
+                is_hide = account.is_hide;
+                if (request.AccountId <= 0)
+                {
+                    _ = TracksCore.RecordParseAccountRequestAsync(
+                        request.Channel,
+                        request.RiskStrategy,
+                        request.LaunchScene,
+                        result.accountId);
+                }
+
+                var plus = new JdUnionPlus(account);
+                result.proxy_name = plus._proxyName;
                 result.proxy_node = plus._proxy?.Address?.Host;
 
                 result = await plus.JdParseAsync(content, request, result, cancellationToken);
@@ -1457,13 +1481,21 @@ namespace molilian.core
                     _ = TkLogCore.ParseLogAsync(result);
                     return PddParseOutput(result);
                 }
-
-                result.accountId = account.id;
-                result.accountName = account.name;
-                result.proxy_node = account.nodeName;
-
-                // 更新当前日使用统计
-                PddPoolCore.UpdateAccountUsage(account.id);
+
+                result.accountId = account.id;
+                result.accountName = account.name;
+                result.proxy_node = account.nodeName;
+                if (request.AccountId <= 0)
+                {
+                    _ = TracksCore.RecordParseAccountRequestAsync(
+                        request.Channel,
+                        request.RiskStrategy,
+                        request.LaunchScene,
+                        result.accountId);
+                }
+
+                // 更新当前日使用统计
+                PddPoolCore.UpdateAccountUsage(account.id);
 
                 var plus = new PddUnionPlus(account);
                 result = await plus.PddParseAsync(content, request.CommerceType, result, cancellationToken);
@@ -1644,13 +1676,21 @@ namespace molilian.core
                     _ = TkLogCore.ParseLogAsync(result);
                     return PddParseOutput(result);
                 }
-
-                result.accountId = account.id;
-                result.accountName = account.name;
-                result.proxy_node = account.nodeName;
-
-                // 更新当前日使用统计
-                PddPoolCore.UpdateAccountUsage(account.id);
+
+                result.accountId = account.id;
+                result.accountName = account.name;
+                result.proxy_node = account.nodeName;
+                if (request.AccountId <= 0)
+                {
+                    _ = TracksCore.RecordParseAccountRequestAsync(
+                        request.Channel,
+                        request.RiskStrategy,
+                        request.LaunchScene,
+                        result.accountId);
+                }
+
+                // 更新当前日使用统计
+                PddPoolCore.UpdateAccountUsage(account.id);
 
                 var plus = new PddUnionPlus(account);
                 result = await plus.PddParseAsync(content, request.CommerceType, result, cancellationToken);

+ 35 - 0
molilian.core/DTO/TracksDTO.cs

@@ -51,6 +51,41 @@ namespace molilian.core
         public string hour_label => $"{hour:00}:00";
         public int event_count { get; set; } = 0;
         public int call_count { get; set; } = 0;
+        public int success_count { get; set; } = 0;
+    }
+
+    public class TrackParseMetricReportDTO
+    {
+        public DateTime report_date { get; set; } = DateTime.Now.Date;
+        public string platform { get; set; } = string.Empty;
+        public string typename { get; set; } = string.Empty;
+        public string risk_strategy { get; set; } = string.Empty;
+        public int launch_scene { get; set; } = -1;
+        public int account_id { get; set; } = 0;
+        public int request_count { get; set; } = 0;
+        public int call_count { get; set; } = 0;
+        public int success_count { get; set; } = 0;
+    }
+
+    public class TrackParseMetricHourlyDTO : TrackParseMetricReportDTO
+    {
+        public int hour { get; set; } = 0;
+        public string hour_label => $"{hour:00}:00";
+    }
+
+    public class TrackParseMetricReportResult
+    {
+        public List<TrackParseMetricReportDTO> list { get; set; } = [];
+        public int count { get; set; } = 0;
+    }
+
+    public class TrackParseMetricBackfillResult
+    {
+        public string report_date { get; set; } = string.Empty;
+        public int mysql_rows { get; set; } = 0;
+        public int redis_dimensions { get; set; } = 0;
+        public int redis_cleared_keys { get; set; } = 0;
+        public List<object> details { get; set; } = [];
     }
 
     [Table("track_request_logs")]

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików