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

@@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "selection_log" DROP CONSTRAINT "selection_log_userId_fkey";
-- AddForeignKey
ALTER TABLE "selection_log" ADD CONSTRAINT "selection_log_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,26 @@
-- AlterTable
ALTER TABLE "user" ADD COLUMN "current_managed_dept" VARCHAR(5);
-- CreateTable
CREATE TABLE "dept_admin" (
"id" SERIAL NOT NULL,
"uid" VARCHAR(30) NOT NULL,
"dept_code" VARCHAR(5) NOT NULL,
"admin_email" VARCHAR(100),
"admin_line_phone" VARCHAR(100),
"admin_mobile_phone" VARCHAR(100),
"note" VARCHAR(1000),
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL,
CONSTRAINT "dept_admin_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "dept_admin_uid_dept_code_key" ON "dept_admin"("uid", "dept_code");
-- AddForeignKey
ALTER TABLE "dept_admin" ADD CONSTRAINT "dept_admin_uid_fkey" FOREIGN KEY ("uid") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "dept_admin" ADD CONSTRAINT "dept_admin_dept_code_fkey" FOREIGN KEY ("dept_code") REFERENCES "dept"("code") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -19,6 +19,7 @@ model User {
name String?
status String? // 在校/减离/NULL
deptCode String? @map("dept_code") // 所属院系代码(外键)
currentManagedDept String? @map("current_managed_dept") @db.VarChar(5) // 当前正在管理的院系代码,用于支持院系管理员类型的角色
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
password String
@@ -29,6 +30,7 @@ model User {
dept Dept? @relation(fields: [deptCode], references: [code])
roles Role[] // 多对多关联角色
selectionLogs SelectionLog[] // 选择日志
deptAdmins DeptAdmin[] // 作为院系管理员的信息
@@map("user")
}
@@ -41,10 +43,31 @@ model Dept {
// 关联
users User[]
deptAdmins DeptAdmin[] // 院系管理员
@@map("dept")
}
// 院系管理员表
model DeptAdmin {
id Int @id @default(autoincrement())
uid String @db.VarChar(30) // 管理员用户ID外键
deptCode String @map("dept_code") @db.VarChar(5) // 院系代码(外键)
adminEmail String? @map("admin_email") @db.VarChar(100) // 管理员邮箱
adminLinePhone String? @map("admin_line_phone") @db.VarChar(100) // 管理员座机
adminMobilePhone String? @map("admin_mobile_phone") @db.VarChar(100) // 管理员手机
note String? @db.VarChar(1000) // 备注
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
// 关联
user User @relation(fields: [uid], references: [id])
dept Dept @relation(fields: [deptCode], references: [code])
@@unique([uid, deptCode], name: "uidx_uid_dept_code")
@@map("dept_admin")
}
// 角色表
model Role {
id Int @id @default(autoincrement())
@@ -68,7 +91,7 @@ model Permission {
model SelectionLog {
id Int @id @default(autoincrement())
userId String // 关联到用户
user User @relation(fields: [userId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// 用于标识是哪个的选项,用.进行分隔,例如"user.filter.dept"
context String

View File

@@ -3,9 +3,13 @@ import bcrypt from 'bcryptjs'
import { Permissions, ALL_PERMISSIONS } from '../src/constants/permissions'
import fs from 'fs'
import path from 'path'
import pLimit from 'p-limit'
const prisma = new PrismaClient()
// 从环境变量获取并发限制默认为16
const dbParallelLimit = pLimit(parseInt(process.env.DB_PARALLEL_LIMIT || '16', 10))
// 解析 JSON 文件并导入院系数据
async function importDepartments() {
const jsonPath = path.join(__dirname, 'init_data', '院系.json')
@@ -16,7 +20,7 @@ async function importDepartments() {
await Promise.all(
departments.map((dept: any) => {
return prisma.dept.upsert({
return dbParallelLimit(() => prisma.dept.upsert({
where: { code: dept.id },
update: {
name: dept.name,
@@ -27,7 +31,7 @@ async function importDepartments() {
name: dept.name,
fullName: dept.full_name,
},
})
}))
})
)
console.log('院系数据导入完成')
@@ -37,13 +41,15 @@ async function main() {
console.log('开始数据库初始化...')
// 插入权限
for (const permName of ALL_PERMISSIONS) {
await prisma.permission.upsert({
where: { name: permName },
update: {},
create: { name: permName },
await Promise.all(
ALL_PERMISSIONS.map((permName) => {
return dbParallelLimit(() => prisma.permission.upsert({
where: { name: permName },
update: {},
create: { name: permName },
}))
})
}
)
// 角色与权限映射
const rolePermissionsMap: Record<string, string[]> = {
@@ -51,18 +57,20 @@ async function main() {
}
// 插入角色
for (const [roleName, perms] of Object.entries(rolePermissionsMap)) {
await prisma.role.upsert({
where: { name: roleName },
update: {},
create: {
name: roleName,
permissions: {
connect: perms.map((name) => ({ name })),
await Promise.all(
Object.entries(rolePermissionsMap).map(([roleName, perms]) => {
return dbParallelLimit(() => prisma.role.upsert({
where: { name: roleName },
update: {},
create: {
name: roleName,
permissions: {
connect: perms.map((name) => ({ name })),
},
},
},
}))
})
}
)
await importDepartments()
@@ -74,33 +82,37 @@ async function main() {
{ id: 'unknown', name: '未知用户', status: '在校', deptCode: '00001', roleNames: [] },
]
for (const u of usersToCreate) {
const password = await bcrypt.hash(u.password ?? '123456', 12)
await prisma.user.upsert({
where: { id: u.id },
update: {
name: u.name,
status: u.status,
deptCode: u.deptCode,
password,
isSuperAdmin: u.isSuperAdmin ?? false,
roles: {
set: u.roleNames.map((name) => ({ name })),
},
},
create: {
id: u.id,
name: u.name,
status: u.status,
deptCode: u.deptCode,
password,
isSuperAdmin: u.isSuperAdmin ?? false,
roles: {
connect: u.roleNames.map((name) => ({ name })),
},
},
await Promise.all(
usersToCreate.map((u) => {
return dbParallelLimit(async () => {
const password = await bcrypt.hash(u.password || process.env.USER_DEFAULT_PASSWORD || 'jeep4ahxahx7ee7U', 12)
await prisma.user.upsert({
where: { id: u.id },
update: {
name: u.name,
status: u.status,
deptCode: u.deptCode,
password,
isSuperAdmin: u.isSuperAdmin ?? false,
roles: {
set: u.roleNames.map((name) => ({ name })),
},
},
create: {
id: u.id,
name: u.name,
status: u.status,
deptCode: u.deptCode,
password,
isSuperAdmin: u.isSuperAdmin ?? false,
roles: {
connect: u.roleNames.map((name) => ({ name })),
},
},
})
})
})
}
)
// 插入文件类型(仅开发环境)
const fileTypes = [
@@ -125,18 +137,19 @@ async function main() {
{ id: 'OTHER', name: '其他', description: '无法归入以上分类的文件' },
]
for (let index = 0; index < fileTypes.length; index++) {
const fileType = fileTypes[index]
await prisma.devFileType.upsert({
where: { id: fileType.id },
update: {
name: fileType.name,
description: fileType.description,
order: (index + 1) * 10,
},
create: {...fileType, order: (index + 1) * 10},
await Promise.all(
fileTypes.map((fileType, index) => {
return dbParallelLimit(() => prisma.devFileType.upsert({
where: { id: fileType.id },
update: {
name: fileType.name,
description: fileType.description,
order: (index + 1) * 10,
},
create: {...fileType, order: (index + 1) * 10},
}))
})
}
)
console.log('文件类型数据初始化完成')
// 插入依赖包类型(仅开发环境)
@@ -152,18 +165,19 @@ async function main() {
{ id: 'NODEJS_CORE', name: 'Node.js核心', description: 'Node.js运行时内置的模块用于底层操作如文件系统、路径处理等例如fs、path、child_process、util、module、os、http、crypto、events、stream、process、net、url、assert。' },
]
for (let index = 0; index < pkgTypes.length; index++) {
const pkgType = pkgTypes[index]
await prisma.devPkgType.upsert({
where: { id: pkgType.id },
update: {
name: pkgType.name,
description: pkgType.description,
order: (index + 1) * 10,
},
create: {...pkgType, order: (index + 1) * 10},
await Promise.all(
pkgTypes.map((pkgType, index) => {
return dbParallelLimit(() => prisma.devPkgType.upsert({
where: { id: pkgType.id },
update: {
name: pkgType.name,
description: pkgType.description,
order: (index + 1) * 10,
},
create: {...pkgType, order: (index + 1) * 10},
}))
})
}
)
console.log('依赖包类型数据初始化完成')
console.log('数据库初始化完成')
@@ -177,4 +191,4 @@ main()
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
})