一、超管端设计优化 - 文档管理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>
194 lines
7.2 KiB
TypeScript
194 lines
7.2 KiB
TypeScript
/**
|
||
* 广东省立中山图书馆 - 租户初始化脚本
|
||
*
|
||
* 运行方式:
|
||
* npx ts-node -r tsconfig-paths/register scripts/init-guangdong-library.ts
|
||
*
|
||
* 功能:
|
||
* 1. 创建广东省图租户(library 类型)
|
||
* 2. 创建管理员账号
|
||
* 3. 分配必要的角色和权限
|
||
* 4. 分配菜单
|
||
*/
|
||
|
||
import { PrismaClient } from '@prisma/client';
|
||
import * as bcrypt from 'bcrypt';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
async function main() {
|
||
console.log('开始初始化广东省图数据...\n');
|
||
|
||
// 1. 创建租户
|
||
const tenantCode = 'gdlib';
|
||
let tenant = await prisma.tenant.findUnique({ where: { code: tenantCode } });
|
||
|
||
if (tenant) {
|
||
console.log(`租户 ${tenantCode} 已存在 (ID: ${tenant.id}),跳过创建`);
|
||
} else {
|
||
tenant = await prisma.tenant.create({
|
||
data: {
|
||
name: '广东省立中山图书馆',
|
||
code: tenantCode,
|
||
tenantType: 'library',
|
||
description: '广东省图少儿绘本创作活动主办方',
|
||
isSuper: 0,
|
||
validState: 1,
|
||
},
|
||
});
|
||
console.log(`创建租户: ${tenant.name} (ID: ${tenant.id})`);
|
||
}
|
||
|
||
const tenantId = tenant.id;
|
||
|
||
// 2. 创建权限
|
||
const permissions = [
|
||
// 活动管理
|
||
{ code: 'contest:create', resource: 'contest', action: 'create', name: '创建活动', description: '允许创建活动' },
|
||
{ code: 'contest:read', resource: 'contest', action: 'read', name: '查看活动', description: '允许查看活动' },
|
||
{ code: 'contest:update', resource: 'contest', action: 'update', name: '更新活动', description: '允许更新活动' },
|
||
{ code: 'contest:delete', resource: 'contest', action: 'delete', name: '删除活动', description: '允许删除活动' },
|
||
{ code: 'contest:publish', resource: 'contest', action: 'publish', name: '发布活动', description: '允许发布活动' },
|
||
{ code: 'contest:finish', resource: 'contest', action: 'finish', name: '结束活动', description: '允许结束活动' },
|
||
// 报名管理
|
||
{ code: 'registration:approve', resource: 'registration', action: 'approve', name: '审核报名', description: '允许审核报名' },
|
||
{ code: 'registration:read', resource: 'registration', action: 'read', name: '查看报名', description: '允许查看报名记录' },
|
||
// 评委管理
|
||
{ code: 'judge:read', resource: 'judge', action: 'read', name: '查看评委', description: '允许查看评委' },
|
||
{ code: 'judge:create', resource: 'judge', action: 'create', name: '添加评委', description: '允许添加评委' },
|
||
{ code: 'judge:assign', resource: 'judge', action: 'assign', name: '分配评委', description: '允许分配评委' },
|
||
// 评审规则
|
||
{ code: 'review-rule:read', resource: 'review-rule', action: 'read', name: '查看评审规则', description: '允许查看评审规则' },
|
||
{ code: 'review-rule:create', resource: 'review-rule', action: 'create', name: '创建评审规则', description: '允许创建评审规则' },
|
||
// 成果发布
|
||
{ code: 'result:read', resource: 'result', action: 'read', name: '查看成果', description: '允许查看活动成果' },
|
||
{ code: 'result:publish', resource: 'result', action: 'publish', name: '发布成果', description: '允许发布活动成果' },
|
||
// 公告
|
||
{ code: 'notice:create', resource: 'notice', action: 'create', name: '创建公告', description: '允许创建活动公告' },
|
||
{ code: 'notice:read', resource: 'notice', action: 'read', name: '查看公告', description: '允许查看活动公告' },
|
||
// 用户管理
|
||
{ code: 'user:read', resource: 'user', action: 'read', name: '查看用户', description: '允许查看用户' },
|
||
{ code: 'user:create', resource: 'user', action: 'create', name: '创建用户', description: '允许创建用户' },
|
||
// 角色管理
|
||
{ code: 'role:read', resource: 'role', action: 'read', name: '查看角色', description: '允许查看角色' },
|
||
// 菜单
|
||
{ code: 'menu:read', resource: 'menu', action: 'read', name: '查看菜单', description: '允许查看菜单' },
|
||
];
|
||
|
||
const permissionIds: number[] = [];
|
||
for (const perm of permissions) {
|
||
const existing = await prisma.permission.findFirst({
|
||
where: { tenantId, code: perm.code },
|
||
});
|
||
if (existing) {
|
||
permissionIds.push(existing.id);
|
||
} else {
|
||
const created = await prisma.permission.create({
|
||
data: { ...perm, tenantId, validState: 1 },
|
||
});
|
||
permissionIds.push(created.id);
|
||
}
|
||
}
|
||
console.log(`权限已创建/确认: ${permissionIds.length} 个`);
|
||
|
||
// 3. 创建管理员角色
|
||
let adminRole = await prisma.role.findFirst({
|
||
where: { tenantId, code: 'tenant_admin' },
|
||
});
|
||
if (!adminRole) {
|
||
adminRole = await prisma.role.create({
|
||
data: {
|
||
tenantId,
|
||
name: '机构管理员',
|
||
code: 'tenant_admin',
|
||
description: '广东省图机构管理员,管理活动和报名',
|
||
validState: 1,
|
||
},
|
||
});
|
||
console.log(`创建角色: ${adminRole.name}`);
|
||
}
|
||
|
||
// 分配所有权限给管理员角色
|
||
for (const permId of permissionIds) {
|
||
const existing = await prisma.rolePermission.findFirst({
|
||
where: { roleId: adminRole.id, permissionId: permId },
|
||
});
|
||
if (!existing) {
|
||
await prisma.rolePermission.create({
|
||
data: { roleId: adminRole.id, permissionId: permId },
|
||
});
|
||
}
|
||
}
|
||
console.log(`角色权限已分配`);
|
||
|
||
// 4. 创建管理员账号
|
||
const adminUsername = 'admin';
|
||
let adminUser = await prisma.user.findFirst({
|
||
where: { tenantId, username: adminUsername },
|
||
});
|
||
if (!adminUser) {
|
||
const hashedPassword = await bcrypt.hash('admin@gdlib', 10);
|
||
adminUser = await prisma.user.create({
|
||
data: {
|
||
tenantId,
|
||
username: adminUsername,
|
||
password: hashedPassword,
|
||
nickname: '广东省图管理员',
|
||
userSource: 'admin_created',
|
||
status: 'enabled',
|
||
validState: 1,
|
||
roles: {
|
||
create: [{ roleId: adminRole.id }],
|
||
},
|
||
},
|
||
});
|
||
console.log(`创建管理员: ${adminUsername} / admin@gdlib`);
|
||
} else {
|
||
console.log(`管理员 ${adminUsername} 已存在`);
|
||
}
|
||
|
||
// 5. 分配菜单(活动管理 + 系统管理)
|
||
const menuNames = ['活动管理', '系统管理'];
|
||
const menus = await prisma.menu.findMany({
|
||
where: { name: { in: menuNames }, parentId: null },
|
||
include: { children: true },
|
||
});
|
||
|
||
for (const menu of menus) {
|
||
// 分配父菜单
|
||
const existing = await prisma.tenantMenu.findFirst({
|
||
where: { tenantId, menuId: menu.id },
|
||
});
|
||
if (!existing) {
|
||
await prisma.tenantMenu.create({
|
||
data: { tenantId, menuId: menu.id },
|
||
});
|
||
}
|
||
// 分配子菜单
|
||
for (const child of menu.children || []) {
|
||
const childExisting = await prisma.tenantMenu.findFirst({
|
||
where: { tenantId, menuId: child.id },
|
||
});
|
||
if (!childExisting) {
|
||
await prisma.tenantMenu.create({
|
||
data: { tenantId, menuId: child.id },
|
||
});
|
||
}
|
||
}
|
||
}
|
||
console.log(`菜单已分配: ${menuNames.join(', ')}`);
|
||
|
||
console.log('\n广东省图初始化完成!');
|
||
console.log(`登录地址: /${tenantCode}/login`);
|
||
console.log(`账号: admin / admin@gdlib`);
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error('初始化失败:', e);
|
||
process.exit(1);
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect();
|
||
});
|