fix(提交): 独立的 /submission/:id 页面上「复制回到题目」抛 problemID

## 复现与根因

在 `/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 警告里能看到 `<Detail submissionID="..." >` 确实没有 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 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 23:59:46 -06:00
parent dd6a6a04eb
commit 75dd9bfa42
3 changed files with 18 additions and 6 deletions

View File

@@ -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),
})