/** * 租户端口管理器 * 集成租户识别系统和端口管理系统 */ import { getTenantId } from '@/utils/domain'; import { getTenantInfo } from '@/api/layout'; import { PortManager, type PortConfig } from './port-manager'; import { portStrategyManager, EnvironmentDetector } from './port-strategy'; import type { Environment } from './port-strategy'; // 租户端口绑定配置 export interface TenantPortBinding { tenantId: string | number; tenantCode: string; environment: Environment; assignedPort: number; customDomain?: string; isActive: boolean; createdAt: number; lastUsed: number; metadata: { projectName: string; version: string; description?: string; }; } // 端口分配结果 export interface PortAllocationResult { success: boolean; port?: number; binding?: TenantPortBinding; error?: string; fallbackPorts?: number[]; recommendations?: string[]; } // 租户端口缓存管理 class TenantPortCache { private static readonly CACHE_KEY = 'tenant-port-bindings'; private static readonly CACHE_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7天 static get(): Map { try { const cached = localStorage.getItem(this.CACHE_KEY); if (!cached) return new Map(); const data = JSON.parse(cached); const now = Date.now(); // 清理过期缓存 const validEntries = Object.entries(data).filter( ([_, binding]: [string, any]) => now - binding.lastUsed < this.CACHE_EXPIRY ); return new Map(validEntries); } catch (error) { console.warn('租户端口缓存读取失败:', error); return new Map(); } } static set(cache: Map): void { try { const data = Object.fromEntries(cache); localStorage.setItem(this.CACHE_KEY, JSON.stringify(data)); } catch (error) { console.warn('租户端口缓存保存失败:', error); } } static clear(): void { localStorage.removeItem(this.CACHE_KEY); } static generateKey( tenantId: string | number, environment: Environment ): string { return `${tenantId}-${environment}`; } } // 租户端口管理器 export class TenantPortManager { private portManager: PortManager; private bindings: Map; private currentEnvironment: Environment; constructor() { this.portManager = new PortManager(); this.bindings = TenantPortCache.get(); this.currentEnvironment = EnvironmentDetector.detectEnvironment(); console.log('🏢 租户端口管理器初始化完成', { environment: this.currentEnvironment, bindingsCount: this.bindings.size }); } /** * 为租户分配端口(主要方法) */ async allocatePortForTenant(options?: { tenantId?: string | number; preferredPort?: number; forceNew?: boolean; }): Promise { try { // 1. 获取租户信息 const tenantId = options?.tenantId || (await getTenantId()); const tenantInfo = await getTenantInfo(); if (!tenantId) { return { success: false, error: '无法获取租户ID', recommendations: ['请检查租户配置', '确保已正确设置租户识别'] }; } // 2. 检查现有绑定 const bindingKey = TenantPortCache.generateKey( tenantId, this.currentEnvironment ); const existingBinding = this.bindings.get(bindingKey); if (existingBinding && !options?.forceNew) { // 验证现有端口是否仍然可用 if (await this.validatePortBinding(existingBinding)) { existingBinding.lastUsed = Date.now(); this.updateBinding(bindingKey, existingBinding); console.log('📋 使用现有租户端口绑定:', existingBinding.assignedPort); return { success: true, port: existingBinding.assignedPort, binding: existingBinding }; } else { console.warn('⚠️ 现有端口绑定已失效,重新分配'); this.bindings.delete(bindingKey); } } // 3. 分配新端口 const portConfig = await this.portManager.getRecommendedPort({ tenantId, projectName: tenantInfo?.name || 'mp-vue', preferredPort: options?.preferredPort }); // 4. 创建租户端口绑定 const binding = this.createTenantBinding( tenantId, tenantInfo, portConfig ); this.updateBinding(bindingKey, binding); console.log('🎯 为租户分配新端口:', { tenantId, port: binding.assignedPort, environment: this.currentEnvironment }); return { success: true, port: binding.assignedPort, binding, recommendations: this.generateRecommendations(binding) }; } catch (error) { console.error('❌ 租户端口分配失败:', error); return { success: false, error: error instanceof Error ? error.message : '未知错误', recommendations: ['检查网络连接', '验证租户配置', '尝试重新启动服务'] }; } } /** * 验证端口绑定是否有效 */ private async validatePortBinding( binding: TenantPortBinding ): Promise { try { // 检查端口是否仍然可用 const response = await fetch(`http://localhost:${binding.assignedPort}`, { method: 'HEAD', mode: 'no-cors', signal: AbortSignal.timeout(2000) }); // 如果能连接,说明端口被占用(可能是我们自己的服务) return true; } catch (error) { // 连接失败,端口可能已释放 return false; } } /** * 创建租户端口绑定 */ private createTenantBinding( tenantId: string | number, tenantInfo: any, portConfig: PortConfig ): TenantPortBinding { return { tenantId, tenantCode: tenantInfo?.code || String(tenantId), environment: this.currentEnvironment, assignedPort: portConfig.port, customDomain: tenantInfo?.domain, isActive: true, createdAt: Date.now(), lastUsed: Date.now(), metadata: { projectName: portConfig.projectName || 'mp-vue', version: '1.0.0', description: `${tenantInfo?.name || '租户'} - ${ this.currentEnvironment }环境` } }; } /** * 更新绑定缓存 */ private updateBinding(key: string, binding: TenantPortBinding): void { this.bindings.set(key, binding); TenantPortCache.set(this.bindings); } /** * 生成建议 */ private generateRecommendations(binding: TenantPortBinding): string[] { const recommendations: string[] = []; const strategy = portStrategyManager.getCurrentStrategy(); // 环境特定建议 switch (binding.environment) { case 'development': recommendations.push('开发环境:建议配置热重载和调试工具'); recommendations.push( `访问地址:http://localhost:${binding.assignedPort}` ); break; case 'test': recommendations.push('测试环境:建议配置自动化测试和监控'); break; case 'production': recommendations.push('生产环境:建议配置HTTPS和负载均衡'); if (binding.customDomain) { recommendations.push(`自定义域名:${binding.customDomain}`); } break; } // 端口范围建议 if ( binding.assignedPort < strategy.portRange[0] || binding.assignedPort > strategy.portRange[1] ) { recommendations.push('⚠️ 分配的端口超出推荐范围,可能存在冲突风险'); } return recommendations; } /** * 获取租户端口信息 */ async getTenantPortInfo(tenantId?: string | number): Promise<{ current?: TenantPortBinding; history: TenantPortBinding[]; recommendations: string[]; }> { const targetTenantId = tenantId || (await getTenantId()); const history: TenantPortBinding[] = []; let current: TenantPortBinding | undefined; // 查找当前和历史绑定 this.bindings.forEach((binding) => { if (binding.tenantId === targetTenantId) { if ( binding.environment === this.currentEnvironment && binding.isActive ) { current = binding; } history.push(binding); } }); // 按时间排序 history.sort((a, b) => b.lastUsed - a.lastUsed); const recommendations = current ? this.generateRecommendations(current) : ['当前环境暂无端口绑定,建议调用 allocatePortForTenant 分配端口']; return { current, history, recommendations }; } /** * 释放租户端口 */ async releaseTenantPort(tenantId?: string | number): Promise { try { const targetTenantId = tenantId || (await getTenantId()); const bindingKey = TenantPortCache.generateKey( targetTenantId, this.currentEnvironment ); const binding = this.bindings.get(bindingKey); if (binding) { binding.isActive = false; this.updateBinding(bindingKey, binding); console.log( `🔓 已释放租户 ${targetTenantId} 的端口 ${binding.assignedPort}` ); return true; } return false; } catch (error) { console.error('释放租户端口失败:', error); return false; } } /** * 获取所有租户端口统计 */ getAllTenantsPortStats(): { totalBindings: number; activeBindings: number; environmentStats: Record; portRangeUsage: { min: number; max: number; average: number }; topTenants: Array<{ tenantId: string | number; bindingsCount: number }>; } { const stats = { totalBindings: this.bindings.size, activeBindings: 0, environmentStats: {} as Record, portRangeUsage: { min: Infinity, max: 0, average: 0 }, topTenants: [] as Array<{ tenantId: string | number; bindingsCount: number; }> }; const tenantCounts = new Map(); let portSum = 0; this.bindings.forEach((binding) => { // 活跃绑定统计 if (binding.isActive) { stats.activeBindings++; } // 环境统计 stats.environmentStats[binding.environment] = (stats.environmentStats[binding.environment] || 0) + 1; // 端口范围统计 stats.portRangeUsage.min = Math.min( stats.portRangeUsage.min, binding.assignedPort ); stats.portRangeUsage.max = Math.max( stats.portRangeUsage.max, binding.assignedPort ); portSum += binding.assignedPort; // 租户统计 const count = tenantCounts.get(binding.tenantId) || 0; tenantCounts.set(binding.tenantId, count + 1); }); // 计算平均端口 stats.portRangeUsage.average = stats.totalBindings > 0 ? Math.round(portSum / stats.totalBindings) : 0; // 修复无限大的情况 if (stats.portRangeUsage.min === Infinity) { stats.portRangeUsage.min = 0; } // 排序租户使用量 stats.topTenants = Array.from(tenantCounts.entries()) .map(([tenantId, count]) => ({ tenantId, bindingsCount: count })) .sort((a, b) => b.bindingsCount - a.bindingsCount) .slice(0, 10); return stats; } /** * 清理过期绑定 */ cleanupExpiredBindings(): number { const now = Date.now(); const expiry = 7 * 24 * 60 * 60 * 1000; // 7天 let cleaned = 0; this.bindings.forEach((binding, key) => { if (now - binding.lastUsed > expiry) { this.bindings.delete(key); cleaned++; } }); if (cleaned > 0) { TenantPortCache.set(this.bindings); console.log(`🧹 清理了 ${cleaned} 个过期的租户端口绑定`); } return cleaned; } /** * 重置所有绑定 */ resetAllBindings(): void { this.bindings.clear(); TenantPortCache.clear(); console.log('🔄 所有租户端口绑定已重置'); } } // 导出默认实例 export const tenantPortManager = new TenantPortManager();