TracksCore.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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 System.Text.Json;
  9. using YunhuiKit;
  10. namespace molilian.core
  11. {
  12. public partial class TracksCore
  13. {
  14. private const string RedisPrefix = ":tracks_v123";
  15. private const int DailyExpireSeconds = 40 * 86400;
  16. private const int HourlyExpireSeconds = 7 * 86400; // keep a week of hourly buckets
  17. private const string DefaultDimensionValue = "";
  18. private const int LinkCacheExpireSeconds = 30 * 86400;
  19. private const string TrackRequestLogKey = ":tracks:request:logs";
  20. private static readonly HashSet<string> SupportEventTypes = new(StringComparer.OrdinalIgnoreCase) { "expose", "click" };
  21. internal static int ExposeTrackId = 2;
  22. internal static int ClickTrackId = 1;
  23. /// <summary>
  24. /// 生成一个监测链接(仅返回 path,不包含域名),同时落地到 track_links
  25. /// </summary>
  26. public static Task<TrackLinkDTO> CreateLinkAsync(string eventType, string typename, string scene, string uniqueId, string description = "", bool forceNew = false)
  27. {
  28. eventType = NormalizeEventType(eventType);
  29. typename = Normalize(typename);
  30. scene = NormalizeOrAll(scene);
  31. uniqueId = NormalizeOrAll(uniqueId);
  32. description = Normalize(description);
  33. if (!SupportEventTypes.Contains(eventType))
  34. {
  35. return Task.FromResult<TrackLinkDTO>(null);
  36. }
  37. string cacheKey = $"{RedisPrefix}:link:{eventType}:{typename}:{scene}:{uniqueId}";
  38. if (!forceNew)
  39. {
  40. try
  41. {
  42. int cachedId = RedisHelper.Get<int>(cacheKey);
  43. if (cachedId > 0)
  44. {
  45. var cachedLink = RedisHelper.Get<TrackLinkDTO>(GetLinkIdCacheKey(cachedId));
  46. if (cachedLink != null) return Task.FromResult(cachedLink);
  47. }
  48. }
  49. catch
  50. {
  51. // ignore cache errors
  52. }
  53. }
  54. try
  55. {
  56. using var conn = DBContext.GetOpenConnection();
  57. TrackLinkDTO exist = null;
  58. if (!forceNew)
  59. {
  60. exist = new DBContext.Table(conn, "track_links")
  61. .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
  62. new { event_type = eventType, platform = typename, scene, unique_id = uniqueId });
  63. }
  64. if (exist != null)
  65. {
  66. exist.description = description;
  67. string path = BuildPath(exist.id, uniqueId);
  68. if (string.IsNullOrEmpty(exist.path) || !exist.path.Contains("track_id"))
  69. {
  70. exist.path = path;
  71. new DBContext.Table(conn, "track_links")
  72. .Add("path", path)
  73. .Add("description", description)
  74. .Add("update_time", DateTime.Now)
  75. .Where("id=@id", new { exist.id })
  76. .Update();
  77. }
  78. _ = RedisHelper.Set(cacheKey, exist.id, LinkCacheExpireSeconds);
  79. _ = RedisHelper.Set(GetLinkIdCacheKey(exist.id), exist, LinkCacheExpireSeconds);
  80. return Task.FromResult(exist);
  81. }
  82. var item = new TrackLinkDTO
  83. {
  84. event_type = eventType,
  85. platform = typename,
  86. scene = scene,
  87. unique_id = uniqueId,
  88. path = string.Empty,
  89. description = description,
  90. create_time = DateTime.Now,
  91. update_time = DateTime.Now
  92. };
  93. var id = conn.Insert(item);
  94. if (id != null && int.TryParse(id.ToString(), out var linkId))
  95. {
  96. item.id = linkId;
  97. item.path = BuildPath(linkId, uniqueId);
  98. new DBContext.Table(conn, "track_links")
  99. .Add("path", item.path)
  100. .Add("description", description)
  101. .Add("update_time", DateTime.Now)
  102. .Where("id=@id", new { item.id })
  103. .Update();
  104. }
  105. _ = RedisHelper.Set(cacheKey, item.id, LinkCacheExpireSeconds);
  106. _ = RedisHelper.Set(GetLinkIdCacheKey(item.id), item, LinkCacheExpireSeconds);
  107. return Task.FromResult(item);
  108. }
  109. catch (Exception ex)
  110. {
  111. _ = new LoggerLibrary("TracksCore", "CreateLink")
  112. .Info(ex.Message, ex.StackTrace)
  113. .SaveAsync();
  114. return Task.FromResult<TrackLinkDTO>(null);
  115. }
  116. }
  117. /// <summary>
  118. /// 曝光/点击触发:计入 Redis,失败不影响返回
  119. /// </summary>
  120. public static Task<bool> TrackAsync(TrackLinkDTO link)
  121. {
  122. string dateStr = DateTime.Now.ToString("yyyyMMdd");
  123. string hourStr = DateTime.Now.ToString("yyyyMMddHH");
  124. var metrics = new List<(string key, string indexKey, string indexValue, int expire)>
  125. {
  126. ($"{RedisPrefix}:daily:{link.event_type}:{link.id}:{dateStr}", $"{RedisPrefix}:daily:index:{dateStr}:track", $"{link.event_type}|{link.id}", DailyExpireSeconds),
  127. ($"{RedisPrefix}:hour:{link.event_type}:{link.id}:{hourStr}", $"{RedisPrefix}:hour:index:{hourStr}:track", $"{link.event_type}|{link.id}", HourlyExpireSeconds),
  128. };
  129. try
  130. {
  131. foreach (var metric in metrics)
  132. {
  133. RedisHelper.IncrBy(metric.key);
  134. RedisHelper.Expire(metric.key, metric.expire);
  135. RedisHelper.SAdd(metric.indexKey, metric.indexValue);
  136. RedisHelper.Expire(metric.indexKey, metric.expire);
  137. }
  138. return Task.FromResult(true);
  139. }
  140. catch (Exception ex)
  141. {
  142. _ = new LoggerLibrary("TracksCore", "TrackAsync")
  143. .Info(ex.Message, ex.StackTrace)
  144. .SaveAsync();
  145. return Task.FromResult(false);
  146. }
  147. }
  148. /// <summary>
  149. /// 日报:拉取昨日 Redis 计数并落地到 track_daily_report(默认统计昨天,可传 reportDate)
  150. /// </summary>
  151. public static async Task<int> FlushDailyAsync(DateTime targetDate)
  152. {
  153. string dateStr = targetDate.ToString("yyyyMMdd");
  154. string indexKey = $"{RedisPrefix}:daily:index:{dateStr}:track";
  155. var indexMembers = await RedisHelper.SMembersAsync<string>(indexKey) ?? [];
  156. if (indexMembers == null || indexMembers.Length == 0) return 0;
  157. int rows = 0;
  158. using var conn = DBContext.GetOpenConnection();
  159. foreach (var member in indexMembers)
  160. {
  161. var parts = member.Split('|');
  162. if (parts.Length != 2) continue;
  163. string eventType = NormalizeEventType(parts[0]);
  164. if (!SupportEventTypes.Contains(eventType)) continue;
  165. if (!int.TryParse(parts[1], out var trackId) || trackId <= 0) continue;
  166. string countKey = $"{RedisPrefix}:daily:{eventType}:{trackId}:{dateStr}";
  167. int total = await RedisHelper.GetAsync<int>(countKey);
  168. if (total <= 0) continue;
  169. var exist = new DBContext.Table(conn, "track_daily_report")
  170. .Fields("id")
  171. .Get<dynamic>("report_date=@report_date AND event_type=@event_type AND track_link_id=@trackId",
  172. new { report_date = targetDate, event_type = eventType, trackId });
  173. var update = new DBContext.Table(conn, "track_daily_report")
  174. .Add("event_count", total)
  175. .Add("track_link_id", trackId)
  176. .Add("platform", string.Empty)
  177. .Add("scene", string.Empty)
  178. .Add("unique_id", string.Empty)
  179. .Add("update_time", DateTime.Now);
  180. if (exist == null)
  181. {
  182. update.Add("report_date", targetDate)
  183. .Add("event_type", eventType)
  184. .Add("create_time", DateTime.Now)
  185. .Create();
  186. }
  187. else
  188. {
  189. update.Where("id=@id", new { exist.id }).Update();
  190. }
  191. rows++;
  192. }
  193. return rows;
  194. }
  195. public static string BuildPath(int trackId, TkDataDTO result)
  196. {
  197. if (result == null || result.parse_type != "dp") return string.Empty;
  198. string unique_id = $"1|{result.itemId}_{result.mktId}";
  199. return BuildPath(trackId, unique_id);
  200. }
  201. public static string BuildPath(int trackId, JdDataDTO result)
  202. {
  203. if (result == null || result.parse_type != "dp") return string.Empty;
  204. string unique_id = $"13|{result.shortLinkurl.UrlEncode()}";
  205. return BuildPath(trackId, unique_id);
  206. }
  207. public static string BuildPath(int trackId, PddDataDTO result)
  208. {
  209. if (result == null || result.parse_type != "dp") return string.Empty;
  210. string unique_id = $"9|{result.shortLinkurl.UrlEncode()}";
  211. return BuildPath(trackId, unique_id);
  212. }
  213. public static string BuildPath(int trackId, string unique_id = "")
  214. {
  215. return $"https://api.molilian.com/tracks/track?track_id={trackId}&unique_id={unique_id}";
  216. }
  217. public static string GetLinkCacheKey(string eventType, string typename, string scene, string uniqueId)
  218. {
  219. eventType = NormalizeEventType(eventType);
  220. typename = Normalize(typename);
  221. scene = NormalizeOrAll(scene);
  222. uniqueId = NormalizeOrAll(uniqueId);
  223. return $"{RedisPrefix}:link:{eventType}:{typename}:{scene}:{uniqueId}";
  224. }
  225. public static string GetLinkIdCacheKey(int trackId)
  226. {
  227. return $"{RedisPrefix}:linkid:{trackId}";
  228. }
  229. public static async Task<TrackLinkDTO> GetLinkByIdAsync(int trackId)
  230. {
  231. if (trackId <= 0) return null;
  232. string cacheKey = GetLinkIdCacheKey(trackId);
  233. try
  234. {
  235. var cached = RedisHelper.Get<TrackLinkDTO>(cacheKey);
  236. if (cached != null) return cached;
  237. }
  238. catch { }
  239. try
  240. {
  241. var item = new DBContext.Table("track_links").Get<TrackLinkDTO>("id=@id", new { id = trackId });
  242. if (item != null)
  243. {
  244. _ = RedisHelper.Set(cacheKey, item, LinkCacheExpireSeconds);
  245. }
  246. return item;
  247. }
  248. catch
  249. {
  250. return null;
  251. }
  252. }
  253. public static Task<bool> LogTrackRequestAsync(TrackRequestLogDTO dto)
  254. {
  255. try
  256. {
  257. dto.event_type = NormalizeEventType(dto.event_type);
  258. dto.platform = Normalize(dto.platform);
  259. dto.typename = Normalize(dto.typename);
  260. dto.scene = NormalizeOrAll(dto.scene);
  261. dto.unique_id = NormalizeOrAll(dto.unique_id);
  262. dto.ip = Normalize(dto.ip);
  263. dto.user_agent = Normalize(dto.user_agent);
  264. dto.referer = Normalize(dto.referer);
  265. dto.create_time = DateTime.Now;
  266. RedisHelper.RPush(TrackRequestLogKey, dto);
  267. return Task.FromResult(true);
  268. }
  269. catch
  270. {
  271. return Task.FromResult(false);
  272. }
  273. }
  274. public static async Task<int> InsertTrackRequestLogAsync(int limit, YunhuiKit.RedisClient redis)
  275. {
  276. int count = 0;
  277. try
  278. {
  279. using var conn = DBContext.GetOpenConnection();
  280. for (int i = 0; i < limit; i++)
  281. {
  282. var entity = await redis.LPopAsync<TrackRequestLogDTO>(TrackRequestLogKey);
  283. if (entity == null) break;
  284. try
  285. {
  286. if (entity.create_time == default) entity.create_time = DateTime.Now;
  287. conn.Insert(entity);
  288. count++;
  289. }
  290. catch
  291. {
  292. // ignore malformed item
  293. }
  294. }
  295. }
  296. catch
  297. {
  298. return count;
  299. }
  300. return count;
  301. }
  302. private static string Normalize(string value)
  303. {
  304. return (value ?? string.Empty).Trim();
  305. }
  306. private static string NormalizeOrAll(string value)
  307. {
  308. var result = Normalize(value);
  309. return string.IsNullOrEmpty(result) ? DefaultDimensionValue : result;
  310. }
  311. private static string NormalizeEventType(string value)
  312. {
  313. return Normalize(value).ToLowerInvariant();
  314. }
  315. private static int GetTrackLinkId(IDbConnection conn, string eventType, string platform, string scene, string uniqueId)
  316. {
  317. try
  318. {
  319. var record = new DBContext.Table(conn, "track_links")
  320. .Fields("id")
  321. .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
  322. new { event_type = eventType, platform, scene, unique_id = uniqueId });
  323. if (record == null) return 0;
  324. return record.id;
  325. }
  326. catch
  327. {
  328. return 0;
  329. }
  330. }
  331. }
  332. }