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