一、超管端设计优化 - 文档管理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>
344 lines
8.3 KiB
TypeScript
344 lines
8.3 KiB
TypeScript
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);
|
||
}
|
||
}
|