 xuyueandClaude Opus 5.5
|
11368813cc
|
feat(流程图): 「写代码 / 画流程图」变成工具栏上的切换,AI 的评分和历次分数挪进结果页签,题单里也能画
- 流程图题的工具栏最左边是「写代码 / 画流程图」。原来流程图藏在语言下拉里,叫一个名叫
Flowchart 的「语言」;画完一道题之后它还被记成语言偏好,下一道能画的题一打开就是画布
- 「交给 AI 点评」交完切到「结果」页签:评的时候写着「AI 正在看你的图」、画布照样能改;
评完是分数、评语、改进建议、四项得分,顶上一排历次分数,点哪次看哪次。
原来是提交按钮旁一个 1000px 的弹框,盖住画布,翻历次评分要一页一页点
- 画到 A / S:给「照着它写代码 →」,题单里 1.5 秒后回题单(和代码通过一样),
课堂条当场标成做完;只在这一页上刚评完的那次才算,进页面读到以前的 A 不跳
- 「把这一版载回画布」只在画布和那一版不一样时出现;载回、进页面读回草稿后都缩放到装下整张图
- 题单入口能画流程图了(原来只有题库入口能画),题单里的代码草稿、流程图草稿各存一份,
加入题单之前写的、画的不会一进题单就摆在编辑器里
- 手机上不给「画流程图」(节点库是拖放,手机上拖不动),题面里以前画好的那张照样看得到
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-28 18:19:16 +08:00 |
|
 xuyueandClaude Opus 5.5
|
5cb7cfa924
|
refactor(流程图): 评分状态挪进 store——交完切去写代码,这次的评分结果不再丢
Deploy / deploy (push) Canceled after 0s
SubmitFlowchart 只在语言选成 Flowchart 时才挂载,评分的全部状态(WebSocket 订阅、
轮询兜底、3 分钟超时)都长在它身上:交完马上切去写代码,组件一卸载就断开连接,
正在评的那次结果丢了 —— 不提示分数,评到 A/S 也不亮到「我的流程图」。
- oj/store/flowchart.ts:提交、评分跟踪、评完的提示、A/S 亮图、最近一次分数;
换题和离开页面时收掉
- SubmitFlowchart 只剩从画布取图和评分详情弹框
- 顺手补了登录检查:原来没登录也能点,请求发出去才被拦,只看到一句「提交失败」
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-28 13:49:45 +08:00 |
|
 xuyueandClaude Opus 5.5
|
1b149a4f3f
|
fix(题目): 在题目页里直接换题(下一题、题号直达)后,例子和语言还停在上一道题
Deploy / deploy (push) Canceled after 0s
题目页换题是同一个组件复用、只换路由参数,几处只在创建时读一次的状态就停在了上一道题:
- 例子:新题下面摆着旧题的例子,「测试」也拿旧例子比对
- 语言:下拉选项不跟着变,也不回退——从 Python 题跳到 SQL 题,语言停在 Python,提交必被拒
- 比赛 / 题单的编辑器只在挂载时载入一次代码
- 流程图:分数按钮、评分弹框、「我的流程图」还是上一道题的,上一道题还没评完的那次会挂到新题上
语言回退挪进 problem store 的 supportedLanguage(),两个编辑器在载入草稿之前调用,
保证先定语言再读这门语言的草稿。没有用给整块加 key 强制重新挂载的办法:课堂协作的
绑定依赖组件复用,重挂载会让协作中切题的老师语言恢复不了。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-28 09:15:50 +08:00 |
|
xuyue
|
d25b5dbe6d
|
update
Deploy / deploy (push) Canceled after 0s
|
2026-09-27 19:36:28 +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
|
572a818d49
|
style: 按 Oxfmt 重新格式化
Deploy / deploy (push) Canceled after 0s
换格式化器带来的差异,只有 5 个文件:长联合类型改成每项一行、以 | 开头。
和上一个提交分开,方便 blame 时跳过(git blame --ignore-rev)。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-27 15:37:23 +08:00 |
|
 xuyueandClaude Opus 5.5
|
7a390323fe
|
refactor(前端): 接口错误改成 ApiError 类,catch 不再写 any
拦截器原来 reject 一个裸对象 `{ error, data }`,没有类型也没有类型守卫,于是 46 处
`catch (err: any)` 直接读 `err.data` / `err.error` —— 代码 bug 抛出的 TypeError 也按
接口错误读,弹出来的是 undefined 或英文。
- utils/api.ts:reject 改成 `ApiError extends Error`,字段名沿用 error(错误码)/
data(文案),存量读法语义不变;加 isApiError / errorCode / errorMessage。
errorMessage 对非接口错误一律用 fallback,不把 TypeError 的英文甩给学生
- 46 处 catch 全部换掉 any(45 处接口错误 + pythonSyntaxCheck 里 Skulpt 的错误对象)
- 断网等没有响应体的情况,文案从英文 "Request failed" 换成「网络异常,请检查网络后重试」
浏览器实跑核过:登录密码错误仍显示「用户名或密码不正确」(错误码分支)、
接口错误 reject 出来是 ApiError、网络不通时拿到中文文案、非接口错误走 fallback。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-27 15:12:40 +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
|
8861393529
|
fix(流程图): 评分明细的顺序是乱的,40 分的那项排在最后
Deploy / deploy (push) Has been cancelled
AI 是按分值从高到低返回的:`逻辑正确性(40) → 完整性(30) → 规范性(20) → 清晰度(10)`。
但 `ai_criteria_details` 存在 jsonb 列里,而 Postgres 的 jsonb **不保留键序** ——
它按「键长度 + 字节序」重排,读出来变成 `完整性 → 清晰度 → 规范性 → 逻辑正确性`,
权重最高的那项被排到了最后。
评分弹框和教师端的评分详情都是直接 `v-for` 遍历这个对象,所以两处都乱。
统计面板因为用的是写死的 `CRITERIA_ORDER`,一直是对的 —— 于是同一份数据在两个
地方的顺序还不一致。
把顺序抽到 `utils/constants` 共享,三处统一走 `sortFlowchartCriteria()`;
表里没有的键排到后面,AI 万一返回别的评分项也不会丢。
顺带把限流的提示改得能看懂:撞上 429 时原来只显示「流程图提交失败」,学生会以为
是自己的图有问题然后反复点,越点等得越久。现在按错误码分支,提示「提交太频繁了,
缓一会儿再交」。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-27 10:53:29 -06:00 |
|
 xuyueandClaude Opus 5
|
5f0fe713dc
|
fix(流程图渲染): 出错后再也画不出来、每失败一次往 body 漏一个 div
## 渲染失败后永久空白
四个渲染点都是同一个写法:
<n-alert v-if="renderError" ... />
<div v-else ref="mermaidContainer"></div>
一旦渲染出错,容器被 `v-else` 卸载,`mermaidContainer.value` 变成 null。而
`renderFlowchart` 一进来就先清 `renderError`、再因为 `!container` 直接 return ——
于是**报错提示消失了,图也再画不出来**,只剩一块空白,只能刷新页面。翻历史提交
最容易踩到。改成容器常驻、用 `v-show` 隐藏,和 MermaidEditor 本来的写法一致。
`FlowchartScoreDetail` 那处 Teleport 上不能挂 v-show,把条件挪到内层容器。
实测:修复前切回合法代码后 0 个 svg(永久空白),修复后正常渲染。
## 每次渲染失败都往 body 漏一个 div
`m.render(id, code)` 没传容器,mermaid 会在 `document.body` 上建一个临时
`div#d{id}`。而 `suppressErrorRendering` 默认关着,看 mermaid 源码,解析出错时
是先 `errorRenderer.draw()` 再 `throw`,**清理临时容器的那行在 throw 之后**,
永远执行不到;每次 render 用的又是新的随机 id,`removeExistingElements` 也清不掉
旧的。于是渲染失败一次就留一个。
出题页是边敲边预览,且没有防抖,每个字符触发一次完整渲染,中间态几乎全是语法
错误 —— 实测逐字符敲 26 个字符,body 里留下 16 个残留 div。打开
`suppressErrorRendering`(该分支是先清理再抛)+ 预览防抖 300ms,实测降到 0,
预览功能不受影响。
## 顺带
`loadMermaid` 缓存的是实例,两个组件同屏挂载时会双双落进 `if (!mermaidInstance)`,
import 和 initialize 各跑两次。改成缓存 Promise,并在失败时清掉缓存,避免一次
网络抖动把后续所有渲染都钉死在这个失败结果上。
`SubmitFlowchart` 的 `updatePage` / `openDetailModal` / `loadToEditor` 一并补了
错误兜底(同文件,见下一个提交的说明):前两个失败时 `rendering` 会卡在 true,
弹框永久转圈;`loadToEditor` 是裸 `JSON.parse`,老提交数据坏掉就点了没反应。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-27 06:14:40 -06:00 |
|
 xuyueandClaude Opus 5
|
64facc5701
|
fix(WebSocket): 断线不重连、评分结果会丢、登出后连接还活着
Deploy / deploy (push) Has been cancelled
排查 WS 这一块时发现的一批问题,多数是「机制写了但从没生效过」。
## 重连
`disconnect()` 里 `enableAutoReconnect = false`,而 `connect()` 从不改回 true ——
登出再登录后,这条连接就永远失去了自动重连能力(configUpdate 那条 watch 上尤其
明显)。改成用 `closedByUser` 表达「用户主动断开」的意图,和 `enableAutoReconnect`
这个**配置**分开。
`scheduleDisconnect` 的回调里断完紧接着一句 `enableAutoReconnect = true`,而
close 是异步的 —— 等 onclose 跑到时标志已经翻回来了,1 秒后又自动连上。那个
「15 分钟空闲省资源」从来没真正断开过。现在只断开,不做事后翻转。
重连的 setTimeout 没存句柄,组件卸载后照样触发 `connect()`,在已销毁的组件上
又建一条连接。现在 `disconnect()` 里 clearTimeout。
退避从「线性 ×5 次」改成「指数 + 抖动、30 秒封顶、次数不封顶」。原来 1+2+3+4+5
只有 15 秒,后端 deploy 重启一次就超了,之后这条连接死到用户刷新为止。抖动是
为了避免一个班几十台机器在同一毫秒一起冲回刚起来的后端。另挂 online /
visibilitychange,网络恢复或切回标签页立刻重连,不必等退避走完。
所有 socket 回调改成闭包住局部 ws 并在入口 `if (ws !== this.ws) return`,旧连接
迟到的 onclose 不再污染新连接的状态 —— 也是让 `disconnect()` 能被 onclose 识别
出来的关键。
## 订阅重放
`pendingSubmissionId` 一发送成功就清空,它只解决了「还没连上就 subscribe」,
**没解决断线重连**。而真正会丢结果的恰恰是后者:服务端收到 subscribe 会回一份
当前状态,掉线期间错过的推送就是靠这次重放补回来的;不重新订阅,重连后只收得到
「将来」的事件,可结果已经是过去式了。改成订阅意图保留到显式 `unsubscribe()`,
每次 onConnected 都重发。
这套逻辑原来只有 SubmissionWebSocket 有,FlowchartWebSocket 是 send 失败打一行
日志了事 —— socket 一掉,那次评分结果就再也回不来,按钮一直转圈。提成公共基类
SubscribingWebSocket,两条通道共用。
流程图另加轮询兜底:提交后 5 秒 WS 还没出结果就每 3 秒拉一次,读 status 2/3
结算,3 分钟上限。判题那边一直有兜底,流程图这边没有,而 Redis pub/sub 是发完
不管的,worker 推的那一刻连接不在就永远丢了。
`useSubmissionMonitor` 里 `watch(wsStatus, ..., { immediate: true })` 的回调在
watch() **返回之前**就同步跑了,已经连着时 `unwatch` 还是 null,if 不成立,
watcher 永远停不掉:每提交一次泄漏一个,往后每次重连它们都会把各自那个早就判完
的旧 submissionId 重新订阅一遍。整块删掉,直接 subscribe —— 基类已经管了时序。
WS handler 原来不校验 submissionId。学生同时开着几道题的页面时,每条连接都订在
同一个用户 topic 上,别的页面的评分结果会被当成自己的。
## 会话
握手时校验过一次会话就再也不管了,这条连接却能挂几个小时:用户在别的标签页
登出、或者会话本身到期,旧 socket 照样收推送。加一条 60 秒一轮的巡检,用
Redis EXPIRE 一条命令同时完成「判断存在」和「续期」(续期是必要的:只开着页面
挂 WS 的人一次 HTTP 请求都不发,不该被算成不活跃踢下线)。Redis 抛错时整轮
放弃,绝不因为一次抖动把全班踢下线。
禁用只改数据库的 isDisabled 列、不动 Redis 里的会话,巡检永远发现不了。加
`session:revoked` 频道主动通知,**两种作用域不能混**:
{ token } 用户登出。只断这一张会话 —— 同一个人在别的设备上是另一张
会话,按 userId 广播会把他手机上的登录一起踢掉
{ userId } 账号被禁用。所有设备都得断
先发一帧 force_logout 再隔 100ms 断开。只断不发的话前端只看到一次普通掉线,
会照常重连、页面上还显示着登录态。token 不进帧里 —— 那是 httpOnly cookie 的值,
推到 WS 上就等于交给了 JS,匹配全在服务端做。
前端在协议层拦截 force_logout(和 pong 一样,不下发给业务 handler),并主动
disconnect —— 否则会一路 401 重连到退避上限,正是这机制要消掉的浪费。表现刻意
和 utils/api.ts 里 account-disabled / login-required 两支保持一致:同一件事从
HTTP 和 WS 两条路进来,学生看到的不该有两个样子。
## 开销与安全
`void handleMessage(...)` 是裸的,里面有两次 DB 查询和一个会抛的 schema.parse,
库抖一下就是一个 unhandled rejection(隔壁 bridgeSubmissionEvents 两处都接住了,
只有这里漏了)。
ping 提到用户查询之前。原来的顺序是「先查 user 再看消息类型」,每个客户端每
30 秒都要为一次心跳打一趟数据库。禁用用户不会因此漏网:推送路径上 bridge 会查,
subscribe 这条真正读数据的路径下面照样查。
bridge 两条 per-user 通道都先看 `server.subscriberCount(topic)`,没人订阅就别
查库了 —— 判题高峰期绝大多数事件的目标用户此刻并不在线。
flowchart 评分失败原来把 error.message 原样推给学生、前端直接弹出来,AI provider
的地址和内部报错就这么进了浏览器。改成真实原因写服务端日志。
加每连接令牌桶(20 突发 + 每秒回填 2)。一条 subscribe 在服务端是一到两次数据库
查询,一个学生开着一条 socket 狂发就能压住库。正常流量离阈值几十倍远。
升级时校验 Origin。会话 cookie 是 SameSite=Lax、WS 握手不是导航,跨站页面本来就
带不上 cookie,所以这是防御纵深不是唯一防线。同源放行;本机开发(Vite 5173 →
API 3000)自动放行,且只在两边都是本机时成立 —— 生产环境 url.hostname 是正式
域名,这条永远不触发;跨域部署走 ALLOWED_WS_ORIGINS。不发 Origin 的一律放行:
真正的攻击面是带着受害者 cookie 的浏览器页面,而浏览器一定会带 Origin。
顺带:useConfigWebSocket 的 handler 从 onMounted 挪到同步注册(调用方在 setup
阶段就 connect() 了),删掉每条消息打完整内容的 console.log 和死字段
ws.data.username。
## 没动的
题目页上并没有两条 /ws/submissions —— Form.vue 里 SubmitFlowchart 和 SubmitCode
是 v-if/v-else,互斥。学生实际是 2 条连接:全站一条 /ws/config + 一条
/ws/submissions,正常,不必合并。
## 验证
55 个用例,分六组打桩跑(假 WebSocket + 假计时器;会话/限流/吊销三组对着真
Redis):
重连语义 7 断开后不再自我复活、卸载后计时器已取消、退避封顶、
online 立即重连、旧 onclose 不污染新连接
订阅重放 9 未就绪时补发、重连后重新订阅、unsubscribe 后不再重放
会话巡检 8 EXPIRE 三态、只断失效的、同 token 只查一次、
Redis 抖动时一个都不踢
Origin/限流 13 跨站与跨端口拒绝、生产不因 localhost 开后门、
突发额度、按时间回填
强制登出 13 登出只断同 token(别的设备不受牵连)、禁用断所有设备、
巡检先通知再断
前端登出 5 两支表现、收到后不再重连、两条通道只处理一次
前两组做了改动前/后对比,老代码该挂的都挂了 —— 「重连后自动重新订阅」正是这么
跑出来的,此前我以为 pendingSubmissionId 已经覆盖了这种情况。
apps/api 的 tsc 和 apps/web 的 vue-tsc 都干净,仓库既有测试照常通过。
**SubmitFlowchart.vue 的轮询兜底未经运行时验证** —— 在 SFC 内,没搭组件挂载
环境,只过了类型检查和人工核对。要验的话,停掉 worker 提交一次流程图,看 5 秒
后是否转入轮询、3 分钟后是否给出超时提示。
这批改动动了 WS 的行为面(限流会断连接、Origin 会拒绝、巡检会踢会话),上线前
建议手测:提交代码看判题、切流程图看评分、后台改配置看全站生效、开两个标签页
在一个里登出、禁用一个在线学生看另一端反应。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-27 05:07:26 -06:00 |
|
 xuyueandClaude 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 |
|
 xuyueandClaude 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
|
ae1fb329b5
|
feat(阶段1): 搬入 ojnext 为 apps/web,未改业务代码
|
2026-08-06 21:18:16 -06:00 |
|