docs: 重新組織架構文檔體系並建立完整的詞卡管理 API 規格
- 重新組織文檔結構,整合到既有的 docs 目錄架構 - 移動架構文檔到 docs/04_technical/ 避免與現有結構衝突 - 移動需求規格文檔到 docs/01_requirement/ 保持分類邏輯 - 重新整理 AI 分析規格文檔到對應目錄 - 建立完整的 DramaLing 架構文檔體系: * 系統架構總覽:整體架構圖和技術棧說明 * 後端架構詳細說明:ASP.NET Core 8.0 實現細節 * 前端架構詳細說明:Next.js 15 組件設計模式 * 詞卡管理 API 規格:基於實際程式碼的完整 API 文檔 - 更新所有跨文檔引用路徑和依賴關係 - 為所有文檔引用添加清楚的用途標注說明 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7f85c79bc3
commit
0e2931ffe6
|
|
@ -13,11 +13,191 @@
|
||||||
- 📊 學習進度追蹤
|
- 📊 學習進度追蹤
|
||||||
- 🔄 批量操作功能
|
- 🔄 批量操作功能
|
||||||
|
|
||||||
## 2. 功能需求分析
|
## 2. 用戶需求分析
|
||||||
|
|
||||||
### 2.1 核心功能模組
|
### 2.1 用戶角色定義
|
||||||
|
|
||||||
#### 2.1.1 詞卡展示頁面 (FlashcardsPage)
|
#### 2.1.1 主要用戶
|
||||||
|
- **英語學習者**: 使用 DramaLing 學習英語詞彙的用戶
|
||||||
|
- **目標**: 有效管理和學習英語詞彙,提升英語水平
|
||||||
|
- **技能水平**: 具備基本的電腦操作能力,英語水平從初學者到高級
|
||||||
|
|
||||||
|
#### 2.1.2 用戶畫像
|
||||||
|
- **年齡**: 16-45歲
|
||||||
|
- **職業**: 學生、上班族、語言學習愛好者
|
||||||
|
- **使用場景**: 通勤時間、休息時間、專門的學習時段
|
||||||
|
- **設備**: 手機、平板、電腦
|
||||||
|
- **學習目標**: 考試準備、工作需要、興趣提升
|
||||||
|
|
||||||
|
### 2.2 用戶故事 (User Stories)
|
||||||
|
|
||||||
|
#### 2.2.1 詞卡檢視與瀏覽
|
||||||
|
```
|
||||||
|
作為一個英語學習者,
|
||||||
|
我想要瀏覽我所有的詞卡,
|
||||||
|
這樣我可以回顧我已經學習的詞彙。
|
||||||
|
|
||||||
|
驗收標準:
|
||||||
|
- 我可以看到詞卡列表,包含詞彙、翻譯、例句圖片
|
||||||
|
- 每個詞卡顯示 CEFR 難度等級和掌握度
|
||||||
|
- 我可以在「所有詞卡」和「收藏詞卡」之間切換
|
||||||
|
- 詞卡按創建時間排序顯示
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.2 詞卡搜尋與篩選
|
||||||
|
```
|
||||||
|
作為一個英語學習者,
|
||||||
|
我想要快速找到特定的詞卡,
|
||||||
|
這樣我可以有效地複習特定的詞彙。
|
||||||
|
|
||||||
|
驗收標準:
|
||||||
|
- 我可以透過搜尋框輸入關鍵字搜尋詞彙、翻譯或定義
|
||||||
|
- 我可以使用進階篩選按 CEFR 等級、詞性、掌握度篩選
|
||||||
|
- 我可以使用快速篩選按鈕找到需要加強的詞卡
|
||||||
|
- 搜尋結果會高亮顯示關鍵字,並顯示找到的詞卡數量
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.3 詞卡收藏管理
|
||||||
|
```
|
||||||
|
作為一個英語學習者,
|
||||||
|
我想要標記重要的詞卡為收藏,
|
||||||
|
這樣我可以優先複習重要的詞彙。
|
||||||
|
|
||||||
|
驗收標準:
|
||||||
|
- 我可以點擊星星圖標將詞卡加入收藏
|
||||||
|
- 已收藏的詞卡會顯示實心黃色星星
|
||||||
|
- 我可以在收藏分頁中查看所有收藏的詞卡
|
||||||
|
- 我可以隨時取消收藏某個詞卡
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.4 詞卡編輯與管理
|
||||||
|
```
|
||||||
|
作為一個英語學習者,
|
||||||
|
我想要編輯或刪除詞卡,
|
||||||
|
這樣我可以更正錯誤或移除不需要的詞卡。
|
||||||
|
|
||||||
|
驗收標準:
|
||||||
|
- 我可以點擊編輯按鈕修改詞卡的任何欄位
|
||||||
|
- 我可以刪除不需要的詞卡,系統會要求確認
|
||||||
|
- 編輯後的詞卡會立即更新並保存
|
||||||
|
- 刪除操作會顯示成功或失敗的回饋訊息
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.5 詞卡創建
|
||||||
|
```
|
||||||
|
作為一個英語學習者,
|
||||||
|
我想要手動創建新的詞卡,
|
||||||
|
這樣我可以將遇到的新詞彙加入學習列表。
|
||||||
|
|
||||||
|
驗收標準:
|
||||||
|
- 我可以點擊「新增詞卡」按鈕開啟創建表單
|
||||||
|
- 我可以填寫詞彙、翻譯、定義、發音、詞性、例句等欄位
|
||||||
|
- 提交後新詞卡會出現在詞卡列表中
|
||||||
|
- 如果有錯誤,系統會顯示明確的錯誤訊息
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2.6 詞卡詳細檢視
|
||||||
|
```
|
||||||
|
作為一個英語學習者,
|
||||||
|
我想要查看詞卡的完整詳細資訊,
|
||||||
|
這樣我可以深入了解詞彙的各種用法和學習狀態。
|
||||||
|
|
||||||
|
驗收標準:
|
||||||
|
- 我可以點擊「詳細」按鈕進入詞卡詳細頁面
|
||||||
|
- 詳細頁面顯示所有詞卡信息和學習統計
|
||||||
|
- 我可以在詳細頁面進行編輯或收藏操作
|
||||||
|
- 我可以查看學習記錄和複習歷史
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 用戶流程 (User Flows)
|
||||||
|
|
||||||
|
#### 2.3.1 詞卡瀏覽流程
|
||||||
|
```
|
||||||
|
用戶進入詞卡頁面 → 查看詞卡列表 → 選擇分頁(所有詞卡/收藏詞卡) → 瀏覽詞卡
|
||||||
|
↓
|
||||||
|
用戶可以執行以下操作:
|
||||||
|
- 點擊收藏/取消收藏
|
||||||
|
- 點擊編輯進入編輯模式
|
||||||
|
- 點擊刪除(需確認)
|
||||||
|
- 點擊詳細查看完整信息
|
||||||
|
- 點擊發音按鈕播放音頻
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.2 詞卡搜尋流程
|
||||||
|
```
|
||||||
|
用戶進入詞卡頁面 → 點擊搜尋框 → 輸入關鍵字 → 查看即時篩選結果
|
||||||
|
↓
|
||||||
|
用戶可以進一步操作:
|
||||||
|
- 點擊「進階篩選」設定更多條件
|
||||||
|
- 使用快速篩選按鈕
|
||||||
|
- 清除搜尋條件
|
||||||
|
- 對搜尋結果執行詞卡操作
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.3 詞卡創建流程
|
||||||
|
```
|
||||||
|
用戶進入詞卡頁面 → 點擊「新增詞卡」按鈕 → 填寫詞卡表單 → 點擊保存
|
||||||
|
↓
|
||||||
|
表單驗證:
|
||||||
|
- 成功:新詞卡出現在列表中,顯示成功訊息
|
||||||
|
- 失敗:顯示錯誤訊息,保持表單開啟狀態
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.4 詞卡編輯流程
|
||||||
|
```
|
||||||
|
用戶選擇詞卡 → 點擊「編輯」按鈕 → 修改詞卡欄位 → 點擊保存
|
||||||
|
↓
|
||||||
|
編輯驗證:
|
||||||
|
- 成功:詞卡更新,顯示成功訊息,關閉編輯表單
|
||||||
|
- 失敗:顯示錯誤訊息,保持編輯模式
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.5 詞卡刪除流程
|
||||||
|
```
|
||||||
|
用戶選擇詞卡 → 點擊「刪除」按鈕 → 確認刪除對話框 → 用戶確認
|
||||||
|
↓
|
||||||
|
刪除處理:
|
||||||
|
- 成功:詞卡從列表中移除,顯示成功訊息
|
||||||
|
- 失敗:顯示錯誤訊息,詞卡保持在列表中
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.6 詞卡收藏管理流程
|
||||||
|
```
|
||||||
|
用戶瀏覽詞卡 → 點擊星星圖標 → 切換收藏狀態
|
||||||
|
↓
|
||||||
|
收藏狀態更新:
|
||||||
|
- 加入收藏:星星變為實心黃色,顯示成功訊息
|
||||||
|
- 取消收藏:星星變為空心灰色,顯示成功訊息
|
||||||
|
- 在收藏分頁中即時更新詞卡列表
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.7 AI 生成詞卡流程 (與其他頁面整合)
|
||||||
|
```
|
||||||
|
用戶在詞卡頁面 → 點擊「AI 生成詞卡」按鈕 → 跳轉到 /generate 頁面
|
||||||
|
↓
|
||||||
|
AI 分析流程:
|
||||||
|
用戶輸入句子 → AI 分析 → 查看分析結果 → 點擊保存詞卡 → 返回詞卡頁面查看
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 用戶體驗目標
|
||||||
|
|
||||||
|
#### 2.4.1 易用性目標
|
||||||
|
- **直觀操作**: 新用戶在 5 分鐘內能完成基本詞卡操作
|
||||||
|
- **快速搜尋**: 搜尋結果在 300ms 內顯示
|
||||||
|
- **清楚回饋**: 所有操作都有明確的成功/失敗回饋
|
||||||
|
- **一致設計**: 整個詞卡管理流程保持視覺和交互一致性
|
||||||
|
|
||||||
|
#### 2.4.2 效率目標
|
||||||
|
- **批量操作**: 支援多選和批量操作(未來功能)
|
||||||
|
- **鍵盤快捷鍵**: 支援 ESC 清除搜尋等常用快捷鍵
|
||||||
|
- **智能提示**: 搜尋框提供輸入建議(未來功能)
|
||||||
|
- **離線功能**: 基本瀏覽功能支援離線使用(未來功能)
|
||||||
|
|
||||||
|
## 3. 功能需求分析
|
||||||
|
|
||||||
|
### 3.1 核心功能模組
|
||||||
|
|
||||||
|
#### 3.1.1 詞卡展示頁面 (FlashcardsPage)
|
||||||
- **位置**: `/flashcards`
|
- **位置**: `/flashcards`
|
||||||
- **主要功能**: 集中管理和展示所有詞卡
|
- **主要功能**: 集中管理和展示所有詞卡
|
||||||
|
|
||||||
|
|
@ -281,14 +461,35 @@ class FlashcardsService {
|
||||||
- **對比度**: 符合 WCAG 2.1 AA 標準
|
- **對比度**: 符合 WCAG 2.1 AA 標準
|
||||||
- **文字大小**: 支援縮放至 200% 而不影響功能
|
- **文字大小**: 支援縮放至 200% 而不影響功能
|
||||||
|
|
||||||
## 4. 技術實現規格
|
## 4. 技術約束與架構
|
||||||
|
|
||||||
### 4.1 前端技術架構
|
> 📋 **架構文檔引用**
|
||||||
- **框架**: Next.js 15 with App Router
|
>
|
||||||
- **語言**: TypeScript
|
> 本功能需求必須遵循既有的系統架構,完整技術規格請參考:
|
||||||
- **樣式**: Tailwind CSS
|
> - [系統架構總覽](../04_technical/system-architecture.md)
|
||||||
- **狀態管理**: React useState/useEffect hooks
|
> - [後端架構詳細說明](../04_technical/backend-architecture.md)
|
||||||
- **API 通信**: Fetch API with error handling
|
> - [前端架構詳細說明](../04_technical/frontend-architecture.md)
|
||||||
|
> - [詞卡 API 規格](../04_technical/flashcard-api-specification.md)
|
||||||
|
|
||||||
|
### 4.1 技術架構約束
|
||||||
|
|
||||||
|
#### 前端技術限制
|
||||||
|
- **框架**: 必須使用 Next.js 15 with App Router
|
||||||
|
- **語言**: 必須使用 TypeScript
|
||||||
|
- **樣式**: 必須使用 Tailwind CSS
|
||||||
|
- **狀態管理**: 使用 React hooks (useState/useEffect)
|
||||||
|
- **API 通信**: 使用現有的 flashcardsService
|
||||||
|
|
||||||
|
#### 後端技術限制
|
||||||
|
- **框架**: 必須使用 ASP.NET Core 8.0
|
||||||
|
- **資料庫**: 必須使用 SQLite + Entity Framework Core
|
||||||
|
- **API 格式**: 必須遵循現有的 RESTful API 設計
|
||||||
|
- **認證**: 必須相容現有的 JWT 認證機制
|
||||||
|
|
||||||
|
#### 資料模型約束
|
||||||
|
- **詞卡模型**: 必須使用現有的 Flashcard.cs 實體
|
||||||
|
- **API 回應**: 必須遵循 `{success, data?, error?}` 格式
|
||||||
|
- **關聯設計**: 必須保持與 User、CardSet 的現有關聯
|
||||||
|
|
||||||
### 4.2 資料處理流程
|
### 4.2 資料處理流程
|
||||||
|
|
||||||
|
|
@ -0,0 +1,945 @@
|
||||||
|
# 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) - 了解前端整合方式
|
||||||
|
|
@ -0,0 +1,590 @@
|
||||||
|
# DramaLing 後端架構詳細說明
|
||||||
|
|
||||||
|
## 1. 技術棧概覽
|
||||||
|
|
||||||
|
### 1.1 核心技術
|
||||||
|
- **框架**: ASP.NET Core 8.0
|
||||||
|
- **語言**: C# .NET 8
|
||||||
|
- **ORM**: Entity Framework Core 8.0
|
||||||
|
- **資料庫**: SQLite 3.x
|
||||||
|
- **認證**: JWT Bearer Token
|
||||||
|
- **依賴注入**: Microsoft.Extensions.DependencyInjection
|
||||||
|
|
||||||
|
### 1.2 專案結構
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/DramaLing.Api/
|
||||||
|
├── Controllers/ # API 控制器
|
||||||
|
│ ├── FlashcardsController.cs
|
||||||
|
│ ├── AIController.cs
|
||||||
|
│ └── AuthController.cs
|
||||||
|
├── Models/
|
||||||
|
│ ├── Entities/ # 資料模型
|
||||||
|
│ │ ├── Flashcard.cs
|
||||||
|
│ │ ├── User.cs
|
||||||
|
│ │ └── CardSet.cs
|
||||||
|
│ ├── DTOs/ # 資料傳輸物件
|
||||||
|
│ └── Configuration/ # 配置模型
|
||||||
|
├── Data/ # 資料存取層
|
||||||
|
│ ├── DramaLingDbContext.cs
|
||||||
|
│ └── Migrations/
|
||||||
|
├── Services/ # 業務邏輯層
|
||||||
|
│ ├── AI/ # AI 服務
|
||||||
|
│ ├── Caching/ # 快取服務
|
||||||
|
│ └── AuthService.cs
|
||||||
|
├── Extensions/ # 擴展方法
|
||||||
|
│ └── ServiceCollectionExtensions.cs
|
||||||
|
└── Program.cs # 應用程式入口
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 資料模型架構
|
||||||
|
|
||||||
|
### 2.1 詞卡實體模型 (Flashcard)
|
||||||
|
|
||||||
|
```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; }
|
||||||
|
|
||||||
|
// 導航屬性
|
||||||
|
public virtual User User { get; set; }
|
||||||
|
public virtual CardSet? CardSet { get; set; }
|
||||||
|
public virtual ICollection<StudyRecord> StudyRecords { get; set; }
|
||||||
|
public virtual ICollection<FlashcardTag> FlashcardTags { get; set; }
|
||||||
|
public virtual ICollection<ErrorReport> ErrorReports { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 資料庫關聯設計
|
||||||
|
|
||||||
|
```
|
||||||
|
Users (1) ──────────────── (*) Flashcards
|
||||||
|
│ │
|
||||||
|
│ │ (*)
|
||||||
|
│ │
|
||||||
|
└─── (1) CardSets (*) ───────┘
|
||||||
|
|
||||||
|
StudyRecords (*) ──── (1) Flashcards
|
||||||
|
ErrorReports (*) ──── (1) Flashcards
|
||||||
|
FlashcardTags (*) ─── (1) Flashcards
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. API 架構設計
|
||||||
|
|
||||||
|
### 3.1 控制器架構
|
||||||
|
|
||||||
|
#### FlashcardsController.cs
|
||||||
|
```csharp
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/flashcards")]
|
||||||
|
[AllowAnonymous] // 開發階段暫時移除認證
|
||||||
|
public class FlashcardsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly DramaLingDbContext _context;
|
||||||
|
private readonly ILogger<FlashcardsController> _logger;
|
||||||
|
|
||||||
|
// 標準 RESTful API 端點
|
||||||
|
[HttpGet] // GET /api/flashcards
|
||||||
|
[HttpGet("{id}")] // GET /api/flashcards/{id}
|
||||||
|
[HttpPost] // POST /api/flashcards
|
||||||
|
[HttpPut("{id}")] // PUT /api/flashcards/{id}
|
||||||
|
[HttpDelete("{id}")] // DELETE /api/flashcards/{id}
|
||||||
|
[HttpPost("{id}/favorite")] // POST /api/flashcards/{id}/favorite
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 API 回應格式標準化
|
||||||
|
|
||||||
|
#### 成功回應格式
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"flashcards": [...],
|
||||||
|
"count": 42
|
||||||
|
},
|
||||||
|
"message": "操作成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 錯誤回應格式
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "錯誤描述",
|
||||||
|
"details": "詳細錯誤信息",
|
||||||
|
"timestamp": "2025-09-24T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 查詢參數支援
|
||||||
|
|
||||||
|
#### GET /api/flashcards
|
||||||
|
```csharp
|
||||||
|
public async Task<ActionResult> GetFlashcards(
|
||||||
|
[FromQuery] string? search = null, // 搜尋關鍵字
|
||||||
|
[FromQuery] bool favoritesOnly = false // 僅收藏詞卡
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 服務層架構
|
||||||
|
|
||||||
|
### 4.1 依賴注入配置 (ServiceCollectionExtensions.cs)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
// 資料庫服務配置
|
||||||
|
public static IServiceCollection AddDatabaseServices(...)
|
||||||
|
|
||||||
|
// Repository 服務配置
|
||||||
|
public static IServiceCollection AddRepositoryServices(...)
|
||||||
|
|
||||||
|
// 快取服務配置
|
||||||
|
public static IServiceCollection AddCachingServices(...)
|
||||||
|
|
||||||
|
// AI 服務配置
|
||||||
|
public static IServiceCollection AddAIServices(...)
|
||||||
|
|
||||||
|
// 業務服務配置
|
||||||
|
public static IServiceCollection AddBusinessServices(...)
|
||||||
|
|
||||||
|
// 認證服務配置
|
||||||
|
public static IServiceCollection AddAuthenticationServices(...)
|
||||||
|
|
||||||
|
// CORS 政策配置
|
||||||
|
public static IServiceCollection AddCorsServices(...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 業務服務層
|
||||||
|
|
||||||
|
#### 已實現的服務
|
||||||
|
```csharp
|
||||||
|
// 認證服務
|
||||||
|
services.AddScoped<IAuthService, AuthService>();
|
||||||
|
|
||||||
|
// 使用量追蹤
|
||||||
|
services.AddScoped<IUsageTrackingService, UsageTrackingService>();
|
||||||
|
|
||||||
|
// Azure 語音服務
|
||||||
|
services.AddScoped<IAzureSpeechService, AzureSpeechService>();
|
||||||
|
|
||||||
|
// 音頻快取
|
||||||
|
services.AddScoped<IAudioCacheService, AudioCacheService>();
|
||||||
|
|
||||||
|
// AI 提供商管理
|
||||||
|
services.AddScoped<IAIProviderManager, AIProviderManager>();
|
||||||
|
services.AddScoped<IAIProvider, GeminiAIProvider>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 資料存取層
|
||||||
|
|
||||||
|
### 5.1 DbContext 配置
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class DramaLingDbContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<User> Users { get; set; }
|
||||||
|
public DbSet<Flashcard> Flashcards { get; set; }
|
||||||
|
public DbSet<CardSet> CardSets { get; set; }
|
||||||
|
public DbSet<StudyRecord> StudyRecords { get; set; }
|
||||||
|
public DbSet<ErrorReport> ErrorReports { get; set; }
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
// 詞卡配置
|
||||||
|
modelBuilder.Entity<Flashcard>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Word).IsRequired().HasMaxLength(255);
|
||||||
|
entity.Property(e => e.Translation).IsRequired();
|
||||||
|
entity.Property(e => e.Definition).IsRequired();
|
||||||
|
|
||||||
|
// 關聯配置
|
||||||
|
entity.HasOne(f => f.User)
|
||||||
|
.WithMany(u => u.Flashcards)
|
||||||
|
.HasForeignKey(f => f.UserId);
|
||||||
|
|
||||||
|
entity.HasOne(f => f.CardSet)
|
||||||
|
.WithMany(cs => cs.Flashcards)
|
||||||
|
.HasForeignKey(f => f.CardSetId)
|
||||||
|
.IsRequired(false); // CardSetId 可為空
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 資料庫連接配置
|
||||||
|
|
||||||
|
#### 開發環境
|
||||||
|
```csharp
|
||||||
|
// 環境變數或配置檔案
|
||||||
|
var connectionString = Environment.GetEnvironmentVariable("DRAMALING_DB_CONNECTION")
|
||||||
|
?? configuration.GetConnectionString("DefaultConnection")
|
||||||
|
?? "Data Source=dramaling_test.db";
|
||||||
|
|
||||||
|
services.AddDbContext<DramaLingDbContext>(options =>
|
||||||
|
options.UseSqlite(connectionString));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 記憶體資料庫 (測試用)
|
||||||
|
```csharp
|
||||||
|
var useInMemoryDb = Environment.GetEnvironmentVariable("USE_INMEMORY_DB") == "true";
|
||||||
|
if (useInMemoryDb)
|
||||||
|
{
|
||||||
|
services.AddDbContext<DramaLingDbContext>(options =>
|
||||||
|
options.UseSqlite("Data Source=:memory:"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 認證與授權
|
||||||
|
|
||||||
|
### 6.1 JWT 配置
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static IServiceCollection AddAuthenticationServices(...)
|
||||||
|
{
|
||||||
|
var supabaseUrl = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_URL")
|
||||||
|
?? "https://localhost";
|
||||||
|
var jwtSecret = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_JWT_SECRET")
|
||||||
|
?? "dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only";
|
||||||
|
|
||||||
|
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))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 開發階段認證處理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 暫時移除認證要求,使用固定測試用戶
|
||||||
|
private Guid GetUserId()
|
||||||
|
{
|
||||||
|
return Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||||
|
|
||||||
|
// 生產環境將啟用:
|
||||||
|
// var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
// if (Guid.TryParse(userIdString, out var userId))
|
||||||
|
// return userId;
|
||||||
|
// throw new UnauthorizedAccessException("Invalid user ID in token");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. CORS 設定
|
||||||
|
|
||||||
|
### 7.1 跨域政策配置
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowFrontend", policy =>
|
||||||
|
{
|
||||||
|
policy.WithOrigins("http://localhost:3000", "http://localhost:3001", "http://localhost:3002")
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowCredentials()
|
||||||
|
.SetPreflightMaxAge(TimeSpan.FromMinutes(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddPolicy("AllowAll", policy =>
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. AI 服務整合
|
||||||
|
|
||||||
|
### 8.1 AI 提供商架構
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// AI 提供商介面
|
||||||
|
public interface IAIProvider
|
||||||
|
{
|
||||||
|
Task<SentenceAnalysisResult> AnalyzeSentenceAsync(string inputText, AnalysisOptions options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gemini AI 實作
|
||||||
|
public class GeminiAIProvider : IAIProvider
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly GeminiOptions _options;
|
||||||
|
|
||||||
|
public async Task<SentenceAnalysisResult> AnalyzeSentenceAsync(...)
|
||||||
|
{
|
||||||
|
// 調用 Google Gemini API
|
||||||
|
// 處理回應和錯誤
|
||||||
|
// 返回標準化結果
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 AI 服務配置
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 強型別配置
|
||||||
|
services.Configure<GeminiOptions>(configuration.GetSection(GeminiOptions.SectionName));
|
||||||
|
services.AddSingleton<IValidateOptions<GeminiOptions>, GeminiOptionsValidator>();
|
||||||
|
|
||||||
|
// AI 服務註冊
|
||||||
|
services.AddHttpClient<GeminiAIProvider>();
|
||||||
|
services.AddScoped<IAIProvider, GeminiAIProvider>();
|
||||||
|
services.AddScoped<IAIProviderManager, AIProviderManager>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 錯誤處理架構
|
||||||
|
|
||||||
|
### 9.1 全域異常處理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
app.UseExceptionHandler(errorApp =>
|
||||||
|
{
|
||||||
|
errorApp.Run(async context =>
|
||||||
|
{
|
||||||
|
var errorFeature = context.Features.Get<IExceptionHandlerFeature>();
|
||||||
|
if (errorFeature != null)
|
||||||
|
{
|
||||||
|
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
|
||||||
|
logger.LogError(errorFeature.Error, "Unhandled exception occurred");
|
||||||
|
|
||||||
|
context.Response.StatusCode = 500;
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Internal server error",
|
||||||
|
timestamp = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(JsonSerializer.Serialize(response));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 控制器級錯誤處理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await flashcardsService.CreateFlashcard(data);
|
||||||
|
return Ok(new { success = true, data = result });
|
||||||
|
}
|
||||||
|
catch (ValidationException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Database error during flashcard creation");
|
||||||
|
return StatusCode(500, new { success = false, error = "Database operation failed" });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Unexpected error during flashcard creation");
|
||||||
|
return StatusCode(500, new { success = false, error = "Internal server error" });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 開發與部署
|
||||||
|
|
||||||
|
### 10.1 開發環境設定
|
||||||
|
|
||||||
|
#### 啟動開發伺服器
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
dotnet run --project DramaLing.Api
|
||||||
|
|
||||||
|
# 伺服器運行於: http://localhost:5008
|
||||||
|
# Swagger UI: http://localhost:5008/swagger
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 環境變數設定
|
||||||
|
```bash
|
||||||
|
export DRAMALING_DB_CONNECTION="Data Source=dramaling_test.db"
|
||||||
|
export DRAMALING_SUPABASE_URL="https://localhost"
|
||||||
|
export DRAMALING_SUPABASE_JWT_SECRET="dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only"
|
||||||
|
export USE_INMEMORY_DB="false"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 資料庫管理
|
||||||
|
|
||||||
|
#### Entity Framework 遷移
|
||||||
|
```bash
|
||||||
|
# 新增遷移
|
||||||
|
dotnet ef migrations add MigrationName
|
||||||
|
|
||||||
|
# 更新資料庫
|
||||||
|
dotnet ef database update
|
||||||
|
|
||||||
|
# 查看遷移狀態
|
||||||
|
dotnet ef migrations list
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 測試資料初始化
|
||||||
|
```csharp
|
||||||
|
// 自動創建測試用戶
|
||||||
|
var testUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||||||
|
if (testUser == null)
|
||||||
|
{
|
||||||
|
testUser = new User
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
Email = "test@dramaling.com",
|
||||||
|
Name = "Test User",
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_context.Users.Add(testUser);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. 效能優化
|
||||||
|
|
||||||
|
### 11.1 查詢優化
|
||||||
|
```csharp
|
||||||
|
// 使用 AsNoTracking 提升查詢效能
|
||||||
|
var flashcards = await _context.Flashcards
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(f => f.UserId == userId)
|
||||||
|
.OrderByDescending(f => f.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// 避免 N+1 查詢問題
|
||||||
|
var flashcardsWithDetails = await _context.Flashcards
|
||||||
|
.Include(f => f.StudyRecords)
|
||||||
|
.Include(f => f.CardSet)
|
||||||
|
.Where(f => f.UserId == userId)
|
||||||
|
.ToListAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.2 快取策略
|
||||||
|
```csharp
|
||||||
|
// 記憶體快取服務
|
||||||
|
services.AddMemoryCache();
|
||||||
|
services.AddScoped<ICacheService, HybridCacheService>();
|
||||||
|
|
||||||
|
// 快取使用範例
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. 安全性措施
|
||||||
|
|
||||||
|
### 12.1 輸入驗證
|
||||||
|
```csharp
|
||||||
|
// 模型驗證特性
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string Word { get; set; }
|
||||||
|
|
||||||
|
// 控制器層驗證
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.2 SQL 注入防護
|
||||||
|
```csharp
|
||||||
|
// Entity Framework 自動參數化查詢
|
||||||
|
var flashcards = _context.Flashcards
|
||||||
|
.Where(f => f.Word.Contains(searchTerm)) // 自動參數化
|
||||||
|
.ToList();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.3 XSS 防護
|
||||||
|
```csharp
|
||||||
|
// 自動 HTML 編碼
|
||||||
|
public string Definition { get; set; } // EF Core 自動處理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 13. 監控與日誌
|
||||||
|
|
||||||
|
### 13.1 結構化日誌
|
||||||
|
```csharp
|
||||||
|
_logger.LogInformation("Creating flashcard for user {UserId}, word: {Word}",
|
||||||
|
userId, request.Word);
|
||||||
|
|
||||||
|
_logger.LogError(ex, "Failed to create flashcard for user {UserId}", userId);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.2 健康檢查
|
||||||
|
```csharp
|
||||||
|
services.AddHealthChecks()
|
||||||
|
.AddDbContextCheck<DramaLingDbContext>();
|
||||||
|
|
||||||
|
app.MapHealthChecks("/health");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文檔版本**: v1.0
|
||||||
|
**建立日期**: 2025-09-24
|
||||||
|
**維護負責**: 後端開發團隊
|
||||||
|
**下次審核**: 架構變更時
|
||||||
|
|
||||||
|
> 📋 相關文檔:
|
||||||
|
> - [系統架構總覽](./system-architecture.md)
|
||||||
|
> - [前端架構詳細說明](./frontend-architecture.md)
|
||||||
|
> - [詞卡 API 規格](./flashcard-api-specification.md)
|
||||||
|
|
@ -0,0 +1,579 @@
|
||||||
|
# DramaLing API 規格總覽
|
||||||
|
|
||||||
|
## 1. API 架構概覽
|
||||||
|
|
||||||
|
### 1.1 基礎資訊
|
||||||
|
- **基礎 URL**: `http://localhost:5008/api` (開發環境)
|
||||||
|
- **協議**: HTTP/HTTPS
|
||||||
|
- **資料格式**: JSON
|
||||||
|
- **認證方式**: JWT Bearer Token
|
||||||
|
- **CORS**: 允許 localhost:3000-3002
|
||||||
|
|
||||||
|
### 1.2 標準回應格式
|
||||||
|
|
||||||
|
#### 成功回應格式
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
// 實際資料內容
|
||||||
|
},
|
||||||
|
"message": "操作成功" // 可選
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 錯誤回應格式
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "錯誤描述",
|
||||||
|
"details": "詳細錯誤信息", // 可選
|
||||||
|
"timestamp": "2025-09-24T10:30:00Z" // 可選
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 詞卡管理 API
|
||||||
|
|
||||||
|
### 2.1 API 端點清單
|
||||||
|
|
||||||
|
| 方法 | 端點 | 描述 | 認證 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| GET | `/api/flashcards` | 取得詞卡列表 | ✅ |
|
||||||
|
| GET | `/api/flashcards/{id}` | 取得單一詞卡 | ✅ |
|
||||||
|
| POST | `/api/flashcards` | 創建新詞卡 | ✅ |
|
||||||
|
| PUT | `/api/flashcards/{id}` | 更新詞卡 | ✅ |
|
||||||
|
| DELETE | `/api/flashcards/{id}` | 刪除詞卡 | ✅ |
|
||||||
|
| POST | `/api/flashcards/{id}/favorite` | 切換收藏狀態 | ✅ |
|
||||||
|
|
||||||
|
### 2.2 詳細 API 規格
|
||||||
|
|
||||||
|
#### GET /api/flashcards
|
||||||
|
**描述**: 取得用戶的詞卡列表,支援搜尋和篩選
|
||||||
|
|
||||||
|
**查詢參數**:
|
||||||
|
```typescript
|
||||||
|
interface GetFlashcardsQuery {
|
||||||
|
search?: string; // 搜尋關鍵字 (詞彙、翻譯、定義)
|
||||||
|
favoritesOnly?: boolean; // 僅顯示收藏詞卡 (預設: false)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/flashcards
|
||||||
|
**描述**: 創建新的詞卡
|
||||||
|
|
||||||
|
**請求體**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"word": "sophisticated",
|
||||||
|
"translation": "精密的",
|
||||||
|
"definition": "Highly developed or complex",
|
||||||
|
"pronunciation": "/səˈfɪstɪkeɪtɪd/",
|
||||||
|
"partOfSpeech": "adjective",
|
||||||
|
"example": "A sophisticated system",
|
||||||
|
"exampleTranslation": "一個精密的系統"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"word": "sophisticated",
|
||||||
|
// ... 完整詞卡資料
|
||||||
|
"createdAt": "2025-09-24T10:30:00Z"
|
||||||
|
},
|
||||||
|
"message": "詞卡創建成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PUT /api/flashcards/{id}
|
||||||
|
**描述**: 更新現有詞卡
|
||||||
|
|
||||||
|
**路徑參數**:
|
||||||
|
- `id`: 詞卡唯一識別碼 (GUID)
|
||||||
|
|
||||||
|
**請求體**: 與 POST 相同格式
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
// ... 更新後的詞卡資料
|
||||||
|
"updatedAt": "2025-09-24T10:35:00Z"
|
||||||
|
},
|
||||||
|
"message": "詞卡更新成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DELETE /api/flashcards/{id}
|
||||||
|
**描述**: 刪除詞卡 (軟刪除,設定 IsArchived = true)
|
||||||
|
|
||||||
|
**路徑參數**:
|
||||||
|
- `id`: 詞卡唯一識別碼 (GUID)
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "詞卡已刪除"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/flashcards/{id}/favorite
|
||||||
|
**描述**: 切換詞卡的收藏狀態
|
||||||
|
|
||||||
|
**路徑參數**:
|
||||||
|
- `id`: 詞卡唯一識別碼 (GUID)
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"isFavorite": true
|
||||||
|
},
|
||||||
|
"message": "已加入收藏"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. AI 分析 API
|
||||||
|
|
||||||
|
### 3.1 API 端點
|
||||||
|
|
||||||
|
| 方法 | 端點 | 描述 | 認證 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| POST | `/api/ai/analyze-sentence` | AI 句子分析 | ✅ |
|
||||||
|
|
||||||
|
### 3.2 句子分析 API
|
||||||
|
|
||||||
|
#### POST /api/ai/analyze-sentence
|
||||||
|
**描述**: 使用 AI 分析英語句子,提供詞彙分析、語法檢查、翻譯等功能
|
||||||
|
|
||||||
|
**請求體**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"inputText": "The sophisticated algorithm processes data efficiently.",
|
||||||
|
"analysisMode": "full",
|
||||||
|
"options": {
|
||||||
|
"includeGrammarCheck": true,
|
||||||
|
"includeVocabularyAnalysis": true,
|
||||||
|
"includeTranslation": true,
|
||||||
|
"includeIdiomDetection": true,
|
||||||
|
"includeExamples": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**回應範例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"originalText": "The sophisticated algorithm processes data efficiently.",
|
||||||
|
"sentenceMeaning": "這個精密的算法高效地處理資料。",
|
||||||
|
"grammarCorrection": {
|
||||||
|
"hasErrors": false,
|
||||||
|
"correctedText": null,
|
||||||
|
"corrections": [],
|
||||||
|
"confidenceScore": 0.95
|
||||||
|
},
|
||||||
|
"vocabularyAnalysis": {
|
||||||
|
"sophisticated": {
|
||||||
|
"word": "sophisticated",
|
||||||
|
"translation": "精密的",
|
||||||
|
"definition": "Highly developed or complex",
|
||||||
|
"partOfSpeech": "adjective",
|
||||||
|
"pronunciation": "/səˈfɪstɪkeɪtɪd/",
|
||||||
|
"difficultyLevel": "C1",
|
||||||
|
"frequency": "high",
|
||||||
|
"cefrLevel": "C1",
|
||||||
|
"synonyms": ["advanced", "complex", "refined"]
|
||||||
|
},
|
||||||
|
"algorithm": {
|
||||||
|
"word": "algorithm",
|
||||||
|
"translation": "算法",
|
||||||
|
"definition": "A set of rules for solving problems",
|
||||||
|
"partOfSpeech": "noun",
|
||||||
|
"pronunciation": "/ˈælɡərɪðəm/",
|
||||||
|
"difficultyLevel": "B2",
|
||||||
|
"frequency": "medium",
|
||||||
|
"cefrLevel": "B2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idioms": [
|
||||||
|
{
|
||||||
|
"idiom": "processes data",
|
||||||
|
"translation": "處理資料",
|
||||||
|
"definition": "To handle and analyze information",
|
||||||
|
"difficultyLevel": "B1",
|
||||||
|
"frequency": "high",
|
||||||
|
"cefrLevel": "B1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"processingTime": "2.34s"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 認證 API
|
||||||
|
|
||||||
|
### 4.1 API 端點
|
||||||
|
|
||||||
|
| 方法 | 端點 | 描述 | 認證 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| POST | `/api/auth/login` | 用戶登入 | ❌ |
|
||||||
|
| POST | `/api/auth/register` | 用戶註冊 | ❌ |
|
||||||
|
| POST | `/api/auth/refresh` | 更新 Token | ✅ |
|
||||||
|
| POST | `/api/auth/logout` | 用戶登出 | ✅ |
|
||||||
|
|
||||||
|
### 4.2 認證流程
|
||||||
|
|
||||||
|
#### 開發階段認證
|
||||||
|
```csharp
|
||||||
|
// 目前使用固定測試用戶 ID
|
||||||
|
private Guid GetUserId()
|
||||||
|
{
|
||||||
|
return Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制器暫時設定為 [AllowAnonymous]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class FlashcardsController : ControllerBase
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 生產環境認證 (未來啟用)
|
||||||
|
```csharp
|
||||||
|
// JWT Token 解析
|
||||||
|
private Guid GetUserId()
|
||||||
|
{
|
||||||
|
var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (Guid.TryParse(userIdString, out var userId))
|
||||||
|
return userId;
|
||||||
|
throw new UnauthorizedAccessException("Invalid user ID in token");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 錯誤代碼標準
|
||||||
|
|
||||||
|
### 5.1 HTTP 狀態碼使用
|
||||||
|
|
||||||
|
| 狀態碼 | 意義 | 使用場景 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| 200 | OK | 請求成功 |
|
||||||
|
| 201 | Created | 資源創建成功 |
|
||||||
|
| 400 | Bad Request | 請求參數錯誤 |
|
||||||
|
| 401 | Unauthorized | 認證失敗 |
|
||||||
|
| 403 | Forbidden | 權限不足 |
|
||||||
|
| 404 | Not Found | 資源不存在 |
|
||||||
|
| 409 | Conflict | 資源衝突 (如重複創建) |
|
||||||
|
| 500 | Internal Server Error | 伺服器內部錯誤 |
|
||||||
|
|
||||||
|
### 5.2 自定義錯誤碼
|
||||||
|
|
||||||
|
| 錯誤碼 | 描述 | HTTP 狀態 |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `FLASHCARD_NOT_FOUND` | 詞卡不存在 | 404 |
|
||||||
|
| `FLASHCARD_ALREADY_EXISTS` | 詞卡已存在 | 409 |
|
||||||
|
| `INVALID_CEFR_LEVEL` | 無效的 CEFR 等級 | 400 |
|
||||||
|
| `USER_NOT_FOUND` | 用戶不存在 | 404 |
|
||||||
|
| `DATABASE_ERROR` | 資料庫操作失敗 | 500 |
|
||||||
|
| `AI_SERVICE_UNAVAILABLE` | AI 服務不可用 | 503 |
|
||||||
|
|
||||||
|
## 6. 請求/回應範例
|
||||||
|
|
||||||
|
### 6.1 詞卡 CRUD 完整範例
|
||||||
|
|
||||||
|
#### 創建詞卡
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5008/api/flashcards \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"word": "elaborate",
|
||||||
|
"translation": "詳細說明",
|
||||||
|
"definition": "To explain in detail",
|
||||||
|
"pronunciation": "/ɪˈlæbərət/",
|
||||||
|
"partOfSpeech": "verb",
|
||||||
|
"example": "Please elaborate on your idea",
|
||||||
|
"exampleTranslation": "請詳細說明你的想法"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 更新詞卡
|
||||||
|
```bash
|
||||||
|
curl -X PUT http://localhost:5008/api/flashcards/550e8400-e29b-41d4-a716-446655440000 \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"word": "elaborate",
|
||||||
|
"translation": "詳細闡述",
|
||||||
|
"definition": "To explain something in greater detail",
|
||||||
|
"pronunciation": "/ɪˈlæbərət/",
|
||||||
|
"partOfSpeech": "verb",
|
||||||
|
"example": "Could you elaborate on that point?",
|
||||||
|
"exampleTranslation": "你能詳細闡述那個觀點嗎?"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 查詢詞卡 (帶搜尋)
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:5008/api/flashcards?search=elaborate&favoritesOnly=false"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 切換收藏
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5008/api/flashcards/550e8400-e29b-41d4-a716-446655440000/favorite
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 AI 分析範例
|
||||||
|
|
||||||
|
#### 句子分析請求
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5008/api/ai/analyze-sentence \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"inputText": "I need to elaborate on this concept",
|
||||||
|
"analysisMode": "full",
|
||||||
|
"options": {
|
||||||
|
"includeGrammarCheck": true,
|
||||||
|
"includeVocabularyAnalysis": true,
|
||||||
|
"includeTranslation": true,
|
||||||
|
"includeIdiomDetection": true,
|
||||||
|
"includeExamples": true
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 資料驗證規則
|
||||||
|
|
||||||
|
### 7.1 詞卡資料驗證
|
||||||
|
|
||||||
|
#### 必填欄位
|
||||||
|
```csharp
|
||||||
|
[Required]
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string Word { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Translation { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Definition { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Example { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 選填欄位約束
|
||||||
|
```csharp
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string? PartOfSpeech { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string? Pronunciation { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(10)]
|
||||||
|
public string? DifficultyLevel { get; set; } // A1, A2, B1, B2, C1, C2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 數值範圍驗證
|
||||||
|
```csharp
|
||||||
|
[Range(0, 100)]
|
||||||
|
public int MasteryLevel { get; set; } = 0;
|
||||||
|
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int TimesReviewed { get; set; } = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 前端驗證規則
|
||||||
|
|
||||||
|
#### TypeScript 型別約束
|
||||||
|
```typescript
|
||||||
|
interface CreateFlashcardRequest {
|
||||||
|
word: string; // 1-255 字元
|
||||||
|
translation: string; // 必填
|
||||||
|
definition: string; // 必填
|
||||||
|
pronunciation: string; // 選填,建議 IPA 格式
|
||||||
|
partOfSpeech: string; // 選填,預設 'noun'
|
||||||
|
example: string; // 必填
|
||||||
|
exampleTranslation?: string; // 選填
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 快取策略
|
||||||
|
|
||||||
|
### 8.1 伺服器端快取
|
||||||
|
|
||||||
|
#### 記憶體快取
|
||||||
|
```csharp
|
||||||
|
// 常用詞卡快取 30 分鐘
|
||||||
|
var cacheKey = $"flashcards:user:{userId}";
|
||||||
|
await _cacheService.SetAsync(cacheKey, flashcards, TimeSpan.FromMinutes(30));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 查詢結果快取
|
||||||
|
```csharp
|
||||||
|
// 搜尋結果快取 10 分鐘
|
||||||
|
var searchCacheKey = $"search:{userId}:{searchTerm}:{favoritesOnly}";
|
||||||
|
await _cacheService.SetAsync(searchCacheKey, results, TimeSpan.FromMinutes(10));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 客戶端快取
|
||||||
|
|
||||||
|
#### API 服務層快取
|
||||||
|
```typescript
|
||||||
|
// 簡單的記憶體快取 (未來可改用 SWR 或 React Query)
|
||||||
|
class FlashcardsService {
|
||||||
|
private cache = new Map<string, any>();
|
||||||
|
|
||||||
|
async getFlashcards(search?: string, favoritesOnly: boolean = false) {
|
||||||
|
const cacheKey = `flashcards:${search || 'all'}:${favoritesOnly}`;
|
||||||
|
|
||||||
|
if (this.cache.has(cacheKey)) {
|
||||||
|
return this.cache.get(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.makeRequest(endpoint);
|
||||||
|
this.cache.set(cacheKey, result);
|
||||||
|
|
||||||
|
// 5 分鐘後清除快取
|
||||||
|
setTimeout(() => this.cache.delete(cacheKey), 5 * 60 * 1000);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 速率限制
|
||||||
|
|
||||||
|
### 9.1 API 速率限制 (未來實作)
|
||||||
|
|
||||||
|
| 端點類型 | 限制 | 時間窗口 |
|
||||||
|
|----------|------|----------|
|
||||||
|
| 詞卡 CRUD | 100 requests | 每分鐘 |
|
||||||
|
| AI 分析 | 10 requests | 每分鐘 |
|
||||||
|
| 搜尋 | 200 requests | 每分鐘 |
|
||||||
|
|
||||||
|
### 9.2 使用量追蹤
|
||||||
|
|
||||||
|
#### AI API 使用量
|
||||||
|
```csharp
|
||||||
|
// 記錄 AI API 使用量
|
||||||
|
public class UsageTrackingService
|
||||||
|
{
|
||||||
|
public async Task RecordApiUsage(Guid userId, string apiType, decimal cost)
|
||||||
|
{
|
||||||
|
var usage = new ApiUsage
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ApiType = apiType,
|
||||||
|
Cost = cost,
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.ApiUsages.Add(usage);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 開發工具
|
||||||
|
|
||||||
|
### 10.1 API 文檔
|
||||||
|
|
||||||
|
#### Swagger 配置
|
||||||
|
```csharp
|
||||||
|
services.AddSwaggerGen(c =>
|
||||||
|
{
|
||||||
|
c.SwaggerDoc("v1", new() { Title = "DramaLing API", Version = "v1" });
|
||||||
|
|
||||||
|
// JWT 認證配置
|
||||||
|
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Description = "JWT Authorization header using the Bearer scheme",
|
||||||
|
Name = "Authorization",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Type = SecuritySchemeType.ApiKey,
|
||||||
|
Scheme = "Bearer"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 存取位置: http://localhost:5008/swagger
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 API 測試
|
||||||
|
|
||||||
|
#### 使用 curl 測試
|
||||||
|
```bash
|
||||||
|
# 設定基礎 URL
|
||||||
|
export API_BASE="http://localhost:5008/api"
|
||||||
|
|
||||||
|
# 測試詞卡列表
|
||||||
|
curl "$API_BASE/flashcards"
|
||||||
|
|
||||||
|
# 測試詞卡創建
|
||||||
|
curl -X POST "$API_BASE/flashcards" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @test-flashcard.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用 Postman Collection (未來)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"name": "DramaLing API",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Flashcards",
|
||||||
|
"item": [
|
||||||
|
// 詞卡相關 API 測試
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文檔版本**: v1.0
|
||||||
|
**建立日期**: 2025-09-24
|
||||||
|
**維護負責**: API 開發團隊
|
||||||
|
**更新頻率**: API 變更時即時更新
|
||||||
|
|
||||||
|
> 📋 相關文檔:
|
||||||
|
> - [系統架構總覽](./system-architecture.md)
|
||||||
|
> - [後端架構詳細說明](./backend-architecture.md)
|
||||||
|
> - [前端架構詳細說明](./frontend-architecture.md)
|
||||||
|
|
@ -0,0 +1,693 @@
|
||||||
|
# DramaLing 前端架構詳細說明
|
||||||
|
|
||||||
|
## 1. 技術棧概覽
|
||||||
|
|
||||||
|
### 1.1 核心技術
|
||||||
|
- **框架**: Next.js 15 (App Router)
|
||||||
|
- **語言**: TypeScript 5.x
|
||||||
|
- **樣式框架**: Tailwind CSS 3.x
|
||||||
|
- **UI 組件**: React 18.x + 自定義組件
|
||||||
|
- **狀態管理**: React useState/useEffect hooks
|
||||||
|
- **API 通信**: Fetch API + 自定義 Service 層
|
||||||
|
|
||||||
|
### 1.2 開發工具
|
||||||
|
- **套件管理**: npm
|
||||||
|
- **建置工具**: Next.js 內建 (Webpack + SWC)
|
||||||
|
- **型別檢查**: TypeScript Compiler
|
||||||
|
- **代碼格式化**: Prettier (如果配置)
|
||||||
|
- **代碼檢查**: ESLint (如果配置)
|
||||||
|
|
||||||
|
## 2. 專案結構
|
||||||
|
|
||||||
|
### 2.1 目錄架構
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── app/ # Next.js App Router 頁面
|
||||||
|
│ ├── flashcards/ # 詞卡管理頁面
|
||||||
|
│ │ ├── page.tsx # 詞卡列表頁面
|
||||||
|
│ │ └── [id]/ # 動態詞卡詳細頁面
|
||||||
|
│ │ └── page.tsx
|
||||||
|
│ ├── generate/ # AI 生成詞卡頁面
|
||||||
|
│ │ └── page.tsx
|
||||||
|
│ ├── settings/ # 設定頁面
|
||||||
|
│ │ └── page.tsx
|
||||||
|
│ ├── layout.tsx # 根佈局
|
||||||
|
│ ├── page.tsx # 首頁
|
||||||
|
│ └── globals.css # 全域樣式
|
||||||
|
├── components/ # 可重用組件
|
||||||
|
│ ├── ClickableTextV2.tsx # 可點擊文字組件
|
||||||
|
│ ├── FlashcardForm.tsx # 詞卡表單組件
|
||||||
|
│ ├── Navigation.tsx # 導航組件
|
||||||
|
│ ├── ProtectedRoute.tsx # 路由保護組件
|
||||||
|
│ └── AudioPlayer.tsx # 音頻播放組件
|
||||||
|
├── lib/ # 工具函數和服務
|
||||||
|
│ ├── services/ # API 服務層
|
||||||
|
│ │ ├── flashcards.ts # 詞卡 API 服務
|
||||||
|
│ │ └── auth.ts # 認證 API 服務
|
||||||
|
│ └── utils/ # 工具函數
|
||||||
|
├── public/ # 靜態資源
|
||||||
|
│ └── images/ # 圖片資源
|
||||||
|
├── package.json # 專案配置
|
||||||
|
├── tailwind.config.js # Tailwind 配置
|
||||||
|
├── tsconfig.json # TypeScript 配置
|
||||||
|
└── next.config.js # Next.js 配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 頁面架構設計
|
||||||
|
|
||||||
|
### 3.1 詞卡管理頁面 (app/flashcards/page.tsx)
|
||||||
|
|
||||||
|
#### 組件層級結構
|
||||||
|
```
|
||||||
|
FlashcardsPage (Protected Route Wrapper)
|
||||||
|
└── FlashcardsContent (Main Logic Component)
|
||||||
|
├── Navigation (Top Navigation Bar)
|
||||||
|
├── Page Header (Title + Action Buttons)
|
||||||
|
├── Tab System (All Cards / Favorites)
|
||||||
|
├── Search & Filter Section
|
||||||
|
│ ├── Main Search Input
|
||||||
|
│ ├── Advanced Filters (Collapsible)
|
||||||
|
│ └── Quick Filter Buttons
|
||||||
|
├── Flashcard List Display
|
||||||
|
│ └── Flashcard Card Components (Repeated)
|
||||||
|
└── FlashcardForm Modal (When Editing)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 狀態管理架構
|
||||||
|
```typescript
|
||||||
|
// 主要狀態
|
||||||
|
const [activeTab, setActiveTab] = useState<'all-cards' | 'favorites'>('all-cards')
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||||
|
const [searchFilters, setSearchFilters] = useState<SearchFilters>({
|
||||||
|
cefrLevel: '',
|
||||||
|
partOfSpeech: '',
|
||||||
|
masteryLevel: '',
|
||||||
|
onlyFavorites: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 資料狀態
|
||||||
|
const [flashcards, setFlashcards] = useState<Flashcard[]>([])
|
||||||
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// 表單狀態
|
||||||
|
const [showForm, setShowForm] = useState<boolean>(false)
|
||||||
|
const [editingCard, setEditingCard] = useState<Flashcard | null>(null)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 AI 分析頁面 (app/generate/page.tsx)
|
||||||
|
|
||||||
|
#### 組件結構
|
||||||
|
```
|
||||||
|
GeneratePage (Protected Route Wrapper)
|
||||||
|
└── GenerateContent (Main Logic Component)
|
||||||
|
├── Navigation
|
||||||
|
├── Input Section (Conditional Rendering)
|
||||||
|
│ ├── Text Input Area
|
||||||
|
│ ├── Character Counter
|
||||||
|
│ └── Analysis Button
|
||||||
|
└── Analysis Results Section (Conditional Rendering)
|
||||||
|
├── Star Explanation Banner
|
||||||
|
├── Grammar Correction Panel (If Needed)
|
||||||
|
├── Main Sentence Display
|
||||||
|
│ ├── Vocabulary Statistics Cards
|
||||||
|
│ ├── ClickableTextV2 Component
|
||||||
|
│ ├── Translation Section
|
||||||
|
│ └── Idioms Display Section
|
||||||
|
└── Action Buttons
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 分析結果資料流
|
||||||
|
```typescript
|
||||||
|
// AI 分析請求
|
||||||
|
handleAnalyzeSentence()
|
||||||
|
→ fetch('/api/ai/analyze-sentence')
|
||||||
|
→ setSentenceAnalysis(apiData)
|
||||||
|
→ setShowAnalysisView(true)
|
||||||
|
|
||||||
|
// 詞卡保存流程
|
||||||
|
handleSaveWord()
|
||||||
|
→ flashcardsService.createFlashcard()
|
||||||
|
→ alert(success/failure message)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 組件設計模式
|
||||||
|
|
||||||
|
### 4.1 可重用組件設計
|
||||||
|
|
||||||
|
#### ClickableTextV2 組件
|
||||||
|
```typescript
|
||||||
|
interface ClickableTextProps {
|
||||||
|
text: string; // 顯示文字
|
||||||
|
analysis?: Record<string, WordAnalysis>; // 詞彙分析資料
|
||||||
|
onWordClick?: (word: string, analysis: WordAnalysis) => void;
|
||||||
|
onSaveWord?: (word: string, analysis: WordAnalysis) => Promise<{success: boolean}>;
|
||||||
|
remainingUsage?: number; // 剩餘使用次數
|
||||||
|
showIdiomsInline?: boolean; // 是否內嵌顯示慣用語
|
||||||
|
}
|
||||||
|
|
||||||
|
// 設計特色:
|
||||||
|
// - 詞彙點擊互動
|
||||||
|
// - CEFR 等級顏色編碼
|
||||||
|
// - 星星標記 (高頻詞彙)
|
||||||
|
// - 彈出式詞彙詳情
|
||||||
|
// - Portal 渲染優化
|
||||||
|
```
|
||||||
|
|
||||||
|
#### FlashcardForm 組件
|
||||||
|
```typescript
|
||||||
|
interface FlashcardFormProps {
|
||||||
|
initialData?: Partial<Flashcard>; // 編輯時的初始資料
|
||||||
|
isEdit?: boolean; // 是否為編輯模式
|
||||||
|
onSuccess: () => void; // 成功回調
|
||||||
|
onCancel: () => void; // 取消回調
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表單欄位:
|
||||||
|
// - word (必填)
|
||||||
|
// - translation (必填)
|
||||||
|
// - definition (必填)
|
||||||
|
// - pronunciation (選填)
|
||||||
|
// - partOfSpeech (選填,下拉選單)
|
||||||
|
// - example (必填)
|
||||||
|
// - exampleTranslation (選填)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 路由保護模式
|
||||||
|
|
||||||
|
#### ProtectedRoute 組件
|
||||||
|
```typescript
|
||||||
|
// 用途:保護需要認證的頁面
|
||||||
|
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||||
|
// 檢查認證狀態
|
||||||
|
// 未登入時導向登入頁面
|
||||||
|
// 已登入時顯示子組件
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* 認證檢查邏輯 */}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用方式:
|
||||||
|
<ProtectedRoute>
|
||||||
|
<FlashcardsContent />
|
||||||
|
</ProtectedRoute>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. API 服務層設計
|
||||||
|
|
||||||
|
### 5.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>
|
||||||
|
|
||||||
|
// CRUD 操作方法
|
||||||
|
async getFlashcards(search?: string, favoritesOnly?: boolean): Promise<ApiResponse<{flashcards: Flashcard[], count: number}>>
|
||||||
|
async getFlashcard(id: string): Promise<ApiResponse<Flashcard>>
|
||||||
|
async createFlashcard(data: CreateFlashcardRequest): Promise<ApiResponse<Flashcard>>
|
||||||
|
async updateFlashcard(id: string, data: CreateFlashcardRequest): Promise<ApiResponse<Flashcard>>
|
||||||
|
async deleteFlashcard(id: string): Promise<ApiResponse<void>>
|
||||||
|
async toggleFavorite(id: string): Promise<ApiResponse<void>>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 單例模式匯出
|
||||||
|
export const flashcardsService = new FlashcardsService();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 型別定義標準化
|
||||||
|
|
||||||
|
#### 核心型別定義
|
||||||
|
```typescript
|
||||||
|
// 詞卡介面定義
|
||||||
|
export interface Flashcard {
|
||||||
|
id: string;
|
||||||
|
word: string;
|
||||||
|
translation: string;
|
||||||
|
definition: string;
|
||||||
|
partOfSpeech: string;
|
||||||
|
pronunciation: string;
|
||||||
|
example: string;
|
||||||
|
exampleTranslation?: string;
|
||||||
|
masteryLevel: number;
|
||||||
|
timesReviewed: number;
|
||||||
|
isFavorite: boolean;
|
||||||
|
nextReviewDate: string;
|
||||||
|
difficultyLevel: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 回應格式
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 創建請求格式
|
||||||
|
export interface CreateFlashcardRequest {
|
||||||
|
word: string;
|
||||||
|
translation: string;
|
||||||
|
definition: string;
|
||||||
|
pronunciation: string;
|
||||||
|
partOfSpeech: string;
|
||||||
|
example: string;
|
||||||
|
exampleTranslation?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 樣式系統架構
|
||||||
|
|
||||||
|
### 6.1 Tailwind CSS 配置
|
||||||
|
|
||||||
|
#### 主要設計系統
|
||||||
|
```javascript
|
||||||
|
// tailwind.config.js
|
||||||
|
module.exports = {
|
||||||
|
content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
DEFAULT: '#3B82F6', // 主色調藍色
|
||||||
|
hover: '#2563EB' // 懸停狀態
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CEFR 等級顏色系統
|
||||||
|
```typescript
|
||||||
|
const getCEFRColor = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
case 'A1': return 'bg-green-100 text-green-700 border-green-200'
|
||||||
|
case 'A2': return 'bg-blue-100 text-blue-700 border-blue-200'
|
||||||
|
case 'B1': return 'bg-yellow-100 text-yellow-700 border-yellow-200'
|
||||||
|
case 'B2': return 'bg-orange-100 text-orange-700 border-orange-200'
|
||||||
|
case 'C1': return 'bg-red-100 text-red-700 border-red-200'
|
||||||
|
case 'C2': return 'bg-purple-100 text-purple-700 border-purple-200'
|
||||||
|
default: return 'bg-gray-100 text-gray-700 border-gray-200'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 響應式設計模式
|
||||||
|
|
||||||
|
#### 斷點策略
|
||||||
|
```css
|
||||||
|
/* 手機版 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.flashcard-grid { grid-template-columns: 1fr; }
|
||||||
|
.search-filters { flex-direction: column; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板版 */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.flashcard-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面版 */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.flashcard-grid { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 效能優化策略
|
||||||
|
|
||||||
|
### 7.1 React 效能優化
|
||||||
|
|
||||||
|
#### useMemo 和 useCallback 使用
|
||||||
|
```typescript
|
||||||
|
// 快取詞彙統計計算
|
||||||
|
const vocabularyStats = useMemo(() => {
|
||||||
|
if (!sentenceAnalysis?.vocabularyAnalysis) return defaultStats;
|
||||||
|
|
||||||
|
// 複雜計算邏輯
|
||||||
|
return calculateStats(sentenceAnalysis.vocabularyAnalysis);
|
||||||
|
}, [sentenceAnalysis])
|
||||||
|
|
||||||
|
// 快取事件處理函數
|
||||||
|
const handleWordClick = useCallback(async (word: string, event: React.MouseEvent) => {
|
||||||
|
// 事件處理邏輯
|
||||||
|
}, [findWordAnalysis, onWordClick, calculatePopupPosition])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 組件懶載入
|
||||||
|
```typescript
|
||||||
|
// 動態導入大型組件
|
||||||
|
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
|
||||||
|
loading: () => <div>載入中...</div>,
|
||||||
|
ssr: false
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 資料載入優化
|
||||||
|
|
||||||
|
#### 條件式資料載入
|
||||||
|
```typescript
|
||||||
|
useEffect(() => {
|
||||||
|
// 只在需要時載入資料
|
||||||
|
if (activeTab === 'favorites') {
|
||||||
|
loadFavoriteFlashcards();
|
||||||
|
} else {
|
||||||
|
loadAllFlashcards();
|
||||||
|
}
|
||||||
|
}, [activeTab])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 錯誤邊界處理
|
||||||
|
```typescript
|
||||||
|
// API 服務層錯誤處理
|
||||||
|
private async makeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
try {
|
||||||
|
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();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API request failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 狀態管理模式
|
||||||
|
|
||||||
|
### 8.1 本地狀態管理
|
||||||
|
|
||||||
|
#### 頁面級狀態
|
||||||
|
```typescript
|
||||||
|
// 每個頁面管理自己的狀態
|
||||||
|
function FlashcardsContent() {
|
||||||
|
// UI 狀態
|
||||||
|
const [activeTab, setActiveTab] = useState('all-cards')
|
||||||
|
const [showForm, setShowForm] = useState(false)
|
||||||
|
|
||||||
|
// 資料狀態
|
||||||
|
const [flashcards, setFlashcards] = useState<Flashcard[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
// 搜尋狀態
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [searchFilters, setSearchFilters] = useState(defaultFilters)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 跨組件狀態同步
|
||||||
|
```typescript
|
||||||
|
// 通過 props 和 callback 實現父子組件通信
|
||||||
|
<FlashcardForm
|
||||||
|
initialData={editingCard}
|
||||||
|
isEdit={!!editingCard}
|
||||||
|
onSuccess={handleFormSuccess} // 成功後重新載入資料
|
||||||
|
onCancel={() => setShowForm(false)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 持久化狀態
|
||||||
|
|
||||||
|
#### localStorage 使用
|
||||||
|
```typescript
|
||||||
|
// 用戶偏好設定
|
||||||
|
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
|
||||||
|
|
||||||
|
// 搜尋歷史 (未來功能)
|
||||||
|
const searchHistory = JSON.parse(localStorage.getItem('searchHistory') || '[]')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 用戶體驗設計
|
||||||
|
|
||||||
|
### 9.1 載入狀態處理
|
||||||
|
|
||||||
|
#### 統一載入狀態
|
||||||
|
```typescript
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-lg">載入中...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 按鈕載入狀態
|
||||||
|
```typescript
|
||||||
|
<button
|
||||||
|
disabled={loading}
|
||||||
|
className="disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? '載入中...' : '提交'}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 錯誤狀態處理
|
||||||
|
|
||||||
|
#### 頁面級錯誤
|
||||||
|
```typescript
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-red-600">{error}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 表單錯誤
|
||||||
|
```typescript
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 空狀態設計
|
||||||
|
|
||||||
|
#### 友善的空狀態
|
||||||
|
```typescript
|
||||||
|
{filteredCards.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-gray-500 mb-4">沒有找到詞卡</p>
|
||||||
|
<Link href="/generate" className="bg-primary text-white px-4 py-2 rounded-lg">
|
||||||
|
創建新詞卡
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 詞卡列表
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 互動設計模式
|
||||||
|
|
||||||
|
### 10.1 搜尋互動
|
||||||
|
|
||||||
|
#### 即時搜尋
|
||||||
|
```typescript
|
||||||
|
// 輸入時即時過濾,無防抖延遲
|
||||||
|
const filteredCards = allCards.filter(card => {
|
||||||
|
if (searchTerm) {
|
||||||
|
const searchLower = searchTerm.toLowerCase()
|
||||||
|
return card.word?.toLowerCase().includes(searchLower) ||
|
||||||
|
card.translation?.toLowerCase().includes(searchLower) ||
|
||||||
|
card.definition?.toLowerCase().includes(searchLower)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 搜尋結果高亮
|
||||||
|
```typescript
|
||||||
|
const highlightSearchTerm = (text: string, searchTerm: string) => {
|
||||||
|
if (!searchTerm || !text) return text
|
||||||
|
|
||||||
|
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
|
||||||
|
const parts = text.split(regex)
|
||||||
|
|
||||||
|
return parts.map((part, index) =>
|
||||||
|
regex.test(part) ? (
|
||||||
|
<mark key={index} className="bg-yellow-200 text-yellow-900 px-1 rounded">
|
||||||
|
{part}
|
||||||
|
</mark>
|
||||||
|
) : part
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 彈窗互動設計
|
||||||
|
|
||||||
|
#### Portal 渲染模式
|
||||||
|
```typescript
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
|
const VocabPopup = () => {
|
||||||
|
if (!selectedWord || !mounted) return null
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<>
|
||||||
|
{/* 遮罩層 */}
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={closePopup} />
|
||||||
|
|
||||||
|
{/* 彈窗內容 */}
|
||||||
|
<div className="fixed z-50 bg-white rounded-xl shadow-lg" style={{...}}>
|
||||||
|
{/* 詞彙詳細資訊 */}
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.3 鍵盤操作支援
|
||||||
|
|
||||||
|
#### ESC 鍵清除搜尋
|
||||||
|
```typescript
|
||||||
|
<input
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setSearchTerm('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. 開發與建置
|
||||||
|
|
||||||
|
### 11.1 開發環境
|
||||||
|
|
||||||
|
#### 開發伺服器啟動
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 開發伺服器運行於: http://localhost:3000
|
||||||
|
# Hot Reload 啟用
|
||||||
|
# Fast Refresh 啟用
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 環境變數配置
|
||||||
|
```bash
|
||||||
|
# .env.local
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:5008
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.2 建置優化
|
||||||
|
|
||||||
|
#### Next.js 配置 (next.config.js)
|
||||||
|
```javascript
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
appDir: true, // 啟用 App Router
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
domains: ['localhost'], // 圖片域名白名單
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TypeScript 配置 (tsconfig.json)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "es6"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"] // 路徑別名
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. 測試策略
|
||||||
|
|
||||||
|
### 12.1 組件測試
|
||||||
|
```typescript
|
||||||
|
// 未來實作:Jest + React Testing Library
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import { FlashcardForm } from './FlashcardForm'
|
||||||
|
|
||||||
|
test('should submit form with valid data', async () => {
|
||||||
|
const onSuccess = jest.fn()
|
||||||
|
render(<FlashcardForm onSuccess={onSuccess} onCancel={() => {}} />)
|
||||||
|
|
||||||
|
// 填寫表單
|
||||||
|
fireEvent.change(screen.getByLabelText('單字'), { target: { value: 'test' } })
|
||||||
|
|
||||||
|
// 提交表單
|
||||||
|
fireEvent.click(screen.getByText('創建詞卡'))
|
||||||
|
|
||||||
|
// 驗證結果
|
||||||
|
expect(onSuccess).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.2 API 服務測試
|
||||||
|
```typescript
|
||||||
|
// Mock fetch 進行單元測試
|
||||||
|
global.fetch = jest.fn()
|
||||||
|
|
||||||
|
test('should create flashcard successfully', async () => {
|
||||||
|
const mockResponse = { success: true, data: mockFlashcard }
|
||||||
|
;(fetch as jest.Mock).mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockResponse,
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await flashcardsService.createFlashcard(mockData)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文檔版本**: v1.0
|
||||||
|
**建立日期**: 2025-09-24
|
||||||
|
**維護負責**: 前端開發團隊
|
||||||
|
**下次審核**: 架構變更時
|
||||||
|
|
||||||
|
> 📋 相關文檔:
|
||||||
|
> - [系統架構總覽](./system-architecture.md)
|
||||||
|
> - [後端架構詳細說明](./backend-architecture.md)
|
||||||
|
> - [詞卡 API 規格](./flashcard-api-specification.md)
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
# DramaLing 系統架構總覽
|
||||||
|
|
||||||
|
## 1. 系統架構概要
|
||||||
|
|
||||||
|
### 1.1 整體架構圖
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ 前端 (React) │◄──►│ 後端 API │◄──►│ 外部服務 │
|
||||||
|
│ Next.js 15 │ │ ASP.NET Core │ │ Google AI │
|
||||||
|
│ TypeScript │ │ C# .NET 8 │ │ Azure Speech │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ │
|
||||||
|
│ 資料存儲 │
|
||||||
|
│ SQLite DB │
|
||||||
|
│ 本地檔案 │
|
||||||
|
│ │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 核心組件
|
||||||
|
|
||||||
|
#### 🖥️ **前端層 (Client)**
|
||||||
|
- **技術棧**: Next.js 15 + TypeScript + Tailwind CSS
|
||||||
|
- **部署**: http://localhost:3000 (開發環境)
|
||||||
|
- **主要職責**: 用戶介面、用戶互動、API 呼叫
|
||||||
|
|
||||||
|
#### ⚙️ **後端層 (Server)**
|
||||||
|
- **技術棧**: ASP.NET Core 8.0 + C#
|
||||||
|
- **部署**: http://localhost:5008 (開發環境)
|
||||||
|
- **主要職責**: 業務邏輯、API 服務、資料處理
|
||||||
|
|
||||||
|
#### 💾 **資料層 (Data)**
|
||||||
|
- **資料庫**: SQLite + Entity Framework Core
|
||||||
|
- **檔案位置**: `dramaling_test.db`
|
||||||
|
- **主要職責**: 資料持久化、關聯管理
|
||||||
|
|
||||||
|
#### 🌐 **外部服務層 (External)**
|
||||||
|
- **AI 服務**: Google Gemini API
|
||||||
|
- **語音服務**: Azure Speech Service
|
||||||
|
- **主要職責**: AI 分析、語音合成
|
||||||
|
|
||||||
|
## 2. 技術棧詳細說明
|
||||||
|
|
||||||
|
### 2.1 前端技術棧
|
||||||
|
|
||||||
|
| 技術組件 | 版本 | 用途 | 檔案位置 |
|
||||||
|
|---------|------|------|----------|
|
||||||
|
| Next.js | 15.x | React 框架 | `/frontend` |
|
||||||
|
| TypeScript | 5.x | 型別安全 | `.tsx`, `.ts` 檔案 |
|
||||||
|
| Tailwind CSS | 3.x | 樣式框架 | `tailwind.config.js` |
|
||||||
|
| React | 18.x | UI 組件 | `/components` |
|
||||||
|
|
||||||
|
### 2.2 後端技術棧
|
||||||
|
|
||||||
|
| 技術組件 | 版本 | 用途 | 檔案位置 |
|
||||||
|
|---------|------|------|----------|
|
||||||
|
| ASP.NET Core | 8.0 | Web API 框架 | `/backend/DramaLing.Api` |
|
||||||
|
| Entity Framework | 8.0 | ORM 框架 | `/Data` |
|
||||||
|
| SQLite | 3.x | 資料庫 | `dramaling_test.db` |
|
||||||
|
| JWT | - | 身份驗證 | `/Services/AuthService.cs` |
|
||||||
|
|
||||||
|
### 2.3 開發工具
|
||||||
|
|
||||||
|
| 工具 | 用途 | 配置檔案 |
|
||||||
|
|------|------|----------|
|
||||||
|
| npm | 前端套件管理 | `package.json` |
|
||||||
|
| dotnet | 後端專案管理 | `*.csproj` |
|
||||||
|
| Git | 版本控制 | `.gitignore` |
|
||||||
|
|
||||||
|
## 3. 服務間通信
|
||||||
|
|
||||||
|
### 3.1 前後端通信
|
||||||
|
- **協議**: HTTP/HTTPS
|
||||||
|
- **格式**: JSON
|
||||||
|
- **認證**: JWT Token
|
||||||
|
- **CORS**: 配置允許 localhost:3000-3002
|
||||||
|
|
||||||
|
### 3.2 API 基礎規範
|
||||||
|
- **基礎路徑**: `http://localhost:5008/api`
|
||||||
|
- **內容類型**: `application/json`
|
||||||
|
- **錯誤格式**: 標準化錯誤回應
|
||||||
|
- **成功格式**: `{success: boolean, data?: T, error?: string}`
|
||||||
|
|
||||||
|
## 4. 資料流架構
|
||||||
|
|
||||||
|
### 4.1 典型請求流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用戶操作 → React Component → API Service → HTTP Request → ASP.NET Controller → Business Service → Entity Framework → SQLite
|
||||||
|
↓ ↑
|
||||||
|
Response ← State Update ← API Response ← HTTP Response ← JSON Serialization ← Business Logic ← Database Query ←────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 錯誤處理流程
|
||||||
|
|
||||||
|
```
|
||||||
|
異常發生 → Exception Handling → Error Response → Frontend Error State → User Feedback
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 安全架構
|
||||||
|
|
||||||
|
### 5.1 認證機制
|
||||||
|
- **JWT Token**: 用戶身份驗證
|
||||||
|
- **Token 來源**: Supabase 相容格式
|
||||||
|
- **驗證位置**: ASP.NET Core Middleware
|
||||||
|
|
||||||
|
### 5.2 資料保護
|
||||||
|
- **輸入驗證**: 前端 + 後端雙重驗證
|
||||||
|
- **SQL 注入防護**: Entity Framework 參數化查詢
|
||||||
|
- **XSS 防護**: React 內建保護機制
|
||||||
|
|
||||||
|
## 6. 開發環境
|
||||||
|
|
||||||
|
### 6.1 本地開發設定
|
||||||
|
|
||||||
|
#### 前端開發伺服器
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
# 運行於: http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 後端開發伺服器
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
dotnet run --project DramaLing.Api
|
||||||
|
# 運行於: http://localhost:5008
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 環境變數
|
||||||
|
|
||||||
|
#### 後端環境變數
|
||||||
|
```bash
|
||||||
|
DRAMALING_DB_CONNECTION=Data Source=dramaling_test.db
|
||||||
|
DRAMALING_SUPABASE_URL=https://localhost
|
||||||
|
DRAMALING_SUPABASE_JWT_SECRET=dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only
|
||||||
|
USE_INMEMORY_DB=false
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 前端環境變數
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:5008
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 部署架構
|
||||||
|
|
||||||
|
### 7.1 開發環境
|
||||||
|
- **前端**: npm run dev (Hot Reload)
|
||||||
|
- **後端**: dotnet run (Hot Reload)
|
||||||
|
- **資料庫**: 本地 SQLite 檔案
|
||||||
|
|
||||||
|
### 7.2 生產環境 (未來)
|
||||||
|
- **前端**: Vercel / Netlify
|
||||||
|
- **後端**: Azure App Service / AWS EC2
|
||||||
|
- **資料庫**: PostgreSQL / Azure SQL
|
||||||
|
|
||||||
|
## 8. 監控與維護
|
||||||
|
|
||||||
|
### 8.1 日誌系統
|
||||||
|
- **前端**: Console.log (開發), Sentry (生產)
|
||||||
|
- **後端**: ILogger, 結構化日誌
|
||||||
|
- **API**: HTTP 請求/回應日誌
|
||||||
|
|
||||||
|
### 8.2 效能監控
|
||||||
|
- **前端**: Next.js 內建分析
|
||||||
|
- **後端**: ASP.NET Core 效能計數器
|
||||||
|
- **資料庫**: EF Core 查詢分析
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文檔版本**: v1.0
|
||||||
|
**建立日期**: 2025-09-24
|
||||||
|
**維護負責**: 開發團隊
|
||||||
|
**更新頻率**: 架構變更時即時更新
|
||||||
|
|
||||||
|
> 📋 此文檔為系統架構總覽,詳細技術規格請參考:
|
||||||
|
> - [後端架構詳細說明](./backend-architecture.md)
|
||||||
|
> - [前端架構詳細說明](./frontend-architecture.md)
|
||||||
|
> - [詞卡 API 規格](./flashcard-api-specification.md)
|
||||||
Loading…
Reference in New Issue