TracksCore.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.Net;
  5. using System.Threading.Tasks;
  6. using Dapper;
  7. using CSRedis;
  8. using dodohold.core;
  9. using System.Text.Json;
  10. using YunhuiKit;
  11. namespace molilian.core
  12. {
  13. public enum TrackType
  14. {
  15. Expose,
  16. Click
  17. }
  18. public partial class TracksCore
  19. {
  20. private const string RedisPrefix = ":tracks_v123";
  21. private const int DailyExpireSeconds = 40 * 86400;
  22. private const int HourlyExpireSeconds = 7 * 86400; // keep a week of hourly buckets
  23. private const string DefaultDimensionValue = "";
  24. private const int LinkCacheExpireSeconds = 30 * 86400;
  25. private const string LinkCacheVersion = "v2";
  26. private const string TrackRequestLogKey = ":tracks:request:logs";
  27. private static readonly HashSet<string> SupportEventTypes = new(StringComparer.OrdinalIgnoreCase) { "expose", "click" };
  28. /// <summary>
  29. /// 生成一个监测链接(仅返回 path,不包含域名),同时落地到 track_links
  30. /// </summary>
  31. public static Task<TrackLinkDTO> CreateLinkAsync(string eventType, string typename, string scene, string uniqueId, string description = "", bool forceNew = false)
  32. {
  33. return CreateLinkAsync(eventType, string.Empty, typename, scene, uniqueId, description, forceNew);
  34. }
  35. /// <summary>
  36. /// 生成一个监测链接(仅返回 path,不包含域名),同时落地到 track_links
  37. /// </summary>
  38. public static Task<TrackLinkDTO> CreateLinkAsync(string eventType, string platform, string typename, string scene, string uniqueId, string description = "", bool forceNew = false)
  39. {
  40. eventType = NormalizeEventType(eventType);
  41. platform = Normalize(platform);
  42. typename = Normalize(typename);
  43. scene = NormalizeOrAll(scene);
  44. uniqueId = NormalizeOrAll(uniqueId);
  45. description = Normalize(description);
  46. if (!SupportEventTypes.Contains(eventType))
  47. {
  48. return Task.FromResult<TrackLinkDTO>(null);
  49. }
  50. string cacheKey = GetLinkCacheKey(eventType, platform, typename, scene, uniqueId);
  51. if (!forceNew)
  52. {
  53. try
  54. {
  55. int cachedId = RedisHelper.Get<int>(cacheKey);
  56. if (cachedId > 0)
  57. {
  58. var cachedLink = RedisHelper.Get<TrackLinkDTO>(GetLinkIdCacheKey(cachedId));
  59. if (cachedLink != null) return Task.FromResult(cachedLink);
  60. }
  61. }
  62. catch
  63. {
  64. // ignore cache errors
  65. }
  66. }
  67. try
  68. {
  69. using var conn = DBContext.GetOpenConnection();
  70. TrackLinkDTO exist = null;
  71. if (!forceNew)
  72. {
  73. exist = new DBContext.Table(conn, "track_links")
  74. .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND typename=@typename AND scene=@scene AND unique_id=@unique_id",
  75. new { event_type = eventType, platform, typename, scene, unique_id = uniqueId });
  76. }
  77. if (exist != null)
  78. {
  79. exist.description = description;
  80. string path = BuildPath(exist.id, uniqueId);
  81. if (string.IsNullOrEmpty(exist.path) || !exist.path.Contains("track_id"))
  82. {
  83. exist.path = path;
  84. new DBContext.Table(conn, "track_links")
  85. .Add("path", path)
  86. .Add("platform", platform)
  87. .Add("typename", typename)
  88. .Add("description", description)
  89. .Add("update_time", DateTime.Now)
  90. .Where("id=@id", new { exist.id })
  91. .Update();
  92. }
  93. _ = RedisHelper.Set(cacheKey, exist.id, LinkCacheExpireSeconds);
  94. _ = RedisHelper.Set(GetLinkIdCacheKey(exist.id), exist, LinkCacheExpireSeconds);
  95. return Task.FromResult(exist);
  96. }
  97. var item = new TrackLinkDTO
  98. {
  99. event_type = eventType,
  100. platform = platform,
  101. typename = typename,
  102. scene = scene,
  103. unique_id = uniqueId,
  104. path = string.Empty,
  105. description = description,
  106. create_time = DateTime.Now,
  107. update_time = DateTime.Now
  108. };
  109. var id = conn.Insert(item);
  110. if (id != null && int.TryParse(id.ToString(), out var linkId))
  111. {
  112. item.id = linkId;
  113. item.path = BuildPath(linkId, uniqueId);
  114. new DBContext.Table(conn, "track_links")
  115. .Add("path", item.path)
  116. .Add("description", description)
  117. .Add("update_time", DateTime.Now)
  118. .Where("id=@id", new { item.id })
  119. .Update();
  120. }
  121. _ = RedisHelper.Set(cacheKey, item.id, LinkCacheExpireSeconds);
  122. _ = RedisHelper.Set(GetLinkIdCacheKey(item.id), item, LinkCacheExpireSeconds);
  123. return Task.FromResult(item);
  124. }
  125. catch (Exception ex)
  126. {
  127. _ = new LoggerLibrary("TracksCore", "CreateLink")
  128. .Info(ex.Message, ex.StackTrace)
  129. .SaveAsync();
  130. return Task.FromResult<TrackLinkDTO>(null);
  131. }
  132. }
  133. /// <summary>
  134. /// 曝光/点击触发:计入 Redis,失败不影响返回
  135. /// </summary>
  136. public static Task<bool> TrackAsync(TrackLinkDTO link, string scene = "", int accountId = 0)
  137. {
  138. string dateStr = DateTime.Now.ToString("yyyyMMdd");
  139. string hourStr = DateTime.Now.ToString("yyyyMMddHH");
  140. string metricScene = ResolveMetricScene(link, scene);
  141. accountId = Math.Max(accountId, 0);
  142. string indexValue = BuildMetricIndexValue(link.event_type, link.id, metricScene);
  143. var metrics = new List<(string key, string indexKey, string indexValue, int expire)>
  144. {
  145. (BuildDailyCountKey(link.event_type, link.id, dateStr, metricScene), $"{RedisPrefix}:daily:index:{dateStr}:track", indexValue, DailyExpireSeconds),
  146. (BuildHourlyCountKey(link.event_type, link.id, hourStr, metricScene), $"{RedisPrefix}:hour:index:{hourStr}:track", indexValue, HourlyExpireSeconds),
  147. };
  148. if (accountId > 0)
  149. {
  150. string accountIndexValue = BuildMetricIndexValue(link.event_type, link.id, metricScene, accountId);
  151. metrics.Add((BuildDailyCountKey(link.event_type, link.id, dateStr, metricScene, accountId), $"{RedisPrefix}:daily:index:{dateStr}:track", accountIndexValue, DailyExpireSeconds));
  152. metrics.Add((BuildHourlyCountKey(link.event_type, link.id, hourStr, metricScene, accountId), $"{RedisPrefix}:hour:index:{hourStr}:track", accountIndexValue, HourlyExpireSeconds));
  153. }
  154. try
  155. {
  156. foreach (var metric in metrics)
  157. {
  158. RedisHelper.IncrBy(metric.key);
  159. RedisHelper.Expire(metric.key, metric.expire);
  160. RedisHelper.SAdd(metric.indexKey, metric.indexValue);
  161. RedisHelper.Expire(metric.indexKey, metric.expire);
  162. }
  163. return Task.FromResult(true);
  164. }
  165. catch (Exception ex)
  166. {
  167. _ = new LoggerLibrary("TracksCore", "TrackAsync")
  168. .Info(ex.Message, ex.StackTrace)
  169. .SaveAsync();
  170. return Task.FromResult(false);
  171. }
  172. }
  173. /// <summary>
  174. /// 日报:拉取指定日期 Redis 计数并落地到 track_daily_report,可高频重复执行。
  175. /// </summary>
  176. public static async Task<int> FlushDailyAsync(DateTime targetDate)
  177. {
  178. string dateStr = targetDate.ToString("yyyyMMdd");
  179. string indexKey = $"{RedisPrefix}:daily:index:{dateStr}:track";
  180. var indexMembers = await RedisHelper.SMembersAsync<string>(indexKey) ?? [];
  181. if (indexMembers == null || indexMembers.Length == 0) return 0;
  182. var buckets = new Dictionary<string, DailyReportBucket>();
  183. using var conn = DBContext.GetOpenConnection();
  184. var linkCache = new Dictionary<int, TrackLinkDTO>();
  185. foreach (var member in indexMembers)
  186. {
  187. var parts = member.Split('|');
  188. if (parts.Length < 2 || parts.Length > 4) continue;
  189. string eventType = NormalizeEventType(parts[0]);
  190. if (!SupportEventTypes.Contains(eventType)) continue;
  191. if (!int.TryParse(parts[1], out var trackId) || trackId <= 0) continue;
  192. bool hasScenePart = parts.Length >= 3;
  193. string memberScene = hasScenePart ? DecodeIndexPart(parts[2]) : string.Empty;
  194. int accountId = 0;
  195. if (parts.Length == 4 && (!int.TryParse(parts[3], out accountId) || accountId <= 0)) continue;
  196. string countKey = BuildDailyCountKey(eventType, trackId, dateStr, hasScenePart ? memberScene : string.Empty, accountId);
  197. int total = await RedisHelper.GetAsync<int>(countKey);
  198. if (total <= 0) continue;
  199. if (!linkCache.TryGetValue(trackId, out var link))
  200. {
  201. link = new DBContext.Table(conn, "track_links")
  202. .Get<TrackLinkDTO>("id=@id", new { id = trackId });
  203. if (link != null) linkCache[trackId] = link;
  204. }
  205. if (link != null)
  206. {
  207. string linkEventType = NormalizeEventType(link.event_type);
  208. if (!SupportEventTypes.Contains(linkEventType)) continue;
  209. if (!string.Equals(eventType, linkEventType, StringComparison.OrdinalIgnoreCase))
  210. {
  211. _ = new LoggerLibrary("TracksCore", "FlushDaily")
  212. .Info($"skip stale track bucket: member={member}, link_event_type={linkEventType}", string.Empty)
  213. .SaveAsync();
  214. continue;
  215. }
  216. eventType = linkEventType;
  217. }
  218. string reportScene = ResolveMetricScene(link, memberScene);
  219. string bucketKey = BuildMetricIndexValue(eventType, trackId, reportScene, accountId);
  220. if (!buckets.TryGetValue(bucketKey, out var bucket))
  221. {
  222. bucket = new DailyReportBucket
  223. {
  224. EventType = eventType,
  225. TrackId = trackId,
  226. Scene = reportScene,
  227. AccountId = accountId,
  228. Link = link
  229. };
  230. buckets[bucketKey] = bucket;
  231. }
  232. bucket.Total += total;
  233. }
  234. int rows = 0;
  235. foreach (var bucket in buckets.Values)
  236. {
  237. await UpsertDailyReportAsync(conn, targetDate.Date, bucket.EventType, bucket.TrackId, bucket.Scene, bucket.AccountId, bucket.Link, bucket.Total);
  238. rows++;
  239. }
  240. return rows;
  241. }
  242. private static Task<int> UpsertDailyReportAsync(IDbConnection conn, DateTime reportDate, string eventType, int trackId, string scene, int accountId, TrackLinkDTO? link, int total)
  243. {
  244. const string sql = @"
  245. INSERT INTO track_daily_report
  246. (report_date, event_type, track_link_id, account_id, platform, typename, scene, unique_id, event_count, create_time, update_time)
  247. VALUES
  248. (@reportDate, @eventType, @trackId, @accountId, @platform, @typename, @scene, @uniqueId, @eventCount, @now, @now)
  249. ON DUPLICATE KEY UPDATE
  250. event_count = VALUES(event_count),
  251. account_id = VALUES(account_id),
  252. platform = VALUES(platform),
  253. typename = VALUES(typename),
  254. scene = VALUES(scene),
  255. unique_id = VALUES(unique_id),
  256. update_time = VALUES(update_time);";
  257. return conn.ExecuteAsync(sql, new
  258. {
  259. reportDate,
  260. eventType,
  261. trackId,
  262. accountId,
  263. platform = link?.platform ?? string.Empty,
  264. typename = link?.typename ?? string.Empty,
  265. scene,
  266. uniqueId = link?.unique_id ?? string.Empty,
  267. eventCount = total,
  268. now = DateTime.Now
  269. });
  270. }
  271. public static async Task<List<TrackHourlyReportDTO>> GetHourlyReportAsync(int trackId, DateTime targetDate, string scene = "", int accountId = 0)
  272. {
  273. var result = new List<TrackHourlyReportDTO>();
  274. if (trackId <= 0) return result;
  275. var link = await GetLinkByIdAsync(trackId);
  276. if (link == null) return result;
  277. string eventType = NormalizeEventType(link.event_type);
  278. if (!SupportEventTypes.Contains(eventType)) return result;
  279. string metricScene = ResolveMetricScene(link, scene);
  280. string linkScene = ResolveMetricScene(link, string.Empty);
  281. string dateStr = targetDate.ToString("yyyyMMdd");
  282. accountId = Math.Max(accountId, 0);
  283. for (int hour = 0; hour < 24; hour++)
  284. {
  285. string hourStr = $"{dateStr}{hour:00}";
  286. int total = await RedisHelper.GetAsync<int>(BuildHourlyCountKey(eventType, trackId, hourStr, metricScene, accountId));
  287. if (accountId == 0 && !string.IsNullOrEmpty(metricScene) && metricScene == linkScene)
  288. {
  289. total += await RedisHelper.GetAsync<int>(BuildHourlyCountKey(eventType, trackId, hourStr, string.Empty));
  290. }
  291. int callCount = await GetTrackHourlyCallCountAsync(link, hourStr, accountId);
  292. result.Add(new TrackHourlyReportDTO
  293. {
  294. track_link_id = trackId,
  295. event_type = eventType,
  296. platform = link.platform ?? string.Empty,
  297. typename = link.typename ?? string.Empty,
  298. scene = metricScene,
  299. unique_id = link.unique_id ?? string.Empty,
  300. account_id = accountId,
  301. report_date = targetDate.Date,
  302. hour = hour,
  303. event_count = total,
  304. call_count = callCount
  305. });
  306. }
  307. return result;
  308. }
  309. private static async Task<int> GetTrackHourlyCallCountAsync(TrackLinkDTO link, string hourStr, int accountId)
  310. {
  311. if (accountId <= 0) return 0;
  312. string channelName = GetTrackCallChannelName(link);
  313. if (string.IsNullOrEmpty(channelName)) return 0;
  314. return await TkLogCore.GetTotalAsync($":parse_total:{channelName}_{accountId}:{hourStr}");
  315. }
  316. private static string GetTrackCallChannelName(TrackLinkDTO link)
  317. {
  318. string platform = Normalize(link.platform).ToLowerInvariant();
  319. if (link.id is 19 or 20 || platform == "jd" || platform == "13" || platform.Contains("京东"))
  320. {
  321. return "jd";
  322. }
  323. if (link.id is 21 or 22 || platform == "pdd" || platform == "9" || platform.Contains("拼多多"))
  324. {
  325. return "pdd";
  326. }
  327. return "tb";
  328. }
  329. public static string BuildPath(TrackType type, TkDataDTO result)
  330. {
  331. int trackId = type == TrackType.Click ? 1 : 2;
  332. if (result == null) return string.Empty;
  333. string unique_id = $"1|{result.itemId}_{result.mktId}";
  334. return BuildPath(trackId, unique_id, GetTrackScene(result.parse_type, result.riskStrategy), result.accountId);
  335. }
  336. public static string BuildPath(TrackType type, JdDataDTO result)
  337. {
  338. int trackId = type == TrackType.Click ? 19 : 20;
  339. if (result == null) return string.Empty;
  340. string unique_id = $"13|{result.shortLinkurl.UrlEncode()}";
  341. return BuildPath(trackId, unique_id, GetTrackScene(result.parse_type, result.riskStrategy), result.accountId);
  342. }
  343. public static string BuildPath(TrackType type, PddDataDTO result)
  344. {
  345. int trackId = type == TrackType.Click ? 21 : 22;
  346. if (result == null) return string.Empty;
  347. string unique_id = $"9|{result.shortLinkurl.UrlEncode()}";
  348. return BuildPath(trackId, unique_id, GetTrackScene(result.parse_type, result.riskStrategy), result.accountId);
  349. }
  350. public static string BuildBrwSimilarPath(TrackType type, TkDataDTO result, PromotionQueryItemDTO similar_goods = null, int index = 0)
  351. {
  352. int trackId = type == TrackType.Click ? 23 : 24;
  353. if (result == null) return string.Empty;
  354. string unique_id = $"1|{result.itemId}_{result.mktId}";
  355. string scene = string.Empty;
  356. if (similar_goods != null)
  357. {
  358. unique_id = $"1|{result.itemId}_{result.mktId}_{index}";
  359. scene = "similar";
  360. }
  361. return BuildPath(trackId, unique_id, scene, result.accountId);
  362. }
  363. public static string BuildPath(int trackId, string unique_id = "", string scene = "", int accountId = 0)
  364. {
  365. scene = NormalizeReportScene(scene);
  366. string url = $"https://api.molilian.com/tracks/track?track_id={trackId}&unique_id={unique_id}";
  367. if (!string.IsNullOrEmpty(scene)) url += $"&scene={scene.UrlEncode()}";
  368. if (accountId > 0) url += $"&account_id={accountId}";
  369. return url;
  370. }
  371. public static string ResolveMetricScene(TrackLinkDTO? link, string scene = "")
  372. {
  373. scene = NormalizeReportScene(scene);
  374. if (!string.IsNullOrEmpty(scene)) return scene;
  375. return NormalizeOrAll(link?.scene ?? string.Empty);
  376. }
  377. public static string GetLinkCacheKey(string eventType, string typename, string scene, string uniqueId)
  378. {
  379. return GetLinkCacheKey(eventType, string.Empty, typename, scene, uniqueId);
  380. }
  381. public static string GetLinkCacheKey(string eventType, string platform, string typename, string scene, string uniqueId)
  382. {
  383. eventType = NormalizeEventType(eventType);
  384. platform = Normalize(platform);
  385. typename = Normalize(typename);
  386. scene = NormalizeOrAll(scene);
  387. uniqueId = NormalizeOrAll(uniqueId);
  388. return $"{RedisPrefix}:cache:{LinkCacheVersion}:link:{eventType}:{platform}:{typename}:{scene}:{uniqueId}";
  389. }
  390. public static string GetLinkIdCacheKey(int trackId)
  391. {
  392. return $"{RedisPrefix}:cache:{LinkCacheVersion}:linkid:{trackId}";
  393. }
  394. public static async Task<TrackLinkDTO> GetLinkByIdAsync(int trackId)
  395. {
  396. if (trackId <= 0) return null;
  397. string cacheKey = GetLinkIdCacheKey(trackId);
  398. try
  399. {
  400. var cached = RedisHelper.Get<TrackLinkDTO>(cacheKey);
  401. if (cached != null) return cached;
  402. }
  403. catch { }
  404. try
  405. {
  406. var item = new DBContext.Table("track_links").Get<TrackLinkDTO>("id=@id", new { id = trackId });
  407. if (item != null)
  408. {
  409. _ = RedisHelper.Set(cacheKey, item, LinkCacheExpireSeconds);
  410. }
  411. return item;
  412. }
  413. catch
  414. {
  415. return null;
  416. }
  417. }
  418. public static Task<bool> LogTrackRequestAsync(TrackRequestLogDTO dto)
  419. {
  420. try
  421. {
  422. dto.event_type = NormalizeEventType(dto.event_type);
  423. dto.platform = Normalize(dto.platform);
  424. dto.typename = Normalize(dto.typename);
  425. dto.scene = NormalizeOrAll(dto.scene);
  426. dto.unique_id = NormalizeOrAll(dto.unique_id);
  427. dto.ip = Normalize(dto.ip);
  428. dto.user_agent = Normalize(dto.user_agent);
  429. dto.referer = Normalize(dto.referer);
  430. dto.create_time = DateTime.Now;
  431. RedisHelper.RPush(TrackRequestLogKey, dto);
  432. return Task.FromResult(true);
  433. }
  434. catch
  435. {
  436. return Task.FromResult(false);
  437. }
  438. }
  439. public static async Task<int> InsertTrackRequestLogAsync(int limit, YunhuiKit.RedisClient redis)
  440. {
  441. int count = 0;
  442. try
  443. {
  444. using var conn = DBContext.GetOpenConnection();
  445. for (int i = 0; i < limit; i++)
  446. {
  447. var entity = await redis.LPopAsync<TrackRequestLogDTO>(TrackRequestLogKey);
  448. if (entity == null) break;
  449. try
  450. {
  451. if (entity.create_time == default) entity.create_time = DateTime.Now;
  452. conn.Insert(entity);
  453. count++;
  454. }
  455. catch
  456. {
  457. // ignore malformed item
  458. }
  459. }
  460. }
  461. catch
  462. {
  463. return count;
  464. }
  465. return count;
  466. }
  467. private static string Normalize(string value)
  468. {
  469. return (value ?? string.Empty).Trim();
  470. }
  471. private static string NormalizeReportScene(string value)
  472. {
  473. var scene = Normalize(value);
  474. return scene == "默认场景" ? string.Empty : scene;
  475. }
  476. private static string NormalizeOrAll(string value)
  477. {
  478. var result = Normalize(value);
  479. return string.IsNullOrEmpty(result) ? DefaultDimensionValue : result;
  480. }
  481. private static string NormalizeEventType(string value)
  482. {
  483. return Normalize(value).ToLowerInvariant();
  484. }
  485. private static string GetTrackScene(string parseType, string riskStrategy)
  486. {
  487. parseType = NormalizeReportScene(parseType);
  488. if (!string.IsNullOrEmpty(parseType)) return parseType;
  489. riskStrategy = NormalizeReportScene(riskStrategy);
  490. if (!string.IsNullOrEmpty(riskStrategy)) return riskStrategy;
  491. return DefaultDimensionValue;
  492. }
  493. private static string BuildDailyCountKey(string eventType, int trackId, string dateStr, string scene, int accountId = 0)
  494. {
  495. eventType = NormalizeEventType(eventType);
  496. scene = NormalizeReportScene(scene);
  497. string key = $"{RedisPrefix}:daily:{eventType}:{trackId}:{dateStr}";
  498. if (!string.IsNullOrEmpty(scene)) key += $":{EncodeIndexPart(scene)}";
  499. if (accountId > 0) key += $":account:{accountId}";
  500. return key;
  501. }
  502. private static string BuildHourlyCountKey(string eventType, int trackId, string hourStr, string scene, int accountId = 0)
  503. {
  504. eventType = NormalizeEventType(eventType);
  505. scene = NormalizeReportScene(scene);
  506. string key = $"{RedisPrefix}:hour:{eventType}:{trackId}:{hourStr}";
  507. if (!string.IsNullOrEmpty(scene)) key += $":{EncodeIndexPart(scene)}";
  508. if (accountId > 0) key += $":account:{accountId}";
  509. return key;
  510. }
  511. private static string BuildMetricIndexValue(string eventType, int trackId, string scene, int accountId = 0)
  512. {
  513. eventType = NormalizeEventType(eventType);
  514. scene = NormalizeReportScene(scene);
  515. if (accountId > 0) return $"{eventType}|{trackId}|{EncodeIndexPart(scene)}|{accountId}";
  516. if (string.IsNullOrEmpty(scene)) return $"{eventType}|{trackId}";
  517. return $"{eventType}|{trackId}|{EncodeIndexPart(scene)}";
  518. }
  519. private static string EncodeIndexPart(string value)
  520. {
  521. return Normalize(value).UrlEncode();
  522. }
  523. private static string DecodeIndexPart(string value)
  524. {
  525. try
  526. {
  527. return Normalize(value).UrlDecode();
  528. }
  529. catch
  530. {
  531. return Normalize(value);
  532. }
  533. }
  534. private sealed class DailyReportBucket
  535. {
  536. public string EventType { get; set; } = string.Empty;
  537. public int TrackId { get; set; }
  538. public string Scene { get; set; } = string.Empty;
  539. public int AccountId { get; set; }
  540. public TrackLinkDTO? Link { get; set; }
  541. public int Total { get; set; }
  542. }
  543. private static int GetTrackLinkId(IDbConnection conn, string eventType, string platform, string scene, string uniqueId)
  544. {
  545. try
  546. {
  547. var record = new DBContext.Table(conn, "track_links")
  548. .Fields("id")
  549. .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
  550. new { event_type = eventType, platform, scene, unique_id = uniqueId });
  551. if (record == null) return 0;
  552. return record.id;
  553. }
  554. catch
  555. {
  556. return 0;
  557. }
  558. }
  559. }
  560. }