feat(tenant): 租户 onboarding 模板 + 级联删除

- 抽取 seed 资源/字典到 tenant-template.ts,initTenant 幂等初始化租户
- 创建租户自动初始化管理员/根组织/角色/资源/字典
- 删除租户级联清理全部业务数据
- 修复 BaseCrudService 对 Tenant 模型的 tenantCode 大小写注入 bug

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 16:32:46 +08:00
parent 53dbc38bab
commit 13510ee216
6 changed files with 262 additions and 234 deletions

View File

@ -1,7 +1,6 @@
import { PrismaClient } from '@prisma/client'; 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 { PLATFORM_ROLE_CODE, PLATFORM_TENANT_CODE } from '../src/common/constants.js';
import { initTenant } from '../src/common/tenant-template.js';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@ -10,243 +9,27 @@ const SUPER_ROLE = PLATFORM_ROLE_CODE;
const ADMIN_USERNAME = 'touyun'; const ADMIN_USERNAME = 'touyun';
const ADMIN_PASSWORD = 'Touyun@123'; 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<void> { async function main(): Promise<void> {
// 1. 租户 // 1. 平台租户
await prisma.tenant.upsert({ await prisma.tenant.upsert({
where: { code: TENANT }, where: { code: TENANT },
update: { name: '投云', status: 'enabled' }, update: { name: '投云', status: 'enabled' },
create: { code: TENANT, name: '投云', status: 'enabled' }, create: { code: TENANT, name: '投云', status: 'enabled' },
}); });
// 2. 根组织 // 2. 平台租户初始化(根组织 + 超管角色 + 管理员 + 全量资源 + 内置字典 + 授权)
await prisma.org.upsert({ await initTenant({
where: { tenantCode_code: { tenantCode: TENANT, code: TENANT } }, prisma,
update: { name: '投云总部' },
create: { tenantCode: TENANT, code: TENANT, name: '投云总部', parentCode: null, sort: 0, status: 'enabled' },
});
// 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, tenantCode: TENANT,
username: ADMIN_USERNAME, tenantName: '投云',
passwordHash, adminRoleCode: SUPER_ROLE,
nickname: '管理员', adminRoleName: '超级管理员',
status: 'enabled', adminUsername: ADMIN_USERNAME,
orgCode: TENANT, adminPassword: ADMIN_PASSWORD,
}, includePlatformPerms: true,
}); });
// 5. 绑定超管角色 console.log(`seed 完成:平台租户 ${TENANT}、用户 ${ADMIN_USERNAME}、角色 ${SUPER_ROLE}。`);
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} 个。`);
} }
main() main()

View File

@ -53,7 +53,7 @@ export class BaseCrudService {
async create(data: Record<string, unknown>) { async create(data: Record<string, unknown>) {
const ctx = getRequestContext(); 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 }); return this.db().create({ data });
} }

View File

@ -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<void> {
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 },
});
}
}

View File

@ -17,9 +17,12 @@ const NO_WHERE_OPS = new Set(['create', 'createMany']);
@Injectable() @Injectable()
export class PrismaService implements OnModuleInit, OnModuleDestroy { export class PrismaService implements OnModuleInit, OnModuleDestroy {
readonly client: PrismaClient; readonly client: PrismaClient;
/** 未扩展的裸客户端(无租户/软删注入),供 seed/租户模板等跨租户初始化使用。 */
readonly raw: PrismaClient;
constructor() { constructor() {
this.client = new PrismaClient().$extends({ this.raw = new PrismaClient();
this.client = this.raw.$extends({
query: { query: {
$allModels: { $allModels: {
async $allOperations({ model, operation, args, query }: any) { async $allOperations({ model, operation, args, query }: any) {

View File

@ -19,6 +19,16 @@ export class CreateTenantDto {
@IsIn(Object.values(CommonStatus)) @IsIn(Object.values(CommonStatus))
status?: string; status?: string;
@ApiPropertyOptional({ description: '初始管理员用户名', default: 'admin' })
@IsOptional()
@IsString()
adminUsername?: string;
@ApiPropertyOptional({ description: '初始管理员密码', default: 'Admin@123456' })
@IsOptional()
@IsString()
adminPassword?: string;
@ApiPropertyOptional({ description: '备注' }) @ApiPropertyOptional({ description: '备注' })
@IsOptional() @IsOptional()
@IsString() @IsString()

View File

@ -1,6 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { BaseCrudService } from '../common/base-crud.service.js'; import { BaseCrudService } from '../common/base-crud.service.js';
import { PrismaService } from '../prisma/prisma.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'; import { CreateTenantDto, ListTenantQuery, UpdateTenantDto } from './dto.js';
@Injectable() @Injectable()
@ -9,8 +11,25 @@ export class TenantsService extends BaseCrudService {
super(prisma, 'tenant'); super(prisma, 'tenant');
} }
/** 创建租户并初始化:根组织 + 租户管理员角色 + 管理员账号 + 资源 + 字典 + 授权。 */
async createTenant(dto: CreateTenantDto) { 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) { listTenants(q: ListTenantQuery) {
@ -27,9 +46,28 @@ export class TenantsService extends BaseCrudService {
return this.update(id, { ...dto }); return this.update(id, { ...dto });
} }
/** 删除租户并级联清理其全部业务数据。 */
async removeTenant(id: string) { async removeTenant(id: string) {
const existing = await this.findById(id); const existing = await this.findById(id);
if (!existing) throw new NotFoundException('租户不存在'); 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;
} }
} }