|
@@ -1,143 +1,143 @@
|
|
|
using System;
|
|
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
|
|
|
|
|
|
|
+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
|
|
|
{
|
|
{
|
|
|
- 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 LinkCacheVersion = "v2";
|
|
|
|
|
- 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();
|
|
|
|
|
|
|
+ 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 LinkCacheVersion = "v2";
|
|
|
|
|
+ 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);
|
|
return Task.FromResult<TrackLinkDTO>(null);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -145,42 +145,42 @@ namespace molilian.core
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 曝光/点击触发:计入 Redis,失败不影响返回
|
|
/// 曝光/点击触发:计入 Redis,失败不影响返回
|
|
|
/// </summary>
|
|
/// </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")
|
|
|
|
|
|
|
+ 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)
|
|
.Info(ex.Message, ex.StackTrace)
|
|
|
.SaveAsync();
|
|
.SaveAsync();
|
|
|
return Task.FromResult(false);
|
|
return Task.FromResult(false);
|
|
@@ -188,412 +188,441 @@ namespace molilian.core
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// 日报:拉取指定日期 Redis 计数并落地到 track_daily_report,可高频重复执行。
|
|
|
|
|
|
|
+ /// 日报:拉取指定日期 Redis 计数并落地到 track_daily_report,可高频重复执行。
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public static async Task<int> FlushDailyAsync(DateTime targetDate)
|
|
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;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (link != null)
|
|
|
|
|
- {
|
|
|
|
|
- string linkEventType = NormalizeEventType(link.event_type);
|
|
|
|
|
- if (!SupportEventTypes.Contains(linkEventType)) continue;
|
|
|
|
|
- if (!string.Equals(eventType, linkEventType, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
- {
|
|
|
|
|
- _ = new LoggerLibrary("TracksCore", "FlushDaily")
|
|
|
|
|
- .Info($"skip stale track bucket: member={member}, link_event_type={linkEventType}", string.Empty)
|
|
|
|
|
- .SaveAsync();
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- eventType = linkEventType;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- 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}:cache:{LinkCacheVersion}:link:{eventType}:{platform}:{typename}:{scene}:{uniqueId}";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public static string GetLinkIdCacheKey(int trackId)
|
|
|
|
|
- {
|
|
|
|
|
- return $"{RedisPrefix}:cache:{LinkCacheVersion}: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 });
|
|
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (link != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ string linkEventType = NormalizeEventType(link.event_type);
|
|
|
|
|
+ if (!SupportEventTypes.Contains(linkEventType)) continue;
|
|
|
|
|
+ if (!string.Equals(eventType, linkEventType, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
+ {
|
|
|
|
|
+ _ = new LoggerLibrary("TracksCore", "FlushDaily")
|
|
|
|
|
+ .Info($"skip stale track bucket: member={member}, link_event_type={linkEventType}", string.Empty)
|
|
|
|
|
+ .SaveAsync();
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ eventType = linkEventType;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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));
|
|
|
|
|
+ }
|
|
|
|
|
+ int callCount = await GetTrackHourlyCallCountAsync(link, hourStr, accountId);
|
|
|
|
|
+
|
|
|
|
|
+ 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,
|
|
|
|
|
+ call_count = callCount
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static async Task<int> GetTrackHourlyCallCountAsync(TrackLinkDTO link, string hourStr, int accountId)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (accountId <= 0) return 0;
|
|
|
|
|
+
|
|
|
|
|
+ string channelName = GetTrackCallChannelName(link);
|
|
|
|
|
+ if (string.IsNullOrEmpty(channelName)) return 0;
|
|
|
|
|
+
|
|
|
|
|
+ return await TkLogCore.GetTotalAsync($":parse_total:{channelName}_{accountId}:{hourStr}");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static string GetTrackCallChannelName(TrackLinkDTO link)
|
|
|
|
|
+ {
|
|
|
|
|
+ string platform = Normalize(link.platform).ToLowerInvariant();
|
|
|
|
|
+
|
|
|
|
|
+ if (link.id is 19 or 20 || platform == "jd" || platform == "13" || platform.Contains("京东"))
|
|
|
|
|
+ {
|
|
|
|
|
+ return "jd";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (link.id is 21 or 22 || platform == "pdd" || platform == "9" || platform.Contains("拼多多"))
|
|
|
|
|
+ {
|
|
|
|
|
+ return "pdd";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return "tb";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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}:cache:{LinkCacheVersion}:link:{eventType}:{platform}:{typename}:{scene}:{uniqueId}";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static string GetLinkIdCacheKey(int trackId)
|
|
|
|
|
+ {
|
|
|
|
|
+ return $"{RedisPrefix}:cache:{LinkCacheVersion}: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;
|
|
if (record == null) return 0;
|
|
|
- return record.id;
|
|
|
|
|
- }
|
|
|
|
|
- catch
|
|
|
|
|
- {
|
|
|
|
|
- return 0;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return record.id;
|
|
|
|
|
+ }
|
|
|
|
|
+ catch
|
|
|
|
|
+ {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-}
|
|
|
|
|
|
|
+
|
|
|
|
|
+}
|