307 lines
11 KiB
C#
307 lines
11 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 cardSetsTask = _context.CardSets
|
|
.Where(cs => cs.UserId == userId)
|
|
.OrderByDescending(cs => cs.CreatedAt)
|
|
.Take(5)
|
|
.Select(cs => new
|
|
{
|
|
cs.Id,
|
|
cs.Name,
|
|
Count = cs.CardCount,
|
|
Progress = cs.CardCount > 0 ?
|
|
_context.Flashcards
|
|
.Where(f => f.CardSetId == cs.Id)
|
|
.Average(f => (double?)f.MasteryLevel) ?? 0 : 0,
|
|
LastStudied = cs.CreatedAt
|
|
})
|
|
.ToListAsync();
|
|
|
|
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, cardSetsTask, recentCardsTask, todayStatsTask);
|
|
|
|
var totalWords = await totalWordsTask;
|
|
var cardSets = await cardSetsTask;
|
|
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 = cardSets
|
|
}
|
|
});
|
|
}
|
|
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
|
|
});
|
|
}
|
|
}
|
|
} |