dramaling-vocab-learning/backend/DramaLing.Api/Services/Infrastructure/Monitoring/UsageTrackingService.cs

255 lines
9.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.EntityFrameworkCore;
using DramaLing.Api.Data;
using DramaLing.Api.Models.Entities;
namespace DramaLing.Api.Services;
public interface IUsageTrackingService
{
Task<bool> CheckUsageLimitAsync(Guid userId, bool isPremium = false);
Task RecordSentenceAnalysisAsync(Guid userId);
Task RecordWordQueryAsync(Guid userId, bool wasHighValue);
Task<UserUsageStats> GetUsageStatsAsync(Guid userId);
}
public class UsageTrackingService : IUsageTrackingService
{
private readonly DramaLingDbContext _context;
private readonly ILogger<UsageTrackingService> _logger;
// 免費用戶限制
private const int FREE_USER_ANALYSIS_LIMIT = 5;
private const int FREE_USER_RESET_HOURS = 3;
public UsageTrackingService(DramaLingDbContext context, ILogger<UsageTrackingService> logger)
{
_context = context;
_logger = logger;
}
/// <summary>
/// 檢查用戶使用限制
/// </summary>
public async Task<bool> 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; // 出錯時拒絕使用
}
}
/// <summary>
/// 記錄句子分析使用
/// </summary>
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);
}
}
/// <summary>
/// 記錄單字查詢使用
/// </summary>
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);
}
}
/// <summary>
/// 獲取用戶使用統計
/// </summary>
public async Task<UserUsageStats> 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 };
}
}
/// <summary>
/// 獲取或創建今日統計記錄
/// </summary>
private async Task<WordQueryUsageStats> 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; }
}