Major Features: - Tenant management CRUD (list, create, edit, delete, module configuration) - Dynamic module management system (modules table with 8 modules) - Multi-tenant module permission merging (ModuleService) - Module access control middleware (requireModule) - User module permission API (GET /api/v1/auth/me/modules) - Frontend module permission filtering (HomePage + TopNavigation) Module Integration: - RVW module integrated with PromptService (editorial + methodology) - All modules (RVW/PKB/ASL/DC) added authenticate + requireModule middleware - Fixed ReviewTask foreign key constraint (cross-schema issue) - Removed all MOCK_USER_ID, unified to request.user?.userId Prompt Management Enhancements: - Module names displayed in Chinese (RVW -> 智能审稿) - Enhanced version history with view content and rollback features - List page shows both activeVersion and draftVersion columns Database Changes: - Added platform_schema.modules table - Modified tenant_modules table (added index and UUID) - Removed ReviewTask foreign key to public.users (cross-schema fix) - Seeded 8 modules: RVW, PKB, ASL, DC, IIT, AIA, SSA, ST Documentation Updates: - Updated ADMIN module development status - Updated TODO checklist (89% progress) - Updated Prompt management plan (Phase 3.5.5 completed) - Added module authentication specification Files Changed: 80+ Status: All features tested and verified locally Next: User management module development
308 lines
7.9 KiB
TypeScript
308 lines
7.9 KiB
TypeScript
/**
|
||
* 租户管理服务
|
||
*/
|
||
|
||
import { prisma } from '../../../config/database.js';
|
||
import { logger } from '../../../common/logging/index.js';
|
||
import { moduleService } from '../../../common/auth/module.service.js';
|
||
import type {
|
||
TenantInfo,
|
||
TenantDetail,
|
||
TenantModuleConfig,
|
||
CreateTenantRequest,
|
||
UpdateTenantRequest,
|
||
TenantListQuery,
|
||
PaginatedResponse,
|
||
TenantStatus,
|
||
} from '../types/tenant.types.js';
|
||
|
||
class TenantService {
|
||
/**
|
||
* 获取租户列表(分页)
|
||
*/
|
||
async listTenants(query: TenantListQuery): Promise<PaginatedResponse<TenantInfo>> {
|
||
const { type, status, search } = query;
|
||
const page = Number(query.page) || 1;
|
||
const limit = Number(query.limit) || 20;
|
||
const skip = (page - 1) * limit;
|
||
|
||
// 构建查询条件
|
||
const where: any = {};
|
||
if (type) where.type = type;
|
||
if (status) where.status = status;
|
||
if (search) {
|
||
where.OR = [
|
||
{ name: { contains: search, mode: 'insensitive' } },
|
||
{ code: { contains: search, mode: 'insensitive' } },
|
||
];
|
||
}
|
||
|
||
// 查询总数
|
||
const total = await prisma.tenants.count({ where });
|
||
|
||
// 查询数据
|
||
const tenants = await prisma.tenants.findMany({
|
||
where,
|
||
skip,
|
||
take: limit,
|
||
orderBy: { created_at: 'desc' },
|
||
select: {
|
||
id: true,
|
||
code: true,
|
||
name: true,
|
||
type: true,
|
||
status: true,
|
||
contact_name: true,
|
||
contact_phone: true,
|
||
contact_email: true,
|
||
expires_at: true,
|
||
created_at: true,
|
||
updated_at: true,
|
||
_count: {
|
||
select: { users: true },
|
||
},
|
||
},
|
||
});
|
||
|
||
return {
|
||
data: tenants.map(t => ({
|
||
id: t.id,
|
||
code: t.code,
|
||
name: t.name,
|
||
type: t.type as any,
|
||
status: t.status as any,
|
||
contactName: t.contact_name,
|
||
contactPhone: t.contact_phone,
|
||
contactEmail: t.contact_email,
|
||
expiresAt: t.expires_at,
|
||
createdAt: t.created_at,
|
||
updatedAt: t.updated_at,
|
||
})),
|
||
total,
|
||
page,
|
||
limit,
|
||
totalPages: Math.ceil(total / limit),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取租户详情(含模块配置)
|
||
*/
|
||
async getTenantDetail(tenantId: string): Promise<TenantDetail | null> {
|
||
const tenant = await prisma.tenants.findUnique({
|
||
where: { id: tenantId },
|
||
include: {
|
||
tenant_modules: true,
|
||
_count: {
|
||
select: { users: true },
|
||
},
|
||
},
|
||
});
|
||
|
||
if (!tenant) return null;
|
||
|
||
// 获取所有可用模块
|
||
const allModules = await moduleService.getAllModules();
|
||
|
||
// 构建模块配置列表
|
||
const modules: TenantModuleConfig[] = allModules.map(m => {
|
||
const tenantModule = tenant.tenant_modules.find(tm => tm.module_code === m.code);
|
||
return {
|
||
code: m.code,
|
||
name: m.name,
|
||
enabled: tenantModule?.is_enabled ?? false,
|
||
expiresAt: tenantModule?.expires_at,
|
||
};
|
||
});
|
||
|
||
return {
|
||
id: tenant.id,
|
||
code: tenant.code,
|
||
name: tenant.name,
|
||
type: tenant.type as any,
|
||
status: tenant.status as any,
|
||
contactName: tenant.contact_name,
|
||
contactPhone: tenant.contact_phone,
|
||
contactEmail: tenant.contact_email,
|
||
expiresAt: tenant.expires_at,
|
||
createdAt: tenant.created_at,
|
||
updatedAt: tenant.updated_at,
|
||
modules,
|
||
userCount: tenant._count.users,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 创建租户
|
||
*/
|
||
async createTenant(data: CreateTenantRequest): Promise<TenantInfo> {
|
||
const { randomUUID } = await import('crypto');
|
||
const tenantId = randomUUID();
|
||
|
||
// 检查 code 是否已存在
|
||
const existing = await prisma.tenants.findUnique({
|
||
where: { code: data.code },
|
||
});
|
||
if (existing) {
|
||
throw new Error(`租户代码 "${data.code}" 已存在`);
|
||
}
|
||
|
||
// 创建租户
|
||
const tenant = await prisma.tenants.create({
|
||
data: {
|
||
id: tenantId,
|
||
code: data.code,
|
||
name: data.name,
|
||
type: data.type as any,
|
||
status: 'ACTIVE',
|
||
contact_name: data.contactName,
|
||
contact_phone: data.contactPhone,
|
||
contact_email: data.contactEmail,
|
||
expires_at: data.expiresAt ? new Date(data.expiresAt) : null,
|
||
updated_at: new Date(),
|
||
},
|
||
});
|
||
|
||
// 如果指定了初始模块,创建模块配置
|
||
if (data.modules && data.modules.length > 0) {
|
||
for (const moduleCode of data.modules) {
|
||
await prisma.tenant_modules.create({
|
||
data: {
|
||
id: randomUUID(),
|
||
tenant_id: tenantId,
|
||
module_code: moduleCode,
|
||
is_enabled: true,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
|
||
logger.info('[TenantService] 创建租户', {
|
||
tenantId,
|
||
code: data.code,
|
||
name: data.name,
|
||
modules: data.modules,
|
||
});
|
||
|
||
return {
|
||
id: tenant.id,
|
||
code: tenant.code,
|
||
name: tenant.name,
|
||
type: tenant.type as any,
|
||
status: tenant.status as any,
|
||
contactName: tenant.contact_name,
|
||
contactPhone: tenant.contact_phone,
|
||
contactEmail: tenant.contact_email,
|
||
expiresAt: tenant.expires_at,
|
||
createdAt: tenant.created_at,
|
||
updatedAt: tenant.updated_at,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 更新租户信息
|
||
*/
|
||
async updateTenant(tenantId: string, data: UpdateTenantRequest): Promise<TenantInfo> {
|
||
const tenant = await prisma.tenants.update({
|
||
where: { id: tenantId },
|
||
data: {
|
||
name: data.name,
|
||
contact_name: data.contactName,
|
||
contact_phone: data.contactPhone,
|
||
contact_email: data.contactEmail,
|
||
expires_at: data.expiresAt === null ? null : (data.expiresAt ? new Date(data.expiresAt) : undefined),
|
||
updated_at: new Date(),
|
||
},
|
||
});
|
||
|
||
logger.info('[TenantService] 更新租户', { tenantId, data });
|
||
|
||
return {
|
||
id: tenant.id,
|
||
code: tenant.code,
|
||
name: tenant.name,
|
||
type: tenant.type as any,
|
||
status: tenant.status as any,
|
||
contactName: tenant.contact_name,
|
||
contactPhone: tenant.contact_phone,
|
||
contactEmail: tenant.contact_email,
|
||
expiresAt: tenant.expires_at,
|
||
createdAt: tenant.created_at,
|
||
updatedAt: tenant.updated_at,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 更新租户状态
|
||
*/
|
||
async updateTenantStatus(tenantId: string, status: TenantStatus): Promise<void> {
|
||
await prisma.tenants.update({
|
||
where: { id: tenantId },
|
||
data: {
|
||
status: status as any,
|
||
updated_at: new Date(),
|
||
},
|
||
});
|
||
|
||
logger.info('[TenantService] 更新租户状态', { tenantId, status });
|
||
}
|
||
|
||
/**
|
||
* 删除租户(软删除 - 标记为 SUSPENDED)
|
||
*/
|
||
async deleteTenant(tenantId: string): Promise<void> {
|
||
// 检查是否有用户
|
||
const userCount = await prisma.user.count({
|
||
where: { tenant_id: tenantId },
|
||
});
|
||
|
||
if (userCount > 0) {
|
||
throw new Error(`无法删除租户:该租户下还有 ${userCount} 个用户`);
|
||
}
|
||
|
||
// 软删除:标记为 SUSPENDED
|
||
await prisma.tenants.update({
|
||
where: { id: tenantId },
|
||
data: {
|
||
status: 'SUSPENDED',
|
||
updated_at: new Date(),
|
||
},
|
||
});
|
||
|
||
logger.info('[TenantService] 删除租户(软删除)', { tenantId });
|
||
}
|
||
|
||
/**
|
||
* 配置租户模块
|
||
*/
|
||
async configureModules(
|
||
tenantId: string,
|
||
modules: { code: string; enabled: boolean; expiresAt?: Date | null }[]
|
||
): Promise<void> {
|
||
await moduleService.setTenantModules(tenantId, modules);
|
||
}
|
||
|
||
/**
|
||
* 获取租户的模块配置
|
||
*/
|
||
async getTenantModules(tenantId: string): Promise<TenantModuleConfig[]> {
|
||
const allModules = await moduleService.getAllModules();
|
||
const tenantModules = await prisma.tenant_modules.findMany({
|
||
where: { tenant_id: tenantId },
|
||
});
|
||
|
||
return allModules.map(m => {
|
||
const tm = tenantModules.find(t => t.module_code === m.code);
|
||
return {
|
||
code: m.code,
|
||
name: m.name,
|
||
enabled: tm?.is_enabled ?? false,
|
||
expiresAt: tm?.expires_at,
|
||
};
|
||
});
|
||
}
|
||
}
|
||
|
||
export const tenantService = new TenantService();
|
||
|