library-picturebook-activity/backend/scripts/init-menus.ts
aid 418aa57ea8 Day4: 超管端设计优化 + UGC绘本创作社区P0实现
一、超管端设计优化
- 文档管理SOP体系建立,docs目录重组
- 统一用户管理:跨租户全局视角,合并用户管理+公众用户
- 活动监管全模块重构:全部活动(统计卡片+阶段筛选+SuperDetail详情页)、报名数据/作品数据/评审进度(两层合一扁平列表)、成果发布(去Tab+统计+隐藏写操作)
- 菜单精简:移除评委管理/评审规则/通知管理
- Bug修复:租户编辑丢失隐藏菜单、pageSize限制、主色统一

二、UGC绘本创作社区P0
- 数据库:10张新表(user_works/user_work_pages/work_tags等)
- 子女账号独立化:Child升级为独立User,家长切换+独立登录
- 用户作品库:CRUD+发布审核,8个API
- AI创作流程:提交→生成→保存到作品库,4个API
- 作品广场:首页改造为推荐流,标签+搜索+排序
- 内容审核(超管端):作品审核+作品管理+标签管理
- 活动联动:WorkSelector作品选择器
- 布局改造:底部5Tab(发现/创作/活动/作品库/我的)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:20:25 +08:00

278 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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';
import * as fs from 'fs';
const prisma = new PrismaClient();
// 从 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'));
// 超级租户可见的菜单名称(工作台只对普通租户可见)
const SUPER_TENANT_MENUS = ['我的评审', '活动管理', '系统管理'];
// 普通租户可见的菜单名称
const NORMAL_TENANT_MENUS = ['工作台', '学校管理', '我的评审', '作业管理', '系统管理'];
// 普通租户在系统管理下排除的子菜单(只保留用户管理和角色管理)
const NORMAL_TENANT_EXCLUDED_SYSTEM_MENUS = ['租户管理', '数据字典', '系统配置', '日志记录', '菜单管理', '权限管理'];
// 普通租户在我的评审下排除的子菜单(只保留活动列表)
const NORMAL_TENANT_EXCLUDED_ACTIVITY_MENUS = ['我的报名', '我的作品'];
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,
permission: menuFields.permission || null,
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,
permission: menuFields.permission || null,
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;
}
// 清空现有菜单(重新初始化)
console.log('🗑️ 清空现有菜单和租户菜单关联...');
// 先删除租户菜单关联
await prisma.tenantMenu.deleteMany({});
// 再删除所有子菜单,再删除父菜单(避免外键约束问题)
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);
});
// 为所有现有租户分配菜单(区分超级租户和普通租户)
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`);
// 获取超级租户菜单ID工作台、我的评审、活动管理、系统管理及其子菜单
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);
}
}
}
// 获取普通租户菜单ID工作台、学校管理、我的评审、作业管理、部分系统管理
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; // 跳过排除的菜单
}
// 我的评审下排除部分子菜单(只保留活动列表)
if (parentMenu.name === '我的评审' && NORMAL_TENANT_EXCLUDED_ACTIVITY_MENUS.includes(menu.name)) {
continue; // 跳过排除的菜单
}
normalTenantMenuIds.add(menu.id);
}
}
}
for (const tenant of allTenants) {
const isSuperTenant = tenant.isSuper === 1;
// 确定要分配的菜单
const menusToAssign = isSuperTenant
? allMenus.filter(m => superTenantMenuIds.has(m.id))
: allMenus.filter(m => normalTenantMenuIds.has(m.id));
// 为租户分配菜单
let addedMenuCount = 0;
for (const menu of menusToAssign) {
await prisma.tenantMenu.create({
data: {
tenantId: tenant.id,
menuId: menu.id,
},
});
addedMenuCount++;
}
const tenantType = isSuperTenant ? '(超级租户)' : '(普通租户)';
console.log(
` ✓ 租户 "${tenant.name}" ${tenantType}: 分配了 ${addedMenuCount} 个菜单`,
);
}
console.log(`\n✅ 菜单分配完成!`);
}
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);
});