Commit Graph
5 Commits
Author SHA1 Message Date
xuyueandClaude Opus 5 ed56a209ea chore(格式): Prettier 统一到全仓,后端和契约一次性格式化
Deploy / deploy (push) Has been cancelled
原来只有 `apps/web` 在 Prettier 下(配置在 `apps/web/.prettierrc.toml`、脚本在
web 的 package.json),后端和契约从来没格式化过 —— 手写在 100 列上下,`db/schema.ts`
还是 drizzle-kit pull 留下的 tab 缩进。两套口径分叉久了,跨端改一处就得记着「这边
什么风格」。

- 配置搬到根目录 `.prettierrc.toml`,内容不变(`semi=false`,其余全默认,
  printWidth 80 —— 和前端已有的格式一致,不另立一套宽度);
- 脚本统一成根目录 `bun run fmt`,覆盖 `apps/*/src`、`apps/web/tests` 和两个构建
  配置;web 自己那份 `fmt` 和重复的 prettier 依赖删掉;
- `.prettierignore` 挡掉两类不该碰的:drizzle-kit 生成的 `src/db/meta/` 结构快照
  (它是 db:generate 的比对输入,只该由 drizzle-kit 写)、unplugin 每次 dev 都会
  重写的 `auto-imports.d.ts` / `components.d.ts`;
- 全量跑了一遍。纯格式,无行为改动:api typecheck / check:routes / check:ast、
  前端 type-check 全过,起 api 打了接口确认正常。前端这 39 个文件的小改动是
  prettier 版本漂移(类型断言的换行口径变了),不是新配置带来的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 08:27:34 -06:00
xuyueandClaude Opus 5 5d60bb15bb perf(redis): 热路径合并往返,限流脚本改走 EVALSHA
Deploy / deploy (push) Has been cancelled
审查 Redis 用法时找到的四处小账,都是确定性改动,语义不变:

- `getUserByToken` 里两条串行 EXPIRE 合成一次 pipeline。这是全后端最热的
  Redis 路径 —— 每个带鉴权的 HTTP 请求都要续一次会话和反向索引。上次给
  `touchSession` 修的正是同一个形状,这处漏了,两边现在一致。
- `createSession` 的 SET / SADD / EXPIRE 三趟并成一趟。登录是突发的,
  一个班同时登录时差别全压在这一下。
- 限流的 Lua 从 `redis.eval` 改成 `defineCommand`,稳态走 EVALSHA 只发
  40 字节 sha1,不再每次带上 937 字节的脚本全文;NOSCRIPT 由 ioredis
  自动回退成 EVAL 重新灌,Redis 重启和 SCRIPT FLUSH 都不用管。
- 删掉 websocket 订阅连接上重复的 error 监听 —— `withErrorLogging` 已经
  打过一遍且带连接名,留着只会把同一条错误打两份。

实跑验证(dev 栈,API 跑在 3999):登录后 `session:*` 多一条、
`user-sessions:1` 的 scard 和 ttl(604800) 都对;把两个键的 TTL 压到 100
再打一次带鉴权的请求,两个都回到 604800。限流侧 `info commandstats` 显示
evalsha calls=3/failed=1 + eval calls=2 —— 失败那次正是 SCRIPT FLUSH 之后
的 NOSCRIPT 回退;令牌桶数值与旧实现逐位一致(10 个初始额度扣 3 剩 7,
再要 20 个被拒并返回 wait=433.33 = (20-7)/0.03)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB
2026-09-07 18:44:59 -06:00
xuyueandClaude Opus 5 ec1509c46d perf(限流): 桶参数加 60 秒进程内缓存;Redis 连接补 error 监听
getBucketConfig 每次都查一次 `throttling` 配置项,而限流点在提交判题、AI 分析、
流程图评分上(5 处调用)—— 判题高峰期等于每条提交多一趟数据库,只为读一个几乎
从不变的值。上一代在 options/options.py 的 my_property 里也是带 TTL 缓存的,
重写时漏掉了。

缓存放进程内而不是 Redis:每站只有一个 api 进程服务读请求(oj-api 单容器、
Bun.serve 没有 reusePort、worker 只消费队列),进程内 Map 就等于全站缓存,
放 Redis 只是多一趟网络加一次序列化。异常分支特意不写缓存 —— 数据库抖一下不该
让接下来一整分钟全站都按默认参数限流。`throttling` 没有后台界面、只能直接改库,
改完最多一分钟后生效。

顺带给三条 Redis 连接都挂上 error 监听。ioredis 对没有监听者的 error 走
silentEmit:不崩进程,但把连接错误直接 console.error 到 stderr,绕开这里的日志,
而且不说是哪条连接 —— 这个进程同时开着会话读写、两条队列、一条订阅,「哪条」正是
要先知道的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB
2026-09-07 07:41:09 -06:00
xuyueandClaude Opus 5 2c4d56b29a refactor(后端): 清掉 Django 遗留的死列与手工级联,角色字符串收成一份
三条迁移,一次部署(0008/0009 含 DROP COLUMN,需要 OJ2_ALLOW_DESTRUCTIVE=1):

- 0008 删 IP 相关:比赛 IP 白名单(前端本来就没有输入框,detail.vue 无条件置空)、
  submission.ip(前端从未显示过)、以及一次都没被调用过的 IP 限流桶。
  judge_server.ip 是运维数据,保留。
- 0009 删九个只有 Django 时代写过、OJ2 一次都没读过的列:user 的 auth_token /
  open_api / open_api_appkey / session_keys,user_profile 的 blog / github /
  school / major / language。open_api 后台连开关都没有,那段「已经开着就不重置
  appkey」的逻辑从上线起没进过 if。判据是「全仓零读取」而不是「看着没用」——
  raw_password 同样刺眼却是在用的,别一起清掉。
- 0010 给 17 条外键补上删除动作,不再是 Django 留下的一律 NO ACTION。父行消失后
  必然无意义、且不构成学生留痕的走 CASCADE(中间表、题单/教程/成就的组成部分、
  user_profile 与 user_stat);需要人看见的继续拦着——submission.problem_id、
  以及 user 的绝大多数外键,删用户撞外键会被 handler 翻译成「请改为禁用账号」,
  这是有意的:全 CASCADE 会静默抹掉成就与进度,而 submission.user_id 压根没有
  外键,结果是一半删一半留。六处手工级联随之删掉。

角色字符串收进 packages/contract/src/roles.ts:原先 ADMIN_ROLES / TEACHER_ROLES
在两个文件各抄一份、学生口径在四个文件各写一遍、前端 USER_TYPE 是第三份副本。
AuthUser.adminType 与 drizzle 的列都收窄成联合类型,二十多处 `=== "Super Admin"`
从此受编译器管着($type 是纯 TS 层的,generate 确认不产生任何 SQL 变更)。

顺带删掉 db/relations.ts —— drizzle-kit pull 的产物,全仓零引用。

一处行为变化:后台用户列表传非法的 ?type= 回 400,不再静默返回空列表;界面上的
下拉只有合法值,打不到这条。

验证:tsc / vue-tsc / vite build / check:routes 全过;三条迁移在 dev 库执行,
并逐条建 fixture 走 HTTP 接口验过删除连坐与拦截(题单五张子表连坐、user_badge
二级连坐、删有提交的题目仍 409、删有表情的用户仍 409)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeJoYc2t2d7cThVqMBYrBF
2026-09-02 22:57:45 -06:00
xuyueandClaude Opus 5 8237909d00 fix(阶段3): 提交可见性守卫补上匿名,详情脱敏,提交接口加限流
F3:`isRegularUser(user)` 对匿名用户(user 为 null)返回 false,
`submission_list_show_all` 关闭时守卫整体短路 —— 匿名能看到全部提交,
权限反而大于登录学生。实测开关关闭时匿名 total=23、登录学生 total=0。
而关闭这个开关的典型场景正是考试。改为 `!isAdminRole(user)`(非管理员即受限),
并删掉 isRegularUser(全仓只有这一处调用,留着就是下一个坑)。

F4:提交详情把 info(含每个测试点的 test_case 编号与 output_md5)与 ip
按「是不是自己的提交」下发。旧后端 submission/views/oj.py 把关的是角色:
is_admin_role() 决定用 SubmissionModelSerializer 还是
SubmissionSafeModelSerializer(exclude=("info", "contest", "ip"))。改为仅管理员可见。

F6:旧后端 SubmissionAPI.throttling 的 TokenBucket 没搬过来。
按旧参数重建(options/options.py:120 的 user 桶:capacity 20、fill_rate 0.03、
default_capacity 10,实际值仍以数据库 throttling 配置项为准),
落在 redis 上并改用 Lua 脚本做成原子操作 —— 旧实现自己注明「不是线程安全的」,
而限流要挡的正是并发突发。挂点位置与旧后端一致:比赛权限校验之后、取题目之前。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:59:19 -06:00