630 lines
14 KiB
Markdown
630 lines
14 KiB
Markdown
# DramaLing 資料模型文檔
|
|
|
|
## 1. 資料庫架構圖
|
|
|
|
```mermaid
|
|
erDiagram
|
|
users ||--o{ decks : creates
|
|
users ||--o{ flashcards : creates
|
|
users ||--o{ learning_records : has
|
|
users ||--o{ user_stats : has
|
|
decks ||--o{ flashcards : contains
|
|
flashcards ||--o{ learning_records : tracks
|
|
flashcards ||--o{ flashcard_tags : has
|
|
tags ||--o{ flashcard_tags : used_in
|
|
|
|
users {
|
|
uuid id PK
|
|
string email UK
|
|
string username UK
|
|
string password_hash
|
|
string avatar_url
|
|
string provider
|
|
boolean email_verified
|
|
timestamp created_at
|
|
timestamp updated_at
|
|
timestamp last_login_at
|
|
}
|
|
|
|
decks {
|
|
uuid id PK
|
|
uuid user_id FK
|
|
string name
|
|
text description
|
|
string cover_image
|
|
boolean is_public
|
|
int flashcard_count
|
|
timestamp created_at
|
|
timestamp updated_at
|
|
}
|
|
|
|
flashcards {
|
|
uuid id PK
|
|
uuid deck_id FK
|
|
uuid user_id FK
|
|
string word
|
|
string translation
|
|
text definition
|
|
string part_of_speech
|
|
string ipa_pronunciation
|
|
text example_sentence
|
|
text example_translation
|
|
string difficulty_level
|
|
text memory_tip
|
|
string image_url
|
|
string audio_url
|
|
jsonb metadata
|
|
timestamp created_at
|
|
timestamp updated_at
|
|
}
|
|
|
|
learning_records {
|
|
uuid id PK
|
|
uuid user_id FK
|
|
uuid flashcard_id FK
|
|
int rating
|
|
float ease_factor
|
|
int interval_days
|
|
timestamp reviewed_at
|
|
int time_spent_seconds
|
|
boolean is_correct
|
|
}
|
|
|
|
tags {
|
|
uuid id PK
|
|
string name UK
|
|
string color
|
|
timestamp created_at
|
|
}
|
|
```
|
|
|
|
## 2. TypeScript 資料模型定義
|
|
|
|
### 2.1 用戶相關模型
|
|
|
|
```typescript
|
|
// types/user.ts
|
|
|
|
export interface User {
|
|
id: string;
|
|
email: string;
|
|
username: string;
|
|
avatarUrl?: string;
|
|
provider: 'email' | 'google';
|
|
emailVerified: boolean;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
lastLoginAt?: Date;
|
|
}
|
|
|
|
export interface UserProfile extends User {
|
|
stats: UserStats;
|
|
preferences: UserPreferences;
|
|
}
|
|
|
|
export interface UserStats {
|
|
id: string;
|
|
userId: string;
|
|
totalFlashcards: number;
|
|
totalDecks: number;
|
|
studyStreak: number;
|
|
totalStudyTime: number; // 分鐘
|
|
cardsStudiedToday: number;
|
|
cardsToReview: number;
|
|
averageAccuracy: number; // 0-100
|
|
level: number;
|
|
experience: number;
|
|
lastStudyDate?: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
export interface UserPreferences {
|
|
id: string;
|
|
userId: string;
|
|
dailyGoal: number; // 每日目標詞數
|
|
reminderTime?: string; // "HH:mm"
|
|
reminderEnabled: boolean;
|
|
soundEnabled: boolean;
|
|
theme: 'light' | 'dark' | 'system';
|
|
language: 'zh-TW' | 'en';
|
|
studyMode: 'normal' | 'speed' | 'hard';
|
|
autoPlayAudio: boolean;
|
|
showPronunciation: boolean;
|
|
}
|
|
|
|
export interface Session {
|
|
id: string;
|
|
userId: string;
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
expiresAt: Date;
|
|
createdAt: Date;
|
|
}
|
|
```
|
|
|
|
### 2.2 詞卡相關模型
|
|
|
|
```typescript
|
|
// types/flashcard.ts
|
|
|
|
export interface Deck {
|
|
id: string;
|
|
userId: string;
|
|
name: string;
|
|
description?: string;
|
|
coverImage?: string;
|
|
isPublic: boolean;
|
|
flashcardCount: number;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
// 關聯資料
|
|
flashcards?: Flashcard[];
|
|
tags?: Tag[];
|
|
}
|
|
|
|
export interface Flashcard {
|
|
id: string;
|
|
deckId: string;
|
|
userId: string;
|
|
// 核心內容
|
|
word: string;
|
|
translation: string;
|
|
definition?: string;
|
|
partOfSpeech?: PartOfSpeech;
|
|
ipaPronunciation?: string;
|
|
// 例句
|
|
exampleSentence?: string;
|
|
exampleTranslation?: string;
|
|
// 學習輔助
|
|
difficultyLevel: DifficultyLevel;
|
|
memoryTip?: string;
|
|
synonyms?: string[];
|
|
antonyms?: string[];
|
|
// 媒體
|
|
imageUrl?: string;
|
|
audioUrl?: string;
|
|
// 元資料
|
|
metadata?: FlashcardMetadata;
|
|
tags?: Tag[];
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
// 學習狀態
|
|
learningStatus?: LearningStatus;
|
|
}
|
|
|
|
export interface FlashcardMetadata {
|
|
source?: 'ai' | 'manual' | 'import';
|
|
sourceText?: string;
|
|
aiModel?: string;
|
|
contextSentence?: string;
|
|
usageNotes?: string;
|
|
culturalNotes?: string;
|
|
frequency?: 'common' | 'uncommon' | 'rare';
|
|
formality?: 'formal' | 'informal' | 'neutral';
|
|
}
|
|
|
|
export type PartOfSpeech =
|
|
| 'noun'
|
|
| 'verb'
|
|
| 'adjective'
|
|
| 'adverb'
|
|
| 'pronoun'
|
|
| 'preposition'
|
|
| 'conjunction'
|
|
| 'interjection'
|
|
| 'phrase'
|
|
| 'idiom';
|
|
|
|
export type DifficultyLevel = 'beginner' | 'intermediate' | 'advanced';
|
|
|
|
export interface Tag {
|
|
id: string;
|
|
name: string;
|
|
color: string;
|
|
createdAt: Date;
|
|
}
|
|
|
|
export interface FlashcardTag {
|
|
flashcardId: string;
|
|
tagId: string;
|
|
createdAt: Date;
|
|
}
|
|
```
|
|
|
|
### 2.3 學習記錄模型
|
|
|
|
```typescript
|
|
// types/learning.ts
|
|
|
|
export interface LearningRecord {
|
|
id: string;
|
|
userId: string;
|
|
flashcardId: string;
|
|
rating: 1 | 2 | 3 | 4 | 5; // SM-2 評分
|
|
easeFactor: number; // 難度係數 (1.3 - 2.5)
|
|
intervalDays: number; // 下次複習間隔
|
|
reviewedAt: Date;
|
|
timeSpentSeconds: number;
|
|
isCorrect: boolean;
|
|
}
|
|
|
|
export interface LearningStatus {
|
|
flashcardId: string;
|
|
userId: string;
|
|
status: 'new' | 'learning' | 'review' | 'mastered';
|
|
easeFactor: number;
|
|
interval: number;
|
|
repetitions: number;
|
|
nextReviewDate: Date;
|
|
lastReviewDate?: Date;
|
|
totalReviews: number;
|
|
correctReviews: number;
|
|
accuracy: number; // 0-100
|
|
}
|
|
|
|
export interface LearningSession {
|
|
id: string;
|
|
userId: string;
|
|
deckId?: string;
|
|
startedAt: Date;
|
|
endedAt?: Date;
|
|
cardsStudied: number;
|
|
correctAnswers: number;
|
|
mode: 'flashcard' | 'quiz' | 'typing' | 'listening';
|
|
completed: boolean;
|
|
}
|
|
|
|
export interface DailyProgress {
|
|
userId: string;
|
|
date: string; // YYYY-MM-DD
|
|
cardsStudied: number;
|
|
newCards: number;
|
|
reviewCards: number;
|
|
studyTimeMinutes: number;
|
|
accuracy: number;
|
|
streakDays: number;
|
|
}
|
|
```
|
|
|
|
### 2.4 AI 生成相關模型
|
|
|
|
```typescript
|
|
// types/generation.ts
|
|
|
|
export interface GenerationRequest {
|
|
id: string;
|
|
userId: string;
|
|
inputText?: string;
|
|
theme?: string;
|
|
count: number;
|
|
difficulty: DifficultyLevel;
|
|
includeExamples: boolean;
|
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
result?: GenerationResult;
|
|
error?: string;
|
|
createdAt: Date;
|
|
completedAt?: Date;
|
|
}
|
|
|
|
export interface GenerationResult {
|
|
flashcards: GeneratedFlashcard[];
|
|
tokensUsed: number;
|
|
processingTime: number; // 毫秒
|
|
}
|
|
|
|
export interface GeneratedFlashcard {
|
|
word: string;
|
|
translation: string;
|
|
definition: string;
|
|
partOfSpeech: PartOfSpeech;
|
|
pronunciation: string;
|
|
example: string;
|
|
exampleTranslation: string;
|
|
difficulty: DifficultyLevel;
|
|
memoryTip?: string;
|
|
synonyms?: string[];
|
|
antonyms?: string[];
|
|
contextFromSource?: string;
|
|
}
|
|
```
|
|
|
|
### 2.5 統計與成就模型
|
|
|
|
```typescript
|
|
// types/statistics.ts
|
|
|
|
export interface Statistics {
|
|
userId: string;
|
|
period: 'daily' | 'weekly' | 'monthly' | 'yearly' | 'all-time';
|
|
periodStart: Date;
|
|
periodEnd: Date;
|
|
totalCards: number;
|
|
newCards: number;
|
|
reviewedCards: number;
|
|
masteredCards: number;
|
|
studyTimeMinutes: number;
|
|
averageAccuracy: number;
|
|
bestStreak: number;
|
|
studySessions: number;
|
|
}
|
|
|
|
export interface Achievement {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
icon: string;
|
|
category: 'milestone' | 'streak' | 'mastery' | 'special';
|
|
requirement: AchievementRequirement;
|
|
points: number;
|
|
}
|
|
|
|
export interface AchievementRequirement {
|
|
type: 'cards_studied' | 'streak_days' | 'accuracy' | 'cards_mastered';
|
|
value: number;
|
|
period?: 'daily' | 'weekly' | 'total';
|
|
}
|
|
|
|
export interface UserAchievement {
|
|
userId: string;
|
|
achievementId: string;
|
|
unlockedAt: Date;
|
|
progress: number; // 0-100
|
|
}
|
|
```
|
|
|
|
## 3. API 請求/響應模型
|
|
|
|
### 3.1 認證相關
|
|
|
|
```typescript
|
|
// types/api/auth.ts
|
|
|
|
export interface RegisterRequest {
|
|
email: string;
|
|
password: string;
|
|
username: string;
|
|
}
|
|
|
|
export interface LoginRequest {
|
|
email: string;
|
|
password: string;
|
|
rememberMe?: boolean;
|
|
}
|
|
|
|
export interface AuthResponse {
|
|
user: User;
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
expiresIn: number;
|
|
}
|
|
|
|
export interface RefreshTokenRequest {
|
|
refreshToken: string;
|
|
}
|
|
|
|
export interface ResetPasswordRequest {
|
|
email: string;
|
|
}
|
|
|
|
export interface ChangePasswordRequest {
|
|
currentPassword: string;
|
|
newPassword: string;
|
|
}
|
|
```
|
|
|
|
### 3.2 詞卡操作
|
|
|
|
```typescript
|
|
// types/api/flashcard.ts
|
|
|
|
export interface CreateFlashcardRequest {
|
|
deckId: string;
|
|
word: string;
|
|
translation: string;
|
|
definition?: string;
|
|
exampleSentence?: string;
|
|
difficulty?: DifficultyLevel;
|
|
tags?: string[];
|
|
}
|
|
|
|
export interface UpdateFlashcardRequest {
|
|
word?: string;
|
|
translation?: string;
|
|
definition?: string;
|
|
exampleSentence?: string;
|
|
difficulty?: DifficultyLevel;
|
|
tags?: string[];
|
|
}
|
|
|
|
export interface GenerateFlashcardsRequest {
|
|
text?: string;
|
|
theme?: string;
|
|
count: number;
|
|
difficulty: DifficultyLevel;
|
|
includeExamples: boolean;
|
|
targetDeckId?: string;
|
|
}
|
|
|
|
export interface BulkOperationRequest {
|
|
flashcardIds: string[];
|
|
operation: 'delete' | 'move' | 'tag' | 'reset';
|
|
targetDeckId?: string;
|
|
tags?: string[];
|
|
}
|
|
```
|
|
|
|
### 3.3 學習相關
|
|
|
|
```typescript
|
|
// types/api/learning.ts
|
|
|
|
export interface StartSessionRequest {
|
|
deckId?: string;
|
|
mode: 'flashcard' | 'quiz' | 'typing' | 'listening';
|
|
cardLimit?: number;
|
|
}
|
|
|
|
export interface SubmitReviewRequest {
|
|
flashcardId: string;
|
|
rating: 1 | 2 | 3 | 4 | 5;
|
|
timeSpent: number;
|
|
isCorrect: boolean;
|
|
}
|
|
|
|
export interface GetCardsToReviewResponse {
|
|
cards: Flashcard[];
|
|
newCards: number;
|
|
reviewCards: number;
|
|
totalCards: number;
|
|
}
|
|
```
|
|
|
|
## 4. 資料驗證 Schema
|
|
|
|
### 4.1 Zod 驗證模式
|
|
|
|
```typescript
|
|
// schemas/validation.ts
|
|
import { z } from 'zod';
|
|
|
|
// 用戶驗證
|
|
export const userSchema = z.object({
|
|
email: z.string().email('Invalid email format'),
|
|
username: z.string()
|
|
.min(3, 'Username must be at least 3 characters')
|
|
.max(20, 'Username must be at most 20 characters')
|
|
.regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),
|
|
password: z.string()
|
|
.min(8, 'Password must be at least 8 characters')
|
|
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
|
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
|
.regex(/[0-9]/, 'Password must contain at least one number')
|
|
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
|
|
});
|
|
|
|
// 詞卡驗證
|
|
export const flashcardSchema = z.object({
|
|
word: z.string()
|
|
.min(1, 'Word is required')
|
|
.max(100, 'Word is too long'),
|
|
translation: z.string()
|
|
.min(1, 'Translation is required')
|
|
.max(200, 'Translation is too long'),
|
|
definition: z.string()
|
|
.max(500, 'Definition is too long')
|
|
.optional(),
|
|
exampleSentence: z.string()
|
|
.max(500, 'Example sentence is too long')
|
|
.optional(),
|
|
difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
|
|
tags: z.array(z.string()).max(10, 'Maximum 10 tags allowed').optional(),
|
|
});
|
|
|
|
// 卡組驗證
|
|
export const deckSchema = z.object({
|
|
name: z.string()
|
|
.min(1, 'Deck name is required')
|
|
.max(50, 'Deck name is too long'),
|
|
description: z.string()
|
|
.max(200, 'Description is too long')
|
|
.optional(),
|
|
isPublic: z.boolean().default(false),
|
|
});
|
|
|
|
// 生成請求驗證
|
|
export const generateRequestSchema = z.object({
|
|
text: z.string()
|
|
.max(5000, 'Text is too long')
|
|
.optional(),
|
|
theme: z.string()
|
|
.max(50, 'Theme is too long')
|
|
.optional(),
|
|
count: z.number()
|
|
.min(1, 'At least 1 card required')
|
|
.max(20, 'Maximum 20 cards allowed'),
|
|
difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
|
|
includeExamples: z.boolean().default(true),
|
|
});
|
|
```
|
|
|
|
## 5. 資料庫索引策略
|
|
|
|
```sql
|
|
-- 用戶相關索引
|
|
CREATE INDEX idx_users_email ON users(email);
|
|
CREATE INDEX idx_users_username ON users(username);
|
|
CREATE INDEX idx_users_provider ON users(provider);
|
|
|
|
-- 詞卡相關索引
|
|
CREATE INDEX idx_flashcards_user_id ON flashcards(user_id);
|
|
CREATE INDEX idx_flashcards_deck_id ON flashcards(deck_id);
|
|
CREATE INDEX idx_flashcards_word ON flashcards(word);
|
|
CREATE INDEX idx_flashcards_difficulty ON flashcards(difficulty_level);
|
|
CREATE INDEX idx_flashcards_created_at ON flashcards(created_at DESC);
|
|
|
|
-- 學習記錄索引
|
|
CREATE INDEX idx_learning_records_user_flashcard ON learning_records(user_id, flashcard_id);
|
|
CREATE INDEX idx_learning_records_reviewed_at ON learning_records(reviewed_at DESC);
|
|
CREATE INDEX idx_learning_status_next_review ON learning_status(user_id, next_review_date);
|
|
|
|
-- 全文搜尋索引
|
|
CREATE INDEX idx_flashcards_fulltext ON flashcards
|
|
USING GIN(to_tsvector('english', word || ' ' || translation || ' ' || COALESCE(example_sentence, '')));
|
|
```
|
|
|
|
## 6. 資料遷移策略
|
|
|
|
### 6.1 版本控制
|
|
- 使用 Supabase Migrations
|
|
- 每個遷移檔案都有時間戳記
|
|
- 保持向後相容性
|
|
|
|
### 6.2 遷移檔案範例
|
|
|
|
```sql
|
|
-- migrations/20240315000001_create_users_table.sql
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
username VARCHAR(50) UNIQUE NOT NULL,
|
|
password_hash VARCHAR(255),
|
|
avatar_url TEXT,
|
|
provider VARCHAR(20) DEFAULT 'email',
|
|
email_verified BOOLEAN DEFAULT false,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
last_login_at TIMESTAMP WITH TIME ZONE
|
|
);
|
|
|
|
-- Enable RLS
|
|
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Policies
|
|
CREATE POLICY "Users can view own profile"
|
|
ON users FOR SELECT
|
|
USING (auth.uid() = id);
|
|
|
|
CREATE POLICY "Users can update own profile"
|
|
ON users FOR UPDATE
|
|
USING (auth.uid() = id);
|
|
```
|
|
|
|
## 7. 資料安全考量
|
|
|
|
### 7.1 敏感資料處理
|
|
- 密碼使用 bcrypt 加密
|
|
- 不存儲原始密碼
|
|
- Token 設置過期時間
|
|
- 敏感操作需要重新驗證
|
|
|
|
### 7.2 資料權限
|
|
- 使用 Row Level Security (RLS)
|
|
- 用戶只能存取自己的資料
|
|
- 公開卡組需要明確標記
|
|
- API 層級再次驗證權限
|
|
|
|
### 7.3 資料備份
|
|
- 每日自動備份
|
|
- 保留 30 天備份
|
|
- 異地備份存儲
|
|
- 定期恢復測試 |