# 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 天備份 - 異地備份存儲 - 定期恢復測試