feat(账号): 新建的号不再加 ks + 班号前缀,班级单独选;学生改不了真名了
- 批量建号先选班再贴名单,用户名就是名字;被别的班或注册的号占了自动加数字(xuyue2)。 查重改成查「这个班里有没有同名的」,老号按剥掉 ks 前缀的名字比 - 后台编辑、新建用户能直接选班级;以前班级只能从用户名的 ks 前缀推,改不了 - 登录点名字时名单接口一并给出用户名,新号老号都登得上;本机记的班级兼容旧的 ks257 写法 - 筛选、抽人、「看看他们的写法」都按班级认人,不再拼 ks 前缀(新号拼出来永远圈不进去) - 接口 PUT /me/profile 不再收真名:界面早没入口,但早几届有 44 人把真名改成了外号 - 0042:两个被推成非班号班级的老号(xrm、王仁杰)改成没填班级 - 老账号不改名,只认 ks 前缀的地方收在契约 username.ts 一处 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48590eee60
commit
221d264701
25 files changed
+4234
-156
No files matched your search
@@ -13,8 +13,11 @@ export const registerRequestSchema = z.object({
|
||||
password: z.string().min(6).max(20),
|
||||
})
|
||||
|
||||
/**
|
||||
* 学生自己能改的资料。**真名不在这里**:它只在后台改(PUT /admin/users/:id)。原来这里收 realName,
|
||||
* 界面上早就没入口了,但接口还认 —— 早几届有 44 人把真名改成了外号(「超级无敌暴龙战士」)
|
||||
*/
|
||||
export const updateProfileRequestSchema = z.object({
|
||||
realName: z.string().max(32).nullable().optional(),
|
||||
avatar: z.string().max(256).optional(),
|
||||
/** 排名上要挂出来(`MOOD_MAX`),太长了榜上放不下 */
|
||||
mood: z.string().trim().max(MOOD_MAX, `个性签名最多 ${MOOD_MAX} 个字`).nullable().optional(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { achievementRaritySchema } from './achievement'
|
||||
import { rankProfileSchema } from './account'
|
||||
import { paginatedSchema, sampleUserSchema } from './common'
|
||||
import { reactionKeySchema } from './content'
|
||||
import { classNameSchema } from './username'
|
||||
import { badgeConditionTypeSchema } from './problemset'
|
||||
import { problemLanguageSchema, runnableLanguageSchema } from './language'
|
||||
import {
|
||||
@@ -285,12 +286,19 @@ export const adminUserListSchema = paginatedSchema(adminUserSchema).extend({
|
||||
neverLoggedIn: z.number().int(),
|
||||
})
|
||||
|
||||
/** 批量建号之前查重:这几个用户名哪些已经有了 */
|
||||
/** 批量建号之前查重:给一个班和一串名字 */
|
||||
export const checkUsernamesRequestSchema = z.object({
|
||||
usernames: z.array(z.string().trim().min(1).max(32)).min(1).max(500),
|
||||
className: classNameSchema,
|
||||
names: z.array(z.string().trim().min(1).max(32)).min(1).max(500),
|
||||
})
|
||||
export const checkUsernamesResponseSchema = z.object({
|
||||
existing: z.array(z.object({ username: z.string(), createTime: z.string().nullable() })),
|
||||
/** 这个班里已经有的同名账号(老号按剥掉 ks 前缀后的名字比)—— 多半是同一个人,建之前要先处理 */
|
||||
existing: z.array(z.object({ name: z.string(), username: z.string(), createTime: z.string().nullable() })),
|
||||
/**
|
||||
* 每个名字会建成的用户名,和 `names` 一一对应。名字没被别人占就是名字本身;占了(别的班同名、
|
||||
* 注册的号)往后加数字,`杨涵宇2`。用户名界面上看不到,只用来登录和查重
|
||||
*/
|
||||
usernames: z.array(z.string()),
|
||||
})
|
||||
|
||||
export const updateUserRequestSchema = z.object({
|
||||
@@ -299,14 +307,19 @@ export const updateUserRequestSchema = z.object({
|
||||
adminType: adminTypeSchema,
|
||||
problemPermission: problemPermissionSchema,
|
||||
realName: z.string().max(32).nullable().default(null),
|
||||
/** 班级。不给就不动(老页面不发这一项);null = 没填班级(老师、管理员) */
|
||||
className: classNameSchema.nullable().optional(),
|
||||
isDisabled: z.boolean(),
|
||||
// 空串表示不改密码,与旧 EditUserSerializer 的 allow_blank 一致
|
||||
password: z.string().max(128).default(''),
|
||||
})
|
||||
|
||||
/** 导入用户:每行 [用户名, 密码, 邮箱, 真名],与前端粘贴的 Excel 列序一致 */
|
||||
/**
|
||||
* 导入用户:每行 [用户名, 密码, 邮箱, 真名, 班级]。班级不给的老写法退回从 `ks` 前缀推(`legacyClassOf`),
|
||||
* 开着旧页面、手粘老名单的照样能建
|
||||
*/
|
||||
export const importUsersRequestSchema = z.object({
|
||||
users: z.array(z.tuple([z.string().trim().min(1).max(32), z.string().min(1), z.string(), z.string()])).min(1),
|
||||
users: z.array(z.tuple([z.string().trim().min(1).max(32), z.string().min(1), z.string(), z.string(), classNameSchema.nullable().optional()])).min(1),
|
||||
})
|
||||
|
||||
export const deleteUsersRequestSchema = z.object({
|
||||
|
||||
@@ -19,3 +19,4 @@ export * from './roles'
|
||||
export * from './site'
|
||||
export * from './submission'
|
||||
export * from './time'
|
||||
export * from './username'
|
||||
@@ -0,0 +1,41 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* 班级号:3~4 位数字(`232` = 23 级 2 班,`2311` = 23 级 11 班)。存在 `user.class_name`,
|
||||
* 全站认班级只认这一列。
|
||||
*/
|
||||
export const CLASS_NAME_MIN_DIGITS = 3
|
||||
export const CLASS_NAME_MAX_DIGITS = 4
|
||||
export const CLASS_NAME_RE = new RegExp(`^\\d{${CLASS_NAME_MIN_DIGITS},${CLASS_NAME_MAX_DIGITS}}$`)
|
||||
export const classNameSchema = z.string().trim().regex(CLASS_NAME_RE, `班级号要是 ${CLASS_NAME_MIN_DIGITS}~${CLASS_NAME_MAX_DIGITS} 位数字`)
|
||||
|
||||
/**
|
||||
* 老账号的用户名带着班级:`ks232张三`(ks 是「考试」的拼音缩写)。2026-11 起新建的号不再带,
|
||||
* 用户名就是名字;老账号不改名(学生要换登录名、历史提交里存着当时的用户名),只是界面上不再露出这段。
|
||||
*
|
||||
* 这条正则是**唯一**还认 `ks` 的地方:显示名、登录名单、建号查重都从这里剥。新号是中文名,不会误中。
|
||||
*/
|
||||
export const LEGACY_CLASS_PREFIX_RE = new RegExp(`^ks\\d{${CLASS_NAME_MIN_DIGITS},${CLASS_NAME_MAX_DIGITS}}`, 'i')
|
||||
|
||||
/** 界面上显示的名字:老号剥掉 `ks232`,新号原样。剥完是空串的(只有前缀的怪号)退回原名 */
|
||||
export function displayName(username: string) {
|
||||
return username.replace(LEGACY_CLASS_PREFIX_RE, '') || username
|
||||
}
|
||||
|
||||
/**
|
||||
* 老号用户名里的班号(`ks232张三` → `232`),只给**没明确给班级**的旧调用方兜底(老页面、手粘的 ks 名单)。
|
||||
* 位数不对返回 undefined,不猜 —— `ks251001` 猜成 2510 班比报错更糟
|
||||
*/
|
||||
export function legacyClassOf(username: string): string | null | undefined {
|
||||
const matched = /^ks(\d+)/i.exec(username)
|
||||
if (!matched) return null
|
||||
const digits = matched[1]!
|
||||
return CLASS_NAME_RE.test(digits) ? digits : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录框点名字用的名单(`GET /classes/:className/usernames`):`name` 是点的那个名字,
|
||||
* `username` 是拿去登录的。原来只回名字、前端拼 `ks` + 班号 + 名字去登录,新号不带前缀就拼错了
|
||||
*/
|
||||
export const classMemberSchema = z.object({ name: z.string(), username: z.string() })
|
||||
export type ClassMember = z.infer<typeof classMemberSchema>
|
||||
Reference in new issue
Block a user