/** * 学习进度上报队列管理器 * 实现断网重连、本地缓存、批量上报等功能 */ import request from './request.js' class ProgressQueue { constructor() { this.queue = [] // 待上报的进度数据队列 this.isUploading = false // 是否正在上传 this.retryCount = 0 // 重试次数 this.maxRetryCount = 3 // 最大重试次数 this.uploadInterval = 5000 // 上传间隔(毫秒) this.uploadTimer = null // 上传定时器 this.storageKey = 'learning_progress_queue' // 本地存储key this.isOnline = true // 网络状态 // 监听网络状态 this.initNetworkListener() // 从本地存储恢复队列 this.loadFromStorage() } /** * 初始化网络状态监听 */ initNetworkListener() { // 监听网络状态变化 uni.onNetworkStatusChange((res) => { const wasOffline = !this.isOnline this.isOnline = res.isConnected if (wasOffline && this.isOnline) { // 网络恢复,立即尝试上传队列 console.log('网络已恢复,开始上传队列') this.processQueue() } else if (!this.isOnline) { console.log('网络已断开,暂停上传') } }) // 获取初始网络状态 uni.getNetworkType({ success: (res) => { this.isOnline = res.networkType !== 'none' } }) } /** * 添加进度数据到队列 * @param {Object} data 进度数据 * @param {Function} onSuccess 上报成功后的回调函数(可选) */ addProgress(data, onSuccess = null) { console.log('[进度队列] 📥 添加进度数据:', data) const progressData = { id: Date.now() + Math.random(), // 唯一ID courseId: data.courseId, coursewareId: data.coursewareId || null, // 课件ID duration: data.duration || 0, videoPosition: data.videoPosition || 0, videoTotalDuration: data.videoTotalDuration || 0, timestamp: Date.now(), onSuccess: onSuccess // 回调函数 } console.log('[进度队列] 构建的进度数据:', progressData) // 检查是否有相同课程和课件的未上报数据,合并它们 const existingIndex = this.queue.findIndex(item => item.courseId === progressData.courseId && item.coursewareId === progressData.coursewareId && !item.uploaded ) if (existingIndex >= 0) { // 合并数据:累加时长,更新视频位置 const existing = this.queue[existingIndex] existing.duration = (existing.duration || 0) + (progressData.duration || 0) existing.videoPosition = progressData.videoPosition // 使用最新的位置 existing.videoTotalDuration = progressData.videoTotalDuration existing.timestamp = progressData.timestamp // 合并回调函数(如果有多个,只保留最新的) if (onSuccess) { existing.onSuccess = onSuccess } } else { // 添加新数据 this.queue.push(progressData) } // 保存到本地存储 this.saveToStorage() console.log('[进度队列] 当前队列状态:', this.getStatus()) console.log('[进度队列] 网络状态:', this.isOnline ? '在线' : '离线') // 如果网络正常,立即尝试上传 if (this.isOnline) { console.log('[进度队列] 🚀 开始处理队列...') this.processQueue() } else { console.log('[进度队列] ⏸️ 网络离线,等待网络恢复后上传') } } /** * 处理队列(上传) */ async processQueue() { if (this.isUploading || !this.isOnline || this.queue.length === 0) { return } this.isUploading = true // 获取未上传的数据 const pendingItems = this.queue.filter(item => !item.uploaded) if (pendingItems.length === 0) { this.isUploading = false return } // 批量上传(每次最多5条) const batchSize = 5 const batches = [] for (let i = 0; i < pendingItems.length; i += batchSize) { batches.push(pendingItems.slice(i, i + batchSize)) } for (const batch of batches) { try { // 批量上报 await this.uploadBatch(batch) // 标记为已上传并执行回调 batch.forEach(item => { item.uploaded = true item.uploadTime = Date.now() // 执行成功回调 if (item.onSuccess && typeof item.onSuccess === 'function') { try { item.onSuccess(item) } catch (error) { console.error('执行上报成功回调失败:', error) } } }) // 从队列中移除已上传的数据(保留最近10条用于调试) this.queue = this.queue.filter(item => !item.uploaded || (Date.now() - item.uploadTime < 60000) ) // 保存到本地存储 this.saveToStorage() this.retryCount = 0 // 重置重试次数 } catch (error) { console.error('批量上传失败:', error) this.retryCount++ if (this.retryCount >= this.maxRetryCount) { console.error('达到最大重试次数,停止上传') this.isUploading = false return } // 等待后重试 await this.delay(2000 * this.retryCount) } } this.isUploading = false // 如果还有未上传的数据,继续处理 if (this.queue.some(item => !item.uploaded)) { setTimeout(() => { this.processQueue() }, this.uploadInterval) } } /** * 批量上传进度数据 */ async uploadBatch(batch) { console.log('[进度上报] 准备上报', batch.length, '条记录') // 如果只有一条数据,使用单条上报接口 if (batch.length === 1) { const item = batch[0] console.log('[进度上报] 单条上报:', { courseId: item.courseId, coursewareId: item.coursewareId, duration: item.duration, videoPosition: item.videoPosition }) const response = await request.post('/study/learningRecord/progress', { courseId: item.courseId, coursewareId: item.coursewareId, duration: item.duration, videoPosition: item.videoPosition, videoTotalDuration: item.videoTotalDuration }) console.log('[进度上报] ✅ 上报成功:', response) return } // 多条数据,按课程和课件分组后批量上报 const groupedByCourseware = {} batch.forEach(item => { const key = `${item.courseId}_${item.coursewareId || 'null'}` if (!groupedByCourseware[key]) { groupedByCourseware[key] = { courseId: item.courseId, coursewareId: item.coursewareId, duration: 0, videoPosition: 0, videoTotalDuration: 0 } } const group = groupedByCourseware[key] group.duration += item.duration || 0 group.videoPosition = item.videoPosition // 使用最新的位置 group.videoTotalDuration = item.videoTotalDuration }) // 逐个上报(如果后端支持批量接口,可以改为一次请求) for (const key in groupedByCourseware) { const data = groupedByCourseware[key] await request.post('/study/learningRecord/progress', data) } } /** * 延迟函数 */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } /** * 保存队列到本地存储 */ saveToStorage() { try { // 只保存未上传的数据和最近的数据 const dataToSave = this.queue.filter(item => !item.uploaded || (Date.now() - item.uploadTime < 60000) ) uni.setStorageSync(this.storageKey, JSON.stringify(dataToSave)) } catch (error) { console.error('保存队列到本地存储失败:', error) } } /** * 从本地存储加载队列 */ loadFromStorage() { try { const stored = uni.getStorageSync(this.storageKey) if (stored) { const data = JSON.parse(stored) // 只恢复未上传的数据 this.queue = data.filter(item => !item.uploaded) || [] // 如果队列不为空且网络正常,尝试上传 if (this.queue.length > 0 && this.isOnline) { setTimeout(() => { this.processQueue() }, 1000) } } } catch (error) { console.error('从本地存储加载队列失败:', error) } } /** * 清空队列 */ clear() { this.queue = [] try { uni.removeStorageSync(this.storageKey) } catch (error) { console.error('清空队列失败:', error) } } /** * 获取队列状态 */ getStatus() { const pending = this.queue.filter(item => !item.uploaded).length return { total: this.queue.length, pending: pending, uploaded: this.queue.length - pending, isOnline: this.isOnline, isUploading: this.isUploading } } } // 创建单例 const progressQueue = new ProgressQueue() export default progressQueue