493 lines
16 KiB
TypeScript
493 lines
16 KiB
TypeScript
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']>;
|