/** * 进度数据管理模块 * 处理进度数据的获取、缓存和轮询 * * 数据降级策略: * 1. 优先从API获取最新数据 * 2. API失败时使用缓存数据(5分钟内有效) * 3. 缓存数据超过30秒标记为"可能过时" * 4. 无缓存时返回默认数据 * 5. 通过UI指示器而非toast显示数据状态 */ import { POLLING_CONFIG, CACHE_CONFIG } from './performance-config' import performanceMonitor from './performance-monitor' import { getActivityProgressApi } from './api.js' // 开发配置 const DEV_CONFIG = { USE_MOCK_DATA: false, MOCK_INITIAL_PROGRESS: 100, MOCK_INCREMENT_MIN: 50, MOCK_INCREMENT_MAX: 100, MOCK_TARGET: 1000, MOCK_NETWORK_DELAY: 200 } // 存储当前模拟进度值 let currentMockProgress = DEV_CONFIG.MOCK_INITIAL_PROGRESS // 缓存配置 const CACHE_EXPIRY_TIME = CACHE_CONFIG.PROGRESS_DATA_EXPIRY const POLLING_INTERVAL_MIN = POLLING_CONFIG.MIN_INTERVAL const POLLING_INTERVAL_MAX = POLLING_CONFIG.MAX_INTERVAL const MAX_RETRY_COUNT = 3 /** * 计算下次轮询间隔,使用标准指数退避 + 均匀抖动 * * 正常情况(attempt=0):[MIN, MAX] 之间随机 * 每次失败 attempt+1,间隔翻倍,上限 MAX_RETRY_INTERVAL(60s) * 均匀抖动避免多客户端同时重试的惊群效应 * * @param {number} attempt - 连续失败次数 * @returns {number} 轮询间隔(毫秒) */ function getRandomPollingInterval(attempt = 0) { const backoff = POLLING_INTERVAL_MIN * Math.pow(2, attempt) const capped = Math.min(backoff, POLLING_CONFIG.MAX_RETRY_INTERVAL) // 均匀抖动:在 [capped * 0.75, capped * 1.25] 范围内随机,最小不低于 MIN_INTERVAL const jitter = capped * 0.75 + Math.random() * capped * 0.5 return Math.max(Math.floor(jitter), POLLING_INTERVAL_MIN) } /** * 生成客户端唯一标识(uni-app 兼容版本) * @returns {number} 客户端偏移量(0-5000ms) */ function getClientOffset() { const globalData = getApp().globalData if (!globalData.__clientOffset) { const systemInfo = uni.getSystemInfoSync() const factors = [ systemInfo.platform?.length || 0, systemInfo.screenWidth || 0, systemInfo.screenHeight || 0, systemInfo.model?.length || 0, systemInfo.system?.length || 0, new Date().getTimezoneOffset() ] const hash = factors.reduce((acc, val) => ((acc << 5) - acc) + val, 0) globalData.__clientOffset = Math.abs(hash % 5000) } return globalData.__clientOffset } /** * 进度数据管理器类 */ export class ProgressManager { constructor(activityId) { this.activityId = activityId this.pollingTimer = null this._offsetTimer = null this.isPolling = false this.retryCount = 0 this.listeners = new Set() this.isPageVisible = true this.visibilityListener = null } addListener(listener) { this.listeners.add(listener) } removeListener(listener) { this.listeners.delete(listener) } notifyListeners(data) { this.listeners.forEach(listener => { try { listener(data) } catch (error) { console.error('监听器执行错误:', error) } }) } getCacheKey() { return `progress_${this.activityId}` } getCachedData() { try { const cached = uni.getStorageSync(this.getCacheKey()) if (!cached || !cached.timestamp) return null const cacheAge = Date.now() - cached.timestamp if (cacheAge > CACHE_EXPIRY_TIME) { uni.removeStorageSync(this.getCacheKey()) return null } return { current: cached.current || 0, target: cached.target || 1000, isFromCache: true, isStale: cacheAge > CACHE_CONFIG.STALE_DATA_THRESHOLD, cacheAge, lastUpdated: cached.timestamp } } catch (error) { console.error('获取缓存数据失败:', error) return null } } cacheData(data) { try { uni.setStorageSync(this.getCacheKey(), { current: data.current || 0, target: data.target || 1000, timestamp: Date.now() }) } catch (error) { console.error('缓存数据失败:', error) } } async fetchFromAPI() { const startTime = Date.now() try { if (DEV_CONFIG.USE_MOCK_DATA) { await new Promise(resolve => setTimeout(resolve, DEV_CONFIG.MOCK_NETWORK_DELAY)) const increment = Math.floor( Math.random() * (DEV_CONFIG.MOCK_INCREMENT_MAX - DEV_CONFIG.MOCK_INCREMENT_MIN + 1) ) + DEV_CONFIG.MOCK_INCREMENT_MIN currentMockProgress = Math.min(currentMockProgress + increment, DEV_CONFIG.MOCK_TARGET) const progressData = { current: currentMockProgress, target: DEV_CONFIG.MOCK_TARGET, isFromCache: false, isStale: false } this.cacheData(progressData) this.retryCount = 0 performanceMonitor.recordPollingRequest(true, Date.now() - startTime) return progressData } const response = await getActivityProgressApi(this.activityId) if (response.code !== 200 || !response.data) { throw new Error(response.message || 'API返回数据异常') } const progressData = { current: response.data.current_progress || 0, target: response.data.target_progress || 1000, isFromCache: false, isStale: false } this.cacheData(progressData) this.retryCount = 0 performanceMonitor.recordPollingRequest(true, Date.now() - startTime) return progressData } catch (error) { console.error('API获取进度数据失败:', error) performanceMonitor.recordPollingRequest(false, Date.now() - startTime) throw error } } async getProgressData() { try { const data = await this.fetchFromAPI() this.notifyListeners(data) return data } catch (error) { console.warn('API获取失败,尝试使用缓存数据:', error.message) const cachedData = this.getCachedData() if (cachedData) { console.log('使用缓存数据,缓存年龄:', cachedData.cacheAge, 'ms') this.notifyListeners(cachedData) return cachedData } const defaultData = { current: 0, target: 1000, isFromCache: false, isStale: false, isDefault: true } this.notifyListeners(defaultData) return defaultData } } /** * 调度下一次轮询(统一入口,消除重复逻辑) */ _scheduleNextPoll() { const interval = Math.max(getRandomPollingInterval(this.retryCount), POLLING_INTERVAL_MIN) console.log(`[轮询] 下次轮询间隔: ${(interval / 1000).toFixed(1)}秒 (重试次数: ${this.retryCount})`) this.pollingTimer = setTimeout(async () => { if (!this.isPolling || !this.isPageVisible) { console.log('[轮询] 状态变化,停止轮询') return } try { await this.getProgressData() this.retryCount = 0 } catch (error) { console.error('[轮询] 轮询失败:', error) this.retryCount = Math.min(this.retryCount + 1, MAX_RETRY_COUNT) } if (this.isPolling && this.isPageVisible) { this._scheduleNextPoll() } else { console.log('[轮询] 页面状态变化,停止调度下一次轮询') } }, interval) } startPolling() { if (this.isPolling) { console.log('[轮询] 轮询已在运行中,跳过启动') return } this.isPolling = true this.isPageVisible = true const clientOffset = getClientOffset() console.log(`[轮询] 准备启动,客户端偏移: ${(clientOffset / 1000).toFixed(1)}秒`) this._offsetTimer = setTimeout(() => { this._offsetTimer = null if (!this.isPolling || !this.isPageVisible) { console.log('[轮询] 页面已隐藏或轮询已停止,取消首次请求') return } this.getProgressData().catch(err => { console.error('[轮询] 首次轮询失败:', err) this.retryCount++ }) this._scheduleNextPoll() }, clientOffset) console.log(`[轮询] 已启动,最小间隔: ${POLLING_INTERVAL_MIN / 1000}秒`) } stopPolling() { if (this._offsetTimer) { clearTimeout(this._offsetTimer) this._offsetTimer = null } if (this.pollingTimer) { clearTimeout(this.pollingTimer) this.pollingTimer = null } this.isPolling = false this.isPageVisible = false console.log('[轮询] 进度数据轮询已停止') } pausePolling() { console.log('[轮询] 页面隐藏,暂停轮询') this.isPageVisible = false if (this._offsetTimer) { clearTimeout(this._offsetTimer) this._offsetTimer = null } if (this.pollingTimer) { clearTimeout(this.pollingTimer) this.pollingTimer = null console.log('[轮询] 已清除轮询定时器') } } resumePolling() { console.log('[轮询] 页面显示,恢复轮询') this.isPageVisible = true // 仅在轮询标志为真且没有任何定时器在运行时才恢复,防止竞态 if (this.isPolling && !this.pollingTimer && !this._offsetTimer) { console.log('[轮询] 重新启动轮询调度') this.getProgressData().catch(err => { console.error('[轮询] 恢复后首次轮询失败:', err) this.retryCount++ }) this._scheduleNextPoll() } } async refresh() { console.log('手动刷新进度数据') // 暂停当前轮询循环,拉取最新数据后恢复,避免 stop+start 引入 clientOffset 延迟 const wasPolling = this.isPolling if (wasPolling) { this.pausePolling() } const data = await this.getProgressData() if (wasPolling) { this.isPolling = true this.isPageVisible = true this._scheduleNextPoll() } return data } destroy() { console.log('[轮询] 销毁进度管理器,清理所有资源') this.stopPolling() this.listeners.clear() console.log('[轮询] 进度管理器已销毁') } } /** * 创建进度管理器实例 * @param {string} activityId - 活动ID * @returns {ProgressManager} */ export function createProgressManager(activityId) { return new ProgressManager(activityId) } // { activityId -> { manager: ProgressManager, refCount: number } } const managerCache = new Map() /** * 获取或创建进度管理器实例,内部维护引用计数。 * 每次调用 getProgressManager 必须对应一次 releaseProgressManager, * 引用计数归零时自动销毁实例并从缓存中移除。 * @param {string} activityId - 活动ID * @returns {ProgressManager} */ export function getProgressManager(activityId) { if (!managerCache.has(activityId)) { managerCache.set(activityId, { manager: new ProgressManager(activityId), refCount: 0 }) } const entry = managerCache.get(activityId) entry.refCount++ return entry.manager } /** * 释放对进度管理器的引用。 * 引用计数归零时自动销毁实例并清理缓存,防止内存泄漏。 * @param {string} activityId - 活动ID */ export function releaseProgressManager(activityId) { const entry = managerCache.get(activityId) if (!entry) return entry.refCount-- if (entry.refCount <= 0) { entry.manager.destroy() managerCache.delete(activityId) console.log(`[缓存] 进度管理器已释放: ${activityId}`) } } /** * 强制清理指定活动的进度管理器(忽略引用计数) * @param {string} activityId - 活动ID */ export function cleanupProgressManager(activityId) { const entry = managerCache.get(activityId) if (entry) { entry.manager.destroy() managerCache.delete(activityId) } } /** * 清理所有进度管理器 */ export function cleanupAllProgressManagers() { managerCache.forEach(entry => entry.manager.destroy()) managerCache.clear() }