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:
@@ -144,7 +144,7 @@ contestRoutes.get("/contests/:id/problems", optionalAuth, requireContestAccess("
|
|||||||
title: problem.title,
|
title: problem.title,
|
||||||
submissionNumber: allowed ? problem.submissionNumber : 0,
|
submissionNumber: allowed ? problem.submissionNumber : 0,
|
||||||
acceptedNumber: allowed ? problem.acceptedNumber : 0,
|
acceptedNumber: allowed ? problem.acceptedNumber : 0,
|
||||||
difficulty: allowed ? problem.difficulty : "",
|
difficulty: allowed ? problem.difficulty : null,
|
||||||
createdBy: sampleUser(user, realName),
|
createdBy: sampleUser(user, realName),
|
||||||
tags: tags.get(problem.id) ?? [],
|
tags: tags.get(problem.id) ?? [],
|
||||||
contestId: contest.id,
|
contestId: contest.id,
|
||||||
@@ -179,7 +179,7 @@ contestRoutes.get("/contests/:id/problems/:displayId", optionalAuth, requireCont
|
|||||||
lastUpdateTime: row.problem.lastUpdateTime,
|
lastUpdateTime: row.problem.lastUpdateTime,
|
||||||
timeLimit: row.problem.timeLimit,
|
timeLimit: row.problem.timeLimit,
|
||||||
memoryLimit: row.problem.memoryLimit,
|
memoryLimit: row.problem.memoryLimit,
|
||||||
difficulty: allowed ? row.problem.difficulty : "",
|
difficulty: allowed ? row.problem.difficulty : null,
|
||||||
source: row.problem.source,
|
source: row.problem.source,
|
||||||
prompt: row.problem.prompt,
|
prompt: row.problem.prompt,
|
||||||
submissionNumber: allowed ? row.problem.submissionNumber : 0,
|
submissionNumber: allowed ? row.problem.submissionNumber : 0,
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ onMounted(() => {
|
|||||||
<n-descriptions-item label="创建时间">
|
<n-descriptions-item label="创建时间">
|
||||||
{{ parseTime(problem.createTime) }}
|
{{ parseTime(problem.createTime) }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="难度">
|
<!-- 比赛进行中,难度不下发(difficulty 为 null),这一项整个不显示 -->
|
||||||
|
<n-descriptions-item v-if="problem.difficulty" label="难度">
|
||||||
<n-tag :type="getTagColor(problem.difficulty)">
|
<n-tag :type="getTagColor(problem.difficulty)">
|
||||||
{{ DIFFICULTY[problem.difficulty] }}
|
{{ DIFFICULTY[problem.difficulty] }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
|||||||
@@ -173,7 +173,9 @@ const baseColumns: DataTableColumn<ProblemFiltered>[] = [
|
|||||||
key: "difficulty",
|
key: "difficulty",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) =>
|
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"),
|
title: renderTableTitle("标签", "streamline-ultimate-color:attachment"),
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function handleProblemClick(problemId: string) {
|
|||||||
|
|
||||||
<n-flex align="center" size="small">
|
<n-flex align="center" size="small">
|
||||||
<n-tag
|
<n-tag
|
||||||
|
v-if="problemSetProblem.problem.difficulty"
|
||||||
:type="getTagColor(problemSetProblem.problem.difficulty)"
|
:type="getTagColor(problemSetProblem.problem.difficulty)"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function filterResult(result: ProblemListItem): ProblemFiltered {
|
|||||||
id: result.id,
|
id: result.id,
|
||||||
_id: result._id,
|
_id: result._id,
|
||||||
title: result.title,
|
title: result.title,
|
||||||
difficulty: DIFFICULTY[result.difficulty],
|
difficulty: result.difficulty ? DIFFICULTY[result.difficulty] : null,
|
||||||
tags: result.tags,
|
tags: result.tags,
|
||||||
submission: result.submissionNumber,
|
submission: result.submissionNumber,
|
||||||
rate: getACRate(result.acceptedNumber, result.submissionNumber),
|
rate: getACRate(result.acceptedNumber, result.submissionNumber),
|
||||||
|
|||||||
@@ -176,7 +176,8 @@ export interface ProblemFiltered {
|
|||||||
_id: string
|
_id: string
|
||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
difficulty: "简单" | "中等" | "困难"
|
// 比赛进行中难度不下发,见 contract 的 maskedProblemDifficultySchema
|
||||||
|
difficulty: "简单" | "中等" | "困难" | null
|
||||||
tags: string[]
|
tags: string[]
|
||||||
submission: number
|
submission: number
|
||||||
rate: string
|
rate: string
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ import { paginatedSchema, sampleUserSchema } from "./common"
|
|||||||
*/
|
*/
|
||||||
export const problemDifficultySchema = z.enum(["Low", "Mid", "High"])
|
export const problemDifficultySchema = z.enum(["Low", "Mid", "High"])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目详情/列表里的难度。**可为 null** —— 比赛进行中、看的人又不是管理员时,
|
||||||
|
* 难度和提交数一样属于「先别告诉参赛者」的信息(旧 Django 的
|
||||||
|
* `ProblemSafeSerializer` 直接把 difficulty 放进 exclude,字段整个不下发)。
|
||||||
|
*
|
||||||
|
* 端上必须当「没有」处理,不要拿它去查 DIFFICULTY 映射表 —— 这正是
|
||||||
|
* problemDifficultySchema 写成严格枚举要防的那件事。
|
||||||
|
*/
|
||||||
|
export const maskedProblemDifficultySchema = problemDifficultySchema.nullable()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SQL 题配置与展示数据。两者都是 `problem.sql_config` / `problem.sql_display`
|
* SQL 题配置与展示数据。两者都是 `problem.sql_config` / `problem.sql_display`
|
||||||
* 的 **JSONB 原文**,所以键名保持 snake_case —— 生产库 9 道 SQL 题存的就是这个形状
|
* 的 **JSONB 原文**,所以键名保持 snake_case —— 生产库 9 道 SQL 题存的就是这个形状
|
||||||
@@ -292,7 +302,7 @@ export const problemDetailSchema = z.object({
|
|||||||
lastUpdateTime: z.string().nullable(),
|
lastUpdateTime: z.string().nullable(),
|
||||||
timeLimit: z.number().int(),
|
timeLimit: z.number().int(),
|
||||||
memoryLimit: z.number().int(),
|
memoryLimit: z.number().int(),
|
||||||
difficulty: problemDifficultySchema,
|
difficulty: maskedProblemDifficultySchema,
|
||||||
source: z.string().nullable(),
|
source: z.string().nullable(),
|
||||||
prompt: z.string().nullable(),
|
prompt: z.string().nullable(),
|
||||||
submissionNumber: z.number().int(),
|
submissionNumber: z.number().int(),
|
||||||
@@ -327,7 +337,7 @@ export const problemListItemSchema = z.object({
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
submissionNumber: z.number().int(),
|
submissionNumber: z.number().int(),
|
||||||
acceptedNumber: z.number().int(),
|
acceptedNumber: z.number().int(),
|
||||||
difficulty: problemDifficultySchema,
|
difficulty: maskedProblemDifficultySchema,
|
||||||
createdBy: sampleUserSchema,
|
createdBy: sampleUserSchema,
|
||||||
tags: z.array(z.string()),
|
tags: z.array(z.string()),
|
||||||
contestId: z.number().int().nullable(),
|
contestId: z.number().int().nullable(),
|
||||||
|
|||||||
Reference in New Issue
Block a user