feat: 增加DEFAULT_USER_PASSWORD,作为创建用户时的默认密码,添加p-limit库,添加DB_PARALLEL_LIMIT = 32环境变量作为“数据库批次操作默认并发数” 限制只有超级管理员才能创建超级管理员用户 删除用户时可以级联删除SelectionLog 添加zustand全局状态管理 一对多的院系管理功能 ,支持增删改查院系管理员信息、用户可以在header中切换管理的院系
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { createTRPCRouter } from '@/server/trpc'
|
||||
import { usersRouter } from './users'
|
||||
import { deptAdminRouter } from './dept-admin'
|
||||
import { selectionRouter } from './selection'
|
||||
import { uploadRouter } from './upload'
|
||||
import { globalRouter } from './global'
|
||||
@@ -14,6 +15,7 @@ import { commonRouter } from './common'
|
||||
export const appRouter = createTRPCRouter({
|
||||
common: commonRouter,
|
||||
users: usersRouter,
|
||||
deptAdmin: deptAdminRouter,
|
||||
selection: selectionRouter,
|
||||
upload: uploadRouter,
|
||||
global: globalRouter,
|
||||
@@ -27,4 +29,4 @@ export const appRouter = createTRPCRouter({
|
||||
} : {})
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
export type AppRouter = typeof appRouter
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// 通用接口,与特定业务关联性不强,需要在不同的地方反复使用
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from '@/server/trpc'
|
||||
import { inferProcedureOutput } from '@trpc/server';
|
||||
|
||||
export const commonRouter = createTRPCRouter({
|
||||
getDepts: permissionRequiredProcedure('').query(({ ctx }) =>
|
||||
ctx.db.dept.findMany({ orderBy: { code: 'asc' } })
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
export type CommonRouter = typeof commonRouter;
|
||||
export type Dept = inferProcedureOutput<CommonRouter['getDepts']>[number]
|
||||
|
||||
206
src/server/routers/dept-admin.ts
Normal file
206
src/server/routers/dept-admin.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from '@/server/trpc'
|
||||
import { Permissions } from '@/constants/permissions'
|
||||
import { dataTableQueryParamsSchema } from '@/lib/schema/data-table'
|
||||
import { transformDataTableQueryParams } from '@/server/utils/data-table-helper'
|
||||
import { z } from 'zod'
|
||||
import { inferProcedureOutput, TRPCError } from '@trpc/server'
|
||||
|
||||
// 创建院系管理员的 schema
|
||||
const createDeptAdminSchema = z.object({
|
||||
uid: z.string().min(1, '用户ID不能为空'),
|
||||
deptCode: z.string().min(1, '院系代码不能为空'),
|
||||
adminEmail: z.string().email('邮箱格式不正确').optional().or(z.literal('')),
|
||||
adminLinePhone: z.string().optional(),
|
||||
adminMobilePhone: z.string().optional(),
|
||||
note: z.string().optional(),
|
||||
})
|
||||
|
||||
// 更新院系管理员的 schema
|
||||
const updateDeptAdminSchema = z.object({
|
||||
id: z.number(),
|
||||
uid: z.string().min(1, '用户ID不能为空'),
|
||||
deptCode: z.string().min(1, '院系代码不能为空'),
|
||||
adminEmail: z.string().email('邮箱格式不正确').optional().or(z.literal('')),
|
||||
adminLinePhone: z.string().optional(),
|
||||
adminMobilePhone: z.string().optional(),
|
||||
note: z.string().optional(),
|
||||
})
|
||||
|
||||
export const deptAdminRouter = createTRPCRouter({
|
||||
list: permissionRequiredProcedure(Permissions.USER_MANAGE)
|
||||
.input(dataTableQueryParamsSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { where, orderBy, skip, take } = transformDataTableQueryParams(input, {
|
||||
model: 'DeptAdmin',
|
||||
columns: {
|
||||
id: { field: 'id', variant: 'number', sortable: true },
|
||||
uid: { field: 'uid', variant: 'text', sortable: true },
|
||||
userName: { field: 'user.name', variant: 'text', sortable: true },
|
||||
deptCode: { field: 'deptCode', variant: 'multiSelect', sortable: true },
|
||||
adminEmail: { field: 'adminEmail', variant: 'text', sortable: true },
|
||||
adminLinePhone: { field: 'adminLinePhone', variant: 'text', sortable: true },
|
||||
adminMobilePhone: { field: 'adminMobilePhone', variant: 'text', sortable: true },
|
||||
createdAt: { field: 'createdAt', sortable: true },
|
||||
updatedAt: { field: 'updatedAt', sortable: true },
|
||||
},
|
||||
})
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
ctx.db.deptAdmin.findMany({
|
||||
where,
|
||||
orderBy: orderBy.some(item => 'id' in item) ? orderBy : [...orderBy, { id: 'asc' }],
|
||||
skip,
|
||||
take,
|
||||
include: {
|
||||
user: { select: { id: true, name: true } },
|
||||
dept: { select: { code: true, name: true, fullName: true } },
|
||||
},
|
||||
}),
|
||||
ctx.db.deptAdmin.count({ where }),
|
||||
])
|
||||
|
||||
return { data, total }
|
||||
}),
|
||||
|
||||
create: permissionRequiredProcedure(Permissions.USER_MANAGE)
|
||||
.input(createDeptAdminSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { uid, deptCode, adminEmail, adminLinePhone, adminMobilePhone, note } = input
|
||||
|
||||
// 检查用户是否存在
|
||||
const user = await ctx.db.user.findUnique({ where: { id: uid } })
|
||||
if (!user) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '用户不存在' })
|
||||
}
|
||||
|
||||
// 检查院系是否存在
|
||||
const dept = await ctx.db.dept.findUnique({ where: { code: deptCode } })
|
||||
if (!dept) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '院系不存在' })
|
||||
}
|
||||
|
||||
// 检查是否已存在相同的用户-院系组合
|
||||
const existing = await ctx.db.deptAdmin.findUnique({
|
||||
where: {
|
||||
uidx_uid_dept_code: {
|
||||
uid,
|
||||
deptCode,
|
||||
},
|
||||
},
|
||||
})
|
||||
if (existing) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '该用户已是该院系的管理员' })
|
||||
}
|
||||
|
||||
return ctx.db.deptAdmin.create({
|
||||
data: {
|
||||
uid,
|
||||
deptCode,
|
||||
adminEmail: adminEmail?.trim() || null,
|
||||
adminLinePhone: adminLinePhone?.trim() || null,
|
||||
adminMobilePhone: adminMobilePhone?.trim() || null,
|
||||
note: note?.trim() || null,
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, name: true } },
|
||||
dept: { select: { code: true, name: true, fullName: true } },
|
||||
},
|
||||
})
|
||||
}),
|
||||
|
||||
update: permissionRequiredProcedure(Permissions.USER_MANAGE)
|
||||
.input(updateDeptAdminSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, uid, deptCode, adminEmail, adminLinePhone, adminMobilePhone, note } = input
|
||||
|
||||
// 检查院系管理员是否存在
|
||||
const existing = await ctx.db.deptAdmin.findUnique({ where: { id } })
|
||||
if (!existing) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '院系管理员不存在' })
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const user = await ctx.db.user.findUnique({ where: { id: uid } })
|
||||
if (!user) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '用户不存在' })
|
||||
}
|
||||
|
||||
// 检查院系是否存在
|
||||
const dept = await ctx.db.dept.findUnique({ where: { code: deptCode } })
|
||||
if (!dept) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '院系不存在' })
|
||||
}
|
||||
|
||||
// 如果修改了 uid 或 deptCode,检查新的组合是否已存在
|
||||
if (uid !== existing.uid || deptCode !== existing.deptCode) {
|
||||
const duplicate = await ctx.db.deptAdmin.findUnique({
|
||||
where: {
|
||||
uidx_uid_dept_code: {
|
||||
uid,
|
||||
deptCode,
|
||||
},
|
||||
},
|
||||
})
|
||||
if (duplicate) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '该用户已是该院系的管理员' })
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.db.deptAdmin.update({
|
||||
where: { id },
|
||||
data: {
|
||||
uid,
|
||||
deptCode,
|
||||
adminEmail: adminEmail?.trim() || null,
|
||||
adminLinePhone: adminLinePhone?.trim() || null,
|
||||
adminMobilePhone: adminMobilePhone?.trim() || null,
|
||||
note: note?.trim() || null,
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, name: true } },
|
||||
dept: { select: { code: true, name: true, fullName: true } },
|
||||
},
|
||||
})
|
||||
}),
|
||||
|
||||
getById: permissionRequiredProcedure(Permissions.USER_MANAGE)
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const deptAdmin = await ctx.db.deptAdmin.findUnique({
|
||||
where: { id: input.id },
|
||||
include: {
|
||||
user: { select: { id: true, name: true } },
|
||||
dept: { select: { code: true, name: true, fullName: true } },
|
||||
},
|
||||
})
|
||||
|
||||
if (!deptAdmin) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '院系管理员不存在' })
|
||||
}
|
||||
|
||||
return deptAdmin
|
||||
}),
|
||||
|
||||
delete: permissionRequiredProcedure(Permissions.USER_MANAGE)
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id } = input
|
||||
|
||||
// 检查院系管理员是否存在
|
||||
const existing = await ctx.db.deptAdmin.findUnique({ where: { id } })
|
||||
if (!existing) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '院系管理员不存在' })
|
||||
}
|
||||
|
||||
return ctx.db.deptAdmin.delete({
|
||||
where: { id },
|
||||
include: {
|
||||
user: { select: { id: true, name: true } },
|
||||
dept: { select: { code: true, name: true, fullName: true } },
|
||||
},
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
export type DeptAdminRouter = typeof deptAdminRouter
|
||||
export type DeptAdmin = inferProcedureOutput<DeptAdminRouter['list']>['data'][number]
|
||||
@@ -6,6 +6,10 @@ 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)
|
||||
@@ -186,14 +190,16 @@ export const usersRouter = createTRPCRouter({
|
||||
|
||||
await Promise.all(
|
||||
batch.map(user =>
|
||||
ctx.db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
roles: action === 'grant'
|
||||
? { connect: { id: roleId } }
|
||||
: { disconnect: { id: roleId } }
|
||||
}
|
||||
})
|
||||
dbParallelLimit(() =>
|
||||
ctx.db.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
roles: action === 'grant'
|
||||
? { connect: { id: roleId } }
|
||||
: { disconnect: { id: roleId } }
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -206,6 +212,11 @@ export const usersRouter = createTRPCRouter({
|
||||
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已存在' })
|
||||
|
||||
@@ -235,6 +246,11 @@ export const usersRouter = createTRPCRouter({
|
||||
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() || '',
|
||||
@@ -372,6 +388,102 @@ export const usersRouter = createTRPCRouter({
|
||||
))
|
||||
).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 }
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user