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' import { normalizeUserStatus, USER_STATUS_RULE_MESSAGE } from '~~/server/utils/user-status' 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. 自己改自己:**只允许改姓名(显示名)**,其余一律禁止。 * * 站长要在「用户管理」里把 admin 的显示名改成自己的名字(登录名仍是 admin), * 原来这里是"自己一律不能改",于是他只看到「禁止在用户管理中修改自己的账户」。 * 放宽到"只有姓名变了、其它字段一个都没动"是安全的:改不了角色/状态/密码/账号名, * 也提不了权,只是换个显示名而已(等价于个人资料页改名字)。 */ if (String(userId) === String(user.id)) { const gradeChanged = String(grade || '').trim() !== String(targetUser.grade || '').trim() const classChanged = String(userClass || '').trim() !== String(targetUser.class || '').trim() const statusChanged = !!status && String(status).toLowerCase() !== String(targetUser.status || '').toLowerCase() const roleChanged = !!role && role !== targetUser.role const onlyNameChanged = nameChanged && !usernameChanged && !gradeChanged && !classChanged && !statusChanged && !roleChanged && !password if (!onlyNameChanged) { 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) { /* * 站长要求用户编辑表单的角色下拉里要有「技术运维」(SUPER_ADMIN)。 * 原 canChangeRoleOf() 一律禁止提升为 SUPER_ADMIN(网页版也这么设计), * 这里对**超级管理员**放开这一条:只有超管能把别人提升为超管,且目标不能是初始账号(ID=1,它本来就是)。 * 效果与 POST /api/admin/users/{id}/promote-super 一致,两条路都可用。 */ const allowPromoteBySuper = user.role === 'SUPER_ADMIN' && role === 'SUPER_ADMIN' && targetUser.id !== 1 if (!allowPromoteBySuper && !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} 无法重置目标用户的密码` }) } /* * 状态:**大小写不敏感**(客户端历史版本发的是大写的 ACTIVE)。 * 认不出来才报错 —— 否则管理员会遇到"编辑任何用户都 400"(已踩过)。 */ const nextStatus = status === undefined || status === null ? null : normalizeUserStatus(status) if (status !== undefined && status !== null && !nextStatus) { throw createError({ statusCode: 400, statusMessage: USER_STATUS_RULE_MESSAGE }) } // 准备更新数据 const updateData = { name, username, role: validRole, grade, class: userClass } // 如果提供了status,则更新状态相关字段 if (nextStatus && nextStatus !== existingUser[0].status) { updateData.status = nextStatus 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 }) } })