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 PendingParseMetricCounters = new(); private static readonly object ParseMetricFlushTimerLock = new(); private static Timer? ParseMetricFlushTimer; private static int ParseMetricFlushRunning; private static int ParseMetricLifecycleHooked; public static Task 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 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 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 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(); 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> 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(); 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 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(); 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(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 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 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 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 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 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 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 ClearParseMetricBucketAsync( string bucketType, string bucketValue, Func? 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(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; } } }