Quellcode durchsuchen

✨ feat(监控链接): 调整字段

dodo hold vor 7 Monaten
Ursprung
Commit
e3eabc9e6d

+ 68 - 0
molilian.api/Controllers/admin/TRACKS_API.md

@@ -0,0 +1,68 @@
+# 监测链接接口说明(含 admin 鉴权)
+
+## 业务场景
+- 为第三方提供优惠券/活动链接,对曝光(`expose`)与点击(`click`)进行监测。
+- 监测数据以多维度(平台 / 场景 / 场景+唯一ID)写入 Redis,并定时落库到 `track_daily_report`,可与 `push_data_report` 汇总分析。
+- 触发接口高频,默认只触达 Redis;首次访问会通过缓存判断并自动补全 `track_links` 记录。
+
+## 名词与存储
+- `track_links`:监测链接配置表(event_type, typename(平台,来源 platformOptions), scene, unique_id, path, description)。
+- `track_daily_report`:日聚合表(event_count, track_link_id 等)。
+- Redis 缓存键:
+  - 链接缓存:`:tracks:link:{eventType}:{platform}:{scene}:{uniqueId}`(30 天)。
+  - 事件计数(日维):`:tracks:daily:{eventType}:{platform}:{scene}:{uniqueId}:{yyyyMMdd}`。
+  - 事件计数(时维):`:tracks:hour:{eventType}:{platform}:{scene}:{uniqueId}:{yyyyMMddHH}`。
+  - 索引集(枚举有数据的维度):`:tracks:daily:index:{yyyyMMdd}:unique` 等。
+- 维度缺省值:`scene`/`uniqueId` 为空时使用空字符串。
+- 路径模板(交付方自行补域名):`/Tracks/Track?track_id={id}&eventType={eventType}`。
+
+## 公共接口(无需鉴权)
+
+### 触发监测
+- `GET /Tracks/Track`
+- 用途:记录曝光/点击,快速返回(fire-and-forget)。
+- 参数:
+  - `track_id` *(必填)*:监测链接 ID
+  - `eventType` *(必填)*:`expose` 或 `click`
+- 响应:`{ success: true, message: "ok" }`
+- 说明:接口不会自动创建链接,必须先调用生成接口拿到 `track_id`。
+
+### 生成监测链接
+- `POST /Tracks/Create`
+- 参数:`eventType` *(必填)*,`typename`、`scene`、`uniqueId`、`description` *(可空,默认空字符串)*
+- 响应:`{ success: true, message: "ok", url: "/Tracks/Track?track_id={id}&eventType=..." }`
+- 说明:如果已存在则复用;内部写入 `track_links` 与缓存。
+
+### 日报落库
+- `GET /Tracks/Daily`
+- 参数:
+  - `daysAgo`:默认 1(统计昨天)
+  - `reportDate`:`YYYY-MM-DD`,优先于 daysAgo
+- 响应:`{ success: true, message: "ok", rows: <写入条数> }`
+- 说明:读取 Redis 的日索引与计数,落库 `track_daily_report`。
+
+## 管理端接口(需 admin 鉴权,路径前缀 `/api/TracksAdmin/*`)
+支持查询/查看/新增/删除/报表查询。
+
+### 列表
+- `POST /api/TracksAdmin/list`
+- Body:`{ "current":1, "pageSize":10, "getTotal":true, "sort":"id", "order":"DESC|ASC|descending|ascending", "keyword":"tb" }`
+- 说明:按 event_type/typename/scene/unique_id 模糊搜索。
+
+### 详情
+- `GET /api/TracksAdmin/info?id=123`
+
+### 新增
+- `POST /api/TracksAdmin/create`
+- Body:`{ "event_type":"expose", "typename":1, "scene":"coupon", "unique_id":"u1", "description":"xxx" }`
+- 说明:`typename` 取值来自前端 `platformOptions`(pushReport.vue 引用的 `platformList`),后台创建始终生成新链接(不复用)。
+
+### 删除
+- `POST /api/TracksAdmin/delete`
+- Body:`{ "id":123 }`
+- 说明:删除 DB 记录并清理缓存 `:tracks:link:{eventType}:{platform}:{scene}:{uniqueId}`。
+
+### 报表列表
+- `POST /api/TracksAdmin/report`
+- Body:`{ "current":1, "pageSize":10, "getTotal":true, "track_link_id":123, "event_type":"expose", "platform":"tb", "scene":"coupon", "unique_id":"u1", "start":"2024-01-01", "end":"2024-01-31" }`
+- 说明:按链接/事件/平台/场景/唯一ID及日期范围筛选 `track_daily_report`,分页返回。

+ 1 - 1
molilian.api/Controllers/admin/TracksAdminController.cs

@@ -102,7 +102,7 @@ namespace molilian.api.Controllers
         public async Task<ActionResult> create([FromBody] TrackLinkDTO data)
         {
             var token = provider.Get(_accessor.HttpContext);
-            var link = await TracksCore.CreateLinkAsync(data.event_type, data.platform, data.scene, data.unique_id);
+            var link = await TracksCore.CreateLinkAsync(data.event_type, data.typename, data.scene, data.unique_id, data.description, true);
             bool success = link != null && link.id > 0;
             return new APIResult(new
             {

+ 14 - 7
molilian.api/Controllers/public/TracksController.cs

@@ -19,21 +19,27 @@ namespace molilian.api.Controllers
         }
 
         [HttpGet]
-        public ActionResult Track([FromQuery] string eventType, [FromQuery] string platform, [FromQuery] string scene, [FromQuery] string uniqueId = "")
+        public async Task<ActionResult> Track([FromQuery] string eventType, [FromQuery] int track_id = 0)
         {
-            if (string.IsNullOrEmpty(platform) || string.IsNullOrEmpty(scene) || string.IsNullOrEmpty(eventType))
+            if (track_id <= 0 || string.IsNullOrEmpty(eventType))
             {
                 return new APIResult(new { success = false, message = "miss" });
             }
-            _ = TracksCore.CreateLinkAsync(eventType, platform, scene, uniqueId);
-            _ = TracksCore.TrackAsync(eventType, platform, scene, uniqueId);
+
+            var link = await TracksCore.GetLinkByIdAsync(track_id);
+            if (link == null)
+            {
+                return new APIResult(new { success = false, message = "not found" });
+            }
+
+            _ = TracksCore.TrackAsync(eventType, link.platform, link.scene, link.unique_id);
             return new APIResult(new { success = true, message = "ok" });
         }
 
         [HttpPost]
-        public async Task<ActionResult> Create([FromQuery] string eventType, [FromQuery] string platform, [FromQuery] string scene, [FromQuery] string uniqueId = "")
+        public async Task<ActionResult> Create([FromQuery] string eventType, [FromQuery] string typename = "", [FromQuery] string scene = "", [FromQuery] string uniqueId = "", [FromQuery] string description = "")
         {
-            var link = await TracksCore.CreateLinkAsync(eventType, platform, scene, uniqueId);
+            var link = await TracksCore.CreateLinkAsync(eventType, typename, scene, uniqueId, description);
             if (link == null)
             {
                 return new APIResult(new { success = false, message = "invalid params or eventType" });
@@ -57,7 +63,8 @@ namespace molilian.api.Controllers
                 date = DateTime.Now.AddDays(-daysAgo).Date;
             }
 
-            int rows = await TracksCore.FlushDailyAsync(date);
+            int rows = await TracksCore.FlushDailyAsync(DateTime.Now.Date);
+            rows += await TracksCore.FlushDailyAsync(date);
             return new APIResult(new { success = true, message = "ok", rows });
 
         }

+ 78 - 37
molilian.core/Core/TracksCore.cs

@@ -11,7 +11,7 @@ namespace molilian.core
 {
     public partial class TracksCore
     {
-        private const string RedisPrefix = ":tracks";
+        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 = "";
@@ -21,71 +21,75 @@ namespace molilian.core
         /// <summary>
         /// 生成一个监测链接(仅返回 path,不包含域名),同时落地到 track_links
         /// </summary>
-        public static Task<TrackLinkDTO> CreateLinkAsync(string eventType, string platform, string scene, string uniqueId)
+        public static Task<TrackLinkDTO> CreateLinkAsync(string eventType, 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) || string.IsNullOrEmpty(platform))
+            if (!SupportEventTypes.Contains(eventType))
             {
                 return Task.FromResult<TrackLinkDTO>(null);
             }
 
-            string cacheKey = $"{RedisPrefix}:link:{eventType}:{platform}:{scene}:{uniqueId}";
-            try
+            string cacheKey = $"{RedisPrefix}:link:{eventType}:{typename}:{scene}:{uniqueId}";
+            if (!forceNew)
             {
-                int cachedId = RedisHelper.Get<int>(cacheKey);
-                if (cachedId > 0)
+                try
                 {
-                    string cachedPath = BuildPath(eventType, platform, scene, uniqueId);
-                    return Task.FromResult(new TrackLinkDTO
+                    int cachedId = RedisHelper.Get<int>(cacheKey);
+                    if (cachedId > 0)
                     {
-                        id = cachedId,
-                        event_type = eventType,
-                        platform = platform,
-                        scene = scene,
-                        unique_id = uniqueId,
-                        path = cachedPath
-                    });
+                        var cachedLink = RedisHelper.Get<TrackLinkDTO>(GetLinkIdCacheKey(cachedId));
+                        if (cachedLink != null) return Task.FromResult(cachedLink);
+                    }
+                }
+                catch
+                {
+                    // ignore cache errors
                 }
-            }
-            catch
-            {
-                // ignore cache errors
             }
 
-            string path = BuildPath(eventType, platform, scene, uniqueId);
             try
             {
                 using var conn = DBContext.GetOpenConnection();
-                var exist = new DBContext.Table(conn, "track_links")
-                    .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 });
+                TrackLinkDTO exist = null;
+                if (!forceNew)
+                {
+                    exist = new DBContext.Table(conn, "track_links")
+                        .Get<TrackLinkDTO>("event_type=@event_type AND platform=@platform AND scene=@scene AND unique_id=@unique_id",
+                            new { event_type = eventType, platform = typename, scene, unique_id = uniqueId });
+                }
 
                 if (exist != null)
                 {
-                    if (string.IsNullOrEmpty(exist.path))
+                    exist.description = description;
+                    string path = BuildPath(exist.id, eventType);
+                    if (string.IsNullOrEmpty(exist.path) || !exist.path.Contains("track_id"))
                     {
                         exist.path = path;
                         new DBContext.Table(conn, "track_links")
                             .Add("path", path)
+                            .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,
+                    platform = typename,
                     scene = scene,
                     unique_id = uniqueId,
-                    path = path,
+                    path = string.Empty,
+                    description = description,
                     create_time = DateTime.Now,
                     update_time = DateTime.Now
                 };
@@ -94,8 +98,16 @@ namespace molilian.core
                 if (id != null && int.TryParse(id.ToString(), out var linkId))
                 {
                     item.id = linkId;
+                    item.path = BuildPath(linkId, eventType);
+                    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)
@@ -117,7 +129,7 @@ namespace molilian.core
             scene = NormalizeOrAll(scene);
             uniqueId = NormalizeOrAll(uniqueId);
 
-            if (!SupportEventTypes.Contains(eventType) || string.IsNullOrEmpty(platform))
+            if (!SupportEventTypes.Contains(eventType))
             {
                 return Task.FromResult(false);
             }
@@ -233,22 +245,51 @@ namespace molilian.core
             return rows;
         }
 
-        public static string BuildPath(string eventType, string platform, string scene, string uniqueId)
+        public static string BuildPath(int trackId, string eventType)
         {
+            eventType = NormalizeEventType(eventType);
             string safeEventType = WebUtility.UrlEncode(eventType);
-            string safePlatform = WebUtility.UrlEncode(platform);
-            string safeScene = WebUtility.UrlEncode(scene);
-            string safeUniqueId = WebUtility.UrlEncode(uniqueId);
-            return $"https://api.molilian.com/tracks/track?eventType={safeEventType}&platform={safePlatform}&scene={safeScene}&uniqueId={safeUniqueId}";
+            return $"https://api.molilian.com/tracks/track?track_id={trackId}&eventType={safeEventType}";
         }
 
-        public static string GetLinkCacheKey(string eventType, string platform, string scene, string uniqueId)
+        public static string GetLinkCacheKey(string eventType, 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}:{scene}:{uniqueId}";
+            return $"{RedisPrefix}:link:{eventType}:{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;
+            }
         }
 
         private static string Normalize(string value)

+ 6 - 0
molilian.core/DTO/TracksDTO.cs

@@ -10,9 +10,15 @@ namespace molilian.core
         public int id { get; set; }
         public string event_type { get; set; } = string.Empty;
         public string platform { get; set; } = string.Empty;
+        public string typename
+        {
+            get => platform;
+            set => platform = value;
+        }
         public string scene { get; set; } = string.Empty;
         public string unique_id { get; set; } = string.Empty;
         public string path { get; set; } = string.Empty;
+        public string description { get; set; } = string.Empty;
         public DateTime create_time { get; set; } = DateTime.Now;
         public DateTime update_time { get; set; } = DateTime.Now;
     }