Files
PastpaperMaster/TECHNICAL.md
Zhao 7a09167261 Initial commit: PastPaper Master full stack
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 12:27:47 +07:00

517 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 原始解<E5A78B><E8A7A3> |
| 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...), 不能只给答案
- 选择题要解释为什么对、为什么其他选项错
- 标注常见错误: `<div class="common-error">...</div>`
- 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``<Document>` + `<Page>` 组件
- 所有页面垂直排列在可滚动容器中 (不是单页切换)
- `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` 监听所有 `<Page>` 元素, 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` | 知<><E79FA5>点/提示/解析 折叠面板 |
| `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<string, "True" | "False">`, `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}` | 获<><E88EB7><EFBFBD>单份试卷信息 |
| 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 替代)
```