1
This commit is contained in:
373
src/lib/port-manager.ts
Normal file
373
src/lib/port-manager.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* 智能端口管理系统
|
||||
* 类似租户识别系统的端口管理解决方案
|
||||
*/
|
||||
|
||||
import { getTenantId } from '@/utils/domain';
|
||||
|
||||
// 端口配置接口
|
||||
export interface PortConfig {
|
||||
port: number;
|
||||
host: string;
|
||||
protocol: 'http' | 'https';
|
||||
environment: 'development' | 'test' | 'production';
|
||||
tenantId?: string | number;
|
||||
projectName?: string;
|
||||
lastUsed: number;
|
||||
isAvailable: boolean;
|
||||
}
|
||||
|
||||
// 端口分配策略
|
||||
export interface PortStrategy {
|
||||
basePort: number;
|
||||
portRange: [number, number];
|
||||
tenantOffset: number;
|
||||
environmentOffset: number;
|
||||
maxRetries: number;
|
||||
}
|
||||
|
||||
// 端口缓存管理
|
||||
class PortCache {
|
||||
private static readonly CACHE_KEY = 'port-manager-cache';
|
||||
private static readonly CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24小时
|
||||
|
||||
static get(): Map<string, PortConfig> {
|
||||
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(
|
||||
([_, config]: [string, any]) =>
|
||||
now - config.lastUsed < this.CACHE_EXPIRY
|
||||
);
|
||||
|
||||
return new Map(validEntries);
|
||||
} catch (error) {
|
||||
console.warn('端口缓存读取失败:', error);
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
static set(cache: Map<string, PortConfig>): 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 getStats(): { total: number; expired: number; active: number } {
|
||||
const cache = this.get();
|
||||
const now = Date.now();
|
||||
let expired = 0;
|
||||
let active = 0;
|
||||
|
||||
cache.forEach((config) => {
|
||||
if (now - config.lastUsed > this.CACHE_EXPIRY) {
|
||||
expired++;
|
||||
} else {
|
||||
active++;
|
||||
}
|
||||
});
|
||||
|
||||
return { total: cache.size, expired, active };
|
||||
}
|
||||
}
|
||||
|
||||
// 端口工具函数
|
||||
class PortUtils {
|
||||
/**
|
||||
* 检查端口是否可用
|
||||
*/
|
||||
static async isPortAvailable(
|
||||
port: number,
|
||||
host = 'localhost'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// 在浏览器环境中,我们无法直接检测端口占用
|
||||
// 这里使用一个模拟的检测方法
|
||||
const response = await fetch(`http://${host}:${port}`, {
|
||||
method: 'HEAD',
|
||||
mode: 'no-cors',
|
||||
signal: AbortSignal.timeout(1000)
|
||||
});
|
||||
|
||||
// 如果能连接到端口,说明端口被占用
|
||||
return false;
|
||||
} catch (error) {
|
||||
// 连接失败,说明端口可用
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取端口范围内的可用端口
|
||||
*/
|
||||
static async findAvailablePort(
|
||||
startPort: number,
|
||||
endPort: number,
|
||||
host = 'localhost'
|
||||
): Promise<number | null> {
|
||||
for (let port = startPort; port <= endPort; port++) {
|
||||
if (await this.isPortAvailable(port, host)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成端口键
|
||||
*/
|
||||
static generatePortKey(
|
||||
tenantId: string | number,
|
||||
environment: string,
|
||||
projectName?: string
|
||||
): string {
|
||||
const parts = [tenantId, environment];
|
||||
if (projectName) parts.push(projectName);
|
||||
return parts.join('-');
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算租户端口偏移
|
||||
*/
|
||||
static calculateTenantOffset(tenantId: string | number): number {
|
||||
const id =
|
||||
typeof tenantId === 'string' ? parseInt(tenantId) || 0 : tenantId;
|
||||
return (id % 1000) * 10; // 每个租户分配10个端口的空间
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算环境端口偏移
|
||||
*/
|
||||
static calculateEnvironmentOffset(environment: string): number {
|
||||
const offsets = {
|
||||
development: 0,
|
||||
test: 1000,
|
||||
production: 2000
|
||||
};
|
||||
return offsets[environment as keyof typeof offsets] || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 智能端口管理器
|
||||
export class PortManager {
|
||||
private cache: Map<string, PortConfig>;
|
||||
private strategy: PortStrategy;
|
||||
private environment: string;
|
||||
|
||||
constructor(strategy?: Partial<PortStrategy>) {
|
||||
this.environment = process.env.NODE_ENV || 'development';
|
||||
this.cache = PortCache.get();
|
||||
|
||||
// 默认策略
|
||||
this.strategy = {
|
||||
basePort: 3000,
|
||||
portRange: [3000, 9999],
|
||||
tenantOffset: 10,
|
||||
environmentOffset: 1000,
|
||||
maxRetries: 50,
|
||||
...strategy
|
||||
};
|
||||
|
||||
console.log('🚀 端口管理器初始化完成', {
|
||||
environment: this.environment,
|
||||
strategy: this.strategy,
|
||||
cacheSize: this.cache.size
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推荐端口(智能分配)
|
||||
*/
|
||||
async getRecommendedPort(options?: {
|
||||
tenantId?: string | number;
|
||||
projectName?: string;
|
||||
preferredPort?: number;
|
||||
}): Promise<PortConfig> {
|
||||
const tenantId = options?.tenantId || (await getTenantId());
|
||||
const projectName = options?.projectName || 'paopao-vue';
|
||||
const portKey = PortUtils.generatePortKey(
|
||||
tenantId,
|
||||
this.environment,
|
||||
projectName
|
||||
);
|
||||
|
||||
// 1. 检查缓存中的端口
|
||||
const cachedPort = this.cache.get(portKey);
|
||||
if (cachedPort && (await PortUtils.isPortAvailable(cachedPort.port))) {
|
||||
cachedPort.lastUsed = Date.now();
|
||||
this.updateCache(portKey, cachedPort);
|
||||
console.log('📋 使用缓存端口:', cachedPort.port);
|
||||
return cachedPort;
|
||||
}
|
||||
|
||||
// 2. 尝试首选端口
|
||||
if (options?.preferredPort) {
|
||||
if (await PortUtils.isPortAvailable(options.preferredPort)) {
|
||||
const config = this.createPortConfig(
|
||||
options.preferredPort,
|
||||
tenantId,
|
||||
projectName
|
||||
);
|
||||
this.updateCache(portKey, config);
|
||||
console.log('✨ 使用首选端口:', options.preferredPort);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 智能分配端口
|
||||
const recommendedPort = await this.allocateSmartPort(tenantId, projectName);
|
||||
const config = this.createPortConfig(
|
||||
recommendedPort,
|
||||
tenantId,
|
||||
projectName
|
||||
);
|
||||
this.updateCache(portKey, config);
|
||||
|
||||
console.log('🎯 智能分配端口:', recommendedPort);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能端口分配算法
|
||||
*/
|
||||
private async allocateSmartPort(
|
||||
tenantId: string | number,
|
||||
projectName: string
|
||||
): Promise<number> {
|
||||
const tenantOffset = PortUtils.calculateTenantOffset(tenantId);
|
||||
const envOffset = PortUtils.calculateEnvironmentOffset(this.environment);
|
||||
|
||||
// 计算推荐端口
|
||||
const recommendedPort = this.strategy.basePort + envOffset + tenantOffset;
|
||||
|
||||
// 在推荐端口附近查找可用端口
|
||||
const searchRange = 20; // 在推荐端口前后20个端口范围内搜索
|
||||
const startPort = Math.max(
|
||||
recommendedPort - searchRange,
|
||||
this.strategy.portRange[0]
|
||||
);
|
||||
const endPort = Math.min(
|
||||
recommendedPort + searchRange,
|
||||
this.strategy.portRange[1]
|
||||
);
|
||||
|
||||
// 优先尝试推荐端口
|
||||
if (await PortUtils.isPortAvailable(recommendedPort)) {
|
||||
return recommendedPort;
|
||||
}
|
||||
|
||||
// 在范围内查找可用端口
|
||||
const availablePort = await PortUtils.findAvailablePort(startPort, endPort);
|
||||
if (availablePort) {
|
||||
return availablePort;
|
||||
}
|
||||
|
||||
// 如果推荐范围内没有可用端口,扩大搜索范围
|
||||
const fallbackPort = await PortUtils.findAvailablePort(
|
||||
this.strategy.portRange[0],
|
||||
this.strategy.portRange[1]
|
||||
);
|
||||
|
||||
if (fallbackPort) {
|
||||
return fallbackPort;
|
||||
}
|
||||
|
||||
// 最后的备选方案
|
||||
throw new Error(
|
||||
`无法在端口范围 ${this.strategy.portRange[0]}-${this.strategy.portRange[1]} 内找到可用端口`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建端口配置
|
||||
*/
|
||||
private createPortConfig(
|
||||
port: number,
|
||||
tenantId: string | number,
|
||||
projectName: string
|
||||
): PortConfig {
|
||||
return {
|
||||
port,
|
||||
host: 'localhost',
|
||||
protocol: 'http',
|
||||
environment: this.environment as any,
|
||||
tenantId,
|
||||
projectName,
|
||||
lastUsed: Date.now(),
|
||||
isAvailable: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存
|
||||
*/
|
||||
private updateCache(key: string, config: PortConfig): void {
|
||||
this.cache.set(key, config);
|
||||
PortCache.set(this.cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取端口使用统计
|
||||
*/
|
||||
getPortStats(): {
|
||||
cacheStats: ReturnType<typeof PortCache.getStats>;
|
||||
currentPorts: PortConfig[];
|
||||
strategy: PortStrategy;
|
||||
} {
|
||||
return {
|
||||
cacheStats: PortCache.getStats(),
|
||||
currentPorts: Array.from(this.cache.values()),
|
||||
strategy: this.strategy
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期端口缓存
|
||||
*/
|
||||
cleanupExpiredPorts(): number {
|
||||
const now = Date.now();
|
||||
const expiry = 24 * 60 * 60 * 1000; // 24小时
|
||||
let cleaned = 0;
|
||||
|
||||
this.cache.forEach((config, key) => {
|
||||
if (now - config.lastUsed > expiry) {
|
||||
this.cache.delete(key);
|
||||
cleaned++;
|
||||
}
|
||||
});
|
||||
|
||||
if (cleaned > 0) {
|
||||
PortCache.set(this.cache);
|
||||
console.log(`🧹 清理了 ${cleaned} 个过期端口缓存`);
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置端口缓存
|
||||
*/
|
||||
resetCache(): void {
|
||||
this.cache.clear();
|
||||
PortCache.clear();
|
||||
console.log('🔄 端口缓存已重置');
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认实例
|
||||
export const portManager = new PortManager();
|
||||
|
||||
// 导出工具函数
|
||||
export { PortUtils, PortCache };
|
||||
431
src/lib/port-strategy.ts
Normal file
431
src/lib/port-strategy.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* 环境端口策略配置
|
||||
* 类似租户识别系统的环境优先级策略
|
||||
*/
|
||||
|
||||
import type { PortStrategy } from './port-manager';
|
||||
|
||||
// 环境类型
|
||||
export type Environment = 'development' | 'test' | 'staging' | 'production';
|
||||
|
||||
// 端口策略优先级
|
||||
export interface PortPriority {
|
||||
environment: Environment;
|
||||
priority: number; // 数字越小优先级越高
|
||||
description: string;
|
||||
}
|
||||
|
||||
// 环境端口策略配置
|
||||
export interface EnvironmentPortStrategy extends PortStrategy {
|
||||
environment: Environment;
|
||||
priority: number;
|
||||
autoDetect: boolean;
|
||||
fallbackStrategy?: EnvironmentPortStrategy;
|
||||
restrictions: {
|
||||
allowedHosts: string[];
|
||||
blockedPorts: number[];
|
||||
requireHttps: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// 端口分配模式
|
||||
export enum PortAllocationMode {
|
||||
TENANT_BASED = 'tenant-based', // 基于租户分配
|
||||
SEQUENTIAL = 'sequential', // 顺序分配
|
||||
RANDOM = 'random', // 随机分配
|
||||
HASH_BASED = 'hash-based' // 基于哈希分配
|
||||
}
|
||||
|
||||
// 环境检测器
|
||||
export class EnvironmentDetector {
|
||||
/**
|
||||
* 检测当前环境
|
||||
*/
|
||||
static detectEnvironment(): Environment {
|
||||
// 1. 检查环境变量
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
if (nodeEnv) {
|
||||
switch (nodeEnv.toLowerCase()) {
|
||||
case 'development':
|
||||
case 'dev':
|
||||
return 'development';
|
||||
case 'test':
|
||||
case 'testing':
|
||||
return 'test';
|
||||
case 'staging':
|
||||
case 'stage':
|
||||
return 'staging';
|
||||
case 'production':
|
||||
case 'prod':
|
||||
return 'production';
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查域名
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {
|
||||
return 'development';
|
||||
}
|
||||
if (hostname.includes('test') || hostname.includes('staging')) {
|
||||
return 'test';
|
||||
}
|
||||
if (hostname.includes('prod') || hostname.includes('www')) {
|
||||
return 'production';
|
||||
}
|
||||
|
||||
// 3. 检查端口
|
||||
const port = window.location.port;
|
||||
if (port && parseInt(port) < 4000) {
|
||||
return 'development';
|
||||
}
|
||||
|
||||
// 默认返回开发环境
|
||||
return 'development';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取环境建议
|
||||
*/
|
||||
static getEnvironmentRecommendation(): {
|
||||
detected: Environment;
|
||||
confidence: number;
|
||||
reasons: string[];
|
||||
suggestions: string[];
|
||||
} {
|
||||
const reasons: string[] = [];
|
||||
const suggestions: string[] = [];
|
||||
let confidence = 0;
|
||||
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
const hostname = window.location.hostname;
|
||||
const port = window.location.port;
|
||||
const protocol = window.location.protocol;
|
||||
|
||||
// 分析环境变量
|
||||
if (nodeEnv) {
|
||||
reasons.push(`NODE_ENV: ${nodeEnv}`);
|
||||
confidence += 40;
|
||||
} else {
|
||||
suggestions.push('建议设置 NODE_ENV 环境变量');
|
||||
}
|
||||
|
||||
// 分析域名
|
||||
if (hostname.includes('localhost')) {
|
||||
reasons.push('域名包含 localhost');
|
||||
confidence += 30;
|
||||
} else if (hostname.includes('test')) {
|
||||
reasons.push('域名包含 test');
|
||||
confidence += 25;
|
||||
} else if (hostname.includes('prod')) {
|
||||
reasons.push('域名包含 prod');
|
||||
confidence += 35;
|
||||
}
|
||||
|
||||
// 分析协议
|
||||
if (protocol === 'https:') {
|
||||
reasons.push('使用 HTTPS 协议');
|
||||
confidence += 10;
|
||||
} else {
|
||||
suggestions.push('生产环境建议使用 HTTPS');
|
||||
}
|
||||
|
||||
// 分析端口
|
||||
if (port) {
|
||||
const portNum = parseInt(port);
|
||||
if (portNum >= 3000 && portNum < 4000) {
|
||||
reasons.push('使用开发端口范围');
|
||||
confidence += 15;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
detected: this.detectEnvironment(),
|
||||
confidence: Math.min(confidence, 100),
|
||||
reasons,
|
||||
suggestions
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 端口策略管理器
|
||||
export class PortStrategyManager {
|
||||
private strategies: Map<Environment, EnvironmentPortStrategy>;
|
||||
private currentEnvironment: Environment;
|
||||
|
||||
constructor() {
|
||||
this.currentEnvironment = EnvironmentDetector.detectEnvironment();
|
||||
this.strategies = new Map();
|
||||
this.initializeDefaultStrategies();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认策略
|
||||
*/
|
||||
private initializeDefaultStrategies(): void {
|
||||
// 开发环境策略
|
||||
this.strategies.set('development', {
|
||||
environment: 'development',
|
||||
priority: 1,
|
||||
basePort: 3000,
|
||||
portRange: [3000, 3999],
|
||||
tenantOffset: 10,
|
||||
environmentOffset: 0,
|
||||
maxRetries: 50,
|
||||
autoDetect: true,
|
||||
restrictions: {
|
||||
allowedHosts: ['localhost', '127.0.0.1', '0.0.0.0'],
|
||||
blockedPorts: [],
|
||||
requireHttps: false
|
||||
}
|
||||
});
|
||||
|
||||
// 测试环境策略
|
||||
this.strategies.set('test', {
|
||||
environment: 'test',
|
||||
priority: 2,
|
||||
basePort: 4000,
|
||||
portRange: [4000, 4999],
|
||||
tenantOffset: 5,
|
||||
environmentOffset: 1000,
|
||||
maxRetries: 30,
|
||||
autoDetect: true,
|
||||
restrictions: {
|
||||
allowedHosts: ['localhost', '127.0.0.1', 'test.local'],
|
||||
blockedPorts: [4444, 4567], // 避免与其他测试工具冲突
|
||||
requireHttps: false
|
||||
}
|
||||
});
|
||||
|
||||
// 预发布环境策略
|
||||
this.strategies.set('staging', {
|
||||
environment: 'staging',
|
||||
priority: 3,
|
||||
basePort: 5000,
|
||||
portRange: [5000, 5999],
|
||||
tenantOffset: 3,
|
||||
environmentOffset: 2000,
|
||||
maxRetries: 20,
|
||||
autoDetect: true,
|
||||
restrictions: {
|
||||
allowedHosts: ['staging.local', 'stage.example.com'],
|
||||
blockedPorts: [],
|
||||
requireHttps: true
|
||||
}
|
||||
});
|
||||
|
||||
// 生产环境策略
|
||||
this.strategies.set('production', {
|
||||
environment: 'production',
|
||||
priority: 4,
|
||||
basePort: 8080,
|
||||
portRange: [8080, 8999],
|
||||
tenantOffset: 1,
|
||||
environmentOffset: 5000,
|
||||
maxRetries: 10,
|
||||
autoDetect: false, // 生产环境不自动检测
|
||||
restrictions: {
|
||||
allowedHosts: ['0.0.0.0'], // 生产环境通常绑定所有接口
|
||||
blockedPorts: [8080, 8443], // 避免与常用服务冲突
|
||||
requireHttps: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前环境策略
|
||||
*/
|
||||
getCurrentStrategy(): EnvironmentPortStrategy {
|
||||
const strategy = this.strategies.get(this.currentEnvironment);
|
||||
if (!strategy) {
|
||||
console.warn(
|
||||
`未找到环境 ${this.currentEnvironment} 的策略,使用开发环境策略`
|
||||
);
|
||||
return this.strategies.get('development')!;
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定环境策略
|
||||
*/
|
||||
getStrategy(environment: Environment): EnvironmentPortStrategy | undefined {
|
||||
return this.strategies.get(environment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置环境策略
|
||||
*/
|
||||
setStrategy(
|
||||
environment: Environment,
|
||||
strategy: EnvironmentPortStrategy
|
||||
): void {
|
||||
this.strategies.set(environment, strategy);
|
||||
console.log(`✅ 已更新 ${environment} 环境的端口策略`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推荐策略(基于环境优先级)
|
||||
*/
|
||||
getRecommendedStrategy(): {
|
||||
primary: EnvironmentPortStrategy;
|
||||
fallback: EnvironmentPortStrategy[];
|
||||
reasoning: string[];
|
||||
} {
|
||||
const current = this.getCurrentStrategy();
|
||||
const reasoning: string[] = [];
|
||||
const fallback: EnvironmentPortStrategy[] = [];
|
||||
|
||||
reasoning.push(`当前环境: ${this.currentEnvironment}`);
|
||||
reasoning.push(`优先级: ${current.priority}`);
|
||||
|
||||
// 获取备选策略(按优先级排序)
|
||||
const allStrategies = Array.from(this.strategies.values())
|
||||
.filter((s) => s.environment !== this.currentEnvironment)
|
||||
.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
fallback.push(...allStrategies);
|
||||
|
||||
// 环境特定的推理
|
||||
switch (this.currentEnvironment) {
|
||||
case 'development':
|
||||
reasoning.push('开发环境优先考虑端口可用性和调试便利性');
|
||||
break;
|
||||
case 'test':
|
||||
reasoning.push('测试环境需要隔离性和可重复性');
|
||||
break;
|
||||
case 'staging':
|
||||
reasoning.push('预发布环境模拟生产环境配置');
|
||||
break;
|
||||
case 'production':
|
||||
reasoning.push('生产环境优先考虑安全性和稳定性');
|
||||
break;
|
||||
}
|
||||
|
||||
return { primary: current, fallback, reasoning };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证端口策略
|
||||
*/
|
||||
validateStrategy(strategy: EnvironmentPortStrategy): {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// 检查端口范围
|
||||
if (strategy.portRange[0] >= strategy.portRange[1]) {
|
||||
errors.push('端口范围无效:起始端口必须小于结束端口');
|
||||
}
|
||||
|
||||
if (strategy.portRange[0] < 1024 && strategy.environment === 'production') {
|
||||
warnings.push('生产环境使用系统端口(<1024)可能需要管理员权限');
|
||||
}
|
||||
|
||||
// 检查基础端口
|
||||
if (
|
||||
strategy.basePort < strategy.portRange[0] ||
|
||||
strategy.basePort > strategy.portRange[1]
|
||||
) {
|
||||
errors.push('基础端口不在允许的端口范围内');
|
||||
}
|
||||
|
||||
// 检查租户偏移
|
||||
if (strategy.tenantOffset <= 0) {
|
||||
warnings.push('租户偏移为0可能导致端口冲突');
|
||||
}
|
||||
|
||||
// 检查环境特定规则
|
||||
if (
|
||||
strategy.environment === 'production' &&
|
||||
!strategy.restrictions.requireHttps
|
||||
) {
|
||||
warnings.push('生产环境建议启用 HTTPS');
|
||||
}
|
||||
|
||||
if (
|
||||
strategy.environment === 'development' &&
|
||||
strategy.restrictions.requireHttps
|
||||
) {
|
||||
warnings.push('开发环境启用 HTTPS 可能增加配置复杂度');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取环境统计信息
|
||||
*/
|
||||
getEnvironmentStats(): {
|
||||
current: Environment;
|
||||
available: Environment[];
|
||||
strategies: Array<{
|
||||
environment: Environment;
|
||||
priority: number;
|
||||
portRange: [number, number];
|
||||
isValid: boolean;
|
||||
}>;
|
||||
} {
|
||||
const strategies = Array.from(this.strategies.entries()).map(
|
||||
([env, strategy]) => ({
|
||||
environment: env,
|
||||
priority: strategy.priority,
|
||||
portRange: strategy.portRange,
|
||||
isValid: this.validateStrategy(strategy).isValid
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
current: this.currentEnvironment,
|
||||
available: Array.from(this.strategies.keys()),
|
||||
strategies: strategies.sort((a, b) => a.priority - b.priority)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换环境
|
||||
*/
|
||||
switchEnvironment(environment: Environment): boolean {
|
||||
if (!this.strategies.has(environment)) {
|
||||
console.error(`环境 ${environment} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.currentEnvironment = environment;
|
||||
console.log(`🔄 已切换到 ${environment} 环境`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认实例
|
||||
export const portStrategyManager = new PortStrategyManager();
|
||||
|
||||
// 导出环境优先级配置
|
||||
export const ENVIRONMENT_PRIORITIES: PortPriority[] = [
|
||||
{
|
||||
environment: 'development',
|
||||
priority: 1,
|
||||
description: '开发环境 - 最高优先级,注重便利性'
|
||||
},
|
||||
{
|
||||
environment: 'test',
|
||||
priority: 2,
|
||||
description: '测试环境 - 高优先级,注重隔离性'
|
||||
},
|
||||
{
|
||||
environment: 'staging',
|
||||
priority: 3,
|
||||
description: '预发布环境 - 中等优先级,模拟生产'
|
||||
},
|
||||
{
|
||||
environment: 'production',
|
||||
priority: 4,
|
||||
description: '生产环境 - 最低优先级,注重安全性'
|
||||
}
|
||||
];
|
||||
445
src/lib/tenant-port-manager.ts
Normal file
445
src/lib/tenant-port-manager.ts
Normal file
@@ -0,0 +1,445 @@
|
||||
/**
|
||||
* 租户端口管理器
|
||||
* 集成租户识别系统和端口管理系统
|
||||
*/
|
||||
|
||||
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 || 'paopao-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 || 'paopao-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();
|
||||
Reference in New Issue
Block a user