Compare commits

...

2 Commits

Author SHA1 Message Date
d3365ecb32 ci(部署): actions/checkout v5→v7、actions/cache v4→v6
Some checks failed
Deploy / deploy (push) Has been cancelled
`oven-sh/setup-bun` 已经在最新大版本(v2.2.0 属于 v2 线),不动。

**actions/cache v4 → v6** 是这次的实际收益:v4 跑在 **node20** 上,GitHub 正在
退役这个 runtime;v6 是 node24,和另外两个 action 一致。v6.0.0 的改动是「迁到
ESM + 更新依赖」,没有输入输出上的破坏性变更。

  ⚠️ cache v5 起要求 Actions Runner >= 2.327.1。这里 `runs-on: ubuntu-latest`
  是 GitHub 托管的、始终满足;哪天换自建 runner 要留意这条(已写进 yml 注释)。

**actions/checkout v5 → v7**:v5 本来就跑在 node24,运行时没变。两条标了
BREAKING 的改动都打不到这个工作流:

- `allow-unsafe-pr-checkout`(更安全的 pull_request_target 默认值)已经反向
  移植到 v2 到 v6 所有线,v5 上本来就有;
- v7.0.0 的「block checking out fork pr」只影响 `pull_request_target` 和
  `workflow_run`。

本工作流的触发器只有 `push: main` 和 `workflow_dispatch`,两者都不涉及。

版本按仓库原有习惯钉大版本号,没钉到 patch。

验证:只做了 YAML 语法校验 —— **这条工作流要真的 push 到 github 才跑得起来,
本机验不了**。下次 `git push github main` 时留意一下 Actions 那边。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 00:01:28 -06:00
75dd9bfa42 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>
2026-08-25 23:59:46 -06:00
4 changed files with 24 additions and 9 deletions

View File

@@ -39,14 +39,17 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# 依赖装到 ~/.bun/install/cache命中的话 install 只剩几秒
- uses: actions/cache@v4
# 依赖装到 ~/.bun/install/cache命中的话 install 只剩几秒
# v6 起跑在 node24 上v4 还是 node20GitHub 正在退役 node20 runtime
# 和上面两个 action 一致。v5+ 要求 runner >= 2.327.1ubuntu-latest 是
# GitHub 托管的、始终满足;换自建 runner 的话要留意这条。
- uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}

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

View File

@@ -78,10 +78,13 @@ function copyToCat() {
function copyToProblem() {
const { code, language, contestId } = submission.value!
// 编辑器的 storageKey 用 display idproblem._id等于 props.problemID
// 而非 submission.problem(内部数字 id
// 编辑器的 storageKey 用 display idproblem._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 },
})
}

View File

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