refactor(api): 请求体解析收成 readJson / parseBody

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 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 15:04:19 +08:00
1 parent c43da01ff2
commit f2ca34fd4b
21 files changed
+234 -432

No files matched your search

+22 -30
View File
@@ -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(