Files
mp-10579/src/lib/tenant-port-manager.ts
赵忠林 4af50e6449 style(api): 统一代码格式化规范
- 调整 import 语句格式,统一空格和引号风格
- 修复函数参数跨行时的格式对齐问题
- 清理多余空行和注释中的空白字符
- 统一对象属性结尾逗号的使用规范
- 规范化字符串拼接和模板语法的格式
- 优化长参数列表的换行和缩进格式
2026-01-17 17:04:46 +08:00

446 lines
12 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 { 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<string, TenantPortBinding> {
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<string, TenantPortBinding>): 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<string, TenantPortBinding>;
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<PortAllocationResult> {
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<boolean> {
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<boolean> {
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<Environment, number>;
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<Environment, number>,
portRangeUsage: { min: Infinity, max: 0, average: 0 },
topTenants: [] as Array<{
tenantId: string | number;
bindingsCount: number;
}>
};
const tenantCounts = new Map<string | number, number>();
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();