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>
This commit is contained in:
2026-08-07 01:25:36 -06:00
parent ec274419c3
commit 8c00cdc947
52 changed files with 4273 additions and 300 deletions

View File

@@ -1,4 +1,4 @@
import { submissionUpdateSchema } from "@oj2/contract"
import { flowchartUpdateSchema, submissionUpdateSchema } from "@oj2/contract"
import { and, eq } from "drizzle-orm"
import { db, schema } from "./db"
@@ -9,6 +9,7 @@ import {
} from "./judge/events"
import { JudgeStatus } from "./judge/status"
import { createSubscriberRedis } from "./redis"
import { parseUserEvent, userEventChannel, userEventTopic } from "./events"
export interface SubmissionSocketData {
userId: number
@@ -25,12 +26,14 @@ export function submissionWebSocketHandler(): Bun.WebSocketHandler<SubmissionSoc
return {
open(ws) {
ws.subscribe(userSubmissionTopic(ws.data.userId))
ws.subscribe(userEventTopic(ws.data.userId))
},
message(ws, message) {
void handleMessage(ws, String(message))
},
close(ws) {
ws.unsubscribe(userSubmissionTopic(ws.data.userId))
ws.unsubscribe(userEventTopic(ws.data.userId))
},
}
}
@@ -87,7 +90,21 @@ async function handleMessage(
.limit(1)
if (!submission) {
ws.send(JSON.stringify({ type: "error", message: "Submission not found" }))
const [flowchart] = await db
.select({ id: schema.flowchartSubmission.id, status: schema.flowchartSubmission.status, score: schema.flowchartSubmission.aiScore, grade: schema.flowchartSubmission.aiGrade })
.from(schema.flowchartSubmission)
.where(and(eq(schema.flowchartSubmission.id, message.submission_id), eq(schema.flowchartSubmission.userId, ws.data.userId)))
.limit(1)
if (!flowchart) {
ws.send(JSON.stringify({ type: "error", message: "Submission not found" }))
return
}
const replay = flowchart.status === 2
? { type: "flowchart_evaluation_completed", submission_id: flowchart.id, score: flowchart.score ?? undefined, grade: flowchart.grade ?? undefined }
: flowchart.status === 3
? { type: "flowchart_evaluation_failed", submission_id: flowchart.id, error: "Evaluation failed" }
: { type: "flowchart_evaluation_update", submission_id: flowchart.id }
ws.send(JSON.stringify(flowchartUpdateSchema.parse(replay)))
return
}
@@ -118,6 +135,22 @@ export async function bridgeSubmissionEvents(
) {
const subscriber = createSubscriberRedis()
subscriber.on("message", (channel, raw) => {
if (channel === userEventChannel) {
const event = parseUserEvent(raw)
if (!event) return
void (async () => {
const [activeUser] = await db
.select({ id: schema.user.id })
.from(schema.user)
.where(and(eq(schema.user.id, event.userId), eq(schema.user.isDisabled, false)))
.limit(1)
if (!activeUser) return
server.publish(userEventTopic(event.userId), JSON.stringify(event.data))
})().catch((error) => {
console.error("Failed to bridge user event", error)
})
return
}
if (channel !== submissionUpdateChannel) return
const event = parseSubmissionEvent(raw)
if (!event) return
@@ -144,6 +177,6 @@ export async function bridgeSubmissionEvents(
subscriber.on("error", (error) => {
console.error("Submission event subscriber error", error)
})
await subscriber.subscribe(submissionUpdateChannel)
await subscriber.subscribe(submissionUpdateChannel, userEventChannel)
return subscriber
}