| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- using System;
- using System.Collections.Generic;
- using System.Data;
- using System.Net;
- using System.Threading.Tasks;
- using Dapper;
- using CSRedis;
- using dodohold.core;
- using System.Text.Json;
- using YunhuiKit;
- namespace molilian.core
- {
- public enum TrackType
- {
- Expose,
- Click
- }
- public partial class TracksCore
- {
- private const string RedisPrefix = ":tracks_v123";
- 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 const string TrackRequestLogKey = ":tracks:request:logs";
- private static readonly HashSet<string> SupportEventTypes = new(StringComparer.OrdinalIgnoreCase) { "expose", "click" };
- /// <summary>
- /// 生成一个监测链接(仅返回 path,不包含域名),同时落地到 track_links
- /// </summary>
- public static Task<TrackLinkDTO> CreateLinkAsync(string eventType, string typename, string scene, string uniqueId, string description = "", bool forceNew = false)
- {
- return CreateLinkAsync(eventType, string.Empty, typename, scene, uniqueId, description, forceNew);
- }
- /// <summary>
- /// 生成一个监测链接(仅返回 path,不包含域名),同时落地到 track_links
- /// </summary>
- public static Task<TrackLinkDTO> CreateLinkAsync(string eventType, string platform, string typename, string scene, string uniqueId, string description = "", bool forceNew = false)
- {
- eventType = NormalizeEventType(eventType);
- platform = Normalize(platform);
- typename = Normalize(typename);
- scene = NormalizeOrAll(scene);
- uniqueId = NormalizeOrAll(uniqueId);
- description = Normalize(description);
- if (!SupportEventTypes.Contains(eventType))
- {
- return Task.FromResult<TrackLinkDTO>(null);
- }
- string cacheKey = GetLinkCacheKey(eventType, platform, typename, scene, uniqueId);
- if (!forceNew)
- {
- try
- {
- int cachedId = RedisHelper.Get<int>(cacheKey);
- if (cachedId > 0)
- {
- var cachedLink = RedisHelper.Get<TrackLinkDTO>(GetLinkIdCacheKey(cachedId));
- if (cachedLink != null) return Task.FromResult(cachedLink);
- }
- }
- catch
- {
- // ignore cache errors
- }
- }
- try
- {
- using var conn = DBContext.GetOpenConnection();
- TrackLinkDTO exist = null;
- if (!forceNew)
- {
- exist = new DBContext.Table(conn, "track_links")
- .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND typename=@typename AND scene=@scene AND unique_id=@unique_id",
- new { event_type = eventType, platform, typename, scene, unique_id = uniqueId });
- }
- if (exist != null)
- {
- exist.description = description;
- string path = BuildPath(exist.id, uniqueId);
- if (string.IsNullOrEmpty(exist.path) || !exist.path.Contains("track_id"))
- {
- exist.path = path;
- new DBContext.Table(conn, "track_links")
- .Add("path", path)
- .Add("platform", platform)
- .Add("typename", typename)
- .Add("description", description)
- .Add("update_time", DateTime.Now)
- .Where("id=@id", new { exist.id })
- .Update();
- }
- _ = RedisHelper.Set(cacheKey, exist.id, LinkCacheExpireSeconds);
- _ = RedisHelper.Set(GetLinkIdCacheKey(exist.id), exist, LinkCacheExpireSeconds);
- return Task.FromResult(exist);
- }
- var item = new TrackLinkDTO
- {
- event_type = eventType,
- platform = platform,
- typename = typename,
- scene = scene,
- unique_id = uniqueId,
- path = string.Empty,
- description = description,
- 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;
- item.path = BuildPath(linkId, uniqueId);
- new DBContext.Table(conn, "track_links")
- .Add("path", item.path)
- .Add("description", description)
- .Add("update_time", DateTime.Now)
- .Where("id=@id", new { item.id })
- .Update();
- }
- _ = RedisHelper.Set(cacheKey, item.id, LinkCacheExpireSeconds);
- _ = RedisHelper.Set(GetLinkIdCacheKey(item.id), item, 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(TrackLinkDTO link, string scene = "", int accountId = 0)
- {
- string dateStr = DateTime.Now.ToString("yyyyMMdd");
- string hourStr = DateTime.Now.ToString("yyyyMMddHH");
- string metricScene = ResolveMetricScene(link, scene);
- accountId = Math.Max(accountId, 0);
- string indexValue = BuildMetricIndexValue(link.event_type, link.id, metricScene);
- var metrics = new List<(string key, string indexKey, string indexValue, int expire)>
- {
- (BuildDailyCountKey(link.event_type, link.id, dateStr, metricScene), $"{RedisPrefix}:daily:index:{dateStr}:track", indexValue, DailyExpireSeconds),
- (BuildHourlyCountKey(link.event_type, link.id, hourStr, metricScene), $"{RedisPrefix}:hour:index:{hourStr}:track", indexValue, HourlyExpireSeconds),
- };
- if (accountId > 0)
- {
- string accountIndexValue = BuildMetricIndexValue(link.event_type, link.id, metricScene, accountId);
- metrics.Add((BuildDailyCountKey(link.event_type, link.id, dateStr, metricScene, accountId), $"{RedisPrefix}:daily:index:{dateStr}:track", accountIndexValue, DailyExpireSeconds));
- metrics.Add((BuildHourlyCountKey(link.event_type, link.id, hourStr, metricScene, accountId), $"{RedisPrefix}:hour:index:{hourStr}:track", accountIndexValue, 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,可高频重复执行。
- /// </summary>
- public static async Task<int> FlushDailyAsync(DateTime targetDate)
- {
- string dateStr = targetDate.ToString("yyyyMMdd");
- string indexKey = $"{RedisPrefix}:daily:index:{dateStr}:track";
- var indexMembers = await RedisHelper.SMembersAsync<string>(indexKey) ?? [];
- if (indexMembers == null || indexMembers.Length == 0) return 0;
- var buckets = new Dictionary<string, DailyReportBucket>();
- using var conn = DBContext.GetOpenConnection();
- var linkCache = new Dictionary<int, TrackLinkDTO>();
- foreach (var member in indexMembers)
- {
- var parts = member.Split('|');
- if (parts.Length < 2 || parts.Length > 4) continue;
- string eventType = NormalizeEventType(parts[0]);
- if (!SupportEventTypes.Contains(eventType)) continue;
- if (!int.TryParse(parts[1], out var trackId) || trackId <= 0) continue;
- bool hasScenePart = parts.Length >= 3;
- string memberScene = hasScenePart ? DecodeIndexPart(parts[2]) : string.Empty;
- int accountId = 0;
- if (parts.Length == 4 && (!int.TryParse(parts[3], out accountId) || accountId <= 0)) continue;
- string countKey = BuildDailyCountKey(eventType, trackId, dateStr, hasScenePart ? memberScene : string.Empty, accountId);
- int total = await RedisHelper.GetAsync<int>(countKey);
- if (total <= 0) continue;
- if (!linkCache.TryGetValue(trackId, out var link))
- {
- link = new DBContext.Table(conn, "track_links")
- .Get<TrackLinkDTO>("id=@id", new { id = trackId });
- if (link != null) linkCache[trackId] = link;
- }
- string reportScene = ResolveMetricScene(link, memberScene);
- string bucketKey = BuildMetricIndexValue(eventType, trackId, reportScene, accountId);
- if (!buckets.TryGetValue(bucketKey, out var bucket))
- {
- bucket = new DailyReportBucket
- {
- EventType = eventType,
- TrackId = trackId,
- Scene = reportScene,
- AccountId = accountId,
- Link = link
- };
- buckets[bucketKey] = bucket;
- }
- bucket.Total += total;
- }
- int rows = 0;
- foreach (var bucket in buckets.Values)
- {
- await UpsertDailyReportAsync(conn, targetDate.Date, bucket.EventType, bucket.TrackId, bucket.Scene, bucket.AccountId, bucket.Link, bucket.Total);
- rows++;
- }
- return rows;
- }
- private static Task<int> UpsertDailyReportAsync(IDbConnection conn, DateTime reportDate, string eventType, int trackId, string scene, int accountId, TrackLinkDTO? link, int total)
- {
- const string sql = @"
- INSERT INTO track_daily_report
- (report_date, event_type, track_link_id, account_id, platform, typename, scene, unique_id, event_count, create_time, update_time)
- VALUES
- (@reportDate, @eventType, @trackId, @accountId, @platform, @typename, @scene, @uniqueId, @eventCount, @now, @now)
- ON DUPLICATE KEY UPDATE
- event_count = VALUES(event_count),
- account_id = VALUES(account_id),
- platform = VALUES(platform),
- typename = VALUES(typename),
- scene = VALUES(scene),
- unique_id = VALUES(unique_id),
- update_time = VALUES(update_time);";
- return conn.ExecuteAsync(sql, new
- {
- reportDate,
- eventType,
- trackId,
- accountId,
- platform = link?.platform ?? string.Empty,
- typename = link?.typename ?? string.Empty,
- scene,
- uniqueId = link?.unique_id ?? string.Empty,
- eventCount = total,
- now = DateTime.Now
- });
- }
- public static async Task<List<TrackHourlyReportDTO>> GetHourlyReportAsync(int trackId, DateTime targetDate, string scene = "", int accountId = 0)
- {
- var result = new List<TrackHourlyReportDTO>();
- if (trackId <= 0) return result;
- var link = await GetLinkByIdAsync(trackId);
- if (link == null) return result;
- string eventType = NormalizeEventType(link.event_type);
- if (!SupportEventTypes.Contains(eventType)) return result;
- string metricScene = ResolveMetricScene(link, scene);
- string linkScene = ResolveMetricScene(link, string.Empty);
- string dateStr = targetDate.ToString("yyyyMMdd");
- accountId = Math.Max(accountId, 0);
- for (int hour = 0; hour < 24; hour++)
- {
- string hourStr = $"{dateStr}{hour:00}";
- int total = await RedisHelper.GetAsync<int>(BuildHourlyCountKey(eventType, trackId, hourStr, metricScene, accountId));
- if (accountId == 0 && !string.IsNullOrEmpty(metricScene) && metricScene == linkScene)
- {
- total += await RedisHelper.GetAsync<int>(BuildHourlyCountKey(eventType, trackId, hourStr, string.Empty));
- }
- result.Add(new TrackHourlyReportDTO
- {
- track_link_id = trackId,
- event_type = eventType,
- platform = link.platform ?? string.Empty,
- typename = link.typename ?? string.Empty,
- scene = metricScene,
- unique_id = link.unique_id ?? string.Empty,
- account_id = accountId,
- report_date = targetDate.Date,
- hour = hour,
- event_count = total
- });
- }
- return result;
- }
- public static string BuildPath(TrackType type, TkDataDTO result)
- {
- int trackId = type == TrackType.Click ? 1 : 2;
- if (result == null) return string.Empty;
- string unique_id = $"1|{result.itemId}_{result.mktId}";
- return BuildPath(trackId, unique_id, GetTrackScene(result.parse_type, result.riskStrategy), result.accountId);
- }
- public static string BuildPath(TrackType type, JdDataDTO result)
- {
- int trackId = type == TrackType.Click ? 19 : 20;
- if (result == null) return string.Empty;
- string unique_id = $"13|{result.shortLinkurl.UrlEncode()}";
- return BuildPath(trackId, unique_id, GetTrackScene(result.parse_type, result.riskStrategy), result.accountId);
- }
- public static string BuildPath(TrackType type, PddDataDTO result)
- {
- int trackId = type == TrackType.Click ? 21 : 22;
- if (result == null) return string.Empty;
- string unique_id = $"9|{result.shortLinkurl.UrlEncode()}";
- return BuildPath(trackId, unique_id, GetTrackScene(result.parse_type, result.riskStrategy), result.accountId);
- }
- public static string BuildBrwSimilarPath(TrackType type, TkDataDTO result, PromotionQueryItemDTO similar_goods = null, int index = 0)
- {
- int trackId = type == TrackType.Click ? 23 : 24;
- if (result == null) return string.Empty;
- string unique_id = $"1|{result.itemId}_{result.mktId}";
- string scene = string.Empty;
- if (similar_goods != null)
- {
- unique_id = $"1|{result.itemId}_{result.mktId}_{index}";
- scene = "similar";
- }
- return BuildPath(trackId, unique_id, scene, result.accountId);
- }
- public static string BuildPath(int trackId, string unique_id = "", string scene = "", int accountId = 0)
- {
- scene = NormalizeReportScene(scene);
- string url = $"https://api.molilian.com/tracks/track?track_id={trackId}&unique_id={unique_id}";
- if (!string.IsNullOrEmpty(scene)) url += $"&scene={scene.UrlEncode()}";
- if (accountId > 0) url += $"&account_id={accountId}";
- return url;
- }
- public static string ResolveMetricScene(TrackLinkDTO? link, string scene = "")
- {
- scene = NormalizeReportScene(scene);
- if (!string.IsNullOrEmpty(scene)) return scene;
- return NormalizeOrAll(link?.scene ?? string.Empty);
- }
- public static string GetLinkCacheKey(string eventType, string typename, string scene, string uniqueId)
- {
- return GetLinkCacheKey(eventType, string.Empty, typename, scene, uniqueId);
- }
- public static string GetLinkCacheKey(string eventType, string platform, string typename, string scene, string uniqueId)
- {
- eventType = NormalizeEventType(eventType);
- platform = Normalize(platform);
- typename = Normalize(typename);
- scene = NormalizeOrAll(scene);
- uniqueId = NormalizeOrAll(uniqueId);
- return $"{RedisPrefix}:link:{eventType}:{platform}:{typename}:{scene}:{uniqueId}";
- }
- public static string GetLinkIdCacheKey(int trackId)
- {
- return $"{RedisPrefix}:linkid:{trackId}";
- }
- public static async Task<TrackLinkDTO> GetLinkByIdAsync(int trackId)
- {
- if (trackId <= 0) return null;
- string cacheKey = GetLinkIdCacheKey(trackId);
- try
- {
- var cached = RedisHelper.Get<TrackLinkDTO>(cacheKey);
- if (cached != null) return cached;
- }
- catch { }
- try
- {
- var item = new DBContext.Table("track_links").Get<TrackLinkDTO>("id=@id", new { id = trackId });
- if (item != null)
- {
- _ = RedisHelper.Set(cacheKey, item, LinkCacheExpireSeconds);
- }
- return item;
- }
- catch
- {
- return null;
- }
- }
- public static Task<bool> LogTrackRequestAsync(TrackRequestLogDTO dto)
- {
- try
- {
- dto.event_type = NormalizeEventType(dto.event_type);
- dto.platform = Normalize(dto.platform);
- dto.typename = Normalize(dto.typename);
- dto.scene = NormalizeOrAll(dto.scene);
- dto.unique_id = NormalizeOrAll(dto.unique_id);
- dto.ip = Normalize(dto.ip);
- dto.user_agent = Normalize(dto.user_agent);
- dto.referer = Normalize(dto.referer);
- dto.create_time = DateTime.Now;
- RedisHelper.RPush(TrackRequestLogKey, dto);
- return Task.FromResult(true);
- }
- catch
- {
- return Task.FromResult(false);
- }
- }
- public static async Task<int> InsertTrackRequestLogAsync(int limit, YunhuiKit.RedisClient redis)
- {
- int count = 0;
- try
- {
- using var conn = DBContext.GetOpenConnection();
- for (int i = 0; i < limit; i++)
- {
- var entity = await redis.LPopAsync<TrackRequestLogDTO>(TrackRequestLogKey);
- if (entity == null) break;
- try
- {
- if (entity.create_time == default) entity.create_time = DateTime.Now;
- conn.Insert(entity);
- count++;
- }
- catch
- {
- // ignore malformed item
- }
- }
- }
- catch
- {
- return count;
- }
- return count;
- }
- private static string Normalize(string value)
- {
- return (value ?? string.Empty).Trim();
- }
- private static string NormalizeReportScene(string value)
- {
- var scene = Normalize(value);
- return scene == "默认场景" ? string.Empty : scene;
- }
- 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 string GetTrackScene(string parseType, string riskStrategy)
- {
- parseType = NormalizeReportScene(parseType);
- if (!string.IsNullOrEmpty(parseType)) return parseType;
- riskStrategy = NormalizeReportScene(riskStrategy);
- if (!string.IsNullOrEmpty(riskStrategy)) return riskStrategy;
- return DefaultDimensionValue;
- }
- private static string BuildDailyCountKey(string eventType, int trackId, string dateStr, string scene, int accountId = 0)
- {
- eventType = NormalizeEventType(eventType);
- scene = NormalizeReportScene(scene);
- string key = $"{RedisPrefix}:daily:{eventType}:{trackId}:{dateStr}";
- if (!string.IsNullOrEmpty(scene)) key += $":{EncodeIndexPart(scene)}";
- if (accountId > 0) key += $":account:{accountId}";
- return key;
- }
- private static string BuildHourlyCountKey(string eventType, int trackId, string hourStr, string scene, int accountId = 0)
- {
- eventType = NormalizeEventType(eventType);
- scene = NormalizeReportScene(scene);
- string key = $"{RedisPrefix}:hour:{eventType}:{trackId}:{hourStr}";
- if (!string.IsNullOrEmpty(scene)) key += $":{EncodeIndexPart(scene)}";
- if (accountId > 0) key += $":account:{accountId}";
- return key;
- }
- private static string BuildMetricIndexValue(string eventType, int trackId, string scene, int accountId = 0)
- {
- eventType = NormalizeEventType(eventType);
- scene = NormalizeReportScene(scene);
- if (accountId > 0) return $"{eventType}|{trackId}|{EncodeIndexPart(scene)}|{accountId}";
- if (string.IsNullOrEmpty(scene)) return $"{eventType}|{trackId}";
- return $"{eventType}|{trackId}|{EncodeIndexPart(scene)}";
- }
- private static string EncodeIndexPart(string value)
- {
- return Normalize(value).UrlEncode();
- }
- private static string DecodeIndexPart(string value)
- {
- try
- {
- return Normalize(value).UrlDecode();
- }
- catch
- {
- return Normalize(value);
- }
- }
- private sealed class DailyReportBucket
- {
- public string EventType { get; set; } = string.Empty;
- public int TrackId { get; set; }
- public string Scene { get; set; } = string.Empty;
- public int AccountId { get; set; }
- public TrackLinkDTO? Link { get; set; }
- public int Total { get; set; }
- }
- 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;
- }
- }
- }
- }
|