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:
2026-01-13 07:34:30 +08:00
parent 5523ef36ea
commit d595037316
51 changed files with 3550 additions and 287 deletions

View 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();