TracksCore.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.Net;
  5. using System.Threading.Tasks;
  6. using CSRedis;
  7. using dodohold.core;
  8. using YunhuiKit;
  9. namespace molilian.core
  10. {
  11. public partial class TracksCore
  12. {
  13. private const string RedisPrefix = ":tracks";
  14. private const int DailyExpireSeconds = 40 * 86400;
  15. private const int HourlyExpireSeconds = 7 * 86400; // keep a week of hourly buckets
  16. private const string DefaultDimensionValue = "";
  17. private const int LinkCacheExpireSeconds = 30 * 86400;
  18. private static readonly HashSet<string> SupportEventTypes = new(StringComparer.OrdinalIgnoreCase) { "expose", "click" };
  19. /// <summary>
  20. /// 生成一个监测链接(仅返回 path,不包含域名),同时落地到 track_links
  21. /// </summary>
  22. public static Task<TrackLinkDTO> CreateLinkAsync(string eventType, string platform, string scene, string uniqueId)
  23. {
  24. eventType = NormalizeEventType(eventType);
  25. platform = Normalize(platform);
  26. scene = NormalizeOrAll(scene);
  27. uniqueId = NormalizeOrAll(uniqueId);
  28. if (!SupportEventTypes.Contains(eventType) || string.IsNullOrEmpty(platform))
  29. {
  30. return Task.FromResult<TrackLinkDTO>(null);
  31. }
  32. string cacheKey = $"{RedisPrefix}:link:{eventType}:{platform}:{scene}:{uniqueId}";
  33. try
  34. {
  35. int cachedId = RedisHelper.Get<int>(cacheKey);
  36. if (cachedId > 0)
  37. {
  38. string cachedPath = BuildPath(eventType, platform, scene, uniqueId);
  39. return Task.FromResult(new TrackLinkDTO
  40. {
  41. id = cachedId,
  42. event_type = eventType,
  43. platform = platform,
  44. scene = scene,
  45. unique_id = uniqueId,
  46. path = cachedPath
  47. });
  48. }
  49. }
  50. catch
  51. {
  52. // ignore cache errors
  53. }
  54. string path = BuildPath(eventType, platform, scene, uniqueId);
  55. try
  56. {
  57. using var conn = DBContext.GetOpenConnection();
  58. var exist = new DBContext.Table(conn, "track_links")
  59. .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
  60. new { event_type = eventType, platform, scene, unique_id = uniqueId });
  61. if (exist != null)
  62. {
  63. if (string.IsNullOrEmpty(exist.path))
  64. {
  65. exist.path = path;
  66. new DBContext.Table(conn, "track_links")
  67. .Add("path", path)
  68. .Add("update_time", DateTime.Now)
  69. .Where("id=@id", new { exist.id })
  70. .Update();
  71. }
  72. _ = RedisHelper.Set(cacheKey, exist.id, LinkCacheExpireSeconds);
  73. return Task.FromResult(exist);
  74. }
  75. var item = new TrackLinkDTO
  76. {
  77. event_type = eventType,
  78. platform = platform,
  79. scene = scene,
  80. unique_id = uniqueId,
  81. path = path,
  82. create_time = DateTime.Now,
  83. update_time = DateTime.Now
  84. };
  85. var id = conn.Insert(item);
  86. if (id != null && int.TryParse(id.ToString(), out var linkId))
  87. {
  88. item.id = linkId;
  89. }
  90. _ = RedisHelper.Set(cacheKey, item.id, LinkCacheExpireSeconds);
  91. return Task.FromResult(item);
  92. }
  93. catch (Exception ex)
  94. {
  95. _ = new LoggerLibrary("TracksCore", "CreateLink")
  96. .Info(ex.Message, ex.StackTrace)
  97. .SaveAsync();
  98. return Task.FromResult<TrackLinkDTO>(null);
  99. }
  100. }
  101. /// <summary>
  102. /// 曝光/点击触发:计入 Redis,失败不影响返回
  103. /// </summary>
  104. public static Task<bool> TrackAsync(string eventType, string platform, string scene, string uniqueId)
  105. {
  106. eventType = NormalizeEventType(eventType);
  107. platform = Normalize(platform);
  108. scene = NormalizeOrAll(scene);
  109. uniqueId = NormalizeOrAll(uniqueId);
  110. if (!SupportEventTypes.Contains(eventType) || string.IsNullOrEmpty(platform))
  111. {
  112. return Task.FromResult(false);
  113. }
  114. string dateStr = DateTime.Now.ToString("yyyyMMdd");
  115. string hourStr = DateTime.Now.ToString("yyyyMMddHH");
  116. var metrics = new List<(string key, string indexKey, string indexValue, int expire)>
  117. {
  118. // Daily buckets
  119. ($"{RedisPrefix}:daily:{eventType}:{platform}:{dateStr}",
  120. $"{RedisPrefix}:daily:index:{dateStr}:platform",
  121. $"{eventType}|{platform}", DailyExpireSeconds),
  122. ($"{RedisPrefix}:daily:{eventType}:{platform}:{scene}:{dateStr}",
  123. $"{RedisPrefix}:daily:index:{dateStr}:scene",
  124. $"{eventType}|{platform}|{scene}", DailyExpireSeconds),
  125. ($"{RedisPrefix}:daily:{eventType}:{platform}:{scene}:{uniqueId}:{dateStr}",
  126. $"{RedisPrefix}:daily:index:{dateStr}:unique",
  127. $"{eventType}|{platform}|{scene}|{uniqueId}", DailyExpireSeconds),
  128. // Hourly buckets
  129. ($"{RedisPrefix}:hour:{eventType}:{platform}:{hourStr}",
  130. $"{RedisPrefix}:hour:index:{hourStr}:platform",
  131. $"{eventType}|{platform}", HourlyExpireSeconds),
  132. ($"{RedisPrefix}:hour:{eventType}:{platform}:{scene}:{hourStr}",
  133. $"{RedisPrefix}:hour:index:{hourStr}:scene",
  134. $"{eventType}|{platform}|{scene}", HourlyExpireSeconds),
  135. ($"{RedisPrefix}:hour:{eventType}:{platform}:{scene}:{uniqueId}:{hourStr}",
  136. $"{RedisPrefix}:hour:index:{hourStr}:unique",
  137. $"{eventType}|{platform}|{scene}|{uniqueId}", HourlyExpireSeconds),
  138. };
  139. try
  140. {
  141. foreach (var metric in metrics)
  142. {
  143. RedisHelper.IncrBy(metric.key);
  144. RedisHelper.Expire(metric.key, metric.expire);
  145. RedisHelper.SAdd(metric.indexKey, metric.indexValue);
  146. RedisHelper.Expire(metric.indexKey, metric.expire);
  147. }
  148. return Task.FromResult(true);
  149. }
  150. catch (Exception ex)
  151. {
  152. _ = new LoggerLibrary("TracksCore", "TrackAsync")
  153. .Info(ex.Message, ex.StackTrace)
  154. .SaveAsync();
  155. return Task.FromResult(false);
  156. }
  157. }
  158. /// <summary>
  159. /// 日报:拉取昨日 Redis 计数并落地到 track_daily_report(默认统计昨天,可传 reportDate)
  160. /// </summary>
  161. public static async Task<int> FlushDailyAsync(DateTime targetDate)
  162. {
  163. string dateStr = targetDate.ToString("yyyyMMdd");
  164. string indexKey = $"{RedisPrefix}:daily:index:{dateStr}:unique";
  165. var indexMembers = await RedisHelper.SMembersAsync<string>(indexKey) ?? [];
  166. if (indexMembers == null || indexMembers.Length == 0) return 0;
  167. int rows = 0;
  168. using var conn = DBContext.GetOpenConnection();
  169. foreach (var member in indexMembers)
  170. {
  171. var parts = member.Split('|');
  172. if (parts.Length != 4) continue;
  173. string eventType = NormalizeEventType(parts[0]);
  174. string platform = Normalize(parts[1]);
  175. string scene = NormalizeOrAll(parts[2]);
  176. string uniqueId = NormalizeOrAll(parts[3]);
  177. if (!SupportEventTypes.Contains(eventType)) continue;
  178. string countKey = $"{RedisPrefix}:daily:{eventType}:{platform}:{scene}:{uniqueId}:{dateStr}";
  179. int total = await RedisHelper.GetAsync<int>(countKey);
  180. if (total <= 0) continue;
  181. int linkId = GetTrackLinkId(conn, eventType, platform, scene, uniqueId);
  182. var exist = new DBContext.Table(conn, "track_daily_report")
  183. .Fields("id")
  184. .Get<dynamic>("report_date=@report_date AND event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
  185. new { report_date = targetDate, event_type = eventType, platform, scene, unique_id = uniqueId });
  186. var update = new DBContext.Table(conn, "track_daily_report")
  187. .Add("event_count", total)
  188. .Add("track_link_id", linkId)
  189. .Add("update_time", DateTime.Now);
  190. if (exist == null)
  191. {
  192. update.Add("report_date", targetDate)
  193. .Add("event_type", eventType)
  194. .Add("platform", platform)
  195. .Add("scene", scene)
  196. .Add("unique_id", uniqueId)
  197. .Add("create_time", DateTime.Now)
  198. .Create();
  199. }
  200. else
  201. {
  202. update.Where("id=@id", new { exist.id }).Update();
  203. }
  204. rows++;
  205. }
  206. return rows;
  207. }
  208. public static string BuildPath(string eventType, string platform, string scene, string uniqueId)
  209. {
  210. string safeEventType = WebUtility.UrlEncode(eventType);
  211. string safePlatform = WebUtility.UrlEncode(platform);
  212. string safeScene = WebUtility.UrlEncode(scene);
  213. string safeUniqueId = WebUtility.UrlEncode(uniqueId);
  214. return $"https://api.molilian.com/tracks/track?eventType={safeEventType}&platform={safePlatform}&scene={safeScene}&uniqueId={safeUniqueId}";
  215. }
  216. public static string GetLinkCacheKey(string eventType, string platform, string scene, string uniqueId)
  217. {
  218. eventType = NormalizeEventType(eventType);
  219. platform = Normalize(platform);
  220. scene = NormalizeOrAll(scene);
  221. uniqueId = NormalizeOrAll(uniqueId);
  222. return $"{RedisPrefix}:link:{eventType}:{platform}:{scene}:{uniqueId}";
  223. }
  224. private static string Normalize(string value)
  225. {
  226. return (value ?? string.Empty).Trim();
  227. }
  228. private static string NormalizeOrAll(string value)
  229. {
  230. var result = Normalize(value);
  231. return string.IsNullOrEmpty(result) ? DefaultDimensionValue : result;
  232. }
  233. private static string NormalizeEventType(string value)
  234. {
  235. return Normalize(value).ToLowerInvariant();
  236. }
  237. private static int GetTrackLinkId(IDbConnection conn, string eventType, string platform, string scene, string uniqueId)
  238. {
  239. try
  240. {
  241. var record = new DBContext.Table(conn, "track_links")
  242. .Fields("id")
  243. .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
  244. new { event_type = eventType, platform, scene, unique_id = uniqueId });
  245. if (record == null) return 0;
  246. return record.id;
  247. }
  248. catch
  249. {
  250. return 0;
  251. }
  252. }
  253. }
  254. }