feat(题单): 「截止时间」改成「布置期」,布置期内没加入的人也藏旧代码;老师能看全班情况,后台一次加好几道题
- 布置期(列名仍是 end_time,机房共库不改名)+ 新列 assigned_at:布置期内题单里这几道题, 学生在布置开始之前交的代码先藏起来,不管加没加入,堵住「先抄再加入」;做对或到期放出来,不拦做题 - 「总分达到 N」奖章换成「做对 N 道」,草稿并进「不公开」(0026) - /problem-sets 带布置期、必做题数、奖章获得时间;题目带做对时刻、错几次、旧代码藏着没有 - 新增 /problem-sets/:id/class-view(老师看一个班:真名 × 每题,加入前就做对过)、/problem-sets/locks - 后台:批量加题、拖动排序、切必做;去掉状态、难度、分数、提示和单独的进度页 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2224478294
commit
8c53e28dcb
11 files changed
+4958
-617
No files matched your search
@@ -6,6 +6,7 @@ import { achievementRaritySchema } from "./achievement"
|
||||
import { rankProfileSchema } from "./account"
|
||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||
import { reactionKeySchema } from "./content"
|
||||
import { badgeConditionTypeSchema } from "./problemset"
|
||||
import { problemLanguageSchema, runnableLanguageSchema } from "./language"
|
||||
import {
|
||||
astRuleSchema,
|
||||
@@ -358,17 +359,22 @@ export const updateAcmHelperRequestSchema = z.object({
|
||||
|
||||
// ---------------------------------------------------------------- 题单管理
|
||||
|
||||
// 难度、状态两列还留在表里(0026 没删列,删列要手工放行破坏性迁移),
|
||||
// 但界面和接口都不再用:线上 16 个题单全是 Easy / active,「状态 + 可见」两道闸合成了「公开」
|
||||
export const problemSetDifficultySchema = z.enum(["Easy", "Medium", "Hard"])
|
||||
export const problemSetStatusSchema = z.enum(["draft", "active", "archived"])
|
||||
export const badgeConditionTypeSchema = z.enum(["all_problems", "problem_count", "score"])
|
||||
|
||||
export const adminProblemSetSchema = z.object({
|
||||
id: z.number().int(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
difficulty: problemSetDifficultySchema,
|
||||
status: problemSetStatusSchema,
|
||||
endTime: z.string().nullable(),
|
||||
/** 布置到哪天(东八区当天结束);null = 没布置 */
|
||||
assignedUntil: z.string().nullable(),
|
||||
/** 这一轮布置开始的时刻 */
|
||||
assignedAt: z.string().nullable(),
|
||||
/** 现在在不在布置期内(服务端的钟) */
|
||||
assigning: z.boolean(),
|
||||
/** 公开:学生能在题单列表里看到、能进 */
|
||||
visible: z.boolean(),
|
||||
createdBy: sampleUserSchema,
|
||||
createTime: z.string(),
|
||||
@@ -376,23 +382,22 @@ export const adminProblemSetSchema = z.object({
|
||||
problemsCount: z.number().int(),
|
||||
/** 加入这份题单的人数,后台用来判断改动会影响多少人 */
|
||||
participantCount: z.number().int(),
|
||||
/** 做完(必做题全对)的人数 */
|
||||
completedCount: z.number().int(),
|
||||
badgeCount: z.number().int(),
|
||||
})
|
||||
|
||||
export const adminProblemSetListSchema = paginatedSchema(adminProblemSetSchema)
|
||||
|
||||
export const createProblemSetRequestSchema = z.object({
|
||||
title: z.string().trim().min(1).max(200),
|
||||
description: z.string(),
|
||||
difficulty: problemSetDifficultySchema.default("Easy"),
|
||||
status: problemSetStatusSchema.default("active"),
|
||||
endTime: z.string().nullable().default(null),
|
||||
description: z.string().default(""),
|
||||
/** 布置到哪天;null = 不布置。从「没在布置」变成「布置到将来」时,服务端把布置开始记成现在 */
|
||||
assignedUntil: z.string().nullable().default(null),
|
||||
visible: z.boolean().default(true),
|
||||
})
|
||||
|
||||
export const updateProblemSetRequestSchema = createProblemSetRequestSchema
|
||||
export const updateProblemSetStatusRequestSchema = z.object({
|
||||
status: problemSetStatusSchema,
|
||||
})
|
||||
|
||||
export const adminProblemSetProblemSchema = z.object({
|
||||
id: z.number().int(),
|
||||
@@ -403,24 +408,28 @@ export const adminProblemSetProblemSchema = z.object({
|
||||
difficulty: z.string(),
|
||||
order: z.number().int(),
|
||||
isRequired: z.boolean(),
|
||||
score: z.number().int(),
|
||||
hint: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const addProblemToSetRequestSchema = z.object({
|
||||
/** 展示用题号(_id),不是自增主键 —— 老师手里只有题号 */
|
||||
problemId: z.string().trim().min(1),
|
||||
order: z.number().int().default(0),
|
||||
isRequired: z.boolean().default(true),
|
||||
score: z.number().int().default(0),
|
||||
hint: z.string().default(""),
|
||||
export const addProblemsToSetRequestSchema = z.object({
|
||||
/** 展示用题号(_id),不是自增主键 —— 老师手里只有题号。按给的顺序接在最后 */
|
||||
problemIds: z.array(z.string().trim().min(1)).min(1).max(100),
|
||||
})
|
||||
|
||||
export const addProblemsToSetResultSchema = z.object({
|
||||
added: z.array(z.string()),
|
||||
/** 题号不存在、不可见或是比赛题 */
|
||||
missing: z.array(z.string()),
|
||||
/** 本来就在题单里 */
|
||||
duplicate: z.array(z.string()),
|
||||
})
|
||||
|
||||
export const updateProblemInSetRequestSchema = z.object({
|
||||
order: z.number().int().optional(),
|
||||
isRequired: z.boolean().optional(),
|
||||
score: z.number().int().optional(),
|
||||
hint: z.string().optional(),
|
||||
isRequired: z.boolean(),
|
||||
})
|
||||
|
||||
/** 拖动排序:题单里题目的链接 id,按新顺序给全 */
|
||||
export const reorderProblemSetProblemsRequestSchema = z.object({
|
||||
ids: z.array(z.number().int()).min(1),
|
||||
})
|
||||
|
||||
export const adminProblemSetBadgeSchema = z.object({
|
||||
@@ -438,27 +447,13 @@ export const adminProblemSetBadgeSchema = z.object({
|
||||
export const createProblemSetBadgeRequestSchema = z.object({
|
||||
name: z.string().trim().min(1).max(100),
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
icon: z.string().min(1),
|
||||
conditionType: badgeConditionTypeSchema,
|
||||
conditionValue: z.number().int().default(0),
|
||||
})
|
||||
|
||||
export const updateProblemSetBadgeRequestSchema = createProblemSetBadgeRequestSchema
|
||||
|
||||
export const adminProblemSetProgressSchema = z.object({
|
||||
id: z.number().int(),
|
||||
userId: z.number().int(),
|
||||
username: z.string(),
|
||||
realName: z.string().nullable(),
|
||||
joinTime: z.string(),
|
||||
completeTime: z.string().nullable(),
|
||||
isCompleted: z.boolean(),
|
||||
progressPercentage: z.number(),
|
||||
completedProblemsCount: z.number().int(),
|
||||
totalProblemsCount: z.number().int(),
|
||||
totalScore: z.number().int(),
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- 标签与题目分析
|
||||
|
||||
export const adminTagSchema = z.object({
|
||||
@@ -927,7 +922,6 @@ export type AstCheckRequest = z.infer<typeof astCheckRequestSchema>
|
||||
export type AstCheckResponse = z.infer<typeof astCheckResponseSchema>
|
||||
export type GenerateTestInputsRequest = z.infer<typeof generateTestInputsRequestSchema>
|
||||
export type GenerateTestInputsResponse = z.infer<typeof generateTestInputsResponseSchema>
|
||||
export type AdminProblemSetProgress = z.infer<typeof adminProblemSetProgressSchema>
|
||||
|
||||
export type AdminAnnouncementList = z.infer<typeof adminAnnouncementListSchema>
|
||||
export type CreateAnnouncementRequest = z.infer<typeof createAnnouncementRequestSchema>
|
||||
@@ -959,14 +953,16 @@ export type UpdateContestRequest = z.infer<typeof updateContestRequestSchema>
|
||||
export type UpdateAcmHelperRequest = z.infer<typeof updateAcmHelperRequestSchema>
|
||||
export type ProblemSetDifficulty = z.infer<typeof problemSetDifficultySchema>
|
||||
export type ProblemSetStatus = z.infer<typeof problemSetStatusSchema>
|
||||
export type BadgeConditionType = z.infer<typeof badgeConditionTypeSchema>
|
||||
export type AdminProblemSet = z.infer<typeof adminProblemSetSchema>
|
||||
export type AdminProblemSetList = z.infer<typeof adminProblemSetListSchema>
|
||||
export type CreateProblemSetRequest = z.infer<typeof createProblemSetRequestSchema>
|
||||
export type UpdateProblemSetRequest = z.infer<typeof updateProblemSetRequestSchema>
|
||||
export type UpdateProblemSetStatusRequest = z.infer<typeof updateProblemSetStatusRequestSchema>
|
||||
export type AdminProblemSetProblem = z.infer<typeof adminProblemSetProblemSchema>
|
||||
export type AddProblemToSetRequest = z.infer<typeof addProblemToSetRequestSchema>
|
||||
export type AddProblemsToSetRequest = z.infer<typeof addProblemsToSetRequestSchema>
|
||||
export type AddProblemsToSetResult = z.infer<typeof addProblemsToSetResultSchema>
|
||||
export type ReorderProblemSetProblemsRequest = z.infer<
|
||||
typeof reorderProblemSetProblemsRequestSchema
|
||||
>
|
||||
export type UpdateProblemInSetRequest = z.infer<typeof updateProblemInSetRequestSchema>
|
||||
export type AdminProblemSetBadge = z.infer<typeof adminProblemSetBadgeSchema>
|
||||
export type CreateProblemSetBadgeRequest = z.infer<typeof createProblemSetBadgeRequestSchema>
|
||||
|
||||
@@ -3,23 +3,38 @@ import { z } from "zod"
|
||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||
import { maskedProblemDifficultySchema } from "./problem"
|
||||
|
||||
/**
|
||||
* 我在这份题单里的进度。
|
||||
*
|
||||
* 两个计数口径不同,别混用:
|
||||
* - completedCount / totalCount 只数**必做题**,进度条和「做完了」看它;
|
||||
* - solvedCount 数做对的**全部**题(含选做),「做对 N 道」奖章看它 ——
|
||||
* 和后端 eligibleForBadge 的 problem_count 同一个口径。
|
||||
*/
|
||||
export const problemSetUserProgressSummarySchema = z.object({
|
||||
isJoined: z.boolean(),
|
||||
progressPercentage: z.number(),
|
||||
joinTime: z.string().nullable(),
|
||||
completedCount: z.number().int(),
|
||||
totalCount: z.number().int(),
|
||||
solvedCount: z.number().int(),
|
||||
isCompleted: z.boolean(),
|
||||
completeTime: z.string().nullable(),
|
||||
})
|
||||
|
||||
/** 奖章只有两种条件:全部做完、做对 N 道(「总分达到 N」随分数一起拿掉了,0026 换算过) */
|
||||
export const badgeConditionTypeSchema = z.enum(["all_problems", "problem_count"])
|
||||
|
||||
export const problemSetBadgeSchema = z.object({
|
||||
id: z.number().int(),
|
||||
problemSetId: z.number().int(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
conditionType: z.enum(["all_problems", "problem_count", "score"]),
|
||||
conditionType: badgeConditionTypeSchema,
|
||||
conditionValue: z.number().int(),
|
||||
isEarned: z.boolean().optional(),
|
||||
/** 我拿到它的时刻;没拿到、没登录为 null */
|
||||
earnedTime: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export const problemSetSchema = z.object({
|
||||
@@ -28,26 +43,26 @@ export const problemSetSchema = z.object({
|
||||
description: z.string(),
|
||||
createdBy: sampleUserSchema,
|
||||
createTime: z.string(),
|
||||
lastUpdateTime: z.string().optional(),
|
||||
difficulty: z.enum(["Easy", "Medium", "Hard"]),
|
||||
status: z.enum(["active", "archived", "draft"]),
|
||||
endTime: z.string().nullable(),
|
||||
visible: z.boolean(),
|
||||
/** 布置到哪天(东八区当天结束的时刻);null = 没布置 */
|
||||
assignedUntil: z.string().nullable(),
|
||||
/** 现在是不是在布置期内。服务端按自己的钟算好,前端不用拿本机时间去比 */
|
||||
assigning: z.boolean(),
|
||||
/** 全部题数(含选做) */
|
||||
problemsCount: z.number().int(),
|
||||
completedCount: z.number().int().optional(),
|
||||
/** 必做题数(一道必做都没标时等于全部题数)——「做完」的分母,没加入时也要用 */
|
||||
requiredCount: z.number().int(),
|
||||
/** 加入过的人数 */
|
||||
joinedCount: z.number().int(),
|
||||
userProgress: problemSetUserProgressSummarySchema,
|
||||
badges: z.array(problemSetBadgeSchema).optional(),
|
||||
badges: z.array(problemSetBadgeSchema),
|
||||
})
|
||||
|
||||
export const problemSetListSchema = paginatedSchema(problemSetSchema)
|
||||
|
||||
/**
|
||||
* 题单页的题目卡片只渲染题号、标题、难度,所以只下发这三样(外加题目主键)。
|
||||
*
|
||||
* 以前这里复用 problemListItemSchema,代价是服务端每次都要多 join 一次 user +
|
||||
* user_profile 凑 createdBy、再多查一次标签表凑 tags,而 tags / submissionNumber /
|
||||
* acceptedNumber / createdBy / flowchart / hasAstRules / myStatus 在题单页一个都不渲染
|
||||
* —— myStatus 甚至是写死的 null。
|
||||
* 题单页的题目行只渲染题号、标题,所以只下发这几样(外加题目主键)。
|
||||
* 以前复用 problemListItemSchema,服务端每次要多 join 一次 user / 标签表,
|
||||
* 而那些字段题单页一个都不渲染。
|
||||
*/
|
||||
export const problemSetProblemItemSchema = z.object({
|
||||
id: z.number().int(),
|
||||
@@ -62,42 +77,72 @@ export const problemSetProblemSchema = z.object({
|
||||
problem: problemSetProblemItemSchema,
|
||||
order: z.number().int(),
|
||||
isRequired: z.boolean(),
|
||||
score: z.number().int(),
|
||||
hint: z.string().nullable(),
|
||||
/** 在这份题单里做对了没有(代码通过,或流程图拿到 A / S) */
|
||||
isCompleted: z.boolean(),
|
||||
/** 在这份题单里做对的时刻 */
|
||||
solvedTime: z.string().nullable(),
|
||||
/** 在这份题单里交错了几次(还没做对时显示「交过 n 次」) */
|
||||
wrongCount: z.number().int(),
|
||||
/**
|
||||
* 布置期内、以前做对过、旧代码正被藏着 —— 行上标「以前做对过 · 旧代码先藏着」。
|
||||
* 规则见后端 services/problemset.ts 的 problemSetLockCutoffs
|
||||
*/
|
||||
oldCodeHidden: z.boolean(),
|
||||
})
|
||||
|
||||
export const joinProblemSetRequestSchema = z.object({
|
||||
problemSetId: z.number().int().positive(),
|
||||
})
|
||||
|
||||
export const completedProblemSchema = z.object({
|
||||
id: z.number().int(),
|
||||
_id: z.string(),
|
||||
title: z.string(),
|
||||
/** 老师看某个班在这份题单里的情况:真名 × 每道题 */
|
||||
export const problemSetClassCellSchema = z.object({
|
||||
/** 在题单里做对的时刻 */
|
||||
solvedTime: z.string().nullable(),
|
||||
/** 在题单里交错的次数 */
|
||||
wrongCount: z.number().int(),
|
||||
/** 加入之前就做对过这道题(任何入口) */
|
||||
solvedBefore: z.boolean(),
|
||||
})
|
||||
|
||||
export const problemSetProgressSchema = z.object({
|
||||
id: z.number().int(),
|
||||
export const problemSetClassStudentSchema = z.object({
|
||||
userId: z.number().int(),
|
||||
username: z.string(),
|
||||
realName: z.string().nullable(),
|
||||
/** 没加入为 null */
|
||||
joinTime: z.string().nullable(),
|
||||
/** 做对的必做题数 */
|
||||
completedCount: z.number().int(),
|
||||
cells: z.array(problemSetClassCellSchema),
|
||||
})
|
||||
|
||||
export const problemSetClassViewSchema = z.object({
|
||||
/** 加入过这份题单的学生分属哪些班:三分之一以上的人加入了的班在前(按最近一次加入),零星几个人的在后 */
|
||||
classes: z.array(
|
||||
z.object({ className: z.string(), joined: z.number().int(), size: z.number().int() }),
|
||||
),
|
||||
/** 这次看的是哪个班;谁都没加入时为 null */
|
||||
className: z.string().nullable(),
|
||||
problems: z.array(
|
||||
z.object({
|
||||
id: z.number().int(),
|
||||
_id: z.string(),
|
||||
title: z.string(),
|
||||
isRequired: z.boolean(),
|
||||
/** 这个班在题单里做对的人数 */
|
||||
solved: z.number().int(),
|
||||
}),
|
||||
),
|
||||
/** 必做题数(全是选做时等于全部题数),「做完」的分母 */
|
||||
totalCount: z.number().int(),
|
||||
/** 这个班的全部学生(按学号),没加入的 joinTime 为 null */
|
||||
students: z.array(problemSetClassStudentSchema),
|
||||
})
|
||||
|
||||
/** 某道题为什么藏着旧代码:落在哪些正在布置的题单里 */
|
||||
export const problemSetLockSchema = z.object({
|
||||
problemSetId: z.number().int(),
|
||||
user: sampleUserSchema,
|
||||
joinTime: z.string(),
|
||||
completeTime: z.string().nullable(),
|
||||
isCompleted: z.boolean(),
|
||||
progressPercentage: z.number(),
|
||||
completedProblemsCount: z.number().int(),
|
||||
totalProblemsCount: z.number().int(),
|
||||
totalScore: z.number().int(),
|
||||
completedProblems: z.array(completedProblemSchema),
|
||||
})
|
||||
|
||||
export const problemSetProgressListSchema = paginatedSchema(problemSetProgressSchema).extend({
|
||||
statistics: z.object({
|
||||
total: z.number().int(),
|
||||
completed: z.number().int(),
|
||||
avgProgress: z.number(),
|
||||
}),
|
||||
problems: z.array(completedProblemSchema),
|
||||
title: z.string(),
|
||||
assignedUntil: z.string(),
|
||||
})
|
||||
|
||||
export const userBadgeSchema = z.object({
|
||||
@@ -112,10 +157,11 @@ export type ProblemSet = z.infer<typeof problemSetSchema>
|
||||
export type ProblemSetList = z.infer<typeof problemSetListSchema>
|
||||
export type ProblemSetBadge = z.infer<typeof problemSetBadgeSchema>
|
||||
export type ProblemSetProblem = z.infer<typeof problemSetProblemSchema>
|
||||
export type ProblemSetProgress = z.infer<typeof problemSetProgressSchema>
|
||||
export type ProblemSetProgressList = z.infer<typeof problemSetProgressListSchema>
|
||||
export type ProblemSetClassView = z.infer<typeof problemSetClassViewSchema>
|
||||
export type ProblemSetClassStudent = z.infer<typeof problemSetClassStudentSchema>
|
||||
export type ProblemSetLock = z.infer<typeof problemSetLockSchema>
|
||||
export type UserBadge = z.infer<typeof userBadgeSchema>
|
||||
export type CompletedProblem = z.infer<typeof completedProblemSchema>
|
||||
export type BadgeConditionType = z.infer<typeof badgeConditionTypeSchema>
|
||||
|
||||
export type ProblemSetUserProgressSummary = z.infer<typeof problemSetUserProgressSummarySchema>
|
||||
export type JoinProblemSetRequest = z.infer<typeof joinProblemSetRequestSchema>
|
||||
Reference in new issue
Block a user