From 7052ccce61a3257b701485ec88cacc9ccd1dcf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 3 Sep 2025 18:52:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(port):=20=E5=AE=9E=E7=8E=B0=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E7=AB=AF=E5=8F=A3=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增端口管理器类,支持端口分配、验证和缓存管理 - 实现环境优先级策略,根据环境自动选择合适的端口范围 - 集成租户识别系统,为每个租户分配独立端口 - 添加端口分配结果统计和历史记录查询功能 - 优化端口缓存机制,自动清理过期绑定 --- .env.development | 2 +- .env.example | 18 + src/api/shop/shopDealerUser/index.ts | 35 + src/api/shop/shopDealerUser/model/index.ts | 7 + src/composables/useSiteData.ts | 12 +- src/lib/port-manager.ts | 355 ++++++++++ src/lib/port-strategy.ts | 415 ++++++++++++ src/lib/tenant-port-manager.ts | 411 ++++++++++++ src/utils/port-config-manager.ts | 357 ++++++++++ .../sdy/sdyUser/components/org-select.vue | 39 ++ src/views/sdy/sdyUser/components/search.vue | 42 ++ .../sdy/sdyUser/components/status-test.vue | 65 ++ .../sdy/sdyUser/components/user-edit.vue | 312 +++++++++ .../sdy/sdyUser/components/user-import.vue | 88 +++ src/views/sdy/sdyUser/index.vue | 603 +++++++++++++++++ .../sdy/shopDealerApply/components/search.vue | 219 +++++++ .../components/shopDealerApplyEdit.vue | 407 ++++++++++++ src/views/sdy/shopDealerApply/index.vue | 494 ++++++++++++++ src/views/sdy/shopDealerUser/README.md | 100 +++ .../sdy/shopDealerUser/components/Import.vue | 110 ++++ .../sdy/shopDealerUser/components/search.vue | 206 ++++++ .../components/shopDealerUserEdit.vue | 516 +++++++++++++++ src/views/sdy/shopDealerUser/index.vue | 364 +++++++++++ src/views/shop/shopDealerUser/README.md | 100 +++ .../shop/shopDealerUser/components/Import.vue | 110 ++++ .../shop/shopDealerUser/components/search.vue | 212 +++++- src/views/shop/shopDealerUser/index.vue | 5 +- .../shop/shopUser/components/org-select.vue | 39 ++ src/views/shop/shopUser/components/search.vue | 42 ++ .../shop/shopUser/components/user-edit.vue | 278 ++++++++ .../shop/shopUser/components/user-import.vue | 88 +++ src/views/shop/shopUser/index.vue | 611 ++++++++++++++++++ vite.config.ts | 80 ++- 33 files changed, 6704 insertions(+), 38 deletions(-) create mode 100644 src/lib/port-manager.ts create mode 100644 src/lib/port-strategy.ts create mode 100644 src/lib/tenant-port-manager.ts create mode 100644 src/utils/port-config-manager.ts create mode 100644 src/views/sdy/sdyUser/components/org-select.vue create mode 100644 src/views/sdy/sdyUser/components/search.vue create mode 100644 src/views/sdy/sdyUser/components/status-test.vue create mode 100644 src/views/sdy/sdyUser/components/user-edit.vue create mode 100644 src/views/sdy/sdyUser/components/user-import.vue create mode 100644 src/views/sdy/sdyUser/index.vue create mode 100644 src/views/sdy/shopDealerApply/components/search.vue create mode 100644 src/views/sdy/shopDealerApply/components/shopDealerApplyEdit.vue create mode 100644 src/views/sdy/shopDealerApply/index.vue create mode 100644 src/views/sdy/shopDealerUser/README.md create mode 100644 src/views/sdy/shopDealerUser/components/Import.vue create mode 100644 src/views/sdy/shopDealerUser/components/search.vue create mode 100644 src/views/sdy/shopDealerUser/components/shopDealerUserEdit.vue create mode 100644 src/views/sdy/shopDealerUser/index.vue create mode 100644 src/views/shop/shopDealerUser/README.md create mode 100644 src/views/shop/shopDealerUser/components/Import.vue create mode 100644 src/views/shop/shopUser/components/org-select.vue create mode 100644 src/views/shop/shopUser/components/search.vue create mode 100644 src/views/shop/shopUser/components/user-edit.vue create mode 100644 src/views/shop/shopUser/components/user-import.vue create mode 100644 src/views/shop/shopUser/index.vue diff --git a/.env.development b/.env.development index b8ccd8b..fbcc74b 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,5 @@ VITE_APP_NAME=后台管理(开发环境) -#VITE_API_URL=http://127.0.0.1:9200/api +VITE_API_URL=http://127.0.0.1:9200/api #VITE_SERVER_API_URL=http://127.0.0.1:8000/api diff --git a/.env.example b/.env.example index 19b436f..eb6f73c 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,24 @@ VITE_TEMPLATE_ID=10258 # 应用密钥 VITE_APP_SECRET=your_app_secret +# 智能端口管理配置 +VITE_PORT_STRATEGY=auto +VITE_BASE_PORT=3000 +VITE_PORT_RANGE_START=3000 +VITE_PORT_RANGE_END=9999 +VITE_TENANT_PORT_OFFSET=10 +VITE_ENVIRONMENT_PORT_OFFSET=1000 +VITE_PORT_AUTO_DETECT=true +VITE_PORT_STRICT_MODE=false +VITE_PORT_CACHE_ENABLED=true +VITE_PORT_CACHE_EXPIRY=86400000 + +# 开发服务器配置 +VITE_DEV_HOST=localhost +VITE_DEV_OPEN_BROWSER=true +VITE_DEV_CORS_ENABLED=true +VITE_DEV_HTTPS_ENABLED=false + # 高德地图配置 (请到高德地图官网申请) VITE_MAP_KEY=your_map_key VITE_MAP_CODE=your_map_security_code diff --git a/src/api/shop/shopDealerUser/index.ts b/src/api/shop/shopDealerUser/index.ts index 657f658..384aeda 100644 --- a/src/api/shop/shopDealerUser/index.ts +++ b/src/api/shop/shopDealerUser/index.ts @@ -103,3 +103,38 @@ export async function getShopDealerUser(id: number) { } return Promise.reject(new Error(res.data.message)); } + +/** + * 导入分销商用户 + */ +export async function importShopDealerUsers(file: File) { + const formData = new FormData(); + formData.append('file', file); + const res = await request.post>( + '/shop/shop-dealer-user/import', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data' + } + } + ); + if (res.data.code === 0) { + return res.data.message; + } + return Promise.reject(new Error(res.data.message)); +} + +/** + * 导出分销商用户 + */ +export async function exportShopDealerUsers(params?: ShopDealerUserParam) { + const res = await request.get( + '/shop/shop-dealer-user/export', + { + params, + responseType: 'blob' + } + ); + return res.data; +} diff --git a/src/api/shop/shopDealerUser/model/index.ts b/src/api/shop/shopDealerUser/model/index.ts index 857b145..1b3bc22 100644 --- a/src/api/shop/shopDealerUser/model/index.ts +++ b/src/api/shop/shopDealerUser/model/index.ts @@ -38,6 +38,13 @@ export interface ShopDealerUser { createTime?: string | Date; // 修改时间 updateTime?: string | Date; + // 扩展字段,用于编辑表单 + shopDealerUserId?: number; + shopDealerUserName?: string; + status?: number; + comments?: string; + sortNumber?: number; + image?: string; } /** diff --git a/src/composables/useSiteData.ts b/src/composables/useSiteData.ts index 963f976..62e29db 100644 --- a/src/composables/useSiteData.ts +++ b/src/composables/useSiteData.ts @@ -12,12 +12,12 @@ export function useSiteData() { // 网站信息相关 const siteInfo = computed(() => siteStore.siteInfo); - const websiteName = computed(() => siteStore.websiteName); - const websiteLogo = computed(() => siteStore.websiteLogo); - const websiteComments = computed(() => siteStore.websiteComments); - const websiteDarkLogo = computed(() => siteStore.websiteDarkLogo); - const websiteDomain = computed(() => siteStore.websiteDomain); - const websiteId = computed(() => siteStore.websiteId); + const websiteName = computed(() => siteStore.appName); + const websiteLogo = computed(() => siteStore.logo); + const websiteComments = computed(() => siteStore.description); + const websiteDarkLogo = computed(() => siteStore.logo); + const websiteDomain = computed(() => siteStore.domain); + const websiteId = computed(() => siteStore.appId); const runDays = computed(() => siteStore.runDays); const siteLoading = computed(() => siteStore.loading); diff --git a/src/lib/port-manager.ts b/src/lib/port-manager.ts new file mode 100644 index 0000000..43caf40 --- /dev/null +++ b/src/lib/port-manager.ts @@ -0,0 +1,355 @@ +/** + * 智能端口管理系统 + * 类似租户识别系统的端口管理解决方案 + */ + +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 { + 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): 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: string = 'localhost'): Promise { + 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: string = 'localhost' + ): Promise { + 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; + private strategy: PortStrategy; + private environment: string; + + constructor(strategy?: Partial) { + 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 { + const tenantId = options?.tenantId || await getTenantId(); + const projectName = options?.projectName || 'mp-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 { + 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; + 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 }; diff --git a/src/lib/port-strategy.ts b/src/lib/port-strategy.ts new file mode 100644 index 0000000..4cc3da5 --- /dev/null +++ b/src/lib/port-strategy.ts @@ -0,0 +1,415 @@ +/** + * 环境端口策略配置 + * 类似租户识别系统的环境优先级策略 + */ + +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; + 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: '生产环境 - 最低优先级,注重安全性' + } +]; diff --git a/src/lib/tenant-port-manager.ts b/src/lib/tenant-port-manager.ts new file mode 100644 index 0000000..dd3c93d --- /dev/null +++ b/src/lib/tenant-port-manager.ts @@ -0,0 +1,411 @@ +/** + * 租户端口管理器 + * 集成租户识别系统和端口管理系统 + */ + +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(); diff --git a/src/utils/port-config-manager.ts b/src/utils/port-config-manager.ts new file mode 100644 index 0000000..d9997c9 --- /dev/null +++ b/src/utils/port-config-manager.ts @@ -0,0 +1,357 @@ +/** + * 端口配置管理器 + * 集成环境变量和智能端口管理 + */ + +import type { PortStrategy } from '@/lib/port-manager'; +import type { Environment } from '@/lib/port-strategy'; + +// 端口配置接口 +export interface PortEnvironmentConfig { + // 基础配置 + strategy: 'auto' | 'manual' | 'tenant-based' | 'sequential'; + basePort: number; + portRangeStart: number; + portRangeEnd: number; + + // 租户配置 + tenantPortOffset: number; + environmentPortOffset: number; + + // 行为配置 + autoDetect: boolean; + strictMode: boolean; + cacheEnabled: boolean; + cacheExpiry: number; + + // 开发服务器配置 + devHost: string; + openBrowser: boolean; + corsEnabled: boolean; + httpsEnabled: boolean; +} + +// 配置验证结果 +export interface ConfigValidationResult { + isValid: boolean; + errors: string[]; + warnings: string[]; + recommendations: string[]; +} + +// 端口配置管理器 +export class PortConfigManager { + private config: PortEnvironmentConfig; + private environment: Environment; + + constructor() { + this.environment = this.detectEnvironment(); + this.config = this.loadConfiguration(); + this.validateConfiguration(); + } + + /** + * 检测当前环境 + */ + private detectEnvironment(): Environment { + // 在 Vite 配置阶段,import.meta.env 可能不可用,使用 process.env + const nodeEnv = (typeof process !== 'undefined' ? process.env.NODE_ENV : undefined) || + (typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env.NODE_ENV : undefined) || + 'development'; + + switch (nodeEnv.toLowerCase()) { + case 'production': + case 'prod': + return 'production'; + case 'test': + case 'testing': + return 'test'; + case 'staging': + case 'stage': + return 'staging'; + default: + return 'development'; + } + } + + /** + * 加载配置 + */ + private loadConfiguration(): PortEnvironmentConfig { + // 获取环境变量的辅助函数 + const getEnv = (key: string, defaultValue?: string) => { + if (typeof process !== 'undefined' && process.env) { + return process.env[key]; + } + if (typeof import.meta !== 'undefined' && import.meta.env) { + return import.meta.env[key]; + } + return defaultValue; + }; + + return { + // 基础配置 + strategy: (getEnv('VITE_PORT_STRATEGY') as any) || 'auto', + basePort: parseInt(getEnv('VITE_BASE_PORT') || '3000'), + portRangeStart: parseInt(getEnv('VITE_PORT_RANGE_START') || '3000'), + portRangeEnd: parseInt(getEnv('VITE_PORT_RANGE_END') || '9999'), + + // 租户配置 + tenantPortOffset: parseInt(getEnv('VITE_TENANT_PORT_OFFSET') || '10'), + environmentPortOffset: parseInt(getEnv('VITE_ENVIRONMENT_PORT_OFFSET') || '1000'), + + // 行为配置 + autoDetect: getEnv('VITE_PORT_AUTO_DETECT') !== 'false', + strictMode: getEnv('VITE_PORT_STRICT_MODE') === 'true', + cacheEnabled: getEnv('VITE_PORT_CACHE_ENABLED') !== 'false', + cacheExpiry: parseInt(getEnv('VITE_PORT_CACHE_EXPIRY') || '86400000'), // 24小时 + + // 开发服务器配置 + devHost: getEnv('VITE_DEV_HOST') || 'localhost', + openBrowser: getEnv('VITE_DEV_OPEN_BROWSER') !== 'false', + corsEnabled: getEnv('VITE_DEV_CORS_ENABLED') !== 'false', + httpsEnabled: getEnv('VITE_DEV_HTTPS_ENABLED') === 'true' + }; + } + + /** + * 验证配置 + */ + private validateConfiguration(): ConfigValidationResult { + const errors: string[] = []; + const warnings: string[] = []; + const recommendations: string[] = []; + + // 验证端口范围 + if (this.config.portRangeStart >= this.config.portRangeEnd) { + errors.push('端口范围无效:起始端口必须小于结束端口'); + } + + if (this.config.portRangeStart < 1024 && this.environment === 'production') { + warnings.push('生产环境使用系统端口(<1024)可能需要管理员权限'); + } + + // 验证基础端口 + if (this.config.basePort < this.config.portRangeStart || + this.config.basePort > this.config.portRangeEnd) { + errors.push('基础端口不在允许的端口范围内'); + } + + // 验证租户偏移 + if (this.config.tenantPortOffset <= 0) { + warnings.push('租户端口偏移为0可能导致端口冲突'); + } + + // 环境特定验证 + switch (this.environment) { + case 'development': + if (this.config.httpsEnabled) { + warnings.push('开发环境启用HTTPS可能增加配置复杂度'); + } + if (!this.config.autoDetect) { + recommendations.push('开发环境建议启用端口自动检测'); + } + break; + + case 'production': + if (!this.config.httpsEnabled) { + warnings.push('生产环境建议启用HTTPS'); + } + if (this.config.autoDetect) { + warnings.push('生产环境不建议启用端口自动检测'); + } + if (!this.config.strictMode) { + recommendations.push('生产环境建议启用严格模式'); + } + break; + + case 'test': + if (this.config.openBrowser) { + recommendations.push('测试环境建议禁用自动打开浏览器'); + } + break; + } + + // 缓存配置验证 + if (this.config.cacheEnabled && this.config.cacheExpiry < 60000) { + warnings.push('缓存过期时间过短可能影响性能'); + } + + const result = { + isValid: errors.length === 0, + errors, + warnings, + recommendations + }; + + // 输出验证结果 + if (errors.length > 0) { + console.error('❌ 端口配置验证失败:', errors); + } + if (warnings.length > 0) { + console.warn('⚠️ 端口配置警告:', warnings); + } + if (recommendations.length > 0) { + console.info('💡 端口配置建议:', recommendations); + } + + return result; + } + + /** + * 获取当前配置 + */ + getConfig(): PortEnvironmentConfig { + return { ...this.config }; + } + + /** + * 获取端口策略配置 + */ + getPortStrategy(): PortStrategy { + return { + basePort: this.config.basePort, + portRange: [this.config.portRangeStart, this.config.portRangeEnd], + tenantOffset: this.config.tenantPortOffset, + environmentOffset: this.config.environmentPortOffset, + maxRetries: this.config.strictMode ? 10 : 50 + }; + } + + /** + * 获取开发服务器配置 + */ + getDevServerConfig(): { + host: string; + port?: number; + open: boolean; + cors: boolean; + https: boolean; + strictPort: boolean; + } { + return { + host: this.config.devHost, + open: this.config.openBrowser, + cors: this.config.corsEnabled, + https: this.config.httpsEnabled, + strictPort: this.config.strictMode + }; + } + + /** + * 获取环境信息 + */ + getEnvironmentInfo(): { + current: Environment; + config: PortEnvironmentConfig; + validation: ConfigValidationResult; + recommendations: string[]; + } { + const validation = this.validateConfiguration(); + const recommendations: string[] = []; + + // 基于环境生成建议 + switch (this.environment) { + case 'development': + recommendations.push('开发环境:优先考虑便利性和调试体验'); + recommendations.push('建议启用热重载和自动刷新功能'); + break; + case 'test': + recommendations.push('测试环境:注重隔离性和可重复性'); + recommendations.push('建议配置独立的端口范围避免冲突'); + break; + case 'production': + recommendations.push('生产环境:优先考虑安全性和稳定性'); + recommendations.push('建议使用固定端口和负载均衡'); + break; + } + + return { + current: this.environment, + config: this.config, + validation, + recommendations: [...validation.recommendations, ...recommendations] + }; + } + + /** + * 更新配置 + */ + updateConfig(updates: Partial): ConfigValidationResult { + this.config = { ...this.config, ...updates }; + return this.validateConfiguration(); + } + + /** + * 重置为默认配置 + */ + resetToDefaults(): void { + this.config = this.loadConfiguration(); + console.log('🔄 端口配置已重置为默认值'); + } + + /** + * 导出配置 + */ + exportConfig(): string { + const configLines = [ + '# 智能端口管理配置', + `VITE_PORT_STRATEGY=${this.config.strategy}`, + `VITE_BASE_PORT=${this.config.basePort}`, + `VITE_PORT_RANGE_START=${this.config.portRangeStart}`, + `VITE_PORT_RANGE_END=${this.config.portRangeEnd}`, + `VITE_TENANT_PORT_OFFSET=${this.config.tenantPortOffset}`, + `VITE_ENVIRONMENT_PORT_OFFSET=${this.config.environmentPortOffset}`, + `VITE_PORT_AUTO_DETECT=${this.config.autoDetect}`, + `VITE_PORT_STRICT_MODE=${this.config.strictMode}`, + `VITE_PORT_CACHE_ENABLED=${this.config.cacheEnabled}`, + `VITE_PORT_CACHE_EXPIRY=${this.config.cacheExpiry}`, + '', + '# 开发服务器配置', + `VITE_DEV_HOST=${this.config.devHost}`, + `VITE_DEV_OPEN_BROWSER=${this.config.openBrowser}`, + `VITE_DEV_CORS_ENABLED=${this.config.corsEnabled}`, + `VITE_DEV_HTTPS_ENABLED=${this.config.httpsEnabled}` + ]; + + return configLines.join('\n'); + } + + /** + * 获取配置摘要 + */ + getConfigSummary(): { + environment: Environment; + strategy: string; + portRange: string; + features: string[]; + status: 'healthy' | 'warning' | 'error'; + } { + const validation = this.validateConfiguration(); + const features: string[] = []; + + if (this.config.autoDetect) features.push('自动检测'); + if (this.config.cacheEnabled) features.push('缓存启用'); + if (this.config.strictMode) features.push('严格模式'); + if (this.config.httpsEnabled) features.push('HTTPS'); + if (this.config.corsEnabled) features.push('CORS'); + + let status: 'healthy' | 'warning' | 'error' = 'healthy'; + if (validation.errors.length > 0) { + status = 'error'; + } else if (validation.warnings.length > 0) { + status = 'warning'; + } + + return { + environment: this.environment, + strategy: this.config.strategy, + portRange: `${this.config.portRangeStart}-${this.config.portRangeEnd}`, + features, + status + }; + } +} + +// 导出默认实例 +export const portConfigManager = new PortConfigManager(); diff --git a/src/views/sdy/sdyUser/components/org-select.vue b/src/views/sdy/sdyUser/components/org-select.vue new file mode 100644 index 0000000..587424f --- /dev/null +++ b/src/views/sdy/sdyUser/components/org-select.vue @@ -0,0 +1,39 @@ + + + + diff --git a/src/views/sdy/sdyUser/components/search.vue b/src/views/sdy/sdyUser/components/search.vue new file mode 100644 index 0000000..82fea9d --- /dev/null +++ b/src/views/sdy/sdyUser/components/search.vue @@ -0,0 +1,42 @@ + + + + diff --git a/src/views/sdy/sdyUser/components/status-test.vue b/src/views/sdy/sdyUser/components/status-test.vue new file mode 100644 index 0000000..714e427 --- /dev/null +++ b/src/views/sdy/sdyUser/components/status-test.vue @@ -0,0 +1,65 @@ + + + + diff --git a/src/views/sdy/sdyUser/components/user-edit.vue b/src/views/sdy/sdyUser/components/user-edit.vue new file mode 100644 index 0000000..195eb48 --- /dev/null +++ b/src/views/sdy/sdyUser/components/user-edit.vue @@ -0,0 +1,312 @@ + + + + diff --git a/src/views/sdy/sdyUser/components/user-import.vue b/src/views/sdy/sdyUser/components/user-import.vue new file mode 100644 index 0000000..2274410 --- /dev/null +++ b/src/views/sdy/sdyUser/components/user-import.vue @@ -0,0 +1,88 @@ + + + + diff --git a/src/views/sdy/sdyUser/index.vue b/src/views/sdy/sdyUser/index.vue new file mode 100644 index 0000000..d2c6379 --- /dev/null +++ b/src/views/sdy/sdyUser/index.vue @@ -0,0 +1,603 @@ + + + + + + + + diff --git a/src/views/sdy/shopDealerApply/components/search.vue b/src/views/sdy/shopDealerApply/components/search.vue new file mode 100644 index 0000000..3a97655 --- /dev/null +++ b/src/views/sdy/shopDealerApply/components/search.vue @@ -0,0 +1,219 @@ + + + + + + diff --git a/src/views/sdy/shopDealerApply/components/shopDealerApplyEdit.vue b/src/views/sdy/shopDealerApply/components/shopDealerApplyEdit.vue new file mode 100644 index 0000000..bad4d1f --- /dev/null +++ b/src/views/sdy/shopDealerApply/components/shopDealerApplyEdit.vue @@ -0,0 +1,407 @@ + + + + + + diff --git a/src/views/sdy/shopDealerApply/index.vue b/src/views/sdy/shopDealerApply/index.vue new file mode 100644 index 0000000..1d34613 --- /dev/null +++ b/src/views/sdy/shopDealerApply/index.vue @@ -0,0 +1,494 @@ + + + + + + + diff --git a/src/views/sdy/shopDealerUser/README.md b/src/views/sdy/shopDealerUser/README.md new file mode 100644 index 0000000..0f48844 --- /dev/null +++ b/src/views/sdy/shopDealerUser/README.md @@ -0,0 +1,100 @@ +# 分销商用户管理模块 + +## 功能说明 + +本模块提供了完整的分销商用户管理功能,包括: + +### 基础功能 +- ✅ 用户列表查看 +- ✅ 用户信息编辑 +- ✅ 用户详情查看 +- ✅ 用户删除(单个/批量) +- ✅ 关键词搜索 + +### 导入导出功能 +- ✅ Excel 数据导出 +- ✅ Excel 数据导入 +- ✅ 导入数据验证 +- ✅ 错误处理 + +## 导入导出使用说明 + +### 导出功能 +1. 点击"导出xls"按钮 +2. 系统会根据当前搜索条件导出数据 +3. 导出的Excel文件包含以下字段: + - 用户ID + - 姓名 + - 手机号 + - 可提现佣金 + - 冻结佣金 + - 累计提现 + - 推荐人ID + - 一级成员数 + - 二级成员数 + - 三级成员数 + - 专属二维码 + - 状态 + - 创建时间 + - 更新时间 + +### 导入功能 +1. 点击"导入xls"按钮 +2. 拖拽或选择Excel文件(支持.xls和.xlsx格式) +3. 文件大小限制:10MB以内 +4. 导入格式要求: + - 必填字段:用户ID、姓名、手机号 + - 佣金字段请填写数字,不要包含货币符号 + - 状态字段:正常 或 已删除 + - 推荐人ID必须是已存在的用户ID + +### 数据格式示例 + +| 用户ID | 姓名 | 手机号 | 可提现佣金 | 冻结佣金 | 累计提现 | 推荐人ID | 一级成员数 | 二级成员数 | 三级成员数 | 专属二维码 | 状态 | +|--------|------|--------|------------|----------|----------|----------|------------|------------|------------|------------|------| +| 1001 | 张三 | 13800138000 | 100.50 | 50.00 | 200.00 | 1000 | 5 | 3 | 2 | DEALER_1001_xxx | 正常 | +| 1002 | 李四 | 13900139000 | 200.00 | 0.00 | 150.00 | | 2 | 1 | 0 | | 正常 | + +## 技术实现 + +### 文件结构 +``` +src/views/shop/shopDealerUser/ +├── index.vue # 主页面 +├── components/ +│ ├── search.vue # 搜索组件(包含导入导出功能) +│ ├── Import.vue # 导入弹窗组件 +│ └── shopDealerUserEdit.vue # 编辑弹窗组件 +└── README.md # 说明文档 +``` + +### API 接口 +```typescript +// 导入接口 +POST /shop/shop-dealer-user/import +Content-Type: multipart/form-data + +// 导出接口 +GET /shop/shop-dealer-user/export +Response-Type: blob +``` + +### 依赖库 +- `xlsx`: Excel文件处理 +- `dayjs`: 日期格式化 +- `ant-design-vue`: UI组件库 + +## 注意事项 + +1. **数据安全**:导入功能会直接操作数据库,请确保导入的数据准确无误 +2. **性能考虑**:大量数据导入时可能需要较长时间,请耐心等待 +3. **错误处理**:导入失败时会显示具体错误信息,请根据提示修正数据 +4. **权限控制**:确保用户有相应的导入导出权限 + +## 更新日志 + +### v1.0.0 (2024-12-19) +- ✅ 实现基础的导入导出功能 +- ✅ 添加数据验证和错误处理 +- ✅ 优化用户体验和界面交互 +- ✅ 完善文档说明 diff --git a/src/views/sdy/shopDealerUser/components/Import.vue b/src/views/sdy/shopDealerUser/components/Import.vue new file mode 100644 index 0000000..fbf5244 --- /dev/null +++ b/src/views/sdy/shopDealerUser/components/Import.vue @@ -0,0 +1,110 @@ + + + + + + diff --git a/src/views/sdy/shopDealerUser/components/search.vue b/src/views/sdy/shopDealerUser/components/search.vue new file mode 100644 index 0000000..19e7393 --- /dev/null +++ b/src/views/sdy/shopDealerUser/components/search.vue @@ -0,0 +1,206 @@ + + + + diff --git a/src/views/sdy/shopDealerUser/components/shopDealerUserEdit.vue b/src/views/sdy/shopDealerUser/components/shopDealerUserEdit.vue new file mode 100644 index 0000000..e2afb6b --- /dev/null +++ b/src/views/sdy/shopDealerUser/components/shopDealerUserEdit.vue @@ -0,0 +1,516 @@ + + + + + + diff --git a/src/views/sdy/shopDealerUser/index.vue b/src/views/sdy/shopDealerUser/index.vue new file mode 100644 index 0000000..5f591d0 --- /dev/null +++ b/src/views/sdy/shopDealerUser/index.vue @@ -0,0 +1,364 @@ + + + + + + + diff --git a/src/views/shop/shopDealerUser/README.md b/src/views/shop/shopDealerUser/README.md new file mode 100644 index 0000000..0f48844 --- /dev/null +++ b/src/views/shop/shopDealerUser/README.md @@ -0,0 +1,100 @@ +# 分销商用户管理模块 + +## 功能说明 + +本模块提供了完整的分销商用户管理功能,包括: + +### 基础功能 +- ✅ 用户列表查看 +- ✅ 用户信息编辑 +- ✅ 用户详情查看 +- ✅ 用户删除(单个/批量) +- ✅ 关键词搜索 + +### 导入导出功能 +- ✅ Excel 数据导出 +- ✅ Excel 数据导入 +- ✅ 导入数据验证 +- ✅ 错误处理 + +## 导入导出使用说明 + +### 导出功能 +1. 点击"导出xls"按钮 +2. 系统会根据当前搜索条件导出数据 +3. 导出的Excel文件包含以下字段: + - 用户ID + - 姓名 + - 手机号 + - 可提现佣金 + - 冻结佣金 + - 累计提现 + - 推荐人ID + - 一级成员数 + - 二级成员数 + - 三级成员数 + - 专属二维码 + - 状态 + - 创建时间 + - 更新时间 + +### 导入功能 +1. 点击"导入xls"按钮 +2. 拖拽或选择Excel文件(支持.xls和.xlsx格式) +3. 文件大小限制:10MB以内 +4. 导入格式要求: + - 必填字段:用户ID、姓名、手机号 + - 佣金字段请填写数字,不要包含货币符号 + - 状态字段:正常 或 已删除 + - 推荐人ID必须是已存在的用户ID + +### 数据格式示例 + +| 用户ID | 姓名 | 手机号 | 可提现佣金 | 冻结佣金 | 累计提现 | 推荐人ID | 一级成员数 | 二级成员数 | 三级成员数 | 专属二维码 | 状态 | +|--------|------|--------|------------|----------|----------|----------|------------|------------|------------|------------|------| +| 1001 | 张三 | 13800138000 | 100.50 | 50.00 | 200.00 | 1000 | 5 | 3 | 2 | DEALER_1001_xxx | 正常 | +| 1002 | 李四 | 13900139000 | 200.00 | 0.00 | 150.00 | | 2 | 1 | 0 | | 正常 | + +## 技术实现 + +### 文件结构 +``` +src/views/shop/shopDealerUser/ +├── index.vue # 主页面 +├── components/ +│ ├── search.vue # 搜索组件(包含导入导出功能) +│ ├── Import.vue # 导入弹窗组件 +│ └── shopDealerUserEdit.vue # 编辑弹窗组件 +└── README.md # 说明文档 +``` + +### API 接口 +```typescript +// 导入接口 +POST /shop/shop-dealer-user/import +Content-Type: multipart/form-data + +// 导出接口 +GET /shop/shop-dealer-user/export +Response-Type: blob +``` + +### 依赖库 +- `xlsx`: Excel文件处理 +- `dayjs`: 日期格式化 +- `ant-design-vue`: UI组件库 + +## 注意事项 + +1. **数据安全**:导入功能会直接操作数据库,请确保导入的数据准确无误 +2. **性能考虑**:大量数据导入时可能需要较长时间,请耐心等待 +3. **错误处理**:导入失败时会显示具体错误信息,请根据提示修正数据 +4. **权限控制**:确保用户有相应的导入导出权限 + +## 更新日志 + +### v1.0.0 (2024-12-19) +- ✅ 实现基础的导入导出功能 +- ✅ 添加数据验证和错误处理 +- ✅ 优化用户体验和界面交互 +- ✅ 完善文档说明 diff --git a/src/views/shop/shopDealerUser/components/Import.vue b/src/views/shop/shopDealerUser/components/Import.vue new file mode 100644 index 0000000..fbf5244 --- /dev/null +++ b/src/views/shop/shopDealerUser/components/Import.vue @@ -0,0 +1,110 @@ + + + + + + diff --git a/src/views/shop/shopDealerUser/components/search.vue b/src/views/shop/shopDealerUser/components/search.vue index 82fea9d..19e7393 100644 --- a/src/views/shop/shopDealerUser/components/search.vue +++ b/src/views/shop/shopDealerUser/components/search.vue @@ -7,36 +7,200 @@ 添加 + + + 批量删除 + + + 重置 + 导出xls + 导入xls + + + diff --git a/src/views/shop/shopDealerUser/index.vue b/src/views/shop/shopDealerUser/index.vue index c970036..4cb4efa 100644 --- a/src/views/shop/shopDealerUser/index.vue +++ b/src/views/shop/shopDealerUser/index.vue @@ -3,12 +3,13 @@