dramaling-vocab-learning/backend/DramaLing.Api/Services/Review/ReviewService.cs

217 lines
8.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 DramaLing.Api.Models.DTOs;
using DramaLing.Api.Models.Entities;
using DramaLing.Api.Repositories;
using DramaLing.Api.Controllers;
using DramaLing.Api.Utils;
namespace DramaLing.Api.Services.Review;
public class ReviewService : IReviewService
{
private readonly IFlashcardReviewRepository _reviewRepository;
private readonly ILogger<ReviewService> _logger;
public ReviewService(
IFlashcardReviewRepository reviewRepository,
ILogger<ReviewService> logger)
{
_reviewRepository = reviewRepository;
_logger = logger;
}
public async Task<ApiResponse<object>> GetDueFlashcardsAsync(Guid userId, DueFlashcardsQuery query)
{
try
{
var dueFlashcards = await _reviewRepository.GetDueFlashcardsAsync(userId, query);
var (todayDue, overdue, totalReviews) = await _reviewRepository.GetReviewStatsAsync(userId);
// 轉換為符合前端期望的格式
var flashcardData = dueFlashcards.Select(item => new
{
// 基本詞卡信息 (匹配 api_seeds.json 格式)
id = item.Flashcard.Id.ToString(),
word = item.Flashcard.Word,
translation = item.Flashcard.Translation,
definition = item.Flashcard.Definition ?? "",
partOfSpeech = item.Flashcard.PartOfSpeech ?? "",
pronunciation = item.Flashcard.Pronunciation ?? "",
example = item.Flashcard.Example ?? "",
exampleTranslation = item.Flashcard.ExampleTranslation ?? "",
isFavorite = item.Flashcard.IsFavorite,
difficultyLevelNumeric = item.Flashcard.DifficultyLevelNumeric,
cefr = CEFRHelper.ToString(item.Flashcard.DifficultyLevelNumeric),
createdAt = item.Flashcard.CreatedAt.ToString("yyyy-MM-ddTHH:mm:ssZ"),
updatedAt = item.Flashcard.UpdatedAt.ToString("yyyy-MM-ddTHH:mm:ssZ"),
// 圖片相關 (暫時設為預設值,因為需要額外查詢)
hasExampleImage = false,
primaryImageUrl = (string?)null,
// 同義詞(暫時空陣列,未來可擴展)
synonyms = new string[] { },
// 複習相關信息 (新增)
reviewInfo = item.Review != null ? new
{
successCount = item.Review.SuccessCount,
nextReviewDate = item.Review.NextReviewDate.ToString("yyyy-MM-ddTHH:mm:ssZ"),
lastReviewDate = item.Review.LastReviewDate?.ToString("yyyy-MM-ddTHH:mm:ssZ"),
totalCorrectCount = item.Review.TotalCorrectCount,
totalWrongCount = item.Review.TotalWrongCount,
totalSkipCount = item.Review.TotalSkipCount,
isOverdue = item.Review.NextReviewDate < DateTime.UtcNow.Date,
daysSinceLastReview = item.Review.LastReviewDate.HasValue
? (int)(DateTime.UtcNow - item.Review.LastReviewDate.Value).TotalDays
: 0
} : new
{
successCount = 0,
nextReviewDate = DateTime.UtcNow.AddDays(1).ToString("yyyy-MM-ddTHH:mm:ssZ"),
lastReviewDate = (string?)null,
totalCorrectCount = 0,
totalWrongCount = 0,
totalSkipCount = 0,
isOverdue = false,
daysSinceLastReview = 0
}
}).ToList();
var response = new
{
flashcards = flashcardData,
count = flashcardData.Count,
metadata = new
{
todayDue = todayDue,
overdue = overdue,
totalReviews = totalReviews,
studyStreak = 0 // 暫時設為0未來可實作
}
};
return new ApiResponse<object>
{
Success = true,
Data = response,
Message = null,
Timestamp = DateTime.UtcNow
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting due flashcards for user {UserId}", userId);
throw;
}
}
public async Task<ApiResponse<ReviewResult>> SubmitReviewAsync(Guid userId, Guid flashcardId, ReviewRequest request)
{
try
{
// 獲取或創建複習記錄
var review = await _reviewRepository.GetOrCreateReviewAsync(userId, flashcardId);
// 處理複習結果
var result = ProcessReview(review, request);
// 更新記錄
await _reviewRepository.UpdateReviewAsync(review);
return new ApiResponse<ReviewResult>
{
Success = true,
Data = result,
Timestamp = DateTime.UtcNow
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error submitting review for flashcard {FlashcardId}", flashcardId);
throw;
}
}
public async Task<ApiResponse<ReviewStats>> GetReviewStatsAsync(Guid userId, string period = "today")
{
try
{
var (todayDue, overdue, totalReviews) = await _reviewRepository.GetReviewStatsAsync(userId);
var stats = new ReviewStats
{
TodayReviewed = 0, // TODO: 實作當日複習統計
TodayDue = todayDue,
Overdue = overdue,
TotalReviews = totalReviews,
AverageAccuracy = 0.0, // TODO: 實作正確率統計
StudyStreak = 0 // TODO: 實作連續學習天數
};
return new ApiResponse<ReviewStats>
{
Success = true,
Data = stats,
Timestamp = DateTime.UtcNow
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting review stats for user {UserId}", userId);
throw;
}
}
/// <summary>
/// 處理複習結果的核心算法
/// </summary>
private ReviewResult ProcessReview(FlashcardReview review, ReviewRequest request)
{
if (request.WasSkipped)
{
// 跳過: 不改變成功次數,明天再複習
review.TotalSkipCount++;
review.NextReviewDate = DateTime.UtcNow.AddDays(1);
}
else
{
// 根據信心度判斷是否答對 (0=不熟悉答錯, 1-2=答對)
var isCorrect = request.Confidence >= 1;
if (isCorrect)
{
// 答對: 增加成功次數,計算新間隔
review.SuccessCount++;
review.TotalCorrectCount++;
review.LastSuccessDate = DateTime.UtcNow;
// 核心公式: 間隔 = 2^成功次數 天
var intervalDays = (int)Math.Pow(2, review.SuccessCount);
var maxInterval = 180; // 最大半年
var finalInterval = Math.Min(intervalDays, maxInterval);
review.NextReviewDate = DateTime.UtcNow.AddDays(finalInterval);
}
else
{
// 答錯: 重置成功次數,明天再複習
review.SuccessCount = 0;
review.TotalWrongCount++;
review.NextReviewDate = DateTime.UtcNow.AddDays(1);
}
}
review.LastReviewDate = DateTime.UtcNow;
review.UpdatedAt = DateTime.UtcNow;
return new ReviewResult
{
FlashcardId = review.FlashcardId,
NewSuccessCount = review.SuccessCount,
NextReviewDate = review.NextReviewDate,
IntervalDays = (int)(review.NextReviewDate - DateTime.UtcNow).TotalDays,
MasteryLevelChange = 0.0, // 暫時設為0
IsNewRecord = review.CreatedAt == review.UpdatedAt
};
}
}