25 KiB
25 KiB
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 開發團隊 更新頻率: 控制器變更時同步更新
📋 相關參考文檔
📋 需求與規格
- 詞卡管理功能需求規格 - 查看完整功能需求和用戶故事
🏗️ 技術架構