dramaling-vocab-learning/docs/03_development/data-models.md

14 KiB

DramaLing 資料模型文檔

1. 資料庫架構圖

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 用戶相關模型

// 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 詞卡相關模型

// 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 學習記錄模型

// 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 生成相關模型

// 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 統計與成就模型

// 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 認證相關

// 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 詞卡操作

// 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 學習相關

// 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 驗證模式

// 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. 資料庫索引策略

-- 用戶相關索引
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 遷移檔案範例

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