Files
BYW-VoiceHub/server/api/admin/users/[id].put.ts
T
Henry_Du dfb4891022 fix(users): 只校验"真的改过"的姓名/账号名,修回"管理员无法编辑其他用户"
站长反馈:管理员无法编辑其他用户信息了。

根因:「姓名/账号名必须纯中文」的校验是**每次编辑都重新做一遍**的,
所以历史账号(早期用拼音/英文建的、名字里带「·」或空格的)连"改个年级"
都会被这条规则拦死 —— 管理员等于没法维护这些账号。admin 账号之前也是同一个毛病。

改成与服务端同一口径的"只校验改动过的字段":
- 没改动 → 不校验(历史数据原样保留,不强迫改名);
- 改了 → 必须满足规则(纯中文;admin 账号的姓名例外);
- 主动把中文名改成拼音/带空格 → 仍然拦。

初始管理员(ID=1)的规则不变:登录名必须保持 admin、角色锁死、密码不在这里改。

(对应的小程序端同样改成"只校验改动字段":编辑时记下原始姓名/账号名,
提交时比对;新增时两个字段都校验。)
2026-09-30 22:02:14 +08:00

257 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createError, defineEventHandler, getRouterParam, readBody } from 'h3'
import { isValidPersonName, isValidUsername, NAME_RULE_MESSAGE, USERNAME_RULE_MESSAGE } from '~~/server/utils/username'
import { db } from '~/drizzle/db'
import { users, userStatusLogs } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
import { updateUserPassword } from '~~/server/services/userService'
import { getRoleLevel, canChangeRoleOf, canResetPasswordFor } from '~~/server/utils/permissions'
export default defineEventHandler(async (event) => {
try {
// 检查认证和权限
const user = event.context.user
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
throw createError({
statusCode: 403,
statusMessage: '没有权限访问'
})
}
const userId = getRouterParam(event, 'id')
const body = await readBody(event)
const { name, username, password, role, grade, class: userClass, status } = body
// 验证必填字段
if (!name || !username) {
throw createError({
statusCode: 400,
statusMessage: '姓名和用户名为必填项'
})
}
// 检查用户是否存在
const existingUser = await db
.select()
.from(users)
.where(eq(users.id, parseInt(userId)))
.limit(1)
if (existingUser.length === 0) {
throw createError({
statusCode: 404,
statusMessage: '用户不存在'
})
}
const targetUser = existingUser[0]
const currentLevel = getRoleLevel(user.role)
const targetLevel = getRoleLevel(targetUser.role)
/*
* 姓名 / 账号名的中文规则:**只校验"这次真的改了"的字段**。
*
* 站长反馈「管理员无法编辑其他用户信息了」——根因就在这里:
* 原来(以及我上一版照搬的写法)是"每次编辑都重新校验姓名和账号名",
* 于是**历史账号**(早期用拼音/英文建的、或者名字里带「·」「空格」的)
* 连"改个年级"都会撞「必须纯中文」而被拦死,管理员等于没法维护这些账号。
* 现在的口径:
* · 没改动 → 不校验(历史数据**原样保留**,不强迫改名);
* · 改了 → 必须满足规则(纯中文;admin 账号例外)。
*/
const nextUsername = String(username || '').trim()
const nextName = String(name || '').trim()
const usernameChanged = nextUsername !== String(targetUser.username || '')
const nameChanged = nextName !== String(targetUser.name || '')
if (usernameChanged && !isValidUsername(nextUsername)) {
throw createError({
statusCode: 400,
statusMessage: USERNAME_RULE_MESSAGE
})
}
if (nameChanged && !isValidPersonName(nextName, nextUsername)) {
throw createError({
statusCode: 400,
statusMessage: NAME_RULE_MESSAGE
})
}
/*
* 系统初始超级管理员(ID=1,登录名 admin)的处理 —— 第 90 轮按站长要求放宽。
*
* 原来这里是"一律禁止修改"(403 无法修改系统初始超级管理员),
* 结果是连它的**显示名都改不了**(站长想把界面上的 admin 改成自己的名字)。
* 现在按"哪些能改、哪些必须锁死"拆开:
* · 允许:姓名(显示名)、年级、班级、状态;
* · 锁死:**登录名必须保持 admin** —— 那是 admin/admin123 的应急登录通道,改了就没了;
* · 锁死:角色不能动(不能把自己降权,否则没人能管站点);
* · 锁死:密码不在这里改(避免被其它管理员顺手重置)。
*/
if (targetUser.id === 1) {
// 登录名必须还是 admin(usernameChanged 已在上面的中文规则里判过"有没有改")
if (usernameChanged) {
throw createError({
statusCode: 400,
statusMessage: '初始超级管理员的登录名必须保持 admin(应急登录通道,改了就无法用它登录)'
})
}
if (role && role !== targetUser.role) {
throw createError({
statusCode: 403,
statusMessage: '不能修改初始超级管理员的角色'
})
}
if (password) {
throw createError({
statusCode: 403,
statusMessage: '不能在用户管理中重置初始超级管理员的密码(请用它登录后在「设置」里自行修改)'
})
}
}
// 2. 禁止对自身进行任何用户管理操作(包括角色、资料、状态、密码等)
if (String(userId) === String(user.id)) {
throw createError({
statusCode: 400,
statusMessage: '禁止在用户管理中修改自己的账户'
})
}
// 3. 非超级管理员必须严格高于目标角色级别
if (user.role !== 'SUPER_ADMIN' && currentLevel <= targetLevel) {
throw createError({
statusCode: 403,
statusMessage: `权限不足:${user.role} 无法操作同级或更高角色的用户`
})
}
// 检查用户名是否被其他用户使用
if (username !== targetUser.username) {
const duplicateUser = await db
.select()
.from(users)
.where(eq(users.username, username))
.limit(1)
if (duplicateUser.length > 0) {
throw createError({
statusCode: 400,
statusMessage: '用户名已被其他用户使用'
})
}
}
// 角色权限控制(统一使用 canChangeRoleOf 语义化检查)
let validRole = targetUser.role
if (role && role !== targetUser.role) {
if (!canChangeRoleOf(user, targetUser, role)) {
throw createError({
statusCode: 403,
statusMessage: user.role === 'SUPER_ADMIN'
? '超级管理员不能将目标用户提升为 SUPER_ADMIN'
: `权限不足:${user.role} 无法将用户角色更改为 ${role}`
})
}
validRole = role
}
// 如果请求中包含密码更新,校验密码重置权限
if (password && !canResetPasswordFor(user, targetUser)) {
throw createError({
statusCode: 403,
statusMessage: `权限不足:${user.role} 无法重置目标用户的密码`
})
}
// 验证status字段的有效性
if (status && !['active', 'withdrawn', 'graduate'].includes(status)) {
throw createError({
statusCode: 400,
statusMessage: '用户状态只能是 active, withdrawn 或 graduate'
})
}
// 准备更新数据
const updateData = {
name,
username,
role: validRole,
grade,
class: userClass
}
// 如果提供了status,则更新状态相关字段
if (status && status !== existingUser[0].status) {
updateData.status = status
updateData.statusChangedAt = new Date()
updateData.statusChangedBy = user.id
}
// 如果提供了密码,则使用统一服务更新密码
if (password) {
await updateUserPassword(parseInt(userId), password, true)
}
// 更新用户其他信息
const updatedUser = await db
.update(users)
.set(updateData)
.where(eq(users.id, parseInt(userId)))
.returning({
id: users.id,
name: users.name,
username: users.username,
role: users.role,
grade: users.grade,
class: users.class,
status: users.status,
statusChangedAt: users.statusChangedAt,
statusChangedBy: users.statusChangedBy,
lastLogin: users.lastLogin,
lastLoginIp: users.lastLoginIp,
passwordChangedAt: users.passwordChangedAt,
createdAt: users.createdAt,
updatedAt: users.updatedAt
})
// 如果状态发生变更,记录到状态变更日志
if (status && status !== existingUser[0].status) {
await db.insert(userStatusLogs).values({
userId: parseInt(userId),
oldStatus: existingUser[0].status,
newStatus: status,
reason: `管理员${user.name || user.username}修改用户状态`,
operatorId: user.id
})
}
// 清除相关缓存
try {
const { cache } = await import('~~/server/utils/cache-helpers')
await cache.deletePattern('songs:*')
// 清除该用户的认证缓存(角色或权限可能已变更)
await cache.delete(`auth:user:${parseInt(userId)}`)
console.log('[Cache] 歌曲和用户认证缓存已清除(用户更新)')
} catch (cacheError) {
console.warn('[Cache] 清除缓存失败:', cacheError)
}
return {
success: true,
user: updatedUser[0],
message: '用户更新成功'
}
} catch (error) {
console.error('更新用户失败:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: '更新用户失败: ' + error.message
})
}
})