library-picturebook-activity/backend/src/tenants/tenants.service.ts
aid 418aa57ea8 Day4: 超管端设计优化 + UGC绘本创作社区P0实现
一、超管端设计优化
- 文档管理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>
2026-03-27 22:20:25 +08:00

344 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}