using System; using System.Collections.Generic; using System.Data; using System.Net; using System.Threading.Tasks; using CSRedis; using dodohold.core; using System.Text.Json; using YunhuiKit; namespace molilian.core { public partial class TracksCore { private const string RedisPrefix = ":tracks_v123"; 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 const string TrackRequestLogKey = ":tracks:request:logs"; private static readonly HashSet SupportEventTypes = new(StringComparer.OrdinalIgnoreCase) { "expose", "click" }; internal static int ExposeTrackId = 2; internal static int ClickTrackId = 1; /// /// 生成一个监测链接(仅返回 path,不包含域名),同时落地到 track_links /// public static Task CreateLinkAsync(string eventType, string typename, string scene, string uniqueId, string description = "", bool forceNew = false) { eventType = NormalizeEventType(eventType); typename = Normalize(typename); scene = NormalizeOrAll(scene); uniqueId = NormalizeOrAll(uniqueId); description = Normalize(description); if (!SupportEventTypes.Contains(eventType)) { return Task.FromResult(null); } string cacheKey = $"{RedisPrefix}:link:{eventType}:{typename}:{scene}:{uniqueId}"; if (!forceNew) { try { int cachedId = RedisHelper.Get(cacheKey); if (cachedId > 0) { var cachedLink = RedisHelper.Get(GetLinkIdCacheKey(cachedId)); if (cachedLink != null) return Task.FromResult(cachedLink); } } catch { // ignore cache errors } } try { using var conn = DBContext.GetOpenConnection(); TrackLinkDTO exist = null; if (!forceNew) { 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 = typename, scene, unique_id = uniqueId }); } if (exist != null) { exist.description = description; string path = BuildPath(exist.id, uniqueId); if (string.IsNullOrEmpty(exist.path) || !exist.path.Contains("track_id")) { exist.path = path; new DBContext.Table(conn, "track_links") .Add("path", path) .Add("description", description) .Add("update_time", DateTime.Now) .Where("id=@id", new { exist.id }) .Update(); } _ = RedisHelper.Set(cacheKey, exist.id, LinkCacheExpireSeconds); _ = RedisHelper.Set(GetLinkIdCacheKey(exist.id), exist, LinkCacheExpireSeconds); return Task.FromResult(exist); } var item = new TrackLinkDTO { event_type = eventType, platform = typename, scene = scene, unique_id = uniqueId, path = string.Empty, description = description, 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; item.path = BuildPath(linkId, uniqueId); new DBContext.Table(conn, "track_links") .Add("path", item.path) .Add("description", description) .Add("update_time", DateTime.Now) .Where("id=@id", new { item.id }) .Update(); } _ = RedisHelper.Set(cacheKey, item.id, LinkCacheExpireSeconds); _ = RedisHelper.Set(GetLinkIdCacheKey(item.id), item, 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(TrackLinkDTO link) { string dateStr = DateTime.Now.ToString("yyyyMMdd"); string hourStr = DateTime.Now.ToString("yyyyMMddHH"); var metrics = new List<(string key, string indexKey, string indexValue, int expire)> { ($"{RedisPrefix}:daily:{link.event_type}:{link.id}:{dateStr}", $"{RedisPrefix}:daily:index:{dateStr}:track", $"{link.event_type}|{link.id}", DailyExpireSeconds), ($"{RedisPrefix}:hour:{link.event_type}:{link.id}:{hourStr}", $"{RedisPrefix}:hour:index:{hourStr}:track", $"{link.event_type}|{link.id}", 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}:track"; 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 != 2) continue; string eventType = NormalizeEventType(parts[0]); if (!SupportEventTypes.Contains(eventType)) continue; if (!int.TryParse(parts[1], out var trackId) || trackId <= 0) continue; string countKey = $"{RedisPrefix}:daily:{eventType}:{trackId}:{dateStr}"; int total = await RedisHelper.GetAsync(countKey); if (total <= 0) continue; var exist = new DBContext.Table(conn, "track_daily_report") .Fields("id") .Get("report_date=@report_date AND event_type=@event_type AND track_link_id=@trackId", new { report_date = targetDate, event_type = eventType, trackId }); var update = new DBContext.Table(conn, "track_daily_report") .Add("event_count", total) .Add("track_link_id", trackId) .Add("platform", string.Empty) .Add("scene", string.Empty) .Add("unique_id", string.Empty) .Add("update_time", DateTime.Now); if (exist == null) { update.Add("report_date", targetDate) .Add("event_type", eventType) .Add("create_time", DateTime.Now) .Create(); } else { update.Where("id=@id", new { exist.id }).Update(); } rows++; } return rows; } public static string BuildPath(int trackId, TkDataDTO result) { if (result == null) return string.Empty; string unique_id = $"1|{result.itemId}_{result.mktId}"; return BuildPath(trackId, unique_id); } public static string BuildPath(int trackId, JdDataDTO result) { if (result == null) return string.Empty; string unique_id = $"13|{result.shortLinkurl.UrlEncode()}"; return BuildPath(trackId, unique_id); } public static string BuildPath(int trackId, PddDataDTO result) { if (result == null) return string.Empty; string unique_id = $"9|{result.shortLinkurl.UrlEncode()}"; return BuildPath(trackId, unique_id); } public static string BuildPath(int trackId, string unique_id = "") { return $"https://api.molilian.com/tracks/track?track_id={trackId}&unique_id={unique_id}"; } public static string GetLinkCacheKey(string eventType, string typename, string scene, string uniqueId) { eventType = NormalizeEventType(eventType); typename = Normalize(typename); scene = NormalizeOrAll(scene); uniqueId = NormalizeOrAll(uniqueId); return $"{RedisPrefix}:link:{eventType}:{typename}:{scene}:{uniqueId}"; } public static string GetLinkIdCacheKey(int trackId) { return $"{RedisPrefix}:linkid:{trackId}"; } public static async Task GetLinkByIdAsync(int trackId) { if (trackId <= 0) return null; string cacheKey = GetLinkIdCacheKey(trackId); try { var cached = RedisHelper.Get(cacheKey); if (cached != null) return cached; } catch { } try { var item = new DBContext.Table("track_links").Get("id=@id", new { id = trackId }); if (item != null) { _ = RedisHelper.Set(cacheKey, item, LinkCacheExpireSeconds); } return item; } catch { return null; } } public static Task LogTrackRequestAsync(TrackRequestLogDTO dto) { try { dto.event_type = NormalizeEventType(dto.event_type); dto.platform = Normalize(dto.platform); dto.typename = Normalize(dto.typename); dto.scene = NormalizeOrAll(dto.scene); dto.unique_id = NormalizeOrAll(dto.unique_id); dto.ip = Normalize(dto.ip); dto.user_agent = Normalize(dto.user_agent); dto.referer = Normalize(dto.referer); dto.create_time = DateTime.Now; RedisHelper.RPush(TrackRequestLogKey, dto); return Task.FromResult(true); } catch { return Task.FromResult(false); } } public static async Task InsertTrackRequestLogAsync(int limit, YunhuiKit.RedisClient redis) { int count = 0; try { using var conn = DBContext.GetOpenConnection(); for (int i = 0; i < limit; i++) { var entity = await redis.LPopAsync(TrackRequestLogKey); if (entity == null) break; try { if (entity.create_time == default) entity.create_time = DateTime.Now; conn.Insert(entity); count++; } catch { // ignore malformed item } } } catch { return count; } return count; } 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; } } } }