 xuyueandClaude Opus 5
|
d3b05b8629
|
refactor(契约): 补齐 80 个类型导出,前端不再手抄形状
契约原来有 184 个 schema 但只导出了 104 个类型,缺的那 80 个前端只能照着
手抄一遍 —— 这是「表格列静默空白」那一类 bug 的根因(d3348f9、2edb8cf,
以及上一个 commit 修的 Top100 两列)。现在 186 个 schema 对 186 个类型,
一一对应,下次要用直接 import。
补导出是机械的(fooSchema → Foo),零命名冲突。真正有价值的是换的过程中
契约逼出来的 5 处分歧 —— 手抄那份在说谎,而 vue-tsc 拦不住,因为类型说它是对的:
- Tutorial.createdBy 手抄成了可选的 `User`(即 AdminUser,带 email、
rawPassword),后端下发的是必有的 SampleUser。读 createdBy.email 会拿到
undefined。列表页因此被迫写 `row.createdBy?.username` 和 `row.createdAt!`,
换成契约类型后两处断言都不需要了。
- TutorialListItem 手抄成 `Omit<Tutorial, "content">`,但后端列表接口连 code
一起省了 —— 类型声称 code 在。
- Testcase 手抄成 `{input_name, output_name, score}`:响应实际有 5 个字段,
且**没有 score**。score 是上传完成后前端按测试点数量平分补上去的,手抄那份
把本地字段说成了响应字段。现在写成 `TestCaseEntry & { score: string }`。
- Tag 手抄成 `{id, name}`,契约是 `{id, name, problemCount}` —— shared/api.ts
只好用 `Tag & { problemCount: number }` 把丢掉的补回来。
- CreateMessage 是旧后端按名字投递的形状(sender/recipient/submission),
契约要的是 recipientId/submissionId。全仓零引用,删掉。
同时删掉另外两个零引用的手写类型:LANGUAGE_SHOW_LABEL、UserAdminType;
本地重复的 SampleUser 换成契约的;oj/problem/list.vue 里本地第三份 Tag 改成
`ContractTag & { checked: boolean }`。
需要收窄的一律**从契约派生再收窄**,字段名跟着契约走,只有真正本地的那一两个
键是自己的:
export type Exercise = Omit<AdminExercise, "data"> & { data: 七种题型的联合 }
export type SubmitCodePayload =
Omit<CreateSubmissionRequest, "language"> & { language: LANGUAGE }
保留不动的窄化:StatisticInfo / SubmissionInfo(判题 JSONB 原文,snake_case)、
SQLDisplay*、ProblemFiltered(视图模型)、Exercise*Data(契约里 data 就是
Record<string, unknown>,七种题型结构不同,后端本来也不校验)。
types.ts 的手写 interface 从 31 个降到 22 个。
顺带修的代码:admin/tutorial/detail.vue 新建教程的表单对象缺三个后端产出的
字段,加了 TutorialEdit(对齐 BlankProblem / BlankContest 的写法);三处测试点
上传原来是拿响应对象原地塞 score,改成 map 出新对象,分数算法一字未改。
验证:apps/api tsc(7.0.2) 0 error、check:routes 168 条无遮蔽、
apps/web vue-tsc 0 error、vite build 通过。改动绝大部分在类型层,运行时只有
测试点上传那三处(等价替换)—— **那条路径要传 zip 才能实跑,没有实打**,
只做了代码等价性核对。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-25 18:07:01 -06:00 |
|
 xuyueandClaude 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 |
|
 xuyueandClaude Opus 5
|
a03e22e60f
|
chore(前端): 删掉三个从 ojnext 搬来就没人用的文件
阶段 1 是「原样搬入、不改业务代码」,所以 ojnext 里已经死掉的东西也一起
搬了过来。这三个文件全仓零引用:
- utils/permissions.ts(usePermissions / checkRoutePermission)
- oj/ai/components/StreakStats.vue
- shared/components/IconButton.vue
确认方式:在 apps/web 全部 .ts / .vue / .json / .html / .js 里搜这五个名字
(排除文件自身)零命中;两个组件也不在自动生成的 components.d.ts 里,
permissions.ts 的两个导出同样不在 auto-imports.d.ts 里 —— 也就是说
它们连自动导入的注册都没拿到,不存在「模板里隐式用到」的可能。
permissions.ts 在 ojnext 里也是死的,不是搬运过程产生的。
vue-tsc 0 error,vite build 通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-25 17:37:58 -06:00 |
|
 xuyueandClaude 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 |
|
 xuyueandClaude Opus 5
|
3d501e8b69
|
fix(试运行): 流程图/SQL 会往 Judge0 发出 language_id: undefined
Judge0 的语言 id 表里只有 C/C++/Java/Golang/JavaScript/Python2/Python3,
而 code.language 的类型包含 Flowchart 和 SQL —— 前者不是可执行代码,
后者由本站自己的 SQL 沙箱判,都走不到 Judge0。之前直接索引拿到 undefined
就发出去了。
改成 Partial<Record<LANGUAGE, number>>,取不到就提前返回提示。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-25 14:03:51 -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 |
|
 xuyueandClaude Opus 5
|
2edb8cfb0d
|
fix(前端): 我改 /api2 前缀时漏了 5 处;修 4 个字段名不匹配;收紧 .gitignore
浏览器走查 + 真实 AI 调用暴露出来的,都是「接口好的、界面坏的」,
只靠脚本打 API 永远发现不了。
## 一、AI 功能和测试用例下载全都会 404(我今天造成的)
把 api2.ts 的 baseURL 从 /api2 改成 /api 时,只改了走 axios 实例的调用,
漏掉 5 处不走它的:
oj/store/ai.ts AI 题解分析 原生 fetch("/api2/...")
oj/problem/.../SubmissionResult.vue AI 提示 原生 fetch
oj/rank/list.vue AI 班级分析 原生 fetch
oj/class/pk.vue AI 班级 PK 原生 fetch
utils/download.ts 测试用例下载 独立 axios,baseURL "/api2/admin"
生产的 Caddy 只认 /api,这 5 条全是 404。阶段 0 的端点清单里就写着
「盲点 2:4 处用原生 fetch」「盲点 3:download.ts 是独立 axios 实例」——
我今天读过那份文档,还是漏了。
修完实测四条 AI 链路全通(真实 DeepSeek 调用):
/api/ai/analysis 200 SSE 176 分片 5.2s start→delta→done→end 完整
/api/ai/hint 200 2.4s 且未泄露参考答案
/api/ai/class-analysis 200 12.6s
/api/ai/class-pk-analysis 200 12.6s
落库确认(ai_analysis 新增,model=deepseek-v4-flash 与旧后端一致)。
hint 对别人的提交回 404 是**正确**的,它用 userId 限定只能看自己的。
## 二、4 个字段名永久对不上(重写引入的回归)
适配器只在**大写字母**前插下划线:`top10Avg` → `top10_avg`;
而旧 Django 给的是 `top_10_avg`(数字前也有)。前端还按旧名读。
影响比"三列空白"大:`row.top_10_avg.toFixed(2)` 在 undefined 上抛异常,
Vue 放弃整个子树 —— 班级PK 和班级排名页的**整个「分层统计」面板都不显示**
(Q1/Q3/四分位距/标准差/前10%/中间80%/后10%/人数 全没了),控制台还不报错。
修完实测:前10%均值 194.80、中间80%均值 93.79、后10%均值 49.40 全部出现。
写脚本按这个规律全仓扫过,确认只有这 4 个,改完复查归零。
## 三、.gitignore 差一点把生产库提交上去
原来只忽略 data/test_case/ 和 data/judge_server/,而 compose 会在
data/postgres/ 生成**整个数据库**(含 raw_password 明文列)、data/redis/、
data/backend/(学生上传的文件)。这次 `git add -A` 报权限错误才发现 ——
只因为 postgres 容器用别的 uid、目录读不了才没提交成功。改成忽略整个 data/。
已确认历史里从没提交过这些。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-08 05:37:24 -06:00 |
|
 xuyueandClaude Opus 5
|
cafa92a102
|
fix(阶段4评审收尾): 清掉三条 Minor,顺带一个真 bug
## M4 禁用账号会把学生卡在登录死循环里(唯一学生会撞上的)
`getSessionUser` 对禁用用户返回 null,于是落到 401 `login-required`,
而前端拦截器见到这个码就弹登录框 —— 一个上课上到一半被禁用的学生会陷入
「弹登录框 → 登进去 → 又被弹」,完全看不出发生了什么。
会话解析改成返回 `{ user } | { user: null, reason: "anonymous" | "disabled" }`,
禁用报 403 `account-disabled`(凭证有效、是账号不让用了,和 login 接口对禁用
账号的回法一致)。会话照删,禁用立即生效。前端补一支:清登录态 + 明确提示,
**不弹登录框**。
实测:会话中途 UPDATE is_disabled=true → 同一会话下一个请求
403 `account-disabled`「账号已被禁用,请联系老师」。
## M3 三个端点的守卫写在 handler 体内
submissions/statistics、submissions/:id/rejudge、flowcharts/statistics 的档位
本来就是对的,但写成 handler 里的 if,违背了「守卫要从注册行上看得出来」的约定,
下一个人加同类端点容易漏掉那个 if。改用 requireTeacher / requireSuperAdmin。
实测档位没变:普通学生三个都 403;教师统计接口 200、重判仍 403。
## M2 from-public 的错误码构成比赛存在性预言机
比赛不存在回 `not-found`、存在但不属于你回 `contest-not-found`,带一个已知
有效的 problemId 就能靠错误码枚举出哪些 contestId 真实存在。统一成
`contest-not-found`,和全仓其余跨租户路径一致。
实测:两种情况现在都是 404 contest-not-found。
## 顺带:比赛里的 SQL 题看不到示例数据
改 M2 时 tsc 报 `sqlDisplay` 声明了没用到 —— 查下去是真 bug:
`POST /contests/:id/problems` 把展示数据算出来了,却往库里写死 null
(公开题那两条路径都是对的)。于是比赛里的 SQL 题打开后没有示例数据表和期望结果。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-08 02:23:23 -06:00 |
|
 xuyueandClaude Opus 5
|
ea521e7b0a
|
feat(阶段5): 镜像、三套 compose,前端切回 /api 与 /ws
## 镜像
一个 Dockerfile 两个 target:api(单二进制)和 web(Caddy + 前端产物)。
旧后端是一个容器里用 supervisord 跑 caddy+gunicorn+dramatiq,这里拆成
oj-web / oj-api / oj-worker 三个容器 —— Docker 本身就是进程管理器,
拆开之后 worker 挂了能单独重启、日志也分得开,少一层 supervisord 要维护。
同一个镜像换个子命令就是 worker,镜像里只有一份运行时。
新增 healthcheck 子命令:运行镜像是 debian-slim,没有 curl/wget,
让二进制自己打 /health(只打 /health 不碰库 —— 库挂了该由库的 healthcheck 报,
不该让 api 跟着被判不健康然后被重启)。
**数据目录照抄旧后端**(test_case、public/upload、public/avatar)。
不是审美问题:切换那天不用搬动任何文件,回滚时旧后端立刻能找到自己的数据。
少一次几十 GB 的 mv,就少一个在停机窗口里出错的机会。
构建路上踩到三个坑,都是「本地能过、容器里过不了」那一类:
- 构建上下文吸进了 data/,judge_server/run 是判题沙箱用别的 uid 建的,
docker 连 stat 都做不了,构建直接失败 → 补 .dockerignore
- mermaid@9.4.3(机房老 Chrome 的 legacy 依赖,不能砍)从容器里连
registry.npmjs.com 稳定失败,主机上没问题 → 换 npmmirror,并重试两次
- 容器里 bun 用 isolated 布局,本地是扁平的。靠「提升」才能解析到的包
在容器里一律解析不到:@node-rs/jieba-linux-x64-gnu(编译要 import 它的 .node)、
以及前端的 @codemirror/{language,state,view} 和 @lezer/highlight。
这些本来就是代码直接 import 的,补成直接依赖。顺手写了个脚本扫全仓,
确认只有这 4 个。
## 前端切回 /api、/ws
迁移期用 /api2、/ws2 指新后端,/api、/ws 还指着 Django。端点已全部搬完,
临时前缀去掉。改动只有三处(api2.ts 的 baseURL、websocket.ts 的两个 URL),
`api2` 那些 import 是模块名不是路径,不动。vite 代理同步收敛成三条。
## compose
- debian:全套(含 postgres,对外开 5445 给机房连)
- school:**没有 postgres**,连服务器的库;本地 Redis + 本地判题沙箱
- 两边共用一个库但各有各的队列和 WS 推送,和旧后端的 Dramatiq/Channels 拓扑一致
密钥一律走 env 且带 `:?`,没设置就直接报错退出,不静默用弱默认值。
COOKIE_SECURE 在机房必须是 false(http 直连 IP,带 Secure 的 Cookie 发不回来,
表现是「登录成功但立刻又变未登录」)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-07 23:41:20 -06:00 |
|
 xuyueandClaude 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 |
|
 xuyueandClaude Opus 5
|
9a6c3ba225
|
feat(阶段4): 后台地基 + 公告管理
地基:
- auth/middleware.ts 加四个角色守卫 requireAdmin / requireTeacher /
requireSuperAdmin / requireProblemPermission,对应旧 account/decorators.py 的
四个装饰器。未登录 401 login-required、角色不够 403 permission-denied,
与前端 api2 拦截器按 code 分流的两支对上。
- routes/admin/ 目录 + 总入口挂在 /api/admin。角色守卫由各子路由自己挂,
不在总入口兜一层 —— 否则「这个接口要什么角色」从注册行看不出来,
正是阶段 3 Minor M2 踩过的坑。
- packages/contract/src/admin.ts 独立放后台契约。同一张表两侧下发的字段集不同
(后台要 visible,oj 侧连键都不该出现),混在一起迟早有人在 oj 侧复用后台那个。
- utils/legacy.ts:把 toLegacy / legacyResponse 从 oj/api.ts 抽出来共用。
admin 侧组件同样读 snake_case,走同一层适配,组件不动。
公告管理(旧 /api/admin/announcement 一个路径四个动词)拆成:
GET/POST admin/announcements
GET/PUT/DELETE admin/announcements/:id
一处有意不对齐旧后端:删除不存在的公告,旧后端 filter().delete() 静默成功,
这里返回 404 —— 后台是人手点删除,静默成功会让人以为删掉了,刷新后它还在。
实测:匿名 401 / 学生 403 / 超管 200;增删改查、列表不含 content、
空标题 400、不存在 404 全部符合预期。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-07 07:06:22 -06:00 |
|
 xuyueandClaude Opus 5
|
6c18c57e7a
|
fix(阶段3): api2 补上鉴权失败的统一处理
utils/http.ts 的拦截器一直有两条全站行为:login-required 清登录态并弹登录框、
permission-denied 弹提示。api2 从建包起就漏了这两条,于是此前已迁移到 api2 的
所有端点在鉴权失败时都是「点了没反应」—— 调用方各自 catch 才能提示,漏一个
就静默。
新加的两个统计端点是教师专属,非教师点「统计」必然 403,会把这个洞放大到
用户能天天撞见,所以一并补上。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-07 06:48:20 -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 |
|
xuyue
|
ec274419c3
|
Build Phase 2 judge vertical slice
|
2026-08-06 22:42:39 -06:00 |
|
xuyue
|
ae1fb329b5
|
feat(阶段1): 搬入 ojnext 为 apps/web,未改业务代码
|
2026-08-06 21:18:16 -06:00 |
|