library-picturebook-activity/backend/scripts/init-guangdong-library.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

194 lines
7.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.

/**
* 广东省立中山图书馆 - 租户初始化脚本
*
* 运行方式:
* 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();
});