dramaling-vocab-learning/docs/03_development/api/flashcard-management-api.md

25 KiB
Raw Blame History

DramaLing 詞卡管理 API 規格書

1. API 概覽

1.1 基本資訊

  • 基礎 URL: http://localhost:5008/api (開發環境)
  • 控制器: FlashcardsController
  • 路由前綴: /api/flashcards
  • 認證方式: JWT Bearer Token (開發階段暫時關閉)
  • 資料格式: JSON (UTF-8)

1.2 架構依賴

📋 技術架構參考文檔

本 API 規格書依賴以下文檔,建議閱讀順序:

🏗️ 系統架構文檔

📋 需求規格文檔

2. 資料模型定義

2.1 詞卡實體 (Flashcard Entity)

C# 實體模型

public class Flashcard
{
    // 基本識別
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public Guid? CardSetId { get; set; }

    // 詞卡內容
    [Required, MaxLength(255)]
    public string Word { get; set; }
    [Required]
    public string Translation { get; set; }
    [Required]
    public string Definition { get; set; }
    [MaxLength(50)]
    public string? PartOfSpeech { get; set; }
    [MaxLength(255)]
    public string? Pronunciation { get; set; }
    public string? Example { get; set; }
    public string? ExampleTranslation { get; set; }

    // SM-2 學習算法參數
    public float EasinessFactor { get; set; } = 2.5f;
    public int Repetitions { get; set; } = 0;
    public int IntervalDays { get; set; } = 1;
    public DateTime NextReviewDate { get; set; }

    // 學習統計
    [Range(0, 100)]
    public int MasteryLevel { get; set; } = 0;
    public int TimesReviewed { get; set; } = 0;
    public int TimesCorrect { get; set; } = 0;
    public DateTime? LastReviewedAt { get; set; }

    // 狀態管理
    public bool IsFavorite { get; set; } = false;
    public bool IsArchived { get; set; } = false;
    [MaxLength(10)]
    public string? DifficultyLevel { get; set; } // A1-C2

    // 時間戳記
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
}

TypeScript 前端型別定義

interface Flashcard {
  id: string;
  word: string;
  translation: string;
  definition: string;
  partOfSpeech: string;
  pronunciation: string;
  example: string;
  exampleTranslation?: string;
  masteryLevel: number;        // 0-100
  timesReviewed: number;
  isFavorite: boolean;
  nextReviewDate: string;      // ISO Date
  difficultyLevel: string;     // A1, A2, B1, B2, C1, C2
  createdAt: string;          // ISO Date
  updatedAt?: string;         // ISO Date
}

interface CreateFlashcardRequest {
  word: string;
  translation: string;
  definition: string;
  pronunciation: string;
  partOfSpeech: string;
  example: string;
  exampleTranslation?: string;
}

2.2 API 回應格式標準

成功回應格式

{
  "success": true,
  "data": {
    // 實際資料內容
  },
  "message": "操作成功描述" // 可選
}

錯誤回應格式

{
  "success": false,
  "error": "錯誤描述",
  "isDuplicate": true, // 特殊情況:重複資料
  "existingCard": { /* 現有詞卡資料 */ } // 重複時的現有資料
}

3. API 端點規格

3.1 端點清單

方法 端點 描述 狀態
GET /api/flashcards 取得詞卡列表 已實現
GET /api/flashcards/{id} 取得單一詞卡 已實現
POST /api/flashcards 創建新詞卡 已實現
PUT /api/flashcards/{id} 更新詞卡 已實現
DELETE /api/flashcards/{id} 刪除詞卡 已實現
POST /api/flashcards/{id}/favorite 切換收藏狀態 已實現

3.2 詳細 API 規格

📖 GET /api/flashcards

功能: 取得用戶的詞卡列表,支援搜尋和篩選

查詢參數:

interface GetFlashcardsParams {
  search?: string;        // 搜尋關鍵字,搜尋範圍:詞彙、翻譯、定義
  favoritesOnly?: boolean; // 僅顯示收藏詞卡 (預設: false)
}

實際實現邏輯:

// 搜尋篩選邏輯
if (!string.IsNullOrEmpty(search))
{
    query = query.Where(f =>
        f.Word.Contains(search) ||
        f.Translation.Contains(search) ||
        (f.Definition != null && f.Definition.Contains(search)));
}

// 收藏篩選
if (favoritesOnly)
{
    query = query.Where(f => f.IsFavorite);
}

// 排序:按創建時間降序
var flashcards = await query.OrderByDescending(f => f.CreatedAt).ToListAsync();

成功回應:

{
  "success": true,
  "data": {
    "flashcards": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "word": "sophisticated",
        "translation": "精密的",
        "definition": "Highly developed or complex",
        "partOfSpeech": "adjective",
        "pronunciation": "/səˈfɪstɪkeɪtɪd/",
        "example": "A sophisticated system",
        "exampleTranslation": "一個精密的系統",
        "masteryLevel": 75,
        "timesReviewed": 12,
        "isFavorite": true,
        "nextReviewDate": "2025-09-25T00:00:00Z",
        "difficultyLevel": "C1",
        "createdAt": "2025-09-20T08:30:00Z",
        "updatedAt": "2025-09-24T10:15:00Z"
      }
    ],
    "count": 1
  }
}

請求範例:

# 取得所有詞卡
curl "http://localhost:5008/api/flashcards"

# 搜尋包含 "sophisticated" 的詞卡
curl "http://localhost:5008/api/flashcards?search=sophisticated"

# 僅取得收藏詞卡
curl "http://localhost:5008/api/flashcards?favoritesOnly=true"

# 組合搜尋:搜尋收藏詞卡中包含 "精密" 的詞卡
curl "http://localhost:5008/api/flashcards?search=精密&favoritesOnly=true"

📖 GET /api/flashcards/{id}

功能: 取得單一詞卡的完整資訊

路徑參數:

  • id: 詞卡唯一識別碼 (GUID 格式)

成功回應:

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "word": "sophisticated",
    // ... 完整詞卡資料,格式同列表 API
    "createdAt": "2025-09-20T08:30:00Z",
    "updatedAt": "2025-09-24T10:15:00Z"
  }
}

錯誤回應:

{
  "success": false,
  "error": "詞卡不存在"
}

✏️ POST /api/flashcards

功能: 創建新的詞卡

請求體:

{
  "word": "elaborate",
  "translation": "詳細說明",
  "definition": "To explain in detail",
  "pronunciation": "/ɪˈlæbərət/",
  "partOfSpeech": "verb",
  "example": "Please elaborate on your idea",
  "exampleTranslation": "請詳細說明你的想法"
}

實際實現邏輯:

// 1. 自動創建測試用戶 (開發階段)
var testUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
if (testUser == null) {
    // 自動創建測試用戶邏輯
}

// 2. 重複詞卡檢測
var existing = await _context.Flashcards
    .FirstOrDefaultAsync(f => f.UserId == userId &&
        f.Word.ToLower() == request.Word.ToLower() &&
        !f.IsArchived);

// 3. 創建新詞卡
var flashcard = new Flashcard
{
    Id = Guid.NewGuid(),
    UserId = userId,
    Word = request.Word,
    Translation = request.Translation,
    // ... 其他欄位
    CreatedAt = DateTime.UtcNow,
    UpdatedAt = DateTime.UtcNow
};

成功回應:

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "word": "elaborate",
    // ... 完整創建的詞卡資料
    "createdAt": "2025-09-24T10:30:00Z"
  },
  "message": "詞卡創建成功"
}

重複詞卡回應:

{
  "success": false,
  "error": "詞卡已存在",
  "isDuplicate": true,
  "existingCard": {
    "id": "existing-id",
    "word": "elaborate",
    // ... 現有詞卡資料
  }
}

✏️ PUT /api/flashcards/{id}

功能: 更新現有詞卡

路徑參數:

  • id: 詞卡唯一識別碼 (GUID)

請求體: 與 POST 相同格式

實際實現邏輯:

// 1. 查找現有詞卡
var flashcard = await _context.Flashcards
    .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId && !f.IsArchived);

if (flashcard == null)
{
    return NotFound(new { Success = false, Error = "詞卡不存在" });
}

// 2. 更新欄位
flashcard.Word = request.Word;
flashcard.Translation = request.Translation;
// ... 更新其他欄位
flashcard.UpdatedAt = DateTime.UtcNow;

// 3. 保存變更
await _context.SaveChangesAsync();

成功回應:

{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    // ... 更新後的完整詞卡資料
    "updatedAt": "2025-09-24T10:35:00Z"
  },
  "message": "詞卡更新成功"
}

🗑️ DELETE /api/flashcards/{id}

功能: 刪除詞卡 (軟刪除機制)

路徑參數:

  • id: 詞卡唯一識別碼 (GUID)

實際實現邏輯:

// 軟刪除:設定 IsArchived = true
var flashcard = await _context.Flashcards
    .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId && !f.IsArchived);

if (flashcard == null)
{
    return NotFound(new { Success = false, Error = "詞卡不存在" });
}

flashcard.IsArchived = true;
flashcard.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();

成功回應:

{
  "success": true,
  "message": "詞卡已刪除"
}

POST /api/flashcards/{id}/favorite

功能: 切換詞卡的收藏狀態

路徑參數:

  • id: 詞卡唯一識別碼 (GUID)

實際實現邏輯:

var flashcard = await _context.Flashcards
    .FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId && !f.IsArchived);

if (flashcard == null)
{
    return NotFound(new { Success = false, Error = "詞卡不存在" });
}

// 切換收藏狀態
flashcard.IsFavorite = !flashcard.IsFavorite;
flashcard.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();

成功回應:

{
  "success": true,
  "data": {
    "isFavorite": true
  },
  "message": "已加入收藏"
}

4. 前端整合規格

4.1 FlashcardsService 類別

TypeScript 服務實現

class FlashcardsService {
  private readonly baseURL = `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'}/api`;

  // 統一請求處理
  private async makeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      ...options,
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({ error: 'Network error' }));
      throw new Error(errorData.error || `HTTP ${response.status}`);
    }

    return response.json();
  }

  // API 方法實現
  async getFlashcards(search?: string, favoritesOnly: boolean = false): Promise<ApiResponse<{flashcards: Flashcard[], count: number}>> {
    const params = new URLSearchParams();
    if (search) params.append('search', search);
    if (favoritesOnly) params.append('favoritesOnly', 'true');

    const queryString = params.toString();
    const endpoint = `/flashcards${queryString ? `?${queryString}` : ''}`;

    return await this.makeRequest<ApiResponse<{flashcards: Flashcard[], count: number}>>(endpoint);
  }

  async createFlashcard(data: CreateFlashcardRequest): Promise<ApiResponse<Flashcard>> {
    return await this.makeRequest<ApiResponse<Flashcard>>('/flashcards', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async updateFlashcard(id: string, data: CreateFlashcardRequest): Promise<ApiResponse<Flashcard>> {
    return await this.makeRequest<ApiResponse<Flashcard>>(`/flashcards/${id}`, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  async deleteFlashcard(id: string): Promise<ApiResponse<void>> {
    return await this.makeRequest<ApiResponse<void>>(`/flashcards/${id}`, {
      method: 'DELETE',
    });
  }

  async toggleFavorite(id: string): Promise<ApiResponse<void>> {
    return await this.makeRequest<ApiResponse<void>>(`/flashcards/${id}/favorite`, {
      method: 'POST',
    });
  }
}

export const flashcardsService = new FlashcardsService();

4.2 前端使用範例

詞卡列表載入

const loadFlashcards = async () => {
  try {
    setLoading(true);
    const result = await flashcardsService.getFlashcards();
    if (result.success && result.data) {
      setFlashcards(result.data.flashcards);
    } else {
      setError(result.error || 'Failed to load flashcards');
    }
  } catch (err) {
    setError('Failed to load flashcards');
  } finally {
    setLoading(false);
  }
};

詞卡保存 (含重複檢測)

const handleSaveWord = async (word: string, analysis: any) => {
  try {
    const cardData = {
      word: word,
      translation: analysis.translation || '',
      definition: analysis.definition || '',
      pronunciation: analysis.pronunciation || `/${word}/`,
      partOfSpeech: analysis.partOfSpeech || 'unknown',
      example: `Example sentence with ${word}.`
    };

    const response = await flashcardsService.createFlashcard(cardData);

    if (response.success) {
      alert(`✅ 已成功將「${word}」保存到詞卡庫!`);
      return { success: true };
    } else if (response.error && response.error.includes('已存在')) {
      alert(`⚠️ 詞卡「${word}」已經存在於詞卡庫中`);
      return { success: false, error: 'duplicate' };
    } else {
      throw new Error(response.error || '保存失敗');
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : '保存失敗';
    alert(`❌ 保存詞卡失敗: ${errorMessage}`);
    return { success: false, error: errorMessage };
  }
};

5. 搜尋與篩選功能

5.1 後端搜尋實現

支援的搜尋欄位

// 目前實現的搜尋範圍
query = query.Where(f =>
    f.Word.Contains(search) ||           // 詞彙本身
    f.Translation.Contains(search) ||    // 中文翻譯
    (f.Definition != null && f.Definition.Contains(search)) // 英文定義
);

// 未來可擴展的搜尋範圍
// f.Example.Contains(search) ||         // 例句內容
// f.ExampleTranslation.Contains(search) // 例句翻譯

搜尋邏輯特性

  • 大小寫敏感: 目前使用 Contains() 進行大小寫敏感搜尋
  • 部分匹配: 支援關鍵字部分匹配
  • 邏輯運算: OR 邏輯 (任一欄位包含關鍵字即匹配)

5.2 前端搜尋與篩選

即時搜尋實現

// 前端即時篩選邏輯
const filteredCards = allCards.filter(card => {
  // 基本文字搜尋
  if (searchTerm) {
    const searchLower = searchTerm.toLowerCase();
    const matchesText =
      card.word?.toLowerCase().includes(searchLower) ||
      card.translation?.toLowerCase().includes(searchLower) ||
      card.definition?.toLowerCase().includes(searchLower);

    if (!matchesText) return false;
  }

  // CEFR 等級篩選
  if (searchFilters.cefrLevel && card.difficultyLevel !== searchFilters.cefrLevel) {
    return false;
  }

  // 詞性篩選
  if (searchFilters.partOfSpeech && card.partOfSpeech !== searchFilters.partOfSpeech) {
    return false;
  }

  // 掌握度篩選
  if (searchFilters.masteryLevel) {
    const mastery = card.masteryLevel || 0;
    if (searchFilters.masteryLevel === 'high' && mastery < 80) return false;
    if (searchFilters.masteryLevel === 'medium' && (mastery < 60 || mastery >= 80)) return false;
    if (searchFilters.masteryLevel === 'low' && mastery >= 60) return false;
  }

  // 收藏篩選
  if (searchFilters.onlyFavorites && !card.isFavorite) {
    return false;
  }

  return true;
});

進階篩選選項

interface SearchFilters {
  cefrLevel: string;        // A1, A2, B1, B2, C1, C2
  partOfSpeech: string;     // noun, verb, adjective, adverb, preposition, interjection
  masteryLevel: string;     // high (80%+), medium (60-79%), low (<60%)
  onlyFavorites: boolean;   // 僅收藏詞卡
}

6. 錯誤處理機制

6.1 後端錯誤處理

統一錯誤處理模式

try
{
    // API 邏輯
    return Ok(new { Success = true, Data = result });
}
catch (DbUpdateException ex)
{
    _logger.LogError(ex, "Database error during flashcard operation");
    return StatusCode(500, new { Success = false, Error = "資料庫操作失敗" });
}
catch (ArgumentException ex)
{
    _logger.LogWarning(ex, "Invalid argument for flashcard operation");
    return BadRequest(new { Success = false, Error = ex.Message });
}
catch (Exception ex)
{
    _logger.LogError(ex, "Unexpected error during flashcard operation");
    return StatusCode(500, new { Success = false, Error = "內部伺服器錯誤" });
}

特殊情況處理

// 重複詞卡檢測
if (existing != null)
{
    return Ok(new
    {
        Success = false,
        Error = "詞卡已存在",
        IsDuplicate = true,
        ExistingCard = new { /* 現有詞卡資料 */ }
    });
}

// 詞卡不存在
if (flashcard == null)
{
    return NotFound(new { Success = false, Error = "詞卡不存在" });
}

6.2 前端錯誤處理

API 服務層錯誤處理

private async makeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
  try {
    const response = await fetch(`${this.baseURL}${endpoint}`, options);

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({ error: 'Network error' }));
      throw new Error(errorData.error || errorData.details || `HTTP ${response.status}`);
    }

    return response.json();
  } catch (error) {
    console.error('API request failed:', error);
    throw error;
  }
}

用戶反饋機制

// 成功操作反饋
if (response.success) {
  alert(`✅ 已成功將「${word}」保存到詞卡庫!`);
  return { success: true };
}

// 重複詞卡反饋
else if (response.error && response.error.includes('已存在')) {
  alert(`⚠️ 詞卡「${word}」已經存在於詞卡庫中`);
  return { success: false, error: 'duplicate' };
}

// 一般錯誤反饋
else {
  alert(`❌ 保存詞卡失敗: ${response.error}`);
  return { success: false, error: response.error };
}

7. 認證與授權

7.1 開發階段認證

目前實現 (測試模式)

[AllowAnonymous] // 暫時移除認證要求
public class FlashcardsController : ControllerBase
{
    private Guid GetUserId()
    {
        // 使用固定測試用戶 ID
        return Guid.Parse("00000000-0000-0000-0000-000000000001");
    }
}

自動測試用戶創建

// 確保測試用戶存在
var testUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
if (testUser == null)
{
    testUser = new User
    {
        Id = userId,
        Username = "testuser",
        Email = "test@example.com",
        DisplayName = "測試用戶",
        SubscriptionType = "free",
        EnglishLevel = "A2",
        CreatedAt = DateTime.UtcNow
    };
    _context.Users.Add(testUser);
    await _context.SaveChangesAsync();
}

7.2 生產環境認證 (未來啟用)

JWT Token 解析

[Authorize] // 生產環境啟用
public class FlashcardsController : ControllerBase
{
    private Guid GetUserId()
    {
        var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ??
                          User.FindFirst("sub")?.Value;

        if (Guid.TryParse(userIdString, out var userId))
            return userId;

        throw new UnauthorizedAccessException("Invalid user ID in token");
    }
}

8. 效能優化

8.1 資料庫查詢優化

索引建議

-- 用戶詞卡查詢索引
CREATE INDEX IX_Flashcards_UserId_IsArchived ON Flashcards(UserId, IsArchived);

-- 搜尋優化索引
CREATE INDEX IX_Flashcards_Word ON Flashcards(Word);
CREATE INDEX IX_Flashcards_Translation ON Flashcards(Translation);

-- 收藏篩選索引
CREATE INDEX IX_Flashcards_IsFavorite ON Flashcards(IsFavorite);

-- 複合查詢索引
CREATE INDEX IX_Flashcards_UserId_IsFavorite_IsArchived ON Flashcards(UserId, IsFavorite, IsArchived);

查詢優化技巧

// 使用 AsNoTracking 提升查詢效能 (只讀查詢)
var flashcards = await query
    .AsNoTracking()
    .OrderByDescending(f => f.CreatedAt)
    .ToListAsync();

// 選擇性載入欄位 (避免載入不必要的關聯資料)
.Select(f => new {
    f.Id, f.Word, f.Translation, f.Definition,
    // 僅選擇需要的欄位
})

8.2 快取策略 (未來實現)

記憶體快取

// 用戶詞卡列表快取 (30分鐘)
var cacheKey = $"flashcards:user:{userId}";
var cachedCards = await _cacheService.GetAsync<List<Flashcard>>(cacheKey);

if (cachedCards == null)
{
    cachedCards = await LoadFlashcardsFromDatabase(userId);
    await _cacheService.SetAsync(cacheKey, cachedCards, TimeSpan.FromMinutes(30));
}

搜尋結果快取

// 搜尋結果快取 (10分鐘)
var searchCacheKey = $"search:{userId}:{searchTerm}:{favoritesOnly}";
var cachedResults = await _cacheService.GetAsync<SearchResult>(searchCacheKey);

9. 測試規格

9.1 API 測試用例

功能測試

# 測試詞卡創建
curl -X POST http://localhost:5008/api/flashcards \
  -H "Content-Type: application/json" \
  -d '{
    "word": "test",
    "translation": "測試",
    "definition": "A trial or examination",
    "pronunciation": "/test/",
    "partOfSpeech": "noun",
    "example": "This is a test sentence"
  }'

# 測試搜尋功能
curl "http://localhost:5008/api/flashcards?search=test"

# 測試收藏功能
curl -X POST http://localhost:5008/api/flashcards/{id}/favorite

# 測試詞卡更新
curl -X PUT http://localhost:5008/api/flashcards/{id} \
  -H "Content-Type: application/json" \
  -d '{ /* 更新的詞卡資料 */ }'

# 測試詞卡刪除
curl -X DELETE http://localhost:5008/api/flashcards/{id}

邊界條件測試

# 測試重複詞卡創建
curl -X POST http://localhost:5008/api/flashcards \
  -d '{"word": "existing-word", ...}'
# 預期回應: success: false, isDuplicate: true

# 測試不存在的詞卡操作
curl http://localhost:5008/api/flashcards/non-existent-id
# 預期回應: 404 Not Found

# 測試空搜尋
curl "http://localhost:5008/api/flashcards?search="
# 預期回應: 返回所有詞卡

9.2 效能測試

載入測試

# 測試大量詞卡載入 (1000+ 詞卡)
time curl "http://localhost:5008/api/flashcards"
# 預期: < 2秒

# 測試搜尋效能
time curl "http://localhost:5008/api/flashcards?search=sophisticated"
# 預期: < 300ms

10. 部署與監控

10.1 健康檢查

API 健康檢查端點

// Program.cs 中配置
services.AddHealthChecks()
    .AddDbContextCheck<DramaLingDbContext>();

app.MapHealthChecks("/health");

健康檢查請求:

curl http://localhost:5008/health

10.2 日誌監控

結構化日誌

_logger.LogInformation("Creating flashcard for user {UserId}, word: {Word}",
    userId, request.Word);

_logger.LogError(ex, "Failed to create flashcard for user {UserId}", userId);

_logger.LogWarning("Duplicate flashcard creation attempt: {Word} for user {UserId}",
    request.Word, userId);

關鍵指標監控

  • API 響應時間: 平均 < 200ms
  • 成功率: > 99.5%
  • 重複詞卡檢測: 準確率 100%
  • 資料庫連接: 健康狀態監控

文檔版本: v1.0 建立日期: 2025-09-24 基於: FlashcardsController.cs v1.0 維護負責: API 開發團隊 更新頻率: 控制器變更時同步更新

📋 相關參考文檔

📋 需求與規格

🏗️ 技術架構