From cebaa87b13b8d4145e39ad7bebfc41a1bbd033cf Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Fri, 7 Aug 2026 07:01:13 -0600 Subject: [PATCH] =?UTF-8?q?fix(Minor=20M-1):=20=E7=AB=99=E5=86=85=E4=BF=A1?= =?UTF-8?q?=E5=86=85=E5=B5=8C=E6=8F=90=E4=BA=A4=E6=94=B9=E7=94=A8=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=20schema=EF=BC=8C=E5=B9=B6=E4=BF=AE=E5=A4=8D=E9=A2=98?= =?UTF-8?q?=E5=8F=B7=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两件事: 1. 内嵌的 submission 之前复用 submissionDetailSchema 并把 info / ip 写死成空值, 于是键仍留在响应里。旧 SubmissionSafeModelSerializer 是 exclude,这三个键 根本不出现。改成独立的 embeddedSubmissionSchema —— 形状对上了,将来有人 把空值改成真值也不会变成泄露,因为这里压根没有这三个字段。 2. 顺带修掉一个评审没覆盖的真回归:内嵌 submission 给的是 problemId(数字主键), 而旧 serializer 的 problem 是 SlugRelatedField(slug_field="_id"),即展示用题号。 oj/user/message.vue:20 拿它拼 /problem/<题号>,迁移后拼出的是 /problem/undefined, 题号那一栏也是空的。改为下发 problem,实测返回 "1004"。 Co-Authored-By: Claude Opus 5 --- apps/api/src/routes/content.ts | 17 +++++++++-------- packages/contract/src/content.ts | 4 ++-- packages/contract/src/submission.ts | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/apps/api/src/routes/content.ts b/apps/api/src/routes/content.ts index 00c17b8..904105f 100644 --- a/apps/api/src/routes/content.ts +++ b/apps/api/src/routes/content.ts @@ -8,7 +8,7 @@ import { reactionKeySchema, reactionStateSchema, setReactionRequestSchema, - submissionDetailSchema, + embeddedSubmissionSchema, tutorialSchema, tutorialSummarySchema, } from "@oj2/contract" @@ -73,32 +73,33 @@ contentRoutes.get("/messages", requireAuth, async (c) => { const offset = queryInteger(c.req.query("offset"), 0, { min: 0 }) const [totalRows, rows] = await Promise.all([ db.select({ value: count() }).from(schema.message).where(eq(schema.message.recipientId, user.id)), - db.select({ message: schema.message, sender: schema.user, realName: schema.userProfile.realName, submission: schema.submission }) + db.select({ message: schema.message, sender: schema.user, realName: schema.userProfile.realName, submission: schema.submission, displayId: schema.problem.displayId }) .from(schema.message).innerJoin(schema.user, eq(schema.message.senderId, schema.user.id)) .leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id)) .innerJoin(schema.submission, eq(schema.message.submissionId, schema.submission.id)) + .innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id)) .where(eq(schema.message.recipientId, user.id)).orderBy(desc(schema.message.createTime)).limit(limit).offset(offset), ]) return success(c, messageListSchema.parse({ - results: rows.map(({ message, sender, realName, submission }) => messageSchema.parse({ + results: rows.map(({ message, sender, realName, submission, displayId }) => messageSchema.parse({ id: message.id, sender: sampleUser(sender, realName), createTime: message.createTime, message: message.message, - submission: submissionDetailSchema.parse({ + submission: embeddedSubmissionSchema.parse({ id: submission.id, createTime: submission.createTime, userId: submission.userId, username: submission.username, code: submission.code, result: submission.result, - info: {}, + // info / ip / contestId 三个字段不在 embeddedSubmissionSchema 里,故不传 —— + // 对齐旧后端 SubmissionSafeModelSerializer 的 exclude,这三个键不出现在响应中 language: submission.language, shared: submission.shared, statisticInfo: objectValue(submission.statisticInfo), - ip: null, - contestId: submission.contestId, - problemId: submission.problemId, + // 展示用题号而非数字主键,站内信页面拿它拼 /problem/<题号> + problem: displayId, showLink: true, canUnshare: false, }), diff --git a/packages/contract/src/content.ts b/packages/contract/src/content.ts index ee4a4be..9b666b4 100644 --- a/packages/contract/src/content.ts +++ b/packages/contract/src/content.ts @@ -1,7 +1,7 @@ import { z } from "zod" import { paginatedSchema, sampleUserSchema } from "./common" -import { submissionDetailSchema } from "./submission" +import { embeddedSubmissionSchema } from "./submission" export const announcementSchema = z.object({ id: z.number().int(), @@ -21,7 +21,7 @@ export const messageSchema = z.object({ sender: sampleUserSchema, createTime: z.string(), message: z.string(), - submission: submissionDetailSchema, + submission: embeddedSubmissionSchema, }) export const messageListSchema = paginatedSchema(messageSchema) diff --git a/packages/contract/src/submission.ts b/packages/contract/src/submission.ts index 710f76c..0ed8ff2 100644 --- a/packages/contract/src/submission.ts +++ b/packages/contract/src/submission.ts @@ -46,6 +46,21 @@ export const submissionDetailSchema = z.object({ canUnshare: z.boolean(), }) +/** + * 内嵌在别处(目前只有站内信)的提交对象。对齐旧后端的 + * `SubmissionSafeModelSerializer(exclude=("info", "contest", "ip"))` —— + * 这三个键**根本不出现**,而不是出现但值为空。 + * + * 独立成一个 schema 而不是复用 submissionDetailSchema 传空值:形状一致了, + * 将来有人「顺手」把空值改成真值就不会变成泄露,因为这里压根没有这三个字段。 + */ +export const embeddedSubmissionSchema = submissionDetailSchema + .omit({ info: true, ip: true, contestId: true, problemId: true }) + // 旧 SubmissionSafeModelSerializer 里 problem 是 + // `SlugRelatedField(slug_field="_id")`,即**展示用题号**而非数字主键。 + // 站内信页面拿它拼 `/problem/<题号>` 链接,给数字 id 会拼出打不开的地址。 + .extend({ problem: z.string() }) + export const submissionUpdateSchema = z.object({ type: z.literal("submission_update"), submission_id: z.string(),