237 lines
9.2 KiB
C#
237 lines
9.2 KiB
C#
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;
|
||
|
||
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,
|
||
|
||
// 同義詞(暫時空陣列,未來可擴展)
|
||
synonyms = new string[] { },
|
||
|
||
// 測驗選項 (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;
|
||
}
|
||
}
|
||
|
||
/// <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
|
||
};
|
||
}
|
||
} |