2026-03-27 22:20:25 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Injectable,
|
|
|
|
|
|
NotFoundException,
|
|
|
|
|
|
BadRequestException,
|
|
|
|
|
|
ForbiddenException,
|
|
|
|
|
|
} from '@nestjs/common';
|
|
|
|
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
|
|
import { CreateTenantDto } from './dto/create-tenant.dto';
|
|
|
|
|
|
import { UpdateTenantDto } from './dto/update-tenant.dto';
|
|
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
export class TenantsService {
|
|
|
|
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查当前用户所属租户是否为超级租户
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async checkSuperTenant(currentTenantId?: number): Promise<void> {
|
|
|
|
|
|
if (!currentTenantId) {
|
|
|
|
|
|
throw new ForbiddenException('无法确定当前租户信息');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentTenant = await this.prisma.tenant.findUnique({
|
|
|
|
|
|
where: { id: currentTenantId },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!currentTenant) {
|
|
|
|
|
|
throw new ForbiddenException('当前租户不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (currentTenant.isSuper !== 1) {
|
|
|
|
|
|
throw new ForbiddenException('只有超级租户才能操作租户管理');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async create(
|
|
|
|
|
|
createTenantDto: CreateTenantDto,
|
|
|
|
|
|
creatorId?: number,
|
|
|
|
|
|
currentTenantId?: number,
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 检查是否为超级租户
|
|
|
|
|
|
await this.checkSuperTenant(currentTenantId);
|
|
|
|
|
|
|
|
|
|
|
|
const { menuIds, ...tenantData } = createTenantDto;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查租户编码是否已存在
|
|
|
|
|
|
const existingTenant = await this.prisma.tenant.findUnique({
|
|
|
|
|
|
where: { code: tenantData.code },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (existingTenant) {
|
|
|
|
|
|
throw new BadRequestException('租户编码已存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果提供了域名,检查域名是否已存在
|
|
|
|
|
|
if (tenantData.domain) {
|
|
|
|
|
|
const existingDomain = await this.prisma.tenant.findUnique({
|
|
|
|
|
|
where: { domain: tenantData.domain },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (existingDomain) {
|
|
|
|
|
|
throw new BadRequestException('租户域名已存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return this.prisma.tenant.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
...tenantData,
|
|
|
|
|
|
creator: creatorId,
|
|
|
|
|
|
menus:
|
|
|
|
|
|
menuIds && menuIds.length > 0
|
|
|
|
|
|
? {
|
|
|
|
|
|
create: menuIds.map((menuId) => ({
|
|
|
|
|
|
menuId,
|
|
|
|
|
|
})),
|
|
|
|
|
|
}
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
},
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menus: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menu: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async findAll(page: number = 1, pageSize: number = 10) {
|
|
|
|
|
|
const skip = (page - 1) * pageSize;
|
|
|
|
|
|
const [list, total] = await Promise.all([
|
|
|
|
|
|
this.prisma.tenant.findMany({
|
|
|
|
|
|
skip,
|
|
|
|
|
|
take: pageSize,
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menus: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menu: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
_count: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
users: true,
|
|
|
|
|
|
roles: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
orderBy: {
|
|
|
|
|
|
createTime: 'desc',
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
this.prisma.tenant.count(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
list,
|
|
|
|
|
|
total,
|
|
|
|
|
|
page,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async findOne(id: number) {
|
|
|
|
|
|
const tenant = await this.prisma.tenant.findUnique({
|
|
|
|
|
|
where: { id },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menus: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menu: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
_count: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
users: true,
|
|
|
|
|
|
roles: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!tenant) {
|
|
|
|
|
|
throw new NotFoundException('租户不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return tenant;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async findByCode(code: string) {
|
|
|
|
|
|
return this.prisma.tenant.findUnique({
|
|
|
|
|
|
where: { code },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menus: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menu: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async findByDomain(domain: string) {
|
|
|
|
|
|
return this.prisma.tenant.findUnique({
|
|
|
|
|
|
where: { domain },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menus: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menu: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async update(
|
|
|
|
|
|
id: number,
|
|
|
|
|
|
updateTenantDto: UpdateTenantDto,
|
|
|
|
|
|
modifierId?: number,
|
|
|
|
|
|
currentTenantId?: number,
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 检查是否为超级租户
|
|
|
|
|
|
await this.checkSuperTenant(currentTenantId);
|
|
|
|
|
|
|
|
|
|
|
|
const { menuIds, ...tenantData } = updateTenantDto;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查租户是否存在
|
|
|
|
|
|
await this.findOne(id);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果更新了code,检查是否冲突
|
|
|
|
|
|
if (tenantData.code) {
|
|
|
|
|
|
const existingTenant = await this.prisma.tenant.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
code: tenantData.code,
|
|
|
|
|
|
id: { not: id },
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
if (existingTenant) {
|
|
|
|
|
|
throw new BadRequestException('租户编码已存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果更新了domain,检查是否冲突
|
|
|
|
|
|
if (tenantData.domain) {
|
|
|
|
|
|
const existingDomain = await this.prisma.tenant.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
domain: tenantData.domain,
|
|
|
|
|
|
id: { not: id },
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
if (existingDomain) {
|
|
|
|
|
|
throw new BadRequestException('租户域名已存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data: any = {
|
|
|
|
|
|
...tenantData,
|
|
|
|
|
|
modifier: modifierId,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 如果提供了 menuIds,更新菜单关联
|
|
|
|
|
|
if (menuIds !== undefined) {
|
|
|
|
|
|
// 先删除所有现有菜单关联
|
|
|
|
|
|
await this.prisma.tenantMenu.deleteMany({
|
|
|
|
|
|
where: { tenantId: id },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 创建新的菜单关联
|
|
|
|
|
|
if (menuIds.length > 0) {
|
|
|
|
|
|
data.menus = {
|
|
|
|
|
|
create: menuIds.map((menuId) => ({
|
|
|
|
|
|
menuId,
|
|
|
|
|
|
})),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return this.prisma.tenant.update({
|
|
|
|
|
|
where: { id },
|
|
|
|
|
|
data,
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menus: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menu: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async remove(id: number, currentTenantId?: number) {
|
|
|
|
|
|
// 检查是否为超级租户
|
|
|
|
|
|
await this.checkSuperTenant(currentTenantId);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查租户是否存在
|
|
|
|
|
|
await this.findOne(id);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查要删除的租户是否为超级租户
|
|
|
|
|
|
const tenant = await this.prisma.tenant.findUnique({
|
|
|
|
|
|
where: { id },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (tenant?.isSuper === 1) {
|
|
|
|
|
|
throw new BadRequestException('不能删除超级租户');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return this.prisma.tenant.delete({
|
|
|
|
|
|
where: { id },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取租户的菜单树(根据租户分配的菜单)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getTenantMenus(tenantId: number) {
|
|
|
|
|
|
const tenant = await this.findOne(tenantId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!tenant) {
|
|
|
|
|
|
throw new NotFoundException('租户不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取租户分配的所有菜单ID
|
|
|
|
|
|
const tenantMenus = await this.prisma.tenantMenu.findMany({
|
|
|
|
|
|
where: { tenantId },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
menu: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const menuIds = tenantMenus.map((tm) => tm.menuId);
|
|
|
|
|
|
|
|
|
|
|
|
if (menuIds.length === 0) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有菜单(包括父菜单,因为子菜单可能被分配)
|
|
|
|
|
|
const allMenus = await this.prisma.menu.findMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
OR: [
|
|
|
|
|
|
{ id: { in: menuIds } },
|
|
|
|
|
|
{ children: { some: { id: { in: menuIds } } } },
|
|
|
|
|
|
],
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
orderBy: {
|
|
|
|
|
|
sort: 'asc',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 构建树形结构
|
|
|
|
|
|
const buildTree = (menus: any[], parentId: number | null = null): any[] => {
|
|
|
|
|
|
return menus
|
|
|
|
|
|
.filter((menu) => menu.parentId === parentId)
|
|
|
|
|
|
.map((menu) => ({
|
|
|
|
|
|
...menu,
|
|
|
|
|
|
children: buildTree(menus, menu.id),
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const menuTree = buildTree(allMenus);
|
|
|
|
|
|
|
|
|
|
|
|
// 过滤:只保留被分配的菜单及其父菜单
|
|
|
|
|
|
const filterMenus = (menus: any[]): any[] => {
|
|
|
|
|
|
return menus
|
|
|
|
|
|
.filter((menu) => {
|
|
|
|
|
|
// 如果菜单被分配,保留
|
|
|
|
|
|
if (menuIds.includes(menu.id)) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果有子菜单被分配,保留
|
|
|
|
|
|
if (menu.children && menu.children.length > 0) {
|
|
|
|
|
|
const filteredChildren = filterMenus(menu.children);
|
|
|
|
|
|
return filteredChildren.length > 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
})
|
|
|
|
|
|
.map((menu) => {
|
|
|
|
|
|
const filtered = { ...menu };
|
|
|
|
|
|
if (menu.children && menu.children.length > 0) {
|
|
|
|
|
|
filtered.children = filterMenus(menu.children);
|
|
|
|
|
|
}
|
|
|
|
|
|
return filtered;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return filterMenus(menuTree);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|