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

284 lines
11 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;
using DramaLing.Api.Services;
using DramaLing.Api.Data;
using DramaLing.Api.Services.AI.Utils;
namespace DramaLing.Api.Services.Review;
public class ReviewService : IReviewService
{
private readonly IFlashcardReviewRepository _reviewRepository;
private readonly IOptionsVocabularyService _optionsService;
private readonly ILogger<ReviewService> _logger;
public ReviewService(
IFlashcardReviewRepository reviewRepository,
IOptionsVocabularyService optionsService,
ILogger<ReviewService> logger)
{
_reviewRepository = reviewRepository;
_optionsService = optionsService;
_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);
// 為每張詞卡生成 quizOptions
var flashcardDataTasks = dueFlashcards.Select(async item =>
{
// 生成混淆選項
var generatedQuizOptions = await _optionsService.GenerateDistractorsAsync(
item.Flashcard.Word,
CEFRHelper.ToString(item.Flashcard.DifficultyLevelNumeric),
item.Flashcard.PartOfSpeech ?? "noun",
3);
return 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,
// 同義詞(從資料庫讀取,使用 AI 工具類解析)
synonyms = SynonymsParser.ParseSynonymsJson(item.Flashcard.Synonyms),
// 測驗選項 (AI 生成的混淆選項)
quizOptions = generatedQuizOptions,
// 複習相關信息 (新增)
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
}
};
});
// 等待所有異步任務完成
var flashcardData = await Task.WhenAll(flashcardDataTasks);
var response = new
{
flashcards = flashcardData,
count = flashcardData.Length,
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;
}
}
public async Task<ApiResponse<object>> MarkWordMasteredAsync(Guid userId, Guid flashcardId)
{
try
{
// 使用 repository 的 GetOrCreate 方法
var review = await _reviewRepository.GetOrCreateReviewAsync(userId, flashcardId);
// 簡化邏輯:直接標記為掌握
review.SuccessCount++;
review.TotalCorrectCount++;
review.LastSuccessDate = DateTime.UtcNow;
review.LastReviewDate = DateTime.UtcNow;
// 核心算法:間隔 = 2^成功次數 天最大180天
var intervalDays = (int)Math.Pow(2, review.SuccessCount);
var maxInterval = 180;
var finalInterval = Math.Min(intervalDays, maxInterval);
review.NextReviewDate = DateTime.UtcNow.AddDays(finalInterval);
review.UpdatedAt = DateTime.UtcNow;
// 使用 repository 的更新方法
await _reviewRepository.UpdateReviewAsync(review);
return new ApiResponse<object>
{
Success = true,
Data = new
{
nextReviewDate = review.NextReviewDate.ToString("yyyy-MM-ddTHH:mm:ssZ"),
intervalDays = finalInterval,
successCount = review.SuccessCount,
message = "詞彙已標記為掌握"
},
Message = "詞彙掌握狀態已更新",
Timestamp = DateTime.UtcNow
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error marking flashcard {FlashcardId} as mastered for user {UserId}", flashcardId, 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
};
}
}