xuyue and Claude Opus 5.5
b09d479893
style(课堂任务): 后台列表分清写代码、先画再写、画流程图,能按做法筛
...
原来写代码当默认值不写出来,只剩「C语言」「Python」,看着和别的分不开。
现在三种都用不同颜色的标签写出来,筛选多一个「做什么」。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-11-03 09:05:23 +08:00
xuyue and Claude 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
xuyue and Claude Opus 5.5
221d264701
feat(账号): 新建的号不再加 ks + 班号前缀,班级单独选;学生改不了真名了
...
- 批量建号先选班再贴名单,用户名就是名字;被别的班或注册的号占了自动加数字(xuyue2)。
查重改成查「这个班里有没有同名的」,老号按剥掉 ks 前缀的名字比
- 后台编辑、新建用户能直接选班级;以前班级只能从用户名的 ks 前缀推,改不了
- 登录点名字时名单接口一并给出用户名,新号老号都登得上;本机记的班级兼容旧的 ks257 写法
- 筛选、抽人、「看看他们的写法」都按班级认人,不再拼 ks 前缀(新号拼出来永远圈不进去)
- 接口 PUT /me/profile 不再收真名:界面早没入口,但早几届有 44 人把真名改成了外号
- 0042:两个被推成非班号班级的老号(xrm、王仁杰)改成没填班级
- 老账号不改名,只认 ks 前缀的地方收在契约 username.ts 一处
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-11-02 23:22:20 +08:00
xuyue and Claude 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
xuyue and Claude Opus 5.5
b344bd1dd6
feat(布置作业): 能按知识点点选题目;一个班一天能布置几节课;数据统计认老师布置的课;后台能管布置过的课
...
Deploy / deploy (push) Canceled after 0s
- 课堂看板「选题」弹框:左边知识点(加必会题),右边搜题号/题名、点选,按点的先后排;
只列这次作业的语言能交的题(/problems、/problem-tags 加 ?language=)
- 一天多节课:0040 拿掉 class_lesson (class_name, day) 唯一约束,每次布置一行;
看板「改」只改当前一节,加「新的一节」;这一节布置超过 100 分钟时先问这节课做哪几道题。
不带 lessonId/newLesson 的老请求照旧改今天最后一节
- 数据统计「一行一节课」:布置过的课以布置为准(题目和顺序照布置、做完按全班算、标「布置」),
这些提交不再参与推断切段,也不算零散;详情页「做完」注明是这一天/这节课/这段时间
- 后台「课堂作业」:一节一行,改(班级/日子/时刻/语言/题目)、看统计、删除、补一节;
看板加「布置过的课」入口。题号校验和「做完」口径抽到 services/lesson.ts,三处共用
⚠️ 机房站点要和服务器一起升级:旧代码布置时 ON CONFLICT (class_name, day) 找不到约束会报错
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-11-02 20:41:25 +08:00
xuyue and Claude Opus 5.5
8427475337
chore(清理): 删掉并行试跑、搬库、Python3 别名、旧链接这些过渡期的兼容写法
...
Deploy / deploy (push) Canceled after 0s
- 部署:JUDGE_STATE_DIR 不再强制设;删掉搬库脚本和只为搬库存在的自检、提醒
- 语言:Python3 / Python2 别名表删掉,normalizeLanguage 改名 parseLanguage
- 旧链接:后台题单详情的跳转、用户列表 -last_login 的转换
- 旧页面:布置作业必须带 language(null = 没选);不再清旧版出题页草稿
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-26 14:15:04 +08:00
xuyue and Claude Opus 5.5
6b5ca745bb
fix(出题页): 改了挑战点保存,提示「保存好了」其实一条都没存上
...
出题页发请求时漏了 challenges 这个字段,后端又把「不带」当成「不动原来的」,
所以改动被静默丢掉。现在出题页带上它,接口也改成必须带(null 是清空),
以后再漏传会直接报错,不会再悄悄丢。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-26 14:15:04 +08:00
xuyue and Claude 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
xuyue and Claude Opus 5.5
1a173f75d7
feat(挑战): 这节课做完了给「还能挑战」,挑战中编辑器顶上一条紫色的,老师看板上看得出谁在挑战
...
Deploy / deploy (push) Canceled after 0s
做完这节课(或题库里自己做对)之后列出还能挑战的题;去挑战后编辑器顶上写着要求,
没做到标出是哪几行,做到了写本班第几个、全站第几个。课堂看板做到的格子贴一枚紫色筹码、
在挑战的加一圈紫边,题卡片写挑战成功几人。出题页多一块「挑战」,能从做对的代码里找候选。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-26 08:52:50 +08:00
xuyue and Claude Opus 5.5
f7c4af0c01
fix(年度趋势): 刚学一个月的届不再拿去和学了一年多的往届比,两边都只算同样学了这么久时做的
...
Deploy / deploy (push) Canceled after 0s
以前往届那一侧把所有记录都算进去,25 级比 24 级显示 73% / 82%,
其实 24 级在这些题上近一半是学了半年以上才做的;同期比是 73% / 75%。
现在每个人以自己第一次交题为起点,两边都只算离起点不超过 N 天的人题对,
N = 两届里学得短的那届一半人开始做题到现在的天数。按人不按日历,因为各届
开始做题的时间不一样(25 级第二年才开始)。比哪届改成后端算,接口带 grade / compare。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-20 21:21:22 +08:00
xuyue and Claude Opus 5.5
e3c0507d41
feat(设置): 能查出并删掉没人用的图片,编辑器里删掉的图和换下来的头像都算
...
Deploy / deploy (push) Canceled after 0s
编辑器里删图、学生换头像都只改库不删文件,盘上越积越多。设置页加「没人用的图片」,
点「查一下」才扫(要扫整个库,不在进页面时跑),按编辑器插图 / 头像分两组,
缩略图可点开,单张删或整组删(先确认)。
判定宁可多留:扫库里每张表的每个文本列,文件名在哪儿出现过都算在用(老域名、
markdown 写法都认,不分大小写);只看 png/jpg/gif/bmp,目录里的 mp4 不碰;
一天内传的不算(可能还在编辑器里没保存);库里一张图都没提到时什么也不列(多半连错了库);
删之前重新扫一遍。服务器和机房图片目录各一份,要各清各的。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-20 20:50:02 +08:00
xuyue and Claude Opus 5.5
d6c3171d13
refactor(请求): 去掉 axios,所有请求统一从 authFetch 出门;换 token 时断网不再平白弹登录框
...
Deploy / deploy (push) Canceled after 0s
utils/api 改成基于 authFetch 的一层 fetch 封装,调用方用法不变;下载测试点也走
authFetch,token 过期时会换一张重试。换 token 那一步断网时,AI 流式接口和自学上报
原来会拿掉 token 重发、弹登录框,现在和普通请求一样报「网络异常」。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-20 19:39:19 +08:00
xuyue and Claude Opus 5.5
4dfd14f95b
refactor(契约): 前端 60 个写请求的请求体接上契约类型,前后端对不上编译期就报
...
Deploy / deploy (push) Canceled after 0s
- 前端 oj/api.ts、admin/api.ts、shared/api.ts 的 post / put 请求体一律 `satisfies XxxRequest`,
只在编译期检查,不加运行时校验(和「只在三处挂运行时闸门」的规矩一致)
- 契约里的 XxxRequest 从 z.infer 改成 z.input:请求类型描述的是「客户端可以发什么」,
带默认值的字段可以不传。后端从不用这些类型(它用 parse 出来的 data),不受影响
- 补了四个漏导出的请求类型(比赛题目排序、题单题目排序、标签分类、检查用户名)
- 接上之后对不上的几处:编辑用户的角色和题目权限经 toAdminType / toProblemPermission
收窄,导入用户的行改成四元组,练习题数据改成 Record,格式化的语言只收后端认的四种
(isFormatLanguage)。出题页和成就的请求体是「表单校验兜底、出站补默认值」,
用 Partial<…> 只核键名和值的类型
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-16 15:03:58 +08:00
xuyue and Claude Opus 5.5
a814776fb9
feat(教程): 改练一练时可以顺手清空学生的作答——改了答案,按旧答案做对的人不再算做对
...
Deploy / deploy (push) Canceled after 0s
练一练是原地改的,作答记录跟着题走:改了答案或选项,按旧答案做对的人仍然显示做对、
「最后错在 选了 B」说的也是旧选项。编辑框底下多一个「同时清空学生的作答(N 人做过)」,
没人做过时不出现;勾上保存,学生那边这道题回到没做。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-16 12:28:47 +08:00
xuyue and Claude Opus 5.5
7839a65f59
feat(自学情况): 每个学生练一练做到哪了——格子分出做了一部分、点名字看逐题明细
...
- 方块格多一档「做了一部分」:按做对的比例从下往上涨绿,一道没做对的底下压一条橙;
鼠标停上去是「练一练 3/5,错了 4 次」
- 「练一练」列改成 做对/应做(应做 = 他打开过的课的练一练总数)
- 点学生名字从右边滑出明细:每课一段,逐题写第几次做对、交了几次、最后错在哪;
手机上返回键关它
- 点开一道练一练,底下多一串「读了这课还没做」的人
- 顺手修:那个弹框七列加起来比弹框宽,「最后作答」被挤没了;改成一列用户名
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-16 11:40:30 +08:00
xuyue and Claude Opus 5.5
5561e0bc3a
feat(后台用户): 用户列表按班级看、「重置密码」常驻;批量建号粘进名单当场列出会建成的账号,重名和已有的先点出来,建好能下载名单、打印账号条
...
用户列表去掉 ID、真名、占位邮箱和四颗彩色按钮。下载的名单开头带 BOM,Excel 打开中文不再乱码。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-13 13:56:03 +08:00
xuyue and Claude Opus 5.5
97049d562b
feat(后台题目): 题目列表重排——1366 屏上不用横滚就看得到「学生看得到」开关;一行只剩「编辑」和 ⋯,有人交过的题删除直接灰掉;勾几道一次放出、藏起、加减知识点
...
去掉内部 ID、难度、看不懂的「功能」图标和反馈表情几列;题目名点了就是编辑。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-13 13:27:39 +08:00
xuyue and Claude Opus 5.5
6e946c99fb
feat(出题): 参考流程图改用画布画,「让学生自己画」不再要参考图
...
Deploy / deploy (push) Canceled after 0s
- 学生画的图现在按例子和测试数据评(拼成程序跑),参考图用不上了:「让学生自己画」
只剩一句说明,保存前改成检查有没有例子;「给学生的提示」那一栏去掉(学生从来看不到,
16 道题也都没填)
- 「给学生看一张参考流程图」改用学生画图的同一个编辑器,存结构树(problem.flowchart_data,
原来是死列),Mermaid 由后端照它生成,题面里的图和学生交的图同一套形状
- 「AI 照 Python 答案画一张」改成出结构树,直接摆在画布上接着改;for 循环画成
计数变量 + 当型循环
- 老题手写的 Mermaid 照旧给代码框,旁边「改用画布画」
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-12 21:39:50 +08:00
xuyue and Claude Opus 5.5
cd75d42a64
feat(年度趋势): 后台年度趋势改成「这届和往届比」,比往届差的题排在最前面
...
Deploy / deploy (push) Canceled after 0s
- 选一届和往届(或另一届)比,两边都至少 20 人做过的题才比;分比往届差 / 差不多 / 比往届好三组,一题一根哑铃条
- 侧栏「历年概况」改回「年度趋势」,地址换成 /admin/problem/trend
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-12 13:10:18 +08:00
xuyue and Claude Opus 5.5
0980ea858a
feat(卡点分析): 后台卡点分析按知识点 → 题 → 一道题重做,年度趋势改名历年概况只留逐学年总表
...
Deploy / deploy (push) Canceled after 0s
- 选班后只列这个班课上布置过的题,右栏换成「交了几次才对」和交得最多的人
- 历年概况可以导出表格;旧地址 top_ac_trend 跳到新页面
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-12 12:55:49 +08:00
xuyue and Claude Opus 5.5
845e41c060
feat(自学): 自学重做——首页课程地图、读一课按项目分目录、代码块就地运行、学完给题做
...
Deploy / deploy (push) Canceled after 0s
- 顶栏「自学」进新的自学首页 /learn:接着学(按服务端记录,不再串到上一个学生)+ 按项目分组的课程地图
- 读一课:左边目录按项目折叠,正文加宽,右边本课大纲带练一练标记;去掉了不能运行的示例代码栏
- 正文里的代码块能改能运行,报错用题目页那套中文解释;读输入的代码出输入框
- 课末:练一练做到几道 → 学完了 → 老师挑的几道题(必会标出来)→ 下一课
- 练一练统一成绿色提交、结果一行字;手机版目录从底部弹出
- 后台:教程列表按项目分组拖动排序,编辑页左写右看、练一练插到光标处、挑相关题;
自学情况改成方块格 + 还没打开过的名单
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-12 09:40:34 +08:00
xuyue and Claude Opus 5.5
c17f88cdbb
feat(题单): 题单页重做——布置中的放最上面,详情顶上一条进度和奖章阶梯,没加入看得见题点不进;老师看全班、后台一屏编辑
...
Deploy / deploy (push) Canceled after 0s
- 列表:布置中的大卡片(做对几道、布置到哪天、再做对几道拿哪个奖章),其余小卡片,加入过的在前
- 详情:进度 + 奖章阶梯(下一个金圈「再做对 n 道」),题目两列写几点做对 / 交过几次;
以前做对过的标「旧代码先藏着」;做完整条变绿,烟花只在做完那一刻放
- 老师多一个「全班情况」页签;题目页从链接直接进来没加入时,条上给「加入题单」
- 后台一屏编辑:布置一周 / 两周 / 四周 / 选日期,公开开关,粘贴多个题号一次加,拖动排序,奖章只剩两种条件
- 旧代码藏着的地方都说清楚是哪个题单、哪天之后能看
- 修掉列表卡片把选做题数说成「新加了 N 题」
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-09 22:01:39 +08:00
xuyue and Claude Opus 5.5
151bbdcb50
feat(比赛): 比赛页一页看题目和自己的名次;排名竞争感做足、结束有领奖台;老师考试中看全班、考完看成绩;新建比赛一屏做完
...
Deploy / deploy (push) Canceled after 0s
- 学生比赛页(设计稿 B):左边题目带几人做对、谁最先做对;右边我第几名、5 分钟里升了几名、再做对 1 道能到第几名、我前后的人
- 期中、期末考试中只有自己的进度,写明「排名考完公布」,题目页的排名入口也藏起来
- 排名页:按班筛、名次变化、最先做对的深绿格、自己那行钉住;结束后前三名领奖台 + 我的成绩卡;排名曲线和学生的「导出数据」去掉
- 老师的「全班情况」:卡在一道题、15 分钟没交的放最上面,没进来的人对上看板的请假;考完变「成绩」,点格子看代码、标看过了(代替后台审核页),导出成绩
- 比赛列表:进行中的一大块「接着比」,已结束的写我第几名,前三带奖杯
- 新建比赛:默认 10 分钟后开始,时长点按钮,搜题号把题加进来自动编号、拖动排顺序
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-09 20:58:59 +08:00
xuyue and Claude Opus 5.5
ba5b9ada0e
feat(后台): 编程题出题页重做——左边写,右边是学生看到的样子;例子和测试数据的输出都由标准答案跑出来,改老题能直接看、改测试数据
...
- 例子就是测试数据里勾了「当例子」的几组,不再手抄输出;老题的例子不在测试数据里的补到最后
- 改了答案,输出变了的那组标黄、写清原来存的是什么;答案在某组上报错就挡住保存
- 没存标准答案的老题输出手写;太大的测试数据(>100 组或单文件 >32KB)只能导入 / 下载 zip
- 预制代码只写学生看得到的那段,前后藏起来的代码收进折叠;语法要求先拿标准答案自测
- 流程图两个互斥开关改成三选一;时间 / 内存限制能改了;草稿只给新建页
- 后端:读回测试点原文、语法要求自测、AI 想测试输入三个接口
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-09 20:07:34 +08:00
xuyue and Claude Opus 5.5
033a245abb
feat(SQL 题): 增删改题除了第 1 组,其余测试数据可以一行都不改——用来抓学生多改(WHERE 写漏、写宽)
...
Deploy / deploy (push) Canceled after 0s
原来每组数据都必须有被标准答案改到的行,所以「这组里没有研发部,看学生会不会把别的部门也改了」
这种测试数据建不了。现在只有第 1 组(题面上给学生看的那组)还要求改到行;其余组一行都没改时
出题页写「一行都没改」、预览写「执行后所有表都没有变化」。删掉第 1 组、别的组顶上来时自动按
新位置重查。判题本来就不管这一条,不受影响。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-09 19:17:42 +08:00
xuyue and Claude Opus 5.5
2cb39fd96b
feat(后台): SQL 题有了自己的出题页——左边写,右边就是学生看到的样子,改答案、改数据马上重跑
...
- 「新建题目」先选编程题还是 SQL 题,选了就不能改;编辑时按题目本身进对应的页
- SQL 出题页(设计稿「SQL 出题页」方案 B):测试数据一组一行,改完自动重跑,
跑不通的用中文说原因;保存时才打包上传测试点,不用再手动「预览」「上传」
- 新建时存草稿,误关页面不丢
- 编程题出题页去掉 SQL 那一堆分支(以前勾上 SQL 会静默清掉语法要求和流程图)
- 题目列表能按题型筛,SQL 题标题后面挂标签
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-10-09 18:59:15 +08:00
xuyue
bde387e8bc
fix
Deploy / deploy (push) Canceled after 0s
2026-10-08 12:29:35 +08:00
xuyue and Claude Opus 5.5
d07459928d
feat(标签): 题目列表的标签分成「知识点」「主题」两组,新建的标签在后台标出待归类
...
Deploy / deploy (push) Canceled after 0s
- 迁移 0022:problem_tag 加 category(knowledge / theme)和 category_confirmed;
存量 25 个题材、题集类标签归主题,其余归知识点,全部记为已确认
- 前台题目列表按两组显示标签,组内按题目数从多到少
- 后台标签管理:每行切换分类;出题时新建的标签默认知识点、标「待归类」并排在最前,
页头提示待归类数量,可点「确认」保留默认;加分类筛选
- 新接口 PUT /admin/problem-tags/:id/category,设置分类即视为确认
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-09-27 20:38:43 +08:00
xuyue
d25b5dbe6d
update
Deploy / deploy (push) Canceled after 0s
2026-09-27 19:36:28 +08:00
xuyue and Claude 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
xuyue and Claude 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
xuyue and Claude Opus 5
856b7a280e
feat(后台用户): 新增「在线优先」排序,列表直接显示在线标记
...
在线状态库里没有、会话也判定不了 —— session 的 TTL 是 7 天且每次请求续期,
「有会话」只说明这人一周内来过。新开一个 Redis sorted set(auth/presence.ts)
记最后活动时间,5 分钟内有活动算在线。
写入全搭在已有的 pipeline 上,不多一趟往返:登录、每个带鉴权请求的续期、
以及 touchSession —— 只挂着 WebSocket 不发请求的人靠最后这条,sweepSessions
每 60 秒一轮,所以窗口取 5 分钟,明显大于那个间隔。登出、改密码、禁用账号
会立刻把人摘掉;过期成员在后台读列表时顺手清理(整个 key 不能设 TTL,
ZADD 不重置 key 的 TTL,到期会把还在线的人一起抹掉)。
排序 orderBy=-online 先捞在线 id,SQL 里 case when 分两档,档内继续按
最近登录排;没人在线时那个 case 恒等于 1,直接省掉。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
Claude-Session: https://claude.ai/code/session_017xu912Rv5JUUuy6MqMcQW2
2026-09-07 19:30:45 -06:00
xuyue and Claude Opus 5
25b86ec17e
refactor(提交): 删掉从来没用起来的「提交互相可见」
...
Deploy / deploy (push) Has been cancelled
problem.share_submission(题目级)和 submission.shared(单条)两个开关,连同
判定分支一起删掉。
生产备份(2026-08-07)里的实际用量:956 道题只有 2 道开过 share_submission,
还都是 contest_id=1 的比赛题 —— 比赛未结束时那条分支根本走不到,等于一天都没
生效过。123140 条提交里 shared=true 的有 40 条,39 条在 2022 年、1 条在
2023-03-30,之后近三年零使用;出题页从来没给过题目级开关,单条分享的入口在更早
那版前端上,ojnext 和 OJ2 都没搬过来,OJ2 里 PUT /submissions/:id 一个调用方
都没有。
canViewSubmission 剩下三条:本人 / 管理员(比赛中的 Student Admin 除外)/ 本题
作者,其余一律看不到。结尾的 `shareSubmission || shared` 没了;它上面那条「比赛
未结束一律不给」也一并去掉 —— 走到那里的必然不是这三种人,现在无论比赛与否都是
false,留着是重复的。allowShared 参数随之消失,它唯一的用途就是分享开关的归属
校验。
一并删掉:PUT /submissions/:id、响应里的 shared / canUnshare(列表、详情、站内
信内嵌三处)、题目详情与后台题目里的 shareSubmission、shareSubmissionRequestSchema,
以及新建提交时那句 shared: false(列上本来就有 default false)。
两个数据库列保留不删,只在 schema.ts 上注明已停用:删列是破坏性迁移,而留着不读
不写零成本,还留着那 40 条的历史痕迹。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB
2026-09-07 07:18:41 -06:00
xuyue and Claude Opus 5
2256597838
fix(后台建题): 修好 dev 代理走错服务,以及新建页带出上一题 id、超长标签报英文
...
三处不相干的小毛病,都在「新建/编辑题目」这条路上。
vite 的 /api 代理写的是 localhost:3000。api 用 Bun.serve 起、只绑 IPv4 的
0.0.0.0:3000,而 Node 解析 localhost 时 ::1 排在前面 —— 别的项目的 dev server
一占 [::1]:3000,两边端口就不冲突(一个 v4 一个 v6,谁都不报错),浏览器发出的
/api/* 整个落到那个站上,表现是后台一进就被弹回首页。改成 127.0.0.1。
新建页和编辑页共用同一个 localStorage 草稿键,而编辑页会把服务器数据连 id 一起
写进去。没保存就离开的话,这个 id 跟着草稿漂到新建页:「下载测试点」下的是那道旧
题的包,SQL 测试点编辑器也会去回显那道旧题的脚本。新建分支进来先把 id 摘掉。
新标签没有长度限制,超过 32 字要到保存时才撞出 zod 的英文原文(Too big:
expected string to have <=32 characters)。把 32 提成契约里的
PROBLEM_TAG_MAX_LENGTH,输入框拿它做 maxlength,validateNewTags 再兜一道粘贴
绕过的情况,提示换成中文。校验的松紧没动,32 字仍然存得进、33 字仍然拦下。
editContestProblem 和 editProblem 函数体逐字相同(都是 PUT admin/problems/:id,
比赛由后端从题目自己推导),合成一个。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB
2026-09-07 07:18:23 -06:00
xuyue and Claude Opus 5
b130d805be
fix(后台题单): 修掉一串前后端字段名对不上导致的空列与失败操作
...
Deploy / deploy (push) Has been cancelled
后台题单接口下发的是扁平结构(displayId / title / isRequired),前端却按前台
题单页的嵌套结构取值,naive-ui 的 key 也不解析点号路径,于是「题目ID」「题目
标题」两列永远是空的。同一处还连累了另外几个操作:
- 编辑题目弹窗读 problem.problem.title,点「编辑」直接抛错;
- 添加题目发 problem_id / is_required,api 层取 problemId,出站是 undefined,
被后端按参数错误 400 掉;
- 编辑题目发 is_required,「必做」怎么改都不生效;
- 新建/编辑奖章发 condition_type,toBadgeBody 读 conditionType,同样 400。
根因是 admin/api.ts 把这批后台接口的返回值标成了 oj 侧的 ProblemSet /
ProblemSetBadge / ProblemSetProblem,加上 detail.vue 四个 handler 用 data: any
接弹窗数据,字段拼错编译器一句都不吭。现在返回值全部换成 Admin* 契约类型,
handler 换成 AddProblemToSetRequest / UpdateProblemInSetRequest 和一个本地
BadgeFormData(条件类型是「完成所有题目」时不带 conditionValue,后端补 0),
两个奖章弹窗里的 const data: any 也一并拆掉。
验证:dev 栈实跑,列表两列有值、编辑弹窗正常打开、加题 / 改必做 / 建奖章 /
改奖章四条链路走完界面都成功落库;bun run type-check 通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
Claude-Session: https://claude.ai/code/session_011WmMi9tPrnSAwwAy2i4WAa
2026-09-03 20:49:57 -06:00
xuyue and Claude 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
xuyue and Claude Opus 5
bd84599174
feat(自学): 教程和练一练都留痕,老师能看到谁学了多少
...
Deploy / deploy (push) Has been cancelled
自学模块以前一个字节都不落库:读到第几课只存在浏览器的 localStorage 里,
练一练的对错是组件内的一个 ref,刷新即失忆。老师能看到的只有「谁交了题」。
现在两张新表:
* tutorial_progress —— 一个学生 × 一课,记打开次数和累计停留秒数
* exercise_attempt —— 一个学生 × 一道练习,记试了几次、错了几次、
第几次做对的、最后一次做错时填的什么
都存聚合不存流水。练习那张表尤其明显:流水会随着学生反复点提交无限长,
而多出来的行回答不了任何新问题 ——「他第 3 次和第 5 次都选了 B」对老师
没有意义,「他试了 7 次才对」有。
停留时长只在页面可见、且十分钟内有过操作时才计。机房的电脑经常开着页面
就走了,不设这道闸的话「停留时长」会变成「电脑开机时长」,老师看到的
数字全是假的。换课、切标签页、关窗口都会先把攒着的秒数冲给**离开的那一课**。
练一练的对错仍然是前端判的:答案本来就随题面一起下发到浏览器,后端再判
一遍也挡不住任何人,只是重复实现七套判题。所以这是教学观察数据,不是成绩。
`last_wrong_answer` 存的是前端拼好的一句人话(「选了 C」「顺序 3-1-2」),
不是原始作答结构 —— 七种题型形状各不相同,存结构就得在后台按题型各写一套
渲染,而老师要看的只是他错在哪。
顺带修掉预测输出题的一个老问题:它的 `submitted` 一旦为真就不再收回,而
`allCorrect` 是跟着输入实时算的,于是学生错一次之后把答案改对,界面直接
跳成「输出正确!」、提交按钮同时禁用,submit() 再也执行不到 —— 这道题
**永远不会被记成做对**。排序/连线/找错/分组四种题本来就在交互处把 submitted
置回 false,只有这里漏了,按同一套补上。
学生端:目录每课显示「✓ 已读 · 11 分钟」和「练一练 3/5」。教程保持免登录
可读,未登录只是不留痕,并明说一句。
老师端:后台新开「自学情况」(教师及以上可进),三个 tab ——
按学生(默认把读得最少的排在最前,这张表要回答的是谁还没开始)、
按练习(每道题的正确率、一次做对几人、做对的人平均试几次;展开看逐人明细
和他们最后错在哪)、按课程。班级框填 3-4 位是具体班级,1-2 位当年级前缀。
外键用了库级 CASCADE,和 Django 建的那批 NO ACTION 不同:删教程、删用户
不必再记得回来手工清子表。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
Claude-Session: https://claude.ai/code/session_01GW5ef6C2kRW8Ru27ghCaUu
2026-09-01 08:54:27 -06:00
xuyue and Claude Opus 5
36a4663193
chore(前端): 清掉四处零引用的死代码
...
Deploy / deploy (push) Has been cancelled
按名字扫了一遍 api 层导出和 utils 常量,四处全仓零引用:
**`getContestProblem`**(admin/api.ts)—— 和紧挨在它上面的 `getProblem` 逐字
相同,同一个 URL、同一个类型。旧后端 admin 侧比赛题和公开题是两个端点,合并
之后这个壳留下来了。
**`createMessage`**(oj/api.ts)—— 两代前端都只有定义没有调用。参数名还是旧
后端那套(recipient / submission),函数体里再映射成契约的
recipientId / submissionId,等于给一个不存在的调用方写了个兼容层。
d3b05b8 删掉了它的类型 CreateMessage,函数漏了。
后端 `POST /messages` 是完整实现的,只是后台那个页面
(admin/communication/messages.vue)两代都是一句「未完待续」的占位。
在端点上加了注释说明这件事,免得下次扫「无人调用的端点」时被当成可删。
**`CONTEST_TYPE`**(constants.ts)—— 和上面 30 行处的 `ContestType` 枚举
一模一样。枚举那份有 8 处在用,这个对象零引用。
**`LANGUAGE_ID`**(constants.ts)—— Judge0 的语言 id。真正在用的那份在
utils/judge.ts 的 JUDGE0_LANGUAGE_ID,而且**只有那份是对的**:死掉的这份把
Golang / JavaScript / Python2 全写成了 0,谁要是拿它去调 Judge0,这三种语言
的在线试运行会静默发出 `language_id: 0`。
顺带确认过没有 `constants["LANGUAGE_ID"]` 这类动态取值。
验证:vue-tsc 0 error、vite build 通过、tsc(apps/api) 0 error、
check:routes 168 条无遮蔽。四处都是零文本引用的纯删除。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-25 23:32:50 -06:00
xuyue and Claude Opus 5
5c772319e9
refactor(契约): 公告两侧形状分开,判题状态码从契约派生
...
Deploy / deploy (push) Has been cancelled
## 公告:一个 schema 兼两种形态,兼出两处谎
前端 utils/types.ts 只手抄了**后台那份**公告形状,oj 侧也拿它当类型用。可
apps/api/src/routes/content.ts 的 `/announcements` 列表既不下发 content 也不
下发 visible —— 于是类型声称列表行有这两个键、运行时都是 undefined。今天没炸
只是因为组件恰好没读(正文是点开后另拉一次详情)。
契约那边则是另一头:announcementSchema 把 content 写成 `.optional()`,让一个
schema 同时兼列表和详情。代价是**详情**拿到的 content 类型也成了
`string | undefined`,组件只能 ?? 兜底。
现在两边都按后台侧早就在用的套路拆开:content 必填,列表用
`.omit({ content: true })` 派生。四个类型各归各位:
oj 列表 AnnouncementListItem 无 content、无 visible
oj 详情 Announcement 有 content、无 visible
后台列表 AdminAnnouncementListItem 无 content、有 visible
后台详情 AdminAnnouncement 有 content、有 visible
前端手抄的 Announcement / AnnouncementEdit / AnnouncementListItem 全部删掉,
AnnouncementEdit 改成从请求体派生(`CreateAnnouncementRequest & { id: number }`)。
后端 content.ts 的列表端点跟着换成 announcementListItemSchema —— 它本来就没
传 content,输出一字不变。
## SUBMISSION_RESULT 不再手抄
`-2 | -1 | 0 | ... | 10` 这 11 个码是从后端抄的,改成 `JudgeStatus | 9`:
后端那部分从契约派生,9 是前端本地的「正在提交」伪状态(后端永远不下发,
所以契约里没有,见 constants.ts 的 SubmissionStatus.submitting)。
这样 CLAUDE.md 说的「三处同步」才真的有人守:**实测过**,往
judgeStatusSchema 加一个 `z.literal(11)`,constants.ts 的 JUDGE_STATUS 立刻
报 TS2741 缺 '11' 的映射,加不上标签就编译不过。
顺带 useSubmissionMonitor.ts 里两处裸 `9`(各带一句 `// 9 = submitting`)
换成 SubmissionStatus.submitting,注释就不用写了。
## 顺手
TestcaseUploadedReturns 这个改名 re-export 去掉,直接用契约的
UploadTestCaseResponse(全仓 2 处引用)。
**没动 transforms.ts。** 之前把它记成「旧前端字段名的化石」,看下来判断有误:
filterResult 里 difficulty 要查 DIFFICULTY 映射表转中文、rate 要 getACRate 算、
status 要把 myStatus 翻成 passed/failed/not_test —— 是实打实的视图模型,不是
单纯改名。改它只会把计算逻辑挪个地方。
## 验证
tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、vite build
通过。因为动了后端响应 schema,起服务实跑了公告的四条路径:
- curl 三个端点逐个核对键集:oj 列表无 content/visible 且只出可见的那条、
oj 详情有 content、后台列表有 visible 无 content 且两条都在。
- 浏览器里 oj 公告列表渲染正常、点开正文能出来;后台列表两行齐全;
编辑页表单载入正确;改标题保存 PUT 200,库里 title 变了、visible/top 没被
带歪,保存后跳回列表并重新拉取。
(agent-browser 的 `find text 保存 click` 打不到 naive-ui 的按钮 handler ——
不发请求也不报错,一开始误判成保存坏了。改用 DOM 上直接 .click() 就正常,
是自动化的坑,不是应用的问题。)
冒烟用的两条公告已删干净。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-25 22:54:01 -06:00
xuyue and Claude Opus 5
0b7b08f2cc
refactor(前端): 去掉 { error, data } 信封,api2 改名 api
...
Deploy / deploy (push) Has been cancelled
信封是 Django 时代的形状:拦截器手工造一个**恒为 null** 的 error 字段,再把
真正的载荷塞进 data。后端 http.ts 的 success 其实只返回 { data },那个 error
从头到尾没人用 —— 全站成功路径读 res.error 的只有 admin/api.ts 的
resetPassword 一处,而它自己就是个把信封拆开再重新包一遍的 shim。
代价是每个调用点都要 .data 一次:47 个组件、3 个 api 层文件、200 多处。
现在拦截器直接返回 response.data.data,ApiResponse<T> 退化成 T,文件末尾那句
`as unknown as Api2Client` 的类型谎言也少了一层。失败路径不动,仍然 reject
`{ error: 错误码, data: 文案 }` —— 和成功路径不对称是故意的,成功没有错误码
可言,接口注释里写清楚了。
顺带把 api2 改回 api:utils/ 下早就没有 api.ts 了,"2" 是迁移期用来和旧
client 区分的,现在只剩下让人多想一秒的作用。
## 怎么改的
**没有全局 sed。** 先把客户端的返回类型从 Promise<ApiResponse<T>> 改成
Promise<T>,让 vue-tsc 把每一处报出来(210 条),再按它给的 file:line:col
精确删 `.data`(192 处),剩下的手工处理:
- 6 处 `const { data } = await ...` 解构 → `const data = await ...`
- 3 个 api 层函数(getProfile / getProblem / getSubmission)自己手工造信封,
改成直接返回值;getProfile 的返回类型跟着从 ApiResponse<Profile|null>
变成 Profile|null
**类型检查抓不到的,人工把剩下的每一处 `.data` 过了一遍** —— 载荷本身带
data 字段、或者载荷是索引签名时,`res.data` 照样过类型。这一遍捞出三条真 bug:
- `getTutorialList` 的载荷是 `{ [key: string]: TutorialListItem[] }`(按
python / c 分组)。索引签名让 `res.data` 编译通过、运行时是 undefined ——
改完信封之后教程列表会**两个 tab 全空且不报错**。实跑确认过修好了。
- `createExercise` / `updateExercise` 返回 `res.data`,而 Exercise 自己有
data 字段(练习内容)。两个调用方都不看返回值,所以类型和运行时都不响。
- `getSimilarProblems` 的 `.then(r => ({ ...r, data: r.data.map(...) }))`
删掉 .data 之后变成往对象里摊一个数组,能跑但形状是错的。
另外两处是**对的**,加了注释免得下次被"顺手清理"掉:
StatisticsPanel 的 `res.data` 是契约 submissionStatisticsSchema 自己的 data
字段(每个学生一行);download.ts 是独立 axios 实例,`res.data` 是 axios 的
响应体(zip 二进制,不走信封)。
## 验证
tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、vite build
通过。**因为这改动碰的是每一个请求,静态检查不够,起了全套服务用浏览器实跑:**
- oj 侧 12 个页面 + 后台 13 个页面逐个打开,断言没有重定向、console 无报错。
- 关键页面进一步断言渲染出了真数据(后台用户列表 3 行、题目列表 10 行、
站点配置表单三个输入框有值、教程列表分组正确)。
- 三条写路径实打:重置密码(库里 student123 → 531554,表格当场刷新)、
公告可见性开关(走 getAnnouncement + editAnnouncement,就是手改解构那处,
库里 visible t → f)、提交代码(POST → 判题机真跑出 -2 → 提交列表和详情页
都正确渲染状态、语言、代码)。
- /rank 有一条 `{error: "class-missing"}` 的未捕获 reject,stash 掉本次改动
复现同样报错,**是既有问题**,不在本次范围内。
本地 dev 库为了打通后台测试改了三处,都只影响本机:devadmin 补了 email 和
user_profile 行(原来缺这两样,getProfile 报 profile-not-found,AUTHED 存不
进去,所有 /admin 路由被守卫弹回首页)、密码重置成 devpass123。冒烟用的教程/
公告/提交三条测试数据已删干净,题目和用户的提交计数也回滚了。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-25 22:42:20 -06:00
xuyue and Claude Opus 5
9653f1541a
refactor(契约): SQL 展示数据进契约,topReaction 收成枚举
...
d3b05b8 把 SQLDisplay* 列进「保留不动的窄化」,理由是契约那边就是
Record<string, unknown>。这次把那个碗底扫了 —— 因为「契约说不知道形状、
前端手抄一份来渲染」正是那个 commit 修的五处分歧的同一个模子。
SQL 题两个 JSONB 列现在有精确 schema:sqlConfigSchema、sqlDisplaySchema、
sqlDisplayTableSchema、sqlDisplayColumnSchema。键名保持 snake_case,是
problem.sql_config / sql_display 的原文,回滚时旧后端要读同一份。前端
utils/types.ts 里手抄的 SQLConfig / SQLDisplay / SQLDisplayTable /
SQLDisplayColumn 共 30 行删掉改成 re-export,Problem 和 AdminProblem 的
Omit 列表各短两项。手抄那份把 SQLDisplayColumn.type 写成了可选,后端一直
是必有的空串。
后端跟着收紧:commonChecks / generateSqlDisplay 的 Record<string, unknown>
换成 SqlConfig,`sqlConfig.mode === "modify" ? "modify" : "query"` 这句防御
删了 —— 现在类型上就只有那两个值。请求侧 createProblemRequestSchema.sqlConfig
也不再是 record:mode 是枚举、order_sensitive 缺省补 false,正好是旧后端
SQLConfigSerializer 的口径(新后端之前反而比旧的松,什么都收)。
顺带修 adminProblemListItemSchema.topReaction 的注释:写着「当前后端恒传
null,get_top_reactions 没跟着迁过来」,但 admin/problem.ts:257 早就在调了,
只有比赛题列表恒 null。这条注释会骗人去补一个已经存在的实现。类型同时从
z.string() 收成 reactionKeySchema —— getTopReactions 本来就把库里认不出的
类型滤掉了,只是类型上没体现,前端因此得在 transforms.ts 写
`as AdminProblemFiltered["topReaction"]`,现在那个强转和 `?? null` 一起没了。
服务层里的 `as ReactionKey` 换成 isReactionKey 类型守卫。
**收紧 JSONB 的 schema 会把「存量数据形状不对」从静默降级变成 500,所以实打了:**
- 从生产库备份捞出 9 道 SQL 题的 sql_config / sql_display 原文,逐条过新
schema,9/9 通过;键集与旧后端 judge/sql_runner.py:build_display 的产出
逐字一致(columns/name/rows/total_rows/truncated,expected 两形态)。
- 把其中 query 形态、modify 形态各一条种进本地库,起 API 打 oj 详情和后台
详情共四个端点,全 200,expected 两种分支都正确解析。
- topReaction:插两条并列票,按 reactionKeySchema 顺序正确取到 confusing;
再插一条库里已下掉的类型,被守卫滤掉返回 null。
另:`bunx vue-tsc --noEmit` 不带 -p 是**无效的**,根 tsconfig.json 是
"files": [],塞个类型错误进去照样 exit 0。要跑 `bun run type-check`
(-p tsconfig.app.json),这次的结论出自它。tsc(apps/api) 0 error、
check:routes 168 条无遮蔽、vite build 通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-25 19:02:25 -06:00
xuyue and Claude Opus 5
78a2fb4fce
feat(排行榜): 重写全服 Top100,补上「我的排名」,后台那条挪进 admin
...
起因是 Top100 的「已解决」「提交数」两列一直空白:列的 key 还是
snake_case(accepted_number / submission_number),而数据早在 c2a8120 拆掉
转换层后就是 camelCase 了。naive-ui 按 row[key] 取值,取到 undefined 就渲染
空白、不报错 —— 和 d3348f9 是同一个病根。
顺着这条线把整个端点重写了:
**上限不再由调用方传。** `top` 参数原来有三个调用方各传各的(100 / 10 / 0),
而它会覆盖 limit 与 offset、total 却按全量算,正是 36e4ac2 那个「每页都是同样
100 条」的成因。现在 100 写死在服务端,参数只剩 limit / offset。
「全服 Top10」不需要另一个上限,它就是这个榜的第一页。
**排序补了第三档 asc(user.id)。** 前两个键完全相同的学生在真实数据里成片存在
(都是 0/0),没有稳定兜底键时 postgres 每次返回的顺序可以不同,翻页会看到重复
或漏掉的人。老代码缺这一档。
**新增 me(我的全服名次)。** 名次 = 排在我前面的人数 + 1,三个排序键逐级比较,
与列表的 orderBy 逐字对应 —— 少比一级就会出现「显示第 7 名、实际排在表格第 9 行」。
榜上高亮我那一行,名次超出 100 时在 footer 单独给一行。未登录、教师/超管返回 null。
**后台那条搬去 /api/admin/rankings/users**(requireSuperAdmin,无上限)。
原来它走的是公开端点的 top=0 分支,也就是任何匿名请求都能 ?top=0&limit=250
翻走全校学生名单和个性签名 —— 而 /profiles/:username 恰恰为了收紧枚举面才做了
「匿名一律返回空」,注释里还专门点了 /rankings/users 的名。这条页面本来就是
requiresSuperAdmin,它调的另外两个接口也都是 requireSuperAdmin,守卫对得上。
顺手去掉恒真条件 gte(acceptedNumber, 0):该列是 notNull default 0。
同一次扫了全仓 218 个表格列定义,筛出 70 个没有 render 的(只有这些才靠 key
直接取值),比对全部类型定义里的字段名 —— 除这两处外没有漏网的。`_id` 和
`test_case` 是真字段名,不能改。另外收掉两处同类的雷:
admin/setting/config.vue 手写的 `interface Testcase` 字段名和类型都是错的
(真实数据是 createTime: number,不是 create_time: string),改用契约的
OrphanTestCase;serverColumns 里 last_heartbeat / create_time 两个残留 key
有 render 兜着没出事,一并改正。
实测(造 120 个探针用户,含 3 个 AC 与提交数完全相同的并列,验完已清库):
122 人时 total=100;offset=95 末页 5 条;offset=100 越界返回空且不发 SQL;
并列三人稳定占据前三;student(ac=2) 拿到 rank=121 走「不在榜上」分支;
把 ac 调到 450 时 rank=4 且表格第 4 行正是 student(名次与行号对得上);
升成超管后 me 变 null;后台端点 total=121 无上限、keyword=probe_01 命中 10 条、
未登录 401;传 top=1 已被忽略。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-25 17:57:55 -06:00
xuyue and Claude Opus 5
1198895b54
refactor(前端): vue-tsc 清零,type-check 可以当 CI 门禁了
...
接上 vue-tsc 时有 143 条既有错误,上一批迁移带走 89 条,这里把剩下的清完。
大部分是噪音(未使用的回调参数、死变量、少数 unknown),但里面躺着两个真问题:
- 个人主页查不存在的用户会抛:getProfile 返回 null 时后面照样取
.acmProblemsStatus,靠 `!` 压着。加了早返回。
- getProblemSetProgress 我上一批标错了类型:后台这个接口返回的是裸数组,
不是分页信封(和 oj 侧的 /user-progress 不一样)。已改正并加注释区分。
其余处理:
- 未使用的回调参数按 TS 约定加 `_` 前缀(v-for 的项、供子类重写的空钩子、
路由守卫的 from);确认无用的局部变量直接删(clickX/clickY 算了点击位置
但飞出方向用的是随机角度,hasToday 下面已经用 lastDateOnly 判过,
prefix 算了周/月但标签里没用上)。
- App.vue 的 highlight.js 注册:Promise.all([...]).then(m => m.map(x => x.default))
会把元组塌成 (HLJSApi | LanguageFn)[],hljs 上就找不到 registerLanguage。
改成逐个取 .default。
- 给一批 api 补上契约类型(getMetrics / getHitokoto / getTutorials /
formatCode / getProblemBeatRate / getClassUsernames 等),契约相应补了
8 个 z.infer 导出。
- ContestRank.submissionInfo 和 AcmHelperItem.acInfo 在 api 边界窄化成
SubmissionInfo —— 契约里是 Record<string, unknown>(JSONB 原文)。
- FlowchartEditor 的 TS2589:vue-flow 的 Node 嵌套太深,ref<Node[]>([]) 的
UnwrapRef 推导撞上实例化层级上限,改成 ref([]) as Ref<Node[]>。
- api2.ts 的响应拦截器**故意**不返回 AxiosResponse(要把 { data } 信封剥掉),
类型上确实说不通,没硬掰,写注释说明为什么用断言。
- 题目列表的「随机」按钮在模板里一直是注释状态,对应的 getRandom 和
getRandomProblemID 一并删除(后端 /problems/random 保留不动)。
验证:vue-tsc 0 条,apps/api tsc、check:routes、web build 全通过;
修过 key 的列都打接口确认过字段真实存在。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-25 14:04:10 -06:00
xuyue and Claude Opus 5
c2a81209d7
refactor(前端): 拆掉 camelCase→snake_case 转换层,契约成为唯一真相
...
utils/legacy.ts 是迁移期的临时层:新后端一律 camelCase,而组件读的还是
旧 Django 的 snake_case,于是在 api 层做一次递归键名重写。它自己的注释就
写了「迁移完成后这一层应当整体拆掉」。现在拆了。
代价不只是那 96 处包装:每个响应都要递归遍历整个对象重写一遍键名,而且
utils/types.ts 和 packages/contract 是两份真相 —— 手抄的那份还抄歪了好几处。
做法是按域推进,每域都用 vue-tsc 相对基线做差,确认零新增错误后再往下走。
前端的类型现在一律以契约为准,只在必要处窄化(比如 languages/template 的键
窄化成 LANGUAGE),删掉的重复定义包括 WebsiteConfig、LoginSummary、
AchievementSummary、ProblemSet、Contest、User、Profile、AdminTag、
StuckProblem 等等,其中 ClassComparison 有两个组件各手抄了一份。
## 顺带修掉的真 bug
- 管理端公告列表的「可见」开关每次都 400:列表响应被契约 omit 掉了 content,
而更新接口要求 content 必填,toggleVisible 把列表行原样回传。而且是乐观
翻转、不 await 不 catch,管理员看到开关动了、实际没存也没有提示。
改成先 GET 整条再 PUT,加失败提示。
- 删有提交的题时只显示笼统的「删除失败」:前端还在 match 旧 Django 的英文
文案,而后端返回的是 problem-has-submissions + 中文。连同另外 8 处同类
匹配一起改成判错误码 —— 文案是后端随时能改的,match 文案改一个字就静默失效。
- SubmissionStatus.time_limit_exceeded 写成 `1 | 2`,TS 按位或算成 3,和
memory_limit_exceeded 撞了同一个值。后端 judge/status.ts 里这是分开的
两个码,按后端拆成 cpu_/real_ 两项。当前没有代码读这两个成员,但
CLAUDE.md 明确要求判题状态码三处同步。
- 流程图历史翻到没有提交的那一页会直接抛:契约里 submission 是 nullable,
被 any 掩盖成看起来非空。补了 null 分支。
## 契约里被逼出来的三处不诚实
- grade 写成 z.string(),但 averageGrade() 在没有可用数据时返回空串,
前端三张图表拿它查 Record<Grade,...> 会查出 undefined。按实际收紧成
z.enum([...,""]),四个查表点都补了「无评级」分支。
- difficulty 写成 z.string()。核对过生产库 dump:956 道题只有
Low/Mid/High 三个值(761/149/46)。收紧成枚举。
- topReaction 写成 z.string(),既对不上前端渲染的 {type,count},也对不上
旧后端 get_top_reactions 下发的形状。改成正确形状并注明当前恒传 null。
## 明确保留 snake_case 的 54 处
判题沙箱原始输出(cpu_time/exit_code/output_md5/compile_output)、
statistic_info 内容(err_info/time_cost/ast_results)、submission_info
JSONB(is_ac/ac_time/error_number,回滚时旧后端还要读)、SQL 判题引擎的
total_rows/order_sensitive/changed_tables、WebSocket 的 submission_id、
以及数据库选项键 enable_maxkb。每一处都在类型定义旁写了为什么不能改。
language 没有跟着收紧契约 —— 它是配置项、随时可能加语言,收紧会让新语言
在后端 parse 时直接抛。改在 api 边界一处窄化。
## 另外
- utils/http.ts 整个模块已是死代码(四处引用全是 import type),删除。
- profile 的 blog/github/school/major/language 五个字段全链路空转,没有
任何组件读,从契约到类型一并摘除(数据库列不动)。
- admin/account.ts 往 user_profile 塞的 totalScore 是 OI 模式遗留,表里
没这一列。Drizzle 按表定义拼列名会把它静默丢弃,所以没出过错,是死代码。
验证:vue-tsc 143 → 54 条且无新增,apps/api tsc、check:routes、web build
全通过;各域响应形状逐条打接口核对过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-25 13:44:24 -06:00
xuyue and Claude Opus 5
0f94e7ecbd
fix(阶段4): 两份独立安全评审的 Critical/Important 全部修掉
...
## 权限边界评审(后台 86 个 handler)
守卫本身一个没漏,问题全在对象级归属校验:
- C1 跨题单删奖章:先按 (id, problemsetId) 校验奖章归属再删,
否则拿自己的题单 id + 别人的奖章 id 就能把别人的 user_badge 删掉
- C2 make-public 无归属校验:补 canEdit,越权者拿不到题面
- I1 两个分析端点被 `/problems/:id` 遮蔽 —— Hono 按注册顺序匹配,
不是静态优先。挪到 `/problem-analytics/*`
- I2 from-public 只校验目标比赛归属:源题也必须是公开题库题
- I3 克隆比赛回传原比赛明文密码:克隆一律 password: null
- I4 upload-image 守卫比旧后端严,教师写题面会 403:收回 requireAdmin
两个互不可见的教师账号实跑复验,六条全部拦住。
## SQL 判题沙箱评审
- I-1 查询题只读被一句 `PRAGMA query_only=0` 关掉,实测 DML 拿到 AC。
query_only 自己就是个 PRAGMA,旧实现靠 authorizer 把 SQLITE_PRAGMA
一律拒了才没这个洞。现在 runStudent 逐语句拦 PRAGMA(用
sqlite3_normalized_sql 判关键字,注释和大小写由 SQLite 抹平),
并在每条语句前重放 query_only 和 max_page_count 兜底。
顺带修掉 M-1 里 max_page_count 学生可自行调大的部分。
- I-2 单条语句进了 step() 就打断不了,只能等父进程 SIGKILL,
而兜底时限是整个作业一口价 25s —— 1s 限的题要 26s 才判 TLE,
判题池只有 2 个槽,几发死循环就能把所有人堵住。
改成分阶段:子进程用 stderr 报 prepare/student/display,
父进程边读边换表,一进学生 SQL 就把兜底收到「题目时限 + 2s」。
实测 26s → 3.06s。
归因也跟着修了:卡在受信脚本(出题人的初始化脚本、标准答案)
现在报 SYSTEM_ERROR,不再当成学生超时甩 TLE。
engine.ts 头部那张防护对照表按实测重写 —— 原来那版把 query_only
写成等价于 authorizer 白名单,是不成立的。另记一笔:stock sql.js 的
wasm 没导出 progress_handler / interrupt / set_authorizer / limit,
想要得自己编,别再去翻了。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 21:59:42 -06:00
xuyue and Claude Opus 5
e3faa689e7
feat(阶段2补课): SQL 判题链路 + 最后两个后台端点
...
新后端此前完全没有 SQL 判题(旧 judge/sql_runner.py 378 行 + sql_dispatcher.py
113 行无对应实现),阶段 2 纵切时漏了这条与沙箱完全不同的路径。
judge/sql/engine.ts 判题核心,移植自 sql_runner.py,判定口径逐条对齐
judge/sql/child.ts 子进程入口
judge/sql/index.ts 父进程:spawn + 硬超时
judge/run.ts language === "SQL" 时分流,不经判题沙箱
POST admin/sql-test-cases/preview 题目页展示数据预览
POST admin/sql-test-cases/generate AI 按标准答案倒推初始化脚本
题目保存时重新生成 sqlDisplay(对齐旧 generate_sql_display):取测试点 1 的初始化
脚本 + 标准答案跑一遍,失败一律拦下不让保存 —— 展示数据直接决定学生看到的表结构
与期望结果,宁可不保存也不能存错的。
## 防护换了实现,逐条实测
bun:sqlite 没有 authorizer / progress_handler / setlimit,且实测 Worker.terminate()
杀不掉跑飞的查询(原生代码占着线程)。改用「WASM 引擎 + 独立子进程」:
ATTACH → WASM 无宿主文件系统绑定,结构上够不到(比旧的 authorizer 更强)
查询题只读 → PRAGMA query_only=1
超时 → 子进程外部 SIGKILL
单值内存 → 子进程 ulimit -d
八条提交实测:正确→Accepted;列少一个/漏过滤→Wrong Answer;语法错误→Compile Error;
查询题里 INSERT→运行错误并说明;递归 CTE 死循环→CPU 超时;hex(zeroblob(2e8))→内存超限;
attach '/etc/passwd'→打不开。
## 踩到的两个坑(已写进 docs/specs/phase3-coverage.md)
1. ulimit 必须用 -d 不能用 -v。-v 限虚拟地址空间而 JS 引擎预留巨量地址,实测 -v 之下
Bun 退出时有概率 panic(SIGILL),结果早已写出但进程异常终止,父进程读到空串
误判成超时 —— 6 次里坏 2 次。换 -d 后 12/12 稳定。
2. 子进程写完结果直接 SIGKILL 自己,不走 process.exit()——后者仍有清理会撞限额。
至此 admin/api.ts 已无任何指向旧后端的调用。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 17:03:41 -06:00
xuyue and Claude Opus 5
007ae8619b
feat(阶段4): SQL 测试点脚本回显
...
GET admin/problems/:id/sql-scripts
只读磁盘上的 N.sql,不需要 SQL 引擎 —— 同组的 sql-preview / sql-ai-gen 要跑 SQLite
生成展示数据,新后端还没有那条链路,那两个仍指向旧后端。
实测:SQL 题回显两个脚本内容正确;测试点不是 SQL 类型返回 409 并说明;题目不存在 404。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 16:45:15 -06:00
xuyue and Claude Opus 5
cd5dd16f3b
feat(阶段4): 测试用例压缩包上传与下载
...
POST admin/test-cases
GET admin/problems/:id/test-cases (返回 zip 二进制)
落盘格式必须与判题沙箱镜像的约定一致(沙箱直接读挂进去的目录),已用真判题验证:
上传 zip → 建题 → 提交 Python 解法 → 沙箱读到用例并判出 Accepted。
安全与健壮性上比旧后端多做的几件事:
- **zip slip 从设计上进不来**:不遍历压缩包条目,只按精确文件名(`N.in`/`N.out`/`N.sql`)
取内容,条目名一律不参与路径拼接。实测带 `../../etc/passwd` 条目的包能正常处理,
且只取到 1.in/1.out。
- 单文件 32MB、解压后总量 128MB、测试点数 500 的上限,防 zip bomb 与写满磁盘 ——
旧后端一概没有,机房那台机器盘写满之后判题也会一起挂。
- 坏 zip 返回 400 而不是 500。
对齐旧后端的细节:CRLF→LF 归一;`stripped_output_md5` 按 Python `bytes.rstrip()`
的口径只剥尾部 ASCII 空白后再算(实测与 hashlib.md5 结果一致);编号从 1 起连续、
遇缺口即停;SQL 包至少 2 个测试点(题目页会展示测试点 1 的期望结果,只有一个时
学生可以对照着硬编码 AC);目录 0710、文件 0640。
## 顺带修掉一个只在判题时才暴露的路径 bug
config 里的相对路径(data/test_case、data/avatar、data/upload)原先按进程 cwd 解析,
而起服务的方式会把 cwd 切到 apps/api/,于是测试点落在 apps/api/data/ 下 ——
但 docker/compose.dev.yml 把**仓库根**的 data/test_case 挂进判题沙箱。两边不是同一个
目录,新传的测试点判题时会「找不到测试数据」,且只在真正判题时才暴露。
改成一律按仓库根解析,实测沙箱能看到新传的目录。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 16:42:54 -06:00
xuyue and Claude Opus 5
e396d78a07
feat(阶段4): 题目管理 / 比赛题目 / 比赛题⇄公开题互转
...
GET/POST admin/problems
GET/PUT/DELETE admin/problems/:id
POST admin/problems/:id/make-public
GET/POST admin/contests/:contestId/problems
POST admin/contests/:contestId/problems/from-public
合并了旧后端拆开的两套路由:旧 admin/problem 与 admin/contest/problem 的
GET 详情、PUT、DELETE 都只按题目 id 取、比赛是从题目推导出来的,分开没有意义,
还逼前端多传一个它未必知道的 contestId。现在共用 /admin/problems/:id。
归属判断按旧后端的口径分开:**比赛题看比赛的创建者,公开题看题目自己的创建者**
(旧 ensure_created_by(problem.contest, user) vs ensure_created_by(problem, user))。
一道比赛题的 created_by 可能是克隆时的操作人,跟谁有权改它没关系。
题号唯一性的作用域也跟着走:公开题在全部公开题里唯一,比赛题在本场比赛内唯一。
删题不删测试用例目录,与旧后端一致(它把 rmtree 注释掉了):删错了还能从磁盘捞回来,
而误删的测试数据没有别处备份;孤儿目录另有清理入口。有提交记录的题目拒绝删除。
## SQL 题目前只能编辑、不能新建
新后端**没有 SQL 判题链路** —— 旧 judge/sql_runner.py(378 行)+ sql_dispatcher.py
(113 行)无对应实现,languages.ts 里也只有 C/C++/Java/Golang/JavaScript/Python3 六种。
题目页给学生看的表结构与期望结果(sql_display)要跑 SQLite 现算,算不出来就没法建题。
因此:新建 SQL 题返回 501 并说明原因;**编辑已有 SQL 题时原样保留 sqlDisplay 不动** ——
不动它比生成一个错的安全,它直接决定学生看到的期望结果。SQL 的前置校验
(必须是唯一语言、要有 sql_config、要有 SQL 标准答案)照旧执行。
实测:学生 403;缺样例/缺输入描述/SQL 混语言三条校验文案与旧后端一致;
题号重复 409、跨比赛题号互不影响;公开题加入比赛后计数归零且标签带过来;
比赛题转公开后 visible=false、contestId=null、重复转 409;删除级联正常。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 16:37:20 -06:00
xuyue and Claude Opus 5
764e0d28cc
feat(阶段4): 标签管理 / 批量打标签 / 题目可见性 / 卡点与 AC 趋势 / 流程图 AI
...
GET/PUT/DELETE admin/problem-tags[/:id]
POST admin/problems/batch-tag
PUT admin/problems/:id/visibility
GET admin/problems/stuck
GET admin/problems/ac-trend
POST admin/problems/flowchart
要点:
- 后台标签列表用 leftJoin 且不加 having,能看到 problemCount=0 的标签 ——
那正是要清理的那些。oj 侧的 /problem-tags 才过滤 >0。
- 标签改名撞上已有标签视为合并:只给「还没挂目标标签」的题目补关系,
否则会撞 (problem_id, problemtag_id) 唯一约束。
- 批量打标签:add 时按需新建标签、remove 时只认已有标签 ——
否则「移除」会顺手造出一堆空标签。名字去重且大小写不敏感。
- **旧 ProblemVisibleAPI 的 `self.error(...)` 少写了 return**,题目不存在时会继续
往下跑并抛 AttributeError(500)。这里正常返回 404。
顺带记下一条阶段 5 的必做项(见 phase3-coverage.md 文末):本地库按显式 id 从生产
导入,序列没跟着走,第一次新建标签就撞 problem_tag_pkey。只要切换流程里有
「导出→导入到新库」这一步,就必须重置全部序列,否则读全正常、第一次写才炸。
实测:学生 403;大小写重复与空白名去重后 tagCount=1、重复 add 幂等、
remove 不存在的标签 404 且不新建;纯改名 merged=false、合并 merged=true
affectedCount=1 且只剩一个标签;可见性取反两次复原、不存在的题 404。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 16:30:48 -06:00