Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑
This commit is contained in:
380
src/server/routers/users.ts
Normal file
380
src/server/routers/users.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
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'
|
||||
|
||||
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 =>
|
||||
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
|
||||
|
||||
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: '用户不存在' })
|
||||
|
||||
// 准备更新数据
|
||||
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),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export type UsersRouter = typeof usersRouter;
|
||||
export type User = inferProcedureOutput<UsersRouter['list']>['data'][number];
|
||||
export type UserProfile = inferProcedureOutput<UsersRouter['getCurrentUserProfile']>;
|
||||
Reference in New Issue
Block a user