feat(admin): Complete tenant management and module access control system
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
This commit is contained in:
307
backend/src/modules/admin/services/tenantService.ts
Normal file
307
backend/src/modules/admin/services/tenantService.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 租户管理服务
|
||||
*/
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user