Project
错题轻舟
面向纸质错题整理与个性化复习的小程序项目,覆盖题目框选、OCR 辅助识别、错题库管理、用户隔离、PDF 试卷生成和本地化部署。
本文目录
状态:开发中
错题轻舟
错题轻舟是一套面向学生、家长和课后辅导场景的错题整理与复习工具。它的目标不是简单保存试卷照片,而是把纸质试卷、练习册和作业本中的错题拆成可编辑、可检索、可复习的结构化资料,并在需要时重新生成复习试卷。
项目分为微信小程序前端和 FastAPI 服务端两部分。小程序端负责图片选择、题目框选、识别结果校对、错题编辑、错题库浏览和试卷预览;服务端负责登录鉴权、用户隔离、图片上传、题图裁剪拼接、OCR 辅助识别、错题数据存储和 PDF 文件生成。
本文是公开版项目记录。文中的域名、端口、路径、密钥、topic、数据库内容和服务器标识均做了脱敏或泛化处理,不对应真实生产配置。
背景
传统错题整理通常有三个痛点:
- 手抄题目耗时,特别是数学题、几何题和带图题。
- 只拍照片又不便于检索、筛选和生成复习资料。
- 不同阶段、不同练习册中的错题容易分散,复习时难以重新组合。
错题轻舟的设计思路是保留“人工确认”的准确性,同时把重复性工作交给系统处理。用户仍然手动框选题目、校对识别结果、填写答案和错因;系统负责图片裁剪、OCR 识别、题图保存、错题入库和试卷排版。
核心目标
- 支持手机拍照或相册选择试卷图片。
- 支持手动框选题目区域,避免全页 OCR 造成分题不准。
- 支持多区域拼接,适配跨栏题、带图题和局部组合题。
- 每道题保存独立题图,而不是只保存整张试卷。
- OCR 只作为题干录入辅助,不自动猜测答案和错因。
- 学科、题型、答案、答案图片和错因由用户确认后保存。
- 错题库支持搜索、编辑、删除和复习筛选。
- 试卷导出前支持预览,并可逐题设置是否编入、是否保留题图。
- 服务端按用户身份隔离数据,避免多用户数据混用。
功能流程
上传图片
-> 手动框选题目区域
-> 多区域裁剪与拼接
-> OCR 辅助识别
-> 用户校对题干
-> 补充答案 / 答案图 / 错因
-> 保存到错题库
-> 筛选题目并预览试卷
-> 导出 PDF 复习试卷
小程序端不把 OCR 识别结果直接视为最终题目内容,而是把它作为“草稿”。用户可以在保存前修改题干、题型和答案信息,这样能够减少 OCR 误识别带来的长期数据污染。
前端设计
小程序前端按页面和业务能力拆分:
app / config
- 全局配置、API 地址、登录状态初始化
utils
- 请求封装
- 坐标转换
- 屏幕布局状态
services
- 用户登录
- 图片上传
- OCR 调用
- 错题增删改查
- PDF 生成请求
features
- 框选会话
- 题目数据归一化
- 试卷选择状态
pages
- 首页
- 框选错题
- 编辑错题
- 错题库
- 生成试卷
- 我的账户
- 软件声明与反馈
框选页面是前端中最复杂的部分。它需要处理图片在不同屏幕中的显示比例、拖动偏移、缩放状态和原图坐标之间的换算。用户看到的框选位置必须能够准确映射到服务端裁剪坐标,否则会出现“框选看起来正确,但裁剪结果偏移”的问题。
为了适配手机、平板、折叠屏和窗口化运行场景,页面布局不做简单等比缩放,而是根据设备宽度、可用高度和操作密度调整区域。小屏幕优先保证图片展示和框选手柄可操作;大屏幕则给预览、题目信息和操作按钮更多空间。
服务端设计
服务端采用 FastAPI 实现,核心模块包括:
认证模块
- 接收 wx.login code
- 换取用户标识
- 生成服务端登录态
上传模块
- 校验图片类型
- 保存原始题图和答案图
- 返回受控访问地址
图片处理模块
- 按原图坐标裁剪
- 支持多区域纵向拼接
- 支持黑白增强、降噪和锐化参数
OCR 模块
- 对题图进行预处理
- 调用 OCR 引擎
- 返回识别文本和结构化结果
错题模块
- 新增、查询、修改、删除
- 按用户身份隔离数据
- 保存题干、答案、错因、题图和时间信息
PDF 模块
- 按用户选择生成复习试卷
- 题目页与答案错因页分离
- 支持保留题图和基础排版
服务端只监听本机内部端口,公网访问由 Nginx 或云入口统一代理。密钥、微信 AppSecret、数据库文件、用户上传图片和导出 PDF 不进入公开仓库,也不在公开文章中展示真实值。
数据模型
当前数据模型保持轻量,适合个人或小规模使用:
users
- id
- openid_hash
- token_hash
- created_at
mistakes
- id
- user_id
- subject
- question_type
- question_text
- answer
- answer_image_path
- wrong_reason
- image_path
- created_at
- updated_at
files
- uploads/
- exports/
- stitched/
- answer-images/
数据库保存结构化字段和文件路径,图片与 PDF 保存在文件系统目录中。这样可以避免把大文件直接写入数据库,也便于后续迁移到对象存储。
OCR 策略
早期尝试过整页 OCR 自动分题,但实际效果不稳定。试卷中经常存在多栏排版、题目跨栏、图文混排、选项挤压和手写痕迹,完全依赖 OCR 自动分题会导致题目边界不准。
因此当前策略改为:
用户先框选题目区域
-> 服务端只识别该区域
-> 用户再校对识别文本
这降低了自动化程度,但显著提高了可控性。OCR 的定位从“自动完成错题录入”调整为“减少用户手打题干”,更符合错题本的真实使用场景。
PDF 生成
试卷生成不是简单把错题列表拼接成 PDF,而是加入了选择和预览步骤:
- 用户先在错题库或试卷页面筛选题目。
- 每道题可以设置是否编入试卷。
- 每道题可以设置是否保留原始题图。
- 试题页不直接显示答案和错因。
- 答案和错因统一放到最后的附录页。
- 选择题排版尽量保留选项分行。
这种设计让 PDF 更接近可打印复习资料,而不是错题详情截图合集。
部署结构
正式部署采用“云服务器公网入口 + 本地 Debian 服务承载”的结构:
微信小程序
-> HTTPS 合法域名
-> 云服务器 Nginx
-> 云服务器本机 FRP 映射端口
-> FRP 隧道
-> 本地 Debian Nginx
-> 本地 FastAPI 127.0.0.1:<APP_PORT>
这样做的原因是:
- 小程序必须访问 HTTPS 合法域名。
- 本地 Debian 方便长期保存数据和迁移。
- 云服务器只承担公网入口、证书和转发,不保存主要错题数据。
- 后续可以在本地 Debian Nginx 中按域名分发更多服务。
云服务器上的 Nginx 不直接承载后端业务,而是把请求转发到 FRP 映射端口。本地 Debian 的 Nginx 再根据 Host 或路径把请求分发到对应服务。当前小程序后端只是其中一个服务,后续可以继续接入博客、自动化工作流或通知中心。
部署步骤概览
公开版部署流程保留结构,不包含真实路径和密钥:
# 本地 Debian
mkdir -p <APP_DIR>
cd <APP_DIR>
python3 -m venv .venv
./.venv/bin/python -m pip install -r requirements.txt
./.venv/bin/python -m pip install -r requirements-ocr.txt
服务端使用 systemd 常驻:
[Service]
WorkingDirectory=<APP_DIR>
ExecStart=<APP_DIR>/.venv/bin/python -m uvicorn main:app --host 127.0.0.1 --port <APP_PORT>
Restart=always
EnvironmentFile=/etc/<APP_NAME>.env
本地 Debian Nginx 只反代本机服务:
server {
listen 80;
server_name <APP_DOMAIN>;
client_max_body_size 30m;
location / {
proxy_pass http://127.0.0.1:<APP_PORT>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
FRP 客户端只把本地 Nginx 暴露给云端映射端口:
serverAddr = "<CLOUD_SERVER>"
serverPort = <FRP_BIND_PORT>
auth.method = "token"
auth.token = "<FRP_TOKEN>"
[[proxies]]
name = "miniapp-api"
type = "tcp"
localIP = "127.0.0.1"
localPort = 80
remotePort = <REMOTE_PORT>
云服务器 Nginx 最终只反代到本机 FRP 映射端口:
server {
listen 443 ssl;
server_name <APP_DOMAIN>;
location / {
proxy_pass http://127.0.0.1:<REMOTE_PORT>;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
隐私与安全处理
这个项目涉及学生错题、短信式登录状态、图片文件和服务端密钥,因此公开记录时遵循以下原则:
- 不公开真实 API Key、AppSecret、FRP token、Webhook、数据库文件和用户图片。
- 不公开真实服务器 IP、完整域名配置、内网机器名和系统用户。
- 不把
uploads/、exports/、mistakes.db、.venv/、日志文件提交到公开仓库。 - 生产环境密钥放在
/etc/<APP_NAME>.env或同级安全位置,文件权限限制为管理员可读。 - 小程序前端中的固定 API Key 只能作为过渡校验,不应作为最终安全边界。
- 用户数据访问以服务端登录态和用户标识为准,所有错题查询都必须按 user_id 过滤。
- 对上传文件限制类型和大小,避免非图片文件进入处理流程。
- 云服务器只开放 80/443 和必要的 FRP 控制端口,SSH 尽量限制来源 IP。
- FRP dashboard 只监听本机,不直接暴露到公网。
当前状态
已完成:
- 小程序页面基础框架。
- 图片上传与题目框选。
- 多区域拼接与题图保存。
- OCR 辅助识别。
- 错题新增、编辑、删除和列表展示。
- 微信登录入口与用户隔离基础。
- PDF 试卷预览与导出流程。
- 服务端迁移到本地 Debian 的基础部署路径。
仍在改进:
- 框选体验和不同设备布局适配。
- OCR 识别后的选项排版清洗。
- PDF 预览和最终导出的一致性。
- 更细的题型判断和筛选条件。
- 用户注销、数据导出和备份恢复流程。
- 部署文档与软著材料的同步维护。
复盘
错题整理系统最容易陷入“完全自动化”的误区。实际使用中,OCR、自动分题和题型判断都存在不确定性,尤其是小学和初中数学题经常带图、跨栏或选项不规整。因此这个项目最终选择了更稳的路线:让用户控制题目区域和最终内容,让服务端承担重复处理。
从工程角度看,当前架构的重点不是追求复杂技术栈,而是把边界划清楚:小程序负责交互,服务端负责计算和存储,云服务器负责公网入口,本地 Debian 承载长期数据。这样后续无论是迁移服务器、扩展通知系统,还是把图片存储迁移到对象存储,都有清晰的替换位置。
系列笔记
这个系列还没有公开笔记。
评论
Giscus 评论尚未配置。填写 GitHub Discussions 的仓库和分类 ID 后,这里会显示评论区。