后端架构
EduBuddy 后端是一个基于 Node.js + Express + TypeScript 的 RESTful API 服务,采用分层架构(路由层 → 服务层 → 数据访问层),通过中间件链实现安全、限流、日志等横切关注点。
技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| Node.js | 18+ | 运行时 |
| Express | 4.x | HTTP 框架,路由、中间件 |
| TypeScript | 5.x | 类型安全,编译为 JS |
| better-sqlite3 | 12.x | SQLite 同步驱动 |
| openai | 4.x | AI 服务 SDK |
| jsonwebtoken | 9.x | JWT 签发与验证 |
| bcryptjs | 2.x | 密码哈希(cost factor 12) |
| multer | 1.x | 文件上传处理 |
| helmet | 7.x | HTTP 安全头 |
| cors | 2.x | 跨域资源共享 |
| express-rate-limit | 7.x | API 速率限制 |
| compression | 1.x | Gzip 响应压缩 |
| winston | 3.x | 结构化日志 |
| uuid | 9.x | 生成 UUID v4 主键 |
| pdf-parse | 1.x | PDF 文字提取 |
中间件链
每个请求经过以下中间件处理链(按注册顺序):
HTTP Request
↓
[1] helmet() → 设置安全响应头(X-Frame-Options, CSP 等)
↓
[2] cors() → 验证 Origin,开发环境允许 localhost:3000/5173
↓
[3] compression() → Gzip 压缩响应体
↓
[4] rateLimit() → 全局速率限制(200次/15分钟)
↓ (AI 接口额外经过 aiLimiter:20次/分钟)
[5] express.json() → 解析 JSON 请求体(最大 10MB)
↓
[6] Route Handler → 具体路由处理
↓
[7] authMiddleware → JWT 验证(受保护路由)
↓
[8] Business Logic → 数据库操作 / AI 调用
↓
[9] Error Handler → 全局错误捕获,生产环境隐藏详情
↓
HTTP Response
路由模块
后端路由按业务领域拆分为 6 个独立模块:
| 模块文件 | 挂载路径 | 主要功能 |
|---|---|---|
routes/auth.ts | /api/auth | 注册、登录、获取用户信息、更新资料、修改密码、用户列表 |
routes/questions.ts | /api/questions | 题目 CRUD、AI 生成、AI 解题、提交答案、从文本提取题目 |
routes/progress.ts | /api/progress | 知识点掌握度、每日统计、练习记录、AI 分析、评估报告、目标管理 |
routes/learningPaths.ts | /api/learning-paths | 学习路线 CRUD、AI 生成、报名、进度更新、节点管理 |
routes/upload.ts | /api/upload | PDF/图片/语音/文本上传解析,上传历史记录 |
routes/sharing.ts | /api/sharing | 发送分享、收件箱、发件箱、标记已读、未读计数 |
认证中间件
middleware/auth.ts 实现 JWT 验证,扩展了 Express 的 Request 类型:
// 扩展 Request 类型
export interface AuthRequest extends Request {
user?: {
userId: string;
username: string;
role: string;
};
}
// 中间件逻辑
export function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1]; // Bearer
if (!token) return res.status(401).json({ message: '未提供认证令牌' });
const payload = verifyToken(token); // 调用 utils/jwt.ts
if (!payload) return res.status(401).json({ message: '令牌无效或已过期' });
req.user = payload; // 注入用户信息
next();
}
数据库访问
database/db.ts 实现单例模式的数据库连接管理:
let db: Database | null = null;
export function getDB(): Database {
if (!db) {
db = new BetterSQLite(DB_PATH);
db.pragma('journal_mode = WAL'); // 启用 WAL 模式
db.pragma('foreign_keys = ON'); // 启用外键约束
initializeSchema(db); // 首次运行建表
}
return db;
}
better-sqlite3 提供同步 API,所有数据库操作在主线程同步执行,无需 async/await。这使得代码更简洁,且避免了异步操作中的事务管理复杂性。对于 I/O 密集型操作(主要是 AI 调用),仍使用 async/await。
文件上传处理
文件上传使用 multer 中间件,配置如下:
- 存储方式:磁盘存储(
diskStorage),UUID 命名避免冲突 - 允许格式:
.pdf、.png、.jpg、.jpeg、.mp3、.wav、.m4a、.txt - 大小限制:默认 10 MB(可通过
MAX_FILE_SIZE环境变量调整) - 存储目录:
./uploads/(自动创建)
日志系统
utils/logger.ts 使用 Winston 构建结构化日志:
- 开发环境:彩色控制台输出
- 生产环境:JSON 格式,支持对接 ELK / CloudWatch 等日志平台
- 日志级别:error > warn > info > debug
- 每个路由处理器在关键操作(登录、题目生成、错误)都有日志记录
错误处理规范
所有路由使用统一的错误响应格式:
// 成功响应
{ "success": true, "data": { ... }, "message": "操作成功" }
// 失败响应
{ "success": false, "message": "错误描述" }
// 常见 HTTP 状态码
200 - 查询/更新成功
201 - 创建成功
400 - 参数错误(必填项缺失、格式不正确)
401 - 未认证或 Token 无效
403 - 无权限(如删除他人题目)
404 - 资源不存在
409 - 冲突(如用户名已存在)
413 - 文件过大
429 - 速率限制超出
500 - 服务器内部错误
知识点掌握度算法
答题后,系统自动更新 knowledge_mastery 表,掌握度计算公式:
// 掌握度 = min(100, 正确率 × log10(练习次数+1) × 200)
mastery = Math.min(100,
(correctCount / totalCount) * 100 * Math.log10(totalCount + 1) * 2
)
该公式综合考虑了正确率和练习次数两个维度:
- 练习次数少时,即使全对,掌握度也不会过高(避免运气因素)
- 随着练习次数增加,掌握度增长曲线趋于平缓(对数函数)
- 掌握度上限为 100