20 KiB
20 KiB
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
# 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
双向联动:
-
题目 → PDF (点击题目, PDF 滚动到对应页)
- QuestionNav 点击 →
handleQuestionSelect(index)→ 记录lastUserSelectTime = Date.now()+setCurrentIndex - PdfViewer 收到
currentPageprop 变化 →useEffect触发el.scrollIntoView({ behavior: "smooth" }) - 设置
programmaticScroll.current = true, 2s 后重置
- QuestionNav 点击 →
-
PDF → 题目 (滚动 PDF, 右侧自动切换到当前题)
IntersectionObserver监听所有<Page>元素, threshold:[0, 0.25, 0.5, 0.75, 1]- 追踪每页的
intersectionRatio, 选出可见占比最高的页码 - 如果
programmaticScroll.current === true, 跳过回调 - 触发
onPageChange(bestPage)→ WorkbenchPagehandlePdfPageChange handlePdfPageChange: 找到page_number <= currentPage的最后一题, 更新currentIndex
防止跳转抢夺 (双层保护):
- WorkbenchPage 层 (核心):
lastUserSelectTimeref — 用户点击题目后 2 秒内,handlePdfPageChange直接 return, 不响应任何 Observer 回调。解决长文档 smooth scroll 经过中间页触发 Observer 导致题目被切走的问题 - PdfViewer 层 (辅助):
programmaticScrollref — scrollIntoView 期间 Observer 回调跳过, 2s 后重置
核心功能三:做题交互 (MC / 填空)
文件
| 文件 | 作用 |
|---|---|
frontend/src/components/workbench/QuestionDetail.tsx |
题目展示 + 答题交互 |
frontend/src/components/workbench/AiTrioPanel.tsx |
知<EFBFBD><EFBFBD>点/提示/解析 折叠面板 |
frontend/src/components/shared/CollapsibleSection.tsx |
可折叠区域组件 |
frontend/src/components/shared/KaTeXRenderer.tsx |
HTML+KaTeX 渲染器 |
QuestionDetail 交互逻辑
选择题 (MC):
- 状态:
selectedOption,checked - 点击选项 → 高亮蓝色 (未检查时)
- 点击 "Check Answer" →
checked=true - 正确: 选项变绿 + "Correct!" / 错误: 选中项变红, 正确项变绿 + 显示正确答案
- 切换题目时自动重置状态 (
useEffectonquestion.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使用 CSSgrid-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):
interface StoredVariant {
id: string; // placeholder ID, e.g. "variant-1"
sourceQuestionNumber: string; // 原题题号
variant: VariantQuestion; // 生成结果
status: "generating" | "ready";
}
流程:
- 用户点击 "Similar Question" →
ActionBar调onVariantStart(placeholderId, questionNumber) - WorkbenchPage 创建
status: "generating"的占位项, 用户可继续做题不受阻塞 - API 返回后 →
onVariantReady(placeholderId, variant)→ 状态更新为ready - 失败 →
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" 按钮 |
后端流程
- 接收图片 → 上传到 Supabase Storage
attempt-photosbucket ocr_photo(photo_bytes)— GPT-4o Vision 识别手写内容- 输入: base64 图片
- 输出: 学生答案文本 (含 LaTeX 公式)
grade_answer(question, student_answer)— Qwen-plus 批改- 输入: 题目信息 + 标准答案 + 学生答案
- 输出:
{ is_correct, score_given, feedback (HTML), error_at_step }
- 写入
user_attempts表 (含 photo_url, photo_ocr_text, feedback, is_correct) - 答错自动
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/withattempt_type: "select", is_correct: true - "Got it wrong" →
POST /api/attempts/withattempt_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} |
获<EFBFBD><EFBFBD><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 替代)