Files
hair-keeper/src/server/routers/users.ts

493 lines
16 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 { createTRPCRouter, permissionRequiredProcedure } from '@/server/trpc'
import { Permissions } from '@/constants/permissions'
import { createUserSchema, updateUserSchema, changePasswordSchema } from '@/lib/schema/user'
import { dataTableQueryParamsSchema } from '@/lib/schema/data-table'
import { transformDataTableQueryParams } from '@/server/utils/data-table-helper'
import bcrypt from 'bcryptjs'
import { z } from 'zod'
import { inferProcedureOutput, TRPCError } from '@trpc/server'
import pLimit from 'p-limit'
// 从环境变量获取并发限制默认为16
const dbParallelLimit = pLimit(parseInt(process.env.DB_PARALLEL_LIMIT || '16', 10))
export const usersRouter = createTRPCRouter({
list: permissionRequiredProcedure(Permissions.USER_MANAGE)
.input(dataTableQueryParamsSchema)
.query(async ({ ctx, input }) => {
const { where, orderBy, skip, take } = transformDataTableQueryParams(input, {
model: 'User',
columns: {
id: { field: 'id', variant: 'text', sortable: true },
name: { field: 'name', variant: 'text', sortable: true },
status: { field: 'status', variant: 'select', sortable: true },
dept: { field: 'deptCode', variant: 'multiSelect', sortable: true },
roles: { field: 'roles.some.id', variant: 'select', transform: Number },
permissions: { field: 'roles.some.permissions.some.id', variant: 'select', transform: Number },
lastLoginAt: { field: 'lastLoginAt', sortable: true },
createdAt: { field: 'createdAt', sortable: true }
},
})
const [data, total] = await Promise.all([
ctx.db.user.findMany({
where,
orderBy: orderBy.some(item => 'id' in item) ? orderBy : [...orderBy, { id: 'asc' }],
skip,
take,
include: { roles: { include: { permissions: true } }, dept: true },
}),
ctx.db.user.count({ where }),
])
return { data, total }
}),
getRoles: permissionRequiredProcedure(Permissions.USER_MANAGE).query(({ ctx }) =>
ctx.db.role.findMany({ orderBy: { name: 'asc' } })
),
getPermissions: permissionRequiredProcedure(Permissions.USER_MANAGE).query(({ ctx }) =>
ctx.db.permission.findMany({ orderBy: { name: 'asc' } })
),
// 角色管理相关API
getRolesWithStats: permissionRequiredProcedure(Permissions.USER_MANAGE).query(async ({ ctx }) => {
const roles = await ctx.db.role.findMany({
include: {
permissions: true,
_count: {
select: { users: true }
}
},
orderBy: { id: 'asc' }
})
return roles.map((role) => ({
id: role.id,
name: role.name,
userCount: role._count.users,
permissions: role.permissions
}))
}),
createRole: permissionRequiredProcedure(Permissions.USER_MANAGE)
.input(z.object({
name: z.string().min(1, '角色名称不能为空'),
permissionIds: z.array(z.number())
}))
.mutation(async ({ ctx, input }) => {
const { name, permissionIds } = input
// 检查角色名是否已存在
const existingRole = await ctx.db.role.findUnique({ where: { name } })
if (existingRole) throw new TRPCError({ code: 'BAD_REQUEST', message: '角色名称已存在' })
return ctx.db.role.create({
data: {
name,
permissions: { connect: permissionIds.map(id => ({ id })) }
},
include: {
permissions: true,
_count: {
select: { users: true }
}
}
})
}),
updateRole: permissionRequiredProcedure(Permissions.USER_MANAGE)
.input(z.object({
id: z.number(),
name: z.string().min(1, '角色名称不能为空'),
permissionIds: z.array(z.number())
}))
.mutation(async ({ ctx, input }) => {
const { id, name, permissionIds } = input
// 检查角色是否存在
const existingRole = await ctx.db.role.findUnique({ where: { id } })
if (!existingRole) throw new TRPCError({ code: 'NOT_FOUND', message: '角色不存在' })
// 检查角色名是否被其他角色占用
const roleWithSameName = await ctx.db.role.findUnique({ where: { name } })
if (roleWithSameName && roleWithSameName.id !== id) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '角色名称已被其他角色使用' })
}
return ctx.db.role.update({
where: { id },
data: {
name,
permissions: { set: permissionIds.map(permissionId => ({ id: permissionId })) }
},
include: {
permissions: true,
_count: {
select: { users: true }
}
}
})
}),
deleteRole: permissionRequiredProcedure(Permissions.USER_MANAGE)
.input(z.object({ id: z.number() }))
.mutation(async ({ ctx, input }) => {
const { id } = input
// 检查角色是否存在
const existingRole = await ctx.db.role.findUnique({
where: { id },
include: { _count: { select: { users: true } } }
})
if (!existingRole) throw new TRPCError({ code: 'NOT_FOUND', message: '角色不存在' })
// 检查是否有用户使用该角色
if (existingRole._count.users > 0) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '该角色还有用户在使用,无法删除' })
}
return ctx.db.role.delete({
where: { id },
include: {
permissions: true,
_count: {
select: { users: true }
}
}
})
}),
batchUpdateRole: permissionRequiredProcedure(Permissions.USER_MANAGE)
.input(z.object({
roleId: z.number(),
deptCodes: z.array(z.string()).optional(),
action: z.enum(['grant', 'revoke'])
}))
.mutation(async ({ ctx, input }) => {
const { roleId, deptCodes, action } = input
// 检查角色是否存在
const role = await ctx.db.role.findUnique({ where: { id: roleId } })
if (!role) throw new TRPCError({ code: 'NOT_FOUND', message: '角色不存在' })
// 构建查询条件
const where: any = {}
if (deptCodes && deptCodes.length > 0) {
where.deptCode = { in: deptCodes }
}
// 获取符合条件的用户
const users = await ctx.db.user.findMany({ where, select: { id: true } })
// 分批处理每批1000个用户
const batchSize = 1000
let processedCount = 0
for (let i = 0; i < users.length; i += batchSize) {
const batch = users.slice(i, i + batchSize)
await Promise.all(
batch.map(user =>
dbParallelLimit(() =>
ctx.db.user.update({
where: { id: user.id },
data: {
roles: action === 'grant'
? { connect: { id: roleId } }
: { disconnect: { id: roleId } }
}
})
)
)
)
processedCount += batch.length
}
return { count: processedCount }
}),
create: permissionRequiredProcedure(Permissions.USER_MANAGE).input(createUserSchema).mutation(async ({ ctx, input }) => {
const { id, name, status, deptCode, password, roleIds, isSuperAdmin } = input
// 检查是否尝试创建超级管理员,只有超级管理员才能创建超级管理员
if (isSuperAdmin && !ctx.session?.user?.isSuperAdmin) {
throw new TRPCError({ code: 'FORBIDDEN', message: '只有超级管理员才能创建超级管理员用户' })
}
const existingUser = await ctx.db.user.findUnique({ where: { id } })
if (existingUser) throw new TRPCError({ code: 'BAD_REQUEST', message: '用户ID已存在' })
const hashedPassword = await bcrypt.hash(password, 12)
return ctx.db.user.create({
data: {
id,
name: name?.trim() || '',
status: status?.trim() || null,
deptCode: deptCode?.trim() || null,
password: hashedPassword,
isSuperAdmin,
roles: { connect: roleIds.map(id => ({ id })) }
},
include: {
roles: { include: { permissions: true } },
dept: true
}
})
}),
update: permissionRequiredProcedure(Permissions.USER_MANAGE).input(updateUserSchema).mutation(async ({ ctx, input }) => {
const { id, name, status, deptCode, password, roleIds, isSuperAdmin } = input
// 检查用户是否存在
const existingUser = await ctx.db.user.findUnique({ where: { id } })
if (!existingUser) throw new TRPCError({ code: 'NOT_FOUND', message: '用户不存在' })
// 检查是否尝试修改 isSuperAdmin 字段,只有超级管理员才能操作
if (isSuperAdmin !== existingUser.isSuperAdmin && !ctx.session?.user?.isSuperAdmin) {
throw new TRPCError({ code: 'FORBIDDEN', message: '只有超级管理员才能修改超级管理员权限' })
}
// 准备更新数据
const updateData: any = {
name: name?.trim() || '',
status: status?.trim() || null,
deptCode: deptCode?.trim() || null,
isSuperAdmin,
roles: { set: roleIds.map(roleId => ({ id: roleId })) }
}
// 如果提供了密码,则更新密码
if (password && password.trim()) {
updateData.password = await bcrypt.hash(password, 12)
}
return ctx.db.user.update({
where: { id },
data: updateData,
include: {
roles: { include: { permissions: true } },
dept: true
}
})
}),
getById: permissionRequiredProcedure(Permissions.USER_MANAGE).input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
const user = await ctx.db.user.findUnique({
where: { id: input.id },
include: {
roles: { include: { permissions: true } },
dept: true
}
})
if (!user) throw new TRPCError({ code: 'NOT_FOUND', message: '用户不存在' })
return user
}),
delete: permissionRequiredProcedure(Permissions.USER_MANAGE).input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
const { id } = input
// 检查用户是否存在
const existingUser = await ctx.db.user.findUnique({ where: { id } })
if (!existingUser) throw new TRPCError({ code: 'NOT_FOUND', message: '用户不存在' })
// 防止用户删除自己
if (ctx.session?.user?.id === id) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '不能删除自己的账户' })
}
// 删除用户
return ctx.db.user.delete({
where: { id },
include: {
roles: { include: { permissions: true } },
dept: true
}
})
}),
changePassword: permissionRequiredProcedure('')
.input(changePasswordSchema)
.mutation(async ({ ctx, input }) => {
const { oldPassword, newPassword } = input
// 获取当前用户ID
const userId = ctx.session?.user?.id
if (!userId) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: '未登录' })
}
// 获取用户信息
const user = await ctx.db.user.findUnique({ where: { id: userId } })
if (!user) {
throw new TRPCError({ code: 'NOT_FOUND', message: '用户不存在' })
}
// 验证旧密码
const isValidPassword = await bcrypt.compare(oldPassword, user.password)
if (!isValidPassword) {
throw new TRPCError({ code: 'BAD_REQUEST', message: '旧密码错误' })
}
// 更新密码
const hashedPassword = await bcrypt.hash(newPassword, 12)
await ctx.db.user.update({
where: { id: userId },
data: { password: hashedPassword }
})
return { success: true }
}),
// 获取当前用户的完整资料
getCurrentUserProfile: permissionRequiredProcedure('')
.query(async ({ ctx }) => {
// 获取当前用户ID
const userId = ctx.session?.user?.id
if (!userId) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: '未登录' })
}
// 获取用户完整信息
const user = await ctx.db.user.findUnique({
where: { id: userId },
include: {
roles: { include: { permissions: true } },
dept: true
}
})
if (!user) {
throw new TRPCError({ code: 'NOT_FOUND', message: '用户不存在' })
}
return {
id: user.id,
name: user.name,
status: user.status,
deptCode: user.deptCode,
deptName: user.dept?.name || null,
deptFullName: user.dept?.fullName || null,
isSuperAdmin: user.isSuperAdmin,
lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt,
roles: user.roles.map(role => ({
id: role.id,
name: role.name,
})),
permissions: Array.from(
new Set(user.roles.flatMap(role =>
role.permissions.map(p => ({
id: p.id,
name: p.name,
}))
))
).sort((a, b) => a.id - b.id),
}
}),
// 获取当前用户可管理的院系列表和正在管理的院系
getManagedDepts: permissionRequiredProcedure('')
.query(async ({ ctx }) => {
const userId = ctx.session!.user.id
// 获取用户当前信息
const currentUser = await ctx.db.user.findUnique({
where: { id: userId },
select: { currentManagedDept: true }
})
let depts: Array<{ code: string; name: string; fullName: string }>
// 超级管理员可以管理所有院系
if (ctx.session?.user?.isSuperAdmin) {
depts = await ctx.db.dept.findMany({
orderBy: { code: 'asc' }
})
} else {
// 普通用户只能管理自己被授权的院系
const deptAdmins = await ctx.db.deptAdmin.findMany({
where: { uid: userId },
include: { dept: true },
orderBy: { deptCode: 'asc' }
})
depts = deptAdmins.map(da => da.dept)
}
// 如果用户当前没有管理院系,但有可管理的院系,自动设置为第一个
let currentDept = currentUser?.currentManagedDept
if (!currentDept && depts.length > 0) {
currentDept = depts[0].code
await ctx.db.user.update({
where: { id: userId },
data: { currentManagedDept: currentDept }
})
}
return {
currentDept,
depts
}
}),
// 切换当前管理的院系
switchManagedDept: permissionRequiredProcedure('')
.input(z.object({
deptCode: z.string().nullable()
}))
.mutation(async ({ ctx, input }) => {
const { deptCode } = input
// 如果要切换到某个院系,需要验证权限
if (deptCode) {
// 超级管理员可以切换到任意院系
if (!ctx.session?.user?.isSuperAdmin) {
// 普通用户需要验证是否有该院系的管理权限
const deptAdmin = await ctx.db.deptAdmin.findUnique({
where: {
uidx_uid_dept_code: {
uid: ctx.session!.user.id,
deptCode: deptCode
}
}
})
if (!deptAdmin) {
throw new TRPCError({
code: 'FORBIDDEN',
message: '您没有该院系的管理权限'
})
}
}
// 验证院系是否存在
const dept = await ctx.db.dept.findUnique({
where: { code: deptCode }
})
if (!dept) {
throw new TRPCError({
code: 'NOT_FOUND',
message: '院系不存在'
})
}
}
// 更新用户的当前管理院系
await ctx.db.user.update({
where: { id: ctx.session!.user.id },
data: { currentManagedDept: deptCode }
})
return { success: true, deptCode }
})
})
export type UsersRouter = typeof usersRouter;
export type User = inferProcedureOutput<UsersRouter['list']>['data'][number];
export type UserProfile = inferProcedureOutput<UsersRouter['getCurrentUserProfile']>;