255 lines
9.2 KiB
C#
255 lines
9.2 KiB
C#
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; }
|
||
} |