Files
AIclinicalresearch/backend/src/modules/admin/services/tenantService.ts
HaHafeng d595037316 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
2026-01-13 07:34:30 +08:00

308 lines
7.9 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 { 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();