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:
@@ -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),
|
||||
})
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user