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