feat: 增加DEFAULT_USER_PASSWORD,作为创建用户时的默认密码,添加p-limit库,添加DB_PARALLEL_LIMIT = 32环境变量作为“数据库批次操作默认并发数” 限制只有超级管理员才能创建超级管理员用户 删除用户时可以级联删除SelectionLog 添加zustand全局状态管理 一对多的院系管理功能 ,支持增删改查院系管理员信息、用户可以在header中切换管理的院系

This commit is contained in:
2025-11-18 20:07:42 +08:00
parent 7f3190a223
commit 2a80a44972
31 changed files with 1651 additions and 96 deletions

View File

@@ -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

View File

@@ -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]

View 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]

View File

@@ -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 }
})
})