|
@@ -1222,6 +1222,322 @@ ORDER BY l.accountId, l.end_point",
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 将淘宝覆盖规则命中产生的转链记录从 success=1 修正为 success=0,并同步修正各节点 Redis 统计。
|
|
|
|
|
+ /// 覆盖规则在选择账号前命中,记录特征:channel=tb、linkType=unknown、accountId=0、itemId 为空。
|
|
|
|
|
+ /// 按「日期 + 节点」处理:先修正该节点 Redis,成功后再更新对应 MySQL 记录;已修正的记录不会被重复计入。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ [HttpGet]
|
|
|
|
|
+ public async Task<ActionResult> RepairTbOverrideSuccess(
|
|
|
|
|
+ DateTime startTime = default,
|
|
|
|
|
+ DateTime endTime = default,
|
|
|
|
|
+ bool dryRun = true,
|
|
|
|
|
+ int commandTimeoutSeconds = 600)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (startTime == default) startTime = new DateTime(2026, 10, 20, 18, 52, 42);
|
|
|
|
|
+ if (endTime == default) endTime = DateTime.Now;
|
|
|
|
|
+ commandTimeoutSeconds = Math.Clamp(commandTimeoutSeconds, 30, 3600);
|
|
|
|
|
+ if (endTime < startTime)
|
|
|
|
|
+ {
|
|
|
|
|
+ return new APIResult(new { success = false, message = "endTime 不能早于 startTime" });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const string condition = @"channel = @channel
|
|
|
|
|
+ AND linkType = @linkType
|
|
|
|
|
+ AND success = 1
|
|
|
|
|
+ AND accountId = 0
|
|
|
|
|
+ AND COALESCE(itemId,'') = ''
|
|
|
|
|
+ AND create_time BETWEEN @from AND @to";
|
|
|
|
|
+
|
|
|
|
|
+ using var conn = CenterHub.GetOpenConnection();
|
|
|
|
|
+ if (conn.State != ConnectionState.Open) conn.Open();
|
|
|
|
|
+
|
|
|
|
|
+ // 与 TkLogCore.GetTotalAsync 读取统计时的节点范围保持一致
|
|
|
|
|
+ var endpoints = EndPointCore.List()
|
|
|
|
|
+ .Where(node => node.is_public_api && !string.IsNullOrEmpty(EndPointCore.GetRedisServer(node)))
|
|
|
|
|
+ .GroupBy(node => node.name, StringComparer.Ordinal)
|
|
|
|
|
+ .ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);
|
|
|
|
|
+
|
|
|
|
|
+ var errors = new List<object>();
|
|
|
|
|
+ var dayReports = new List<object>();
|
|
|
|
|
+ foreach (var date in EachDay(startTime.Date, endTime.Date))
|
|
|
|
|
+ {
|
|
|
|
|
+ DateTime from = date < startTime ? startTime : date;
|
|
|
|
|
+ DateTime dayEnd = date.AddDays(1).AddTicks(-1);
|
|
|
|
|
+ DateTime to = dayEnd > endTime ? endTime : dayEnd;
|
|
|
|
|
+ string tableName = $"tk_parse_logs_{date:yyyyMMdd}";
|
|
|
|
|
+
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!TableExists(conn, tableName, commandTimeoutSeconds))
|
|
|
|
|
+ {
|
|
|
|
|
+ dayReports.Add(new { date = date.ToString("yyyy-MM-dd"), tableName, skipped = true, reason = "table not exists" });
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var args = new { channel = (int)TkChannelEnum.tb, linkType = (int)LinkTypeEnum.unknown, from, to };
|
|
|
|
|
+ var groups = SqlMapper.Query<TbOverrideRepairGroupRow>(
|
|
|
|
|
+ conn,
|
|
|
|
|
+ $@"
|
|
|
|
|
+SELECT
|
|
|
|
|
+ COALESCE(end_point,'') end_point,
|
|
|
|
|
+ DATE_FORMAT(create_time,'%Y%m%d%H') hour_key,
|
|
|
|
|
+ COALESCE(deeplink_url,'') deeplink_url,
|
|
|
|
|
+ COALESCE(riskStrategy,'') riskStrategy,
|
|
|
|
|
+ launchScene,
|
|
|
|
|
+ COUNT(*) row_count
|
|
|
|
|
+FROM {tableName}
|
|
|
|
|
+WHERE {condition}
|
|
|
|
|
+GROUP BY COALESCE(end_point,''), DATE_FORMAT(create_time,'%Y%m%d%H'), COALESCE(deeplink_url,''), COALESCE(riskStrategy,''), launchScene",
|
|
|
|
|
+ args,
|
|
|
|
|
+ commandTimeout: commandTimeoutSeconds).ToList();
|
|
|
|
|
+
|
|
|
|
|
+ var summary = SqlMapper.Query<TbOverrideRepairSummaryRow>(
|
|
|
|
|
+ conn,
|
|
|
|
|
+ $@"
|
|
|
|
|
+SELECT CAST(subCode AS CHAR) subCode, COALESCE(message,'') message, COALESCE(reason,'') reason, COUNT(*) row_count
|
|
|
|
|
+FROM {tableName}
|
|
|
|
|
+WHERE {condition}
|
|
|
|
|
+GROUP BY CAST(subCode AS CHAR), COALESCE(message,''), COALESCE(reason,'')
|
|
|
|
|
+ORDER BY row_count DESC",
|
|
|
|
|
+ args,
|
|
|
|
|
+ commandTimeout: commandTimeoutSeconds).ToList();
|
|
|
|
|
+
|
|
|
|
|
+ var topDeeplinks = groups
|
|
|
|
|
+ .GroupBy(item => item.deeplink_url)
|
|
|
|
|
+ .Select(group => new { deeplink_url = group.Key, row_count = group.Sum(item => item.row_count) })
|
|
|
|
|
+ .OrderByDescending(item => item.row_count)
|
|
|
|
|
+ .Take(20)
|
|
|
|
|
+ .ToList();
|
|
|
|
|
+
|
|
|
|
|
+ var endpointReports = new List<object>();
|
|
|
|
|
+ foreach (var endpointGroup in groups.GroupBy(item => item.end_point))
|
|
|
|
|
+ {
|
|
|
|
|
+ long rowCount = endpointGroup.Sum(item => item.row_count);
|
|
|
|
|
+ if (!endpoints.TryGetValue(endpointGroup.Key, out var endpoint))
|
|
|
|
|
+ {
|
|
|
|
|
+ errors.Add(new { scope = "endpoint_not_found", date = date.ToString("yyyy-MM-dd"), end_point = endpointGroup.Key, rowCount });
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var deltas = BuildTbOverrideRepairDeltas(endpointGroup);
|
|
|
|
|
+ var keyReports = new List<object>();
|
|
|
|
|
+ int appliedKeys = 0;
|
|
|
|
|
+ Dictionary<string, int> mysql;
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ await using var scope = await RedisClientFactory.CreateScopeAsync(EndPointCore.GetRedisServer(endpoint));
|
|
|
|
|
+ var redis = scope.Client;
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var (key, (delta, expireSeconds)) in deltas)
|
|
|
|
|
+ {
|
|
|
|
|
+ long before = await redis.GetAsync<long>(key);
|
|
|
|
|
+ // 只减到 0 为止,避免计数出现负数
|
|
|
|
|
+ long applied = delta < 0 ? -Math.Min(-delta, Math.Max(before, 0)) : delta;
|
|
|
|
|
+
|
|
|
|
|
+ if (!dryRun && applied != 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ await redis.IncrByAsync(key, applied);
|
|
|
|
|
+ if (before == 0 && applied > 0 && expireSeconds > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ await redis.ExpireAsync(key, expireSeconds);
|
|
|
|
|
+ }
|
|
|
|
|
+ appliedKeys++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 小时级 key 数量较多,只在报告中列出日、月级 key
|
|
|
|
|
+ if (!IsHourlyStatsKey(key))
|
|
|
|
|
+ {
|
|
|
|
|
+ keyReports.Add(new { key, before, delta, applied });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ errors.Add(new
|
|
|
|
|
+ {
|
|
|
|
|
+ scope = "redis_endpoint",
|
|
|
|
|
+ date = date.ToString("yyyy-MM-dd"),
|
|
|
|
|
+ end_point = endpoint.name,
|
|
|
|
|
+ appliedKeys,
|
|
|
|
|
+ totalKeys = deltas.Count,
|
|
|
|
|
+ error = FormatRepairError(ex)
|
|
|
|
|
+ });
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ mysql = RepairTbOverrideMysqlRows(conn, date, condition, args, endpoint.name, dryRun, commandTimeoutSeconds);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ errors.Add(new { scope = "mysql_endpoint", date = date.ToString("yyyy-MM-dd"), end_point = endpoint.name, redisApplied = !dryRun, error = FormatRepairError(ex) });
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ endpointReports.Add(new
|
|
|
|
|
+ {
|
|
|
|
|
+ end_point = endpoint.name,
|
|
|
|
|
+ rowCount,
|
|
|
|
|
+ totalKeys = deltas.Count,
|
|
|
|
|
+ appliedKeys,
|
|
|
|
|
+ mysql,
|
|
|
|
|
+ keys = keyReports
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ dayReports.Add(new
|
|
|
|
|
+ {
|
|
|
|
|
+ date = date.ToString("yyyy-MM-dd"),
|
|
|
|
|
+ from,
|
|
|
|
|
+ to,
|
|
|
|
|
+ rowCount = groups.Sum(item => item.row_count),
|
|
|
|
|
+ summary,
|
|
|
|
|
+ topDeeplinks,
|
|
|
|
|
+ endpoints = endpointReports
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ errors.Add(new { scope = "day", date = date.ToString("yyyy-MM-dd"), tableName, error = FormatRepairError(ex) });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return new APIResult(new
|
|
|
|
|
+ {
|
|
|
|
|
+ success = errors.Count == 0,
|
|
|
|
|
+ dryRun,
|
|
|
|
|
+ startTime,
|
|
|
|
|
+ endTime,
|
|
|
|
|
+ days = dayReports,
|
|
|
|
|
+ errors
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 计算单个节点需要修正的 Redis 计数增量,对应 TkLogCore.ParseLogAsync 中 success 相关的写入:
|
|
|
|
|
+ /// parse_total 的 total/success/fail(dp_{flag} 可能由 success 变为 fail)以及 parse_metric 的 success。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private static Dictionary<string, (long delta, int expireSeconds)> BuildTbOverrideRepairDeltas(IEnumerable<TbOverrideRepairGroupRow> rows)
|
|
|
|
|
+ {
|
|
|
|
|
+ const string channel = "tb";
|
|
|
|
|
+ var deltas = new Dictionary<string, (long delta, int expireSeconds)>(StringComparer.Ordinal);
|
|
|
|
|
+
|
|
|
|
|
+ void Add(string key, long delta, int expireSeconds)
|
|
|
|
|
+ {
|
|
|
|
|
+ deltas.TryGetValue(key, out var current);
|
|
|
|
|
+ deltas[key] = (current.delta + delta, expireSeconds);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var row in rows)
|
|
|
|
|
+ {
|
|
|
|
|
+ var metricTime = DateTime.ParseExact(row.hour_key, "yyyyMMddHH", null);
|
|
|
|
|
+ string[] timeKeys = [metricTime.ToString("yyyyMM"), metricTime.ToString("yyyyMMdd"), row.hour_key];
|
|
|
|
|
+
|
|
|
|
|
+ string oldFlag = TkLogCore.GetParseDeeplinkFlag(row.deeplink_url, true);
|
|
|
|
|
+ string newFlag = TkLogCore.GetParseDeeplinkFlag(row.deeplink_url, false);
|
|
|
|
|
+ string[] oldBuckets = [$"dp_{oldFlag}:all", $"dp_{oldFlag}:{channel}", "all", $"dp_channel:{channel}", channel];
|
|
|
|
|
+ string[] newBuckets = [$"dp_{newFlag}:all", $"dp_{newFlag}:{channel}", "all", $"dp_channel:{channel}", channel];
|
|
|
|
|
+
|
|
|
|
|
+ foreach (string timeKey in timeKeys)
|
|
|
|
|
+ {
|
|
|
|
|
+ int expireSeconds = TkLogCore.GetStatsExpireSeconds(timeKey);
|
|
|
|
|
+ foreach (string bucket in oldBuckets)
|
|
|
|
|
+ {
|
|
|
|
|
+ Add($":parse_total:{bucket}:{timeKey}", -row.row_count, expireSeconds);
|
|
|
|
|
+ Add($":parse_total:{bucket}:success:{timeKey}", -row.row_count, expireSeconds);
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach (string bucket in newBuckets)
|
|
|
|
|
+ {
|
|
|
|
|
+ Add($":parse_total:{bucket}:{timeKey}", row.row_count, expireSeconds);
|
|
|
|
|
+ Add($":parse_total:{bucket}:fail:{timeKey}", row.row_count, expireSeconds);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach (string key in TracksCore.BuildParseMetricSuccessKeys(metricTime, channel, row.riskStrategy, row.launchScene))
|
|
|
|
|
+ {
|
|
|
|
|
+ Add(key, -row.row_count, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return deltas
|
|
|
|
|
+ .Where(item => item.Value.delta != 0)
|
|
|
|
|
+ .ToDictionary(item => item.Key, item => item.Value, StringComparer.Ordinal);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static bool IsHourlyStatsKey(string key)
|
|
|
|
|
+ {
|
|
|
|
|
+ int index = key.LastIndexOf(':');
|
|
|
|
|
+ string last = index >= 0 ? key[(index + 1)..] : key;
|
|
|
|
|
+ if (last.Length == 10 && last.All(char.IsDigit)) return true;
|
|
|
|
|
+ // parse_metric key 的时间段位于中间::parse_metric:hour:success:{yyyyMMddHH}:...
|
|
|
|
|
+ return key.Contains(":parse_metric:hour:", StringComparison.Ordinal);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 修正(dryRun 时仅统计)单个节点在指定日期的覆盖规则记录:
|
|
|
|
|
+ /// 日表、dp 日表、测试表改为 success=0;success 汇总表删除对应记录。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private static Dictionary<string, int> RepairTbOverrideMysqlRows(
|
|
|
|
|
+ IDbConnection conn,
|
|
|
|
|
+ DateTime date,
|
|
|
|
|
+ string condition,
|
|
|
|
|
+ object args,
|
|
|
|
|
+ string endPoint,
|
|
|
|
|
+ bool dryRun,
|
|
|
|
|
+ int commandTimeoutSeconds)
|
|
|
|
|
+ {
|
|
|
|
|
+ var parameters = new DynamicParameters(args);
|
|
|
|
|
+ parameters.Add("end_point", endPoint);
|
|
|
|
|
+ string where = $"{condition} AND COALESCE(end_point,'') = @end_point";
|
|
|
|
|
+ string dpWhere = $"{where} AND parse_type = 'dp'";
|
|
|
|
|
+
|
|
|
|
|
+ var targets = new (string table, string where, bool delete)[]
|
|
|
|
|
+ {
|
|
|
|
|
+ ($"tk_parse_logs_{date:yyyyMMdd}", where, false),
|
|
|
|
|
+ ($"tk_dp_logs_{date:yyyyMMdd}", dpWhere, false),
|
|
|
|
|
+ ("tk_parse_logs_test", where, false),
|
|
|
|
|
+ ("tk_success_parse_logs", where, true),
|
|
|
|
|
+ ("tk_dp_logs_success", dpWhere, true),
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ var result = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
|
|
|
+ foreach (var (table, tableWhere, delete) in targets)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!TableExists(conn, table, commandTimeoutSeconds)) continue;
|
|
|
|
|
+
|
|
|
|
|
+ string sql = dryRun
|
|
|
|
|
+ ? $"SELECT COUNT(*) FROM {table} WHERE {tableWhere}"
|
|
|
|
|
+ : delete
|
|
|
|
|
+ ? $"DELETE FROM {table} WHERE {tableWhere}"
|
|
|
|
|
+ : $"UPDATE {table} SET success = 0 WHERE {tableWhere}";
|
|
|
|
|
+
|
|
|
|
|
+ result[table] = dryRun
|
|
|
|
|
+ ? SqlMapper.ExecuteScalar<int>(conn, sql, parameters, commandTimeout: commandTimeoutSeconds)
|
|
|
|
|
+ : SqlMapper.Execute(conn, sql, parameters, commandTimeout: commandTimeoutSeconds);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private sealed class TbOverrideRepairGroupRow
|
|
|
|
|
+ {
|
|
|
|
|
+ public string end_point { get; set; } = string.Empty;
|
|
|
|
|
+ public string hour_key { get; set; } = string.Empty;
|
|
|
|
|
+ public string deeplink_url { get; set; } = string.Empty;
|
|
|
|
|
+ public string riskStrategy { get; set; } = string.Empty;
|
|
|
|
|
+ public int launchScene { get; set; }
|
|
|
|
|
+ public long row_count { get; set; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private sealed class TbOverrideRepairSummaryRow
|
|
|
|
|
+ {
|
|
|
|
|
+ public string subCode { get; set; } = string.Empty;
|
|
|
|
|
+ public string message { get; set; } = string.Empty;
|
|
|
|
|
+ public string reason { get; set; } = string.Empty;
|
|
|
|
|
+ public long row_count { get; set; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static HashSet<string> GetKnownDeeplinkReportChannelNames()
|
|
private static HashSet<string> GetKnownDeeplinkReportChannelNames()
|
|
|
{
|
|
{
|
|
|
var channelNames = Enum.GetNames(typeof(TkChannelEnum))
|
|
var channelNames = Enum.GetNames(typeof(TkChannelEnum))
|