using System; using System.Collections.Generic; using System.Data; using System.Net; using System.Threading.Tasks; using CSRedis; using dodohold.core; using YunhuiKit; namespace molilian.core { public partial class TracksCore { private const string RedisPrefix = ":tracks"; private const int DailyExpireSeconds = 40 * 86400; private const int HourlyExpireSeconds = 7 * 86400; // keep a week of hourly buckets private const string DefaultDimensionValue = ""; private const int LinkCacheExpireSeconds = 30 * 86400; private static readonly HashSet SupportEventTypes = new(StringComparer.OrdinalIgnoreCase) { "expose", "click" }; /// /// 生成一个监测链接(仅返回 path,不包含域名),同时落地到 track_links /// public static Task CreateLinkAsync(string eventType, string platform, string scene, string uniqueId) { eventType = NormalizeEventType(eventType); platform = Normalize(platform); scene = NormalizeOrAll(scene); uniqueId = NormalizeOrAll(uniqueId); if (!SupportEventTypes.Contains(eventType) || string.IsNullOrEmpty(platform)) { return Task.FromResult(null); } string cacheKey = $"{RedisPrefix}:link:{eventType}:{platform}:{scene}:{uniqueId}"; try { int cachedId = RedisHelper.Get(cacheKey); if (cachedId > 0) { string cachedPath = BuildPath(eventType, platform, scene, uniqueId); return Task.FromResult(new TrackLinkDTO { id = cachedId, event_type = eventType, platform = platform, scene = scene, unique_id = uniqueId, path = cachedPath }); } } catch { // ignore cache errors } string path = BuildPath(eventType, platform, scene, uniqueId); try { using var conn = DBContext.GetOpenConnection(); var exist = new DBContext.Table(conn, "track_links") .Get("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id", new { event_type = eventType, platform, scene, unique_id = uniqueId }); if (exist != null) { if (string.IsNullOrEmpty(exist.path)) { exist.path = path; new DBContext.Table(conn, "track_links") .Add("path", path) .Add("update_time", DateTime.Now) .Where("id=@id", new { exist.id }) .Update(); } _ = RedisHelper.Set(cacheKey, exist.id, LinkCacheExpireSeconds); return Task.FromResult(exist); } var item = new TrackLinkDTO { event_type = eventType, platform = platform, scene = scene, unique_id = uniqueId, path = path, create_time = DateTime.Now, update_time = DateTime.Now }; var id = conn.Insert(item); if (id != null && int.TryParse(id.ToString(), out var linkId)) { item.id = linkId; } _ = RedisHelper.Set(cacheKey, item.id, LinkCacheExpireSeconds); return Task.FromResult(item); } catch (Exception ex) { _ = new LoggerLibrary("TracksCore", "CreateLink") .Info(ex.Message, ex.StackTrace) .SaveAsync(); return Task.FromResult(null); } } /// /// 曝光/点击触发:计入 Redis,失败不影响返回 /// public static Task TrackAsync(string eventType, string platform, string scene, string uniqueId) { eventType = NormalizeEventType(eventType); platform = Normalize(platform); scene = NormalizeOrAll(scene); uniqueId = NormalizeOrAll(uniqueId); if (!SupportEventTypes.Contains(eventType) || string.IsNullOrEmpty(platform)) { return Task.FromResult(false); } string dateStr = DateTime.Now.ToString("yyyyMMdd"); string hourStr = DateTime.Now.ToString("yyyyMMddHH"); var metrics = new List<(string key, string indexKey, string indexValue, int expire)> { // Daily buckets ($"{RedisPrefix}:daily:{eventType}:{platform}:{dateStr}", $"{RedisPrefix}:daily:index:{dateStr}:platform", $"{eventType}|{platform}", DailyExpireSeconds), ($"{RedisPrefix}:daily:{eventType}:{platform}:{scene}:{dateStr}", $"{RedisPrefix}:daily:index:{dateStr}:scene", $"{eventType}|{platform}|{scene}", DailyExpireSeconds), ($"{RedisPrefix}:daily:{eventType}:{platform}:{scene}:{uniqueId}:{dateStr}", $"{RedisPrefix}:daily:index:{dateStr}:unique", $"{eventType}|{platform}|{scene}|{uniqueId}", DailyExpireSeconds), // Hourly buckets ($"{RedisPrefix}:hour:{eventType}:{platform}:{hourStr}", $"{RedisPrefix}:hour:index:{hourStr}:platform", $"{eventType}|{platform}", HourlyExpireSeconds), ($"{RedisPrefix}:hour:{eventType}:{platform}:{scene}:{hourStr}", $"{RedisPrefix}:hour:index:{hourStr}:scene", $"{eventType}|{platform}|{scene}", HourlyExpireSeconds), ($"{RedisPrefix}:hour:{eventType}:{platform}:{scene}:{uniqueId}:{hourStr}", $"{RedisPrefix}:hour:index:{hourStr}:unique", $"{eventType}|{platform}|{scene}|{uniqueId}", HourlyExpireSeconds), }; try { foreach (var metric in metrics) { RedisHelper.IncrBy(metric.key); RedisHelper.Expire(metric.key, metric.expire); RedisHelper.SAdd(metric.indexKey, metric.indexValue); RedisHelper.Expire(metric.indexKey, metric.expire); } return Task.FromResult(true); } catch (Exception ex) { _ = new LoggerLibrary("TracksCore", "TrackAsync") .Info(ex.Message, ex.StackTrace) .SaveAsync(); return Task.FromResult(false); } } /// /// 日报:拉取昨日 Redis 计数并落地到 track_daily_report(默认统计昨天,可传 reportDate) /// public static async Task FlushDailyAsync(DateTime targetDate) { string dateStr = targetDate.ToString("yyyyMMdd"); string indexKey = $"{RedisPrefix}:daily:index:{dateStr}:unique"; var indexMembers = await RedisHelper.SMembersAsync(indexKey) ?? []; if (indexMembers == null || indexMembers.Length == 0) return 0; int rows = 0; using var conn = DBContext.GetOpenConnection(); foreach (var member in indexMembers) { var parts = member.Split('|'); if (parts.Length != 4) continue; string eventType = NormalizeEventType(parts[0]); string platform = Normalize(parts[1]); string scene = NormalizeOrAll(parts[2]); string uniqueId = NormalizeOrAll(parts[3]); if (!SupportEventTypes.Contains(eventType)) continue; string countKey = $"{RedisPrefix}:daily:{eventType}:{platform}:{scene}:{uniqueId}:{dateStr}"; int total = await RedisHelper.GetAsync(countKey); if (total <= 0) continue; int linkId = GetTrackLinkId(conn, eventType, platform, scene, uniqueId); var exist = new DBContext.Table(conn, "track_daily_report") .Fields("id") .Get("report_date=@report_date AND event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id", new { report_date = targetDate, event_type = eventType, platform, scene, unique_id = uniqueId }); var update = new DBContext.Table(conn, "track_daily_report") .Add("event_count", total) .Add("track_link_id", linkId) .Add("update_time", DateTime.Now); if (exist == null) { update.Add("report_date", targetDate) .Add("event_type", eventType) .Add("platform", platform) .Add("scene", scene) .Add("unique_id", uniqueId) .Add("create_time", DateTime.Now) .Create(); } else { update.Where("id=@id", new { exist.id }).Update(); } rows++; } return rows; } public static string BuildPath(string eventType, string platform, string scene, string uniqueId) { string safeEventType = WebUtility.UrlEncode(eventType); string safePlatform = WebUtility.UrlEncode(platform); string safeScene = WebUtility.UrlEncode(scene); string safeUniqueId = WebUtility.UrlEncode(uniqueId); return $"https://api.molilian.com/tracks/track?eventType={safeEventType}&platform={safePlatform}&scene={safeScene}&uniqueId={safeUniqueId}"; } public static string GetLinkCacheKey(string eventType, string platform, string scene, string uniqueId) { eventType = NormalizeEventType(eventType); platform = Normalize(platform); scene = NormalizeOrAll(scene); uniqueId = NormalizeOrAll(uniqueId); return $"{RedisPrefix}:link:{eventType}:{platform}:{scene}:{uniqueId}"; } private static string Normalize(string value) { return (value ?? string.Empty).Trim(); } private static string NormalizeOrAll(string value) { var result = Normalize(value); return string.IsNullOrEmpty(result) ? DefaultDimensionValue : result; } private static string NormalizeEventType(string value) { return Normalize(value).ToLowerInvariant(); } private static int GetTrackLinkId(IDbConnection conn, string eventType, string platform, string scene, string uniqueId) { try { var record = new DBContext.Table(conn, "track_links") .Fields("id") .Get("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id", new { event_type = eventType, platform, scene, unique_id = uniqueId }); if (record == null) return 0; return record.id; } catch { return 0; } } } }