2025-11-23 14:04:20 +08:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
|
// @ts-nocheck
|
|
|
|
|
|
// 加载环境变量(必须在其他导入之前)
|
|
|
|
|
|
import * as dotenv from 'dotenv';
|
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
|
|
|
|
|
|
|
// 根据 NODE_ENV 加载对应的环境配置文件
|
|
|
|
|
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
|
|
|
|
const envFile = `.env.${nodeEnv}`;
|
|
|
|
|
|
// scripts 目录的父目录就是 backend 目录
|
|
|
|
|
|
const backendDir = path.resolve(__dirname, '..');
|
|
|
|
|
|
const envPath = path.resolve(backendDir, envFile);
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试加载环境特定的配置文件
|
|
|
|
|
|
dotenv.config({ path: envPath });
|
|
|
|
|
|
|
|
|
|
|
|
// 如果环境特定文件不存在,尝试加载默认的 .env 文件
|
|
|
|
|
|
if (!process.env.DATABASE_URL) {
|
|
|
|
|
|
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证必要的环境变量
|
|
|
|
|
|
if (!process.env.DATABASE_URL) {
|
|
|
|
|
|
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
|
|
|
|
|
console.error(` 请确保存在以下文件之一:`);
|
|
|
|
|
|
console.error(` - ${envPath}`);
|
|
|
|
|
|
console.error(` - ${path.resolve(backendDir, '.env')}`);
|
|
|
|
|
|
console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
import { PrismaClient } from '@prisma/client';
|
2025-12-09 11:10:36 +08:00
|
|
|
|
import * as fs from 'fs';
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
|
|
|
2025-12-09 11:10:36 +08:00
|
|
|
|
// 从 JSON 文件加载菜单数据
|
|
|
|
|
|
const menusFilePath = path.resolve(backendDir, 'data', 'menus.json');
|
|
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(menusFilePath)) {
|
|
|
|
|
|
console.error(`❌ 错误: 菜单数据文件不存在: ${menusFilePath}`);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const menus = JSON.parse(fs.readFileSync(menusFilePath, 'utf-8'));
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
2026-01-13 14:01:17 +08:00
|
|
|
|
// 超级租户可见的菜单名称(工作台只对普通租户可见)
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const SUPER_TENANT_MENUS = ['我的评审', '活动管理', '系统管理'];
|
2026-01-12 16:06:34 +08:00
|
|
|
|
|
2026-01-12 20:04:11 +08:00
|
|
|
|
// 普通租户可见的菜单名称
|
2026-03-27 22:20:25 +08:00
|
|
|
|
const NORMAL_TENANT_MENUS = ['工作台', '学校管理', '我的评审', '作业管理', '系统管理'];
|
2026-01-12 20:04:11 +08:00
|
|
|
|
|
|
|
|
|
|
// 普通租户在系统管理下排除的子菜单(只保留用户管理和角色管理)
|
|
|
|
|
|
const NORMAL_TENANT_EXCLUDED_SYSTEM_MENUS = ['租户管理', '数据字典', '系统配置', '日志记录', '菜单管理', '权限管理'];
|
|
|
|
|
|
|
2026-03-27 22:20:25 +08:00
|
|
|
|
// 普通租户在我的评审下排除的子菜单(只保留活动列表)
|
2026-01-12 20:04:11 +08:00
|
|
|
|
const NORMAL_TENANT_EXCLUDED_ACTIVITY_MENUS = ['我的报名', '我的作品'];
|
|
|
|
|
|
|
2025-11-23 14:04:20 +08:00
|
|
|
|
async function initMenus() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('🚀 开始初始化菜单数据...\n');
|
|
|
|
|
|
|
|
|
|
|
|
// 递归创建菜单
|
|
|
|
|
|
async function createMenu(menuData: any, parentId: number | null = null) {
|
|
|
|
|
|
const { children, ...menuFields } = menuData;
|
|
|
|
|
|
|
|
|
|
|
|
// 查找是否已存在相同名称和父菜单的菜单
|
|
|
|
|
|
const existingMenu = await prisma.menu.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
name: menuFields.name,
|
|
|
|
|
|
parentId: parentId,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let menu;
|
|
|
|
|
|
if (existingMenu) {
|
|
|
|
|
|
// 更新现有菜单
|
|
|
|
|
|
menu = await prisma.menu.update({
|
|
|
|
|
|
where: { id: existingMenu.id },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
name: menuFields.name,
|
|
|
|
|
|
path: menuFields.path || null,
|
|
|
|
|
|
icon: menuFields.icon || null,
|
|
|
|
|
|
component: menuFields.component || null,
|
2025-12-09 11:10:36 +08:00
|
|
|
|
permission: menuFields.permission || null,
|
2025-11-23 14:04:20 +08:00
|
|
|
|
parentId: parentId,
|
|
|
|
|
|
sort: menuFields.sort || 0,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 创建新菜单
|
|
|
|
|
|
menu = await prisma.menu.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
name: menuFields.name,
|
|
|
|
|
|
path: menuFields.path || null,
|
|
|
|
|
|
icon: menuFields.icon || null,
|
|
|
|
|
|
component: menuFields.component || null,
|
2025-12-09 11:10:36 +08:00
|
|
|
|
permission: menuFields.permission || null,
|
2025-11-23 14:04:20 +08:00
|
|
|
|
parentId: parentId,
|
|
|
|
|
|
sort: menuFields.sort || 0,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(` ✓ ${menu.name} (${menu.path || '无路径'})`);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有子菜单,递归创建
|
|
|
|
|
|
if (children && children.length > 0) {
|
|
|
|
|
|
for (const child of children) {
|
|
|
|
|
|
await createMenu(child, menu.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return menu;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清空现有菜单(重新初始化)
|
2026-01-12 16:06:34 +08:00
|
|
|
|
console.log('🗑️ 清空现有菜单和租户菜单关联...');
|
|
|
|
|
|
// 先删除租户菜单关联
|
|
|
|
|
|
await prisma.tenantMenu.deleteMany({});
|
|
|
|
|
|
// 再删除所有子菜单,再删除父菜单(避免外键约束问题)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
await prisma.menu.deleteMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
parentId: {
|
|
|
|
|
|
not: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
await prisma.menu.deleteMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
parentId: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log('✅ 已清空现有菜单\n');
|
|
|
|
|
|
|
|
|
|
|
|
// 创建所有菜单
|
|
|
|
|
|
console.log('📝 创建菜单...\n');
|
|
|
|
|
|
for (const menu of menus) {
|
|
|
|
|
|
await createMenu(menu);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证结果
|
|
|
|
|
|
console.log('\n🔍 验证结果...');
|
|
|
|
|
|
const allMenus = await prisma.menu.findMany({
|
|
|
|
|
|
orderBy: [{ sort: 'asc' }, { id: 'asc' }],
|
|
|
|
|
|
include: {
|
|
|
|
|
|
children: {
|
|
|
|
|
|
orderBy: {
|
|
|
|
|
|
sort: 'asc',
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const topLevelMenus = allMenus.filter((m) => !m.parentId);
|
|
|
|
|
|
const totalMenus = allMenus.length;
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`\n📊 初始化结果:`);
|
|
|
|
|
|
console.log(` 顶级菜单数量: ${topLevelMenus.length}`);
|
|
|
|
|
|
console.log(` 总菜单数量: ${totalMenus}`);
|
|
|
|
|
|
console.log(`\n📋 菜单结构:`);
|
|
|
|
|
|
|
|
|
|
|
|
function printMenuTree(menu: any, indent: string = '') {
|
|
|
|
|
|
console.log(`${indent}├─ ${menu.name} (${menu.path || '无路径'})`);
|
|
|
|
|
|
if (menu.children && menu.children.length > 0) {
|
|
|
|
|
|
menu.children.forEach((child: any, index: number) => {
|
|
|
|
|
|
const isLast = index === menu.children.length - 1;
|
|
|
|
|
|
const childIndent = indent + (isLast ? ' ' : '│ ');
|
|
|
|
|
|
printMenuTree(child, childIndent);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
topLevelMenus.forEach((menu) => {
|
|
|
|
|
|
printMenuTree(menu);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-12 16:06:34 +08:00
|
|
|
|
// 为所有现有租户分配菜单(区分超级租户和普通租户)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
console.log(`\n📋 为所有租户分配菜单...`);
|
|
|
|
|
|
const allTenants = await prisma.tenant.findMany({
|
|
|
|
|
|
where: { validState: 1 },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (allTenants.length === 0) {
|
|
|
|
|
|
console.log('⚠️ 没有找到任何有效租户,跳过菜单分配\n');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(` 找到 ${allTenants.length} 个租户\n`);
|
|
|
|
|
|
|
2026-03-27 22:20:25 +08:00
|
|
|
|
// 获取超级租户菜单ID(工作台、我的评审、活动管理、系统管理及其子菜单)
|
2026-01-12 16:06:34 +08:00
|
|
|
|
const superTenantMenuIds = new Set<number>();
|
|
|
|
|
|
for (const menu of allMenus) {
|
|
|
|
|
|
// 顶级菜单
|
|
|
|
|
|
if (!menu.parentId && SUPER_TENANT_MENUS.includes(menu.name)) {
|
|
|
|
|
|
superTenantMenuIds.add(menu.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 子菜单(检查父菜单是否在超级租户菜单中)
|
|
|
|
|
|
if (menu.parentId) {
|
|
|
|
|
|
const parentMenu = allMenus.find(m => m.id === menu.parentId);
|
|
|
|
|
|
if (parentMenu && SUPER_TENANT_MENUS.includes(parentMenu.name)) {
|
|
|
|
|
|
superTenantMenuIds.add(menu.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 22:20:25 +08:00
|
|
|
|
// 获取普通租户菜单ID(工作台、学校管理、我的评审、作业管理、部分系统管理)
|
2026-01-12 20:04:11 +08:00
|
|
|
|
const normalTenantMenuIds = new Set<number>();
|
|
|
|
|
|
for (const menu of allMenus) {
|
|
|
|
|
|
// 顶级菜单
|
|
|
|
|
|
if (!menu.parentId && NORMAL_TENANT_MENUS.includes(menu.name)) {
|
|
|
|
|
|
normalTenantMenuIds.add(menu.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 子菜单
|
|
|
|
|
|
if (menu.parentId) {
|
|
|
|
|
|
const parentMenu = allMenus.find(m => m.id === menu.parentId);
|
|
|
|
|
|
if (parentMenu && NORMAL_TENANT_MENUS.includes(parentMenu.name)) {
|
|
|
|
|
|
// 系统管理下排除部分子菜单
|
|
|
|
|
|
if (parentMenu.name === '系统管理' && NORMAL_TENANT_EXCLUDED_SYSTEM_MENUS.includes(menu.name)) {
|
|
|
|
|
|
continue; // 跳过排除的菜单
|
|
|
|
|
|
}
|
2026-03-27 22:20:25 +08:00
|
|
|
|
// 我的评审下排除部分子菜单(只保留活动列表)
|
|
|
|
|
|
if (parentMenu.name === '我的评审' && NORMAL_TENANT_EXCLUDED_ACTIVITY_MENUS.includes(menu.name)) {
|
2026-01-12 20:04:11 +08:00
|
|
|
|
continue; // 跳过排除的菜单
|
|
|
|
|
|
}
|
|
|
|
|
|
normalTenantMenuIds.add(menu.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
for (const tenant of allTenants) {
|
2026-01-12 16:06:34 +08:00
|
|
|
|
const isSuperTenant = tenant.isSuper === 1;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-12 16:06:34 +08:00
|
|
|
|
// 确定要分配的菜单
|
|
|
|
|
|
const menusToAssign = isSuperTenant
|
|
|
|
|
|
? allMenus.filter(m => superTenantMenuIds.has(m.id))
|
2026-01-12 20:04:11 +08:00
|
|
|
|
: allMenus.filter(m => normalTenantMenuIds.has(m.id));
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
2026-01-12 16:06:34 +08:00
|
|
|
|
// 为租户分配菜单
|
2026-01-08 09:17:46 +08:00
|
|
|
|
let addedMenuCount = 0;
|
2026-01-12 16:06:34 +08:00
|
|
|
|
for (const menu of menusToAssign) {
|
|
|
|
|
|
await prisma.tenantMenu.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
tenantId: tenant.id,
|
|
|
|
|
|
menuId: menu.id,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
addedMenuCount++;
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 16:06:34 +08:00
|
|
|
|
const tenantType = isSuperTenant ? '(超级租户)' : '(普通租户)';
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
` ✓ 租户 "${tenant.name}" ${tenantType}: 分配了 ${addedMenuCount} 个菜单`,
|
|
|
|
|
|
);
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
console.log(`\n✅ 菜单分配完成!`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 14:04:20 +08:00
|
|
|
|
console.log(`\n✅ 菜单初始化完成!`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('\n💥 初始化菜单失败:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
await prisma.$disconnect();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 执行初始化
|
|
|
|
|
|
initMenus()
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
console.log('\n🎉 菜单初始化脚本执行完成!');
|
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
|
console.error('\n💥 菜单初始化脚本执行失败:', error);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
});
|