教育类小程序开发:课程播放进度保存与续播功能 分类:公司动态 发布时间:2026-05-20

据《2026年中国在线教育行业发展报告》显示,87.3%的用户会在单次学习15-30分钟后中断,而72.6%的用户表示"无法准确续播"是导致放弃学习的首要原因。课程播放进度保存与续播功能看似简单,却是决定用户留存率、完课率和付费转化率的关键基础设施。本文将小程序开发从技术架构、核心实现、性能优化到安全合规,全面解析这一功能的专业开发方案。
 
一、功能核心价值与设计原则
 
1. 核心业务价值
(1)学习连续性保障:解决用户碎片化学习痛点,支持"随时暂停、随地继续",单次学习时长可从30分钟延长至1.5小时以上
(2)用户体验提升:避免用户手动拖动进度条查找位置,尤其对于1小时以上的专业课程,可节省平均2-3分钟/次的操作时间
(3)数据驱动运营:精确的进度数据为完课率统计、学习行为分析、课程质量评估提供真实依据,比单纯的点击统计准确率提升60%以上
(4)付费转化促进:流畅的学习体验可使课程续费率提升15%-20%,用户推荐率提升25%
 
2. 核心设计原则
(1)数据优先本地:所有进度数据首先写入本地存储,确保网络中断时数据不丢失
(2)异步批量同步:采用"本地缓存+异步上传"模式,减少网络请求次数和服务器压力
(3)冲突智能解决:多设备同步时以"最后更新时间"为唯一判断标准,避免数据混乱
(4)用户可控性:提供明确的续播确认和"从头开始"选项,尊重用户选择权
(5)容错性强:任何环节出现异常都不能影响视频正常播放
 
二、整体技术架构设计
 
1. 分层架构
采用"客户端-服务端"协同架构,明确各层职责:
 
┌─────────────────────────────────────────────────┐
│                  客户端层                        │
│  ┌─────────────┐  ┌─────────────┐  ┌──────────┐ │
│  │ 视频播放器   │  │ 进度采集器  │  │ 本地缓存 │ │
│  └─────────────┘  └─────────────┘  └──────────┘ │
│  ┌─────────────┐  ┌─────────────┐               │
│  │ 续播控制器  │  │ 同步管理器  │               │
│  └─────────────┘  └─────────────┘               │
└───────────────────────────┬─────────────────────┘
                            │
┌───────────────────────────┼─────────────────────┐
│                  网络层                            │
│  ┌─────────────┐  ┌─────────────┐  ┌──────────┐ │
│  │ 请求封装    │  │ 失败重试    │  │ 离线队列 │ │
│  └─────────────┘  └─────────────┘  └──────────┘ │
└───────────────────────────┬─────────────────────┘
                            │
┌───────────────────────────┼─────────────────────┐
│                  服务端层                        │
│  ┌─────────────┐  ┌─────────────┐  ┌──────────┐ │
│  │ 进度接口    │  │ 冲突处理    │  │ 数据校验 │ │
│  └─────────────┘  └─────────────┘  └──────────┘ │
│  ┌─────────────┐  ┌─────────────┐               │
│  │ 持久化存储  │  │ 缓存服务    │               │
│  └─────────────┘  └─────────────┘               │
└─────────────────────────────────────────────────┘
 
2. 核心数据流程
(1)用户进入课程播放页,客户端首先从本地缓存加载历史进度
(2)同时异步向服务端请求最新进度,进行数据合并
(3)播放过程中,进度采集器定时采集播放时间并写入本地缓存
(4)同步管理器按照预设策略将本地进度批量上传到服务端
(5)服务端验证数据有效性,解决多设备冲突后持久化存储
(6)用户再次进入课程时,重复步骤1-2,自动跳转到最新进度位置
 
3. 数据模型设计
 
(1)客户端本地存储模型
 
// 键名规范:course_progress_{userId}_{courseId}_{chapterId}
{
  "courseId": "course_20260520001",
  "chapterId": "chapter_003",
  "currentTime": 1234.567, // 精确到毫秒的播放时间(秒)
  "totalTime": 3600.000,
  "progress": 0.3429, // 0-1之间的小数,保留4位
  "isCompleted": false,
  "lastUpdateTime": 1716166502000, // 本地最后更新时间戳
  "isSynced": false, // 是否已同步到服务端
  "syncRetryCount": 0, // 同步失败重试次数
  "deviceId": "wx_1a2b3c4d5e6f7g8h"
}
 
(2)服务端数据库模型
 
CREATE TABLE `course_play_progress` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint unsigned NOT NULL COMMENT '用户ID',
  `course_id` varchar(64) NOT NULL COMMENT '课程ID',
  `chapter_id` varchar(64) NOT NULL COMMENT '章节ID',
  `current_time` decimal(10,3) NOT NULL DEFAULT '0.000' COMMENT '当前播放时间(秒)',
  `total_time` decimal(10,3) NOT NULL DEFAULT '0.000' COMMENT '视频总时长(秒)',
  `progress` decimal(5,4) NOT NULL DEFAULT '0.0000' COMMENT '播放进度百分比',
  `is_completed` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否完成观看(0:否,1:是)',
  `last_play_device` varchar(128) DEFAULT NULL COMMENT '最后播放设备标识',
  `last_play_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后播放时间',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_course_chapter` (`user_id`,`course_id`,`chapter_id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_course_id` (`course_id`),
  KEY `idx_last_play_time` (`last_play_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程播放进度表';
 
三、客户端核心功能实现
 
1. 进度采集策略
进度采集的准确性直接决定续播体验,需采用"定时采集+关键节点采集"的混合策略:
 
// 进度采集防抖函数,避免频繁触发
const debounce = (fn, delay = 300) => {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
};
 
Page({
  data: {
    courseId: '',
    chapterId: '',
    videoSrc: '',
    currentTime: 0,
    totalTime: 0,
    isPlaying: false
  },
 
  onLoad(options) {
    this.setData({
      courseId: options.courseId,
      chapterId: options.chapterId,
      videoSrc: options.videoSrc
    });
    this.videoContext = wx.createVideoContext('courseVideo');
    this.initProgressCollector();
    this.initResumePlay();
  },
 
  // 初始化进度采集器
  initProgressCollector() {
    // 定时采集:每10秒一次(防抖处理后实际约10.3秒)
    this.debouncedSave = debounce(this.saveProgress.bind(this), 300);
    this.timer = setInterval(() => {
      if (this.data.isPlaying) {
        this.debouncedSave();
      }
    }, 10000);
  },
 
  // 关键节点事件监听
  onTimeUpdate(e) {
    const { currentTime, duration } = e.detail;
    this.setData({
      currentTime,
      totalTime: duration || this.data.totalTime
    });
  },
 
  onPlay() {
    this.setData({ isPlaying: true });
  },
 
  onPause() {
    this.setData({ isPlaying: false });
    this.saveProgress(); // 暂停时立即保存
  },
 
  onEnded() {
    this.setData({
      isPlaying: false,
      currentTime: this.data.totalTime,
      isCompleted: true
    });
    this.saveProgress(); // 播放结束时立即保存
  },
 
  onSeeked(e) {
    this.setData({ currentTime: e.detail.currentTime });
    this.saveProgress(); // 拖动进度条后立即保存
  },
 
  onUnload() {
    clearInterval(this.timer);
    this.saveProgress(); // 页面卸载时立即保存
  },
 
  onHide() {
    this.saveProgress(); // 小程序切后台时立即保存
  }
});
 
2. 本地缓存与同步管理
本地缓存是保障数据可靠性的第一道防线,需实现完整的缓存管理和同步机制:
 
// 本地存储工具类
const ProgressStorage = {
  // 生成存储键名
  getKey(userId, courseId, chapterId) {
    return `course_progress_${userId}_${courseId}_${chapterId}`;
  },
 
  // 保存进度到本地
  save(userId, courseId, chapterId, data) {
    const key = this.getKey(userId, courseId, chapterId);
    try {
      const progressData = {
        ...data,
        lastUpdateTime: Date.now(),
        isSynced: false,
        syncRetryCount: 0
      };
      wx.setStorageSync(key, progressData);
      return true;
    } catch (e) {
      console.error('本地进度保存失败:', e);
      return false;
    }
  },
 
  // 从本地获取进度
  get(userId, courseId, chapterId) {
    const key = this.getKey(userId, courseId, chapterId);
    try {
      return wx.getStorageSync(key) || null;
    } catch (e) {
      console.error('本地进度获取失败:', e);
      return null;
    }
  },
 
  // 获取所有未同步的进度
  getUnsyncedList(userId) {
    const unsyncedList = [];
    const keys = wx.getStorageInfoSync().keys;
    const prefix = `course_progress_${userId}_`;
    
    keys.forEach(key => {
      if (key.startsWith(prefix)) {
        const progress = wx.getStorageSync(key);
        if (progress && !progress.isSynced && progress.syncRetryCount < 3) {
          unsyncedList.push(progress);
        }
      }
    });
    
    return unsyncedList;
  },
 
  // 标记为已同步
  markAsSynced(userId, courseId, chapterId) {
    const key = this.getKey(userId, courseId, chapterId);
    const progress = this.get(userId, courseId, chapterId);
    if (progress) {
      progress.isSynced = true;
      wx.setStorageSync(key, progress);
    }
  },
 
  // 清理过期数据(保留30天)
  cleanExpired(userId) {
    const keys = wx.getStorageInfoSync().keys;
    const prefix = `course_progress_${userId}_`;
    const now = Date.now();
    const expireTime = 30 * 24 * 60 * 60 * 1000;
    
    keys.forEach(key => {
      if (key.startsWith(prefix)) {
        const progress = wx.getStorageSync(key);
        if (progress && now progress.lastUpdateTime > expireTime) {
          wx.removeStorageSync(key);
        }
      }
    });
  }
};
 
3. 续播逻辑实现
续播逻辑的核心是"本地优先、服务端补充、用户确认":
 
// 初始化续播
async initResumePlay() {
  const { courseId, chapterId } = this.data;
  const userId = wx.getStorageSync('userId');
  
  // 1. 从本地获取进度
  const localProgress = ProgressStorage.get(userId, courseId, chapterId);
  
  // 2. 异步从服务端获取最新进度
  let serverProgress = null;
  try {
    const res = await wx.request({
      url: `${getApp().globalData.baseUrl}/api/course/progress/get`,
      method: 'GET',
      data: { courseId, chapterId },
      header: { 'Authorization': `Bearer ${wx.getStorageSync('token')}` }
    });
    
    if (res.data.code === 200 && res.data.data) {
      serverProgress = res.data.data;
      // 将服务端进度同步到本地
      ProgressStorage.save(userId, courseId, chapterId, serverProgress);
    }
  } catch (e) {
    console.error('服务端进度获取失败:', e);
  }
  
  // 3. 合并进度,取更新时间较新的
  const latestProgress = this.mergeProgress(localProgress, serverProgress);
  
  // 4. 判断是否需要续播
  if (latestProgress && latestProgress.currentTime > 5 && latestProgress.progress < 0.95) {
    this.showResumeModal(latestProgress);
  } else {
    // 没有有效进度或已完成,从头开始
    this.videoContext.seek(0);
    this.videoContext.play();
  }
},
 
// 合并本地和服务端进度
mergeProgress(local, server) {
  if (!local && !server) return null;
  if (!local) return server;
  if (!server) return local;
  
  // 以最后更新时间为准
  return local.lastUpdateTime > new Date(server.lastPlayTime).getTime() 
    ? local 
    : server;
},
 
// 显示续播确认弹窗
showResumeModal(progress) {
  const timeStr = this.formatTime(progress.currentTime);
  wx.showModal({
    title: '继续学习',
    content: `上次看到 ${timeStr},是否继续播放?`,
    confirmText: '继续播放',
    cancelText: '从头开始',
    success: (res) => {
      if (res.confirm) {
        this.videoContext.seek(progress.currentTime);
        this.videoContext.play();
      } else {
        this.videoContext.seek(0);
        this.videoContext.play();
      }
    }
  });
},
 
// 格式化时间
formatTime(seconds) {
  const h = Math.floor(seconds / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = Math.floor(seconds % 60);
  
  if (h > 0) {
    return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
  } else {
    return `${m}:${s.toString().padStart(2, '0')}`;
  }
}
 
四、服务端核心功能实现
 
1. 进度同步接口
服务端接口需实现数据验证、幂等性保证和冲突解决:
 
// Node.js + Express 服务端实现
const express = require('express');
const router = express.Router();
const { CourseProgress } = require('../models');
 
// 批量同步进度接口
router.post('/sync', async (req, res) => {
  const userId = req.user.id; // 从JWT中获取用户ID
  const { progressList } = req.body;
  
  if (!Array.isArray(progressList) || progressList.length === 0) {
    return res.status(400).json({ code: 400, message: '进度数据不能为空' });
  }
  
  try {
    const results = [];
    
    for (const progress of progressList) {
      const { courseId, chapterId, currentTime, totalTime, isCompleted, lastUpdateTime } = progress;
      
      // 数据验证
      if (!courseId || !chapterId || currentTime < 0 || totalTime <= 0 || currentTime > totalTime) {
        results.push({ courseId, chapterId, status: 'failed', message: '无效数据' });
        continue;
      }
      
      // 查找现有记录
      const existingProgress = await CourseProgress.findOne({
        where: { userId, courseId, chapterId }
      });
      
      if (existingProgress) {
        // 冲突解决:以客户端最后更新时间为准
        const clientUpdateTime = new Date(lastUpdateTime);
        if (clientUpdateTime > existingProgress.lastPlayTime) {
          await existingProgress.update({
            currentTime,
            totalTime,
            progress: Math.min(currentTime / totalTime, 1.0),
            isCompleted: isCompleted || currentTime >= totalTime * 0.95,
            lastPlayDevice: req.headers['user-agent'],
            lastPlayTime: clientUpdateTime
          });
          results.push({ courseId, chapterId, status: 'success' });
        } else {
          results.push({ courseId, chapterId, status: 'skipped', message: '服务端数据更新' });
        }
      } else {
        // 创建新记录
        await CourseProgress.create({
          userId,
          courseId,
          chapterId,
          currentTime,
          totalTime,
          progress: Math.min(currentTime / totalTime, 1.0),
          isCompleted: isCompleted || currentTime >= totalTime * 0.95,
          lastPlayDevice: req.headers['user-agent'],
          lastPlayTime: new Date(lastUpdateTime)
        });
        results.push({ courseId, chapterId, status: 'success' });
      }
    }
    
    res.json({ code: 200, message: '同步完成', data: results });
  } catch (error) {
    console.error('进度同步失败:', error);
    res.status(500).json({ code: 500, message: '服务器内部错误' });
  }
});
 
// 获取单章节进度接口
router.get('/get', async (req, res) => {
  const userId = req.user.id;
  const { courseId, chapterId } = req.query;
  
  if (!courseId || !chapterId) {
    return res.status(400).json({ code: 400, message: '参数不全' });
  }
  
  try {
    const progress = await CourseProgress.findOne({
      where: { userId, courseId, chapterId },
      attributes: ['courseId', 'chapterId', 'currentTime', 'totalTime', 'progress', 'isCompleted', 'lastPlayTime']
    });
    
    res.json({ code: 200, data: progress });
  } catch (error) {
    console.error('进度查询失败:', error);
    res.status(500).json({ code: 500, message: '服务器内部错误' });
  }
});
 
2. 缓存优化
对于热门课程和活跃用户,使用Redis缓存进度数据,提高查询速度:
 
const redis = require('../config/redis');
 
// 带缓存的进度查询
async function getProgressWithCache(userId, courseId, chapterId) {
  const cacheKey = `progress:${userId}:${courseId}:${chapterId}`;
  
  // 先从Redis获取
  const cachedData = await redis.get(cacheKey);
  if (cachedData) {
    return JSON.parse(cachedData);
  }
  
  // 从数据库获取
  const progress = await CourseProgress.findOne({
    where: { userId, courseId, chapterId },
    attributes: ['courseId', 'chapterId', 'currentTime', 'totalTime', 'progress', 'isCompleted', 'lastPlayTime']
  });
  
  // 写入Redis,缓存1小时
  if (progress) {
    await redis.setex(cacheKey, 3600, JSON.stringify(progress));
  }
  
  return progress;
}
 
// 更新进度时清除缓存
async function updateProgress(userId, courseId, chapterId, data) {
  const cacheKey = `progress:${userId}:${courseId}:${chapterId}`;
  
  // 更新数据库
  await CourseProgress.update(data, {
    where: { userId, courseId, chapterId }
  });
  
  // 清除缓存
  await redis.del(cacheKey);
}
 
五、高级功能扩展
 
1. 多设备同步
实现多设备无缝同步的关键是服务端作为唯一数据源,客户端定期拉取:
 
// 客户端定期同步服务端进度
startServerSync() {
  // 每5分钟从服务端拉取一次最新进度
  this.syncTimer = setInterval(async () => {
    const { courseId, chapterId } = this.data;
    const userId = wx.getStorageSync('userId');
    
    try {
      const res = await wx.request({
        url: `${getApp().globalData.baseUrl}/api/course/progress/get`,
        method: 'GET',
        data: { courseId, chapterId },
        header: { 'Authorization': `Bearer ${wx.getStorageSync('token')}` }
      });
      
      if (res.data.code === 200 && res.data.data) {
        const serverProgress = res.data.data;
        const localProgress = ProgressStorage.get(userId, courseId, chapterId);
        
        // 如果服务端进度更新,更新本地并提示用户
        if (serverProgress && new Date(serverProgress.lastPlayTime).getTime() > localProgress.lastUpdateTime) {
          ProgressStorage.save(userId, courseId, chapterId, serverProgress);
          
          // 如果当前没有播放,提示用户有更新的进度
          if (!this.data.isPlaying) {
            wx.showToast({
              title: '已同步其他设备进度',
              icon: 'success',
              duration: 2000
            });
          }
        }
      }
    } catch (e) {
      console.error('服务端进度同步失败:', e);
    }
  }, 300000); // 5分钟
}
 
2. 离线播放进度同步
支持离线下载视频的教育小程序开发,需实现离线进度的自动同步:
 
// 离线播放进度保存
saveOfflineProgress(courseId, chapterId, currentTime, totalTime) {
  const userId = wx.getStorageSync('userId');
  ProgressStorage.save(userId, courseId, chapterId, {
    currentTime,
    totalTime,
    progress: currentTime / totalTime,
    isCompleted: currentTime >= totalTime * 0.95
  });
  
  // 添加到离线同步队列
  const offlineQueue = wx.getStorageSync('offlineProgressQueue') || [];
  const key = `${userId}_${courseId}_${chapterId}`;
  
  // 去重
  const index = offlineQueue.findIndex(item => item.key === key);
  if (index > -1) {
    offlineQueue.splice(index, 1);
  }
  
  offlineQueue.push({
    key,
    userId,
    courseId,
    chapterId,
    addTime: Date.now()
  });
  
  wx.setStorageSync('offlineProgressQueue', offlineQueue);
},
 
// 联网后自动同步离线进度
syncOfflineProgress() {
  const offlineQueue = wx.getStorageSync('offlineProgressQueue') || [];
  if (offlineQueue.length === 0) return;
  
  const userId = wx.getStorageSync('userId');
  const progressList = [];
  
  offlineQueue.forEach(item => {
    if (item.userId === userId) {
      const progress = ProgressStorage.get(item.userId, item.courseId, item.chapterId);
      if (progress) {
        progressList.push(progress);
      }
    }
  });
  
  if (progressList.length > 0) {
    wx.request({
      url: `${getApp().globalData.baseUrl}/api/course/progress/sync`,
      method: 'POST',
      data: { progressList },
      header: { 'Authorization': `Bearer ${wx.getStorageSync('token')}` },
      success: (res) => {
        if (res.data.code === 200) {
          // 同步成功,清除队列
          wx.setStorageSync('offlineProgressQueue', []);
          wx.showToast({
            title: '离线进度已同步',
            icon: 'success'
          });
        }
      }
    });
  }
}
 
六、性能优化与异常处理
 
1. 性能优化要点
(1)减少网络请求:采用批量上传策略,每次最多上传10条进度数据
(2)防抖节流:对timeupdate事件进行防抖处理,避免每秒触发多次
(3)数据库优化:建立联合唯一索引,避免重复数据;使用批量插入和更新
(4)缓存策略:热点数据缓存到Redis,过期时间设置为1小时
(5)资源预加载:预加载下一章的视频信息和进度数据,提高切换速度
 
2. 异常处理机制
(1)网络异常:网络中断时,所有进度数据保存到本地,联网后自动重试
(2)服务端异常:服务端返回500错误时,使用本地进度继续播放,3分钟后重试
(3)数据异常:对采集到的进度数据进行范围验证,过滤currentTime < 0或currentTime > totalTime的无效数据
(4)播放器异常:监听video组件的error事件,出现错误时提示用户并尝试重新加载视频
 
七、安全与合规要求
 
1. 数据安全
(1)用户认证:所有进度接口必须携带有效的JWT令牌,验证用户身份
(2)数据隔离:不同用户的进度数据严格隔离,通过userId进行权限控制
(3)传输加密:使用HTTPS协议传输数据,防止数据被窃听和篡改
(4)数据备份:每日备份数据库,保留30天的备份数据,防止数据丢失
 
2. 合规要求
(1)隐私政策:在隐私政策中明确说明收集播放进度数据的目的、方式和范围
(2)用户授权:在用户首次使用时,明确告知并获取用户同意
(3)数据删除:用户注销账号时,必须删除所有相关的播放进度数据
(4)数据保留:按照《个人信息保护法》要求,数据保留期限不超过服务所需的必要期限
 
八、测试要点与验收标准
 
1. 功能测试要点
(1)正常播放、暂停、退出时进度是否正确保存
(2)再次进入课程时是否正确跳转到上次播放位置
(3)多设备登录时进度是否正确同步
(4)离线播放时进度是否正确记录,联网后是否自动同步
(5)视频播放完成后是否正确标记为已完成
(6)拖动进度条后进度是否正确更新
(7)小程序切后台再切回时进度是否正确保存
 
2. 性能测试要点
(1)1000用户同时上传进度时服务端的响应时间
(2)长视频(2小时以上)的进度采集和续播性能
(3)弱网络环境下的功能表现
(4)小程序内存占用情况
 
3. 验收标准
(1)进度保存准确率达到99.9%以上
(2)续播位置误差不超过1秒
(3)多设备同步延迟不超过5分钟
(4)服务端接口响应时间不超过200ms
(5)异常场景下无数据丢失
 
在实际小程序开发中,开发者应根据自身平台的特点进行适当调整:对于以短视频课程为主的平台,可适当提高进度采集频率;对于支持离线下载的平台,需重点优化离线进度同步机制;对于多端产品,需统一数据格式和同步策略。
在线咨询
服务项目
获取报价
意见反馈
返回顶部