using dodohold.core; using Microsoft.AspNetCore.Mvc; using molilian.core; using System.Text.Json; namespace molilian.api.Controllers { [ApiController] [MyAuthorize("admin")] [Route("api/[controller]/[action]")] public class DeeplinkReportController : ControllerBase { private const int MaxRangeDays = 90; private const string ReportAccountName = "tool"; readonly IAuthorizationProvider provider = new AdminProvider(); protected IHttpContextAccessor _accessor; public DeeplinkReportController(IHttpContextAccessor accessor) { _accessor = accessor; } [HttpPost] public async Task daily([FromBody] JsonElement form) { _ = provider.Get(_accessor.HttpContext); var queryDate = form.PathReadArray("query_date[]"); var channelName = form.Read("channel_name", string.Empty).Trim(); DateTime start = DateTime.Now.Date.AddDays(-6); DateTime end = DateTime.Now.Date; if (queryDate.Count == 2) { if (DateTime.TryParse(queryDate[0], out var parsedStart)) { start = parsedStart.Date; } if (DateTime.TryParse(queryDate[1], out var parsedEnd)) { end = parsedEnd.Date; } } if (end < start) { (start, end) = (end, start); } if ((end - start).TotalDays >= MaxRangeDays) { end = start.AddDays(MaxRangeDays - 1); } var isChannelFiltered = !string.IsNullOrWhiteSpace(channelName); var reportChannels = GetReportChannels(channelName); var channelNames = reportChannels .Select(item => item.channel_name) .ToList(); var channelTotals = channelNames.ToDictionary( item => item, _ => 0L, StringComparer.OrdinalIgnoreCase ); var dailyRows = new List(); for (var date = start; date <= end; date = date.AddDays(1)) { var dateKey = date.ToString("yyyyMMdd"); var indexedChannelNames = await TkLogCore.GetTotalKeysAsync( $":parse_total:{ReportAccountName}:channels:{dateKey}" ); var useLegacyChannelStats = indexedChannelNames.Length == 0; var dailyReportChannels = isChannelFiltered ? reportChannels : GetIndexedReportChannels(reportChannels, indexedChannelNames); var channelStats = useLegacyChannelStats ? await GetLegacyChannelStatsAsync(dailyReportChannels, dateKey) : await GetChannelStatsAsync(dailyReportChannels, dateKey); var reportDate = date.ToString("yyyy-MM-dd"); var accountTotalCount = channelStats.Sum(item => item.total_count); var accountSuccessCount = channelStats.Sum(item => item.success_count); var accountFailCount = channelStats.Sum(item => item.fail_count); foreach (var item in channelStats) { channelTotals.TryAdd(item.channel_name, 0); channelTotals[item.channel_name] += item.total_count; } var row = new DeeplinkDailyReportRow { row_key = reportDate, row_type = "daily", report_date = reportDate, request_count = accountTotalCount, total_count = accountTotalCount, success_count = accountSuccessCount, fail_count = accountFailCount }; row.children = channelStats .Select(item => new DeeplinkDailyReportRow { row_key = $"{reportDate}:{item.channel_name}", row_type = "channel", report_date = item.display_name, channel_name = item.channel_name, display_name = item.display_name, request_count = item.request_count, total_count = item.total_count, success_count = item.success_count, fail_count = item.fail_count }) .ToList(); dailyRows.Add(row); } var channels = channelTotals .Where(item => !string.IsNullOrWhiteSpace(channelName) || item.Value > 0) .OrderByDescending(item => item.Value) .ThenBy(item => item.Key) .Select(item => new DeeplinkDailyReportChannel { channel_name = item.Key, display_name = GetDisplayName(reportChannels, item.Key), request_count = item.Value, total_count = item.Value }) .ToList(); var activeChannelNames = channels .Select(item => item.channel_name) .ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var row in dailyRows) { row.children = (row.children ?? new List()) .Where(item => activeChannelNames.Contains(item.channel_name)) .OrderByDescending(item => item.total_count) .ThenBy(item => item.channel_name) .ToList(); if (!row.children.Any()) { row.children = null; } } var list = dailyRows .OrderByDescending(item => item.report_date) .ToList(); var summary = new DeeplinkDailyReportSummary { request_count = list.Sum(item => item.request_count), total_count = list.Sum(item => item.total_count), success_count = list.Sum(item => item.success_count), fail_count = list.Sum(item => item.fail_count) }; return new APIResult(new { data = new { list, count = list.Count, summary, channels, maxRangeDays = MaxRangeDays } }); } private static List GetReportChannels(string channelName) { var channels = new Dictionary(StringComparer.OrdinalIgnoreCase); try { var rules = new DBContext.Table("deeplink_parse_rule") .Select(); if (rules != null) { foreach (var rule in rules) { AddChannel(channels, rule.channel_name, rule.name); } } } catch { // Channel names are display metadata. Redis statistics remain usable if this lookup fails. } if (!string.IsNullOrWhiteSpace(channelName)) { var selectedDisplayName = channels.TryGetValue(channelName, out var configuredDisplayName) ? configuredDisplayName : channelName; channels.Clear(); AddChannel(channels, channelName, selectedDisplayName); } else { foreach (var name in Enum.GetNames(typeof(TkChannelEnum))) { AddChannel(channels, name, name); } } return channels .OrderBy(item => item.Key) .Select(item => new DeeplinkDailyReportChannel { channel_name = item.Key, display_name = item.Value }) .ToList(); } private static void AddChannel( Dictionary channels, string channelName, string displayName ) { if (string.IsNullOrWhiteSpace(channelName)) return; var normalizedChannel = channelName.Trim(); var normalizedDisplay = string.IsNullOrWhiteSpace(displayName) ? normalizedChannel : displayName.Trim(); if (!channels.ContainsKey(normalizedChannel) || channels[normalizedChannel].Equals(normalizedChannel, StringComparison.OrdinalIgnoreCase)) { channels[normalizedChannel] = normalizedDisplay; } } private static string GetDisplayName( List channels, string channelName ) { return channels .FirstOrDefault(item => item.channel_name.Equals( channelName, StringComparison.OrdinalIgnoreCase )) ?.display_name ?? channelName; } private static async Task> GetChannelStatsAsync( List channels, string dateKey ) { var tasks = channels.Select(async channel => { var channelBucket = $"{ReportAccountName}:channel:{channel.channel_name}"; var channelTotalCount = await TkLogCore.GetTotalAsync($":parse_total:{channelBucket}:{dateKey}"); return new DeeplinkDailyReportChannel { channel_name = channel.channel_name, display_name = channel.display_name, request_count = channelTotalCount, total_count = channelTotalCount, success_count = await TkLogCore.GetTotalAsync($":parse_total:{channelBucket}:success:{dateKey}"), fail_count = await TkLogCore.GetTotalAsync($":parse_total:{channelBucket}:fail:{dateKey}") }; }); var results = await Task.WhenAll(tasks); return results.ToList(); } private static async Task> GetLegacyChannelStatsAsync( List channels, string dateKey ) { var tasks = channels .Where(channel => IsLegacyReportSourceChannel(channel.channel_name)) .Select(async channel => { var channelTotalCount = await TkLogCore.GetTotalAsync( $":parse_total:{channel.channel_name}:{dateKey}" ); return new DeeplinkDailyReportChannel { channel_name = channel.channel_name, display_name = channel.display_name, request_count = channelTotalCount, total_count = channelTotalCount, success_count = await TkLogCore.GetTotalAsync( $":parse_total:{channel.channel_name}:success:{dateKey}" ), fail_count = await TkLogCore.GetTotalAsync( $":parse_total:{channel.channel_name}:fail:{dateKey}" ) }; }); var results = await Task.WhenAll(tasks); return results.ToList(); } private static bool IsLegacyReportSourceChannel(string channelName) { return !channelName.Equals("all", StringComparison.OrdinalIgnoreCase) && !channelName.Equals(ReportAccountName, StringComparison.OrdinalIgnoreCase); } private static List GetIndexedReportChannels( List configuredChannels, IEnumerable indexedChannelNames ) { var channels = configuredChannels.ToDictionary( item => item.channel_name, item => item.display_name, StringComparer.OrdinalIgnoreCase ); foreach (var indexedChannelName in indexedChannelNames) { AddChannel(channels, indexedChannelName, indexedChannelName); } return channels .OrderBy(item => item.Key) .Select(item => new DeeplinkDailyReportChannel { channel_name = item.Key, display_name = item.Value }) .ToList(); } } public class DeeplinkDailyReportChannel { public string channel_name { get; set; } = string.Empty; public string display_name { get; set; } = string.Empty; public long request_count { get; set; } = 0; public long total_count { get; set; } = 0; public long success_count { get; set; } = 0; public long fail_count { get; set; } = 0; } public class DeeplinkDailyReportRow { public string row_key { get; set; } = string.Empty; public string row_type { get; set; } = "daily"; public string report_date { get; set; } = string.Empty; public string channel_name { get; set; } = string.Empty; public string display_name { get; set; } = string.Empty; public long request_count { get; set; } = 0; public long total_count { get; set; } = 0; public long success_count { get; set; } = 0; public long fail_count { get; set; } = 0; public List? children { get; set; } } public class DeeplinkDailyReportSummary { public long request_count { get; set; } = 0; public long total_count { get; set; } = 0; public long success_count { get; set; } = 0; public long fail_count { get; set; } = 0; } }