From f2ca34fd4bfa4beb3b862401052ffe351f14dc09 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sun, 27 Sep 2026 15:04:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor(api):=20=E8=AF=B7=E6=B1=82=E4=BD=93?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E6=94=B6=E6=88=90=20readJson=20/=20parseBody?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/api/src/http.ts | 40 ++++++++++ apps/api/src/routes/account.ts | 20 ++--- apps/api/src/routes/achievement.ts | 11 +-- apps/api/src/routes/admin/account.ts | 35 ++------- apps/api/src/routes/admin/achievement.ts | 28 ++----- apps/api/src/routes/admin/announcement.ts | 28 ++----- apps/api/src/routes/admin/conf.ts | 24 ++---- apps/api/src/routes/admin/contest.ts | 41 ++--------- apps/api/src/routes/admin/problem.ts | 90 +++++------------------ apps/api/src/routes/admin/problemset.ts | 84 ++++++--------------- apps/api/src/routes/admin/tag.ts | 31 +++----- apps/api/src/routes/admin/tutorial.ts | 63 ++++------------ apps/api/src/routes/ai.ts | 52 ++++++------- apps/api/src/routes/auth.ts | 17 ++--- apps/api/src/routes/classroom.ts | 11 +-- apps/api/src/routes/content.ts | 38 +++++----- apps/api/src/routes/contest.ts | 11 +-- apps/api/src/routes/flowchart.ts | 6 +- apps/api/src/routes/judge-server.ts | 4 +- apps/api/src/routes/problemset.ts | 11 +-- apps/api/src/routes/submission.ts | 21 +++--- 21 files changed, 234 insertions(+), 432 deletions(-) diff --git a/apps/api/src/http.ts b/apps/api/src/http.ts index d2993b7..319d560 100644 --- a/apps/api/src/http.ts +++ b/apps/api/src/http.ts @@ -1,4 +1,5 @@ import type { Context } from "hono" +import type { z } from "zod" export function success(c: Context, data: T, status = 200) { return c.json({ data }, status as 200) @@ -12,3 +13,42 @@ export function failure( ) { return c.json({ error: { code, message } }, status) } + +/** + * 读 JSON 请求体并按契约校验。body 不是合法 JSON 时按 `null` 交给 schema, + * 由 schema 统一判失败 —— 原来每个 handler 都手写一遍 `c.req.json().catch(() => null)`, + * 漏写 `.catch` 就是一个畸形请求打出 500。 + */ +export async function readJson(c: Context, schema: S) { + return schema.safeParse(await c.req.json().catch(() => null)) +} + +/** + * `readJson` + 校验失败时的 400。绝大多数入参端点就是这个形状: + * + * const parsed = await parseBody(c, schema, "标签名不能为空") + * if (!parsed.success) return parsed.response + * + * 不给 `message` 就回 zod 的第一条 issue(后台表单要告诉老师具体哪一栏不对)。 + * 失败时要带别的错误码、或者和路径参数一起判的,用 `readJson` 自己写。 + */ +export async function parseBody( + c: Context, + schema: S, + message?: string, +): Promise< + { success: true; data: z.output } | { success: false; response: Response } +> { + const parsed = await readJson(c, schema) + if (parsed.success) return { success: true, data: parsed.data } + return { + success: false, + response: failure( + c, + 400, + "invalid-request", + // issues 不会是空的,?? 只是给类型一个交代 + message ?? parsed.error.issues[0]?.message ?? "参数错误", + ), + } +} diff --git a/apps/api/src/routes/account.ts b/apps/api/src/routes/account.ts index e3ba696..8c0385e 100644 --- a/apps/api/src/routes/account.ts +++ b/apps/api/src/routes/account.ts @@ -43,7 +43,7 @@ import { onlineUserIds } from "../auth/presence" import { optionalAuth, requireAuth, type AppEnv } from "../auth/middleware" import { config } from "../config" import { db, schema } from "../db" -import { failure, success } from "../http" +import { failure, parseBody, success } from "../http" import { JudgeStatus } from "../judge/status" import { getBooleanOption } from "../services/options" import { getUserProfileById } from "../services/profile" @@ -70,11 +70,12 @@ export const accountRoutes = new Hono() const REGISTER_PER_IP: AttemptRule = { limit: 100, windowSeconds: 60 * 60 } accountRoutes.post("/users", async (c) => { - const parsed = registerRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + registerRequestSchema, + "Invalid registration payload", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Invalid registration payload") + if (!parsed.success) return parsed.response if (!(await getBooleanOption("allow_register", true))) { return failure( c, @@ -173,11 +174,12 @@ accountRoutes.get("/profiles/:username", optionalAuth, async (c) => { }) accountRoutes.put("/me/profile", requireAuth, async (c) => { - const parsed = updateProfileRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + updateProfileRequestSchema, + "Invalid profile payload", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Invalid profile payload") + if (!parsed.success) return parsed.response const values = Object.fromEntries( Object.entries(parsed.data).map(([key, value]) => [ key, diff --git a/apps/api/src/routes/achievement.ts b/apps/api/src/routes/achievement.ts index 84de59b..ab62beb 100644 --- a/apps/api/src/routes/achievement.ts +++ b/apps/api/src/routes/achievement.ts @@ -10,7 +10,7 @@ import { Hono } from "hono" import { requireAuth, type AppEnv } from "../auth/middleware" import { db, schema } from "../db" -import { failure, success } from "../http" +import { failure, parseBody, success } from "../http" import { objectValue } from "./helpers" export const achievementRoutes = new Hono() @@ -177,11 +177,12 @@ achievementRoutes.get("/achievements/pending", requireAuth, async (c) => { }) achievementRoutes.post("/achievements/pending/read", requireAuth, async (c) => { - const parsed = markAchievementsReadSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + markAchievementsReadSchema, + "Invalid achievement ids", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Invalid achievement ids") + if (!parsed.success) return parsed.response if (parsed.data.ids.length > 0) { await db .update(schema.userAchievement) diff --git a/apps/api/src/routes/admin/account.ts b/apps/api/src/routes/admin/account.ts index 63aec52..49adbe9 100644 --- a/apps/api/src/routes/admin/account.ts +++ b/apps/api/src/routes/admin/account.ts @@ -33,7 +33,7 @@ import { isUserOnline, onlineUserIds } from "../../auth/presence" import { revokeUserSessions } from "../../auth/session" import { requireSuperAdmin, type AppEnv } from "../../auth/middleware" import { db, schema } from "../../db" -import { failure, success } from "../../http" +import { failure, parseBody, success } from "../../http" import { queryInteger, sampleUser } from "../helpers" export const adminAccountRoutes = new Hono() @@ -252,17 +252,8 @@ adminAccountRoutes.get("/users/:id", requireSuperAdmin, async (c) => { adminAccountRoutes.put("/users/:id", requireSuperAdmin, async (c) => { const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = updateUserRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, updateUserRequestSchema) + if (!parsed.success) return parsed.response const data = parsed.data const [existing] = await selectUser(id) if (!existing) return failure(c, 404, "user-not-found", "User does not exist") @@ -350,17 +341,8 @@ adminAccountRoutes.put("/users/:id", requireSuperAdmin, async (c) => { }) adminAccountRoutes.post("/users", requireSuperAdmin, async (c) => { - const parsed = importUsersRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, importUsersRequestSchema) + if (!parsed.success) return parsed.response const rows = parsed.data.users type Prepared = { username: string @@ -544,11 +526,8 @@ function isForeignKeyViolation(error: unknown) { class UserHasSubmissionsError extends Error {} adminAccountRoutes.delete("/users", requireSuperAdmin, async (c) => { - const parsed = deleteUsersRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "ids is required") + const parsed = await parseBody(c, deleteUsersRequestSchema, "ids is required") + if (!parsed.success) return parsed.response const me = c.get("user")!.id if (parsed.data.ids.includes(me)) { return failure( diff --git a/apps/api/src/routes/admin/achievement.ts b/apps/api/src/routes/admin/achievement.ts index 4f10805..051335c 100644 --- a/apps/api/src/routes/admin/achievement.ts +++ b/apps/api/src/routes/admin/achievement.ts @@ -9,7 +9,7 @@ import { Hono } from "hono" import { requireSuperAdmin, type AppEnv } from "../../auth/middleware" import { db, schema } from "../../db" -import { failure, success } from "../../http" +import { failure, parseBody, success } from "../../http" import { ACHIEVEMENT_METRICS, findMetric, @@ -72,17 +72,8 @@ adminAchievementRoutes.get( ) adminAchievementRoutes.post("/achievements", requireSuperAdmin, async (c) => { - const parsed = createAchievementRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, createAchievementRequestSchema) + if (!parsed.success) return parsed.response if (!findMetric(parsed.data.metric)) return failure(c, 400, "invalid-metric", "指标不存在") @@ -111,17 +102,8 @@ adminAchievementRoutes.put( requireSuperAdmin, async (c) => { const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = updateAchievementRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, updateAchievementRequestSchema) + if (!parsed.success) return parsed.response if (!findMetric(parsed.data.metric)) return failure(c, 400, "invalid-metric", "指标不存在") diff --git a/apps/api/src/routes/admin/announcement.ts b/apps/api/src/routes/admin/announcement.ts index 9b7a23f..b181fd3 100644 --- a/apps/api/src/routes/admin/announcement.ts +++ b/apps/api/src/routes/admin/announcement.ts @@ -9,7 +9,7 @@ import { Hono } from "hono" import { requireSuperAdmin, type AppEnv } from "../../auth/middleware" import { db, schema } from "../../db" -import { failure, success } from "../../http" +import { failure, parseBody, success } from "../../http" import { queryInteger, sampleUser } from "../helpers" export const adminAnnouncementRoutes = new Hono() @@ -78,17 +78,8 @@ adminAnnouncementRoutes.get("/announcements", requireSuperAdmin, async (c) => { }) adminAnnouncementRoutes.post("/announcements", requireSuperAdmin, async (c) => { - const parsed = createAnnouncementRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, createAnnouncementRequestSchema) + if (!parsed.success) return parsed.response const now = new Date().toISOString() const [created] = await db .insert(schema.announcement) @@ -126,17 +117,8 @@ adminAnnouncementRoutes.put( requireSuperAdmin, async (c) => { const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = updateAnnouncementRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, updateAnnouncementRequestSchema) + if (!parsed.success) return parsed.response const updated = await db .update(schema.announcement) .set({ ...parsed.data, lastUpdateTime: new Date().toISOString() }) diff --git a/apps/api/src/routes/admin/conf.ts b/apps/api/src/routes/admin/conf.ts index 700b335..eb33107 100644 --- a/apps/api/src/routes/admin/conf.ts +++ b/apps/api/src/routes/admin/conf.ts @@ -22,7 +22,7 @@ import { import { config } from "../../config" import { db, schema } from "../../db" import { publishConfigUpdate } from "../../events" -import { failure, success } from "../../http" +import { failure, parseBody, success } from "../../http" import { getWebsiteOptions } from "../../services/options" import { todayStart } from "../../time" import { queryInteger } from "../helpers" @@ -77,17 +77,8 @@ adminConfRoutes.get("/website", requireSuperAdmin, async (c) => { }) adminConfRoutes.post("/website", requireSuperAdmin, async (c) => { - const parsed = updateWebsiteConfigRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, updateWebsiteConfigRequestSchema) + if (!parsed.success) return parsed.response const entries = ( Object.entries(OPTION_KEYS) as [keyof typeof OPTION_KEYS, string][] ).map(([field, key]) => ({ field, key, value: parsed.data[field] })) @@ -131,11 +122,12 @@ adminConfRoutes.get("/judge-servers", requireSuperAdmin, async (c) => { }) adminConfRoutes.put("/judge-servers/:id", requireSuperAdmin, async (c) => { - const parsed = updateJudgeServerRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + updateJudgeServerRequestSchema, + "isDisabled is required", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "isDisabled is required") + if (!parsed.success) return parsed.response const updated = await db .update(schema.judgeServer) .set({ isDisabled: parsed.data.isDisabled }) diff --git a/apps/api/src/routes/admin/contest.ts b/apps/api/src/routes/admin/contest.ts index 6670b3d..7c26fc2 100644 --- a/apps/api/src/routes/admin/contest.ts +++ b/apps/api/src/routes/admin/contest.ts @@ -12,7 +12,7 @@ import { Hono } from "hono" import { requireTeacher, type AppEnv } from "../../auth/middleware" import type { AuthUser } from "../../auth/session" import { db, schema } from "../../db" -import { failure, success } from "../../http" +import { failure, parseBody, success } from "../../http" import { contestStatus } from "../../services/contest" import { objectValue, queryInteger, sampleUser } from "../helpers" @@ -120,17 +120,8 @@ adminContestRoutes.get("/contests/:id", requireTeacher, async (c) => { }) adminContestRoutes.post("/contests", requireTeacher, async (c) => { - const parsed = createContestRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, createContestRequestSchema) + if (!parsed.success) return parsed.response const error = validatePayload(parsed.data) if (error) return failure(c, 400, "invalid-contest", error) @@ -157,17 +148,8 @@ adminContestRoutes.post("/contests", requireTeacher, async (c) => { adminContestRoutes.put("/contests/:id", requireTeacher, async (c) => { const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = updateContestRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, updateContestRequestSchema) + if (!parsed.success) return parsed.response const [existing] = await selectContest(id) if (!existing || !ownedBy(c.get("user")!, existing.contest)) { return failure(c, 404, "contest-not-found", "Contest does not exist") @@ -386,17 +368,8 @@ adminContestRoutes.put( requireTeacher, async (c) => { const contestId = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = updateAcmHelperRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, updateAcmHelperRequestSchema) + if (!parsed.success) return parsed.response const [contest] = await db .select() .from(schema.contest) diff --git a/apps/api/src/routes/admin/problem.ts b/apps/api/src/routes/admin/problem.ts index 0e8cf90..c481d60 100644 --- a/apps/api/src/routes/admin/problem.ts +++ b/apps/api/src/routes/admin/problem.ts @@ -32,7 +32,7 @@ import { Hono } from "hono" import { requireProblemPermission, type AppEnv } from "../../auth/middleware" import type { AuthUser } from "../../auth/session" import { db, schema } from "../../db" -import { failure, success } from "../../http" +import { failure, parseBody, success } from "../../http" import { astRulesError, pickAstRules } from "../../judge/ast" import { buildSqlDisplay } from "../../judge/sql" import { completeChat } from "../../services/ai" @@ -432,17 +432,8 @@ adminProblemRoutes.get("/problems/:id", requireProblemPermission, async (c) => { }) adminProblemRoutes.post("/problems", requireProblemPermission, async (c) => { - const parsed = createProblemRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, createProblemRequestSchema) + if (!parsed.success) return parsed.response const checked = commonChecks(parsed.data) if ("error" in checked) return failure(c, 400, "invalid-problem", checked.error) @@ -495,17 +486,8 @@ adminProblemRoutes.post("/problems", requireProblemPermission, async (c) => { adminProblemRoutes.put("/problems/:id", requireProblemPermission, async (c) => { const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = updateProblemRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, updateProblemRequestSchema) + if (!parsed.success) return parsed.response const [existing] = await db .select() .from(schema.problem) @@ -702,17 +684,8 @@ adminProblemRoutes.post( ) { return failure(c, 404, "contest-not-found", "Contest does not exist") } - const parsed = createProblemRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, createProblemRequestSchema) + if (!parsed.success) return parsed.response const checked = commonChecks(parsed.data) if ("error" in checked) return failure(c, 400, "invalid-problem", checked.error) @@ -774,12 +747,12 @@ adminProblemRoutes.post( requireProblemPermission, async (c) => { const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = makeProblemPublicRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + makeProblemPublicRequestSchema, + "displayId 不能为空", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "displayId 不能为空") - + if (!parsed.success) return parsed.response const [problem] = await db .select() .from(schema.problem) @@ -856,17 +829,8 @@ adminProblemRoutes.post( requireProblemPermission, async (c) => { const contestId = queryInteger(c.req.param("contestId"), 0, { min: 1 }) - const parsed = addContestProblemRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, addContestProblemRequestSchema) + if (!parsed.success) return parsed.response const [contest] = await db .select() .from(schema.contest) @@ -1055,17 +1019,8 @@ adminProblemRoutes.post( "/sql-test-cases/preview", requireProblemPermission, async (c) => { - const parsed = sqlPreviewRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, sqlPreviewRequestSchema) + if (!parsed.success) return parsed.response const outcome = await buildSqlDisplay( parsed.data.initSql, parsed.data.refSql, @@ -1082,17 +1037,8 @@ adminProblemRoutes.post( "/sql-test-cases/generate", requireProblemPermission, async (c) => { - const parsed = generateSqlTestCaseRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, generateSqlTestCaseRequestSchema) + if (!parsed.success) return parsed.response try { const sql = await completeChat( `你是一个 SQL 出题助手。用户会给你一道 SQL 题的标准答案(查询题的 diff --git a/apps/api/src/routes/admin/problemset.ts b/apps/api/src/routes/admin/problemset.ts index 24ab658..2168ded 100644 --- a/apps/api/src/routes/admin/problemset.ts +++ b/apps/api/src/routes/admin/problemset.ts @@ -29,7 +29,7 @@ import { Hono } from "hono" import { requireTeacher, type AppEnv } from "../../auth/middleware" import type { AuthUser } from "../../auth/session" import { db, schema } from "../../db" -import { failure, success } from "../../http" +import { failure, parseBody, success } from "../../http" import { recalculateBadge, resyncProgress } from "../../services/problemset" import { asFilterValue, queryInteger, sampleUser } from "../helpers" @@ -175,17 +175,8 @@ adminProblemSetRoutes.get("/problem-sets", requireTeacher, async (c) => { }) adminProblemSetRoutes.post("/problem-sets", requireTeacher, async (c) => { - const parsed = createProblemSetRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, createProblemSetRequestSchema) + if (!parsed.success) return parsed.response const now = new Date().toISOString() const [created] = await db .insert(schema.problemset) @@ -211,17 +202,8 @@ adminProblemSetRoutes.get("/problem-sets/:id", requireTeacher, async (c) => { adminProblemSetRoutes.put("/problem-sets/:id", requireTeacher, async (c) => { const row = await loadOwned(c, c.get("user")!) if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在") - const parsed = updateProblemSetRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, updateProblemSetRequestSchema) + if (!parsed.success) return parsed.response const [updated] = await db .update(schema.problemset) .set({ @@ -258,11 +240,12 @@ adminProblemSetRoutes.put( async (c) => { const row = await loadOwned(c, c.get("user")!) if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在") - const parsed = updateProblemSetStatusRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + updateProblemSetStatusRequestSchema, + "status 不合法", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "status 不合法") + if (!parsed.success) return parsed.response const [updated] = await db .update(schema.problemset) .set({ @@ -331,17 +314,8 @@ adminProblemSetRoutes.post( async (c) => { const row = await loadOwned(c, c.get("user")!) if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在") - const parsed = addProblemToSetRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, addProblemToSetRequestSchema) + if (!parsed.success) return parsed.response const [problem] = await db .select({ id: schema.problem.id }) .from(schema.problem) @@ -394,10 +368,12 @@ adminProblemSetRoutes.put( async (c) => { const row = await loadOwned(c, c.get("user")!) if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在") - const parsed = updateProblemInSetRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + updateProblemInSetRequestSchema, + "参数错误", ) - if (!parsed.success) return failure(c, 400, "invalid-request", "参数错误") + if (!parsed.success) return parsed.response const updated = await db .update(schema.problemsetProblem) .set(parsed.data) @@ -512,17 +488,8 @@ adminProblemSetRoutes.post( async (c) => { const row = await loadOwned(c, c.get("user")!) if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在") - const parsed = createProblemSetBadgeRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, createProblemSetBadgeRequestSchema) + if (!parsed.success) return parsed.response const [created] = await db .insert(schema.problemsetBadge) .values({ @@ -542,17 +509,8 @@ adminProblemSetRoutes.put( async (c) => { const row = await loadOwned(c, c.get("user")!) if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在") - const parsed = updateProblemSetBadgeRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, updateProblemSetBadgeRequestSchema) + if (!parsed.success) return parsed.response const [updated] = await db .update(schema.problemsetBadge) .set(parsed.data) diff --git a/apps/api/src/routes/admin/tag.ts b/apps/api/src/routes/admin/tag.ts index bc1d60a..3c5a579 100644 --- a/apps/api/src/routes/admin/tag.ts +++ b/apps/api/src/routes/admin/tag.ts @@ -33,7 +33,7 @@ import { } from "../../auth/middleware" import type { AuthUser } from "../../auth/session" import { db, schema } from "../../db" -import { failure, success } from "../../http" +import { failure, parseBody, success } from "../../http" import { JudgeStatus } from "../../judge/status" import { completeChat } from "../../services/ai" import { localTime, localYear } from "../../time" @@ -82,11 +82,8 @@ adminTagRoutes.get("/problem-tags", requireProblemPermission, async (c) => { adminTagRoutes.put("/problem-tags/:id", requireProblemPermission, async (c) => { const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = renameTagRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "标签名不能为空") + const parsed = await parseBody(c, renameTagRequestSchema, "标签名不能为空") + if (!parsed.success) return parsed.response const name = parsed.data.name const [tag] = await db @@ -177,17 +174,8 @@ adminTagRoutes.post( "/problems/batch-tag", requireProblemPermission, async (c) => { - const parsed = batchProblemTagRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "参数错误", - ) - } + const parsed = await parseBody(c, batchProblemTagRequestSchema) + if (!parsed.success) return parsed.response const user = c.get("user")! const filters = [ inArray(schema.problem.id, parsed.data.problemIds), @@ -462,11 +450,12 @@ adminTagRoutes.post( "/problems/flowchart", requireProblemPermission, async (c) => { - const parsed = generateFlowchartRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + generateFlowchartRequestSchema, + "python 代码不能为空", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "python 代码不能为空") + if (!parsed.success) return parsed.response try { const flowchart = await completeChat( `你是一个可以将Python代码转换为mermaid的助手。 diff --git a/apps/api/src/routes/admin/tutorial.ts b/apps/api/src/routes/admin/tutorial.ts index 1a9ceae..81d5c9d 100644 --- a/apps/api/src/routes/admin/tutorial.ts +++ b/apps/api/src/routes/admin/tutorial.ts @@ -13,7 +13,7 @@ import { Hono } from "hono" import { requireSuperAdmin, type AppEnv } from "../../auth/middleware" import { db, schema } from "../../db" -import { failure, success } from "../../http" +import { failure, parseBody, success } from "../../http" import { exerciseDataError } from "../../services/exercise" import { objectValue, queryInteger, sampleUser } from "../helpers" @@ -72,17 +72,8 @@ adminTutorialRoutes.get("/tutorials", requireSuperAdmin, async (c) => { }) adminTutorialRoutes.post("/tutorials", requireSuperAdmin, async (c) => { - const parsed = createTutorialRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, createTutorialRequestSchema) + if (!parsed.success) return parsed.response const now = new Date().toISOString() const [created] = await db .insert(schema.tutorial) @@ -108,17 +99,8 @@ adminTutorialRoutes.get("/tutorials/:id", requireSuperAdmin, async (c) => { adminTutorialRoutes.put("/tutorials/:id", requireSuperAdmin, async (c) => { const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = updateTutorialRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, updateTutorialRequestSchema) + if (!parsed.success) return parsed.response const updated = await db .update(schema.tutorial) .set({ ...parsed.data, updatedAt: new Date().toISOString() }) @@ -135,11 +117,12 @@ adminTutorialRoutes.put( requireSuperAdmin, async (c) => { const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = setTutorialVisibilityRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + setTutorialVisibilityRequestSchema, + "isPublic is required", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "isPublic is required") + if (!parsed.success) return parsed.response // 只改可见性,不动 updatedAt —— 上下架不是内容修改,改了会打乱按更新时间排序的直觉 const updated = await db .update(schema.tutorial) @@ -199,17 +182,8 @@ adminTutorialRoutes.get( ) adminTutorialRoutes.post("/exercises", requireSuperAdmin, async (c) => { - const parsed = createExerciseRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, createExerciseRequestSchema) + if (!parsed.success) return parsed.response const [tutorial] = await db .select({ id: schema.tutorial.id }) .from(schema.tutorial) @@ -233,17 +207,8 @@ adminTutorialRoutes.post("/exercises", requireSuperAdmin, async (c) => { }) adminTutorialRoutes.put("/exercises/:id", requireSuperAdmin, async (c) => { - const parsed = updateExerciseRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - parsed.error.issues[0]?.message ?? "Invalid payload", - ) - } + const parsed = await parseBody(c, updateExerciseRequestSchema) + if (!parsed.success) return parsed.response const dataError = exerciseDataError(parsed.data.type, parsed.data.data) if (dataError) return failure(c, 400, "invalid-exercise", dataError) const [updated] = await db diff --git a/apps/api/src/routes/ai.ts b/apps/api/src/routes/ai.ts index f8d6968..46f4741 100644 --- a/apps/api/src/routes/ai.ts +++ b/apps/api/src/routes/ai.ts @@ -36,7 +36,7 @@ import { getPreviousLogin, type AuthUser } from "../auth/session" import { config } from "../config" import { db, schema } from "../db" import { JudgeStatus, type JudgeStatusValue } from "../judge/status" -import { failure, success } from "../http" +import { failure, parseBody, readJson, success } from "../http" import { completeChat, streamChat, streamWhole } from "../services/ai" import { generateFilteredHint } from "../services/hint-filter" import { decideHintLevel } from "../services/hint-level" @@ -882,16 +882,12 @@ aiRoutes.get("/ai/pinned", requireAuth, async (c) => { }) aiRoutes.post("/ai/analysis", requireAuth, async (c) => { - const parsed = aiAnalysisRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + aiAnalysisRequestSchema, + "start, end and duration are required", ) - if (!parsed.success) - return failure( - c, - 400, - "invalid-request", - "start, end and duration are required", - ) + if (!parsed.success) return parsed.response if ( Number.isNaN(Date.parse(parsed.data.start)) || Number.isNaN(Date.parse(parsed.data.end)) @@ -983,11 +979,12 @@ async function recordHint( } aiRoutes.post("/ai/hint", requireAuth, async (c) => { - const parsed = aiHintRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + aiHintRequestSchema, + "submissionId is required", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "submissionId is required") + if (!parsed.success) return parsed.response const [row] = await db .select({ submission: schema.submission, problem: schema.problem }) .from(schema.submission) @@ -1081,9 +1078,7 @@ aiRoutes.post("/ai/hint", requireAuth, async (c) => { aiRoutes.post("/ai/hint/:id/feedback", requireAuth, async (c) => { const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = aiHintFeedbackRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) + const parsed = await readJson(c, aiHintFeedbackRequestSchema) if (!id || !parsed.success) return failure(c, 400, "invalid-request", "helpful is required") // 只能评自己的提示:顺着 submission 核对是不是本人。别人的和不存在的一样回 404, @@ -1114,11 +1109,12 @@ aiRoutes.post("/ai/hint/:id/feedback", requireAuth, async (c) => { aiRoutes.post("/ai/class-analysis", requireAuth, async (c) => { if (!isTeacherOrAbove(c.get("user"))) return failure(c, 403, "permission-denied", "Permission denied") - const parsed = classAnalysisRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + classAnalysisRequestSchema, + "Class data is required", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Class data is required") + if (!parsed.success) return parsed.response const limited = await throttleAi(c) if (limited) return limited return streamChat( @@ -1130,16 +1126,12 @@ aiRoutes.post("/ai/class-analysis", requireAuth, async (c) => { aiRoutes.post("/ai/class-pk-analysis", requireAuth, async (c) => { if (!isTeacherOrAbove(c.get("user"))) return failure(c, 403, "permission-denied", "Permission denied") - const parsed = classPkAnalysisRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + classPkAnalysisRequestSchema, + "At least two classes are required", ) - if (!parsed.success) - return failure( - c, - 400, - "invalid-request", - "At least two classes are required", - ) + if (!parsed.success) return parsed.response const limited = await throttleAi(c) if (limited) return limited return streamChat( diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index a92f0ce..e7f3f67 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -7,7 +7,7 @@ import { createSession, destroySession } from "../auth/session" import { publishSessionRevoked } from "../events" import { hashPassword, verifyPassword } from "../auth/password" import { db, schema } from "../db" -import { failure, success } from "../http" +import { failure, parseBody, success } from "../http" import { getUserProfileById } from "../services/profile" import { clientIp, @@ -32,17 +32,12 @@ const LOGIN_PER_USERNAME: AttemptRule = { limit: 10, windowSeconds: 15 * 60 } const LOGIN_PER_IP: AttemptRule = { limit: 100, windowSeconds: 15 * 60 } authRoutes.post("/auth/login", async (c) => { - const parsed = loginRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + loginRequestSchema, + "Username and password are required", ) - if (!parsed.success) { - return failure( - c, - 400, - "invalid-request", - "Username and password are required", - ) - } + if (!parsed.success) return parsed.response const usernameKey = `login:user:${parsed.data.username.toLowerCase()}` const ipKey = `login:ip:${clientIp(c)}` diff --git a/apps/api/src/routes/classroom.ts b/apps/api/src/routes/classroom.ts index 7d56711..b02ab5c 100644 --- a/apps/api/src/routes/classroom.ts +++ b/apps/api/src/routes/classroom.ts @@ -11,7 +11,7 @@ import { Hono } from "hono" import { requireAuth, type AppEnv } from "../auth/middleware" import { db, schema } from "../db" -import { failure, success } from "../http" +import { failure, parseBody, success } from "../http" import { JudgeStatus } from "../judge/status" import { queryInteger, rounded } from "./helpers" @@ -165,11 +165,12 @@ classroomRoutes.get("/me/class-rank", requireAuth, async (c) => { }) classroomRoutes.post("/classes/comparison", async (c) => { - const parsed = classComparisonRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + classComparisonRequestSchema, + "At least one class is required", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "At least one class is required") + if (!parsed.success) return parsed.response const users = await loadClassUsers(parsed.data.classNames) const allAc = users.map((user) => user.acceptedNumber) const globalQ1 = quantile(allAc, 0.25) diff --git a/apps/api/src/routes/content.ts b/apps/api/src/routes/content.ts index 31530c9..d6d5e55 100644 --- a/apps/api/src/routes/content.ts +++ b/apps/api/src/routes/content.ts @@ -23,7 +23,7 @@ import { Hono } from "hono" import { requireAuth, requireSuperAdmin, type AppEnv } from "../auth/middleware" import { db, schema } from "../db" -import { failure, success } from "../http" +import { failure, parseBody, success } from "../http" import { JudgeStatus } from "../judge/status" import { objectValue, queryInteger, sampleUser } from "./helpers" @@ -187,11 +187,12 @@ contentRoutes.get("/messages", requireAuth, async (c) => { */ contentRoutes.post("/messages", requireSuperAdmin, async (c) => { const user = c.get("user")! - const parsed = createMessageRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + createMessageRequestSchema, + "Invalid message payload", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Invalid message payload") + if (!parsed.success) return parsed.response if (parsed.data.recipientId === user.id) return failure( c, @@ -263,11 +264,12 @@ contentRoutes.get("/problems/:id/reaction", requireAuth, async (c) => { contentRoutes.post("/problems/:id/reaction", requireAuth, async (c) => { const problemId = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = setReactionRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + setReactionRequestSchema, + "Invalid reaction", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Invalid reaction") + if (!parsed.success) return parsed.response const user = c.get("user")! const [[problem], [solved]] = await Promise.all([ db @@ -447,11 +449,12 @@ contentRoutes.get("/learn/progress", requireAuth, async (c) => { contentRoutes.post("/tutorials/:id/progress", requireAuth, async (c) => { const user = c.get("user")! const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = tutorialProgressPingSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + tutorialProgressPingSchema, + "Invalid progress payload", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Invalid progress payload") + if (!parsed.success) return parsed.response const [tutorial] = await db .select({ id: schema.tutorial.id }) .from(schema.tutorial) @@ -501,11 +504,12 @@ contentRoutes.post("/tutorials/:id/progress", requireAuth, async (c) => { contentRoutes.post("/exercises/:id/attempts", requireAuth, async (c) => { const user = c.get("user")! const id = queryInteger(c.req.param("id"), 0, { min: 1 }) - const parsed = exerciseAttemptRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + exerciseAttemptRequestSchema, + "Invalid attempt payload", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Invalid attempt payload") + if (!parsed.success) return parsed.response // 练习跟着教程走:教程没公开,它底下的练习也不该能上报 const [exercise] = await db .select({ id: schema.exercise.id }) diff --git a/apps/api/src/routes/contest.ts b/apps/api/src/routes/contest.ts index f7f8048..e02a394 100644 --- a/apps/api/src/routes/contest.ts +++ b/apps/api/src/routes/contest.ts @@ -27,7 +27,7 @@ import { optionalAuth, requireAuth } from "../auth/middleware" import { setContestPassword } from "../auth/session" import { db, schema } from "../db" import { astRequirements } from "../judge/ast" -import { failure, success } from "../http" +import { failure, parseBody, success } from "../http" import { canAccessContest, checkContestPassword, @@ -163,11 +163,12 @@ contestRoutes.post("/contests/:id/access", requireAuth, async (c) => { ) if (!contest || !contest.password) return failure(c, 404, "contest-not-found", "Contest does not exist") - const parsed = contestPasswordRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + contestPasswordRequestSchema, + "Password is required", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Password is required") + if (!parsed.success) return parsed.response // 比赛密码往往就是几位数字,不限的话一个脚本几分钟就能扫完。 // 按「人 × 比赛」计失败次数:猜错的是自己,锁的也只是自己进这一场。 const attemptKey = `contest-password:${contest.id}:${c.get("user")!.id}` diff --git a/apps/api/src/routes/flowchart.ts b/apps/api/src/routes/flowchart.ts index 49d8b39..9d82449 100644 --- a/apps/api/src/routes/flowchart.ts +++ b/apps/api/src/routes/flowchart.ts @@ -26,7 +26,7 @@ import { Hono } from "hono" import { requireAuth, requireTeacher, type AppEnv } from "../auth/middleware" import { config } from "../config" import { db, schema } from "../db" -import { failure, success } from "../http" +import { failure, readJson, success } from "../http" import { flowchartQueue } from "../queue" import { getBooleanOption } from "../services/options" import { consumeToken } from "../services/throttling" @@ -85,9 +85,7 @@ function flowchartData( } flowchartRoutes.post("/flowcharts", requireAuth, async (c) => { - const parsed = createFlowchartRequestSchema.safeParse( - await c.req.json().catch(() => null), - ) + const parsed = await readJson(c, createFlowchartRequestSchema) if ( !parsed.success || JSON.stringify(parsed.data?.flowchartData ?? {}).length > 500 * 1024 diff --git a/apps/api/src/routes/judge-server.ts b/apps/api/src/routes/judge-server.ts index 2032d82..5abb244 100644 --- a/apps/api/src/routes/judge-server.ts +++ b/apps/api/src/routes/judge-server.ts @@ -6,7 +6,7 @@ import { z } from "zod" import { config } from "../config" import { db, schema } from "../db" -import { failure } from "../http" +import { failure, readJson } from "../http" const heartbeatSchema = z.object({ hostname: z.string().min(1).max(128), @@ -38,7 +38,7 @@ async function heartbeat(c: Context) { return failure(c, 403, "invalid-judge-token", "Invalid token") } - const parsed = heartbeatSchema.safeParse(await c.req.json().catch(() => null)) + const parsed = await readJson(c, heartbeatSchema) if (!parsed.success) { return failure(c, 400, "invalid-heartbeat", "Invalid heartbeat payload") } diff --git a/apps/api/src/routes/problemset.ts b/apps/api/src/routes/problemset.ts index e08437c..c5d78f2 100644 --- a/apps/api/src/routes/problemset.ts +++ b/apps/api/src/routes/problemset.ts @@ -31,7 +31,7 @@ import { type AppEnv, } from "../auth/middleware" import { db, schema } from "../db" -import { failure, success } from "../http" +import { failure, parseBody, success } from "../http" import { computeProgress } from "../services/problemset" import { asFilterValue, objectValue, queryInteger, sampleUser } from "./helpers" @@ -342,11 +342,12 @@ async function recomputeProgress( } problemsetRoutes.post("/problem-set-progress", requireAuth, async (c) => { - const parsed = joinProblemSetRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + joinProblemSetRequestSchema, + "Invalid problem set", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Invalid problem set") + if (!parsed.success) return parsed.response const user = c.get("user")! const [problemSet] = await db .select({ id: schema.problemset.id }) diff --git a/apps/api/src/routes/submission.ts b/apps/api/src/routes/submission.ts index 18f8045..0ad1ff6 100644 --- a/apps/api/src/routes/submission.ts +++ b/apps/api/src/routes/submission.ts @@ -31,7 +31,7 @@ import { } from "../auth/middleware" import type { AuthUser } from "../auth/session" import { db, schema } from "../db" -import { failure, success } from "../http" +import { failure, parseBody, success } from "../http" import { JudgeStatus } from "../judge/status" import { judgeQueue } from "../queue" import { @@ -93,12 +93,12 @@ async function saveTrace( } submissionRoutes.post("/submissions", requireAuth, async (c) => { - const parsed = createSubmissionRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + createSubmissionRequestSchema, + "Invalid submission payload", ) - if (!parsed.success) { - return failure(c, 400, "invalid-request", "Invalid submission payload") - } + if (!parsed.success) return parsed.response let contestId: number | null = null if (parsed.data.contestId) { // 这里用不了 requireContestAccess 中间件:比赛 id 来自请求体, @@ -293,11 +293,12 @@ submissionRoutes.post( ) submissionRoutes.post("/code/format", requireAuth, async (c) => { - const parsed = formatCodeRequestSchema.safeParse( - await c.req.json().catch(() => null), + const parsed = await parseBody( + c, + formatCodeRequestSchema, + "Invalid format payload", ) - if (!parsed.success) - return failure(c, 400, "invalid-request", "Invalid format payload") + if (!parsed.success) return parsed.response try { const code = await formatCode(parsed.data.code, parsed.data.language) return success(c, { code } satisfies FormatCodeResponse)