diff --git a/apps/api/src/routes/admin/problem.ts b/apps/api/src/routes/admin/problem.ts index 61e0cde..e595d54 100644 --- a/apps/api/src/routes/admin/problem.ts +++ b/apps/api/src/routes/admin/problem.ts @@ -26,6 +26,7 @@ import { packTestCaseZip, processTestCaseZip, readInfo, readSqlScripts, TestCase import { config } from "../../config" import { readFile } from "node:fs/promises" import { resolve } from "node:path" +import { getTopReactions } from "../../services/reaction" import { objectValue, queryInteger, sampleUser, stringArray } from "../helpers" export const adminProblemRoutes = new Hono() @@ -252,6 +253,8 @@ adminProblemRoutes.get("/problems", requireProblemPermission, async (c) => { .leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id)) .where(where).orderBy(desc(schema.problem.createTime)).limit(limit).offset(offset), ]) + // 只有公开题列表下发最高票评价,比赛题列表不下发 —— 与旧后端一致 + const topReactions = await getTopReactions(rows.map(({ problem }) => problem.id)) return success(c, adminProblemListSchema.parse({ results: await Promise.all(rows.map(async ({ problem, user: creator, realName }) => adminProblemListItemSchema.parse({ @@ -266,7 +269,7 @@ adminProblemRoutes.get("/problems", requireProblemPermission, async (c) => { hasAstRules: problem.astRules !== null, allowFlowchart: problem.allowFlowchart, showFlowchart: problem.showFlowchart, - topReaction: null, + topReaction: topReactions.get(problem.id) ?? null, }))), total: totalRow[0]?.value ?? 0, })) diff --git a/apps/api/src/services/reaction.ts b/apps/api/src/services/reaction.ts new file mode 100644 index 0000000..7b640eb --- /dev/null +++ b/apps/api/src/services/reaction.ts @@ -0,0 +1,55 @@ +import { reactionKeySchema } from "@oj2/contract" +import { count, inArray } from "drizzle-orm" + +import { db, schema } from "../db" + +/** + * 并列票数时按 reactionKeySchema 的定义顺序取靠前的那个,与前端 REACTIONS + * 的顺序一致 —— 对齐旧后端 `reaction/services.py` 的 TYPE_ORDER。 + * 改这里的顺序会让同票题目的展示结果跟着变。 + */ +const TYPE_ORDER = new Map( + reactionKeySchema.options.map((key, index) => [key, index]), +) + +export interface TopReaction { + type: string + count: number +} + +/** + * 批量取每道题得票最高的评价,返回 `Map`, + * **只包含有评价的题目**(没有的题不进 Map,调用方自己兜 null)。 + * + * 对齐旧后端 `reaction/services.py:get_top_reactions`。 + */ +export async function getTopReactions(problemIds: number[]) { + const top = new Map() + if (problemIds.length === 0) return top + + const rows = await db + .select({ + problemId: schema.reaction.problemId, + type: schema.reaction.type, + value: count(), + }) + .from(schema.reaction) + .where(inArray(schema.reaction.problemId, problemIds)) + .groupBy(schema.reaction.problemId, schema.reaction.type) + + for (const row of rows) { + const order = TYPE_ORDER.get(row.type) + // 库里可能残留前端已经下掉的旧类型。跳过而不是当成并列最优 —— + // 否则一个已经不展示的类型会把真正的最高票挤掉。 + if (order === undefined) continue + const current = top.get(row.problemId) + if ( + !current || + row.value > current.count || + (row.value === current.count && order < TYPE_ORDER.get(current.type)!) + ) { + top.set(row.problemId, { type: row.type, count: row.value }) + } + } + return top +} diff --git a/apps/web/src/admin/problem/list.vue b/apps/web/src/admin/problem/list.vue index bba2fd0..b162d0d 100644 --- a/apps/web/src/admin/problem/list.vue +++ b/apps/web/src/admin/problem/list.vue @@ -137,7 +137,7 @@ const baseColumns: DataTableColumn[] = [ }, { title: "反馈", - key: "top_reaction", + key: "topReaction", width: 60, render: (row) => { const top = row.topReaction