Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑

This commit is contained in:
2025-11-13 15:24:54 +08:00
commit 42be39b343
249 changed files with 38843 additions and 0 deletions

380
src/server/routers/users.ts Normal file
View 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']>;