学习进度追踪与分析系统设计指南

HTMLPAGE 团队
18分钟 分钟阅读

全面讲解在线教育平台中学习进度追踪的核心设计,包括学习行为采集、进度计算、数据可视化和智能预警等功能实现。

#在线教育 #学习分析 #进度追踪 #数据可视化 #学习行为

学习进度追踪的价值

学习进度追踪不仅是展示"完成了多少",更重要的是通过数据分析发现学习问题,提供个性化的学习建议。

应用场景关键指标决策支持
学生自学完成率、学习时长调整学习计划
教师教学班级整体进度、落后预警针对性辅导
家长监督每日学习时长、任务完成情况了解学习状态
平台运营课程热度、完课率优化课程内容

学习行为数据采集

行为事件定义

// 学习行为事件类型
type LearningEventType = 
  | 'video-play'          // 开始播放视频
  | 'video-pause'         // 暂停视频
  | 'video-seek'          // 跳转进度
  | 'video-complete'      // 视频播放完成
  | 'document-open'       // 打开文档
  | 'document-scroll'     // 滚动文档
  | 'quiz-start'          // 开始测验
  | 'quiz-submit'         // 提交测验
  | 'assignment-start'    // 开始作业
  | 'assignment-submit'   // 提交作业
  | 'discussion-post'     // 发表讨论
  | 'note-create'         // 创建笔记

interface LearningEvent {
  id: string
  type: LearningEventType
  userId: string
  courseId: string
  lessonId: string
  resourceId?: string     // 具体资源ID
  timestamp: Date
  duration?: number       // 持续时长(秒)
  metadata: Record<string, any>
  sessionId: string       // 学习会话ID
}

前端埋点实现

// 视频播放追踪
function setupVideoTracking(videoElement: HTMLVideoElement, lessonId: string) {
  let playStartTime: number | null = null
  let totalWatchTime = 0
  const watchedSegments: [number, number][] = []
  
  videoElement.addEventListener('play', () => {
    playStartTime = Date.now()
    trackEvent({
      type: 'video-play',
      lessonId,
      metadata: { currentTime: videoElement.currentTime }
    })
  })
  
  videoElement.addEventListener('pause', () => {
    if (playStartTime) {
      const watchDuration = (Date.now() - playStartTime) / 1000
      totalWatchTime += watchDuration
      
      trackEvent({
        type: 'video-pause',
        lessonId,
        duration: watchDuration,
        metadata: { 
          currentTime: videoElement.currentTime,
          totalWatchTime 
        }
      })
      playStartTime = null
    }
  })
  
  videoElement.addEventListener('ended', () => {
    trackEvent({
      type: 'video-complete',
      lessonId,
      metadata: { 
        videoDuration: videoElement.duration,
        totalWatchTime,
        watchedPercentage: (totalWatchTime / videoElement.duration) * 100
      }
    })
  })
}

关键说明: 视频学习不能仅依赖"播放完成"事件判断是否学完。需要记录实际观看时长和观看片段,避免学生快进跳过内容。

学习会话管理

interface LearningSession {
  id: string
  userId: string
  courseId: string
  startTime: Date
  endTime?: Date
  activeTime: number       // 活跃时长(排除发呆时间)
  events: LearningEvent[]
  deviceInfo: DeviceInfo
}

// 检测用户活跃状态
function createActivityDetector(onIdle: () => void, onActive: () => void) {
  let idleTimer: NodeJS.Timeout
  const IDLE_THRESHOLD = 5 * 60 * 1000  // 5分钟无操作视为发呆
  
  function resetTimer() {
    clearTimeout(idleTimer)
    onActive()
    idleTimer = setTimeout(onIdle, IDLE_THRESHOLD)
  }
  
  // 监听用户活动
  ;['mousemove', 'keydown', 'scroll', 'click'].forEach(event => {
    document.addEventListener(event, resetTimer, { passive: true })
  })
  
  resetTimer()
}

进度计算模型

课程完成度计算

interface CourseProgress {
  courseId: string
  userId: string
  overallProgress: number    // 总体完成度 0-100
  lessonProgress: LessonProgress[]
  totalLearningTime: number  // 总学习时长(分钟)
  lastAccessedAt: Date
  startedAt: Date
  completedAt?: Date
}

interface LessonProgress {
  lessonId: string
  status: 'not-started' | 'in-progress' | 'completed'
  progress: number           // 0-100
  videoProgress?: number     // 视频观看进度
  quizScore?: number         // 测验分数
  assignmentScore?: number   // 作业分数
}

async function calculateCourseProgress(
  userId: string,
  courseId: string
): Promise<CourseProgress> {
  const course = await getCourse(courseId)
  const events = await getLearningEvents(userId, courseId)
  
  const lessonProgress = await Promise.all(
    course.lessons.map(lesson => calculateLessonProgress(userId, lesson, events))
  )
  
  // 按权重计算总进度
  const weightedProgress = lessonProgress.reduce((sum, lp, index) => {
    const weight = course.lessons[index].weight || 1
    return sum + lp.progress * weight
  }, 0)
  
  const totalWeight = course.lessons.reduce((sum, l) => sum + (l.weight || 1), 0)
  
  return {
    courseId,
    userId,
    overallProgress: Math.round(weightedProgress / totalWeight),
    lessonProgress,
    totalLearningTime: calculateTotalLearningTime(events),
    lastAccessedAt: getLastEventTime(events),
    startedAt: getFirstEventTime(events),
    completedAt: lessonProgress.every(lp => lp.status === 'completed') 
      ? new Date() 
      : undefined
  }
}

单课时进度计算

async function calculateLessonProgress(
  userId: string,
  lesson: Lesson,
  events: LearningEvent[]
): Promise<LessonProgress> {
  const lessonEvents = events.filter(e => e.lessonId === lesson.id)
  
  // 根据课时类型计算进度
  let progress = 0
  const components: { weight: number; progress: number }[] = []
  
  // 视频进度
  if (lesson.videoId) {
    const videoProgress = calculateVideoProgress(lessonEvents, lesson.videoDuration)
    components.push({ weight: 0.5, progress: videoProgress })
  }
  
  // 测验进度
  if (lesson.quizId) {
    const quizResult = await getQuizResult(userId, lesson.quizId)
    const quizProgress = quizResult ? 100 : 0
    components.push({ weight: 0.3, progress: quizProgress })
  }
  
  // 文档阅读进度
  if (lesson.documentId) {
    const docProgress = calculateDocumentProgress(lessonEvents)
    components.push({ weight: 0.2, progress: docProgress })
  }
  
  // 加权平均
  if (components.length > 0) {
    progress = components.reduce((sum, c) => sum + c.weight * c.progress, 0) /
               components.reduce((sum, c) => sum + c.weight, 0)
  }
  
  return {
    lessonId: lesson.id,
    status: progress === 0 ? 'not-started' : progress >= 100 ? 'completed' : 'in-progress',
    progress: Math.min(100, Math.round(progress)),
    videoProgress: components.find(c => c.weight === 0.5)?.progress
  }
}

function calculateVideoProgress(
  events: LearningEvent[],
  videoDuration: number
): number {
  // 合并观看片段,计算有效覆盖率
  const segments = events
    .filter(e => e.type === 'video-pause' || e.type === 'video-complete')
    .map(e => [
      e.metadata.currentTime - e.duration,
      e.metadata.currentTime
    ] as [number, number])
  
  const mergedSegments = mergeOverlappingSegments(segments)
  const watchedDuration = mergedSegments.reduce(
    (sum, [start, end]) => sum + (end - start), 0
  )
  
  return Math.min(100, (watchedDuration / videoDuration) * 100)
}

进度可视化展示

学习路径图

// 课程学习路径数据结构
interface LearningPath {
  nodes: PathNode[]
  edges: PathEdge[]
}

interface PathNode {
  id: string
  label: string
  type: 'lesson' | 'checkpoint' | 'milestone'
  status: 'locked' | 'available' | 'in-progress' | 'completed'
  progress: number
  position: { x: number; y: number }
}

// 渲染学习路径图
function renderLearningPath(container: HTMLElement, path: LearningPath) {
  // 使用 D3.js 或 Canvas 绘制
  path.nodes.forEach(node => {
    const element = createNodeElement(node)
    
    // 根据状态设置样式
    element.classList.add(`status-${node.status}`)
    
    // 添加进度指示器
    if (node.status === 'in-progress') {
      const progressRing = createProgressRing(node.progress)
      element.appendChild(progressRing)
    }
    
    container.appendChild(element)
  })
}

学习日历热力图

展示每日学习情况,类似 GitHub 贡献图:

interface DailyLearning {
  date: string           // YYYY-MM-DD
  learningTime: number   // 分钟
  lessonsCompleted: number
  intensity: 0 | 1 | 2 | 3 | 4  // 学习强度等级
}

function calculateLearningIntensity(learningTime: number): number {
  if (learningTime === 0) return 0
  if (learningTime < 30) return 1
  if (learningTime < 60) return 2
  if (learningTime < 120) return 3
  return 4
}

// 生成过去一年的学习数据
async function getLearningHeatmap(userId: string): Promise<DailyLearning[]> {
  const startDate = dayjs().subtract(1, 'year')
  const endDate = dayjs()
  
  const events = await db.learningEvents.findMany({
    where: {
      userId,
      timestamp: { gte: startDate.toDate(), lte: endDate.toDate() }
    }
  })
  
  // 按日期分组统计
  const dailyStats = groupBy(events, e => dayjs(e.timestamp).format('YYYY-MM-DD'))
  
  const result: DailyLearning[] = []
  let current = startDate
  
  while (current.isBefore(endDate)) {
    const dateStr = current.format('YYYY-MM-DD')
    const dayEvents = dailyStats[dateStr] || []
    const learningTime = sumLearningTime(dayEvents)
    
    result.push({
      date: dateStr,
      learningTime,
      lessonsCompleted: countCompletedLessons(dayEvents),
      intensity: calculateLearningIntensity(learningTime)
    })
    
    current = current.add(1, 'day')
  }
  
  return result
}

智能预警与建议

学习落后预警

interface LearningAlert {
  type: 'behind-schedule' | 'inactive' | 'struggling' | 'at-risk'
  severity: 'low' | 'medium' | 'high'
  userId: string
  message: string
  suggestions: string[]
  triggerData: any
}

async function checkLearningAlerts(userId: string): Promise<LearningAlert[]> {
  const alerts: LearningAlert[] = []
  const progress = await getUserProgress(userId)
  const schedule = await getLearningSchedule(userId)
  
  // 进度落后检测
  if (schedule) {
    const expectedProgress = calculateExpectedProgress(schedule)
    const actualProgress = progress.overallProgress
    const gap = expectedProgress - actualProgress
    
    if (gap > 20) {
      alerts.push({
        type: 'behind-schedule',
        severity: gap > 40 ? 'high' : 'medium',
        userId,
        message: `学习进度落后预期 ${gap}%`,
        suggestions: [
          '建议每天增加30分钟学习时间',
          '优先完成未完成的测验',
          '可以考虑调整学习计划'
        ],
        triggerData: { expectedProgress, actualProgress, gap }
      })
    }
  }
  
  // 长时间未学习检测
  const lastActivity = await getLastLearningActivity(userId)
  const daysSinceLastActivity = dayjs().diff(lastActivity, 'day')
  
  if (daysSinceLastActivity >= 7) {
    alerts.push({
      type: 'inactive',
      severity: daysSinceLastActivity >= 14 ? 'high' : 'medium',
      userId,
      message: `已有 ${daysSinceLastActivity} 天未学习`,
      suggestions: [
        '设置每日学习提醒',
        '从简短的复习开始恢复学习'
      ],
      triggerData: { daysSinceLastActivity, lastActivity }
    })
  }
  
  return alerts
}

个性化学习建议

interface LearningRecommendation {
  type: 'next-lesson' | 'review' | 'practice' | 'challenge'
  resourceId: string
  title: string
  reason: string
  estimatedTime: number
}

async function getRecommendations(userId: string): Promise<LearningRecommendation[]> {
  const progress = await getUserProgress(userId)
  const weakPoints = await identifyWeakPoints(userId)
  
  const recommendations: LearningRecommendation[] = []
  
  // 推荐下一课时
  const nextLesson = findNextAvailableLesson(progress)
  if (nextLesson) {
    recommendations.push({
      type: 'next-lesson',
      resourceId: nextLesson.id,
      title: nextLesson.title,
      reason: '继续学习新内容',
      estimatedTime: nextLesson.estimatedDuration
    })
  }
  
  // 推荐复习薄弱知识点
  for (const weak of weakPoints.slice(0, 2)) {
    recommendations.push({
      type: 'review',
      resourceId: weak.lessonId,
      title: `复习: ${weak.knowledgePoint}`,
      reason: `上次测验该知识点得分较低 (${weak.score}%)`,
      estimatedTime: 15
    })
  }
  
  return recommendations
}

数据报告导出

支持导出学习报告供家长或教师查看:

报告内容数据项
学习概览总学习时长、完成课程数、平均成绩
时间分布每日/每周学习时长
进度详情各课程完成情况
知识掌握各知识点掌握程度
学习趋势成绩变化、学习时长变化

最佳实践

  1. 隐私保护:学习数据属于敏感信息,需要加密存储并控制访问权限
  2. 实时性平衡:核心进度实时更新,详细分析可以延迟处理
  3. 激励机制:结合进度展示成就徽章、连续学习奖励等
  4. 合理目标:避免设置过高的进度期望,造成学习压力

通过以上设计,可以构建一个全面、智能的学习进度追踪系统,有效支持个性化学习。