From 23844ae301405516837d8e0e7e0a1e7e33a6a6db Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sun, 27 Sep 2026 19:34:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E7=8F=AD=E7=BA=A7=E7=AD=9B=E9=80=89):=20?= =?UTF-8?q?=E8=BE=93=E5=85=A5=20ks251=20=E4=BC=9A=E6=8A=8A=202511=20?= =?UTF-8?q?=E7=8F=AD=E7=9A=84=E5=AD=A6=E7=94=9F=E4=B9=9F=E7=AE=97=E8=BF=9B?= =?UTF-8?q?=E6=9D=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 班级号 3~4 位并存,按用户名前缀/包含匹配时 ks251 会捞到 ks2510~ks2519。 新增 classPrefixCondition:ks + 3 位以上数字按具体班级匹配(后面不能再跟数字), 1~2 位仍是年级前缀。后台首页的用户排名、随机抽签,以及题单的学生进度筛选 (只填数字时补上 ks)都改用它。 Co-Authored-By: Claude Opus 5.5 --- apps/api/src/routes/admin/account.ts | 8 ++++++-- apps/api/src/routes/admin/conf.ts | 11 +++++++---- apps/api/src/routes/helpers.ts | 18 +++++++++++++++++- apps/api/src/routes/problemset.ts | 18 ++++++++++++++++-- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/apps/api/src/routes/admin/account.ts b/apps/api/src/routes/admin/account.ts index 49adbe9..d3f5c81 100644 --- a/apps/api/src/routes/admin/account.ts +++ b/apps/api/src/routes/admin/account.ts @@ -34,7 +34,7 @@ import { revokeUserSessions } from "../../auth/session" import { requireSuperAdmin, type AppEnv } from "../../auth/middleware" import { db, schema } from "../../db" import { failure, parseBody, success } from "../../http" -import { queryInteger, sampleUser } from "../helpers" +import { classPrefixCondition, queryInteger, sampleUser } from "../helpers" export const adminAccountRoutes = new Hono() @@ -130,7 +130,11 @@ adminAccountRoutes.get("/rankings/users", requireSuperAdmin, async (c) => { const where = and( inArray(schema.user.adminType, [...STUDENT_ROLES]), eq(schema.user.isDisabled, false), - keyword ? ilike(schema.user.username, `%${keyword}%`) : undefined, + // 输入班级前缀(ks251)按班级匹配,否则照旧按用户名包含 + keyword + ? (classPrefixCondition(keyword) ?? + ilike(schema.user.username, `%${keyword}%`)) + : undefined, ) const [totalRows, rows] = await Promise.all([ diff --git a/apps/api/src/routes/admin/conf.ts b/apps/api/src/routes/admin/conf.ts index 355fa05..8e59ce6 100644 --- a/apps/api/src/routes/admin/conf.ts +++ b/apps/api/src/routes/admin/conf.ts @@ -26,7 +26,7 @@ import { failure, parseBody, success } from "../../http" import { sniffImageExtension } from "../../services/image" import { getWebsiteOptions } from "../../services/options" import { dayStart } from "../../time" -import { queryInteger } from "../helpers" +import { classPrefixCondition, queryInteger } from "../helpers" export const adminConfRoutes = new Hono() @@ -253,14 +253,17 @@ adminConfRoutes.get("/dashboard", requireSuperAdmin, async (c) => { adminConfRoutes.get("/random-usernames", requireSuperAdmin, async (c) => { // 传的是**班级前缀**(形如 ks251),不是班级号 —— 前端输入框写的就是「班级前缀」, - // 拿到结果后按这个前缀 split 取姓名。这里按前缀匹配,与旧 istartswith 一致, - // 不额外按 className 过滤:那会改变旧行为,而这个功能就是随机点名,宁可宽松 + // 拿到结果后按这个前缀 split 取姓名。班级前缀走 classPrefixCondition(ks251 不带上 + // ks2511);别的形状的输入照旧 istartswith,与旧接口一致 const classroom = c.req.query("classroom")?.trim() if (!classroom) return failure(c, 400, "invalid-request", "需要班级号") const rows = await db .select({ username: schema.user.username }) .from(schema.user) - .where(ilike(schema.user.username, `${classroom}%`)) + .where( + classPrefixCondition(classroom) ?? + ilike(schema.user.username, `${classroom}%`), + ) .orderBy(sql`random()`) .limit(10) return success( diff --git a/apps/api/src/routes/helpers.ts b/apps/api/src/routes/helpers.ts index 0ab5a20..a6c65f5 100644 --- a/apps/api/src/routes/helpers.ts +++ b/apps/api/src/routes/helpers.ts @@ -1,6 +1,6 @@ import { ADMIN_ROLES, TEACHER_ROLES, type SampleUser } from "@oj2/contract" -import { and, count, eq, ilike, notInArray } from "drizzle-orm" +import { and, count, eq, ilike, notInArray, sql } from "drizzle-orm" import type { AuthUser } from "../auth/session" import { db, schema } from "../db" @@ -44,6 +44,22 @@ export function stripClassPrefix( return username.startsWith(prefix) ? username.slice(prefix.length) : username } +/** + * 用户名按「班级前缀」匹配:`ks251` 只匹配 251 班,**不能**把 `ks2511张三` 也捞进来。 + * + * 班级号 3~4 位并存(见 admin/account.ts 的 classNameOf),裸 `ilike 'ks251%'` 会把 + * 2510~2519 班全算进 251 班。所以输入是 `ks` + 3 位以上数字时,要求紧跟着的不是数字; + * 1~2 位(`ks25`)是年级前缀,照旧宽松匹配。输入不是 `ks+数字` 形状时返回 undefined, + * 由调用方决定退回什么匹配方式。 + */ +export function classPrefixCondition(input: string) { + const matched = /^ks(\d+)$/i.exec(input) + if (!matched) return undefined + const prefix = ilike(schema.user.username, `${input}%`) + if (matched[1]!.length < 3) return prefix + return and(prefix, sql`${schema.user.username} !~* ${`^${input}[0-9]`}`) +} + /** * 拿 query 里的筛选值去比对 `$type` 收窄过的列(`submission.result`、`problem.difficulty` 这些)。 * diff --git a/apps/api/src/routes/problemset.ts b/apps/api/src/routes/problemset.ts index c1c617f..b1644dc 100644 --- a/apps/api/src/routes/problemset.ts +++ b/apps/api/src/routes/problemset.ts @@ -33,7 +33,13 @@ import { import { db, schema } from "../db" import { failure, parseBody, success } from "../http" import { computeProgress } from "../services/problemset" -import { asFilterValue, asRecord, queryInteger, sampleUser } from "./helpers" +import { + asFilterValue, + asRecord, + classPrefixCondition, + queryInteger, + sampleUser, +} from "./helpers" export const problemsetRoutes = new Hono() @@ -499,7 +505,15 @@ problemsetRoutes.get( const className = c.req.query("className")?.trim() const completion = c.req.query("completionStatus")?.trim() const filters = [eq(schema.problemsetProgress.problemsetId, id)] - if (className) filters.push(ilike(schema.user.username, `%${className}%`)) + // 只填数字(251)当班级号补上 ks;班级按 classPrefixCondition 匹配, + // 否则 251 会把 2511 班也算进来。别的输入照旧按用户名包含 + if (className) { + const classPrefix = /^\d+$/.test(className) ? `ks${className}` : className + filters.push( + classPrefixCondition(classPrefix) ?? + ilike(schema.user.username, `%${className}%`), + ) + } if (completion === "completed") filters.push(eq(schema.problemsetProgress.isCompleted, true)) else if (completion === "in_progress")