diff --git a/選項詞彙庫功能規格書.md b/選項詞彙庫功能規格書.md
index c896ba8..ec81ff7 100644
--- a/選項詞彙庫功能規格書.md
+++ b/選項詞彙庫功能規格書.md
@@ -104,10 +104,12 @@ public class OptionsVocabulary
public string CEFRLevel { get; set; } = string.Empty;
///
- /// 詞性 (noun, verb, adjective, adverb, etc.)
+ /// 詞性 (noun, verb, adjective, adverb, pronoun, preposition, conjunction, interjection, idiom)
///
[Required]
[MaxLength(20)]
+ [RegularExpression("^(noun|verb|adjective|adverb|pronoun|preposition|conjunction|interjection|idiom)$",
+ ErrorMessage = "詞性必須為有效值")]
[Index("IX_OptionsVocabulary_PartOfSpeech")]
public string PartOfSpeech { get; set; } = string.Empty;
@@ -192,62 +194,79 @@ public interface IOptionsVocabularyService
}
```
-### DistractorGenerationService 核心邏輯
+### QuestionGeneratorService 整合設計
```csharp
-public class DistractorGenerationService
+public class QuestionGeneratorService : IQuestionGeneratorService
{
private readonly DramaLingDbContext _context;
- private readonly IMemoryCache _cache;
- private readonly ILogger _logger;
+ private readonly IOptionsVocabularyService _optionsVocabularyService;
+ private readonly ILogger _logger;
- public async Task> GenerateDistractorsAsync(
- string targetWord,
- string cefrLevel,
- string partOfSpeech)
+ public QuestionGeneratorService(
+ DramaLingDbContext context,
+ IOptionsVocabularyService optionsVocabularyService,
+ ILogger logger)
{
- var targetLength = targetWord.Length;
-
- // 1. 基礎篩選條件
- var baseQuery = _context.OptionsVocabularies
- .Where(v => v.IsActive && v.Word != targetWord);
-
- // 2. CEFR 等級匹配(相同等級 + 相鄰等級)
- var allowedCEFRLevels = GetAllowedCEFRLevels(cefrLevel);
- baseQuery = baseQuery.Where(v => allowedCEFRLevels.Contains(v.CEFRLevel));
-
- // 3. 詞性匹配
- baseQuery = baseQuery.Where(v => v.PartOfSpeech == partOfSpeech);
-
- // 4. 字數匹配(±2 字元範圍)
- var minLength = Math.Max(1, targetLength - 2);
- var maxLength = targetLength + 2;
- baseQuery = baseQuery.Where(v => v.WordLength >= minLength && v.WordLength <= maxLength);
-
- // 5. 隨機排序選取候選詞
- var candidates = await baseQuery
- .OrderBy(v => Guid.NewGuid())
- .Take(10) // 取更多候選詞再篩選
- .Select(v => v.Word)
- .ToListAsync();
-
- // 7. 最終篩選和回傳
- return candidates.Take(3).ToList();
+ _context = context;
+ _optionsVocabularyService = optionsVocabularyService;
+ _logger = logger;
}
- private List GetAllowedCEFRLevels(string targetLevel)
+ ///
+ /// 生成詞彙選擇題選項(整合選項詞彙庫)
+ ///
+ private async Task GenerateVocabChoiceAsync(Flashcard flashcard)
{
- var levels = new[] { "A1", "A2", "B1", "B2", "C1", "C2" };
- var targetIndex = Array.IndexOf(levels, targetLevel);
+ try
+ {
+ // 優先使用選項詞彙庫生成干擾項
+ var distractors = await _optionsVocabularyService.GenerateDistractorsAsync(
+ flashcard.Word,
+ flashcard.DifficultyLevel ?? "B1",
+ flashcard.PartOfSpeech ?? "noun");
- if (targetIndex == -1) return new List { targetLevel };
+ // 如果詞彙庫沒有足夠的選項,回退到用戶其他詞卡
+ if (distractors.Count < 3)
+ {
+ var fallbackDistractors = await GetFallbackDistractorsAsync(flashcard);
+ distractors.AddRange(fallbackDistractors.Take(3 - distractors.Count));
+ }
- var allowed = new List { targetLevel };
+ var options = new List { flashcard.Word };
+ options.AddRange(distractors.Take(3));
- // 加入相鄰等級
- if (targetIndex > 0) allowed.Add(levels[targetIndex - 1]);
- if (targetIndex < levels.Length - 1) allowed.Add(levels[targetIndex + 1]);
+ // 隨機打亂選項順序
+ var shuffledOptions = options.OrderBy(x => Guid.NewGuid()).ToArray();
- return allowed;
+ return new QuestionData
+ {
+ QuestionType = "vocab-choice",
+ Options = shuffledOptions,
+ CorrectAnswer = flashcard.Word
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to generate options from vocabulary database, using fallback for {Word}", flashcard.Word);
+
+ // 完全回退到原有邏輯
+ return await GenerateVocabChoiceWithFallbackAsync(flashcard);
+ }
+ }
+
+ ///
+ /// 回退選項生成(使用用戶其他詞卡)
+ ///
+ private async Task> GetFallbackDistractorsAsync(Flashcard flashcard)
+ {
+ return await _context.Flashcards
+ .Where(f => f.UserId == flashcard.UserId &&
+ f.Id != flashcard.Id &&
+ !f.IsArchived)
+ .OrderBy(x => Guid.NewGuid())
+ .Take(3)
+ .Select(f => f.Word)
+ .ToListAsync();
}
}
```
@@ -256,82 +275,49 @@ public class DistractorGenerationService
## 🌐 API 設計
-### 新增到 StudyController
+### 整合到現有 FlashcardsController
+選項詞彙庫功能將整合到現有的 `POST /api/flashcards/{id}/question` API 端點中。
+
```csharp
-///
-/// 生成測驗選項(使用詞彙庫)
-///
-[HttpGet("question-options/{flashcardId}")]
-public async Task> GenerateQuestionOptions(
- Guid flashcardId,
- [FromQuery] string questionType = "vocab-choice")
+// 現有的 FlashcardsController.GenerateQuestion 方法會自動使用改進後的 QuestionGeneratorService
+// 不需要新增額外的 API 端點
+
+[HttpPost("{id}/question")]
+public async Task GenerateQuestion(Guid id, [FromBody] QuestionRequest request)
{
try
{
- var flashcard = await _context.Flashcards.FindAsync(flashcardId);
- if (flashcard == null)
- return NotFound(new { Error = "Flashcard not found" });
+ // QuestionGeneratorService 內部會使用 OptionsVocabularyService 生成更好的選項
+ var questionData = await _questionGeneratorService.GenerateQuestionAsync(id, request.QuestionType);
- var options = await _distractorGenerationService.GenerateDistractorsAsync(
- flashcard.Word,
- flashcard.DifficultyLevel ?? "B1",
- flashcard.PartOfSpeech ?? "noun");
-
- // 加入正確答案並隨機打亂
- var allOptions = new List { flashcard.Word };
- allOptions.AddRange(options);
- var shuffledOptions = allOptions.OrderBy(x => Guid.NewGuid()).ToArray();
-
- return Ok(new QuestionOptionsResponse
- {
- QuestionType = questionType,
- Options = shuffledOptions,
- CorrectAnswer = flashcard.Word,
- TargetWord = flashcard.Word,
- CEFRLevel = flashcard.DifficultyLevel,
- PartOfSpeech = flashcard.PartOfSpeech
- });
+ return Ok(new { success = true, data = questionData });
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error generating question options for flashcard {FlashcardId}", flashcardId);
- return StatusCode(500, new { Error = "Internal server error" });
+ _logger.LogError(ex, "Error generating question for flashcard {FlashcardId}", id);
+ return StatusCode(500, new { success = false, error = "Failed to generate question" });
}
}
```
-### 詞彙庫管理 API
+### 詞彙庫管理 API(選用功能)
+> **注意**:以下管理 API 為選用功能,主要供管理員批量管理詞彙庫使用。
+> 核心選項生成功能已整合到現有的測驗 API 中,不依賴這些管理端點。
+
```csharp
///
-/// 詞彙庫管理控制器
+/// 詞彙庫管理控制器(選用)
+/// 僅在需要管理員批量管理詞彙庫時實作
///
[ApiController]
-[Route("api/[controller]")]
+[Route("api/admin/[controller]")]
[Authorize(Roles = "Admin")]
public class OptionsVocabularyController : ControllerBase
{
private readonly IOptionsVocabularyService _vocabularyService;
///
- /// 新增詞彙到選項庫
- ///
- [HttpPost]
- public async Task AddVocabulary([FromBody] AddVocabularyRequest request)
- {
- var vocabulary = new OptionsVocabulary
- {
- Word = request.Word,
- CEFRLevel = request.CEFRLevel,
- PartOfSpeech = request.PartOfSpeech,
- WordLength = request.Word.Length
- };
-
- var success = await _vocabularyService.AddVocabularyAsync(vocabulary);
- return success ? Ok() : BadRequest();
- }
-
- ///
- /// 批量匯入詞彙
+ /// 批量匯入詞彙(管理員功能)
///
[HttpPost("bulk-import")]
public async Task BulkImport([FromBody] List requests)
@@ -349,19 +335,13 @@ public class OptionsVocabularyController : ControllerBase
}
///
- /// 搜尋詞彙庫
+ /// 搜尋詞彙庫統計(管理員功能)
///
- [HttpGet("search")]
- public async Task>> SearchVocabularies(
- [FromQuery] string? cefrLevel = null,
- [FromQuery] string? partOfSpeech = null,
- [FromQuery] int? minLength = null,
- [FromQuery] int? maxLength = null,
- [FromQuery] int limit = 100)
+ [HttpGet("stats")]
+ public async Task GetVocabularyStats()
{
- var vocabularies = await _vocabularyService.SearchVocabulariesAsync(
- cefrLevel, partOfSpeech, minLength, maxLength, limit);
- return Ok(vocabularies);
+ var stats = await _vocabularyService.GetVocabularyStatsAsync();
+ return Ok(stats);
}
}
```
@@ -400,6 +380,8 @@ public class AddVocabularyRequest
[Required]
[MaxLength(20)]
+ [RegularExpression("^(noun|verb|adjective|adverb|pronoun|preposition|conjunction|interjection|idiom)$",
+ ErrorMessage = "詞性必須為有效值")]
public string PartOfSpeech { get; set; } = string.Empty;
}
@@ -462,38 +444,70 @@ public partial class AddOptionsVocabularyTable : Migration
## 🔄 使用案例
-### 案例 1:詞彙選擇題
+### 案例 1:詞彙選擇題 API 流程
```
-目標詞彙: "beautiful" (B1, adjective, 9字元)
+前端請求:
+POST /api/flashcards/{id}/question
+{
+ "questionType": "vocab-choice"
+}
-篩選條件:
-- CEFR: A2, B1, B2 (相鄰等級)
-- 詞性: adjective
-- 字數: 7-11 字元
+後端處理:
+1. 查詢詞卡: "beautiful" (B1, adjective, 9字元)
+2. 從選項詞彙庫篩選干擾項:
+ - CEFR: A2, B1, B2 (相鄰等級)
+ - 詞性: adjective
+ - 字數: 7-11 字元
+3. 選出干擾項: ["wonderful", "excellent", "attractive"]
-可能的干擾項:
-- "wonderful" (B1, adjective, 9字元)
-- "excellent" (B2, adjective, 9字元)
-- "attractive" (B2, adjective, 10字元)
-
-最終選項: ["beautiful", "wonderful", "excellent", "attractive"]
+API 回應:
+{
+ "success": true,
+ "data": {
+ "questionType": "vocab-choice",
+ "options": ["beautiful", "wonderful", "excellent", "attractive"],
+ "correctAnswer": "beautiful"
+ }
+}
```
-### 案例 2:聽力測驗
+### 案例 2:聽力測驗 API 流程
```
-目標詞彙: "running" (A2, verb, 7字元)
+前端請求:
+POST /api/flashcards/{id}/question
+{
+ "questionType": "sentence-listening"
+}
-篩選條件:
-- CEFR: A1, A2, B1
-- 詞性: verb
-- 字數: 5-9 字元
+後端處理:
+1. 查詢詞卡: "running" (A2, verb, 7字元)
+2. 從選項詞彙庫篩選干擾項:
+ - CEFR: A1, A2, B1
+ - 詞性: verb
+ - 字數: 5-9 字元
+3. 選出干擾項: ["jumping", "walking", "playing"]
-可能的干擾項:
-- "jumping" (A2, verb, 7字元)
-- "walking" (A1, verb, 7字元)
-- "playing" (A2, verb, 7字元)
+API 回應:
+{
+ "success": true,
+ "data": {
+ "questionType": "sentence-listening",
+ "options": ["running", "jumping", "walking", "playing"],
+ "correctAnswer": "running"
+ }
+}
+```
-最終選項: ["running", "jumping", "walking", "playing"]
+### 案例 3:回退機制
+```
+情境: 詞彙庫中沒有足夠的相符選項
+
+處理流程:
+1. 嘗試從選項詞彙庫獲取干擾項 → 只找到 1 個
+2. 啟動回退機制:從用戶其他詞卡補足 2 個選項
+3. 確保總是能提供 3 個干擾項
+
+優點:確保系統穩定性,即使詞彙庫不完整也能正常運作
```
---
@@ -575,11 +589,41 @@ public class VocabularySeeder
new() { Word = "run", CEFRLevel = "A1", PartOfSpeech = "verb", WordLength = 3 },
new() { Word = "walk", CEFRLevel = "A1", PartOfSpeech = "verb", WordLength = 4 },
+ // A1 Level - 代名詞
+ new() { Word = "he", CEFRLevel = "A1", PartOfSpeech = "pronoun", WordLength = 2 },
+ new() { Word = "she", CEFRLevel = "A1", PartOfSpeech = "pronoun", WordLength = 3 },
+ new() { Word = "they", CEFRLevel = "A1", PartOfSpeech = "pronoun", WordLength = 4 },
+
+ // A2 Level - 介系詞
+ new() { Word = "under", CEFRLevel = "A2", PartOfSpeech = "preposition", WordLength = 5 },
+ new() { Word = "above", CEFRLevel = "A2", PartOfSpeech = "preposition", WordLength = 5 },
+ new() { Word = "behind", CEFRLevel = "A2", PartOfSpeech = "preposition", WordLength = 6 },
+
// B1 Level - 形容詞
new() { Word = "beautiful", CEFRLevel = "B1", PartOfSpeech = "adjective", WordLength = 9 },
new() { Word = "wonderful", CEFRLevel = "B1", PartOfSpeech = "adjective", WordLength = 9 },
new() { Word = "excellent", CEFRLevel = "B2", PartOfSpeech = "adjective", WordLength = 9 },
+ // B1 Level - 副詞
+ new() { Word = "quickly", CEFRLevel = "B1", PartOfSpeech = "adverb", WordLength = 7 },
+ new() { Word = "carefully", CEFRLevel = "B1", PartOfSpeech = "adverb", WordLength = 9 },
+ new() { Word = "suddenly", CEFRLevel = "B1", PartOfSpeech = "adverb", WordLength = 8 },
+
+ // B2 Level - 連接詞
+ new() { Word = "however", CEFRLevel = "B2", PartOfSpeech = "conjunction", WordLength = 7 },
+ new() { Word = "therefore", CEFRLevel = "B2", PartOfSpeech = "conjunction", WordLength = 9 },
+ new() { Word = "although", CEFRLevel = "B2", PartOfSpeech = "conjunction", WordLength = 8 },
+
+ // 感嘆詞
+ new() { Word = "wow", CEFRLevel = "A1", PartOfSpeech = "interjection", WordLength = 3 },
+ new() { Word = "ouch", CEFRLevel = "A2", PartOfSpeech = "interjection", WordLength = 4 },
+ new() { Word = "alas", CEFRLevel = "C1", PartOfSpeech = "interjection", WordLength = 4 },
+
+ // 慣用語
+ new() { Word = "break the ice", CEFRLevel = "B2", PartOfSpeech = "idiom", WordLength = 12 },
+ new() { Word = "piece of cake", CEFRLevel = "B1", PartOfSpeech = "idiom", WordLength = 12 },
+ new() { Word = "hit the books", CEFRLevel = "B2", PartOfSpeech = "idiom", WordLength = 12 },
+
// ... 更多詞彙
};
@@ -644,12 +688,14 @@ public class DistractorQualityMetrics
- [ ] 實作品質評分系統
- [ ] 加入快取機制
-### Phase 3: 前端整合 (3-5 天)
-- [ ] 修改前端 generateOptions 函數
-- [ ] 整合新的 API 端點
-- [ ] 測試各種測驗類型
+### Phase 3: 前端整合 (1-2 天)
+- [ ] 測試現有 API 端點的改進效果
+- [ ] 驗證各種測驗類型的選項品質
- [ ] 效能測試和優化
+> **注意**:由於選項生成功能已整合到現有 API,前端不需要修改任何程式碼。
+> 只需要確保後端改進後的選項生成效果符合預期。
+
### Phase 4: 進階功能 (1-2 週)
- [ ] 管理介面開發
- [ ] 批量匯入工具
@@ -666,7 +712,7 @@ public class DistractorQualityMetrics
- [ ] 生成的選項無重複
- [ ] 支援各種測驗類型
-### 品質驗收
+### 品質驗收
- [ ] 干擾項難度適中(不會太簡單或太困難)
- [ ] 無明顯的同義詞作為干擾項
- [ ] 拼寫差異合理(避免過於相似)