guoyu/fronted_uniapp/utils/progressQueue.js
2025-12-03 18:58:36 +08:00

319 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 学习进度上报队列管理器
* 实现断网重连、本地缓存、批量上报等功能
*/
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