Files
OJ2/packages/contract/src/problemset.ts
yuetsh d9e6a2a3f0 perf(题单): 学生端题目列表少两次 join 一次查询,顺带把并列的 order 排稳
## 排序

学生端 /problem-sets/:id/problems 只按 order 排,没有 tiebreaker,而卡片是按数组
下标编号的(#1 #2 #3)。order 并列时 Postgres 不保证次序,题单 8(3 道题 order 都是
10)、题单 11(2 道并列 4)、题单 14(3 道并列 0)实际就有并列,于是「第 3 题」指
哪道题每次刷新都可能变。后台那条列表一直是 order + id 排的,两边本来就不一致。

补上 asc(id)。教师进度视图里那份题目清单(同一路由文件 :398)有同样的问题,一并补。

实跑:四道题、三道 order 并列,连打 5 次次序完全一致。

## 载荷

这个接口原来复用 problemListItemSchema,为此要多 join user + user_profile 凑
createdBy、再多查一次标签表凑 tags —— 而题单卡片只渲染题号、标题、难度、分数和
完成标记。tags / submissionNumber / acceptedNumber / createdBy / contestId /
allowFlowchart / showFlowchart / hasAstRules 一个都不用,myStatus 甚至写死是 null。

而且 select 的是 schema.problem 整行,题面、样例、答案、ast_rules、flowchart_data、
sql_display 全都白拉回来一遍。

改成 problemSetProblemItemSchema(id / _id / title / difficulty 四个字段),查询相应
收窄:两次 join 去掉,标签那次查询整条去掉,行查询只取四列。每次请求从 4 条查询降到
3 条,其中最大的那条不再拖整行题面。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
2026-08-31 08:47:57 -06:00

129 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod"
import { paginatedSchema, sampleUserSchema } from "./common"
import { maskedProblemDifficultySchema } from "./problem"
export const problemSetUserProgressSummarySchema = z.object({
isJoined: z.boolean(),
progressPercentage: z.number(),
completedCount: z.number().int(),
totalCount: z.number().int(),
isCompleted: z.boolean(),
})
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"]),
conditionValue: z.number().int(),
isEarned: z.boolean().optional(),
})
export const problemSetSchema = z.object({
id: z.number().int(),
title: z.string(),
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(),
problemsCount: z.number().int(),
completedCount: z.number().int().optional(),
userProgress: problemSetUserProgressSummarySchema,
badges: z.array(problemSetBadgeSchema).optional(),
})
export const problemSetListSchema = paginatedSchema(problemSetSchema)
/**
* 题单页的题目卡片只渲染题号、标题、难度,所以只下发这三样(外加题目主键)。
*
* 以前这里复用 problemListItemSchema代价是服务端每次都要多 join 一次 user +
* user_profile 凑 createdBy、再多查一次标签表凑 tags而 tags / submissionNumber /
* acceptedNumber / createdBy / flowchart / hasAstRules / myStatus 在题单页一个都不渲染
* —— myStatus 甚至是写死的 null。
*/
export const problemSetProblemItemSchema = z.object({
id: z.number().int(),
_id: z.string(),
title: z.string(),
difficulty: maskedProblemDifficultySchema,
})
export const problemSetProblemSchema = z.object({
id: z.number().int(),
problemsetId: z.number().int(),
problem: problemSetProblemItemSchema,
order: z.number().int(),
isRequired: z.boolean(),
score: z.number().int(),
hint: z.string().nullable(),
isCompleted: z.boolean(),
})
export const joinProblemSetRequestSchema = z.object({
problemSetId: z.number().int().positive(),
})
export const updateProblemSetProgressRequestSchema = z.object({
problemSetId: z.number().int().positive(),
problemId: z.number().int().positive(),
submissionId: z.string().min(1),
})
export const completedProblemSchema = z.object({
id: z.number().int(),
_id: z.string(),
title: z.string(),
})
export const problemSetProgressSchema = z.object({
id: z.number().int(),
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),
})
export const userBadgeSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
badge: problemSetBadgeSchema,
earnedTime: z.string(),
problemset: z.object({ id: z.number().int(), title: z.string() }),
})
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 UserBadge = z.infer<typeof userBadgeSchema>
export type CompletedProblem = z.infer<typeof completedProblemSchema>
export type ProblemSetUserProgressSummary = z.infer<typeof problemSetUserProgressSummarySchema>
export type JoinProblemSetRequest = z.infer<typeof joinProblemSetRequestSchema>
export type UpdateProblemSetProgressRequest = z.infer<typeof updateProblemSetProgressRequestSchema>