- 调整 import 语句格式,统一空格和引号风格 - 修复函数参数跨行时的格式对齐问题 - 清理多余空行和注释中的空白字符 - 统一对象属性结尾逗号的使用规范 - 规范化字符串拼接和模板语法的格式 - 优化长参数列表的换行和缩进格式
446 lines
12 KiB
TypeScript
446 lines
12 KiB
TypeScript
/**
|
||
* 租户端口管理器
|
||
* 集成租户识别系统和端口管理系统
|
||
*/
|
||
|
||
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();
|