fix(比赛): 比赛进行中,学生打开比赛题必 500

contest.ts 两处把「藏起来的难度」表示成空串,而 problemDifficultySchema
是严格的三值枚举,parse 直接抛 ZodError。contestDetailsAllowed 在
「比赛进行中 + 看的人不是管理员」时就是假 —— 也就是正常学生在正常比赛里,
比赛题列表和详情页一开就挂。dev 库 contest 表一直是空的,这条路径没被跑到过。

改成下发 null,契约里给这两个字段单独起了 maskedProblemDifficultySchema。
语义上对齐旧 Django:ProblemSafeSerializer 把 difficulty 放在 exclude 里,
字段整个不下发,而不是给一个假值。

端上顺着 null 补了四处:ProblemInfo 的「难度」项整条不渲染(和旧栈表现一致),
题目列表、题单列表同理,transforms 的 ProblemFiltered.difficulty 也放开为可空。
这几处是 vue-tsc 逐个揪出来的,正是严格枚举该起的作用。

实测(学生 / 超管 × 比赛题 / 普通题 四种组合):学生看比赛题详情与列表
从 500 变 200 且 difficulty 为 null,超管仍拿到真值 Low,普通题不受影响;
页面上「难度」整项消失,统计面板其余内容正常。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016DHxhKNxXfG89JnVzHbvgj
This commit is contained in:
2026-08-30 09:01:56 -06:00
parent c7132025f7
commit 4b6242ba70
7 changed files with 23 additions and 8 deletions

View File

@@ -144,7 +144,7 @@ contestRoutes.get("/contests/:id/problems", optionalAuth, requireContestAccess("
title: problem.title,
submissionNumber: allowed ? problem.submissionNumber : 0,
acceptedNumber: allowed ? problem.acceptedNumber : 0,
difficulty: allowed ? problem.difficulty : "",
difficulty: allowed ? problem.difficulty : null,
createdBy: sampleUser(user, realName),
tags: tags.get(problem.id) ?? [],
contestId: contest.id,
@@ -179,7 +179,7 @@ contestRoutes.get("/contests/:id/problems/:displayId", optionalAuth, requireCont
lastUpdateTime: row.problem.lastUpdateTime,
timeLimit: row.problem.timeLimit,
memoryLimit: row.problem.memoryLimit,
difficulty: allowed ? row.problem.difficulty : "",
difficulty: allowed ? row.problem.difficulty : null,
source: row.problem.source,
prompt: row.problem.prompt,
submissionNumber: allowed ? row.problem.submissionNumber : 0,

View File

@@ -121,7 +121,8 @@ onMounted(() => {
<n-descriptions-item label="创建时间">
{{ parseTime(problem.createTime) }}
</n-descriptions-item>
<n-descriptions-item label="难度">
<!-- 比赛进行中难度不下发difficulty null这一项整个不显示 -->
<n-descriptions-item v-if="problem.difficulty" label="难度">
<n-tag :type="getTagColor(problem.difficulty)">
{{ DIFFICULTY[problem.difficulty] }}
</n-tag>

View File

@@ -173,7 +173,9 @@ const baseColumns: DataTableColumn<ProblemFiltered>[] = [
key: "difficulty",
width: 100,
render: (row) =>
h(NTag, { type: getTagColor(row.difficulty) }, () => row.difficulty),
row.difficulty
? h(NTag, { type: getTagColor(row.difficulty) }, () => row.difficulty)
: null,
},
{
title: renderTableTitle("标签", "streamline-ultimate-color:attachment"),

View File

@@ -54,6 +54,7 @@ function handleProblemClick(problemId: string) {
<n-flex align="center" size="small">
<n-tag
v-if="problemSetProblem.problem.difficulty"
:type="getTagColor(problemSetProblem.problem.difficulty)"
size="small"
>

View File

@@ -8,7 +8,7 @@ export function filterResult(result: ProblemListItem): ProblemFiltered {
id: result.id,
_id: result._id,
title: result.title,
difficulty: DIFFICULTY[result.difficulty],
difficulty: result.difficulty ? DIFFICULTY[result.difficulty] : null,
tags: result.tags,
submission: result.submissionNumber,
rate: getACRate(result.acceptedNumber, result.submissionNumber),

View File

@@ -176,7 +176,8 @@ export interface ProblemFiltered {
_id: string
id: number
title: string
difficulty: "简单" | "中等" | "困难"
// 比赛进行中难度不下发,见 contract 的 maskedProblemDifficultySchema
difficulty: "简单" | "中等" | "困难" | null
tags: string[]
submission: number
rate: string