From 75dd9bfa42fd3def1844aebf249f0a7557f366b3 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 25 Aug 2026 23:59:46 -0600 Subject: [PATCH] =?UTF-8?q?fix(=E6=8F=90=E4=BA=A4):=20=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E7=9A=84=20/submission/:id=20=E9=A1=B5=E9=9D=A2=E4=B8=8A?= =?UTF-8?q?=E3=80=8C=E5=A4=8D=E5=88=B6=E5=9B=9E=E5=88=B0=E9=A2=98=E7=9B=AE?= =?UTF-8?q?=E3=80=8D=E6=8A=9B=20problemID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 复现与根因 在 `/submission/<提交id>` 这个独立页面点「复制回到题目」,必抛: Error: Missing required param "problemID" `detail.vue` 的 `problemID` 是**可选** prop,但 `copyToProblem()` 无条件把它塞 进 `router.push({ params: { problemID } })`。三个组件调用方(提交列表弹框、 题目页提交弹框、后台 ACM 助手)都显式传了这个 prop,所以弹框里一直是好的; 而路由 `submission/:submissionID` 配的是 `props: true`,只喂得进 `submissionID` —— 那条路上这个 prop 恒为 undefined。 组件想自己兜底也兜不了:`submissionDetailSchema` 只有 `problemId`(内部数字 id),没有拼路由要用的 display id(`problem._id`)。 ojnext 里是**一模一样**的代码,不是这次重写引入的。 ## 改法 后端补 `problemDisplayId`:`submissionDetail()` 本来就 join 了 problem 表, 不额外查库。前端 `props.problemID ?? submission.problemDisplayId`。 ## ⚠️ 还剩一条边没关 非管理员看到的 `contestId` 是被抹掉的(`full ? contestId : null`,对齐旧后端 `SubmissionSafeModelSerializer` 的 exclude,把关的是角色不是归属)。所以**学生 自己的比赛提交**从这个独立 URL 打开时,组件判断不出它属于比赛,会跳到公开题 路由 `/problem/<显示编号>`,那里查的是 `contestId is null`,落到「题目不存在」。 改之前这条路是抛异常,改之后是跳错地方 —— 都不对,但主路径(非比赛提交)现在 是对的。要彻底关掉得让后端把 `contestId` 也发给**提交本人**(不只是管理员), 那是在动一条刻意对齐旧后端的决定,留给你定。另外这条路由**全站没有任何入口 链接**(查过了),只有直接输 URL 或外部链接才会到。 ## 验证 真起服务、真点按钮: - 修之前:点击不跳转,控制台 `Missing required param "problemID"`, Vue 警告里能看到 `` 确实没有 problemID。 - 修之后:无异常,跳到 `/problem/1004`(那条提交对应的题), 编辑器里是这条提交的代码(`n = int(input())…`),语言也带过去了。 - 回归:走提交列表弹框(有传 prop 的那条路)照样无异常、照样跳 `/problem/1004`。 - tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、build 通过。 Co-Authored-By: Claude Opus 5 --- apps/api/src/routes/submission.ts | 2 ++ apps/web/src/oj/submission/detail.vue | 15 +++++++++------ packages/contract/src/submission.ts | 7 +++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/api/src/routes/submission.ts b/apps/api/src/routes/submission.ts index 0aad5fc..9f2a294 100644 --- a/apps/api/src/routes/submission.ts +++ b/apps/api/src/routes/submission.ts @@ -413,6 +413,8 @@ async function submissionDetail(id: string, user: AuthUser) { // 首轮修复只处理了 info 与 ip,这里补齐。 contestId: full ? row.submission.contestId : null, problemId: row.submission.problemId, + // problem 表本来就 join 了,不额外查库 + problemDisplayId: row.problem.displayId, showLink: true, canUnshare: canViewSubmission(user, row.submission, row.problem, row.contest, false), }) diff --git a/apps/web/src/oj/submission/detail.vue b/apps/web/src/oj/submission/detail.vue index 6d32baa..14677e0 100644 --- a/apps/web/src/oj/submission/detail.vue +++ b/apps/web/src/oj/submission/detail.vue @@ -78,10 +78,13 @@ function copyToCat() { function copyToProblem() { const { code, language, contestId } = submission.value! - // 编辑器的 storageKey 用 display id(problem._id),等于 props.problemID, - // 而非 submission.problem(内部数字 id) + // 编辑器的 storageKey 用 display id(problem._id),不是 submission.problemId + // (内部数字 id)。**不能只靠 props.problemID** —— 独立的 /submission/:id 路由 + // 只喂 submissionID,那个 prop 是 undefined,原来会一路带进 router.push 抛 + // `Missing required param "problemID"`。响应里的 problemDisplayId 就是干这个的。 + const problemID = props.problemID ?? submission.value!.problemDisplayId const contestIDForKey = contestId || null - const storageKey = `problem_${props.problemID}_contest_${contestIDForKey}_lang_${language}` + const storageKey = `problem_${problemID}_contest_${contestIDForKey}_lang_${language}` storage.set(storageKey, code) // 设置语言 + 代码:localStorage 覆盖全新挂载的编辑器, // setCode 覆盖已挂载(同页 modal)的编辑器 @@ -92,17 +95,17 @@ function copyToProblem() { if (contestId) { router.push({ name: "contest problem", - params: { contestID: String(contestId), problemID: props.problemID }, + params: { contestID: String(contestId), problemID }, }) } else if (problemSetId) { router.push({ name: "problemset problem", - params: { problemSetId, problemID: props.problemID }, + params: { problemSetId, problemID }, }) } else { router.push({ name: "problem", - params: { problemID: props.problemID }, + params: { problemID }, }) } diff --git a/packages/contract/src/submission.ts b/packages/contract/src/submission.ts index bc257a5..8d06ab6 100644 --- a/packages/contract/src/submission.ts +++ b/packages/contract/src/submission.ts @@ -42,6 +42,13 @@ export const submissionDetailSchema = z.object({ ip: z.string().nullable(), contestId: z.number().int().nullable(), problemId: z.number().int(), + /** + * 题目的展示编号(problem._id)。**独立的 /submission/:id 页面要靠它** —— + * 那条路由只喂 submissionID,组件拿不到 display id,而「复制回到题目」要用它 + * 拼路由。原来只给内部数字 id,于是那个按钮在这条路由上一点就抛 + * `Missing required param "problemID"`。 + */ + problemDisplayId: z.string(), showLink: z.boolean(), canUnshare: z.boolean(), })