-- ============================================ -- PastPaper Master — 初始数据库 Schema -- Version: 001 -- Date: 2025-03-11 -- ============================================ -- 启用必要的扩展 CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ============================================ -- Table 1: papers — 上传的试卷 -- ============================================ CREATE TABLE papers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, -- 元信息(用户上传时填写) course_code TEXT NOT NULL, -- "COMP2011" year INTEGER NOT NULL, -- 2024 term TEXT NOT NULL CHECK (term IN ('fall', 'spring', 'summer')), exam_type TEXT NOT NULL CHECK (exam_type IN ('midterm', 'final', 'quiz')), -- 文件 (Supabase Storage) paper_file_url TEXT NOT NULL, -- 试卷 PDF answer_file_url TEXT, -- 答案 PDF(可选) -- 处理状态 status TEXT NOT NULL DEFAULT 'uploaded' CHECK (status IN ('uploaded', 'processing', 'ready', 'error')), error_message TEXT, -- 处理失败时的错误信息 -- 提取的原始文本(缓存) paper_extracted_text TEXT, answer_extracted_text TEXT, -- 整卷概览(AI 生成) total_score INTEGER, question_count INTEGER, topics_summary JSONB, -- {"Linked List": 40, "Recursion": 30} difficulty_level TEXT CHECK (difficulty_level IN ('easy', 'medium', 'hard')), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================ -- Table 2: paper_questions — 逐题数据 -- ============================================ CREATE TABLE paper_questions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), paper_id UUID NOT NULL REFERENCES papers(id) ON DELETE CASCADE, -- 题目标识 question_number TEXT NOT NULL, -- "1", "1a", "2b" parent_question TEXT, -- 子题的父题号: "1a" → "1" display_order INTEGER NOT NULL, -- 显示顺序 -- 题目内容 question_type TEXT NOT NULL CHECK (question_type IN ('mc', 'fill_blank', 'long_question')), question_text TEXT NOT NULL, -- 题目原文 score INTEGER, -- 分值 page_number INTEGER, -- PDF 页码(左右联动) -- 选择题专用 options JSONB, -- [{"label":"A","text":"..."},...] correct_option TEXT, -- "B" -- 填空题专用 correct_answer TEXT, -- 正确答案 accept_variants TEXT[], -- 等价表达 ["O(nlogn)","O(n log n)"] -- 答案 PDF 提取的原始答案(所有题型) raw_answer_text TEXT, -- 知识点标签 topics TEXT[], -- ["Linked List","Pointer"] difficulty TEXT CHECK (difficulty IN ('easy', 'medium', 'hard')), -- AI 三件套(HTML + KaTeX) knowledge_reminder TEXT, -- 知识点 Reminder ai_hint TEXT, -- AI Hint solution TEXT, -- Solution(逐步 derivation) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================ -- Table 3: user_attempts — 用户答题记录 -- Phase 4 实现,先建好表结构 -- ============================================ CREATE TABLE user_attempts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, question_id UUID NOT NULL REFERENCES paper_questions(id) ON DELETE CASCADE, -- 用户的作答 attempt_type TEXT NOT NULL CHECK (attempt_type IN ('select', 'input', 'photo')), user_answer TEXT, -- 选项 / 输入的答案 photo_url TEXT, -- 上传的照片 photo_ocr_text TEXT, -- OCR 识别结果 -- AI 判定 is_correct BOOLEAN, feedback TEXT, -- HTML — 逐步错误分析 error_at_step INTEGER, -- 第几步开始错 -- 错题本 in_error_book BOOLEAN NOT NULL DEFAULT false, mastered BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================ -- 索引 -- ============================================ CREATE INDEX idx_papers_user ON papers(user_id); CREATE INDEX idx_papers_course ON papers(course_code); CREATE INDEX idx_papers_status ON papers(status); CREATE INDEX idx_questions_paper ON paper_questions(paper_id); CREATE INDEX idx_questions_type ON paper_questions(question_type); CREATE INDEX idx_questions_topics ON paper_questions USING GIN(topics); CREATE INDEX idx_attempts_user ON user_attempts(user_id); CREATE INDEX idx_attempts_question ON user_attempts(question_id); CREATE INDEX idx_attempts_errorbook ON user_attempts(user_id) WHERE in_error_book = true; -- ============================================ -- RLS 策略 -- ============================================ ALTER TABLE papers ENABLE ROW LEVEL SECURITY; ALTER TABLE paper_questions ENABLE ROW LEVEL SECURITY; ALTER TABLE user_attempts ENABLE ROW LEVEL SECURITY; -- papers: 用户只能看自己上传的(以后加公共库时再调整) CREATE POLICY "Users can view own papers" ON papers FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Users can insert own papers" ON papers FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can update own papers" ON papers FOR UPDATE USING (auth.uid() = user_id); CREATE POLICY "Users can delete own papers" ON papers FOR DELETE USING (auth.uid() = user_id); -- paper_questions: 跟随 paper 的权限 CREATE POLICY "Users can view questions of own papers" ON paper_questions FOR SELECT USING ( EXISTS ( SELECT 1 FROM papers WHERE papers.id = paper_questions.paper_id AND papers.user_id = auth.uid() ) ); -- service_role 用于后端写入 questions(处理管线用) -- 前端不直接写 questions,通过 API 触发后端处理 -- user_attempts: 用户只能看/写自己的 CREATE POLICY "Users can view own attempts" ON user_attempts FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Users can insert own attempts" ON user_attempts FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can update own attempts" ON user_attempts FOR UPDATE USING (auth.uid() = user_id); -- ============================================ -- updated_at 自动更新触发器 -- ============================================ CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER papers_updated_at BEFORE UPDATE ON papers FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER questions_updated_at BEFORE UPDATE ON paper_questions FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- ============================================ -- Storage bucket -- ============================================ -- 在 Supabase Dashboard 中手动创建 bucket: "papers" -- 或通过 API 创建(后端初始化时处理)