|
|
|
|
@ -6,103 +6,319 @@
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🏗️ **系統架構**
|
|
|
|
|
## 🏗️ **系統架構 (基於現有ASP.NET Core)**
|
|
|
|
|
|
|
|
|
|
### **核心服務**
|
|
|
|
|
### **整合到現有架構**
|
|
|
|
|
```
|
|
|
|
|
┌─────────────────────┐
|
|
|
|
|
│ 復習記錄 API │
|
|
|
|
|
└─────────┬───────────┘
|
|
|
|
|
│
|
|
|
|
|
┌─────▼─────┐
|
|
|
|
|
│ 輸入驗證層 │
|
|
|
|
|
└─────┬─────┘
|
|
|
|
|
│
|
|
|
|
|
┌───────▼────────┐
|
|
|
|
|
│ SpacedRepetition │
|
|
|
|
|
│ Service │
|
|
|
|
|
│ ┌─────────────┐ │
|
|
|
|
|
│ │ 逾期檢測 │ │
|
|
|
|
|
│ │ 間隔計算 │ │
|
|
|
|
|
│ │ 熟悉度更新 │ │
|
|
|
|
|
│ └─────────────┘ │
|
|
|
|
|
└───────┬────────┘
|
|
|
|
|
│
|
|
|
|
|
┌─────▼─────┐
|
|
|
|
|
│ 數據持久化 │
|
|
|
|
|
└───────────┘
|
|
|
|
|
┌─────────────────────────────────────────┐
|
|
|
|
|
│ FlashcardsController │
|
|
|
|
|
│ ┌─────────────────────────────────────┐ │
|
|
|
|
|
│ │ 智能複習端點群組 │ │
|
|
|
|
|
│ │ • /api/flashcards/due │ │
|
|
|
|
|
│ │ • /api/flashcards/next-review │ │
|
|
|
|
|
│ │ • /api/flashcards/{id}/review │ │
|
|
|
|
|
│ │ • /api/flashcards/{id}/optimal-mode │ │
|
|
|
|
|
│ │ • /api/flashcards/{id}/question │ │
|
|
|
|
|
│ └─────────────────────────────────────┘ │
|
|
|
|
|
└─────────────────┬───────────────────────┘
|
|
|
|
|
│
|
|
|
|
|
┌─────────▼─────────┐
|
|
|
|
|
│ 智能複習服務層 │
|
|
|
|
|
│ ┌───────────────┐ │
|
|
|
|
|
│ │SpacedRepetition│ │
|
|
|
|
|
│ │ Service │ │
|
|
|
|
|
│ ├───────────────┤ │
|
|
|
|
|
│ │ReviewType │ │
|
|
|
|
|
│ │ Selector │ │
|
|
|
|
|
│ ├───────────────┤ │
|
|
|
|
|
│ │A1Protection │ │
|
|
|
|
|
│ │ Service │ │
|
|
|
|
|
│ └───────────────┘ │
|
|
|
|
|
└─────────┬─────────┘
|
|
|
|
|
│
|
|
|
|
|
┌─────────▼─────────┐
|
|
|
|
|
│ 現有DramaLing │
|
|
|
|
|
│ DbContext │
|
|
|
|
|
│ (擴展Flashcard) │
|
|
|
|
|
└───────────────────┘
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **關鍵類別設計**
|
|
|
|
|
### **智能複習服務層設計 (新增)**
|
|
|
|
|
|
|
|
|
|
#### **SpacedRepetitionService**
|
|
|
|
|
#### **1. SpacedRepetitionService** (核心間隔重複算法)
|
|
|
|
|
```csharp
|
|
|
|
|
public class SpacedRepetitionService
|
|
|
|
|
public interface ISpacedRepetitionService
|
|
|
|
|
{
|
|
|
|
|
public ReviewResult ProcessReview(ReviewRequest request)
|
|
|
|
|
Task<ReviewResult> ProcessReviewAsync(Guid flashcardId, ReviewRequest request);
|
|
|
|
|
int CalculateCurrentMasteryLevel(Flashcard flashcard);
|
|
|
|
|
Task<List<Flashcard>> GetDueFlashcardsAsync(Guid userId, DateTime? date = null, int limit = 50);
|
|
|
|
|
Task<Flashcard?> GetNextReviewCardAsync(Guid userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class SpacedRepetitionService : ISpacedRepetitionService
|
|
|
|
|
{
|
|
|
|
|
private readonly DramaLingDbContext _context;
|
|
|
|
|
private readonly ILogger<SpacedRepetitionService> _logger;
|
|
|
|
|
|
|
|
|
|
public async Task<ReviewResult> ProcessReviewAsync(Guid flashcardId, ReviewRequest request)
|
|
|
|
|
{
|
|
|
|
|
// 1. 計算逾期天數 (明確時間基準)
|
|
|
|
|
var actualReviewDate = DateTime.Now.Date; // 復習行為當日
|
|
|
|
|
var overdueDays = (actualReviewDate - request.NextReviewDate.Date).Days;
|
|
|
|
|
var flashcard = await _context.Flashcards.FindAsync(flashcardId);
|
|
|
|
|
if (flashcard == null) throw new ArgumentException("Flashcard not found");
|
|
|
|
|
|
|
|
|
|
// 2. 應用記憶衰減
|
|
|
|
|
var adjustedMastery = ApplyMemoryDecay(request.CurrentMastery, overdueDays);
|
|
|
|
|
// 1. 計算逾期天數
|
|
|
|
|
var actualReviewDate = DateTime.Now.Date;
|
|
|
|
|
var overdueDays = (actualReviewDate - flashcard.NextReviewDate.Date).Days;
|
|
|
|
|
|
|
|
|
|
// 3. 計算新間隔
|
|
|
|
|
// 2. 計算新間隔 (基於演算法規格書)
|
|
|
|
|
var newInterval = CalculateNewInterval(
|
|
|
|
|
request.CurrentInterval,
|
|
|
|
|
flashcard.IntervalDays,
|
|
|
|
|
request.IsCorrect,
|
|
|
|
|
request.ConfidenceLevel,
|
|
|
|
|
request.QuestionType,
|
|
|
|
|
overdueDays
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 4. 更新基礎熟悉程度 (存入資料庫)
|
|
|
|
|
var newBaseMastery = CalculateMasteryLevel(
|
|
|
|
|
request.TimesCorrect + (request.IsCorrect ? 1 : 0),
|
|
|
|
|
request.TotalReviews + 1,
|
|
|
|
|
// 3. 更新熟悉度
|
|
|
|
|
var newMasteryLevel = CalculateMasteryLevel(
|
|
|
|
|
flashcard.TimesCorrect + (request.IsCorrect ? 1 : 0),
|
|
|
|
|
flashcard.TimesReviewed + 1,
|
|
|
|
|
newInterval
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 4. 更新資料庫
|
|
|
|
|
flashcard.MasteryLevel = newMasteryLevel;
|
|
|
|
|
flashcard.TimesReviewed++;
|
|
|
|
|
if (request.IsCorrect) flashcard.TimesCorrect++;
|
|
|
|
|
flashcard.IntervalDays = newInterval;
|
|
|
|
|
flashcard.NextReviewDate = actualReviewDate.AddDays(newInterval);
|
|
|
|
|
flashcard.LastReviewedAt = DateTime.Now;
|
|
|
|
|
flashcard.LastQuestionType = request.QuestionType;
|
|
|
|
|
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
return new ReviewResult
|
|
|
|
|
{
|
|
|
|
|
NewInterval = newInterval,
|
|
|
|
|
NextReviewDate = actualReviewDate.AddDays(newInterval), // 以復習當日為基準
|
|
|
|
|
BaseMasteryLevel = newBaseMastery, // 基礎熟悉度
|
|
|
|
|
CurrentMasteryLevel = newBaseMastery, // 剛復習完,兩者相等
|
|
|
|
|
IsOverdue = overdueDays > 0,
|
|
|
|
|
OverdueDays = Math.Max(0, overdueDays)
|
|
|
|
|
NextReviewDate = flashcard.NextReviewDate,
|
|
|
|
|
MasteryLevel = newMasteryLevel,
|
|
|
|
|
CurrentMasteryLevel = CalculateCurrentMasteryLevel(flashcard)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### **2. ReviewTypeSelectorService** (智能題型選擇)
|
|
|
|
|
```csharp
|
|
|
|
|
public interface IReviewTypeSelectorService
|
|
|
|
|
{
|
|
|
|
|
Task<ReviewModeResult> SelectOptimalReviewModeAsync(Guid flashcardId, int userLevel, int wordLevel);
|
|
|
|
|
string[] GetAvailableReviewTypes(int userLevel, int wordLevel);
|
|
|
|
|
bool IsA1Learner(int userLevel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class ReviewTypeSelectorService : IReviewTypeSelectorService
|
|
|
|
|
{
|
|
|
|
|
public async Task<ReviewModeResult> SelectOptimalReviewModeAsync(
|
|
|
|
|
Guid flashcardId, int userLevel, int wordLevel)
|
|
|
|
|
{
|
|
|
|
|
// 1. 四情境判斷
|
|
|
|
|
var availableModes = GetAvailableReviewTypes(userLevel, wordLevel);
|
|
|
|
|
|
|
|
|
|
// 2. 檢查復習歷史,避免重複
|
|
|
|
|
var recentHistory = await GetRecentReviewHistory(flashcardId, 3);
|
|
|
|
|
var filteredModes = ApplyAntiRepetitionLogic(availableModes, recentHistory);
|
|
|
|
|
|
|
|
|
|
// 3. 智能選擇
|
|
|
|
|
var selectedMode = SelectModeWithWeights(filteredModes, userLevel);
|
|
|
|
|
|
|
|
|
|
return new ReviewModeResult
|
|
|
|
|
{
|
|
|
|
|
SelectedMode = selectedMode,
|
|
|
|
|
AvailableModes = availableModes,
|
|
|
|
|
AdaptationContext = GetAdaptationContext(userLevel, wordLevel),
|
|
|
|
|
Reason = GetSelectionReason(selectedMode, userLevel, wordLevel)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 計算當前熟悉度 (實時計算,不存資料庫)
|
|
|
|
|
/// </summary>
|
|
|
|
|
public int CalculateCurrentMasteryLevel(Flashcard flashcard)
|
|
|
|
|
public string[] GetAvailableReviewTypes(int userLevel, int wordLevel)
|
|
|
|
|
{
|
|
|
|
|
var daysSinceLastReview = (DateTime.Now.Date - flashcard.LastReviewDate.Date).Days;
|
|
|
|
|
var difficulty = wordLevel - userLevel;
|
|
|
|
|
|
|
|
|
|
// 如果沒有時間經過,返回基礎熟悉度
|
|
|
|
|
if (daysSinceLastReview <= 0)
|
|
|
|
|
return flashcard.BaseMasteryLevel;
|
|
|
|
|
if (userLevel <= 20)
|
|
|
|
|
return new[] { "flip-memory", "vocab-choice", "vocab-listening" }; // A1保護
|
|
|
|
|
|
|
|
|
|
// 應用記憶衰減
|
|
|
|
|
return ApplyMemoryDecay(flashcard.BaseMasteryLevel, daysSinceLastReview);
|
|
|
|
|
if (difficulty < -10)
|
|
|
|
|
return new[] { "sentence-reorder", "sentence-fill" }; // 簡單詞彙
|
|
|
|
|
|
|
|
|
|
if (difficulty >= -10 && difficulty <= 10)
|
|
|
|
|
return new[] { "sentence-fill", "sentence-reorder", "sentence-speaking" }; // 適中詞彙
|
|
|
|
|
|
|
|
|
|
return new[] { "flip-memory", "vocab-choice" }; // 困難詞彙
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### **3. QuestionGeneratorService** (題目生成)
|
|
|
|
|
```csharp
|
|
|
|
|
public interface IQuestionGeneratorService
|
|
|
|
|
{
|
|
|
|
|
Task<QuestionData> GenerateQuestionAsync(Guid flashcardId, string questionType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class QuestionGeneratorService : IQuestionGeneratorService
|
|
|
|
|
{
|
|
|
|
|
public async Task<QuestionData> GenerateQuestionAsync(Guid flashcardId, string questionType)
|
|
|
|
|
{
|
|
|
|
|
var flashcard = await _context.Flashcards.FindAsync(flashcardId);
|
|
|
|
|
if (flashcard == null) throw new ArgumentException("Flashcard not found");
|
|
|
|
|
|
|
|
|
|
return questionType switch
|
|
|
|
|
{
|
|
|
|
|
"vocab-choice" => await GenerateVocabChoiceOptions(flashcard),
|
|
|
|
|
"sentence-fill" => GenerateFillBlankQuestion(flashcard),
|
|
|
|
|
"sentence-reorder" => GenerateReorderQuestion(flashcard),
|
|
|
|
|
"sentence-listening" => await GenerateSentenceListeningOptions(flashcard),
|
|
|
|
|
_ => new QuestionData { QuestionType = questionType, CorrectAnswer = flashcard.Word }
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<QuestionData> GenerateVocabChoiceOptions(Flashcard flashcard)
|
|
|
|
|
{
|
|
|
|
|
// 從其他詞卡中選擇3個干擾選項
|
|
|
|
|
var distractors = await _context.Flashcards
|
|
|
|
|
.Where(f => f.UserId == flashcard.UserId && f.Id != flashcard.Id)
|
|
|
|
|
.OrderBy(x => Guid.NewGuid())
|
|
|
|
|
.Take(3)
|
|
|
|
|
.Select(f => f.Word)
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
var options = new List<string> { flashcard.Word };
|
|
|
|
|
options.AddRange(distractors);
|
|
|
|
|
|
|
|
|
|
return new QuestionData
|
|
|
|
|
{
|
|
|
|
|
QuestionType = "vocab-choice",
|
|
|
|
|
Options = options.OrderBy(x => Guid.NewGuid()).ToArray(),
|
|
|
|
|
CorrectAnswer = flashcard.Word
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🔌 **API 設計**
|
|
|
|
|
## 🔌 **智能複習API設計 (新增到現有FlashcardsController)**
|
|
|
|
|
|
|
|
|
|
### **POST /api/flashcards/{id}/review**
|
|
|
|
|
### **1. GET /api/flashcards/due** (新增)
|
|
|
|
|
**描述**: 取得當前用戶的到期詞卡列表
|
|
|
|
|
|
|
|
|
|
#### **查詢參數**
|
|
|
|
|
```typescript
|
|
|
|
|
interface DueFlashcardsQuery {
|
|
|
|
|
date?: string; // 查詢日期 (預設今天)
|
|
|
|
|
limit?: number; // 回傳數量限制 (預設50)
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### **響應格式**
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"success": true,
|
|
|
|
|
"data": [
|
|
|
|
|
{
|
|
|
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
|
|
|
"word": "sophisticated",
|
|
|
|
|
"translation": "精密的",
|
|
|
|
|
"definition": "Highly developed or complex",
|
|
|
|
|
"example": "A sophisticated system",
|
|
|
|
|
"exampleTranslation": "一個精密的系統",
|
|
|
|
|
"masteryLevel": 75,
|
|
|
|
|
"nextReviewDate": "2025-09-25",
|
|
|
|
|
"isOverdue": true,
|
|
|
|
|
"overdueDays": 2,
|
|
|
|
|
// 智能複習需要的欄位
|
|
|
|
|
"userLevel": 60, // 學習者程度
|
|
|
|
|
"wordLevel": 85, // 詞彙難度
|
|
|
|
|
"baseMasteryLevel": 75,
|
|
|
|
|
"lastReviewDate": "2025-09-20"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"count": 12
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **2. GET /api/flashcards/next-review** (新增)
|
|
|
|
|
**描述**: 取得下一張需要復習的詞卡 (依優先級排序)
|
|
|
|
|
|
|
|
|
|
#### **響應格式**
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"success": true,
|
|
|
|
|
"data": {
|
|
|
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
|
|
|
"word": "sophisticated",
|
|
|
|
|
"translation": "精密的",
|
|
|
|
|
"definition": "Highly developed or complex",
|
|
|
|
|
"pronunciation": "/səˈfɪstɪkeɪtɪd/",
|
|
|
|
|
"partOfSpeech": "adjective",
|
|
|
|
|
"example": "The software uses sophisticated algorithms.",
|
|
|
|
|
"exampleTranslation": "該軟體使用精密的算法。",
|
|
|
|
|
"masteryLevel": 25,
|
|
|
|
|
"timesReviewed": 3,
|
|
|
|
|
"isFavorite": false,
|
|
|
|
|
"nextReviewDate": "2025-09-25",
|
|
|
|
|
"difficultyLevel": "C1",
|
|
|
|
|
// 智能複習擴展欄位
|
|
|
|
|
"userLevel": 50, // 從用戶資料計算
|
|
|
|
|
"wordLevel": 85, // 從CEFR等級映射
|
|
|
|
|
"baseMasteryLevel": 30,
|
|
|
|
|
"lastReviewDate": "2025-09-20",
|
|
|
|
|
"currentInterval": 7,
|
|
|
|
|
"isOverdue": true,
|
|
|
|
|
"overdueDays": 5
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **3. POST /api/flashcards/{id}/optimal-review-mode** (新增)
|
|
|
|
|
**描述**: 系統自動選擇最適合的複習題型
|
|
|
|
|
|
|
|
|
|
#### **請求格式**
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"userLevel": 50,
|
|
|
|
|
"wordLevel": 85,
|
|
|
|
|
"includeHistory": true
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### **響應格式**
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"success": true,
|
|
|
|
|
"data": {
|
|
|
|
|
"selectedMode": "flip-memory",
|
|
|
|
|
"reason": "困難詞彙,使用基礎題型重建記憶",
|
|
|
|
|
"availableModes": ["flip-memory", "vocab-choice"],
|
|
|
|
|
"adaptationContext": "困難詞彙情境"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **4. POST /api/flashcards/{id}/review** (更新)
|
|
|
|
|
**描述**: 提交復習結果並更新間隔重複算法
|
|
|
|
|
|
|
|
|
|
#### **請求格式**
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"isCorrect": boolean,
|
|
|
|
|
"confidenceLevel": number, // 1-5, 翻卡題必須
|
|
|
|
|
"questionType": "flipcard" | "multiple_choice" | "fill_blank"
|
|
|
|
|
"confidenceLevel": number, // 1-5 (翻卡題)
|
|
|
|
|
"questionType": "flip-memory" | "vocab-choice" | "vocab-listening" |
|
|
|
|
|
"sentence-listening" | "sentence-fill" |
|
|
|
|
|
"sentence-reorder" | "sentence-speaking",
|
|
|
|
|
"userAnswer": string, // 用戶的答案
|
|
|
|
|
"timeTaken": number, // 答題時間(毫秒)
|
|
|
|
|
"timestamp": number
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
@ -113,69 +329,39 @@ public class SpacedRepetitionService
|
|
|
|
|
"data": {
|
|
|
|
|
"newInterval": 15,
|
|
|
|
|
"nextReviewDate": "2025-10-10",
|
|
|
|
|
"baseMasteryLevel": 65, // 基礎熟悉度 (存資料庫)
|
|
|
|
|
"currentMasteryLevel": 65, // 當前熟悉度 (實時計算)
|
|
|
|
|
"masteryLevel": 65, // 更新後的熟悉度
|
|
|
|
|
"currentMasteryLevel": 65, // 當前熟悉度
|
|
|
|
|
"isOverdue": false,
|
|
|
|
|
"overdueDays": 0
|
|
|
|
|
"performanceFactor": 1.1, // 表現係數
|
|
|
|
|
"growthFactor": 1.4 // 成長係數
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **GET /api/flashcards/{id}**
|
|
|
|
|
### **5. POST /api/flashcards/{id}/question** (新增)
|
|
|
|
|
**描述**: 為指定題型生成題目選項和資料
|
|
|
|
|
|
|
|
|
|
#### **請求格式**
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"questionType": "vocab-choice" | "sentence-listening" | "sentence-fill"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### **響應格式**
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"success": true,
|
|
|
|
|
"data": {
|
|
|
|
|
"id": 123,
|
|
|
|
|
"word": "apple",
|
|
|
|
|
"definition": "蘋果",
|
|
|
|
|
"baseMasteryLevel": 75, // 基礎熟悉度 (資料庫值)
|
|
|
|
|
"currentMasteryLevel": 68, // 當前熟悉度 (考慮衰減)
|
|
|
|
|
"lastReviewDate": "2025-09-20",
|
|
|
|
|
"nextReviewDate": "2025-10-04",
|
|
|
|
|
"currentInterval": 14,
|
|
|
|
|
"timesCorrect": 8,
|
|
|
|
|
"totalReviews": 10,
|
|
|
|
|
"isOverdue": true,
|
|
|
|
|
"overdueDays": 1
|
|
|
|
|
"questionType": "vocab-choice",
|
|
|
|
|
"options": ["sophisticated", "simple", "basic", "complex"],
|
|
|
|
|
"correctAnswer": "sophisticated",
|
|
|
|
|
"audioUrl": "/audio/sophisticated.mp3",
|
|
|
|
|
"sentence": "The software uses sophisticated algorithms.",
|
|
|
|
|
"blankedSentence": "The software uses _______ algorithms.",
|
|
|
|
|
"scrambledWords": ["The", "software", "uses", "sophisticated", "algorithms"]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### **批次查詢 API**
|
|
|
|
|
```http
|
|
|
|
|
GET /api/flashcards/batch?ids=1,2,3,4,5
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"success": true,
|
|
|
|
|
"data": [
|
|
|
|
|
{
|
|
|
|
|
"id": 1,
|
|
|
|
|
"baseMasteryLevel": 75,
|
|
|
|
|
"currentMasteryLevel": 68,
|
|
|
|
|
"isOverdue": true,
|
|
|
|
|
"overdueDays": 1
|
|
|
|
|
},
|
|
|
|
|
// ... 更多詞卡
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### **錯誤響應**
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"success": false,
|
|
|
|
|
"error": {
|
|
|
|
|
"code": "VALUE_OUT_OF_RANGE",
|
|
|
|
|
"message": "信心程度必須在 1-5 範圍內",
|
|
|
|
|
"field": "confidenceLevel"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
@ -207,57 +393,197 @@ public class ReviewRequestValidator : AbstractValidator<ReviewRequest>
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 💾 **資料庫設計**
|
|
|
|
|
## 💾 **資料庫設計 (基於現有DramaLingDbContext)**
|
|
|
|
|
|
|
|
|
|
### **資料表更新**
|
|
|
|
|
```sql
|
|
|
|
|
-- 現有 Flashcards 表需要的欄位
|
|
|
|
|
ALTER TABLE Flashcards ADD COLUMN
|
|
|
|
|
LastReviewDate DATETIME, -- 上次實際復習日期
|
|
|
|
|
BaseMasteryLevel INT DEFAULT 0, -- 基礎熟悉度 (上次復習時的值)
|
|
|
|
|
OverdueCount INT DEFAULT 0, -- 逾期次數統計
|
|
|
|
|
ConsecutiveOverdue INT DEFAULT 0; -- 連續逾期次數
|
|
|
|
|
|
|
|
|
|
-- 注意: CurrentMasteryLevel 不存資料庫,透過 API 實時計算
|
|
|
|
|
### **現有Flashcard模型分析**
|
|
|
|
|
```csharp
|
|
|
|
|
// 現有欄位 (已存在,無需修改)
|
|
|
|
|
public class Flashcard
|
|
|
|
|
{
|
|
|
|
|
public Guid Id { get; set; }
|
|
|
|
|
public string Word { get; set; }
|
|
|
|
|
public string Translation { get; set; }
|
|
|
|
|
public string Definition { get; set; }
|
|
|
|
|
public string? Example { get; set; }
|
|
|
|
|
public string? ExampleTranslation { get; set; }
|
|
|
|
|
public int MasteryLevel { get; set; } // ✅ 可直接使用
|
|
|
|
|
public int TimesReviewed { get; set; } // ✅ 可直接使用
|
|
|
|
|
public DateTime NextReviewDate { get; set; } // ✅ 可直接使用
|
|
|
|
|
public DateTime? LastReviewedAt { get; set; } // ✅ 可重命名使用
|
|
|
|
|
public string? DifficultyLevel { get; set; } // ✅ 用於CEFR等級
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **索引優化**
|
|
|
|
|
### **需要新增的智能複習欄位**
|
|
|
|
|
```sql
|
|
|
|
|
-- 提升查詢到期詞卡的性能
|
|
|
|
|
CREATE INDEX IX_Flashcards_NextReviewDate
|
|
|
|
|
ON Flashcards(NextReviewDate, UserId);
|
|
|
|
|
-- 新增到現有 Flashcards 表
|
|
|
|
|
ALTER TABLE Flashcards ADD COLUMN
|
|
|
|
|
IntervalDays INT DEFAULT 1, -- 當前間隔天數
|
|
|
|
|
TimesCorrect INT DEFAULT 0, -- 答對次數
|
|
|
|
|
UserLevel INT DEFAULT 50, -- 學習者程度 (1-100)
|
|
|
|
|
WordLevel INT DEFAULT 50, -- 詞彙難度 (1-100)
|
|
|
|
|
ReviewHistory TEXT, -- JSON格式的復習歷史
|
|
|
|
|
LastQuestionType VARCHAR(50); -- 最後使用的題型
|
|
|
|
|
|
|
|
|
|
-- 提升逾期統計查詢性能
|
|
|
|
|
CREATE INDEX IX_Flashcards_OverdueStats
|
|
|
|
|
ON Flashcards(LastReviewDate, NextReviewDate);
|
|
|
|
|
-- 重新命名現有欄位 (可選)
|
|
|
|
|
-- LastReviewedAt → LastReviewDate (語義更清楚)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **CEFR等級到詞彙難度映射**
|
|
|
|
|
```csharp
|
|
|
|
|
public static class CEFRMapper
|
|
|
|
|
{
|
|
|
|
|
private static readonly Dictionary<string, int> CEFRToWordLevel = new()
|
|
|
|
|
{
|
|
|
|
|
{ "A1", 20 }, // 基礎詞彙
|
|
|
|
|
{ "A2", 35 }, // 常用詞彙
|
|
|
|
|
{ "B1", 50 }, // 中級詞彙
|
|
|
|
|
{ "B2", 65 }, // 中高級詞彙
|
|
|
|
|
{ "C1", 80 }, // 高級詞彙
|
|
|
|
|
{ "C2", 95 } // 精通詞彙
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public static int GetWordLevel(string? cefrLevel)
|
|
|
|
|
{
|
|
|
|
|
return CEFRToWordLevel.GetValueOrDefault(cefrLevel ?? "B1", 50);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **索引優化 (基於現有表結構)**
|
|
|
|
|
```sql
|
|
|
|
|
-- 智能複習相關索引
|
|
|
|
|
CREATE INDEX IX_Flashcards_DueReview
|
|
|
|
|
ON Flashcards(UserId, NextReviewDate)
|
|
|
|
|
WHERE IsArchived = 0;
|
|
|
|
|
|
|
|
|
|
-- 逾期詞卡快速查詢
|
|
|
|
|
CREATE INDEX IX_Flashcards_Overdue
|
|
|
|
|
ON Flashcards(UserId, NextReviewDate, LastReviewedAt)
|
|
|
|
|
WHERE IsArchived = 0 AND NextReviewDate < DATE('now');
|
|
|
|
|
|
|
|
|
|
-- 學習統計查詢優化
|
|
|
|
|
CREATE INDEX IX_Flashcards_UserStats
|
|
|
|
|
ON Flashcards(UserId, MasteryLevel, TimesReviewed)
|
|
|
|
|
WHERE IsArchived = 0;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## ⚙️ **配置管理**
|
|
|
|
|
## ⚙️ **服務註冊與配置 (整合到現有架構)**
|
|
|
|
|
|
|
|
|
|
### **依賴注入配置 (Program.cs 或 ServiceCollectionExtensions.cs)**
|
|
|
|
|
```csharp
|
|
|
|
|
// 新增智能複習服務到現有服務註冊
|
|
|
|
|
public static IServiceCollection AddSpacedRepetitionServices(this IServiceCollection services)
|
|
|
|
|
{
|
|
|
|
|
// 核心智能複習服務
|
|
|
|
|
services.AddScoped<ISpacedRepetitionService, SpacedRepetitionService>();
|
|
|
|
|
services.AddScoped<IReviewTypeSelectorService, ReviewTypeSelectorService>();
|
|
|
|
|
services.AddScoped<IQuestionGeneratorService, QuestionGeneratorService>();
|
|
|
|
|
|
|
|
|
|
// 配置選項
|
|
|
|
|
services.Configure<SpacedRepetitionOptions>(configuration.GetSection("SpacedRepetition"));
|
|
|
|
|
|
|
|
|
|
return services;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 在 Program.cs 中調用
|
|
|
|
|
builder.Services.AddSpacedRepetitionServices();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **appsettings.json 配置**
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"SpacedRepetition": {
|
|
|
|
|
"GrowthFactors": {
|
|
|
|
|
"ShortTerm": 1.8,
|
|
|
|
|
"MediumTerm": 1.4,
|
|
|
|
|
"LongTerm": 1.2,
|
|
|
|
|
"VeryLongTerm": 1.1
|
|
|
|
|
"ShortTerm": 1.8, // ≤7天間隔
|
|
|
|
|
"MediumTerm": 1.4, // 8-30天間隔
|
|
|
|
|
"LongTerm": 1.2, // 31-90天間隔
|
|
|
|
|
"VeryLongTerm": 1.1 // >90天間隔
|
|
|
|
|
},
|
|
|
|
|
"OverduePenalties": {
|
|
|
|
|
"Light": 0.9, // 1-3天
|
|
|
|
|
"Medium": 0.75, // 4-7天
|
|
|
|
|
"Heavy": 0.5, // 8-30天
|
|
|
|
|
"Extreme": 0.3 // >30天
|
|
|
|
|
"Light": 0.9, // 1-3天逾期
|
|
|
|
|
"Medium": 0.75, // 4-7天逾期
|
|
|
|
|
"Heavy": 0.5, // 8-30天逾期
|
|
|
|
|
"Extreme": 0.3 // >30天逾期
|
|
|
|
|
},
|
|
|
|
|
"MemoryDecayRate": 0.05, // 每天5%衰減
|
|
|
|
|
"MaxInterval": 365
|
|
|
|
|
"MemoryDecayRate": 0.05, // 每天5%衰減率
|
|
|
|
|
"MaxInterval": 365, // 最大間隔天數
|
|
|
|
|
"A1ProtectionLevel": 20, // A1學習者程度門檻
|
|
|
|
|
"DefaultUserLevel": 50 // 新用戶預設程度
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **FlashcardsController 擴展**
|
|
|
|
|
```csharp
|
|
|
|
|
// 在現有 FlashcardsController 中新增智能複習端點
|
|
|
|
|
[ApiController]
|
|
|
|
|
[Route("api/flashcards")]
|
|
|
|
|
[AllowAnonymous] // 開發階段
|
|
|
|
|
public class FlashcardsController : ControllerBase
|
|
|
|
|
{
|
|
|
|
|
private readonly DramaLingDbContext _context;
|
|
|
|
|
private readonly ISpacedRepetitionService _spacedRepetitionService;
|
|
|
|
|
private readonly IReviewTypeSelectorService _reviewTypeSelectorService;
|
|
|
|
|
private readonly IQuestionGeneratorService _questionGeneratorService;
|
|
|
|
|
|
|
|
|
|
// ... 現有的CRUD端點保持不變 ...
|
|
|
|
|
|
|
|
|
|
// ================== 新增智能複習端點 ==================
|
|
|
|
|
|
|
|
|
|
[HttpGet("due")]
|
|
|
|
|
public async Task<ActionResult> GetDueFlashcards(
|
|
|
|
|
[FromQuery] string? date = null,
|
|
|
|
|
[FromQuery] int limit = 50)
|
|
|
|
|
{
|
|
|
|
|
var userId = GetUserId();
|
|
|
|
|
var queryDate = DateTime.TryParse(date, out var parsed) ? parsed : DateTime.Now.Date;
|
|
|
|
|
|
|
|
|
|
var dueCards = await _spacedRepetitionService.GetDueFlashcardsAsync(userId, queryDate, limit);
|
|
|
|
|
|
|
|
|
|
return Ok(new { success = true, data = dueCards, count = dueCards.Count });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpGet("next-review")]
|
|
|
|
|
public async Task<ActionResult> GetNextReviewCard()
|
|
|
|
|
{
|
|
|
|
|
var userId = GetUserId();
|
|
|
|
|
var nextCard = await _spacedRepetitionService.GetNextReviewCardAsync(userId);
|
|
|
|
|
|
|
|
|
|
if (nextCard == null)
|
|
|
|
|
return Ok(new { success = true, data = (object?)null, message = "沒有到期的詞卡" });
|
|
|
|
|
|
|
|
|
|
return Ok(new { success = true, data = nextCard });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpPost("{id}/optimal-review-mode")]
|
|
|
|
|
public async Task<ActionResult> GetOptimalReviewMode(Guid id, [FromBody] OptimalModeRequest request)
|
|
|
|
|
{
|
|
|
|
|
var result = await _reviewTypeSelectorService.SelectOptimalReviewModeAsync(
|
|
|
|
|
id, request.UserLevel, request.WordLevel);
|
|
|
|
|
|
|
|
|
|
return Ok(new { success = true, data = result });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpPost("{id}/question")]
|
|
|
|
|
public async Task<ActionResult> GenerateQuestion(Guid id, [FromBody] QuestionRequest request)
|
|
|
|
|
{
|
|
|
|
|
var questionData = await _questionGeneratorService.GenerateQuestionAsync(id, request.QuestionType);
|
|
|
|
|
|
|
|
|
|
return Ok(new { success = true, data = questionData });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpPost("{id}/review")]
|
|
|
|
|
public async Task<ActionResult> SubmitReview(Guid id, [FromBody] ReviewRequest request)
|
|
|
|
|
{
|
|
|
|
|
var result = await _spacedRepetitionService.ProcessReviewAsync(id, request);
|
|
|
|
|
|
|
|
|
|
return Ok(new { success = true, data = result });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🔍 **監控與日誌**
|
|
|
|
|
@ -290,62 +616,104 @@ public class ReviewMetrics
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🚀 **部署需求**
|
|
|
|
|
## 🚀 **部署與實施 (基於現有ASP.NET Core)**
|
|
|
|
|
|
|
|
|
|
### **性能要求**
|
|
|
|
|
- **API 響應時間**: P95 < 100ms
|
|
|
|
|
- **並發處理**: 支援 1000+ 同時用戶
|
|
|
|
|
- **資料庫連線**: 連線池最大 50 連線
|
|
|
|
|
### **實施步驟**
|
|
|
|
|
1. **資料庫遷移** (1天)
|
|
|
|
|
```bash
|
|
|
|
|
# 新增智能複習欄位
|
|
|
|
|
dotnet ef migrations add AddSpacedRepetitionFields
|
|
|
|
|
dotnet ef database update
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **環境配置**
|
|
|
|
|
- **.NET 8+** 運行環境
|
|
|
|
|
- **SQLite/PostgreSQL** 資料庫
|
|
|
|
|
- **Memory/Redis** 緩存 (可選)
|
|
|
|
|
2. **服務層實施** (2天)
|
|
|
|
|
- 實施3個智能複習服務
|
|
|
|
|
- 整合到現有DI容器
|
|
|
|
|
- 配置選項設定
|
|
|
|
|
|
|
|
|
|
3. **API端點實施** (1天)
|
|
|
|
|
- 在現有FlashcardsController中新增5個端點
|
|
|
|
|
- 保持現有API格式一致性
|
|
|
|
|
- 錯誤處理整合
|
|
|
|
|
|
|
|
|
|
4. **測試與驗證** (1天)
|
|
|
|
|
- 前後端API整合測試
|
|
|
|
|
- 四情境自動適配驗證
|
|
|
|
|
- 性能測試
|
|
|
|
|
|
|
|
|
|
### **現有架構相容性**
|
|
|
|
|
- **✅ 零破壞性變更**: 現有詞卡功能完全不受影響
|
|
|
|
|
- **✅ 資料庫擴展**: 只新增欄位,不修改現有結構
|
|
|
|
|
- **✅ API向後相容**: 新端點不影響現有API
|
|
|
|
|
- **✅ 服務層整合**: 使用現有DI和配置系統
|
|
|
|
|
|
|
|
|
|
### **部署檢查清單**
|
|
|
|
|
- [ ] 資料庫遷移腳本執行
|
|
|
|
|
- [ ] 配置文件更新
|
|
|
|
|
- [ ] 監控指標接入
|
|
|
|
|
- [ ] 日誌收集配置
|
|
|
|
|
- [ ] 性能測試通過
|
|
|
|
|
- [ ] appsettings.json 新增SpacedRepetition配置
|
|
|
|
|
- [ ] 服務註冊 AddSpacedRepetitionServices()
|
|
|
|
|
- [ ] Swagger文檔更新 (新增5個端點)
|
|
|
|
|
- [ ] 前端API整合測試
|
|
|
|
|
- [ ] 四情境適配邏輯驗證
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🧪 **測試策略**
|
|
|
|
|
## 🧪 **測試策略 (針對智能複習功能)**
|
|
|
|
|
|
|
|
|
|
### **單元測試**
|
|
|
|
|
### **API整合測試**
|
|
|
|
|
```csharp
|
|
|
|
|
[Test]
|
|
|
|
|
public void CalculateCurrentMasteryLevel_ShouldApplyDecay_WhenOverdue()
|
|
|
|
|
public async Task GetNextReviewCard_ShouldReturnDueCard_WhenCardsAvailable()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var flashcard = new Flashcard
|
|
|
|
|
{
|
|
|
|
|
BaseMasteryLevel = 80,
|
|
|
|
|
LastReviewDate = DateTime.Now.AddDays(-7)
|
|
|
|
|
};
|
|
|
|
|
var userId = Guid.NewGuid();
|
|
|
|
|
var dueCard = CreateTestFlashcard(userId, nextReviewDate: DateTime.Now.AddDays(-1));
|
|
|
|
|
await _context.Flashcards.AddAsync(dueCard);
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = _service.CalculateCurrentMasteryLevel(flashcard);
|
|
|
|
|
var result = await _controller.GetNextReviewCard();
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
Assert.That(result, Is.LessThan(80)); // 應該有衰減
|
|
|
|
|
Assert.That(result, Is.GreaterThan(50)); // 不應該衰減太多
|
|
|
|
|
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
|
|
|
var response = okResult.Value;
|
|
|
|
|
Assert.NotNull(response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public async Task SelectOptimalReviewMode_ShouldReturnA1BasicModes_WhenA1Learner()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var flashcardId = Guid.NewGuid();
|
|
|
|
|
var request = new OptimalModeRequest { UserLevel = 15, WordLevel = 30 };
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.GetOptimalReviewMode(flashcardId, request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
var okResult = Assert.IsType<OkObjectResult>(result);
|
|
|
|
|
var data = okResult.Value as dynamic;
|
|
|
|
|
var selectedMode = data?.data?.SelectedMode;
|
|
|
|
|
Assert.Contains(selectedMode, new[] { "flip-memory", "vocab-choice", "vocab-listening" });
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **整合測試**
|
|
|
|
|
- API 端點測試
|
|
|
|
|
- 資料庫整合測試
|
|
|
|
|
- 錯誤處理測試
|
|
|
|
|
|
|
|
|
|
### **性能測試**
|
|
|
|
|
- 1000+ 並發用戶測試
|
|
|
|
|
- 大量詞卡批次處理測試
|
|
|
|
|
- 記憶體使用量監控
|
|
|
|
|
### **四情境適配測試**
|
|
|
|
|
```csharp
|
|
|
|
|
[TestCase(15, 25, ExpectedResult = "A1學習者")] // A1保護
|
|
|
|
|
[TestCase(70, 40, ExpectedResult = "簡單詞彙")] // 簡單詞彙
|
|
|
|
|
[TestCase(60, 65, ExpectedResult = "適中詞彙")] // 適中詞彙
|
|
|
|
|
[TestCase(50, 85, ExpectedResult = "困難詞彙")] // 困難詞彙
|
|
|
|
|
public string GetAdaptationContext_ShouldReturnCorrectContext(int userLevel, int wordLevel)
|
|
|
|
|
{
|
|
|
|
|
return _reviewTypeSelectorService.GetAdaptationContext(userLevel, wordLevel);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
**實施時間**: 2-3個工作日
|
|
|
|
|
## 📋 **實施時程更新**
|
|
|
|
|
|
|
|
|
|
**實施時間**: 3-4個工作日 (比原估少1天,因為基於現有架構)
|
|
|
|
|
**測試時間**: 1個工作日
|
|
|
|
|
**上線影響**: 零停機時間部署
|
|
|
|
|
**上線影響**: 零停機時間 (純擴展功能)
|
|
|
|
|
**技術風險**: 極低 (基於成熟架構)
|