班级号 3~4 位并存,按用户名前缀/包含匹配时 ks251 会捞到 ks2510~ks2519。 新增 classPrefixCondition:ks + 3 位以上数字按具体班级匹配(后面不能再跟数字), 1~2 位仍是年级前缀。后台首页的用户排名、随机抽签,以及题单的学生进度筛选 (只填数字时补上 ks)都改用它。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
41cd141769
commit
23844ae301
4 files changed
+46
-9
No files matched your search
@@ -34,7 +34,7 @@ import { revokeUserSessions } from "../../auth/session"
|
|||||||
import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
|
import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
|
||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, parseBody, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { queryInteger, sampleUser } from "../helpers"
|
import { classPrefixCondition, queryInteger, sampleUser } from "../helpers"
|
||||||
|
|
||||||
export const adminAccountRoutes = new Hono<AppEnv>()
|
export const adminAccountRoutes = new Hono<AppEnv>()
|
||||||
|
|
||||||
@@ -130,7 +130,11 @@ adminAccountRoutes.get("/rankings/users", requireSuperAdmin, async (c) => {
|
|||||||
const where = and(
|
const where = and(
|
||||||
inArray(schema.user.adminType, [...STUDENT_ROLES]),
|
inArray(schema.user.adminType, [...STUDENT_ROLES]),
|
||||||
eq(schema.user.isDisabled, false),
|
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([
|
const [totalRows, rows] = await Promise.all([
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { failure, parseBody, success } from "../../http"
|
|||||||
import { sniffImageExtension } from "../../services/image"
|
import { sniffImageExtension } from "../../services/image"
|
||||||
import { getWebsiteOptions } from "../../services/options"
|
import { getWebsiteOptions } from "../../services/options"
|
||||||
import { dayStart } from "../../time"
|
import { dayStart } from "../../time"
|
||||||
import { queryInteger } from "../helpers"
|
import { classPrefixCondition, queryInteger } from "../helpers"
|
||||||
|
|
||||||
export const adminConfRoutes = new Hono<AppEnv>()
|
export const adminConfRoutes = new Hono<AppEnv>()
|
||||||
|
|
||||||
@@ -253,14 +253,17 @@ adminConfRoutes.get("/dashboard", requireSuperAdmin, async (c) => {
|
|||||||
|
|
||||||
adminConfRoutes.get("/random-usernames", requireSuperAdmin, async (c) => {
|
adminConfRoutes.get("/random-usernames", requireSuperAdmin, async (c) => {
|
||||||
// 传的是**班级前缀**(形如 ks251),不是班级号 —— 前端输入框写的就是「班级前缀」,
|
// 传的是**班级前缀**(形如 ks251),不是班级号 —— 前端输入框写的就是「班级前缀」,
|
||||||
// 拿到结果后按这个前缀 split 取姓名。这里按前缀匹配,与旧 istartswith 一致,
|
// 拿到结果后按这个前缀 split 取姓名。班级前缀走 classPrefixCondition(ks251 不带上
|
||||||
// 不额外按 className 过滤:那会改变旧行为,而这个功能就是随机点名,宁可宽松
|
// ks2511);别的形状的输入照旧 istartswith,与旧接口一致
|
||||||
const classroom = c.req.query("classroom")?.trim()
|
const classroom = c.req.query("classroom")?.trim()
|
||||||
if (!classroom) return failure(c, 400, "invalid-request", "需要班级号")
|
if (!classroom) return failure(c, 400, "invalid-request", "需要班级号")
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ username: schema.user.username })
|
.select({ username: schema.user.username })
|
||||||
.from(schema.user)
|
.from(schema.user)
|
||||||
.where(ilike(schema.user.username, `${classroom}%`))
|
.where(
|
||||||
|
classPrefixCondition(classroom) ??
|
||||||
|
ilike(schema.user.username, `${classroom}%`),
|
||||||
|
)
|
||||||
.orderBy(sql`random()`)
|
.orderBy(sql`random()`)
|
||||||
.limit(10)
|
.limit(10)
|
||||||
return success(
|
return success(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ADMIN_ROLES, TEACHER_ROLES, type SampleUser } from "@oj2/contract"
|
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 type { AuthUser } from "../auth/session"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
@@ -44,6 +44,22 @@ export function stripClassPrefix(
|
|||||||
return username.startsWith(prefix) ? username.slice(prefix.length) : username
|
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` 这些)。
|
* 拿 query 里的筛选值去比对 `$type` 收窄过的列(`submission.result`、`problem.difficulty` 这些)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ import {
|
|||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure, parseBody, success } from "../http"
|
import { failure, parseBody, success } from "../http"
|
||||||
import { computeProgress } from "../services/problemset"
|
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<AppEnv>()
|
export const problemsetRoutes = new Hono<AppEnv>()
|
||||||
|
|
||||||
@@ -499,7 +505,15 @@ problemsetRoutes.get(
|
|||||||
const className = c.req.query("className")?.trim()
|
const className = c.req.query("className")?.trim()
|
||||||
const completion = c.req.query("completionStatus")?.trim()
|
const completion = c.req.query("completionStatus")?.trim()
|
||||||
const filters = [eq(schema.problemsetProgress.problemsetId, id)]
|
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")
|
if (completion === "completed")
|
||||||
filters.push(eq(schema.problemsetProgress.isCompleted, true))
|
filters.push(eq(schema.problemsetProgress.isCompleted, true))
|
||||||
else if (completion === "in_progress")
|
else if (completion === "in_progress")
|
||||||
|
|||||||
Reference in new issue
Block a user