Kaynağa Gözat

fix(链路报表): 修复转链结果计数漏记

将新旧结果计数前置,并隔离各 Redis 维度的写入失败。
修正当天淘宝报表的账号统计键回退与实时数据覆盖逻辑。
dodo hold 1 ay önce
ebeveyn
işleme
762c75b6b5

+ 41 - 37
molilian.api/Controllers/admin/TaobaoController.cs

@@ -523,21 +523,22 @@ namespace molilian.api.Controllers
 #endif
                 //var tk = TkPoolCore.GetOne(item.accountId);
                 //if (tk != null) item.accountName = tk.company;
-                accountName = $"{TkChannelEnum.tb}_{item.accountId}";
-
-                if (item.total_count == 0)
-                {
-                    item.total_count = await TkLogCore.GetTotalAsync($":total:{accountName}:{item.report_date:yyyyMMdd}");
-                    if (item.total_count > 0)
-                    {
-                        //todo  过度用, 0613 跑两天就可以删掉
-                        accountName = item.accountName;
-                        item.total_count = await TkLogCore.GetTotalAsync($":total:{accountName}:{item.report_date:yyyyMMdd}");
-                    }
-                    if (item.total_count > 0)
-                    {
-                        item.success_count = await TkLogCore.GetTotalAsync($":total:{accountName}:success:{item.report_date:yyyyMMdd}");
-                        item.abandon_count = await TkLogCore.GetTotalAsync($":total:{accountName}:放弃转链:{item.report_date:yyyyMMdd}");
+                string redisAccountName = $"{TkChannelEnum.tb}_{item.accountId}";
+
+                if (item.total_count == 0)
+                {
+                    string totalAccountName = redisAccountName;
+                    item.total_count = await TkLogCore.GetTotalAsync($":total:{totalAccountName}:{item.report_date:yyyyMMdd}");
+                    if (item.total_count == 0 && !string.Equals(item.accountName, totalAccountName, StringComparison.Ordinal))
+                    {
+                        //todo  过度用, 0613 跑两天就可以删掉
+                        totalAccountName = item.accountName;
+                        item.total_count = await TkLogCore.GetTotalAsync($":total:{totalAccountName}:{item.report_date:yyyyMMdd}");
+                    }
+                    if (item.total_count > 0)
+                    {
+                        item.success_count = await TkLogCore.GetTotalAsync($":total:{totalAccountName}:success:{item.report_date:yyyyMMdd}");
+                        item.abandon_count = await TkLogCore.GetTotalAsync($":total:{totalAccountName}:放弃转链:{item.report_date:yyyyMMdd}");
                         if (item.total_count > 0)
                         {
                             item.success_percentage = $"{item.success_count / (double)item.total_count * 100:f2}%";
@@ -546,28 +547,31 @@ namespace molilian.api.Controllers
                     }
                 }
 
-                if (item.parse_total_count == 0)
-                {
-                    item.parse_total_count = await TkLogCore.GetTotalAsync($":parse_total:{accountName}:{item.report_date:yyyyMMdd}");
-                    if (item.parse_total_count > 0)
-                    {
-                        item.parse_success_count = await TkLogCore.GetTotalAsync($":parse_total:{accountName}:success:{item.report_date:yyyyMMdd}");
-                        item.parse_abandon_count = await TkLogCore.GetTotalAsync($":parse_total:{accountName}:放弃转链:{item.report_date:yyyyMMdd}");
-                        if (item.parse_total_count > 0)
-                        {
-                            item.parse_success_percentage = $"{item.parse_success_count / (double)item.parse_total_count * 100:f2}%";
-                            item.parse_abandon_percentage = $"{item.parse_abandon_count / (double)item.parse_total_count * 100:f2}%";
-                        }
-                    }
-                }
-
-                if (item.coupon_total_count == 0)
-                {
-                    item.coupon_total_count = await TkLogCore.GetTotalAsync($":coupon_total:{accountName}:{item.report_date:yyyyMMdd}", false);
-                    if (item.coupon_total_count > 0)
-                    {
-                        item.coupon_success_count = await TkLogCore.GetTotalAsync($":coupon_total:{accountName}:success:{item.report_date:yyyyMMdd}", false);
-                        item.coupon_abandon_count = await TkLogCore.GetTotalAsync($":coupon_total:{accountName}:放弃转链:{item.report_date:yyyyMMdd}", false);
+                string parseAccountName = redisAccountName;
+                int parseTotalCount = await TkLogCore.GetTotalAsync($":parse_total:{parseAccountName}:{item.report_date:yyyyMMdd}");
+                if (parseTotalCount == 0 && !string.Equals(item.accountName, parseAccountName, StringComparison.Ordinal))
+                {
+                    parseAccountName = item.accountName;
+                    parseTotalCount = await TkLogCore.GetTotalAsync($":parse_total:{parseAccountName}:{item.report_date:yyyyMMdd}");
+                }
+                // Today's DB row may be an earlier snapshot. Prefer the live Redis values
+                // whenever they exist instead of only filling an initially-zero total.
+                if (parseTotalCount > 0)
+                {
+                    item.parse_total_count = parseTotalCount;
+                    item.parse_success_count = await TkLogCore.GetTotalAsync($":parse_total:{parseAccountName}:success:{item.report_date:yyyyMMdd}");
+                    item.parse_abandon_count = await TkLogCore.GetTotalAsync($":parse_total:{parseAccountName}:放弃转链:{item.report_date:yyyyMMdd}");
+                    item.parse_success_percentage = $"{item.parse_success_count / (double)item.parse_total_count * 100:f2}%";
+                    item.parse_abandon_percentage = $"{item.parse_abandon_count / (double)item.parse_total_count * 100:f2}%";
+                }
+
+                if (item.coupon_total_count == 0)
+                {
+                    item.coupon_total_count = await TkLogCore.GetTotalAsync($":coupon_total:{redisAccountName}:{item.report_date:yyyyMMdd}", false);
+                    if (item.coupon_total_count > 0)
+                    {
+                        item.coupon_success_count = await TkLogCore.GetTotalAsync($":coupon_total:{redisAccountName}:success:{item.report_date:yyyyMMdd}", false);
+                        item.coupon_abandon_count = await TkLogCore.GetTotalAsync($":coupon_total:{redisAccountName}:放弃转链:{item.report_date:yyyyMMdd}", false);
                         if (item.coupon_total_count > 0)
                         {
                             item.coupon_success_percentage = $"{item.coupon_success_count / (double)item.coupon_total_count * 100:f2}%";

+ 86 - 25
molilian.core/Core/log/base.cs

@@ -321,11 +321,18 @@ namespace molilian.core
         }
 
 
-        private static async Task SaveParseCacheAsync(string channel, int accountId, string accountName,
-            bool success, string message, string reason, string deeplink)
-        {
-            string dp_flag = deeplink switch
-            {
+        private static async Task SaveParseCacheAsync(string channel, int accountId, string accountName,
+            bool success, string message, string reason, string deeplink)
+        {
+            // Account reports read this dimension directly. Record it first so failures in
+            // secondary aggregate dimensions cannot leave the account row incomplete.
+            if (accountId != 0)
+            {
+                await SaveParseAccountCacheAsync($"{channel}_{accountId}", success, message, reason);
+            }
+
+            string dp_flag = deeplink switch
+            {
                 "" => "none",
                 "tbopen://m.taobao.com/tbopen/index.html" or
                 "pinduoduo://com.xunmeng.pinduoduo/" or
@@ -354,11 +361,47 @@ namespace molilian.core
             {
                 await SaveParseAccountCacheAsync($"{accountName}", success, message, reason);
             }
-            if (accountId != 0)
+        }
+
+        private static async Task RecordParseMetricsAsync(
+            string channel,
+            int accountId,
+            string accountName,
+            bool success,
+            string message,
+            string reason,
+            string deeplink,
+            string riskStrategy,
+            int launchScene)
+        {
+            // The new parse_metric counters must not depend on the legacy Redis statistics
+            // completing successfully. The MySQL log queue is also independent of both.
+            await TracksCore.RecordParseResultAsync(
+                channel,
+                riskStrategy,
+                launchScene,
+                accountId,
+                success);
+
+            try
             {
-                await SaveParseAccountCacheAsync($"{channel}_{accountId}", success, message, reason);
+                await SaveParseCacheAsync(
+                    channel,
+                    accountId,
+                    accountName,
+                    success,
+                    message,
+                    reason,
+                    deeplink);
             }
-        }
+            catch (Exception ex)
+            {
+                _ = new LoggerLibrary("TkLogCore", "RecordParseMetrics")
+                    .Info($"channel={channel}, accountId={accountId}, riskStrategy={riskStrategy}, launchScene={launchScene}")
+                    .Info(ex.Message, ex.StackTrace)
+                    .SaveAsync();
+            }
+        }
 
         private static async Task SaveParseAccountCacheAsync(string accountName, bool success,
             string message, string reason)
@@ -441,26 +484,44 @@ namespace molilian.core
             string message, string reason)
         {
             var now = DateTime.Now;
-            string month = now.ToString("yyyyMM");
-            string day = now.ToString("yyyyMMdd");
-            string hour = now.ToString("yyyyMMddHH");
-
-            await SaveStatsCountAsync(prefix, accountName, month);
-            await SaveStatsCountAsync(prefix, accountName, day);
-            await SaveStatsCountAsync(prefix, accountName, hour);
-
             string result = success ? "success" : "fail";
-            await SaveStatsCountAsync(prefix, accountName, month, result);
-            await SaveStatsCountAsync(prefix, accountName, day, result);
-            await SaveStatsCountAsync(prefix, accountName, hour, result);
+            string[] timeKeys =
+            [
+                now.ToString("yyyyMM"),
+                now.ToString("yyyyMMdd"),
+                now.ToString("yyyyMMddHH")
+            ];
+            Exception? firstError = null;
+
+            async Task TrySaveAsync(Func<Task> action)
+            {
+                try
+                {
+                    await action();
+                }
+                catch (Exception ex)
+                {
+                    firstError ??= ex;
+                }
+            }
 
-            await SaveStatsDimensionAsync(prefix, accountName, "message", message, month);
-            await SaveStatsDimensionAsync(prefix, accountName, "message", message, day);
-            await SaveStatsDimensionAsync(prefix, accountName, "message", message, hour);
+            foreach (string timeKey in timeKeys)
+            {
+                // Keep total and result writes adjacent and isolate every dimension. A single
+                // transient Redis error must not suppress all success/fail counters that follow.
+                await TrySaveAsync(() => SaveStatsCountAsync(prefix, accountName, timeKey));
+                await TrySaveAsync(() => SaveStatsCountAsync(prefix, accountName, timeKey, result));
+                await TrySaveAsync(() => SaveStatsDimensionAsync(prefix, accountName, "message", message, timeKey));
+                await TrySaveAsync(() => SaveStatsDimensionAsync(prefix, accountName, "reason", reason, timeKey));
+            }
 
-            await SaveStatsDimensionAsync(prefix, accountName, "reason", reason, month);
-            await SaveStatsDimensionAsync(prefix, accountName, "reason", reason, day);
-            await SaveStatsDimensionAsync(prefix, accountName, "reason", reason, hour);
+            if (firstError != null)
+            {
+                _ = new LoggerLibrary("TkLogCore", "SaveStatsAccountCache")
+                    .Info($"prefix={prefix}, accountName={accountName}")
+                    .Info(firstError.Message, firstError.StackTrace)
+                    .SaveAsync();
+            }
         }
 
         private static void SaveStatsCount(string prefix, string accountName, string timeKey, string? dimension = null)

+ 4 - 4
molilian.core/Core/log/jd.cs

@@ -143,10 +143,10 @@ namespace molilian.core
 
                 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);
+                    await RecordParseMetricsAsync(
+                        response.channel.ToString(), response.accountId, response.accountName,
+                        response.success, response.message, response.reason, response.deeplink_url,
+                        response.riskStrategy, response.launchScene);
                 }
 
                 if (response.success) await saveClientRequestTotalAsync(response.channel, response.ip, response.oaid);

+ 5 - 5
molilian.core/Core/log/pdd.cs

@@ -122,12 +122,12 @@ namespace molilian.core
                 response.elapsedTime = (int)ts.TotalMilliseconds;
                 _ = RedisKit.RPushAsync(queue_parse_pdd_key, response);
 
-                if (!TestParseCore.InWhitelist(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);
+                    await RecordParseMetricsAsync(
+                        response.channel.ToString(), response.accountId, response.accountName,
+                        response.success, response.message, response.reason, response.deeplink_url,
+                        response.riskStrategy, response.launchScene);
                 }
 
                 //if (response.success) saveClientRequestTotal(response.channel, response.ip, response.oaid);

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

@@ -99,10 +99,18 @@ namespace molilian.core
             try
             {
                 var ts = DateTime.Now - response.create_time;
-                response.elapsedTime = (int)ts.TotalMilliseconds;
-                _ = RedisKit.RPushAsync(queue_parse_tb_key, response);
-
-                if (response.success || response.message.Equals("转链失败") || response.message.Equals("放弃转链"))
+                response.elapsedTime = (int)ts.TotalMilliseconds;
+                _ = RedisKit.RPushAsync(queue_parse_tb_key, response);
+
+                if (!TestParseCore.InWhitelist(response.ip, response.oaid))
+                {
+                    await RecordParseMetricsAsync(
+                        response.channel.ToString(), response.accountId, response.accountName,
+                        response.success, response.message, response.reason, response.deeplink_url,
+                        response.riskStrategy, response.launchScene);
+                }
+
+                if (response.success || response.message.Equals("转链失败") || response.message.Equals("放弃转链"))
                 {
                     await RiskControlCore.CallsIncrByAsync(response.channel, response.accountId);
                     await saveClientRequestTotalAsync(response.channel, response.ip, response.oaid);
@@ -111,14 +119,6 @@ namespace molilian.core
                 {
                     _ = saveUnionCouponParseCacheAsync(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);
-                    await TracksCore.RecordParseResultAsync(response.channel.ToString(), response.riskStrategy, response.launchScene, response.accountId, response.success);
-                }
-
                 var account = await TkPoolCore.ALLGetOneAsync(response.accountId);
                 if (!string.IsNullOrEmpty(response.itemId)) TkOrderTrackingCore.SaveLinkSummary(response, account);