1248 lines
38 KiB
Markdown
1248 lines
38 KiB
Markdown
# 智能複習系統 - 技術實作架構規格書 (TAS)
|
|
|
|
**目標讀者**: 全端開發工程師、系統架構師、技術主管
|
|
**版本**: 1.0
|
|
**日期**: 2025-09-29
|
|
**實施狀態**: 🎯 **架構設計階段**
|
|
|
|
---
|
|
|
|
## 🏗️ **整體系統架構設計**
|
|
|
|
### **三層架構模式**
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 🌐 前端層 (React) │
|
|
├─────────────────┬─────────────────┬─────────────────┬──────────────┤
|
|
│ 複習流程組件 │ 測驗類型組件 │ 狀態管理 │ API整合層 │
|
|
│ │ │ │ │
|
|
│ SmartReview │ FlipMemoryTest │ ReviewContext │ ReviewAPI │
|
|
│ Container │ VocabChoice │ QueueManager │ FlashcardAPI│
|
|
│ TestQueue │ SentenceFill │ StateRecovery │ ReviewAPI │
|
|
│ Progress │ SentenceReorder│ Navigation │ │
|
|
│ Tracker │ ListeningTest │ Controller │ │
|
|
│ │ SpeakingTest │ │ │
|
|
└─────────────────┴─────────────────┴─────────────────┴──────────────┘
|
|
│
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 🔗 API 契約層 │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ 統一API規範 + 錯誤處理 + 認證授權 + 快取策略 │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 🏛️ 後端層 (.NET Core) │
|
|
├─────────────────┬─────────────────┬─────────────────┬──────────────┤
|
|
│ 控制器層 │ 服務層 │ 資料層 │ 基礎設施 │
|
|
│ │ │ │ │
|
|
│ ReviewController │ SpacedRepetition│ ReviewRecord │ JWTAuth │
|
|
│ FlashcardCtrl │ ReviewSelector │ Flashcard │ Cache │
|
|
│ StatsController │ QuestionGen │ DailyStats │ Logging │
|
|
│ │ BlankGeneration │ OptionsVocab │ Monitoring │
|
|
│ │ CEFRMapping │ │ │
|
|
└─────────────────┴─────────────────┴─────────────────┴──────────────┘
|
|
│
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 💾 資料庫層 (SQLite/PostgreSQL) │
|
|
│ 智能索引 + 關聯關係 + 效能優化 │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 **前端組件架構設計**
|
|
|
|
### **核心組件層次結構**
|
|
```typescript
|
|
// 最外層容器
|
|
SmartReviewContainer
|
|
├── ReviewContextProvider // 全域狀態管理
|
|
├── TestQueueManager // 測驗隊列管理
|
|
├── ProgressTracker // 進度追蹤和可視化
|
|
├── NavigationController // 導航邏輯控制
|
|
└── TestRenderer // 動態測驗渲染器
|
|
├── FlipMemoryTest // 翻卡記憶測驗
|
|
├── VocabChoiceTest // 詞彙選擇測驗
|
|
├── SentenceFillTest // 例句填空測驗
|
|
├── SentenceReorderTest // 例句重組測驗
|
|
├── VocabListeningTest // 詞彙聽力測驗
|
|
├── SentenceListeningTest // 例句聽力測驗
|
|
└── SentenceSpeakingTest // 例句口說測驗
|
|
```
|
|
|
|
### **狀態管理架構**
|
|
```typescript
|
|
// 🧠 全域複習狀態 Context
|
|
interface ReviewContextState {
|
|
// 基礎數據
|
|
dueCards: Flashcard[]
|
|
completedTests: CompletedTest[]
|
|
|
|
// 隊列管理
|
|
testQueue: TestItem[]
|
|
currentTestIndex: number
|
|
skippedTests: TestItem[]
|
|
|
|
// 用戶狀態
|
|
userCEFRLevel: string
|
|
learningSession: LearningSession
|
|
|
|
// UI 狀態
|
|
isAnswered: boolean
|
|
showResult: boolean
|
|
navigationState: 'skip' | 'continue'
|
|
}
|
|
|
|
// 🎯 隊列管理邏輯
|
|
class TestQueueManager {
|
|
// 智能排序邏輯
|
|
prioritizeTests(tests: TestItem[]): TestItem[] {
|
|
return tests.sort((a, b) => {
|
|
// 1. 新測驗 (最高優先級)
|
|
if (a.status === 'new' && b.status !== 'new') return -1
|
|
if (b.status === 'new' && a.status !== 'new') return 1
|
|
|
|
// 2. 答錯測驗 (中等優先級)
|
|
if (a.status === 'incorrect' && b.status === 'skipped') return -1
|
|
if (b.status === 'incorrect' && a.status === 'skipped') return 1
|
|
|
|
// 3. 跳過測驗 (最低優先級)
|
|
return 0
|
|
})
|
|
}
|
|
|
|
// 處理測驗結果
|
|
handleTestResult(testId: string, result: 'correct' | 'incorrect' | 'skipped') {
|
|
switch (result) {
|
|
case 'correct':
|
|
// 從隊列完全移除
|
|
this.removeFromQueue(testId)
|
|
break
|
|
case 'incorrect':
|
|
// 移到隊列最後
|
|
this.moveToEnd(testId, 'incorrect')
|
|
break
|
|
case 'skipped':
|
|
// 移到隊列最後
|
|
this.moveToEnd(testId, 'skipped')
|
|
break
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### **測驗組件標準化介面**
|
|
```typescript
|
|
// 🧩 測驗組件統一介面
|
|
interface TestComponentProps {
|
|
flashcard: Flashcard
|
|
testType: TestType
|
|
onSubmit: (result: TestResult) => void
|
|
onSkip: () => void
|
|
isDisabled: boolean
|
|
}
|
|
|
|
// 📝 測驗結果標準格式
|
|
interface TestResult {
|
|
flashcardId: string
|
|
testType: TestType
|
|
isCorrect: boolean
|
|
userAnswer: string
|
|
confidenceLevel?: number
|
|
responseTimeMs: number
|
|
}
|
|
|
|
// 🎮 導航狀態管理
|
|
type NavigationState = 'pre-answer' | 'post-answer'
|
|
type ButtonAction = 'skip' | 'continue'
|
|
```
|
|
|
|
---
|
|
|
|
## 🏛️ **後端服務架構設計**
|
|
|
|
### **控制器職責重新劃分**
|
|
```csharp
|
|
// 📚 FlashcardsController - 純粹詞卡資料管理
|
|
[Route("api/flashcards")]
|
|
public class FlashcardsController
|
|
{
|
|
// 純粹的 CRUD 操作
|
|
GET / // 詞卡列表查詢
|
|
POST / // 創建新詞卡
|
|
GET /{id} // 詞卡詳情
|
|
PUT /{id} // 更新詞卡
|
|
DELETE /{id} // 刪除詞卡
|
|
POST /{id}/favorite // 收藏切換
|
|
}
|
|
|
|
// 🎯 ReviewController - 完整複習管理
|
|
[Route("api/review")]
|
|
public class ReviewController
|
|
{
|
|
// 複習狀態管理
|
|
GET /today // 今日複習總覽
|
|
GET /next // 下一個測驗
|
|
GET /progress // 複習進度
|
|
|
|
// 測驗執行
|
|
POST /{id}/question // 生成題目選項
|
|
POST /{id}/submit // 提交測驗結果
|
|
POST /{id}/skip // 跳過測驗
|
|
|
|
// 智能適配
|
|
POST /{id}/optimal-mode // 智能模式選擇
|
|
|
|
// 狀態持久化
|
|
GET /completed-tests // 已完成測驗
|
|
POST /record-test // 記錄測驗
|
|
GET /stats // 複習統計
|
|
}
|
|
```
|
|
|
|
### **智能複習服務群架構**
|
|
```csharp
|
|
// 🧠 核心算法服務
|
|
public interface ISpacedRepetitionService
|
|
{
|
|
Task<ReviewResult> ProcessReviewAsync(Guid flashcardId, ReviewRequest request);
|
|
Task<List<Flashcard>> GetTodaysDueCardsAsync(Guid userId);
|
|
Task<TestItem?> GetNextTestAsync(Guid userId);
|
|
int CalculateCurrentMasteryLevel(Flashcard flashcard);
|
|
}
|
|
|
|
// 🎯 智能適配服務
|
|
public interface IReviewAdaptationService
|
|
{
|
|
Task<ReviewModeResult> SelectOptimalModeAsync(Flashcard flashcard, User user);
|
|
string[] GetAvailableTestTypes(string userCEFR, string wordCEFR);
|
|
AdaptationContext GetAdaptationContext(string userCEFR, string wordCEFR);
|
|
}
|
|
|
|
// 🔄 隊列管理服務
|
|
public interface ITestQueueService
|
|
{
|
|
Task<TestQueue> BuildTodaysQueueAsync(Guid userId);
|
|
Task<TestItem?> GetNextTestItemAsync(Guid userId);
|
|
Task HandleTestResultAsync(Guid userId, TestResult result);
|
|
Task<QueueStatus> GetQueueStatusAsync(Guid userId);
|
|
}
|
|
|
|
// 📝 題目生成服務
|
|
public interface IQuestionGeneratorService
|
|
{
|
|
Task<QuestionData> GenerateQuestionAsync(Guid flashcardId, TestType testType);
|
|
}
|
|
|
|
// 💾 狀態持久化服務
|
|
public interface IReviewStateService
|
|
{
|
|
Task<CompletedTest[]> GetCompletedTestsAsync(Guid userId, DateTime? date = null);
|
|
Task<bool> RecordTestCompletionAsync(Guid userId, TestResult result);
|
|
Task<LearningProgress> GetProgressAsync(Guid userId);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 💾 **資料模型設計規範**
|
|
|
|
### **核心實體關係**
|
|
```csharp
|
|
// 👤 用戶實體 (擴展 CEFR 支援)
|
|
public class User
|
|
{
|
|
public Guid Id { get; set; }
|
|
public string Username { get; set; }
|
|
public string Email { get; set; }
|
|
|
|
// 🆕 CEFR 智能適配
|
|
public string EnglishLevel { get; set; } = "A2"; // A1-C2
|
|
public DateTime LevelUpdatedAt { get; set; }
|
|
public bool IsLevelVerified { get; set; }
|
|
|
|
// Navigation Properties
|
|
public ICollection<Flashcard> Flashcards { get; set; }
|
|
public ICollection<ReviewRecord> ReviewRecords { get; set; }
|
|
public ICollection<DailyStats> DailyStats { get; set; }
|
|
}
|
|
|
|
// 📚 詞卡實體 (智能複習增強)
|
|
public class Flashcard
|
|
{
|
|
public Guid Id { get; set; }
|
|
public Guid UserId { get; set; }
|
|
|
|
// 詞卡內容
|
|
public string Word { get; set; }
|
|
public string Translation { get; set; }
|
|
public string Definition { get; set; }
|
|
public string? PartOfSpeech { get; set; }
|
|
public string? Pronunciation { get; set; }
|
|
public string? Example { get; set; }
|
|
public string? ExampleTranslation { get; set; }
|
|
|
|
// 🆕 智能複習參數
|
|
public string? DifficultyLevel { get; set; } // A1-C2
|
|
public float EasinessFactor { get; set; } = 2.5f; // SM-2 算法
|
|
public int Repetitions { get; set; } = 0;
|
|
public int IntervalDays { get; set; } = 1;
|
|
public DateTime NextReviewDate { get; set; }
|
|
public int MasteryLevel { get; set; } = 0; // 0-100
|
|
public int TimesReviewed { get; set; } = 0;
|
|
public int TimesCorrect { get; set; } = 0;
|
|
public DateTime? LastReviewedAt { get; set; }
|
|
|
|
// 🆕 測驗歷史追蹤
|
|
public string? ReviewHistory { get; set; } // JSON
|
|
public string? LastQuestionType { get; set; }
|
|
|
|
// Navigation Properties
|
|
public User User { get; set; }
|
|
public ICollection<ReviewRecord> ReviewRecords { get; set; }
|
|
}
|
|
|
|
// 📊 複習記錄實體 (簡化無 Session)
|
|
public class ReviewRecord
|
|
{
|
|
public Guid Id { get; set; }
|
|
public Guid UserId { get; set; }
|
|
public Guid FlashcardId { get; set; }
|
|
|
|
// 測驗資訊
|
|
public string ReviewMode { get; set; } // 測驗類型
|
|
public int QualityRating { get; set; } // 1-5 (SM-2)
|
|
public bool IsCorrect { get; set; }
|
|
public string? UserAnswer { get; set; }
|
|
public int? ResponseTimeMs { get; set; }
|
|
public DateTime StudiedAt { get; set; }
|
|
|
|
// SM-2 追蹤參數
|
|
public double? PreviousEasinessFactor { get; set; }
|
|
public double? NewEasinessFactor { get; set; }
|
|
public int? PreviousIntervalDays { get; set; }
|
|
public int? NewIntervalDays { get; set; }
|
|
public DateTime? NextReviewDate { get; set; }
|
|
|
|
// Navigation Properties
|
|
public User User { get; set; }
|
|
public Flashcard Flashcard { get; set; }
|
|
}
|
|
|
|
// 📈 每日統計實體
|
|
public class DailyStats
|
|
{
|
|
public Guid Id { get; set; }
|
|
public Guid UserId { get; set; }
|
|
public DateOnly Date { get; set; }
|
|
|
|
// 複習統計
|
|
public int WordsStudied { get; set; } = 0;
|
|
public int WordsCorrect { get; set; } = 0;
|
|
public int ReviewTimeSeconds { get; set; } = 0;
|
|
public int SessionCount { get; set; } = 0;
|
|
|
|
// Navigation Properties
|
|
public User User { get; set; }
|
|
}
|
|
```
|
|
|
|
### **資料庫索引策略**
|
|
```sql
|
|
-- 🎯 智能複習核心索引
|
|
CREATE INDEX IX_Flashcards_UserDue
|
|
ON flashcards(user_id, next_review_date)
|
|
WHERE is_archived = 0;
|
|
|
|
-- 📊 複習記錄查詢優化
|
|
CREATE UNIQUE INDEX IX_ReviewRecord_UserCardTest
|
|
ON review_records(user_id, flashcard_id, review_mode);
|
|
|
|
-- 📈 統計查詢優化
|
|
CREATE UNIQUE INDEX IX_DailyStats_UserDate
|
|
ON daily_stats(user_id, date);
|
|
|
|
-- 🔍 詞卡搜尋優化
|
|
CREATE INDEX IX_Flashcards_Search
|
|
ON flashcards(user_id, word, translation)
|
|
WHERE is_archived = 0;
|
|
|
|
-- ⚡ CEFR 等級查詢優化
|
|
CREATE INDEX IX_Flashcards_CEFR
|
|
ON flashcards(user_id, difficulty_level, mastery_level);
|
|
```
|
|
|
|
---
|
|
|
|
## 🔗 **API 設計規範**
|
|
|
|
### **統一 API 契約**
|
|
```typescript
|
|
// 🌐 統一響應格式
|
|
interface ApiResponse<T> {
|
|
success: boolean
|
|
data?: T
|
|
error?: string
|
|
message?: string
|
|
timestamp: string
|
|
requestId?: string
|
|
}
|
|
|
|
// ⚠️ 統一錯誤格式
|
|
interface ApiError {
|
|
code: string
|
|
message: string
|
|
details?: any
|
|
suggestions?: string[]
|
|
}
|
|
```
|
|
|
|
### **智能複習 API 設計**
|
|
```typescript
|
|
// 📋 今日複習總覽 API
|
|
GET /api/review/today
|
|
Response: {
|
|
success: true,
|
|
data: {
|
|
dueCards: Flashcard[],
|
|
totalTests: number,
|
|
completedTests: number,
|
|
estimatedTimeMinutes: number,
|
|
adaptationSummary: {
|
|
a1Protection: boolean,
|
|
primaryAdaptation: "簡單詞彙" | "適中詞彙" | "困難詞彙",
|
|
recommendedTestTypes: string[]
|
|
}
|
|
}
|
|
}
|
|
|
|
// 🎯 下一個測驗 API
|
|
GET /api/review/next
|
|
Response: {
|
|
success: true,
|
|
data: {
|
|
testItem: {
|
|
flashcardId: string,
|
|
testType: string,
|
|
flashcard: Flashcard,
|
|
questionData: QuestionData
|
|
},
|
|
queueStatus: {
|
|
remaining: number,
|
|
completed: number,
|
|
skipped: number
|
|
},
|
|
navigationState: "skip" | "continue"
|
|
}
|
|
}
|
|
|
|
// 📝 提交測驗 API
|
|
POST /api/review/{flashcardId}/submit
|
|
Request: {
|
|
testType: string,
|
|
isCorrect: boolean,
|
|
userAnswer: string,
|
|
confidenceLevel?: number,
|
|
responseTimeMs: number
|
|
}
|
|
Response: {
|
|
success: true,
|
|
data: {
|
|
reviewResult: ReviewResult,
|
|
nextAction: "continue" | "session_complete",
|
|
masteryUpdate: {
|
|
previousLevel: number,
|
|
newLevel: number,
|
|
nextReviewDate: string
|
|
}
|
|
}
|
|
}
|
|
|
|
// ⏭️ 跳過測驗 API
|
|
POST /api/review/{flashcardId}/skip
|
|
Request: {
|
|
testType: string,
|
|
reason?: string
|
|
}
|
|
Response: {
|
|
success: true,
|
|
data: {
|
|
skippedTestId: string,
|
|
nextAction: "continue",
|
|
queueStatus: QueueStatus
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🧠 **智能適配算法設計**
|
|
|
|
### **CEFR 四情境適配邏輯**
|
|
```csharp
|
|
// 🎯 CEFRMappingService - 等級轉換服務
|
|
public static class CEFRMappingService
|
|
{
|
|
private static readonly Dictionary<string, int> CEFRToLevel = new()
|
|
{
|
|
{ "A1", 20 }, { "A2", 35 }, { "B1", 50 },
|
|
{ "B2", 65 }, { "C1", 80 }, { "C2", 95 }
|
|
};
|
|
|
|
public static int GetNumericLevel(string cefrLevel)
|
|
=> CEFRToLevel.GetValueOrDefault(cefrLevel, 50);
|
|
|
|
public static string GetCEFRLevel(int numericLevel)
|
|
=> CEFRToLevel.FirstOrDefault(kvp => kvp.Value == numericLevel).Key ?? "B1";
|
|
}
|
|
|
|
// 🛡️ 智能適配服務實現
|
|
public class ReviewAdaptationService : IReviewAdaptationService
|
|
{
|
|
public async Task<ReviewModeResult> SelectOptimalModeAsync(Flashcard flashcard, User user)
|
|
{
|
|
var userLevel = CEFRMappingService.GetNumericLevel(user.EnglishLevel);
|
|
var wordLevel = CEFRMappingService.GetNumericLevel(flashcard.DifficultyLevel ?? "B1");
|
|
|
|
// 四情境判斷邏輯
|
|
var adaptationContext = GetAdaptationContext(userLevel, wordLevel);
|
|
var availableTestTypes = GetAvailableTestTypes(adaptationContext);
|
|
var selectedMode = await SelectModeWithHistory(flashcard.Id, availableTestTypes);
|
|
|
|
return new ReviewModeResult
|
|
{
|
|
SelectedMode = selectedMode,
|
|
AdaptationContext = adaptationContext,
|
|
AvailableTestTypes = availableTestTypes,
|
|
Reason = GetSelectionReason(adaptationContext, selectedMode)
|
|
};
|
|
}
|
|
|
|
private AdaptationContext GetAdaptationContext(int userLevel, int wordLevel)
|
|
{
|
|
var difficulty = wordLevel - userLevel;
|
|
|
|
if (userLevel <= 20) // A1 保護
|
|
return AdaptationContext.A1Protection;
|
|
|
|
if (difficulty < -10) // 簡單詞彙
|
|
return AdaptationContext.EasyVocabulary;
|
|
|
|
if (difficulty >= -10 && difficulty <= 10) // 適中詞彙
|
|
return AdaptationContext.ModerateVocabulary;
|
|
|
|
return AdaptationContext.DifficultVocabulary; // 困難詞彙
|
|
}
|
|
|
|
private string[] GetAvailableTestTypes(AdaptationContext context)
|
|
{
|
|
return context switch
|
|
{
|
|
AdaptationContext.A1Protection => new[] { "flip-memory", "vocab-choice", "vocab-listening" },
|
|
AdaptationContext.EasyVocabulary => new[] { "sentence-reorder", "sentence-fill" },
|
|
AdaptationContext.ModerateVocabulary => new[] { "sentence-fill", "sentence-reorder", "sentence-speaking" },
|
|
AdaptationContext.DifficultVocabulary => new[] { "flip-memory", "vocab-choice" },
|
|
_ => new[] { "flip-memory", "vocab-choice" }
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### **隊列管理算法**
|
|
```csharp
|
|
// 🔄 測驗隊列服務
|
|
public class TestQueueService : ITestQueueService
|
|
{
|
|
public async Task<TestQueue> BuildTodaysQueueAsync(Guid userId)
|
|
{
|
|
// 1. 獲取今日到期詞卡
|
|
var dueCards = await GetTodaysDueCardsAsync(userId);
|
|
|
|
// 2. 獲取已完成測驗
|
|
var completedTests = await GetCompletedTestsAsync(userId, DateTime.Today);
|
|
|
|
// 3. 計算剩餘測驗
|
|
var allTests = GenerateAllPossibleTests(dueCards);
|
|
var remainingTests = FilterCompletedTests(allTests, completedTests);
|
|
|
|
// 4. 智能排序
|
|
var prioritizedTests = PrioritizeTests(remainingTests);
|
|
|
|
return new TestQueue
|
|
{
|
|
Tests = prioritizedTests,
|
|
TotalCount = allTests.Count,
|
|
CompletedCount = completedTests.Count,
|
|
RemainingCount = remainingTests.Count
|
|
};
|
|
}
|
|
|
|
private List<TestItem> PrioritizeTests(List<TestItem> tests)
|
|
{
|
|
return tests
|
|
.OrderBy(t => t.Status switch {
|
|
TestStatus.New => 0, // 優先處理新測驗
|
|
TestStatus.Incorrect => 1, // 然後是答錯的
|
|
TestStatus.Skipped => 2, // 最後是跳過的
|
|
_ => 3
|
|
})
|
|
.ThenBy(t => t.LastAttemptAt ?? DateTime.MinValue) // 按最後嘗試時間排序
|
|
.ToList();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎮 **前端狀態管理架構**
|
|
|
|
### **Context + Hooks 架構**
|
|
```typescript
|
|
// 🧠 Review Context Provider
|
|
export const ReviewContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
const [state, dispatch] = useReducer(reviewReducer, initialState)
|
|
|
|
// 核心方法
|
|
const contextValue = {
|
|
// 狀態
|
|
...state,
|
|
|
|
// 操作方法
|
|
loadTodaysReview: () => dispatch({ type: 'LOAD_TODAYS_REVIEW' }),
|
|
submitTest: (result: TestResult) => dispatch({ type: 'SUBMIT_TEST', payload: result }),
|
|
skipTest: (testId: string) => dispatch({ type: 'SKIP_TEST', payload: testId }),
|
|
proceedToNext: () => dispatch({ type: 'PROCEED_TO_NEXT' }),
|
|
|
|
// 狀態查詢
|
|
getCurrentTest: () => state.testQueue[state.currentTestIndex],
|
|
getNavigationState: () => state.isAnswered ? 'continue' : 'skip',
|
|
isSessionComplete: () => state.testQueue.every(t => t.status === 'completed')
|
|
}
|
|
|
|
return (
|
|
<ReviewContext.Provider value={contextValue}>
|
|
{children}
|
|
</ReviewContext.Provider>
|
|
)
|
|
}
|
|
|
|
// 🎯 自定義 Hooks
|
|
export const useReviewFlow = () => {
|
|
const context = useContext(ReviewContext)
|
|
|
|
return {
|
|
// 當前測驗
|
|
currentTest: context.getCurrentTest(),
|
|
|
|
// 導航控制
|
|
navigationState: context.getNavigationState(),
|
|
canSkip: !context.isAnswered,
|
|
canContinue: context.isAnswered,
|
|
|
|
// 操作方法
|
|
submitAnswer: context.submitTest,
|
|
skipCurrent: context.skipTest,
|
|
proceedNext: context.proceedToNext,
|
|
|
|
// 進度資訊
|
|
progress: {
|
|
completed: context.testQueue.filter(t => t.status === 'completed').length,
|
|
total: context.testQueue.length,
|
|
isComplete: context.isSessionComplete()
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### **組件化架構實現**
|
|
```typescript
|
|
// 🎮 智能複習主容器
|
|
export const SmartReviewContainer: React.FC = () => {
|
|
return (
|
|
<ReviewContextProvider>
|
|
<div className="smart-review-container">
|
|
<ProgressTracker />
|
|
<TestRenderer />
|
|
<NavigationController />
|
|
</div>
|
|
</ReviewContextProvider>
|
|
)
|
|
}
|
|
|
|
// 📊 進度追蹤組件
|
|
export const ProgressTracker: React.FC = () => {
|
|
const { progress } = useReviewFlow()
|
|
|
|
return (
|
|
<div className="progress-tracker">
|
|
{/* 雙層進度條實現 */}
|
|
<div className="card-progress">
|
|
<div className="progress-bar">
|
|
<div
|
|
className="progress-fill"
|
|
style={{ width: `${(progress.completed / progress.total) * 100}%` }}
|
|
/>
|
|
</div>
|
|
<span>{progress.completed}/{progress.total} 已完成</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 🎯 動態測驗渲染器
|
|
export const TestRenderer: React.FC = () => {
|
|
const { currentTest } = useReviewFlow()
|
|
|
|
if (!currentTest) {
|
|
return <SessionCompleteScreen />
|
|
}
|
|
|
|
// 動態載入對應的測驗組件
|
|
const TestComponent = getTestComponent(currentTest.testType)
|
|
|
|
return (
|
|
<div className="test-renderer">
|
|
<TestComponent
|
|
flashcard={currentTest.flashcard}
|
|
questionData={currentTest.questionData}
|
|
onSubmit={handleTestSubmit}
|
|
onSkip={handleTestSkip}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 🧭 導航控制器
|
|
export const NavigationController: React.FC = () => {
|
|
const { navigationState, canSkip, canContinue, skipCurrent, proceedNext } = useReviewFlow()
|
|
|
|
return (
|
|
<div className="navigation-controller">
|
|
{navigationState === 'skip' && canSkip && (
|
|
<button onClick={skipCurrent} className="skip-button">
|
|
⏭️ 跳過這題
|
|
</button>
|
|
)}
|
|
|
|
{navigationState === 'continue' && canContinue && (
|
|
<button onClick={proceedNext} className="continue-button">
|
|
➡️ 繼續下一題
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 **服務層實現規範**
|
|
|
|
### **依賴注入配置**
|
|
```csharp
|
|
// 📦 服務註冊 (Program.cs)
|
|
public static class ServiceCollectionExtensions
|
|
{
|
|
public static IServiceCollection AddSmartReviewServices(this IServiceCollection services)
|
|
{
|
|
// 核心智能複習服務
|
|
services.AddScoped<ISpacedRepetitionService, SpacedRepetitionService>();
|
|
services.AddScoped<IReviewAdaptationService, ReviewAdaptationService>();
|
|
services.AddScoped<ITestQueueService, TestQueueService>();
|
|
services.AddScoped<IQuestionGeneratorService, QuestionGeneratorService>();
|
|
services.AddScoped<IReviewStateService, ReviewStateService>();
|
|
|
|
// 輔助服務
|
|
services.AddSingleton<CEFRMappingService>();
|
|
services.AddScoped<IOptionsVocabularyService, OptionsVocabularyService>();
|
|
services.AddScoped<IBlankGenerationService, BlankGenerationService>();
|
|
|
|
// 配置選項
|
|
services.Configure<SpacedRepetitionOptions>(config.GetSection("SpacedRepetition"));
|
|
|
|
return services;
|
|
}
|
|
}
|
|
```
|
|
|
|
### **錯誤處理統一標準**
|
|
```csharp
|
|
// 🛡️ 全域錯誤處理中間件
|
|
public class SmartReviewErrorHandlingMiddleware
|
|
{
|
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
|
{
|
|
try
|
|
{
|
|
await next(context);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await HandleExceptionAsync(context, ex);
|
|
}
|
|
}
|
|
|
|
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
|
|
{
|
|
var response = ex switch
|
|
{
|
|
ArgumentException => CreateErrorResponse("INVALID_INPUT", ex.Message, 400),
|
|
UnauthorizedAccessException => CreateErrorResponse("UNAUTHORIZED", "認證失敗", 401),
|
|
InvalidOperationException => CreateErrorResponse("INVALID_OPERATION", ex.Message, 400),
|
|
_ => CreateErrorResponse("INTERNAL_ERROR", "系統內部錯誤", 500)
|
|
};
|
|
|
|
context.Response.StatusCode = response.StatusCode;
|
|
context.Response.ContentType = "application/json";
|
|
await context.Response.WriteAsync(JsonSerializer.Serialize(response.Content));
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 **效能優化架構**
|
|
|
|
### **快取策略設計**
|
|
```csharp
|
|
// 🚀 多層快取架構
|
|
public class SmartReviewCacheService
|
|
{
|
|
// L1: Memory Cache (最快)
|
|
private readonly IMemoryCache _memoryCache;
|
|
|
|
// L2: Distributed Cache (Redis/SQL)
|
|
private readonly IDistributedCache _distributedCache;
|
|
|
|
// L3: Database (最持久)
|
|
private readonly DramaLingDbContext _context;
|
|
|
|
public async Task<T?> GetAsync<T>(string key) where T : class
|
|
{
|
|
// 1. 嘗試記憶體快取
|
|
if (_memoryCache.TryGetValue(key, out T? cachedValue))
|
|
return cachedValue;
|
|
|
|
// 2. 嘗試分散式快取
|
|
var distributedValue = await _distributedCache.GetStringAsync(key);
|
|
if (distributedValue != null)
|
|
{
|
|
var deserializedValue = JsonSerializer.Deserialize<T>(distributedValue);
|
|
_memoryCache.Set(key, deserializedValue, TimeSpan.FromMinutes(5));
|
|
return deserializedValue;
|
|
}
|
|
|
|
// 3. 資料庫查詢 (最後選擇)
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 📈 關鍵快取策略
|
|
var cacheStrategies = new Dictionary<string, CacheStrategy>
|
|
{
|
|
["today_due_cards"] = new(TimeSpan.FromMinutes(15), CacheLevel.Memory),
|
|
["completed_tests"] = new(TimeSpan.FromMinutes(30), CacheLevel.Distributed),
|
|
["user_cefr_mapping"] = new(TimeSpan.FromHours(1), CacheLevel.Memory),
|
|
["question_options"] = new(TimeSpan.FromMinutes(10), CacheLevel.Memory)
|
|
};
|
|
```
|
|
|
|
### **API 效能優化**
|
|
```csharp
|
|
// ⚡ 查詢優化策略
|
|
public class OptimizedReviewService
|
|
{
|
|
// 批量預載入相關資料
|
|
public async Task<List<Flashcard>> GetDueCardsWithOptimizationAsync(Guid userId)
|
|
{
|
|
return await _context.Flashcards
|
|
.Where(f => f.UserId == userId && f.NextReviewDate <= DateTime.Today)
|
|
.Include(f => f.ReviewRecords.Where(sr => sr.StudiedAt.Date == DateTime.Today))
|
|
.AsNoTracking() // 只讀查詢優化
|
|
.AsSplitQuery() // 分割查詢避免笛卡爾積
|
|
.ToListAsync();
|
|
}
|
|
|
|
// 智能預測下一個測驗
|
|
public async Task<TestItem?> PredictNextTestAsync(Guid userId)
|
|
{
|
|
// 使用演算法預測,減少即時計算
|
|
var cachedPrediction = await _cache.GetAsync($"next_test:{userId}");
|
|
if (cachedPrediction != null) return cachedPrediction;
|
|
|
|
// 重新計算並快取
|
|
var nextTest = await ComputeNextTestAsync(userId);
|
|
await _cache.SetAsync($"next_test:{userId}", nextTest, TimeSpan.FromMinutes(5));
|
|
|
|
return nextTest;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔐 **安全與認證架構**
|
|
|
|
### **JWT 認證策略**
|
|
```csharp
|
|
// 🔑 JWT 配置 (Program.cs)
|
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
.AddJwtBearer(options =>
|
|
{
|
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
{
|
|
ValidateIssuer = true,
|
|
ValidateAudience = true,
|
|
ValidateLifetime = true,
|
|
ValidateIssuerSigningKey = true,
|
|
ValidIssuer = supabaseUrl,
|
|
ValidAudience = "authenticated",
|
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret))
|
|
};
|
|
|
|
// 智能複習特殊處理:允許過期 token 緩衝期
|
|
options.Events = new JwtBearerEvents
|
|
{
|
|
OnTokenValidated = context =>
|
|
{
|
|
// 檢查複習狀態,允許正在複習的用戶有 5 分鐘緩衝
|
|
return Task.CompletedTask;
|
|
}
|
|
};
|
|
});
|
|
```
|
|
|
|
### **權限控制策略**
|
|
```csharp
|
|
// 🛡️ 智能複習權限檢查
|
|
[AttributeUsage(AttributeTargets.Method)]
|
|
public class RequireFlashcardOwnershipAttribute : Attribute, IAuthorizationFilter
|
|
{
|
|
public void OnAuthorization(AuthorizationFilterContext context)
|
|
{
|
|
// 檢查詞卡所有權
|
|
var flashcardId = GetFlashcardIdFromRoute(context);
|
|
var userId = GetUserIdFromToken(context);
|
|
|
|
if (!ValidateOwnership(flashcardId, userId))
|
|
{
|
|
context.Result = new UnauthorizedResult();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 使用範例
|
|
[HttpPost("{id}/submit")]
|
|
[RequireFlashcardOwnership]
|
|
public async Task<ActionResult> SubmitTest(Guid id, [FromBody] TestResult result)
|
|
{
|
|
// 已通過權限檢查,直接處理業務邏輯
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📈 **監控與可觀測性**
|
|
|
|
### **關鍵指標監控**
|
|
```csharp
|
|
// 📊 智能複習指標收集
|
|
public class SmartReviewMetrics
|
|
{
|
|
// 業務指標
|
|
[Counter("smart_review_tests_completed_total")]
|
|
public static readonly Counter TestsCompleted;
|
|
|
|
[Counter("smart_review_tests_skipped_total")]
|
|
public static readonly Counter TestsSkipped;
|
|
|
|
[Histogram("smart_review_response_time_ms")]
|
|
public static readonly Histogram ResponseTime;
|
|
|
|
[Gauge("smart_review_active_sessions")]
|
|
public static readonly Gauge ActiveSessions;
|
|
|
|
// CEFR 適配指標
|
|
[Counter("cefr_adaptation_selections_total")]
|
|
public static readonly Counter CEFRAdaptations;
|
|
|
|
[Histogram("cefr_adaptation_accuracy")]
|
|
public static readonly Histogram AdaptationAccuracy;
|
|
}
|
|
```
|
|
|
|
### **健康檢查端點**
|
|
```csharp
|
|
// 🩺 智能複習系統健康檢查
|
|
[HttpGet("/health/smart-review")]
|
|
public async Task<ActionResult> GetSmartReviewHealth()
|
|
{
|
|
var healthChecks = new Dictionary<string, object>
|
|
{
|
|
["database"] = await CheckDatabaseConnectionAsync(),
|
|
["cefr_mapping"] = CheckCEFRMappingService(),
|
|
["spaced_repetition"] = CheckSpacedRepetitionAlgorithm(),
|
|
["test_queue"] = await CheckTestQueueServiceAsync(),
|
|
["api_response_time"] = await MeasureAPIResponseTimeAsync()
|
|
};
|
|
|
|
var isHealthy = healthChecks.Values.All(v => v.ToString() == "Healthy");
|
|
|
|
return Ok(new
|
|
{
|
|
Status = isHealthy ? "Healthy" : "Degraded",
|
|
Checks = healthChecks,
|
|
Timestamp = DateTime.UtcNow,
|
|
SystemInfo = new
|
|
{
|
|
ActiveUsers = await GetActiveUserCountAsync(),
|
|
TestsCompletedToday = await GetTodaysTestCountAsync(),
|
|
AverageResponseTimeMs = GetAverageResponseTime()
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 **部署架構規範**
|
|
|
|
### **環境配置標準**
|
|
```json
|
|
// 🔧 appsettings.json (生產環境)
|
|
{
|
|
"SmartReview": {
|
|
"EnableAdaptiveAlgorithm": true,
|
|
"A1ProtectionLevel": 20,
|
|
"CEFRMappingCache": {
|
|
"ExpirationMinutes": 60,
|
|
"MaxEntries": 1000
|
|
},
|
|
"TestQueue": {
|
|
"MaxQueueSize": 100,
|
|
"PriorityRecalculationMinutes": 15,
|
|
"SkipLimitPerSession": 50
|
|
},
|
|
"Performance": {
|
|
"MaxConcurrentSessions": 1000,
|
|
"ResponseTimeThresholdMs": 100,
|
|
"CacheHitRateThreshold": 0.8
|
|
}
|
|
},
|
|
"SpacedRepetition": {
|
|
"Algorithm": "SM2Enhanced",
|
|
"GrowthFactors": {
|
|
"ShortTerm": 1.8,
|
|
"MediumTerm": 1.4,
|
|
"LongTerm": 1.2,
|
|
"VeryLongTerm": 1.1
|
|
},
|
|
"OverduePenalties": {
|
|
"Light": 0.9,
|
|
"Medium": 0.75,
|
|
"Heavy": 0.5,
|
|
"Extreme": 0.3
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### **容器化部署配置**
|
|
```dockerfile
|
|
# 🐳 Dockerfile (生產環境)
|
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
|
WORKDIR /app
|
|
EXPOSE 8080
|
|
|
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
|
WORKDIR /src
|
|
COPY ["DramaLing.Api.csproj", "."]
|
|
RUN dotnet restore
|
|
|
|
COPY . .
|
|
RUN dotnet build -c Release -o /app/build
|
|
|
|
FROM build AS publish
|
|
RUN dotnet publish -c Release -o /app/publish
|
|
|
|
FROM base AS final
|
|
WORKDIR /app
|
|
COPY --from=publish /app/publish .
|
|
|
|
# 智能複習系統特殊配置
|
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
|
ENV SMART_REVIEW_ENABLED=true
|
|
ENV CEFR_CACHE_SIZE=1000
|
|
|
|
ENTRYPOINT ["dotnet", "DramaLing.Api.dll"]
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 **測試架構設計**
|
|
|
|
### **測試策略規範**
|
|
```csharp
|
|
// 🧪 智能複習服務測試
|
|
[TestFixture]
|
|
public class SmartReviewServiceTests
|
|
{
|
|
private ITestQueueService _queueService;
|
|
private ISpacedRepetitionService _spacedRepService;
|
|
private Mock<DramaLingDbContext> _mockContext;
|
|
|
|
[Test]
|
|
public async Task BuildTodaysQueue_ShouldPrioritizeNewTests()
|
|
{
|
|
// Arrange
|
|
var userId = Guid.NewGuid();
|
|
var dueCards = CreateTestDueCards();
|
|
var completedTests = CreateTestCompletedTests();
|
|
|
|
// Act
|
|
var queue = await _queueService.BuildTodaysQueueAsync(userId);
|
|
|
|
// Assert
|
|
Assert.That(queue.Tests.First().Status, Is.EqualTo(TestStatus.New));
|
|
Assert.That(queue.Tests.Where(t => t.Status == TestStatus.New).Count(), Is.GreaterThan(0));
|
|
}
|
|
|
|
[Test]
|
|
public async Task CEFRAdaptation_A1User_ShouldLimitToBasicTests()
|
|
{
|
|
// Arrange
|
|
var userLevel = 20; // A1
|
|
var wordLevel = 50; // B1
|
|
|
|
// Act
|
|
var availableTests = _adaptationService.GetAvailableTestTypes(userLevel, wordLevel);
|
|
|
|
// Assert
|
|
Assert.That(availableTests, Is.EquivalentTo(new[] { "flip-memory", "vocab-choice", "vocab-listening" }));
|
|
}
|
|
}
|
|
```
|
|
|
|
### **前端組件測試**
|
|
```typescript
|
|
// 🧪 React 組件測試
|
|
describe('SmartReviewContainer', () => {
|
|
test('should load todays review on mount', async () => {
|
|
render(
|
|
<ReviewContextProvider>
|
|
<SmartReviewContainer />
|
|
</ReviewContextProvider>
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/今日複習/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
test('should show skip button before answering', () => {
|
|
const { getByText } = renderWithContext(<NavigationController />, {
|
|
isAnswered: false,
|
|
navigationState: 'skip'
|
|
})
|
|
|
|
expect(getByText('跳過這題')).toBeInTheDocument()
|
|
})
|
|
|
|
test('should show continue button after answering', () => {
|
|
const { getByText } = renderWithContext(<NavigationController />, {
|
|
isAnswered: true,
|
|
navigationState: 'continue'
|
|
})
|
|
|
|
expect(getByText('繼續下一題')).toBeInTheDocument()
|
|
})
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 **實施路線圖**
|
|
|
|
### **階段一:核心架構 (第1-2週)**
|
|
1. **後端服務層重構**
|
|
- 實現 TestQueueService
|
|
- 重構 ReviewController API
|
|
- 移除 Session 複雜性
|
|
|
|
2. **前端組件基礎**
|
|
- 建立 ReviewContext
|
|
- 實現核心 Hooks
|
|
- 基礎組件框架
|
|
|
|
### **階段二:智能適配 (第3-4週)**
|
|
1. **CEFR 適配系統**
|
|
- ReviewAdaptationService 實現
|
|
- 四情境邏輯完整實現
|
|
- A1 保護機制
|
|
|
|
2. **測驗類型組件**
|
|
- 7 種測驗組件實現
|
|
- 統一介面標準化
|
|
- 答案驗證邏輯
|
|
|
|
### **階段三:隊列管理 (第5-6週)**
|
|
1. **智能隊列系統**
|
|
- 優先級排序算法
|
|
- 跳過邏輯實現
|
|
- 狀態持久化
|
|
|
|
2. **導航控制系統**
|
|
- 狀態驅動導航
|
|
- 流暢的用戶體驗
|
|
- 錯誤恢復機制
|
|
|
|
### **階段四:優化與監控 (第7-8週)**
|
|
1. **效能優化**
|
|
- 快取策略實施
|
|
- 查詢優化
|
|
- 響應時間優化
|
|
|
|
2. **監控與測試**
|
|
- 監控指標實施
|
|
- 自動化測試
|
|
- 效能基準測試
|
|
|
|
---
|
|
|
|
## 📋 **開發檢查清單**
|
|
|
|
### **前端開發檢查項目**
|
|
- [ ] ReviewContext 實現完成
|
|
- [ ] 7 個測驗組件實現完成
|
|
- [ ] 導航控制邏輯實現
|
|
- [ ] 隊列管理邏輯實現
|
|
- [ ] 狀態持久化機制
|
|
- [ ] 錯誤處理和重試機制
|
|
- [ ] 響應式設計適配
|
|
- [ ] 無障礙設計支援
|
|
|
|
### **後端開發檢查項目**
|
|
- [ ] ReviewController 重構完成
|
|
- [ ] 智能複習服務實現
|
|
- [ ] CEFR 適配邏輯實現
|
|
- [ ] 隊列管理服務實現
|
|
- [ ] API 錯誤處理統一
|
|
- [ ] 認證授權機制
|
|
- [ ] 效能優化實施
|
|
- [ ] 監控指標收集
|
|
|
|
### **整合測試檢查項目**
|
|
- [ ] 前後端 API 契約測試
|
|
- [ ] 四情境適配邏輯測試
|
|
- [ ] 隊列管理端到端測試
|
|
- [ ] 狀態持久化測試
|
|
- [ ] 效能基準測試
|
|
- [ ] 用戶體驗流程測試
|
|
|
|
---
|
|
|
|
**文檔版本**: 1.0
|
|
**創建日期**: 2025-09-29
|
|
**技術負責**: 開發團隊
|
|
**審核狀態**: 待審核
|
|
**實施優先級**: P0 (最高優先級) |