# PastPaper Master — 技术文档
## 系统架构总览
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (React 19 + Vite 7) │
│ Pages: Home / Upload / Workbench / ErrorBook │
│ PDF: react-pdf v10 | Math: KaTeX 0.16 | Style: Tailwind v4 │
└────────────────────────────┬────────────────────────────────────┘
│ /api (Vite proxy → :8000)
┌────────────────────────────▼────────────────────────────────────┐
│ Backend (FastAPI + Python) │
│ Routers: papers / attempts / questions │
│ Services: paper_processor / grader / llm_clients / text_extractor│
└────────┬───────────────────┬──────────────────┬─────────────────┘
│ │ │
┌─────▼─────┐ ┌────────▼───────┐ ┌───────▼──────┐
│ Supabase │ │ GPT-4o │ │ Qwen-plus │
│ PostgreSQL │ │ (laozhang API) │ │ (DashScope) │
│ + Storage │ │ 结构化/OCR/变体 │ │ AI三件套/判分 │
└───────────┘ └────────────────┘ └──────────────┘
```
**技术栈一览:**
- **Frontend**: React 19, TypeScript, Vite 7, Tailwind CSS v4, react-pdf v10, KaTeX 0.16
- **Backend**: FastAPI, Python 3.12, uv (包管理)
- **Database**: Supabase (PostgreSQL + Row Level Security)
- **Storage**: Supabase Storage (buckets: `papers`, `attempt-photos`)
- **LLM**: GPT-4o (laozhang API 代理), Qwen-plus (阿里 DashScope)
---
## 数据库 Schema
> 文件: `supabase/migrations/001_init_schema.sql`
### Table: `papers` — 试卷
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | 自动生成 |
| user_id | UUID FK → auth.users | 上传者 |
| course_code | TEXT | 课程代码, e.g. "COMP2011" |
| year / term / exam_type | INT/TEXT/TEXT | 元信息 |
| paper_file_url | TEXT | 试卷 PDF (Supabase Storage) |
| answer_file_url | TEXT? | 答案 PDF (可选) |
| status | TEXT | `uploaded` → `processing` → `ready` / `error` |
| paper_extracted_text | TEXT | PyMuPDF 提取的原始文本 (缓存) |
| total_score / question_count | INT | AI 提取的整卷概览 |
| topics_summary | JSONB | `{"Linked List": 40, "Recursion": 30}` |
| difficulty_level | TEXT | easy / medium / hard |
### Table: `paper_questions` — 逐题数据
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | |
| paper_id | UUID FK → papers | |
| question_number | TEXT | "1", "1a", "2b" |
| parent_question | TEXT? | 子题父题号: "1a" → "1" |
| display_order | INT | 排序 |
| question_type | TEXT | `mc` / `true_false` / `fill_blank` / `long_question` |
| question_text | TEXT | 题目原文 |
| score / page_number | INT | 分值, PDF 页码 (PDF-题目联动用) |
| options | JSONB | MC 选项: `[{"label":"A","text":"..."}]` |
| correct_option | TEXT | MC 正确选项 |
| correct_answer | TEXT | 填空题正确答案 |
| raw_answer_text | TEXT | 答案 PDF 原始解�� |
| topics | TEXT[] | 知识点标签 |
| difficulty | TEXT | easy / medium / hard |
| knowledge_reminder | TEXT | AI 知识点提醒 (HTML+KaTeX) |
| ai_hint | TEXT | AI 思路提示 (HTML+KaTeX) |
| solution | TEXT | AI 完整解题过程 (HTML+KaTeX) |
### Table: `user_attempts` — 用户答题记录
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | |
| user_id / question_id | UUID FK | |
| attempt_type | TEXT | `select` / `input` / `photo` |
| user_answer | TEXT | 用户的选项或输入 |
| photo_url / photo_ocr_text | TEXT | 拍照上传的图片和 OCR 结果 |
| is_correct | BOOL | AI 判定 |
| feedback | TEXT | HTML 逐步错误分析 |
| error_at_step | INT | 第几步出错 |
| in_error_book / mastered | BOOL | 错题本状态 |
---
## 核心功能一:试卷分析管线
### 流程概述
```
用户上传 PDF → 后台 BackgroundTask → 5 步管线 → 状态变 ready
```
### 文件
| 文件 | 作用 |
|------|------|
| `backend/app/routers/papers.py` | 上传接口, 触发后台处理 |
| `backend/app/services/paper_processor.py` | **核心管线**, 5 步处理逻辑 |
| `backend/app/services/text_extractor.py` | PDF → 文本提取 (PyMuPDF) |
| `backend/app/services/llm_clients.py` | GPT-4o / Qwen 客户端单例 |
### 管线 5 步 (`paper_processor.py: process_paper()`)
**Step 1 — PDF 文本提取**
- 使用 PyMuPDF (`fitz`) 逐页提取文本
- 如果某页文本 < 50 字符 (可能是扫描件), 额外保存该页为 base64 图片备用
- 提取结果缓存到 `papers.paper_extracted_text`
```python
# text_extractor.py
extract_pdf(file_bytes) → ExtractedContent(pages_text, page_images, total_pages, has_images)
get_full_text(extracted) → "--- Page 1 ---\n{text}\n\n--- Page 2 ---\n..."
```
**Step 2 — GPT-4o 结构化拆题**
- Model: `gpt-4o`, temperature=0, response_format=json_object
- 输入: 整卷文本
- 输出: JSON 包含 total_score, difficulty_level, topics_summary, questions[]
- 每题提取: question_number, parent_question, question_type, question_text, score, page_number, options, topics, difficulty
- 更新 `papers` 表的概览字段 (total_score, question_count, topics_summary, difficulty_level)
**Step 3 — 答案匹配 (如果有答案 PDF)**
- Model: `gpt-4o`, temperature=0
- 输入: 题目结构 JSON + 答案文本
- 输出: 逐题匹配 — correct_option / correct_answer / raw_answer_text
- 选择题 → correct_option, 填空题 → correct_answer, 大题 → raw_answer_text
**Step 4 — Qwen 生成 AI 三件套 (逐题)**
- Model: `qwen-plus`, temperature=0.3
- 逐题调用, 输入题目信息 + 标准答案
- 输出 JSON 三件套:
- `knowledge_reminder`: 前置知识要点 (HTML+KaTeX)
- `ai_hint`: 不给答案的思路引导 (HTML+KaTeX)
- `solution`: 完整逐步解题过程 (HTML+KaTeX)
- 写入 `paper_questions` 表
**Step 5 — 标记完成**
- `papers.status` 更新为 `ready`
- 如果任何步骤抛异常, status 设为 `error`, 错误信息写入 `error_message`
### 关键 Prompt 设计
**STRUCTURE_PROMPT** — 结构化拆题
- 限定 question_type 只能是 mc / true_false / fill_blank / long_question
- 判断题 (True/False) 用 `true_false` 类型,options 为 `[{label:"True",text:"True"},{label:"False",text:"False"}]`
- 选择题必须提取 options 数组
- 子题通过 parent_question 关联 (e.g. "1a" parent 是 "1")
- 要求推断 page_number, topics, difficulty
**ANSWER_MATCH_PROMPT** — 答案匹配
- 输入包含 questions_json (题号+题型) 和 answer_text
- 按题型输出不同字段: MC → correct_option, fill → correct_answer, 大题 → raw_answer_text
**ANALYSIS_PROMPT** — AI 三件套
- Solution 要求带完整过程 (Step 1, 2, 3...), 不能只给答案
- 选择题要解释为什么对、为什么其他选项错
- 标注常见错误: `
...
`
- KaTeX 规则: 块级 `$$...$$`, 行内 `$...$`
---
## 核心功能二:PDF 滚动 + 题目联动
### 文件
| 文件 | 作用 |
|------|------|
| `frontend/src/components/workbench/PdfViewer.tsx` | PDF 连续滚动渲染 + 可见页检测 |
| `frontend/src/components/workbench/QuestionNav.tsx` | 题目水平导航栏 |
| `frontend/src/pages/WorkbenchPage.tsx` | 双向联动调度中枢 |
### 实现方案
**布局**: 左侧 60% PDF, 右侧 40% 题目面板
**PDF 连续滚动 (`PdfViewer.tsx`)**
- 使用 `react-pdf` 的 `` + `` 组件
- 所有页面垂直排列在可滚动容器中 (不是单页切换)
- `ResizeObserver` 监听容器宽度, 动态设置 Page width
- 手动跳转: 输入页码 → `scrollIntoView`
**双向联动:**
1. **题目 → PDF (点击题目, PDF 滚动到对应页)**
- QuestionNav 点击 → `handleQuestionSelect(index)` → 记录 `lastUserSelectTime = Date.now()` + `setCurrentIndex`
- PdfViewer 收到 `currentPage` prop 变化 → `useEffect` 触发 `el.scrollIntoView({ behavior: "smooth" })`
- 设置 `programmaticScroll.current = true`, 2s 后重置
2. **PDF → 题目 (滚动 PDF, 右侧自动切换到当前题)**
- `IntersectionObserver` 监听所有 `` 元素, threshold: `[0, 0.25, 0.5, 0.75, 1]`
- 追踪每页的 `intersectionRatio`, 选出可见占比最高的页码
- 如果 `programmaticScroll.current === true`, 跳过回调
- 触发 `onPageChange(bestPage)` → WorkbenchPage `handlePdfPageChange`
- `handlePdfPageChange`: 找到 `page_number <= currentPage` 的最后一题, 更新 `currentIndex`
**防止跳转抢夺 (双层保护):**
- **WorkbenchPage 层 (核心)**: `lastUserSelectTime` ref — 用户点击题目后 2 秒内, `handlePdfPageChange` 直接 return, 不响应任何 Observer 回调。解决长文档 smooth scroll 经过中间页触发 Observer 导致题目被切走的问题
- **PdfViewer 层 (辅助)**: `programmaticScroll` ref — scrollIntoView 期间 Observer 回调跳过, 2s 后重置
---
## 核心功能三:做题交互 (MC / 填空)
### 文件
| 文件 | 作用 |
|------|------|
| `frontend/src/components/workbench/QuestionDetail.tsx` | 题目展示 + 答题交互 |
| `frontend/src/components/workbench/AiTrioPanel.tsx` | 知��点/提示/解析 折叠面板 |
| `frontend/src/components/shared/CollapsibleSection.tsx` | 可折叠区域组件 |
| `frontend/src/components/shared/KaTeXRenderer.tsx` | HTML+KaTeX 渲染器 |
### QuestionDetail 交互逻辑
**选择题 (MC):**
- 状态: `selectedOption`, `checked`
- 点击选项 → 高亮蓝色 (未检查时)
- 点击 "Check Answer" → `checked=true`
- 正确: 选项变绿 + "Correct!" / 错误: 选中项变红, 正确项变绿 + 显示正确答案
- 切换题目时自动重置状态 (`useEffect` on `question.id`)
**判断题 (True/False):**
- 状态: `tfAnswers: Record`, `tfChecked`
- 每个 statement 右侧有 T / F 两个按钮, 独立切换
- 选中高亮蓝色, 全部选完后可点 "Submit Answers"
- 提交后提示查看 solution 对答案 (因为逐条正确答案暂未单独存储)
**填空题 (Fill Blank):**
- 文本输入框 + "Check" 按钮
- Enter 键可直接检查
- 大小写不敏感比较 (`toLowerCase()`)
- 检查后输入框变色: 绿色 (对) / 红色 (错)
**回调**: `onAnswerResult(isCorrect, userAnswer)` → WorkbenchPage → `recordAttempt` API
### AiTrioPanel
- 三个 `CollapsibleSection`: Knowledge Reminder (蓝, 默认展开), AI Hint (琥珀), Solution (绿)
- `CollapsibleSection` 使用 CSS `grid-template-rows: 0fr → 1fr` 动画平滑展开收起
- 内容通过 `KaTeXRenderer` 渲染 (HTML + KaTeX 公式)
---
## 核心功能四:变体题生成 (Similar Question)
### 文件
| 文件 | 作用 |
|------|------|
| `backend/app/routers/questions.py` | `POST /{question_id}/variant` 端点 |
| `backend/app/services/grader.py` | `generate_variant()` — GPT-4o 生成变体 |
| `frontend/src/components/workbench/ActionBar.tsx` | "Similar Question" 按钮, 异步触发 |
| `frontend/src/pages/WorkbenchPage.tsx` | Variants Tab 状态管理 |
| `frontend/src/components/workbench/VariantDetail.tsx` | 变体题作答界面 |
### 后端
- `POST /api/questions/{question_id}/variant`
- 从 DB 查原题 → 调 `generate_variant(question)` → 附上原题的 `knowledge_reminder` → 返回
- Model: `gpt-4o`, temperature=0.5, response_format=json_object
- VARIANT_PROMPT 要求: 同知识点, 相似难度, 不同数据/场景, 输出 HTML 格式 (非 markdown)
- 输出字段: question_text, question_type, options (if MC), correct_answer, ai_hint, solution
### 前端交互 (Tab-based 异步流程)
**状态管理 (`WorkbenchPage.tsx`):**
```typescript
interface StoredVariant {
id: string; // placeholder ID, e.g. "variant-1"
sourceQuestionNumber: string; // 原题题号
variant: VariantQuestion; // 生成结果
status: "generating" | "ready";
}
```
**流程:**
1. 用户点击 "Similar Question" → `ActionBar` 调 `onVariantStart(placeholderId, questionNumber)`
2. WorkbenchPage 创建 `status: "generating"` 的占位项, 用户可继续做题不受阻塞
3. API 返回后 → `onVariantReady(placeholderId, variant)` → 状态更新为 `ready`
4. 失败 → `onVariantFailed(placeholderId)` → 删除占位项
**右侧面板三种视图:**
- **Questions Tab**: 题目导航 + QuestionDetail + AiTrioPanel + ActionBar
- **Variants Tab**: 变体列表 (Generating.../Ready), 每项显示题号和预览文本
- **Variant Detail**: 点击 "Start" 后整个右侧替换为 VariantDetail 组件 + "Back" 按钮
**VariantDetail 组件**: 紫色主题, 包含完整 MC/填空交互 + AI 三件套 (CollapsibleSection)
---
## 核心功能五:拍照批改
### 文件
| 文件 | 作用 |
|------|------|
| `backend/app/routers/attempts.py` | `POST /photo` — 上传+OCR+批改 |
| `backend/app/services/grader.py` | `ocr_photo()` + `grade_answer()` |
| `frontend/src/components/workbench/PhotoUpload.tsx` | 拍照上传 Modal |
| `frontend/src/components/workbench/ActionBar.tsx` | "Upload handwritten answer" 按钮 |
### 后端流程
1. 接收图片 → 上传到 Supabase Storage `attempt-photos` bucket
2. `ocr_photo(photo_bytes)` — GPT-4o Vision 识别手写内容
- 输入: base64 图片
- 输出: 学生答案文本 (含 LaTeX 公式)
3. `grade_answer(question, student_answer)` — Qwen-plus 批改
- 输入: 题目信息 + 标准答案 + 学生答案
- 输出: `{ is_correct, score_given, feedback (HTML), error_at_step }`
4. 写入 `user_attempts` 表 (含 photo_url, photo_ocr_text, feedback, is_correct)
5. 答错自动 `in_error_book = true`
### 前端
- PhotoUpload: Modal 弹窗, 支持拖拽/点击选择图片
- 预览 → 提交 → 显示 OCR 识别结果 + AI 批改反馈
- 所有题型均可使用 (MC / 填空 / 大题)
---
## 核心功能六:错题本
### 文件
| 文件 | 作用 |
|------|------|
| `backend/app/routers/attempts.py` | `GET /error-book` + `PATCH /{attempt_id}` |
| `frontend/src/pages/ErrorBookPage.tsx` | 错题本页面 |
| `frontend/src/lib/api.ts` | `getErrorBook()` + `updateAttempt()` |
### 后端
- `GET /api/attempts/error-book?user_id=xxx`
- 查询 `in_error_book=true AND mastered=false`
- JOIN `paper_questions` 返回完整题目信息
- `PATCH /api/attempts/{attempt_id}`
- 更新 `in_error_book` 或 `mastered` 标记
### 前端
- 列表展示: 题目信息 + 用户答案 + AI 反馈
- 操作: "Review in Workbench" (跳转) / "Mastered" (标记掌握) / "Remove" (移出错题本)
---
## 核心功能七:答题记录
### 文件
| 文件 | 作用 |
|------|------|
| `backend/app/routers/attempts.py` | `POST /` — 记录答题 |
| `frontend/src/components/workbench/ActionBar.tsx` | "Got it right" / "Got it wrong" 按钮 |
### 流程
- "Got it right" → `POST /api/attempts/` with `attempt_type: "select", is_correct: true`
- "Got it wrong" → `POST /api/attempts/` with `attempt_type: "select", is_correct: false`
- 后端自动 `in_error_book = true`
- Toast 提示操作结果
---
## API 接口汇总
### Papers Router (`/api/papers`)
| Method | Path | 说明 |
|--------|------|------|
| GET | `/` | 列出所有试卷 (可按 user_id 过滤) |
| POST | `/upload` | 上传试卷 PDF + 可选答案 PDF |
| GET | `/{paper_id}` | 获���单份试卷信息 |
| GET | `/{paper_id}/questions` | 获取试卷所有题目 |
### Attempts Router (`/api/attempts`)
| Method | Path | 说明 |
|--------|------|------|
| POST | `/` | 记录一次答题 |
| POST | `/photo` | 拍照上传 + OCR + AI 批改 |
| GET | `/error-book?user_id=` | 获取错题本 |
| PATCH | `/{attempt_id}` | 更新错题本/掌握状态 |
### Questions Router (`/api/questions`)
| Method | Path | 说明 |
|--------|------|------|
| POST | `/{question_id}/variant` | 生成变体题 |
---
## 前端路由
| 路径 | 页面 | 文件 |
|------|------|------|
| `/` | 首页 — 试卷列表 | `src/pages/HomePage.tsx` |
| `/upload` | 上传试卷 | `src/pages/UploadPage.tsx` |
| `/paper/:id` | 做题工作台 | `src/pages/WorkbenchPage.tsx` |
| `/error-book` | 错题本 | `src/pages/ErrorBookPage.tsx` |
---
## 前端组件树 (Workbench)
```
WorkbenchPage
├── Header # 顶部导航 (课程+试卷标题)
├── PdfViewer # 左侧 60% — PDF 连续滚动
└── Right Panel (40%)
├── [Questions Tab]
│ ├── QuestionNav # 题目水平导航 Q1 Q2 Q3...
│ ├── QuestionDetail # 题目展示 + MC/填空交互
│ ├── AiTrioPanel # 知识点/提示/解析 (3x CollapsibleSection)
│ └── ActionBar # 底部按钮 (对/错/变体/拍照)
├── [Variants Tab]
│ └── Variant Cards # 变体列表 (Generating.../Ready)
└── [Variant Detail View] # 替换整个右侧
├── Back Button
└── VariantDetail # 变体题作答 + AI 三件套
```
---
## LLM 调用模型分工
| 任务 | 模型 | Provider | 文件 |
|------|------|----------|------|
| 结构化拆题 | gpt-4o | laozhang API | paper_processor.py |
| 答案匹配 | gpt-4o | laozhang API | paper_processor.py |
| AI 三件套 (knowledge/hint/solution) | qwen-plus | DashScope | paper_processor.py |
| 手写 OCR | gpt-4o (Vision) | laozhang API | grader.py |
| 答案批改 | qwen-plus | DashScope | grader.py |
| 变体题生成 | gpt-4o | laozhang API | grader.py |
---
## 配置与环境变量
> 文件: `backend/app/config.py`, `.env`
| 变量 | 说明 |
|------|------|
| SUPABASE_URL | Supabase 项目 URL |
| SUPABASE_ANON_KEY | 前端用匿名 Key |
| SUPABASE_SERVICE_ROLE_KEY | 后端用 Service Role Key (绕过 RLS) |
| LAOZHANG_BASE_URL | GPT-4o 代理 API 地址 |
| LAOZHANG_API_KEY | GPT-4o 代理 API Key |
| DASHSCOPE_BASE_URL | 阿里 DashScope API |
| DASHSCOPE_API_KEY | DashScope API Key |
---
## 文件完整索引
### Backend (`backend/app/`)
```
main.py # FastAPI 入口, CORS, 路由注册
config.py # Pydantic Settings, 环境变量
routers/
papers.py # 试卷 CRUD + 上传触发处理
attempts.py # 答题记录 + 拍照OCR批改 + 错题本
questions.py # 变体题生成
services/
paper_processor.py # 核心5步管线: PDF→结构化→答案匹配→AI三件套
text_extractor.py # PyMuPDF 文本提取
grader.py # OCR + 批改 + 变体生成 (Prompt + LLM 调用)
llm_clients.py # GPT-4o / Qwen 客户端单例
supabase_client.py # Supabase 客户端
```
### Frontend (`frontend/src/`)
```
App.tsx # React Router 路由定义
main.tsx # ReactDOM 入口
lib/
api.ts # 所有 API 调用封装 (9 个函数)
types/
api.ts # TypeScript 类型定义
hooks/
usePaper.ts # 轮询获取试卷状态 (3s interval)
useQuestions.ts # 获取题目列表
pages/
HomePage.tsx # 首页 — 试卷列表
UploadPage.tsx # 上传页
WorkbenchPage.tsx # 做题工作台 — 核心调度组件
ErrorBookPage.tsx # 错题本
components/
layout/
Header.tsx # 顶部导航栏
shared/
KaTeXRenderer.tsx # HTML+KaTeX 公式渲染
CollapsibleSection.tsx # 折叠面板 (grid动画)
StatusBadge.tsx # 状态标签
upload/
UploadForm.tsx # 上传表单
FilePickerField.tsx # 文件选择器
workbench/
PdfViewer.tsx # PDF 连续滚动 + IntersectionObserver
QuestionNav.tsx # 题目导航栏
QuestionDetail.tsx # 题目展示 + MC/填空交互
AiTrioPanel.tsx # AI 三件套面板
ActionBar.tsx # 底部操作按钮
PhotoUpload.tsx # 拍照上传 Modal
VariantDetail.tsx # 变体题内联作答
VariantModal.tsx # (已废弃, 被 VariantDetail 替代)
```