945 lines
25 KiB
Markdown
945 lines
25 KiB
Markdown
# DramaLing 詞卡管理 API 規格書
|
||
|
||
## 1. API 概覽
|
||
|
||
### 1.1 基本資訊
|
||
- **基礎 URL**: `http://localhost:5008/api` (開發環境)
|
||
- **控制器**: `FlashcardsController`
|
||
- **路由前綴**: `/api/flashcards`
|
||
- **認證方式**: JWT Bearer Token (開發階段暫時關閉)
|
||
- **資料格式**: JSON (UTF-8)
|
||
|
||
### 1.2 架構依賴
|
||
|
||
> 📋 **技術架構參考文檔**
|
||
>
|
||
> 本 API 規格書依賴以下文檔,建議閱讀順序:
|
||
>
|
||
> **🏗️ 系統架構文檔**
|
||
> - [系統架構總覽](../../04_technical/system-architecture.md) - 了解整體架構設計
|
||
> - [後端架構詳細說明](../../04_technical/backend-architecture.md) - 了解 ASP.NET Core 架構細節
|
||
>
|
||
> **📋 需求規格文檔**
|
||
> - [詞卡管理功能產品需求規格](../../01_requirement/詞卡管理功能產品需求規格.md) - 了解功能需求和用戶故事
|
||
|
||
## 2. 資料模型定義
|
||
|
||
### 2.1 詞卡實體 (Flashcard Entity)
|
||
|
||
#### C# 實體模型
|
||
```csharp
|
||
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 前端型別定義
|
||
```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 回應格式標準
|
||
|
||
#### 成功回應格式
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
// 實際資料內容
|
||
},
|
||
"message": "操作成功描述" // 可選
|
||
}
|
||
```
|
||
|
||
#### 錯誤回應格式
|
||
```json
|
||
{
|
||
"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
|
||
**功能**: 取得用戶的詞卡列表,支援搜尋和篩選
|
||
|
||
**查詢參數**:
|
||
```typescript
|
||
interface GetFlashcardsParams {
|
||
search?: string; // 搜尋關鍵字,搜尋範圍:詞彙、翻譯、定義
|
||
favoritesOnly?: boolean; // 僅顯示收藏詞卡 (預設: false)
|
||
}
|
||
```
|
||
|
||
**實際實現邏輯**:
|
||
```csharp
|
||
// 搜尋篩選邏輯
|
||
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();
|
||
```
|
||
|
||
**成功回應**:
|
||
```json
|
||
{
|
||
"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
|
||
}
|
||
}
|
||
```
|
||
|
||
**請求範例**:
|
||
```bash
|
||
# 取得所有詞卡
|
||
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 格式)
|
||
|
||
**成功回應**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"word": "sophisticated",
|
||
// ... 完整詞卡資料,格式同列表 API
|
||
"createdAt": "2025-09-20T08:30:00Z",
|
||
"updatedAt": "2025-09-24T10:15:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**錯誤回應**:
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": "詞卡不存在"
|
||
}
|
||
```
|
||
|
||
#### ✏️ POST /api/flashcards
|
||
**功能**: 創建新的詞卡
|
||
|
||
**請求體**:
|
||
```json
|
||
{
|
||
"word": "elaborate",
|
||
"translation": "詳細說明",
|
||
"definition": "To explain in detail",
|
||
"pronunciation": "/ɪˈlæbərət/",
|
||
"partOfSpeech": "verb",
|
||
"example": "Please elaborate on your idea",
|
||
"exampleTranslation": "請詳細說明你的想法"
|
||
}
|
||
```
|
||
|
||
**實際實現邏輯**:
|
||
```csharp
|
||
// 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
|
||
};
|
||
```
|
||
|
||
**成功回應**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"word": "elaborate",
|
||
// ... 完整創建的詞卡資料
|
||
"createdAt": "2025-09-24T10:30:00Z"
|
||
},
|
||
"message": "詞卡創建成功"
|
||
}
|
||
```
|
||
|
||
**重複詞卡回應**:
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": "詞卡已存在",
|
||
"isDuplicate": true,
|
||
"existingCard": {
|
||
"id": "existing-id",
|
||
"word": "elaborate",
|
||
// ... 現有詞卡資料
|
||
}
|
||
}
|
||
```
|
||
|
||
#### ✏️ PUT /api/flashcards/{id}
|
||
**功能**: 更新現有詞卡
|
||
|
||
**路徑參數**:
|
||
- `id`: 詞卡唯一識別碼 (GUID)
|
||
|
||
**請求體**: 與 POST 相同格式
|
||
|
||
**實際實現邏輯**:
|
||
```csharp
|
||
// 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();
|
||
```
|
||
|
||
**成功回應**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||
// ... 更新後的完整詞卡資料
|
||
"updatedAt": "2025-09-24T10:35:00Z"
|
||
},
|
||
"message": "詞卡更新成功"
|
||
}
|
||
```
|
||
|
||
#### 🗑️ DELETE /api/flashcards/{id}
|
||
**功能**: 刪除詞卡 (軟刪除機制)
|
||
|
||
**路徑參數**:
|
||
- `id`: 詞卡唯一識別碼 (GUID)
|
||
|
||
**實際實現邏輯**:
|
||
```csharp
|
||
// 軟刪除:設定 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();
|
||
```
|
||
|
||
**成功回應**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "詞卡已刪除"
|
||
}
|
||
```
|
||
|
||
#### ⭐ POST /api/flashcards/{id}/favorite
|
||
**功能**: 切換詞卡的收藏狀態
|
||
|
||
**路徑參數**:
|
||
- `id`: 詞卡唯一識別碼 (GUID)
|
||
|
||
**實際實現邏輯**:
|
||
```csharp
|
||
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();
|
||
```
|
||
|
||
**成功回應**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"isFavorite": true
|
||
},
|
||
"message": "已加入收藏"
|
||
}
|
||
```
|
||
|
||
## 4. 前端整合規格
|
||
|
||
### 4.1 FlashcardsService 類別
|
||
|
||
#### TypeScript 服務實現
|
||
```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 前端使用範例
|
||
|
||
#### 詞卡列表載入
|
||
```typescript
|
||
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);
|
||
}
|
||
};
|
||
```
|
||
|
||
#### 詞卡保存 (含重複檢測)
|
||
```typescript
|
||
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 後端搜尋實現
|
||
|
||
#### 支援的搜尋欄位
|
||
```csharp
|
||
// 目前實現的搜尋範圍
|
||
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 前端搜尋與篩選
|
||
|
||
#### 即時搜尋實現
|
||
```typescript
|
||
// 前端即時篩選邏輯
|
||
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;
|
||
});
|
||
```
|
||
|
||
#### 進階篩選選項
|
||
```typescript
|
||
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 後端錯誤處理
|
||
|
||
#### 統一錯誤處理模式
|
||
```csharp
|
||
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 = "內部伺服器錯誤" });
|
||
}
|
||
```
|
||
|
||
#### 特殊情況處理
|
||
```csharp
|
||
// 重複詞卡檢測
|
||
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 服務層錯誤處理
|
||
```typescript
|
||
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;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 用戶反饋機制
|
||
```typescript
|
||
// 成功操作反饋
|
||
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 開發階段認證
|
||
|
||
#### 目前實現 (測試模式)
|
||
```csharp
|
||
[AllowAnonymous] // 暫時移除認證要求
|
||
public class FlashcardsController : ControllerBase
|
||
{
|
||
private Guid GetUserId()
|
||
{
|
||
// 使用固定測試用戶 ID
|
||
return Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 自動測試用戶創建
|
||
```csharp
|
||
// 確保測試用戶存在
|
||
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 解析
|
||
```csharp
|
||
[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 資料庫查詢優化
|
||
|
||
#### 索引建議
|
||
```sql
|
||
-- 用戶詞卡查詢索引
|
||
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);
|
||
```
|
||
|
||
#### 查詢優化技巧
|
||
```csharp
|
||
// 使用 AsNoTracking 提升查詢效能 (只讀查詢)
|
||
var flashcards = await query
|
||
.AsNoTracking()
|
||
.OrderByDescending(f => f.CreatedAt)
|
||
.ToListAsync();
|
||
|
||
// 選擇性載入欄位 (避免載入不必要的關聯資料)
|
||
.Select(f => new {
|
||
f.Id, f.Word, f.Translation, f.Definition,
|
||
// 僅選擇需要的欄位
|
||
})
|
||
```
|
||
|
||
### 8.2 快取策略 (未來實現)
|
||
|
||
#### 記憶體快取
|
||
```csharp
|
||
// 用戶詞卡列表快取 (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));
|
||
}
|
||
```
|
||
|
||
#### 搜尋結果快取
|
||
```csharp
|
||
// 搜尋結果快取 (10分鐘)
|
||
var searchCacheKey = $"search:{userId}:{searchTerm}:{favoritesOnly}";
|
||
var cachedResults = await _cacheService.GetAsync<SearchResult>(searchCacheKey);
|
||
```
|
||
|
||
## 9. 測試規格
|
||
|
||
### 9.1 API 測試用例
|
||
|
||
#### 功能測試
|
||
```bash
|
||
# 測試詞卡創建
|
||
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}
|
||
```
|
||
|
||
#### 邊界條件測試
|
||
```bash
|
||
# 測試重複詞卡創建
|
||
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 效能測試
|
||
|
||
#### 載入測試
|
||
```bash
|
||
# 測試大量詞卡載入 (1000+ 詞卡)
|
||
time curl "http://localhost:5008/api/flashcards"
|
||
# 預期: < 2秒
|
||
|
||
# 測試搜尋效能
|
||
time curl "http://localhost:5008/api/flashcards?search=sophisticated"
|
||
# 預期: < 300ms
|
||
```
|
||
|
||
## 10. 部署與監控
|
||
|
||
### 10.1 健康檢查
|
||
|
||
#### API 健康檢查端點
|
||
```csharp
|
||
// Program.cs 中配置
|
||
services.AddHealthChecks()
|
||
.AddDbContextCheck<DramaLingDbContext>();
|
||
|
||
app.MapHealthChecks("/health");
|
||
```
|
||
|
||
**健康檢查請求**:
|
||
```bash
|
||
curl http://localhost:5008/health
|
||
```
|
||
|
||
### 10.2 日誌監控
|
||
|
||
#### 結構化日誌
|
||
```csharp
|
||
_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 開發團隊
|
||
**更新頻率**: 控制器變更時同步更新
|
||
|
||
> 📋 **相關參考文檔**
|
||
>
|
||
> **📋 需求與規格**
|
||
> - [詞卡管理功能需求規格](../../01_requirement/詞卡管理功能產品需求規格.md) - 查看完整功能需求和用戶故事
|
||
>
|
||
> **🏗️ 技術架構**
|
||
> - [後端架構詳細說明](../../04_technical/backend-architecture.md) - 了解後端技術實現細節
|
||
> - [前端架構詳細說明](../../04_technical/frontend-architecture.md) - 了解前端整合方式 |