diff --git a/prisma/seed.ts b/prisma/seed.ts index 9d81434..b283c4c 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,7 +1,6 @@ import { PrismaClient } from '@prisma/client'; -import bcrypt from 'bcryptjs'; -import { Permissions } from '../src/common/permissions.js'; import { PLATFORM_ROLE_CODE, PLATFORM_TENANT_CODE } from '../src/common/constants.js'; +import { initTenant } from '../src/common/tenant-template.js'; const prisma = new PrismaClient(); @@ -10,243 +9,27 @@ const SUPER_ROLE = PLATFORM_ROLE_CODE; const ADMIN_USERNAME = 'touyun'; const ADMIN_PASSWORD = 'Touyun@123'; -/** 菜单资源(导航)。 */ -const MENUS = [ - { code: 'dashboard', name: '工作台', parent: null, path: '/', component: 'dashboard/index', icon: 'mdi:view-dashboard', sort: 1 }, - { code: 'permission', name: '权限管理', parent: null, path: '/permission', component: 'Layout', icon: 'mdi:shield-account', sort: 2 }, - { code: 'user', name: '用户管理', parent: 'permission', path: '/system/user', component: 'system/user/index', icon: 'mdi:account-group', sort: 1 }, - { code: 'role', name: '角色管理', parent: 'permission', path: '/system/role', component: 'system/role/index', icon: 'mdi:account-key', sort: 2 }, - { code: 'resource', name: '资源管理', parent: 'permission', path: '/system/resource', component: 'system/resource/index', icon: 'mdi:view-list', sort: 3 }, - { code: 'org', name: '组织管理', parent: 'permission', path: '/system/org', component: 'system/org/index', icon: 'mdi:sitemap', sort: 4 }, - { code: 'system', name: '系统管理', parent: null, path: '/system', component: 'Layout', icon: 'mdi:cog', sort: 3 }, - { code: 'tenant', name: '租户管理', parent: 'system', path: '/system/tenant', component: 'system/tenant/index', icon: 'mdi:domain', sort: 1 }, - { code: 'dict', name: '字典管理', parent: 'system', path: '/system/dict', component: 'system/dict/index', icon: 'mdi:book-open-variant', sort: 2 }, - { code: 'config', name: '参数配置', parent: 'system', path: '/system/config', component: 'system/config/index', icon: 'mdi:tune', sort: 3 }, - { code: 'monitor', name: '系统监控', parent: null, path: '/monitor', component: 'Layout', icon: 'mdi:monitor-dashboard', sort: 4 }, - { code: 'audit', name: '操作日志', parent: 'monitor', path: '/monitor/audit', component: 'monitor/audit/index', icon: 'mdi:clipboard-text', sort: 1 }, - { code: 'login-log', name: '登录日志', parent: 'monitor', path: '/monitor/login-log', component: 'monitor/login-log/index', icon: 'mdi:login', sort: 2 }, -]; - -/** 接口资源(每个受控接口一个,code = 权限码)。 */ -const APIS: { code: string; name: string; method: string; path: string; parent: string }[] = [ - { code: Permissions.tenant.list, name: '租户列表', method: 'GET', path: '/api/v1/tenants', parent: 'tenant' }, - { code: Permissions.tenant.create, name: '创建租户', method: 'POST', path: '/api/v1/tenants', parent: 'tenant' }, - { code: Permissions.tenant.update, name: '更新租户', method: 'PATCH', path: '/api/v1/tenants/:id', parent: 'tenant' }, - { code: Permissions.tenant.delete, name: '删除租户', method: 'DELETE', path: '/api/v1/tenants/:id', parent: 'tenant' }, - { code: Permissions.user.list, name: '用户列表', method: 'GET', path: '/api/v1/users', parent: 'user' }, - { code: Permissions.user.create, name: '创建用户', method: 'POST', path: '/api/v1/users', parent: 'user' }, - { code: Permissions.user.update, name: '更新用户', method: 'PATCH', path: '/api/v1/users/:id', parent: 'user' }, - { code: Permissions.user.delete, name: '删除用户', method: 'DELETE', path: '/api/v1/users/:id', parent: 'user' }, - { code: Permissions.user.resetPwd, name: '重置密码', method: 'PATCH', path: '/api/v1/users/:id/password', parent: 'user' }, - { code: Permissions.user.assignRole, name: '分配角色', method: 'PUT', path: '/api/v1/users/:id/roles', parent: 'user' }, - { code: Permissions.role.list, name: '角色列表', method: 'GET', path: '/api/v1/roles', parent: 'role' }, - { code: Permissions.role.create, name: '创建角色', method: 'POST', path: '/api/v1/roles', parent: 'role' }, - { code: Permissions.role.update, name: '更新角色', method: 'PATCH', path: '/api/v1/roles/:id', parent: 'role' }, - { code: Permissions.role.delete, name: '删除角色', method: 'DELETE', path: '/api/v1/roles/:id', parent: 'role' }, - { code: Permissions.role.assignResource, name: '分配资源', method: 'PUT', path: '/api/v1/roles/:id/resources', parent: 'role' }, - { code: Permissions.resource.list, name: '资源列表', method: 'GET', path: '/api/v1/resources', parent: 'resource' }, - { code: Permissions.resource.create, name: '创建资源', method: 'POST', path: '/api/v1/resources', parent: 'resource' }, - { code: Permissions.resource.update, name: '更新资源', method: 'PATCH', path: '/api/v1/resources/:id', parent: 'resource' }, - { code: Permissions.resource.delete, name: '删除资源', method: 'DELETE', path: '/api/v1/resources/:id', parent: 'resource' }, - { code: Permissions.org.list, name: '组织列表', method: 'GET', path: '/api/v1/orgs', parent: 'org' }, - { code: Permissions.org.create, name: '创建组织', method: 'POST', path: '/api/v1/orgs', parent: 'org' }, - { code: Permissions.org.update, name: '更新组织', method: 'PATCH', path: '/api/v1/orgs/:id', parent: 'org' }, - { code: Permissions.org.delete, name: '删除组织', method: 'DELETE', path: '/api/v1/orgs/:id', parent: 'org' }, - { code: Permissions.dict.list, name: '字典列表', method: 'GET', path: '/api/v1/dict-types', parent: 'dict' }, - { code: Permissions.dict.create, name: '创建字典', method: 'POST', path: '/api/v1/dict-types', parent: 'dict' }, - { code: Permissions.dict.update, name: '更新字典', method: 'PATCH', path: '/api/v1/dict-types/:id', parent: 'dict' }, - { code: Permissions.dict.delete, name: '删除字典', method: 'DELETE', path: '/api/v1/dict-types/:id', parent: 'dict' }, - { code: Permissions.config.list, name: '配置列表', method: 'GET', path: '/api/v1/configs', parent: 'config' }, - { code: Permissions.config.create, name: '创建配置', method: 'POST', path: '/api/v1/configs', parent: 'config' }, - { code: Permissions.config.update, name: '更新配置', method: 'PATCH', path: '/api/v1/configs/:id', parent: 'config' }, - { code: Permissions.config.delete, name: '删除配置', method: 'DELETE', path: '/api/v1/configs/:id', parent: 'config' }, - { code: Permissions.audit.list, name: '操作日志列表', method: 'GET', path: '/api/v1/audit-logs', parent: 'audit' }, - { code: Permissions.loginLog.list, name: '登录日志列表', method: 'GET', path: '/api/v1/login-logs', parent: 'login-log' }, -]; - -/** 按钮资源(纯前端按钮,无独立受控接口)。 */ -const BUTTONS = [ - { code: 'user:export', name: '用户导出', parent: 'user' }, - { code: 'role:export', name: '角色导出', parent: 'role' }, - { code: 'audit:export', name: '操作日志导出', parent: 'audit' }, - { code: 'login-log:export', name: '登录日志导出', parent: 'login-log' }, -]; - -/** 内置枚举字典(值对齐 src/common/enums.ts,仅展示层 label/颜色/排序可改)。 */ -const DICTS: { code: string; name: string; items: { label: string; value: string; colorType: string; sort: number }[] }[] = [ - { - code: 'common_status', - name: '通用状态', - items: [ - { label: '启用', value: 'enabled', colorType: 'success', sort: 1 }, - { label: '停用', value: 'disabled', colorType: 'info', sort: 2 }, - ], - }, - { - code: 'user_status', - name: '用户状态', - items: [ - { label: '启用', value: 'enabled', colorType: 'success', sort: 1 }, - { label: '停用', value: 'disabled', colorType: 'info', sort: 2 }, - { label: '锁定', value: 'locked', colorType: 'danger', sort: 3 }, - ], - }, - { - code: 'resource_type', - name: '资源类型', - items: [ - { label: '菜单', value: 'menu', colorType: 'primary', sort: 1 }, - { label: '按钮', value: 'button', colorType: 'warning', sort: 2 }, - { label: '接口', value: 'api', colorType: 'info', sort: 3 }, - ], - }, - { - code: 'data_scope', - name: '数据权限', - items: [ - { label: '全部', value: 'all', colorType: 'success', sort: 1 }, - { label: '本部门', value: 'dept', colorType: 'info', sort: 2 }, - { label: '本部门及以下', value: 'dept_and_child', colorType: 'warning', sort: 3 }, - { label: '本人', value: 'self', colorType: 'danger', sort: 4 }, - { label: '自定义', value: 'custom', colorType: 'primary', sort: 5 }, - ], - }, - { - code: 'login_status', - name: '登录状态', - items: [ - { label: '成功', value: 'success', colorType: 'success', sort: 1 }, - { label: '失败', value: 'fail', colorType: 'danger', sort: 2 }, - ], - }, -]; - async function main(): Promise { - // 1. 租户 + // 1. 平台租户 await prisma.tenant.upsert({ where: { code: TENANT }, update: { name: '投云', status: 'enabled' }, create: { code: TENANT, name: '投云', status: 'enabled' }, }); - // 2. 根组织 - await prisma.org.upsert({ - where: { tenantCode_code: { tenantCode: TENANT, code: TENANT } }, - update: { name: '投云总部' }, - create: { tenantCode: TENANT, code: TENANT, name: '投云总部', parentCode: null, sort: 0, status: 'enabled' }, + // 2. 平台租户初始化(根组织 + 超管角色 + 管理员 + 全量资源 + 内置字典 + 授权) + await initTenant({ + prisma, + tenantCode: TENANT, + tenantName: '投云', + adminRoleCode: SUPER_ROLE, + adminRoleName: '超级管理员', + adminUsername: ADMIN_USERNAME, + adminPassword: ADMIN_PASSWORD, + includePlatformPerms: true, }); - // 3. 超管角色 - await prisma.role.upsert({ - where: { tenantCode_code: { tenantCode: TENANT, code: SUPER_ROLE } }, - update: { name: '超级管理员', dataScope: 'all', status: 'enabled' }, - create: { tenantCode: TENANT, code: SUPER_ROLE, name: '超级管理员', dataScope: 'all', status: 'enabled' }, - }); - - // 4. 管理员用户(update 分支不重置密码/状态,避免重跑 seed 重置管理员密码或重新启用账号) - const passwordHash = await bcrypt.hash(ADMIN_PASSWORD, 10); - const user = await prisma.user.upsert({ - where: { tenantCode_username: { tenantCode: TENANT, username: ADMIN_USERNAME } }, - update: { nickname: '管理员' }, - create: { - tenantCode: TENANT, - username: ADMIN_USERNAME, - passwordHash, - nickname: '管理员', - status: 'enabled', - orgCode: TENANT, - }, - }); - - // 5. 绑定超管角色 - await prisma.userRole.upsert({ - where: { tenantCode_userId_roleCode: { tenantCode: TENANT, userId: user.id, roleCode: SUPER_ROLE } }, - update: {}, - create: { tenantCode: TENANT, userId: user.id, roleCode: SUPER_ROLE }, - }); - - // 6. 资源树(菜单 + 接口 + 按钮) - const allResourceCodes: string[] = []; - for (const m of MENUS) { - allResourceCodes.push(m.code); - await prisma.resource.upsert({ - where: { tenantCode_code: { tenantCode: TENANT, code: m.code } }, - update: { name: m.name, parentCode: m.parent, path: m.path, component: m.component, icon: m.icon, sort: m.sort, type: 'menu' }, - create: { - tenantCode: TENANT, - code: m.code, - name: m.name, - parentCode: m.parent, - type: 'menu', - path: m.path, - component: m.component, - icon: m.icon, - sort: m.sort, - status: 'enabled', - }, - }); - } - for (const a of APIS) { - allResourceCodes.push(a.code); - await prisma.resource.upsert({ - where: { tenantCode_code: { tenantCode: TENANT, code: a.code } }, - update: { name: a.name, parentCode: a.parent, method: a.method, apiPath: a.path, type: 'api' }, - create: { - tenantCode: TENANT, - code: a.code, - name: a.name, - parentCode: a.parent, - type: 'api', - method: a.method, - apiPath: a.path, - status: 'enabled', - }, - }); - } - for (const b of BUTTONS) { - allResourceCodes.push(b.code); - await prisma.resource.upsert({ - where: { tenantCode_code: { tenantCode: TENANT, code: b.code } }, - update: { name: b.name, parentCode: b.parent, type: 'button' }, - create: { tenantCode: TENANT, code: b.code, name: b.name, parentCode: b.parent, type: 'button', status: 'enabled' }, - }); - } - - // 7. 超管全量授权(菜单 + 接口 + 按钮) - for (const resourceCode of allResourceCodes) { - await prisma.roleResource.upsert({ - where: { tenantCode_roleCode_resourceCode: { tenantCode: TENANT, roleCode: SUPER_ROLE, resourceCode } }, - update: {}, - create: { tenantCode: TENANT, roleCode: SUPER_ROLE, resourceCode }, - }); - } - - // 8. 内置字典(类型 + 项,幂等) - for (const d of DICTS) { - await prisma.dictType.upsert({ - where: { tenantCode_code: { tenantCode: TENANT, code: d.code } }, - update: { name: d.name, status: 'enabled', builtIn: true }, - create: { tenantCode: TENANT, code: d.code, name: d.name, status: 'enabled', builtIn: true }, - }); - for (const it of d.items) { - await prisma.dictItem.upsert({ - where: { tenantCode_dictTypeCode_value: { tenantCode: TENANT, dictTypeCode: d.code, value: it.value } }, - update: { label: it.label, colorType: it.colorType, sort: it.sort, status: 'enabled', builtIn: true }, - create: { - tenantCode: TENANT, - dictTypeCode: d.code, - label: it.label, - value: it.value, - colorType: it.colorType, - sort: it.sort, - status: 'enabled', - builtIn: true, - }, - }); - } - } - - console.log(`seed 完成:租户 ${TENANT}、用户 ${ADMIN_USERNAME}、角色 ${SUPER_ROLE}、资源 ${allResourceCodes.length} 个(已全量授权)、内置字典 ${DICTS.length} 个。`); + console.log(`seed 完成:平台租户 ${TENANT}、用户 ${ADMIN_USERNAME}、角色 ${SUPER_ROLE}。`); } main() diff --git a/src/common/base-crud.service.ts b/src/common/base-crud.service.ts index 29636be..82c351c 100644 --- a/src/common/base-crud.service.ts +++ b/src/common/base-crud.service.ts @@ -53,7 +53,7 @@ export class BaseCrudService { async create(data: Record) { const ctx = getRequestContext(); - if (ctx && this.modelName !== 'Tenant') data.tenantCode = ctx.tenantCode; + if (ctx && this.modelName !== 'tenant') data.tenantCode = ctx.tenantCode; return this.db().create({ data }); } diff --git a/src/common/tenant-template.ts b/src/common/tenant-template.ts new file mode 100644 index 0000000..aabaf2c --- /dev/null +++ b/src/common/tenant-template.ts @@ -0,0 +1,194 @@ +import bcrypt from 'bcryptjs'; +import type { PrismaClient } from '@prisma/client'; +import { Permissions } from './permissions.js'; + +/** 菜单资源(导航)。 */ +export const MENUS = [ + { code: 'dashboard', name: '工作台', parent: null, path: '/', component: 'dashboard/index', icon: 'mdi:view-dashboard', sort: 1 }, + { code: 'permission', name: '权限管理', parent: null, path: '/permission', component: 'Layout', icon: 'mdi:shield-account', sort: 2 }, + { code: 'user', name: '用户管理', parent: 'permission', path: '/system/user', component: 'system/user/index', icon: 'mdi:account-group', sort: 1 }, + { code: 'role', name: '角色管理', parent: 'permission', path: '/system/role', component: 'system/role/index', icon: 'mdi:account-key', sort: 2 }, + { code: 'resource', name: '资源管理', parent: 'permission', path: '/system/resource', component: 'system/resource/index', icon: 'mdi:view-list', sort: 3 }, + { code: 'org', name: '组织管理', parent: 'permission', path: '/system/org', component: 'system/org/index', icon: 'mdi:sitemap', sort: 4 }, + { code: 'system', name: '系统管理', parent: null, path: '/system', component: 'Layout', icon: 'mdi:cog', sort: 3 }, + { code: 'tenant', name: '租户管理', parent: 'system', path: '/system/tenant', component: 'system/tenant/index', icon: 'mdi:domain', sort: 1 }, + { code: 'dict', name: '字典管理', parent: 'system', path: '/system/dict', component: 'system/dict/index', icon: 'mdi:book-open-variant', sort: 2 }, + { code: 'config', name: '参数配置', parent: 'system', path: '/system/config', component: 'system/config/index', icon: 'mdi:tune', sort: 3 }, + { code: 'monitor', name: '系统监控', parent: null, path: '/monitor', component: 'Layout', icon: 'mdi:monitor-dashboard', sort: 4 }, + { code: 'audit', name: '操作日志', parent: 'monitor', path: '/monitor/audit', component: 'monitor/audit/index', icon: 'mdi:clipboard-text', sort: 1 }, + { code: 'login-log', name: '登录日志', parent: 'monitor', path: '/monitor/login-log', component: 'monitor/login-log/index', icon: 'mdi:login', sort: 2 }, +]; + +/** 接口资源(每个受控接口一个,code = 权限码)。 */ +export const APIS: { code: string; name: string; method: string; path: string; parent: string }[] = [ + { code: Permissions.tenant.list, name: '租户列表', method: 'GET', path: '/api/v1/tenants', parent: 'tenant' }, + { code: Permissions.tenant.create, name: '创建租户', method: 'POST', path: '/api/v1/tenants', parent: 'tenant' }, + { code: Permissions.tenant.update, name: '更新租户', method: 'PATCH', path: '/api/v1/tenants/:id', parent: 'tenant' }, + { code: Permissions.tenant.delete, name: '删除租户', method: 'DELETE', path: '/api/v1/tenants/:id', parent: 'tenant' }, + { code: Permissions.user.list, name: '用户列表', method: 'GET', path: '/api/v1/users', parent: 'user' }, + { code: Permissions.user.create, name: '创建用户', method: 'POST', path: '/api/v1/users', parent: 'user' }, + { code: Permissions.user.update, name: '更新用户', method: 'PATCH', path: '/api/v1/users/:id', parent: 'user' }, + { code: Permissions.user.delete, name: '删除用户', method: 'DELETE', path: '/api/v1/users/:id', parent: 'user' }, + { code: Permissions.user.resetPwd, name: '重置密码', method: 'PATCH', path: '/api/v1/users/:id/password', parent: 'user' }, + { code: Permissions.user.assignRole, name: '分配角色', method: 'PUT', path: '/api/v1/users/:id/roles', parent: 'user' }, + { code: Permissions.role.list, name: '角色列表', method: 'GET', path: '/api/v1/roles', parent: 'role' }, + { code: Permissions.role.create, name: '创建角色', method: 'POST', path: '/api/v1/roles', parent: 'role' }, + { code: Permissions.role.update, name: '更新角色', method: 'PATCH', path: '/api/v1/roles/:id', parent: 'role' }, + { code: Permissions.role.delete, name: '删除角色', method: 'DELETE', path: '/api/v1/roles/:id', parent: 'role' }, + { code: Permissions.role.assignResource, name: '分配资源', method: 'PUT', path: '/api/v1/roles/:id/resources', parent: 'role' }, + { code: Permissions.resource.list, name: '资源列表', method: 'GET', path: '/api/v1/resources', parent: 'resource' }, + { code: Permissions.resource.create, name: '创建资源', method: 'POST', path: '/api/v1/resources', parent: 'resource' }, + { code: Permissions.resource.update, name: '更新资源', method: 'PATCH', path: '/api/v1/resources/:id', parent: 'resource' }, + { code: Permissions.resource.delete, name: '删除资源', method: 'DELETE', path: '/api/v1/resources/:id', parent: 'resource' }, + { code: Permissions.org.list, name: '组织列表', method: 'GET', path: '/api/v1/orgs', parent: 'org' }, + { code: Permissions.org.create, name: '创建组织', method: 'POST', path: '/api/v1/orgs', parent: 'org' }, + { code: Permissions.org.update, name: '更新组织', method: 'PATCH', path: '/api/v1/orgs/:id', parent: 'org' }, + { code: Permissions.org.delete, name: '删除组织', method: 'DELETE', path: '/api/v1/orgs/:id', parent: 'org' }, + { code: Permissions.dict.list, name: '字典列表', method: 'GET', path: '/api/v1/dict-types', parent: 'dict' }, + { code: Permissions.dict.create, name: '创建字典', method: 'POST', path: '/api/v1/dict-types', parent: 'dict' }, + { code: Permissions.dict.update, name: '更新字典', method: 'PATCH', path: '/api/v1/dict-types/:id', parent: 'dict' }, + { code: Permissions.dict.delete, name: '删除字典', method: 'DELETE', path: '/api/v1/dict-types/:id', parent: 'dict' }, + { code: Permissions.config.list, name: '配置列表', method: 'GET', path: '/api/v1/configs', parent: 'config' }, + { code: Permissions.config.create, name: '创建配置', method: 'POST', path: '/api/v1/configs', parent: 'config' }, + { code: Permissions.config.update, name: '更新配置', method: 'PATCH', path: '/api/v1/configs/:id', parent: 'config' }, + { code: Permissions.config.delete, name: '删除配置', method: 'DELETE', path: '/api/v1/configs/:id', parent: 'config' }, + { code: Permissions.audit.list, name: '操作日志列表', method: 'GET', path: '/api/v1/audit-logs', parent: 'audit' }, + { code: Permissions.loginLog.list, name: '登录日志列表', method: 'GET', path: '/api/v1/login-logs', parent: 'login-log' }, +]; + +/** 按钮资源(纯前端按钮,无独立受控接口)。 */ +export const BUTTONS = [ + { code: 'user:export', name: '用户导出', parent: 'user' }, + { code: 'role:export', name: '角色导出', parent: 'role' }, + { code: 'audit:export', name: '操作日志导出', parent: 'audit' }, + { code: 'login-log:export', name: '登录日志导出', parent: 'login-log' }, +]; + +/** 内置枚举字典(值对齐 enums.ts,仅展示层 label/颜色/排序可改)。 */ +export const DICTS: { code: string; name: string; items: { label: string; value: string; colorType: string; sort: number }[] }[] = [ + { code: 'common_status', name: '通用状态', items: [ + { label: '启用', value: 'enabled', colorType: 'success', sort: 1 }, + { label: '停用', value: 'disabled', colorType: 'info', sort: 2 }, + ] }, + { code: 'user_status', name: '用户状态', items: [ + { label: '启用', value: 'enabled', colorType: 'success', sort: 1 }, + { label: '停用', value: 'disabled', colorType: 'info', sort: 2 }, + { label: '锁定', value: 'locked', colorType: 'danger', sort: 3 }, + ] }, + { code: 'resource_type', name: '资源类型', items: [ + { label: '菜单', value: 'menu', colorType: 'primary', sort: 1 }, + { label: '按钮', value: 'button', colorType: 'warning', sort: 2 }, + { label: '接口', value: 'api', colorType: 'info', sort: 3 }, + ] }, + { code: 'data_scope', name: '数据权限', items: [ + { label: '全部', value: 'all', colorType: 'success', sort: 1 }, + { label: '本部门', value: 'dept', colorType: 'info', sort: 2 }, + { label: '本部门及以下', value: 'dept_and_child', colorType: 'warning', sort: 3 }, + { label: '本人', value: 'self', colorType: 'danger', sort: 4 }, + { label: '自定义', value: 'custom', colorType: 'primary', sort: 5 }, + ] }, + { code: 'login_status', name: '登录状态', items: [ + { label: '成功', value: 'success', colorType: 'success', sort: 1 }, + { label: '失败', value: 'fail', colorType: 'danger', sort: 2 }, + ] }, +]; + +export interface InitTenantOptions { + prisma: PrismaClient; + tenantCode: string; + tenantName: string; + /** 顶层管理员角色码:平台=super_admin,业务租户=tenant_super_admin。 */ + adminRoleCode: string; + adminRoleName: string; + adminUsername: string; + adminPassword: string; + /** 是否包含平台级权限(tenant:*)——仅平台租户为 true。 */ + includePlatformPerms: boolean; +} + +/** 初始化一个租户:根组织、顶层角色、管理员、全量资源、内置字典、授权(幂等)。 */ +export async function initTenant(opts: InitTenantOptions): Promise { + const { prisma, tenantCode, tenantName, adminRoleCode, adminRoleName, adminUsername, adminPassword, includePlatformPerms } = opts; + + // 1. 根组织 + await prisma.org.upsert({ + where: { tenantCode_code: { tenantCode, code: tenantCode } }, + update: {}, + create: { tenantCode, code: tenantCode, name: `${tenantName}总部`, parentCode: null, sort: 0, status: 'enabled' }, + }); + + // 2. 顶层角色 + await prisma.role.upsert({ + where: { tenantCode_code: { tenantCode, code: adminRoleCode } }, + update: {}, + create: { tenantCode, code: adminRoleCode, name: adminRoleName, dataScope: 'all', status: 'enabled' }, + }); + + // 3. 管理员账号(update 不重置密码/状态) + const passwordHash = await bcrypt.hash(adminPassword, 10); + const admin = await prisma.user.upsert({ + where: { tenantCode_username: { tenantCode, username: adminUsername } }, + update: {}, + create: { tenantCode, username: adminUsername, passwordHash, nickname: '管理员', status: 'enabled', orgCode: tenantCode }, + }); + + // 4. 资源(菜单 + 接口 + 按钮;业务租户剥离 tenant:* 平台级接口) + const apis = includePlatformPerms ? APIS : APIS.filter((a) => !a.code.startsWith('tenant:')); + const allResourceCodes: string[] = []; + for (const m of MENUS) { + allResourceCodes.push(m.code); + await prisma.resource.upsert({ + where: { tenantCode_code: { tenantCode, code: m.code } }, + update: { name: m.name, parentCode: m.parent, path: m.path, component: m.component, icon: m.icon, sort: m.sort, type: 'menu' }, + create: { tenantCode, code: m.code, name: m.name, parentCode: m.parent, type: 'menu', path: m.path, component: m.component, icon: m.icon, sort: m.sort, status: 'enabled' }, + }); + } + for (const a of apis) { + allResourceCodes.push(a.code); + await prisma.resource.upsert({ + where: { tenantCode_code: { tenantCode, code: a.code } }, + update: { name: a.name, parentCode: a.parent, method: a.method, apiPath: a.path, type: 'api' }, + create: { tenantCode, code: a.code, name: a.name, parentCode: a.parent, type: 'api', method: a.method, apiPath: a.path, status: 'enabled' }, + }); + } + for (const b of BUTTONS) { + allResourceCodes.push(b.code); + await prisma.resource.upsert({ + where: { tenantCode_code: { tenantCode, code: b.code } }, + update: { name: b.name, parentCode: b.parent, type: 'button' }, + create: { tenantCode, code: b.code, name: b.name, parentCode: b.parent, type: 'button', status: 'enabled' }, + }); + } + + // 5. 内置字典 + for (const d of DICTS) { + await prisma.dictType.upsert({ + where: { tenantCode_code: { tenantCode, code: d.code } }, + update: { name: d.name, status: 'enabled', builtIn: true }, + create: { tenantCode, code: d.code, name: d.name, status: 'enabled', builtIn: true }, + }); + for (const it of d.items) { + await prisma.dictItem.upsert({ + where: { tenantCode_dictTypeCode_value: { tenantCode, dictTypeCode: d.code, value: it.value } }, + update: { label: it.label, colorType: it.colorType, sort: it.sort, status: 'enabled', builtIn: true }, + create: { tenantCode, dictTypeCode: d.code, label: it.label, value: it.value, colorType: it.colorType, sort: it.sort, status: 'enabled', builtIn: true }, + }); + } + } + + // 6. 绑定管理员到顶层角色 + await prisma.userRole.upsert({ + where: { tenantCode_userId_roleCode: { tenantCode, userId: admin.id, roleCode: adminRoleCode } }, + update: {}, + create: { tenantCode, userId: admin.id, roleCode: adminRoleCode }, + }); + + // 7. 顶层角色全量授权 + for (const code of allResourceCodes) { + await prisma.roleResource.upsert({ + where: { tenantCode_roleCode_resourceCode: { tenantCode, roleCode: adminRoleCode, resourceCode: code } }, + update: {}, + create: { tenantCode, roleCode: adminRoleCode, resourceCode: code }, + }); + } +} diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 7bc96c7..97098f3 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -17,9 +17,12 @@ const NO_WHERE_OPS = new Set(['create', 'createMany']); @Injectable() export class PrismaService implements OnModuleInit, OnModuleDestroy { readonly client: PrismaClient; + /** 未扩展的裸客户端(无租户/软删注入),供 seed/租户模板等跨租户初始化使用。 */ + readonly raw: PrismaClient; constructor() { - this.client = new PrismaClient().$extends({ + this.raw = new PrismaClient(); + this.client = this.raw.$extends({ query: { $allModels: { async $allOperations({ model, operation, args, query }: any) { diff --git a/src/tenants/dto.ts b/src/tenants/dto.ts index e9752ee..9a3e9bc 100644 --- a/src/tenants/dto.ts +++ b/src/tenants/dto.ts @@ -19,6 +19,16 @@ export class CreateTenantDto { @IsIn(Object.values(CommonStatus)) status?: string; + @ApiPropertyOptional({ description: '初始管理员用户名', default: 'admin' }) + @IsOptional() + @IsString() + adminUsername?: string; + + @ApiPropertyOptional({ description: '初始管理员密码', default: 'Admin@123456' }) + @IsOptional() + @IsString() + adminPassword?: string; + @ApiPropertyOptional({ description: '备注' }) @IsOptional() @IsString() diff --git a/src/tenants/tenants.service.ts b/src/tenants/tenants.service.ts index 7613f4a..c6b00ed 100644 --- a/src/tenants/tenants.service.ts +++ b/src/tenants/tenants.service.ts @@ -1,6 +1,8 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { BaseCrudService } from '../common/base-crud.service.js'; import { PrismaService } from '../prisma/prisma.service.js'; +import { TENANT_ADMIN_ROLE_CODE } from '../common/constants.js'; +import { initTenant } from '../common/tenant-template.js'; import { CreateTenantDto, ListTenantQuery, UpdateTenantDto } from './dto.js'; @Injectable() @@ -9,8 +11,25 @@ export class TenantsService extends BaseCrudService { super(prisma, 'tenant'); } + /** 创建租户并初始化:根组织 + 租户管理员角色 + 管理员账号 + 资源 + 字典 + 授权。 */ async createTenant(dto: CreateTenantDto) { - return this.create({ ...dto }); + const tenant = await this.create({ + code: dto.code, + name: dto.name, + status: dto.status ?? 'enabled', + remark: dto.remark, + }); + await initTenant({ + prisma: this.prisma.raw, + tenantCode: dto.code, + tenantName: dto.name, + adminRoleCode: TENANT_ADMIN_ROLE_CODE, + adminRoleName: '租户管理员', + adminUsername: dto.adminUsername ?? 'admin', + adminPassword: dto.adminPassword ?? 'Admin@123456', + includePlatformPerms: false, + }); + return tenant; } listTenants(q: ListTenantQuery) { @@ -27,9 +46,28 @@ export class TenantsService extends BaseCrudService { return this.update(id, { ...dto }); } + /** 删除租户并级联清理其全部业务数据。 */ async removeTenant(id: string) { const existing = await this.findById(id); if (!existing) throw new NotFoundException('租户不存在'); - return this.db().delete({ where: { id } }); + const code = existing.code; + const db = this.prisma.raw; + await db.$transaction([ + db.userRole.deleteMany({ where: { tenantCode: code } }), + db.roleResource.deleteMany({ where: { tenantCode: code } }), + db.roleOrg.deleteMany({ where: { tenantCode: code } }), + db.refreshToken.deleteMany({ where: { tenantCode: code } }), + db.loginLog.deleteMany({ where: { tenantCode: code } }), + db.auditLog.deleteMany({ where: { tenantCode: code } }), + db.config.deleteMany({ where: { tenantCode: code } }), + db.dictItem.deleteMany({ where: { tenantCode: code } }), + db.dictType.deleteMany({ where: { tenantCode: code } }), + db.org.deleteMany({ where: { tenantCode: code } }), + db.resource.deleteMany({ where: { tenantCode: code } }), + db.role.deleteMany({ where: { tenantCode: code } }), + db.user.deleteMany({ where: { tenantCode: code } }), + db.tenant.delete({ where: { id } }), + ]); + return null; } }