本项目是对 ThinkSpiritLab/leverage 后端的完整重构。 原项目是一个 Online Judge 平台,已运行多年,存在大量技术债务。
原始代码路径(参考用):/Users/yuzhe/.openclaw/workspace/projects/leverage/src/
原始文档路径:/Users/yuzhe/.openclaw/workspace/projects/leverage/docs/
| 项目 | 原版 | 新版 |
|---|---|---|
| 框架 | NestJS (旧版) | NestJS latest |
| ORM | TypeORM (旧版,有废弃 API) | TypeORM latest |
| 数据库 | MariaDB (假设 10.6+) | 同,不改 schema |
| 认证 | Session Cookie | JWT (access + refresh token) |
| 队列 | 自研 Queue(基于 Redis List) | BullMQ(Redis Stream,成熟库) |
| 日志 | console.log | nestjs-pino(结构化 JSON) |
| 监控 | 无 | Prometheus endpoint(@willsoto/nestjs-prometheus) |
| 运行模式 | PM2 cluster(多进程,有 bug) | 单进程,无状态(水平扩展靠 nginx) |
| 排行榜 | 全表扫描,每 15 分钟重建 | Redis Sorted Set(ZADD/ZRANK,实时更新) |
| 部署 | 裸机 PM2 | Docker + docker-compose |
- 不改数据库 Schema(除非特别必要,需提出并等待确认)
- 功能对等:所有原有功能必须保留(可重命名路由,不影响实际功能)
- pass 测试:核心链路单元测试覆盖率 ≥ 80%,其他模块 ≥ 60%
- 大刀阔斧:去除所有代码坏味道,不保留原有屎山写法
- JWT 替换 Session:ContestUser 认证也用 JWT,payload 含 contestId,Guard 内验设备/IP
[前端 Nuxt 4] ←→ [后端 NestJS] ←→ [MariaDB]
↕ ↕
[Redis] [BullMQ Queue]
↕
[heng-controller] ←→ [评测机 × N]
用户 POST /submissions
→ 写 DB(status: PENDING)
→ BullMQ: 推入 judge-tx 队列
→ Worker: HMAC 签名 → HTTP POST heng-controller /c/v1/judges
→ heng-controller 回调 POST /heng/update/:submissionId/:judgeId(中间状态)
→ heng-controller 回调 POST /heng/finish/:submissionId/:judgeId(最终结果)
→ 更新 DB(Submission + SubmissionMisc + User + Problem 统计)
→ Redis Sorted Set 实时更新排行榜
→ 更新 UserProblemStatus 缓存
- 每次提交生成
judgeId(32 字节随机 hex) - Redis SADD
judge-ids:{submissionId}→judgeId - 回调时验证 judgeId 在 Set 中,否则忽略(防止旧评测结果覆盖重评)
按优先级顺序实现:
- ✅ auth — JWT 认证,login/logout/refresh,PBKDF2-SHA256+salt 密码哈希
- ✅ submission — 提交代码,频率限制(Redis),资源倍增(Java/Python 等)
- ✅ heng — 与 heng-controller 通信,HMAC 签名,BullMQ judge-tx/judge-rx
- ✅ receive — 接收评测结果,更新 DB,更新排行榜 Sorted Set
- ✅ problem — 题目 CRUD,测试数据上传(校验必须是 zip),缓存
- ✅ user — 用户管理,权限体系(数字权重:sa=0 < admin=1 < supervisor=2 < user=3 < guest=5)
- ✅ contest — 竞赛管理,ContestUser(支持独立密码、设备绑定、IP 绑定)
- ✅ course — 课程管理,CourseUser,按条件导出提交
- ✅ rank — 排行榜,Redis Sorted Set,排名日志
- ✅ setting — 系统配置,支持 string/number 类型,12s 缓存(fix #36)
- ✅ tag — 题目标签
- ✅ log — 操作日志
- ✅ notification — 通知
- ✅ media — 媒体文件上传
- ✅ profession-college — 专业/学院,批量导入用户时校验
- ✅ statistics — 统计(题目通过率 / 用户活跃度 / 系统概览)
- ✅ suspicion — 查重(代码哈希 + 评分,sus-xlsx 导出接口)
- ✅ init — 初始化(通过环境变量
SKIP_INIT=true跳过,INIT_SA_USERNAME/PASSWORD)
- ✅ compete — Bot 对战系统(Game/Gamer CRUD + 发起对局 + 排行榜)
- ✅ redis — RedisService + CacheService(通用缓存封装)
- ✅ database — TypeORM 配置,MariaDB
- ✅ queue — BullMQ 配置(替代自研 Queue)
- ✅ logger — nestjs-pino 配置
- ✅ metrics — Prometheus endpoint
- ✅ Docker — 多阶段 Dockerfile + docker-compose(MariaDB + Redis + App)
来自代码审查 docs/TODO.md:
- ✅
manager.increment()缺少 await → 数据不一致 - ✅ main.ts 硬编码开发者用户名 → 改用
SKIP_INITenv - ✅ SSL 证书验证
rejectUnauthorized: false→ 默认开启,HENG_ALLOW_INSECURE_TLS控制 - ✅ 密码无 salt → PBKDF2-SHA256 + random salt
- ✅ pendingSet 内存变量多进程不安全 → Redis Set(重构用 Sorted Set 完全替代)
- ✅ filter-sheet rangeMatch 匹配整个文本而非单行
- 排行榜全表扫描卡进程 → Redis Sorted Set 实时维护
- 分页 offset 硬编码 bug → 正确的 page × perpage
- getHashes
for...in遍历数组 →for...of - 魔法数字全部提取为 named constants
- 零测试覆盖 → 写测试
# 数据库
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=leverage
DB_USERNAME=leverage
DB_PASSWORD=secret
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# JWT
JWT_ACCESS_SECRET=your-access-secret
JWT_REFRESH_SECRET=your-refresh-secret
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
# Heng Controller
HENG_BASE_URL=http://localhost:3001
HENG_AK=your-ak
HENG_SK=your-sk
HENG_ALLOW_INSECURE_TLS=false
# App
PORT=3000
SKIP_INIT=false
MAX_SUBMISSION_PER_MINUTE=10- RESTful,路径小写 kebab-case
- 统一响应格式:
{ "data": ..., "message": "ok" } - 错误格式:
{ "statusCode": 400, "message": "具体错误", "error": "Bad Request" } - Swagger 文档:
/api/docs(@nestjs/swagger) - 所有 Controller 用
@ApiTags+@ApiOperation注释
粒度要细,越碎越好,每完成一个小单元就 commit,不要攒着。
格式:type(scope): 描述
类型:
feat— 新功能fix— bug 修复refactor— 重构(不改功能)test— 测试chore— 配置/依赖/工具docs— 文档
粒度示例(正确做法):
feat(auth): 添加 JWT access token 生成
feat(auth): 添加 JWT refresh token 逻辑
feat(auth): 添加 login 接口
feat(auth): 添加 logout 接口
feat(auth): 添加 JwtAuthGuard
test(auth): 添加 AuthService 密码哈希测试
test(auth): 添加 AuthService login 流程测试
feat(submission): 添加提交实体和 DTO
feat(submission): 添加频率限制逻辑(Redis INCR)
feat(submission): 添加资源倍增(bonus)逻辑
test(submission): 添加频率限制单元测试
错误示范:
feat(auth): 完成认证模块 ← 太粗
feat: 添加各种功能 ← 毫无意义
每个 commit 应该只做一件事,能用一句话说清楚。
- 框架:Jest +
@nestjs/testing - 单元测试:所有 Service 方法都有对应测试,mock 所有外部依赖
- 核心链路(submission/receive/heng/auth)覆盖率 ≥ 80%
- 其他模块 ≥ 60%
- 运行:
pnpm test,pnpm test:cov
需要提供:
Dockerfile(multi-stage build,最终镜像基于 node alpine)docker-compose.yml(app + mariadb + redis).env.example(所有环境变量模板)
- ContestUser 有双重认证:
allowDirectLogin=true用全站密码,false用竞赛独立密码 - 竞赛支持设备绑定(
deviceBindType)和 IP 绑定(bindIp) - Problem 有逻辑 ID(
logicId+prefix,如 P1001)和数据库 ID 两套体系 - SubmissionMisc 分表设计保留(代码字段大,列表查询不 JOIN)
- 资源倍增规则保留(Java ×5 内存 ×2 时间,Python ×3 内存 ×2 时间等)
- Heng 协议参考:
/Users/yuzhe/.openclaw/workspace/projects/Heng-Protocol/