|
@@ -0,0 +1,288 @@
|
|
|
|
|
+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<string> SupportEventTypes = new(StringComparer.OrdinalIgnoreCase) { "expose", "click" };
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 生成一个监测链接(仅返回 path,不包含域名),同时落地到 track_links
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public static Task<TrackLinkDTO> 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<TrackLinkDTO>(null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ string cacheKey = $"{RedisPrefix}:link:{eventType}:{platform}:{scene}:{uniqueId}";
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ int cachedId = RedisHelper.Get<int>(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<TrackLinkDTO>("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<TrackLinkDTO>(null);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 曝光/点击触发:计入 Redis,失败不影响返回
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public static Task<bool> 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 日报:拉取昨日 Redis 计数并落地到 track_daily_report(默认统计昨天,可传 reportDate)
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public static async Task<int> FlushDailyAsync(DateTime targetDate)
|
|
|
|
|
+ {
|
|
|
|
|
+ string dateStr = targetDate.ToString("yyyyMMdd");
|
|
|
|
|
+ string indexKey = $"{RedisPrefix}:daily:index:{dateStr}:unique";
|
|
|
|
|
+
|
|
|
|
|
+ var indexMembers = await RedisHelper.SMembersAsync<string>(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<int>(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<dynamic>("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<TrackLinkDTO>("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;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|