TracksCore.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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_v123";
  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 typename, string scene, string uniqueId, string description = "", bool forceNew = false)
  23. {
  24. eventType = NormalizeEventType(eventType);
  25. typename = Normalize(typename);
  26. scene = NormalizeOrAll(scene);
  27. uniqueId = NormalizeOrAll(uniqueId);
  28. description = Normalize(description);
  29. if (!SupportEventTypes.Contains(eventType))
  30. {
  31. return Task.FromResult<TrackLinkDTO>(null);
  32. }
  33. string cacheKey = $"{RedisPrefix}:link:{eventType}:{typename}:{scene}:{uniqueId}";
  34. if (!forceNew)
  35. {
  36. try
  37. {
  38. int cachedId = RedisHelper.Get<int>(cacheKey);
  39. if (cachedId > 0)
  40. {
  41. var cachedLink = RedisHelper.Get<TrackLinkDTO>(GetLinkIdCacheKey(cachedId));
  42. if (cachedLink != null) return Task.FromResult(cachedLink);
  43. }
  44. }
  45. catch
  46. {
  47. // ignore cache errors
  48. }
  49. }
  50. try
  51. {
  52. using var conn = DBContext.GetOpenConnection();
  53. TrackLinkDTO exist = null;
  54. if (!forceNew)
  55. {
  56. exist = new DBContext.Table(conn, "track_links")
  57. .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
  58. new { event_type = eventType, platform = typename, scene, unique_id = uniqueId });
  59. }
  60. if (exist != null)
  61. {
  62. exist.description = description;
  63. string path = BuildPath(exist.id, eventType);
  64. if (string.IsNullOrEmpty(exist.path) || !exist.path.Contains("track_id"))
  65. {
  66. exist.path = path;
  67. new DBContext.Table(conn, "track_links")
  68. .Add("path", path)
  69. .Add("description", description)
  70. .Add("update_time", DateTime.Now)
  71. .Where("id=@id", new { exist.id })
  72. .Update();
  73. }
  74. _ = RedisHelper.Set(cacheKey, exist.id, LinkCacheExpireSeconds);
  75. _ = RedisHelper.Set(GetLinkIdCacheKey(exist.id), exist, LinkCacheExpireSeconds);
  76. return Task.FromResult(exist);
  77. }
  78. var item = new TrackLinkDTO
  79. {
  80. event_type = eventType,
  81. platform = typename,
  82. scene = scene,
  83. unique_id = uniqueId,
  84. path = string.Empty,
  85. description = description,
  86. create_time = DateTime.Now,
  87. update_time = DateTime.Now
  88. };
  89. var id = conn.Insert(item);
  90. if (id != null && int.TryParse(id.ToString(), out var linkId))
  91. {
  92. item.id = linkId;
  93. item.path = BuildPath(linkId, eventType);
  94. new DBContext.Table(conn, "track_links")
  95. .Add("path", item.path)
  96. .Add("description", description)
  97. .Add("update_time", DateTime.Now)
  98. .Where("id=@id", new { item.id })
  99. .Update();
  100. }
  101. _ = RedisHelper.Set(cacheKey, item.id, LinkCacheExpireSeconds);
  102. _ = RedisHelper.Set(GetLinkIdCacheKey(item.id), item, LinkCacheExpireSeconds);
  103. return Task.FromResult(item);
  104. }
  105. catch (Exception ex)
  106. {
  107. _ = new LoggerLibrary("TracksCore", "CreateLink")
  108. .Info(ex.Message, ex.StackTrace)
  109. .SaveAsync();
  110. return Task.FromResult<TrackLinkDTO>(null);
  111. }
  112. }
  113. /// <summary>
  114. /// 曝光/点击触发:计入 Redis,失败不影响返回
  115. /// </summary>
  116. public static Task<bool> TrackAsync(string eventType, string platform, string scene, string uniqueId)
  117. {
  118. eventType = NormalizeEventType(eventType);
  119. platform = Normalize(platform);
  120. scene = NormalizeOrAll(scene);
  121. uniqueId = NormalizeOrAll(uniqueId);
  122. if (!SupportEventTypes.Contains(eventType))
  123. {
  124. return Task.FromResult(false);
  125. }
  126. string dateStr = DateTime.Now.ToString("yyyyMMdd");
  127. string hourStr = DateTime.Now.ToString("yyyyMMddHH");
  128. var metrics = new List<(string key, string indexKey, string indexValue, int expire)>
  129. {
  130. // Daily buckets
  131. ($"{RedisPrefix}:daily:{eventType}:{platform}:{dateStr}",
  132. $"{RedisPrefix}:daily:index:{dateStr}:platform",
  133. $"{eventType}|{platform}", DailyExpireSeconds),
  134. ($"{RedisPrefix}:daily:{eventType}:{platform}:{scene}:{dateStr}",
  135. $"{RedisPrefix}:daily:index:{dateStr}:scene",
  136. $"{eventType}|{platform}|{scene}", DailyExpireSeconds),
  137. ($"{RedisPrefix}:daily:{eventType}:{platform}:{scene}:{uniqueId}:{dateStr}",
  138. $"{RedisPrefix}:daily:index:{dateStr}:unique",
  139. $"{eventType}|{platform}|{scene}|{uniqueId}", DailyExpireSeconds),
  140. // Hourly buckets
  141. ($"{RedisPrefix}:hour:{eventType}:{platform}:{hourStr}",
  142. $"{RedisPrefix}:hour:index:{hourStr}:platform",
  143. $"{eventType}|{platform}", HourlyExpireSeconds),
  144. ($"{RedisPrefix}:hour:{eventType}:{platform}:{scene}:{hourStr}",
  145. $"{RedisPrefix}:hour:index:{hourStr}:scene",
  146. $"{eventType}|{platform}|{scene}", HourlyExpireSeconds),
  147. ($"{RedisPrefix}:hour:{eventType}:{platform}:{scene}:{uniqueId}:{hourStr}",
  148. $"{RedisPrefix}:hour:index:{hourStr}:unique",
  149. $"{eventType}|{platform}|{scene}|{uniqueId}", HourlyExpireSeconds),
  150. };
  151. try
  152. {
  153. foreach (var metric in metrics)
  154. {
  155. RedisHelper.IncrBy(metric.key);
  156. RedisHelper.Expire(metric.key, metric.expire);
  157. RedisHelper.SAdd(metric.indexKey, metric.indexValue);
  158. RedisHelper.Expire(metric.indexKey, metric.expire);
  159. }
  160. return Task.FromResult(true);
  161. }
  162. catch (Exception ex)
  163. {
  164. _ = new LoggerLibrary("TracksCore", "TrackAsync")
  165. .Info(ex.Message, ex.StackTrace)
  166. .SaveAsync();
  167. return Task.FromResult(false);
  168. }
  169. }
  170. /// <summary>
  171. /// 日报:拉取昨日 Redis 计数并落地到 track_daily_report(默认统计昨天,可传 reportDate)
  172. /// </summary>
  173. public static async Task<int> FlushDailyAsync(DateTime targetDate)
  174. {
  175. string dateStr = targetDate.ToString("yyyyMMdd");
  176. string indexKey = $"{RedisPrefix}:daily:index:{dateStr}:unique";
  177. var indexMembers = await RedisHelper.SMembersAsync<string>(indexKey) ?? [];
  178. if (indexMembers == null || indexMembers.Length == 0) return 0;
  179. int rows = 0;
  180. using var conn = DBContext.GetOpenConnection();
  181. foreach (var member in indexMembers)
  182. {
  183. var parts = member.Split('|');
  184. if (parts.Length != 4) continue;
  185. string eventType = NormalizeEventType(parts[0]);
  186. string platform = Normalize(parts[1]);
  187. string scene = NormalizeOrAll(parts[2]);
  188. string uniqueId = NormalizeOrAll(parts[3]);
  189. if (!SupportEventTypes.Contains(eventType)) continue;
  190. string countKey = $"{RedisPrefix}:daily:{eventType}:{platform}:{scene}:{uniqueId}:{dateStr}";
  191. int total = await RedisHelper.GetAsync<int>(countKey);
  192. if (total <= 0) continue;
  193. int linkId = GetTrackLinkId(conn, eventType, platform, scene, uniqueId);
  194. var exist = new DBContext.Table(conn, "track_daily_report")
  195. .Fields("id")
  196. .Get<dynamic>("report_date=@report_date AND event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
  197. new { report_date = targetDate, event_type = eventType, platform, scene, unique_id = uniqueId });
  198. var update = new DBContext.Table(conn, "track_daily_report")
  199. .Add("event_count", total)
  200. .Add("track_link_id", linkId)
  201. .Add("update_time", DateTime.Now);
  202. if (exist == null)
  203. {
  204. update.Add("report_date", targetDate)
  205. .Add("event_type", eventType)
  206. .Add("platform", platform)
  207. .Add("scene", scene)
  208. .Add("unique_id", uniqueId)
  209. .Add("create_time", DateTime.Now)
  210. .Create();
  211. }
  212. else
  213. {
  214. update.Where("id=@id", new { exist.id }).Update();
  215. }
  216. rows++;
  217. }
  218. return rows;
  219. }
  220. public static string BuildPath(int trackId, string eventType)
  221. {
  222. eventType = NormalizeEventType(eventType);
  223. string safeEventType = WebUtility.UrlEncode(eventType);
  224. return $"https://api.molilian.com/tracks/track?track_id={trackId}&eventType={safeEventType}";
  225. }
  226. public static string GetLinkCacheKey(string eventType, string typename, string scene, string uniqueId)
  227. {
  228. eventType = NormalizeEventType(eventType);
  229. typename = Normalize(typename);
  230. scene = NormalizeOrAll(scene);
  231. uniqueId = NormalizeOrAll(uniqueId);
  232. return $"{RedisPrefix}:link:{eventType}:{typename}:{scene}:{uniqueId}";
  233. }
  234. public static string GetLinkIdCacheKey(int trackId)
  235. {
  236. return $"{RedisPrefix}:linkid:{trackId}";
  237. }
  238. public static async Task<TrackLinkDTO> GetLinkByIdAsync(int trackId)
  239. {
  240. if (trackId <= 0) return null;
  241. string cacheKey = GetLinkIdCacheKey(trackId);
  242. try
  243. {
  244. var cached = RedisHelper.Get<TrackLinkDTO>(cacheKey);
  245. if (cached != null) return cached;
  246. }
  247. catch { }
  248. try
  249. {
  250. var item = new DBContext.Table("track_links").Get<TrackLinkDTO>("id=@id", new { id = trackId });
  251. if (item != null)
  252. {
  253. _ = RedisHelper.Set(cacheKey, item, LinkCacheExpireSeconds);
  254. }
  255. return item;
  256. }
  257. catch
  258. {
  259. return null;
  260. }
  261. }
  262. private static string Normalize(string value)
  263. {
  264. return (value ?? string.Empty).Trim();
  265. }
  266. private static string NormalizeOrAll(string value)
  267. {
  268. var result = Normalize(value);
  269. return string.IsNullOrEmpty(result) ? DefaultDimensionValue : result;
  270. }
  271. private static string NormalizeEventType(string value)
  272. {
  273. return Normalize(value).ToLowerInvariant();
  274. }
  275. private static int GetTrackLinkId(IDbConnection conn, string eventType, string platform, string scene, string uniqueId)
  276. {
  277. try
  278. {
  279. var record = new DBContext.Table(conn, "track_links")
  280. .Fields("id")
  281. .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
  282. new { event_type = eventType, platform, scene, unique_id = uniqueId });
  283. if (record == null) return 0;
  284. return record.id;
  285. }
  286. catch
  287. {
  288. return 0;
  289. }
  290. }
  291. }
  292. }