dramaling-vocab-learning/backend/DramaLing.Api/Controllers/StatsController.cs

290 lines
10 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using DramaLing.Api.Data;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
namespace DramaLing.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class StatsController : ControllerBase
{
private readonly DramaLingDbContext _context;
public StatsController(DramaLingDbContext context)
{
_context = context;
}
private Guid GetUserId()
{
var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ??
User.FindFirst("sub")?.Value;
if (Guid.TryParse(userIdString, out var userId))
return userId;
throw new UnauthorizedAccessException("Invalid user ID");
}
[HttpGet("dashboard")]
public async Task<ActionResult> GetDashboardStats()
{
try
{
var userId = GetUserId();
var today = DateOnly.FromDateTime(DateTime.Today);
// 並行獲取統計數據
var totalWordsTask = _context.Flashcards.CountAsync(f => f.UserId == userId);
var recentCardsTask = _context.Flashcards
.Where(f => f.UserId == userId)
.OrderByDescending(f => f.LastReviewedAt ?? f.CreatedAt)
.Take(4)
.Select(f => new
{
f.Word,
f.Translation,
Status = f.MasteryLevel >= 80 ? "learned" :
f.MasteryLevel >= 40 ? "learning" : "new"
})
.ToListAsync();
var todayStatsTask = _context.DailyStats
.FirstOrDefaultAsync(ds => ds.UserId == userId && ds.Date == today);
// 等待所有查詢完成
await Task.WhenAll(totalWordsTask, recentCardsTask, todayStatsTask);
var totalWords = await totalWordsTask;
var recentCards = await recentCardsTask;
var todayStats = await todayStatsTask;
// 計算統計數據
var wordsToday = todayStats?.WordsStudied ?? 0;
var wordsCorrect = todayStats?.WordsCorrect ?? 0;
var accuracy = wordsToday > 0 ? (int)Math.Round((double)wordsCorrect / wordsToday * 100) : 85;
// 模擬連續天數 (Phase 1 簡化)
var streakDays = 7;
var todayReviewCount = 23; // 模擬數據
return Ok(new
{
Success = true,
Data = new
{
TotalWords = totalWords,
WordsToday = wordsToday,
StreakDays = streakDays,
AccuracyPercentage = accuracy,
TodayReviewCount = todayReviewCount,
CompletedToday = wordsToday,
RecentWords = recentCards.Count > 0 ? recentCards.Cast<object>().ToList() : new List<object>
{
new { Word = "negotiate", Translation = "協商", Status = "learned" },
new { Word = "accomplish", Translation = "完成", Status = "learning" },
new { Word = "perspective", Translation = "觀點", Status = "new" },
new { Word = "substantial", Translation = "大量的", Status = "learned" }
},
CardSets = new object[0] // 移除 CardSet 功能
}
});
}
catch (UnauthorizedAccessException)
{
return Unauthorized(new { Success = false, Error = "Unauthorized" });
}
catch (Exception ex)
{
return StatusCode(500, new
{
Success = false,
Error = "Failed to fetch dashboard stats",
Timestamp = DateTime.UtcNow
});
}
}
[HttpGet("trends")]
public async Task<ActionResult> GetTrends([FromQuery] string period = "week")
{
try
{
var userId = GetUserId();
var days = period switch
{
"week" => 7,
"month" => 30,
"year" => 365,
_ => 7
};
var endDate = DateOnly.FromDateTime(DateTime.Today);
var startDate = endDate.AddDays(-days + 1);
var dailyStats = await _context.DailyStats
.Where(ds => ds.UserId == userId && ds.Date >= startDate && ds.Date <= endDate)
.OrderBy(ds => ds.Date)
.ToListAsync();
// 生成完整的日期序列
var dateRange = Enumerable.Range(0, days)
.Select(i => startDate.AddDays(i))
.ToList();
var statsMap = dailyStats.ToDictionary(ds => ds.Date, ds => ds);
var completeStats = dateRange.Select(date =>
{
var stat = statsMap.GetValueOrDefault(date);
return new
{
Date = date.ToString("yyyy-MM-dd"),
WordsStudied = stat?.WordsStudied ?? 0,
WordsCorrect = stat?.WordsCorrect ?? 0,
StudyTimeSeconds = stat?.StudyTimeSeconds ?? 0,
SessionCount = stat?.SessionCount ?? 0,
CardsGenerated = stat?.CardsGenerated ?? 0,
Accuracy = stat != null && stat.WordsStudied > 0
? (int)Math.Round((double)stat.WordsCorrect / stat.WordsStudied * 100)
: 0
};
}).ToList();
// 計算總結數據
var totalWordsStudied = completeStats.Sum(s => s.WordsStudied);
var totalCorrect = completeStats.Sum(s => s.WordsCorrect);
var totalStudyTime = completeStats.Sum(s => s.StudyTimeSeconds);
var totalSessions = completeStats.Sum(s => s.SessionCount);
var averageAccuracy = totalWordsStudied > 0
? (int)Math.Round((double)totalCorrect / totalWordsStudied * 100)
: 0;
return Ok(new
{
Success = true,
Data = new
{
Period = period,
DateRange = new
{
Start = startDate.ToString("yyyy-MM-dd"),
End = endDate.ToString("yyyy-MM-dd")
},
DailyCounts = completeStats,
Summary = new
{
TotalWordsStudied = totalWordsStudied,
TotalCorrect = totalCorrect,
TotalStudyTimeSeconds = totalStudyTime,
TotalSessions = totalSessions,
AverageAccuracy = averageAccuracy,
AverageDailyWords = (int)Math.Round((double)totalWordsStudied / days),
AverageSessionDuration = totalSessions > 0
? totalStudyTime / totalSessions
: 0
}
}
});
}
catch (UnauthorizedAccessException)
{
return Unauthorized(new { Success = false, Error = "Unauthorized" });
}
catch (Exception ex)
{
return StatusCode(500, new
{
Success = false,
Error = "Failed to fetch learning trends",
Timestamp = DateTime.UtcNow
});
}
}
[HttpGet("detailed")]
public async Task<ActionResult> GetDetailedStats()
{
try
{
var userId = GetUserId();
// 獲取詞卡統計
var flashcards = await _context.Flashcards
.Where(f => f.UserId == userId)
.ToListAsync();
// 按難度分類
var difficultyStats = flashcards
.GroupBy(f => f.DifficultyLevel ?? "unknown")
.ToDictionary(g => g.Key, g => g.Count());
// 按詞性分類
var partOfSpeechStats = flashcards
.GroupBy(f => f.PartOfSpeech ?? "unknown")
.ToDictionary(g => g.Key, g => g.Count());
// 掌握度分布
var masteryDistribution = new
{
Mastered = flashcards.Count(f => f.MasteryLevel >= 80),
Learning = flashcards.Count(f => f.MasteryLevel >= 40 && f.MasteryLevel < 80),
New = flashcards.Count(f => f.MasteryLevel < 40)
};
// 最近30天的學習記錄 (模擬數據)
var learningCurve = Enumerable.Range(0, 30)
.Select(i =>
{
var date = DateTime.Today.AddDays(-29 + i);
var accuracy = 70 + new Random(date.DayOfYear).Next(-15, 25); // 模擬準確率
return new
{
Date = date.ToString("yyyy-MM-dd"),
Accuracy = Math.Clamp(accuracy, 0, 100),
Count = new Random(date.DayOfYear).Next(0, 15)
};
}).ToList();
return Ok(new
{
Success = true,
Data = new
{
ByDifficulty = difficultyStats,
ByPartOfSpeech = partOfSpeechStats,
MasteryDistribution = masteryDistribution,
LearningCurve = learningCurve,
Summary = new
{
TotalCards = flashcards.Count,
AverageMastery = flashcards.Count > 0
? (int)flashcards.Average(f => f.MasteryLevel)
: 0,
OverallAccuracy = flashcards.Count > 0 && flashcards.Sum(f => f.TimesReviewed) > 0
? (int)Math.Round((double)flashcards.Sum(f => f.TimesCorrect) / flashcards.Sum(f => f.TimesReviewed) * 100)
: 0
}
}
});
}
catch (UnauthorizedAccessException)
{
return Unauthorized(new { Success = false, Error = "Unauthorized" });
}
catch (Exception ex)
{
return StatusCode(500, new
{
Success = false,
Error = "Failed to fetch detailed statistics",
Timestamp = DateTime.UtcNow
});
}
}
}