forked from admin/hair-keeper
feat: Hair Keeper v1.1.0 版本更新
本次更新包含以下主要改进: ## 新功能 - 添加quickstart.sh脚本帮助用户快速使用模板项目 - 添加simple_deploy.sh便于部署 - 新增院系管理功能(DeptAdmin),支持增删改查院系管理员信息 - 用户可以在header中切换管理的院系 - 添加zustand全局状态管理 - 添加DEFAULT_USER_PASSWORD环境变量,作为创建用户时的默认密码 - 添加p-limit库和DB_PARALLEL_LIMIT环境变量控制数据库批次操作并发数 ## 安全修复 - 修复Next.js CVE-2025-66478漏洞 - 限制只有超级管理员才能创建超级管理员用户 ## 开发环境优化 - 开发终端兼容云端环境 - MinIO客户端直传兼容云端环境 - 开发容器增加vim和Claude Code插件 - 编程代理改用Claude - docker-compose.yml添加全局name属性 ## Bug修复与代码优化 - 删除用户时级联删除SelectionLog - 手机端关闭侧边栏后刷新页面延迟调整(300ms=>350ms) - instrumentation.ts移至src内部以适配生产环境 - 删除部分引发类型错误的无用代码 - 优化quickstart.sh远程仓库推送相关配置 ## 文件变更 - 新增49个文件,修改多个配置和源代码文件 - 重构用户管理模块目录结构 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
148
prisma/seed.ts
148
prisma/seed.ts
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user