using Microsoft.EntityFrameworkCore; using DramaLing.Api.Data; using DramaLing.Api.Models.Entities; namespace DramaLing.Api.Services; public interface IUsageTrackingService { Task CheckUsageLimitAsync(Guid userId, bool isPremium = false); Task RecordSentenceAnalysisAsync(Guid userId); Task RecordWordQueryAsync(Guid userId, bool wasHighValue); Task GetUsageStatsAsync(Guid userId); } public class UsageTrackingService : IUsageTrackingService { private readonly DramaLingDbContext _context; private readonly ILogger _logger; // 免費用戶限制 private const int FREE_USER_ANALYSIS_LIMIT = 5; private const int FREE_USER_RESET_HOURS = 3; public UsageTrackingService(DramaLingDbContext context, ILogger logger) { _context = context; _logger = logger; } /// /// 檢查用戶使用限制 /// public async Task CheckUsageLimitAsync(Guid userId, bool isPremium = false) { try { if (isPremium) { return true; // 付費用戶無限制 } var resetTime = DateTime.UtcNow.AddHours(-FREE_USER_RESET_HOURS); var recentUsage = await _context.WordQueryUsageStats .Where(stats => stats.UserId == userId && stats.CreatedAt >= resetTime) .SumAsync(stats => stats.SentenceAnalysisCount + stats.LowValueWordClicks); var canUse = recentUsage < FREE_USER_ANALYSIS_LIMIT; _logger.LogInformation("Usage check for user {UserId}: {RecentUsage}/{Limit}, Can use: {CanUse}", userId, recentUsage, FREE_USER_ANALYSIS_LIMIT, canUse); return canUse; } catch (Exception ex) { _logger.LogError(ex, "Error checking usage limit for user {UserId}", userId); return false; // 出錯時拒絕使用 } } /// /// 記錄句子分析使用 /// public async Task RecordSentenceAnalysisAsync(Guid userId) { try { var today = DateOnly.FromDateTime(DateTime.Today); var stats = await GetOrCreateTodayStatsAsync(userId, today); stats.SentenceAnalysisCount++; stats.TotalApiCalls++; stats.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Recorded sentence analysis for user {UserId}, total today: {Count}", userId, stats.SentenceAnalysisCount); } catch (Exception ex) { _logger.LogError(ex, "Error recording sentence analysis for user {UserId}", userId); } } /// /// 記錄單字查詢使用 /// public async Task RecordWordQueryAsync(Guid userId, bool wasHighValue) { try { var today = DateOnly.FromDateTime(DateTime.Today); var stats = await GetOrCreateTodayStatsAsync(userId, today); if (wasHighValue) { stats.HighValueWordClicks++; } else { stats.LowValueWordClicks++; stats.TotalApiCalls++; // 低價值詞彙需要API調用 } stats.UniqueWordsQueried++; // 簡化:每次查詢都算一個獨特詞彙 stats.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Recorded word query for user {UserId}, high value: {IsHighValue}", userId, wasHighValue); } catch (Exception ex) { _logger.LogError(ex, "Error recording word query for user {UserId}", userId); } } /// /// 獲取用戶使用統計 /// public async Task GetUsageStatsAsync(Guid userId) { try { var today = DateOnly.FromDateTime(DateTime.Today); var resetTime = DateTime.UtcNow.AddHours(-FREE_USER_RESET_HOURS); // 今日統計 var todayStats = await _context.WordQueryUsageStats .FirstOrDefaultAsync(stats => stats.UserId == userId && stats.Date == today) ?? new WordQueryUsageStats { UserId = userId, Date = today }; // 最近3小時使用量(用於限制檢查) var recentUsage = await _context.WordQueryUsageStats .Where(stats => stats.UserId == userId && stats.CreatedAt >= resetTime) .SumAsync(stats => stats.SentenceAnalysisCount + stats.LowValueWordClicks); // 本週統計 var weekStart = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek); var weekStats = await _context.WordQueryUsageStats .Where(stats => stats.UserId == userId && stats.Date >= DateOnly.FromDateTime(weekStart)) .GroupBy(stats => 1) .Select(g => new { TotalAnalysis = g.Sum(s => s.SentenceAnalysisCount), TotalWordClicks = g.Sum(s => s.HighValueWordClicks + s.LowValueWordClicks), TotalApiCalls = g.Sum(s => s.TotalApiCalls), UniqueWords = g.Sum(s => s.UniqueWordsQueried) }) .FirstOrDefaultAsync(); return new UserUsageStats { UserId = userId, Today = new DailyUsageStats { Date = today, SentenceAnalysisCount = todayStats.SentenceAnalysisCount, HighValueWordClicks = todayStats.HighValueWordClicks, LowValueWordClicks = todayStats.LowValueWordClicks, TotalApiCalls = todayStats.TotalApiCalls, UniqueWordsQueried = todayStats.UniqueWordsQueried }, RecentUsage = new UsageLimitInfo { UsedInWindow = recentUsage, WindowLimit = FREE_USER_ANALYSIS_LIMIT, WindowHours = FREE_USER_RESET_HOURS, ResetTime = DateTime.UtcNow.AddHours(FREE_USER_RESET_HOURS - ((DateTime.UtcNow - resetTime).TotalHours % FREE_USER_RESET_HOURS)) }, ThisWeek = weekStats != null ? new WeeklyUsageStats { StartDate = DateOnly.FromDateTime(weekStart), EndDate = DateOnly.FromDateTime(weekStart.AddDays(6)), TotalSentenceAnalysis = weekStats.TotalAnalysis, TotalWordClicks = weekStats.TotalWordClicks, TotalApiCalls = weekStats.TotalApiCalls, UniqueWordsQueried = weekStats.UniqueWords } : new WeeklyUsageStats() }; } catch (Exception ex) { _logger.LogError(ex, "Error getting usage stats for user {UserId}", userId); return new UserUsageStats { UserId = userId }; } } /// /// 獲取或創建今日統計記錄 /// private async Task GetOrCreateTodayStatsAsync(Guid userId, DateOnly date) { var stats = await _context.WordQueryUsageStats .FirstOrDefaultAsync(s => s.UserId == userId && s.Date == date); if (stats == null) { stats = new WordQueryUsageStats { Id = Guid.NewGuid(), UserId = userId, Date = date, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; _context.WordQueryUsageStats.Add(stats); } return stats; } } // 回應用的 DTO 類別 public class UserUsageStats { public Guid UserId { get; set; } public DailyUsageStats Today { get; set; } = new(); public UsageLimitInfo RecentUsage { get; set; } = new(); public WeeklyUsageStats ThisWeek { get; set; } = new(); } public class DailyUsageStats { public DateOnly Date { get; set; } public int SentenceAnalysisCount { get; set; } public int HighValueWordClicks { get; set; } public int LowValueWordClicks { get; set; } public int TotalApiCalls { get; set; } public int UniqueWordsQueried { get; set; } } public class UsageLimitInfo { public int UsedInWindow { get; set; } public int WindowLimit { get; set; } public int WindowHours { get; set; } public DateTime ResetTime { get; set; } public bool CanUse => UsedInWindow < WindowLimit; public int Remaining => Math.Max(0, WindowLimit - UsedInWindow); } public class WeeklyUsageStats { public DateOnly StartDate { get; set; } public DateOnly EndDate { get; set; } public int TotalSentenceAnalysis { get; set; } public int TotalWordClicks { get; set; } public int TotalApiCalls { get; set; } public int UniqueWordsQueried { get; set; } }