diff --git a/apps/api/src/routes/contest.ts b/apps/api/src/routes/contest.ts
index f430874..aa05e36 100644
--- a/apps/api/src/routes/contest.ts
+++ b/apps/api/src/routes/contest.ts
@@ -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,
diff --git a/apps/web/src/oj/problem/components/ProblemInfo.vue b/apps/web/src/oj/problem/components/ProblemInfo.vue
index 02047c3..06d470f 100644
--- a/apps/web/src/oj/problem/components/ProblemInfo.vue
+++ b/apps/web/src/oj/problem/components/ProblemInfo.vue
@@ -121,7 +121,8 @@ onMounted(() => {
{{ parseTime(problem.createTime) }}
-
+
+
{{ DIFFICULTY[problem.difficulty] }}
diff --git a/apps/web/src/oj/problem/list.vue b/apps/web/src/oj/problem/list.vue
index a690ed2..6194d5c 100644
--- a/apps/web/src/oj/problem/list.vue
+++ b/apps/web/src/oj/problem/list.vue
@@ -173,7 +173,9 @@ const baseColumns: DataTableColumn[] = [
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"),
diff --git a/apps/web/src/oj/problemset/components/ProblemSetProblemsList.vue b/apps/web/src/oj/problemset/components/ProblemSetProblemsList.vue
index e419691..65590c6 100644
--- a/apps/web/src/oj/problemset/components/ProblemSetProblemsList.vue
+++ b/apps/web/src/oj/problemset/components/ProblemSetProblemsList.vue
@@ -54,6 +54,7 @@ function handleProblemClick(problemId: string) {
diff --git a/apps/web/src/oj/transforms.ts b/apps/web/src/oj/transforms.ts
index 38504e9..edd56d5 100644
--- a/apps/web/src/oj/transforms.ts
+++ b/apps/web/src/oj/transforms.ts
@@ -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),
diff --git a/apps/web/src/utils/types.ts b/apps/web/src/utils/types.ts
index 07810f2..6f4f9e7 100644
--- a/apps/web/src/utils/types.ts
+++ b/apps/web/src/utils/types.ts
@@ -176,7 +176,8 @@ export interface ProblemFiltered {
_id: string
id: number
title: string
- difficulty: "简单" | "中等" | "困难"
+ // 比赛进行中难度不下发,见 contract 的 maskedProblemDifficultySchema
+ difficulty: "简单" | "中等" | "困难" | null
tags: string[]
submission: number
rate: string
diff --git a/packages/contract/src/problem.ts b/packages/contract/src/problem.ts
index 57bbb6d..9a926ca 100644
--- a/packages/contract/src/problem.ts
+++ b/packages/contract/src/problem.ts
@@ -9,6 +9,16 @@ import { paginatedSchema, sampleUserSchema } from "./common"
*/
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`
* 的 **JSONB 原文**,所以键名保持 snake_case —— 生产库 9 道 SQL 题存的就是这个形状
@@ -292,7 +302,7 @@ export const problemDetailSchema = z.object({
lastUpdateTime: z.string().nullable(),
timeLimit: z.number().int(),
memoryLimit: z.number().int(),
- difficulty: problemDifficultySchema,
+ difficulty: maskedProblemDifficultySchema,
source: z.string().nullable(),
prompt: z.string().nullable(),
submissionNumber: z.number().int(),
@@ -327,7 +337,7 @@ export const problemListItemSchema = z.object({
title: z.string(),
submissionNumber: z.number().int(),
acceptedNumber: z.number().int(),
- difficulty: problemDifficultySchema,
+ difficulty: maskedProblemDifficultySchema,
createdBy: sampleUserSchema,
tags: z.array(z.string()),
contestId: z.number().int().nullable(),