ec-nest/src/tenants/tenants.service.ts
liuyun 8eefa7fa44 feat(error): 业务错误码注册表(业务域+错误序)+ BusinessException
- error-codes 单一注册表 + BusinessException + 过滤器识别
- 迁移登录/锁定/待改密/用户名冲突/租户/会话等关键错误

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 22:42:12 +08:00

100 lines
4.1 KiB
TypeScript

import { HttpStatus, Injectable } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import type { PrismaClient } from '@prisma/client';
import { BaseCrudService } from '../common/base-crud.service.js';
import { PrismaService } from '../prisma/prisma.service.js';
import { RedisService } from '../common/redis/redis.service.js';
import { getRequestContext } from '../common/context/request-context.js';
import { TENANT_ADMIN_ROLE_CODE } from '../common/constants.js';
import { BusinessException } from '../common/business-exception.js';
import { ErrorCodes } from '../common/error-codes.js';
import { initTenant } from '../common/tenant-template.js';
import { CreateTenantDto, ListTenantQuery, UpdateTenantDto } from './dto.js';
@Injectable()
export class TenantsService extends BaseCrudService {
constructor(
prisma: PrismaService,
private readonly redis: RedisService,
) {
super(prisma, 'tenant');
}
/** 创建租户并初始化(事务化);未提供管理员密码时生成随机密码返回。 */
async createTenant(dto: CreateTenantDto) {
const ctx = getRequestContext();
const code = dto.code;
const adminPassword = dto.adminPassword ?? randomUUID().slice(0, 12);
await this.prisma.raw.$transaction(async (tx) => {
await tx.tenant.create({
data: {
code,
name: dto.name,
status: dto.status ?? 'enabled',
remark: dto.remark,
brandName: dto.brandName,
logoUrl: dto.logoUrl,
themeColor: dto.themeColor,
copyright: dto.copyright,
createdBy: ctx?.userId ?? null,
},
});
await initTenant({
prisma: tx as unknown as PrismaClient,
tenantCode: code,
tenantName: dto.name,
adminRoleCode: TENANT_ADMIN_ROLE_CODE,
adminRoleName: '租户管理员',
adminUsername: dto.adminUsername ?? 'admin',
adminPassword,
includePlatformPerms: false,
forcePasswordChange: true,
});
});
const tenant = await this.db().findUnique({ where: { code } });
return dto.adminPassword ? tenant : { ...tenant, adminPassword };
}
listTenants(q: ListTenantQuery) {
const where: Record<string, unknown> = {};
if (q.name) where.name = { contains: q.name };
if (q.code) where.code = { contains: q.code };
if (q.status) where.status = q.status;
return this.paginate(q.page, q.pageSize, where);
}
async updateTenant(id: string, dto: UpdateTenantDto) {
const existing = await this.findById(id);
if (!existing) throw new BusinessException(ErrorCodes.TENANT_NOT_FOUND, '租户不存在', HttpStatus.NOT_FOUND);
return this.update(id, { ...dto });
}
/** 删除租户并级联清理全部业务数据与 Redis 缓存。 */
async removeTenant(id: string) {
const existing = await this.findById(id);
if (!existing) throw new BusinessException(ErrorCodes.TENANT_NOT_FOUND, '租户不存在', HttpStatus.NOT_FOUND);
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 } }),
]);
// 清该租户的权限缓存与用户状态缓存(旧 token 立即失效)
await this.redis.delByPrefix(`perm:${code}:`);
await this.redis.delByPrefix(`user:${code}:`);
return null;
}
}