feat(后台题目): 题目列表带上做对 / 做过的人数和「学生看得到 / 藏着的」各几道,能一次放出或藏起好几道题;比赛题目也能按题号搜
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
567c745432
commit
952edbf86f
3 files changed
+80
-4
No files matched your search
@@ -50,6 +50,7 @@ import { config } from "../../config"
|
|||||||
import { readFile } from "node:fs/promises"
|
import { readFile } from "node:fs/promises"
|
||||||
import { resolve } from "node:path"
|
import { resolve } from "node:path"
|
||||||
import { getTopReactions } from "../../services/reaction"
|
import { getTopReactions } from "../../services/reaction"
|
||||||
|
import { problemUserCounts } from "../../services/problem-progress"
|
||||||
import { asRecord, queryInteger, sampleUser } from "../helpers"
|
import { asRecord, queryInteger, sampleUser } from "../helpers"
|
||||||
|
|
||||||
export const adminProblemRoutes = new Hono<AppEnv>()
|
export const adminProblemRoutes = new Hono<AppEnv>()
|
||||||
@@ -447,8 +448,13 @@ adminProblemRoutes.get("/problems", requireProblemPermission, async (c) => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 「全部 / 学生看得到 / 藏着的」:三个按钮上的数按除可见性以外的条件数,列表再按它筛
|
||||||
|
const beforeVisible = and(...filters)
|
||||||
|
const visibleQuery = c.req.query("visible")
|
||||||
|
if (visibleQuery === "1") filters.push(eq(schema.problem.visible, true))
|
||||||
|
else if (visibleQuery === "0") filters.push(eq(schema.problem.visible, false))
|
||||||
const where = and(...filters)
|
const where = and(...filters)
|
||||||
const [totalRow, rows] = await Promise.all([
|
const [totalRow, rows, visibleRows] = await Promise.all([
|
||||||
db
|
db
|
||||||
.select({ value: count() })
|
.select({ value: count() })
|
||||||
.from(schema.problem)
|
.from(schema.problem)
|
||||||
@@ -467,13 +473,24 @@ adminProblemRoutes.get("/problems", requireProblemPermission, async (c) => {
|
|||||||
.orderBy(desc(schema.problem.createTime))
|
.orderBy(desc(schema.problem.createTime))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset),
|
.offset(offset),
|
||||||
|
library
|
||||||
|
? []
|
||||||
|
: db
|
||||||
|
.select({ visible: schema.problem.visible, value: count() })
|
||||||
|
.from(schema.problem)
|
||||||
|
.innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id))
|
||||||
|
.where(beforeVisible)
|
||||||
|
.groupBy(schema.problem.visible),
|
||||||
])
|
])
|
||||||
// 只有公开题列表下发最高票评价,比赛题列表不下发 —— 与旧后端一致
|
// 只有公开题列表下发最高票评价,比赛题列表不下发 —— 与旧后端一致
|
||||||
const problemIds = rows.map(({ problem }) => problem.id)
|
const problemIds = rows.map(({ problem }) => problem.id)
|
||||||
const [topReactions, tags] = await Promise.all([
|
const [topReactions, tags, userCounts] = await Promise.all([
|
||||||
getTopReactions(problemIds),
|
getTopReactions(problemIds),
|
||||||
tagNamesFor(problemIds),
|
tagNamesFor(problemIds),
|
||||||
|
problemUserCounts(problemIds),
|
||||||
])
|
])
|
||||||
|
const visibleCount = visibleRows.find((row) => row.visible)?.value ?? 0
|
||||||
|
const hiddenCount = visibleRows.find((row) => !row.visible)?.value ?? 0
|
||||||
return success(c, {
|
return success(c, {
|
||||||
results: rows.map(
|
results: rows.map(
|
||||||
({ problem, user: creator, realName }) =>
|
({ problem, user: creator, realName }) =>
|
||||||
@@ -491,9 +508,14 @@ adminProblemRoutes.get("/problems", requireProblemPermission, async (c) => {
|
|||||||
allowFlowchart: problem.allowFlowchart,
|
allowFlowchart: problem.allowFlowchart,
|
||||||
showFlowchart: problem.showFlowchart,
|
showFlowchart: problem.showFlowchart,
|
||||||
topReaction: topReactions.get(problem.id) ?? null,
|
topReaction: topReactions.get(problem.id) ?? null,
|
||||||
|
solvedUsers: userCounts.get(problem.id)?.solved ?? 0,
|
||||||
|
triedUsers: userCounts.get(problem.id)?.tried ?? 0,
|
||||||
}) satisfies AdminProblemListItem,
|
}) satisfies AdminProblemListItem,
|
||||||
),
|
),
|
||||||
total: totalRow[0]?.value ?? 0,
|
total: totalRow[0]?.value ?? 0,
|
||||||
|
counts: library
|
||||||
|
? undefined
|
||||||
|
: { all: visibleCount + hiddenCount, visible: visibleCount, hidden: hiddenCount },
|
||||||
} satisfies AdminProblemList)
|
} satisfies AdminProblemList)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -637,7 +659,15 @@ adminProblemRoutes.get("/contests/:contestId/problems", requireProblemPermission
|
|||||||
const offset = queryInteger(c.req.query("offset"), 0, { min: 0 })
|
const offset = queryInteger(c.req.query("offset"), 0, { min: 0 })
|
||||||
const filters = [eq(schema.problem.contestId, contestId)]
|
const filters = [eq(schema.problem.contestId, contestId)]
|
||||||
const keyword = c.req.query("keyword")?.trim()
|
const keyword = c.req.query("keyword")?.trim()
|
||||||
if (keyword) filters.push(ilike(schema.problem.title, `%${keyword}%`))
|
// 和公开题列表一样,题号也能搜(搜索框写的是「题号或题目名」)
|
||||||
|
if (keyword) {
|
||||||
|
filters.push(
|
||||||
|
or(
|
||||||
|
ilike(schema.problem.title, `%${keyword}%`),
|
||||||
|
ilike(schema.problem.displayId, `%${keyword}%`),
|
||||||
|
)!,
|
||||||
|
)
|
||||||
|
}
|
||||||
const where = and(...filters)
|
const where = and(...filters)
|
||||||
const [totalRow, rows] = await Promise.all([
|
const [totalRow, rows] = await Promise.all([
|
||||||
db.select({ value: count() }).from(schema.problem).where(where),
|
db.select({ value: count() }).from(schema.problem).where(where),
|
||||||
@@ -673,6 +703,8 @@ adminProblemRoutes.get("/contests/:contestId/problems", requireProblemPermission
|
|||||||
allowFlowchart: problem.allowFlowchart,
|
allowFlowchart: problem.allowFlowchart,
|
||||||
showFlowchart: problem.showFlowchart,
|
showFlowchart: problem.showFlowchart,
|
||||||
topReaction: null,
|
topReaction: null,
|
||||||
|
solvedUsers: 0,
|
||||||
|
triedUsers: 0,
|
||||||
}) satisfies AdminProblemListItem,
|
}) satisfies AdminProblemListItem,
|
||||||
),
|
),
|
||||||
total: totalRow[0]?.value ?? 0,
|
total: totalRow[0]?.value ?? 0,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
parseFlowDoc,
|
parseFlowDoc,
|
||||||
batchProblemTagRequestSchema,
|
batchProblemTagRequestSchema,
|
||||||
|
batchProblemVisibleRequestSchema,
|
||||||
generateFlowchartRequestSchema,
|
generateFlowchartRequestSchema,
|
||||||
renameTagRequestSchema,
|
renameTagRequestSchema,
|
||||||
setTagCategoryRequestSchema,
|
setTagCategoryRequestSchema,
|
||||||
@@ -253,6 +254,28 @@ adminTagRoutes.put("/problems/:id/visibility", requireProblemPermission, async (
|
|||||||
return success(c, { visible: !problem.visible })
|
return success(c, { visible: !problem.visible })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 勾了几道,一次让学生看得到 / 藏起来(后台题目列表「已选 N 道」那一条)。
|
||||||
|
* 和批量加标签同一个范围:只认公开题,不是全部题目管理员就只认自己出的
|
||||||
|
*/
|
||||||
|
adminTagRoutes.post("/problems/batch-visible", requireProblemPermission, async (c) => {
|
||||||
|
const parsed = await parseBody(c, batchProblemVisibleRequestSchema)
|
||||||
|
if (!parsed.success) return parsed.response
|
||||||
|
const user = c.get("user")!
|
||||||
|
const filters = [
|
||||||
|
inArray(schema.problem.id, parsed.data.problemIds),
|
||||||
|
isNull(schema.problem.contestId),
|
||||||
|
]
|
||||||
|
if (!canManageAllProblems(user)) filters.push(eq(schema.problem.createdById, user.id))
|
||||||
|
const updated = await db
|
||||||
|
.update(schema.problem)
|
||||||
|
.set({ visible: parsed.data.visible })
|
||||||
|
.where(and(...filters))
|
||||||
|
.returning({ id: schema.problem.id })
|
||||||
|
if (updated.length === 0) return failure(c, 404, "no-problems", "没有可操作的题目")
|
||||||
|
return success(c, { problemCount: updated.length })
|
||||||
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Python → 流程图
|
// ---------------------------------------------------------------- Python → 流程图
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -529,6 +529,12 @@ export const batchProblemTagRequestSchema = z.object({
|
|||||||
action: z.enum(["add", "remove"]),
|
action: z.enum(["add", "remove"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 勾了几道题,一次让学生看得到 / 藏起来。只认公开题、只认自己能改的 */
|
||||||
|
export const batchProblemVisibleRequestSchema = z.object({
|
||||||
|
problemIds: z.array(z.number().int().positive()).min(1),
|
||||||
|
visible: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
export const batchProblemTagResponseSchema = z.object({
|
export const batchProblemTagResponseSchema = z.object({
|
||||||
problemCount: z.number().int(),
|
problemCount: z.number().int(),
|
||||||
tagCount: z.number().int(),
|
tagCount: z.number().int(),
|
||||||
@@ -699,9 +705,23 @@ export const adminProblemListItemSchema = z.object({
|
|||||||
// 最高票评价 {type, count}。**只有公开题列表下发**,比赛题列表恒传 null ——
|
// 最高票评价 {type, count}。**只有公开题列表下发**,比赛题列表恒传 null ——
|
||||||
// 与旧后端 reaction/services.py:get_top_reactions 的口径一致。
|
// 与旧后端 reaction/services.py:get_top_reactions 的口径一致。
|
||||||
topReaction: z.object({ type: reactionKeySchema, count: z.number().int() }).nullable(),
|
topReaction: z.object({ type: reactionKeySchema, count: z.number().int() }).nullable(),
|
||||||
|
/**
|
||||||
|
* 做对 / 做过的人数(代码提交,和前台题目列表同一口径)。比赛题列表恒为 0 ——
|
||||||
|
* 那个口径只数题库里交的。做过的人数 > 0 就删不掉(有提交),列表里「删除」据此灰掉
|
||||||
|
*/
|
||||||
|
solvedUsers: z.number().int(),
|
||||||
|
triedUsers: z.number().int(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const adminProblemListSchema = paginatedSchema(adminProblemListItemSchema)
|
export const adminProblemListSchema = paginatedSchema(adminProblemListItemSchema).extend({
|
||||||
|
/**
|
||||||
|
* 「全部 / 学生看得到 / 藏着的」三个按钮上的数:除了可见性,筛选条件和列表一样。
|
||||||
|
* 只有公开题列表给,比赛题列表没有这组按钮
|
||||||
|
*/
|
||||||
|
counts: z
|
||||||
|
.object({ all: z.number().int(), visible: z.number().int(), hidden: z.number().int() })
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 题面样例 / 标准答案 / 测试点分值。这三个旧后端都是逐字段校验的
|
* 题面样例 / 标准答案 / 测试点分值。这三个旧后端都是逐字段校验的
|
||||||
@@ -1058,6 +1078,7 @@ export type CreateProblemSetBadgeRequest = z.infer<typeof createProblemSetBadgeR
|
|||||||
export type UpdateProblemSetBadgeRequest = z.infer<typeof updateProblemSetBadgeRequestSchema>
|
export type UpdateProblemSetBadgeRequest = z.infer<typeof updateProblemSetBadgeRequestSchema>
|
||||||
export type RenameTagRequest = z.infer<typeof renameTagRequestSchema>
|
export type RenameTagRequest = z.infer<typeof renameTagRequestSchema>
|
||||||
export type BatchProblemTagRequest = z.infer<typeof batchProblemTagRequestSchema>
|
export type BatchProblemTagRequest = z.infer<typeof batchProblemTagRequestSchema>
|
||||||
|
export type BatchProblemVisibleRequest = z.infer<typeof batchProblemVisibleRequestSchema>
|
||||||
export type GenerateFlowchartRequest = z.infer<typeof generateFlowchartRequestSchema>
|
export type GenerateFlowchartRequest = z.infer<typeof generateFlowchartRequestSchema>
|
||||||
export type GenerateFlowchartResponse = z.infer<typeof generateFlowchartResponseSchema>
|
export type GenerateFlowchartResponse = z.infer<typeof generateFlowchartResponseSchema>
|
||||||
export type UpdateProblemRequest = z.infer<typeof updateProblemRequestSchema>
|
export type UpdateProblemRequest = z.infer<typeof updateProblemRequestSchema>
|
||||||
|
|||||||
Reference in new issue
Block a user