 xuyueandClaude Opus 5.5
|
1b703d13ae
|
style(专题): 「题单」改名「专题」,顶栏、列表、详情、后台、成就说法统一
Deploy / deploy (push) Canceled after 0s
只改界面文字和注释:网址 /problemset、接口、表名、字段名都不动,老链接照常能打开。
设计稿页名「题单重设计」和已执行的迁移文件保留原样。
线上 3 个成就(题单收藏家 / 入门 / 通关)已同步改名。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-11-03 00:12:18 +08:00 |
|
 xuyueandClaude Opus 5.5
|
29c31640b5
|
refactor(课堂任务): 「课堂看板」改名「课堂任务」,地址换成 /task;班级 PK 换成 /pk
- 界面上的「课堂看板」「课堂作业」统一叫「课堂任务」,顶栏、老师首页、后台菜单一起改
- 前端 oj/classroom → oj/task、oj/class → oj/pk,两个名字不再只差几个字母却毫不相干;旧地址不留跳转
- 后端 routes/classroom.ts 拆开:课堂任务的接口进 routes/task.ts(/task/board|lesson|today),
首页的「上次来」「班里在做」进 routes/home.ts;共用的取题口径挪进 services/lesson.ts
- 契约 classroom.ts 同样拆成 task.ts / home.ts,ClassBoard* 改叫 TaskBoard*
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-11-02 22:53:07 +08:00 |
|
 xuyueandClaude Opus 5.5
|
195069450f
|
style(格式化): 改成单引号、一行最长 160,全仓重新格式化
Deploy / deploy (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-10-26 13:52:24 +08:00 |
|
 xuyueandClaude Opus 5.5
|
646c57c748
|
refactor(题单): 难度、状态、分数、提示、总分、百分比和 problemset_submission 表不再读写,先补默认值、下一步再删
Deploy / deploy (push) Canceled after 0s
机房和服务器共用一个库、不一定同时升级,所以分两步退役:0027 只给还非空的几列补默认值,
新代码插行不用再填、没升级的那边照旧能跑;两个站点都换上新代码后,再单独一条迁移删列删表
(要手工放行破坏性迁移,语句写在 0027 的注释里)。
进度算法随之去掉分数和百分比,判题记进度不再往 problemset_submission 写一行(从来没人读)。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-10-09 22:08:15 +08:00 |
|
 xuyueandClaude Opus 5.5
|
8c53e28dcb
|
feat(题单): 「截止时间」改成「布置期」,布置期内没加入的人也藏旧代码;老师能看全班情况,后台一次加好几道题
- 布置期(列名仍是 end_time,机房共库不改名)+ 新列 assigned_at:布置期内题单里这几道题,
学生在布置开始之前交的代码先藏起来,不管加没加入,堵住「先抄再加入」;做对或到期放出来,不拦做题
- 「总分达到 N」奖章换成「做对 N 道」,草稿并进「不公开」(0026)
- /problem-sets 带布置期、必做题数、奖章获得时间;题目带做对时刻、错几次、旧代码藏着没有
- 新增 /problem-sets/:id/class-view(老师看一个班:真名 × 每题,加入前就做对过)、/problem-sets/locks
- 后台:批量加题、拖动排序、切必做;去掉状态、难度、分数、提示和单独的进度页
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-10-09 22:01:39 +08:00 |
|
 xuyueandClaude Opus 5.5
|
af2ca31de5
|
fix(学生管理员): 260 班看板 23 人、数据统计只有 22 人,学生管理员还能看同学的作业代码——学生管理员一律按学生算
Deploy / deploy (push) Canceled after 0s
- 统计、题目页各班表、流程图统计的班级名单原来只认普通用户,改成和看板、排行榜一样的
STUDENT_ROLES,学生管理员进名单、进分母
- 看别人的提交代码和流程图只给老师、超管和出题人;题单防抄对学生管理员也生效。
原来「比赛进行中学生管理员不能看」那条特例随之去掉
- 题单的「用户进度」原来只给题单创建者,其余老师点开报「题单不存在」;改成所有老师都能看,
和看板、统计对所有班开放一致。后台能改能删的接口仍只给创建者
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-10-08 14:00:11 +08:00 |
|
xuyue
|
d25b5dbe6d
|
update
Deploy / deploy (push) Canceled after 0s
|
2026-09-27 19:36:28 +08:00 |
|
 xuyueandClaude Opus 5.5
|
23844ae301
|
fix(班级筛选): 输入 ks251 会把 2511 班的学生也算进来
Deploy / deploy (push) Canceled after 0s
班级号 3~4 位并存,按用户名前缀/包含匹配时 ks251 会捞到 ks2510~ks2519。
新增 classPrefixCondition:ks + 3 位以上数字按具体班级匹配(后面不能再跟数字),
1~2 位仍是年级前缀。后台首页的用户排名、随机抽签,以及题单的学生进度筛选
(只填数字时补上 ks)都改用它。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-27 19:34:28 +08:00 |
|
 xuyueandClaude Opus 5.5
|
9412ba3cf6
|
refactor(契约): 展示题号统一叫 problemDisplayId,题单 id 统一叫 problemSetId
Deploy / deploy (push) Canceled after 0s
前后端同时改,部署前开着的旧页面要刷新一次(题号列会是空的、提交筛选会失效)。
- 挂在非题目对象上的展示题号,原来叫 problem(字符串)或 displayId,统一成
problemDisplayId:提交列表、流程图列表、统计的 lastFailure / items /
今日最热题、站内信里的提交、后台题单的题目列表。别处的 problem 都是对象
或数字 id,这几处是字符串,最容易看错
- 列表筛选的 query 参数 ?problemId= 实际传的是展示题号,改成
?problemDisplayId=(提交列表、流程图列表、三个统计接口)
- 契约里 problemsetId / problemSetId、problemset / problemSet 两种拼法并存,
统一成 problemSetId / problemSet,和 ProblemSet* 类型名、problem-sets
路径、前端路由参数一致。drizzle 的列键跟表名走,没动
- aiDetail.user(用户名字符串,而契约里别的 user 都是对象)→ username。
ai_analysis.data 里存的历史快照仍是 user,没有代码读回它
- 后台题单题目表的列是按字符串键 'displayId' 取的,类型检查抓不到,一并改
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-27 16:08:31 +08:00 |
|
 xuyueandClaude Opus 5.5
|
5ae830a7f9
|
refactor: 一批名不副实的命名改掉(只动一端,不碰契约)
后端:
- getUserByToken → authenticateToken:它会删会话、续 TTL、记在线,不是纯读
- todayStart → dayStart(instant):本来就接受任意时刻,不只是「今天」
- objectValue → asRecord,删掉 4 份一模一样的私有拷贝和 `as toObject` 别名
- 报错文案 "does not exists" / "Already be a public problem" 改通顺
前端:
- utils/types 里 Contest / User / Problem 三个别名删掉:Contest 原来指后台
比赛、契约里同名的却是学生侧。现在 Contest = 学生侧、AdminContest = 后台,
后台用户表单叫 AdminUserForm,题目详情直接用 ProblemDetail
- SubmissionInfo → ContestSubmissionInfo,免得和 submission.info 撞名
- utoa / atou → compressToBase64 / decompressFromBase64(实际是 zlib + base64),
encode / decode → base64EncodeUtf8 / base64DecodeUtf8
- filterResult → toProblemRow、toProblemListItem → toAdminProblemRow,
行字段 submission / rate → submissionCount / acRate
- 组件:Modal → AddFromLibraryModal、Form → EditorToolbar、
ContestEditor → BasicEditor(题单也在用它)、rank 的 Index → RankMedal、
Name → UserNameCell、Password → PasswordCell、ContestType → ContestAccessTag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-27 16:08:21 +08:00 |
|
 xuyueandClaude Opus 5.5
|
f2ca34fd4b
|
refactor(api): 请求体解析收成 readJson / parseBody
54 处 handler 都手写同一段:
const parsed = X.safeParse(await c.req.json().catch(() => null))
if (!parsed.success) return failure(c, 400, "invalid-request", ...)
漏写 .catch 就是一个畸形请求打出 500。
- http.ts 加 readJson(读 + 校验)和 parseBody(再加上 400)。parseBody 不给
message 时回 zod 的第一条 issue —— 后台表单原来就是这么写的,fallback 文案
「Invalid payload」/「参数错误」只在 issues 为空时才用得上,实际走不到
- 51 处换成 parseBody;另 3 处失败时带别的错误码或和路径参数一起判,换成 readJson
- 错误码、状态码、文案、判断顺序都不变
实跑核过:非 JSON 请求体回原来的固定文案,公告标题为空回 zod issue。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-27 15:04:19 +08:00 |
|
 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
|
eda1eb7eee
|
chore(题单): 删掉客户端自报进度的 PUT /problem-set-progress
Deploy / deploy (push) Has been cancelled
这个端点是「AC 之后前端回调一下,把进度写进题单」那套设计的残留,早就被服务端记账
取代了 —— SubmitCode.vue 里留着当时的说明:客户端那条路只认路由参数里的那一个题单
(从普通题库入口做出同一道题不计进度),网络一抖、页面提前关掉进度就静默丢失;
现在判完之后由 judge/run.ts → services/problemset.ts 的 recordSolvedProblem 记账,
而且记进所有已加入且包含这道题的题单。
上一个提交删掉前端最后一个调用方 updateProblemSetProgress 之后,它就彻底没人打了。
留着的代价不只是死代码:那是一条**学生可以自己写进度**的写入口。
删之前逐个核过:
- 前端零引用(唯一的 wrapper 已在上个提交删掉);
- recomputeProgress 还有 POST 那条在用,保留;computeProgress / eligibleForBadge /
updateAchievementsForProblemSet / publishAchievementNotification 在别处都有调用方;
- 它往 problemset_submission 写的那一笔,services/problemset.ts:260 做的是一模一样的
去重后插入,不会因此少写。
顺带清掉因此变成孤儿的四个 import 和契约里的 updateProblemSetProgressRequestSchema
与 UpdateProblemSetProgressRequest。
## 验证
起服务实跑:
- PUT /api/problem-set-progress 现在 404;
- 保留的 POST(加入题单)仍然 201,progress 行照常由 recomputeProgress 建出来;
- 服务端那条替代路径端到端跑通:新建题单 → 加入题目 1004 → 学生加入 → 交一发 AC,
判完后 problemset_progress 自动变成 completed=1/total=1/100%/得分 10,
problemset_submission 也落了一行 —— 全程没有任何客户端回调。
tsc、check:routes、vue-tsc、单二进制编译均通过;测试题单已清理。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH
|
2026-09-10 19:42:17 -06:00 |
|
 xuyueandClaude Opus 5
|
ab47e71d6f
|
refactor(契约): 出参不再 parse,后台老题详情和站内信页不再 500
Deploy / deploy (push) Has been cancelled
## 出参改 satisfies
出参是后端自己刚拼出来的字面量,TS 编译期已经验过;再 xxxSchema.parse({...}) 一遍
拿不到任何新信息,唯一可能失败的输入是库里的历史数据,而失败的代价是 500。136 处
全部撤掉,撤的时候当场炸出两个一直存在的线上故障:
- 后台打开任何一道没编辑过的题都是 500 —— problem.last_update_time 是全库唯一可空
的列(961 道题里 470 道是 NULL),而 adminProblemSchema.lastUpdateTime 写的是
z.string();
- 收到过站内信的人打开消息页全是 500 —— embeddedSubmissionSchema 从
submissionDetailSchema 继承了 problemDisplayId 却没 omit,路由只填了同义的
problem;列表为空时才碰巧不炸,所以一直没人报。
两个都是读出侧校验自己造出来的故障,不是它拦住的故障。
## 校验责任挪回写入侧
- db/schema.ts:枚举型的列和几个形状确定的 JSONB 挂 .$type<>()(submission.result /
.language、problem.difficulty / .languages / .template / .astRules / .sqlConfig /
.sqlDisplay、achievement.rarity / .operator、exercise.type、reaction.type、
tutorial.type、problemset.difficulty / .status、flowchart_submission.status、
problemset_badge.condition_type、acm_contest_rank.submission_info)。只影响 TS、
不产生 SQL,断言逐列拿根目录那份生产备份核过全量数据。
- createProblemRequestSchema.languages 收窄成 problemLanguageSchema,兑现
problem.languages 列上的断言。
- 新增 routes/helpers.ts 的 asFilterValue():query 筛选值(result / language /
difficulty / status)要和收窄过的列比较时做纯类型交接,不加校验 —— 在这儿拦一道
会把「筛出空列表」变成「筛条件被忽略、返回全部」。
- 判题产物(submission.info / statistic_info / exercise.data)照旧放行,形状真相
在判题机那边;judge/sql、flowchart/run、events.ts 里对自家产物的 parse 一并撤掉。
- 仍然 parse 的只有 judge/events.ts 的 parseSubmissionEvent —— 从 Redis 收回来的
报文是真边界,失败返回 null 而不是 500。
顺带清掉两处重复的真相:stringArray 原本在 routes/helpers.ts、routes/problem.ts、
routes/submission.ts 各有一份拷贝,5 个调用点全部只作用于 problem.languages,列有类型后
三份一起删;routes/site.ts 里和契约同名同形的本地 interface Quote 也删了 —— loadSentences
读入时已经逐字段守过,那处 parse 同样是多余的。
## 文档
CLAUDE.md 那一节从「契约收紧要挑地方」改写成「出参不 parse,用 satisfies」,写明
三处写入侧闸门(入参 safeParse 58 处、列上 $type、语义校验函数);apps/web/CLAUDE.md
同步 —— 现在收紧字段的后果落在 tsc 编译期,但契约形状仍要对得上存量数据。
## 验证
- 生产备份全量:12.4 万条提交的 result 全在 -2..6,10、961 道题的 languages 均为合法
数组、10050 条榜单条目形状全对,无一例外;
- tsc -p apps/api 与 vue-tsc --noEmit 均 exit 0;check:routes 检查 177 条路由,无遮蔽;
前端 build、单二进制编译并在仓库目录之外启动均通过;
- 实跑 40+ 端点(学生端 / 后台 / AI / 榜单 / 题目回写往返),以及一次完整比赛 e2e:
建比赛 → 复制题目 → 错解 → 正解,把 judge/run.ts 榜单写入的三个分支全走到
(error_number 0→1、is_first_ac + ac_time 671、totalTime 1871 = 671 + 1×20×60),
后台核查页的勾选与 404 分支一并验过,测试数据已清理;
- 两个 500 用抓到的真实响应对着改动前的契约复验:lastUpdateTime 收到 null、
problemDisplayId 收到 undefined,改动后同样两个响应均通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH
|
2026-09-10 18:14:05 -06:00 |
|
 xuyueandClaude Opus 5
|
e5a6f2d1e7
|
fix(题单): 归属校验、进度分母、截止时间可见性等六处零碎
Deploy / deploy (push) Has been cancelled
## user-progress 缺归属校验
学生端那条 GET /problem-sets/:id/user-progress 只有 requireTeacher,没有归属校验,
任何 Teacher Admin 都能读到别人建的题单的学生名单与进度。补上,口径与后台
loadOwned 一致(超管放行,其余人只能看自己建的),越权报 404。
顺带订正 docs/specs/phase4-review-authz.md:449:那条写着题单进度「显式下发真名」,
与代码不符 —— 学生端这条走 sampleUser 且没传 includeRealName,realName 恒为 null
(SQL 里那次 leftJoin userProfile 是白查的)。真正下发真名的是后台那条
GET /admin/problem-sets/:id/progress,结论仍成立,但当时漏掉了归属校验这个缺口。
## 头部进度的分母
分母只算必做题之后,ProblemSetHeader 还在拿 completedCount / problemsCount 算 ——
前者是必做完成数,后者是总题数。做完全部必做题的人会看到「9 / 10、90%」,而同一张
卡片上又标着「已完成」,题单 6 那 10 个人正是这种。改成读 userProgress,另外把
「另有 N 道选做」标出来,否则「共 10 道题目」和「9 / 9」对不上。
## 截止时间
end_time 管的不是「到点不能做了」,是「到点之前看不到自己加入题单之前的旧代码」,
而学生端一个字都不显示 —— 被挡住的人不知道为什么,也不知道什么时候解锁。头部加一个
带解释的「截止 …」标签;提交列表那个锁图标的说明也补上另外两条解锁路径。
## 两个必然筛空的筛选器
学生端题单列表的难度、状态两个下拉:线上 16 个题单全是 Easy / active,选「中等」
「困难」「已归档」永远是空列表。撤掉,保留关键词搜索。接口那两个 query 参数留着,
哪天真的用起这两个字段再把 select 加回来。
## 题目移出题单时的提交记录
旧栈 problemset/signals.py 的 post_delete 会清掉该题在本题单的 ProblemSetSubmission,
OJ2 没做,于是那张表一直在攒指向已移出题单的孤儿行。补上。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
|
2026-09-01 00:01:58 -06:00 |
|
 xuyueandClaude Opus 5
|
74b97c610f
|
fix(题单): 「选做」终于作数了,分母只算必做题
isRequired 一直只是卡片上的一行字:卡片写着「(选做)」,进度分母和 all_problems
奖章却照样要求做完。结果是学生按提示跳过选做题,进度条卡在 100% 以下、全通奖章也
拿不到 —— 快照里 22 个人做完了全部必做题却显示未完成(题单 5 三人、6 十人、8 两人、
11 七人)。旧栈的 update_progress 同样不区分,是一路继承下来的。
改成:分母只算必做题,选做题做了仍然计分(totalScore 把它算进去),只是不卡完成。
一道必做都没标的题单退回「全部都算必做」—— 那种题单多半是没用这个字段,而不是真的
整单选做,不兜住的话它永远完不成。
## problem_count 奖章不能跟着改
老师当初是按题单的**总题数**设阈值的:题单 5 的「一职欧拉」要 8 题,而它的必做只有
7 道。要是 problem_count 也改用只数必做的 completedProblemsCount,这枚奖章一夜之间
不可得,76 个已经拿到的人会被 recalculateBadge 收回。所以它数的是「做出的题目总数
(含选做)」,从 progress_detail 的键数来。score 类同理不受影响。
预演证实了这道闸门有效:22 人拿到完成状态,奖章补发 56 条、**收回 0 条**。
## 顺带:第三份手抄的达标逻辑
PUT /problem-set-progress 里还藏着一份 inline 的奖章判定,和 services 里那份、
补发脚本里那份是三份各写各的 —— 这次改 problem_count 的语义,漏掉任何一份都会让
学生提交时发的奖章和后台重算的结果对不上。三处统一到 eligibleForBadge。
## 工具改名并扩到进度
backfill-badges → backfill-problemsets。语义变更之后,已有的进度行要跑一遍才会按新
规则重算,否则那 22 个人得等到老师下次动题单才生效。两笔账本来也是同一笔:进度一变,
奖章达标面就跟着变,所以落库走 resyncProgress(它重算进度后会顺手重算该题单全部奖章),
预演里的奖章差异也是照着订正后的进度算的,保证预演和 --apply 的结果一致。
在生产快照上实跑:
合计:进度 494 条要重算(完成 +22 / -0),奖章补发 56 条、收回 0 条
已订正 10 个题单 → 复核通过:题单数据与规则一致
user_badge 1180 → 1236,已完成 621 → 643
「未完成但有完成时间」仍是 4 条(d7a6414 那条语义保住了)
重复跑幂等:进度 0 条、奖章 0 条
抽查题单 6(9 必做 + 1 选做):只做必做的显示 9/9 100% 已完成、80 分;连选做一起做的
同样 9/9 已完成,但 90 分。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
|
2026-08-31 08:55:36 -06:00 |
|
 xuyueandClaude Opus 5
|
d9e6a2a3f0
|
perf(题单): 学生端题目列表少两次 join 一次查询,顺带把并列的 order 排稳
## 排序
学生端 /problem-sets/:id/problems 只按 order 排,没有 tiebreaker,而卡片是按数组
下标编号的(#1 #2 #3)。order 并列时 Postgres 不保证次序,题单 8(3 道题 order 都是
10)、题单 11(2 道并列 4)、题单 14(3 道并列 0)实际就有并列,于是「第 3 题」指
哪道题每次刷新都可能变。后台那条列表一直是 order + id 排的,两边本来就不一致。
补上 asc(id)。教师进度视图里那份题目清单(同一路由文件 :398)有同样的问题,一并补。
实跑:四道题、三道 order 并列,连打 5 次次序完全一致。
## 载荷
这个接口原来复用 problemListItemSchema,为此要多 join user + user_profile 凑
createdBy、再多查一次标签表凑 tags —— 而题单卡片只渲染题号、标题、难度、分数和
完成标记。tags / submissionNumber / acceptedNumber / createdBy / contestId /
allowFlowchart / showFlowchart / hasAstRules 一个都不用,myStatus 甚至写死是 null。
而且 select 的是 schema.problem 整行,题面、样例、答案、ast_rules、flowchart_data、
sql_display 全都白拉回来一遍。
改成 problemSetProblemItemSchema(id / _id / title / difficulty 四个字段),查询相应
收窄:两次 join 去掉,标签那次查询整条去掉,行查询只取四列。每次请求从 4 条查询降到
3 条,其中最大的那条不再拖整行题面。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
|
2026-08-31 08:47:57 -06:00 |
|
 xuyueandClaude Opus 5
|
f9354b0df1
|
fix(题单): 进度重算漏了分数和完成状态,奖章没人补发
Deploy / deploy (push) Has been cancelled
进度算法在学生路径和后台路径各写了一遍,于是各自漂了一段。后台那份
(resyncProgress)名叫「把所有参与者的进度重算一遍」,实际只更新分母和百分比:
- 不碰 total_score。改题目分值时专门调了它,函数体里却没这个字段,score 类奖章
因此按陈旧分数判定。
- 不碰 is_completed。加一道题之后分母变大、百分比掉下来,人还标着「已完成」。
- 不清理 progress_detail。删掉一道题之后 least(completed, total) 只保证不超过分母,
会把没做的题算成做了 —— 3 题的题单只做出 C,删掉 C 就变成 1/2 = 50%。
- 不重算奖章。题目集一变 all_problems 的达标面就变了,没有任何地方补发。
最后一条攒下了实打实的欠账:8-07 的快照里 53 条应发未发,涉及 30 名学生,误发 0 条。
其中 23 条来自 2026-05-22 00:50 那次一分钟内跨 7 个题单的批量补进度 —— 进度补了,
奖章没人回头判。
两边不再分叉的唯一办法是只留一处算法,所以抽出 services/problemset.ts:
computeProgress 是纯函数,学生做出一题和后台改题目都只是调用者;
eligibleForBadge / recalculateBadge / resyncProgress 一并搬过来,补发脚本才能复用
同一套判定。批量写回仍是一条 UPDATE ... FROM (VALUES ...),没退回逐行往返。
顺带修掉空题单的坑:isCompleted 加了 total > 0 前提。原来 0 === 0 也成立,老师先建
题单、学生先加入、题目后加,加入那一刻就写下 complete_time 并计进「完成题单数」成就,
而且后面补上题目也不会自愈。
一处行为变化:退回未完成时 complete_time 会清空。后台那份原来保留旧时间,学生那份
一直是清空的,统一成后者 —— 否则同一行会出现「未完成 + 有完成时间」,破坏现有数据里
「complete_time 非空 ⟺ is_completed」这条不变量。代价是加题再删题会把历史完成时间
洗成「现在」。
scripts/backfill-problemset-badges.ts 补历史欠账,默认只读预演,--apply 才落库。
只要存在误发就拒绝执行并打出名单,要连收回一起做得显式加 --allow-revoke ——
recalculateBadge 的删除是真删,user_badge 的 earned_time 没有别处备份。
在生产快照上实跑:补发 1180 → 1233 条,复核全部一致,历史 earned_time 未被刷新;
resyncProgress 对题单 9(7 题 / 117 人 / 100 人完成)加题后正确退回 0 人完成并收回
100 枚奖章,删题后完整恢复;改题目分值 10→20 使总分和 7460 → 8590,正好 +113×10。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
|
2026-08-31 08:23:38 -06:00 |
|
 xuyueandClaude Opus 5
|
a75c70c82d
|
perf(后端): 干掉 14 处 N+1 查询
Deploy / deploy (push) Has been cancelled
列表接口按行发查询是从阶段 3 一路带过来的写法:`Promise.all(rows.map(...))`
看着是并发的,但每行都往库里打一次,行数一多就是几百上千次往返。同样的模式
也散在几个后台批处理和写入路径里。全部改成「先收集 id,一条 inArray/group by
查回来建 Map」——`routes/problem.ts` 的 getProblemTags 早就是这么写的,这次
只是把剩下的地方对齐。
用户可见的列表:
- `GET /problem-sets` 每行 5 条(题目数/我的进度/奖章/已获奖章/创建者),
limit 上限 250 就是 1250 次往返。改成固定 5 条,与行数无关。
- `GET /contests` 每场比赛一条 creator 查询。后台的比赛列表本来就是 join
出来的,只有这条公开列表漏了。
- `GET /admin/problems`、`GET /admin/contests/:id/problems` 每题一条标签查询。
- `GET /admin/problem-sets` 每行 3 条;`.../badges` 每个奖章一条 count。
后台批处理:
- `refreshContestJoinedForAll` 原来是每个用户 1 条 count 加一个独立事务里的
insert/select for update/update。改成一条 group by 出全部用户的场次,再分批
upsert,`metrics || excluded.metrics` 是 jsonb 浅合并,只覆盖 contest_joined
一个键,其余指标原样保留 —— 合并在一条语句里完成,for update 那把锁不再需要。
- `rescanAchievement` 补发循环、`unlockAchievements`:命中的一次插完,
onConflictDoNothing 的 returning 就是真新解锁的那批,unlockCount 改成一次 +N。
- `resyncProgress` 逐行 UPDATE 改成一条,completed 用 least() 夹住。
写入路径:
- 标签解析抽出 normalizeTagNames + findTagsByName(一条 lower(name) IN),
新建题、改题、批量打标签三条路共用。
- 克隆比赛:题面一条 INSERT、标签一条 SELECT 加一条 INSERT。新旧题的对应
关系靠 _id 认,不依赖 returning 的行序。
- `POST /admin/website` 8 个键一条多行 upsert。
- 题单奖章判定一次插完。
`/ai/duration`:原来每个时间桶两条查询、桶之间还串行,一年 12 个桶 24 次往返。
改成先算桶、再一条查询把整段区间拉回来在内存里分桶。时间戳用
`extract(epoch) * 1000` 取毫秒回来比,别指望 Date.parse 认 pg 那个
`2026-08-12 00:00:00+00` 格式。**相邻桶首尾相接、两端闭区间**(落在边界上的
提交两个桶都算)这条旧语义是照搬的,不要顺手改成半开区间。
验证:本机起 dev 栈,造了覆盖各分支的种子数据(创建者重复的题单、零题目/零
奖章的题单、completed > total 的脏进度、除不尽的百分比、大小写混写的已有标签、
带/不带标签的比赛题、正好落在分桶边界上的提交),旧代码跑一遍、新代码跑一遍:
- 51 个接口响应逐字节一致
- 12 张表的快照逐行一致(唯一差别是 progress_detail 里的 submit_time 墙钟值)
- 打开 log_statement=all 数过条数,例如 `GET /admin/problems` 20 道题
24 → 5,`GET /problem-sets` 28 → 8,`/ai/duration` years:1 28 → 5,
202 个用户的成就补发 1828 → 614
一处可观察的行为变化:补发现在整批共用一个 unlockTime,原来是每人一个
new Date()。rescanAchievement 上方的注释本来就写着补发会给几百人盖同一个
时间戳、前端据此只显示「已获得」不显示日期,所以这个方向是对的。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-26 23:43:57 -06:00 |
|
 xuyueandClaude Opus 5
|
95625d6711
|
refactor: 另外两处守卫也收到注册行上
走查题单进度时撞到 `/problem-sets/:id/user-progress` 对学生 403 —— 那是**正确**的
(它是给教师看全班进度的),但守卫又写在 handler 体内。评审的 M3 只列了 3 处,
按同一模式扫全仓,还有 2 处:
content.ts POST /messages isSuperAdmin
problemset.ts GET /problem-sets/:id/user-progress isTeacherOrAbove
改用 requireSuperAdmin / requireTeacher,理由同 M3:守卫要能从注册行上看出来,
不然下一个人加同类端点容易漏掉那个 if。
实测档位没变:学生两条都 403,教师看全班进度 200。
路由遮蔽检查仍然干净。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-08 06:28:44 -06:00 |
|
 xuyueandClaude Opus 5
|
b4b61af6b0
|
fix(阶段3): 匿名不可读用户档案,真名改为默认不下发
F1:GET /profiles/:username 只挂了 optionalAuth、handler 内无登录判断,
匿名可读 email、adminType、className、lastLogin。用户名又能经 /rankings/users
公开枚举,等于可以无 cookie 批量收集全校学生的邮箱与最后登录时间。
handler 开头补上未登录即返回空,对齐旧后端 account/views/oj.py 的
UserProfileAPI.get 首行 `if not user.is_authenticated: return self.success()`。
F2:旧后端把「是否下发真名」做成 UsernameSerializer(need_real_name=False)
的默认关闭开关,全仓 11 处调用只有比赛榜单一处显式打开;新后端没搬这一层,
真名随用户对象无条件下发,13 个下发点里 8 个匿名可达。
这里补回同一层:helpers.ts 新增 sampleUser(),realName 默认不下发,
需要的地方显式传 { includeRealName: true }。12 个下发点改为走这个函数,
只有比赛榜单一处打开(对齐 contest/serializers.py:84 的 is_contest_admin)。
没有逐处删字段 —— 那样下次新增端点还会重犯。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-07 01:59:05 -06:00 |
|
 xuyueandClaude Opus 5
|
8c00cdc947
|
feat(阶段3): oj 侧端点铺开(基线提交,未经评审)
由外部 agent (Codex) 在本会话额度中断期间完成。原样提交作为基线,
后续修复单独成 commit,便于区分与回退。
覆盖 oj 侧 65 个端点,新增 9 组路由(account/achievement/ai/classroom/
content/contest/flowchart/problemset/site)与对应 Zod 契约。
已核验:tsc --noEmit 退出码 0;API 可启动;/api/problems 返回真实数据;
judge 与 flowchart worker 均 ready。
未核验:权限边界与数据泄露,评审进行中。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-07 01:25:36 -06:00 |
|