|
|
@@ -0,0 +1,315 @@
|
|
|
+using dodohold.core;
|
|
|
+using System.Net;
|
|
|
+using System.Globalization;
|
|
|
+using YunhuiKit;
|
|
|
+
|
|
|
+namespace molilian.core
|
|
|
+{
|
|
|
+ public static class XhsZlongForwardCore
|
|
|
+ {
|
|
|
+ public const string RedisPrefix = ":union_parse:xhs_zlong_forward";
|
|
|
+ public const string ExceptionRedisPrefix = $"{RedisPrefix}:exception";
|
|
|
+ private const string ExceptionAggregateType = "all";
|
|
|
+ private const string ForwardUrl = "https://tracker.z-long.cn/ad/tracker/special/liutiyun/xhs";
|
|
|
+ private static readonly HttpClient HttpClient = new()
|
|
|
+ {
|
|
|
+ Timeout = TimeSpan.FromSeconds(10)
|
|
|
+ };
|
|
|
+
|
|
|
+ public static async Task ForwardAsync(string oaid)
|
|
|
+ {
|
|
|
+ string requestUrl = string.Empty;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ long unixTimeMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
|
+ requestUrl = $"{ForwardUrl}?oaid={WebUtility.UrlEncode(oaid ?? string.Empty)}&datetime={unixTimeMilliseconds}";
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _ = new LoggerLibrary("xhs_zlong_forward")
|
|
|
+ .Info(requestUrl)
|
|
|
+ .Info(ex.Message, ex.StackTrace)
|
|
|
+ .SaveAsync();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ SaveCount();
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _ = new LoggerLibrary("xhs_zlong_forward")
|
|
|
+ .Info(requestUrl)
|
|
|
+ .Info(ex.Message, ex.StackTrace)
|
|
|
+ .SaveAsync();
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using var response = await HttpClient.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead);
|
|
|
+ if (response.IsSuccessStatusCode) return;
|
|
|
+
|
|
|
+ SaveExceptionCount($"status_{(int)response.StatusCode}");
|
|
|
+ }
|
|
|
+ catch (TaskCanceledException)
|
|
|
+ {
|
|
|
+ SaveExceptionCount("timeout");
|
|
|
+ }
|
|
|
+ catch (Exception)
|
|
|
+ {
|
|
|
+ SaveExceptionCount("request_error");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void SaveCount()
|
|
|
+ {
|
|
|
+ DateTime now = DateTime.Now;
|
|
|
+ var metrics = GetBucketMetrics(now);
|
|
|
+
|
|
|
+ foreach (var metric in metrics)
|
|
|
+ {
|
|
|
+ string key = GetCountKey(metric.bucketType, metric.bucketValue);
|
|
|
+ SaveMetricCount(key, GetIndexKey(metric.bucketType), metric.expireSeconds);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static async Task<XhsZlongForwardReportResult> QueryReportAsync(string bucketType, DateTime? start, DateTime? end, int page, int size)
|
|
|
+ {
|
|
|
+ string normalizedBucketType = NormalizeBucketType(bucketType);
|
|
|
+ page = Math.Max(page, 1);
|
|
|
+ size = size <= 0 ? 20 : size;
|
|
|
+
|
|
|
+ var list = new List<XhsZlongForwardReportItem>();
|
|
|
+ string[] keys = await TkLogCore.GetTotalKeysAsync(GetIndexKey(normalizedBucketType));
|
|
|
+
|
|
|
+ DateTime? startTime = start?.Date;
|
|
|
+ DateTime? endTime = end?.Date.AddDays(1).AddTicks(-1);
|
|
|
+
|
|
|
+ foreach (var key in keys)
|
|
|
+ {
|
|
|
+ if (!TryParseKey(key, out var item)) continue;
|
|
|
+ if (!string.Equals(item.bucket_type, normalizedBucketType, StringComparison.OrdinalIgnoreCase)) continue;
|
|
|
+ if (startTime.HasValue && item.report_time < startTime.Value) continue;
|
|
|
+ if (endTime.HasValue && item.report_time > endTime.Value) continue;
|
|
|
+
|
|
|
+ list.Add(item);
|
|
|
+ }
|
|
|
+
|
|
|
+ var totalTasks = list.Select(async item =>
|
|
|
+ {
|
|
|
+ var totalTask = TkLogCore.GetTotalAsync(item.redis_key);
|
|
|
+ var exceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(ExceptionAggregateType, item.bucket_type, item.bucket_value));
|
|
|
+ await Task.WhenAll(totalTask, exceptionTask);
|
|
|
+
|
|
|
+ item.total = totalTask.Result;
|
|
|
+ item.exception_count = exceptionTask.Result;
|
|
|
+ return item;
|
|
|
+ });
|
|
|
+
|
|
|
+ list = (await Task.WhenAll(totalTasks))
|
|
|
+ .Where(x => x.total > 0)
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ list = list
|
|
|
+ .OrderByDescending(x => x.report_time)
|
|
|
+ .ThenByDescending(x => x.bucket_value)
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ int count = list.Count;
|
|
|
+ int skip = (page - 1) * size;
|
|
|
+ int pageCount = count == 0 ? 0 : (int)Math.Ceiling(count / (double)size);
|
|
|
+
|
|
|
+ return new XhsZlongForwardReportResult
|
|
|
+ {
|
|
|
+ list = list.Skip(skip).Take(size).ToList(),
|
|
|
+ page = page,
|
|
|
+ size = size,
|
|
|
+ count = count,
|
|
|
+ pageCount = pageCount,
|
|
|
+ summary = await GetSummaryAsync(),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ public static async Task<XhsZlongForwardReportSummary> GetSummaryAsync()
|
|
|
+ {
|
|
|
+ DateTime now = DateTime.Now;
|
|
|
+ var hourTask = TkLogCore.GetTotalAsync(GetCountKey("hour", now.ToString("yyyyMMddHH")));
|
|
|
+ var dayTask = TkLogCore.GetTotalAsync(GetCountKey("day", now.ToString("yyyyMMdd")));
|
|
|
+ var monthTask = TkLogCore.GetTotalAsync(GetCountKey("month", now.ToString("yyyyMM")));
|
|
|
+ var yearTask = TkLogCore.GetTotalAsync(GetCountKey("year", now.ToString("yyyy")));
|
|
|
+
|
|
|
+ var hourExceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(ExceptionAggregateType, "hour", now.ToString("yyyyMMddHH")));
|
|
|
+ var dayExceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(ExceptionAggregateType, "day", now.ToString("yyyyMMdd")));
|
|
|
+ var monthExceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(ExceptionAggregateType, "month", now.ToString("yyyyMM")));
|
|
|
+ var yearExceptionTask = TkLogCore.GetTotalAsync(GetExceptionCountKey(ExceptionAggregateType, "year", now.ToString("yyyy")));
|
|
|
+
|
|
|
+ await Task.WhenAll(hourTask, dayTask, monthTask, yearTask, hourExceptionTask, dayExceptionTask, monthExceptionTask, yearExceptionTask);
|
|
|
+
|
|
|
+ return new XhsZlongForwardReportSummary
|
|
|
+ {
|
|
|
+ hour = hourTask.Result,
|
|
|
+ day = dayTask.Result,
|
|
|
+ month = monthTask.Result,
|
|
|
+ year = yearTask.Result,
|
|
|
+ hour_exception = hourExceptionTask.Result,
|
|
|
+ day_exception = dayExceptionTask.Result,
|
|
|
+ month_exception = monthExceptionTask.Result,
|
|
|
+ year_exception = yearExceptionTask.Result,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string NormalizeBucketType(string bucketType)
|
|
|
+ {
|
|
|
+ return bucketType?.Trim().ToLowerInvariant() switch
|
|
|
+ {
|
|
|
+ "hour" => "hour",
|
|
|
+ "month" => "month",
|
|
|
+ "year" => "year",
|
|
|
+ _ => "day",
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string GetIndexKey(string bucketType)
|
|
|
+ {
|
|
|
+ return $"{RedisPrefix}:index:{bucketType}";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string GetCountKey(string bucketType, string bucketValue)
|
|
|
+ {
|
|
|
+ return $"{RedisPrefix}:{bucketType}:{bucketValue}";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string GetExceptionCountKey(string exceptionType, string bucketType, string bucketValue)
|
|
|
+ {
|
|
|
+ return $"{ExceptionRedisPrefix}:{exceptionType}:{bucketType}:{bucketValue}";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string GetExceptionIndexKey(string exceptionType, string bucketType)
|
|
|
+ {
|
|
|
+ return $"{ExceptionRedisPrefix}:index:{exceptionType}:{bucketType}";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static (string bucketType, string bucketValue, int expireSeconds)[] GetBucketMetrics(DateTime now)
|
|
|
+ {
|
|
|
+ return
|
|
|
+ [
|
|
|
+ ("hour", now.ToString("yyyyMMddHH"), 86400 * 90),
|
|
|
+ ("day", now.ToString("yyyyMMdd"), 86400 * 366),
|
|
|
+ ("month", now.ToString("yyyyMM"), 86400 * 3660),
|
|
|
+ ("year", now.ToString("yyyy"), 86400 * 3660),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void SaveMetricCount(string key, string indexKey, int expireSeconds)
|
|
|
+ {
|
|
|
+ RedisHelper.IncrBy(key);
|
|
|
+ RedisHelper.Expire(key, expireSeconds);
|
|
|
+ RedisHelper.SAdd(indexKey, key);
|
|
|
+ RedisHelper.Expire(indexKey, expireSeconds);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void SaveExceptionCount(string exceptionType)
|
|
|
+ {
|
|
|
+ DateTime now = DateTime.Now;
|
|
|
+ var metrics = GetBucketMetrics(now);
|
|
|
+
|
|
|
+ foreach (var metric in metrics)
|
|
|
+ {
|
|
|
+ string key = GetExceptionCountKey(exceptionType, metric.bucketType, metric.bucketValue);
|
|
|
+ SaveMetricCount(key, GetExceptionIndexKey(exceptionType, metric.bucketType), metric.expireSeconds);
|
|
|
+
|
|
|
+ string aggregateKey = GetExceptionCountKey(ExceptionAggregateType, metric.bucketType, metric.bucketValue);
|
|
|
+ SaveMetricCount(aggregateKey, GetExceptionIndexKey(ExceptionAggregateType, metric.bucketType), metric.expireSeconds);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool TryParseKey(string key, out XhsZlongForwardReportItem item)
|
|
|
+ {
|
|
|
+ item = new XhsZlongForwardReportItem();
|
|
|
+ if (string.IsNullOrWhiteSpace(key)) return false;
|
|
|
+
|
|
|
+ var parts = key.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
|
|
+ if (parts.Length < 4) return false;
|
|
|
+
|
|
|
+ string bucketType = parts[^2];
|
|
|
+ string bucketValue = parts[^1];
|
|
|
+ if (!TryParseBucket(bucketType, bucketValue, out var reportTime, out var displayTime)) return false;
|
|
|
+
|
|
|
+ item.bucket_type = bucketType;
|
|
|
+ item.bucket_value = bucketValue;
|
|
|
+ item.report_time = reportTime;
|
|
|
+ item.display_time = displayTime;
|
|
|
+ item.redis_key = key;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool TryParseBucket(string bucketType, string bucketValue, out DateTime reportTime, out string displayTime)
|
|
|
+ {
|
|
|
+ reportTime = DateTime.MinValue;
|
|
|
+ displayTime = string.Empty;
|
|
|
+
|
|
|
+ string format = bucketType switch
|
|
|
+ {
|
|
|
+ "hour" => "yyyyMMddHH",
|
|
|
+ "day" => "yyyyMMdd",
|
|
|
+ "month" => "yyyyMM",
|
|
|
+ "year" => "yyyy",
|
|
|
+ _ => string.Empty,
|
|
|
+ };
|
|
|
+ if (string.IsNullOrEmpty(format)) return false;
|
|
|
+
|
|
|
+ bool success = DateTime.TryParseExact(
|
|
|
+ bucketValue,
|
|
|
+ format,
|
|
|
+ CultureInfo.InvariantCulture,
|
|
|
+ DateTimeStyles.None,
|
|
|
+ out reportTime);
|
|
|
+
|
|
|
+ if (!success) return false;
|
|
|
+
|
|
|
+ displayTime = bucketType switch
|
|
|
+ {
|
|
|
+ "hour" => reportTime.ToString("yyyy-MM-dd HH:00"),
|
|
|
+ "day" => reportTime.ToString("yyyy-MM-dd"),
|
|
|
+ "month" => reportTime.ToString("yyyy-MM"),
|
|
|
+ "year" => reportTime.ToString("yyyy"),
|
|
|
+ _ => bucketValue,
|
|
|
+ };
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public class XhsZlongForwardReportSummary
|
|
|
+ {
|
|
|
+ public long hour { get; set; }
|
|
|
+ public long day { get; set; }
|
|
|
+ public long month { get; set; }
|
|
|
+ public long year { get; set; }
|
|
|
+ public long hour_exception { get; set; }
|
|
|
+ public long day_exception { get; set; }
|
|
|
+ public long month_exception { get; set; }
|
|
|
+ public long year_exception { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ public class XhsZlongForwardReportItem
|
|
|
+ {
|
|
|
+ public string bucket_type { get; set; } = string.Empty;
|
|
|
+ public string bucket_value { get; set; } = string.Empty;
|
|
|
+ public DateTime report_time { get; set; } = DateTime.MinValue;
|
|
|
+ public string display_time { get; set; } = string.Empty;
|
|
|
+ public long total { get; set; }
|
|
|
+ public long exception_count { get; set; }
|
|
|
+ public string redis_key { get; set; } = string.Empty;
|
|
|
+ }
|
|
|
+
|
|
|
+ public class XhsZlongForwardReportResult
|
|
|
+ {
|
|
|
+ public int page { get; set; }
|
|
|
+ public int pageCount { get; set; }
|
|
|
+ public int size { get; set; }
|
|
|
+ public int count { get; set; }
|
|
|
+ public List<XhsZlongForwardReportItem> list { get; set; } = [];
|
|
|
+ public XhsZlongForwardReportSummary summary { get; set; } = new();
|
|
|
+ }
|
|
|
+}
|