Files
2026-08-05 23:59:22 +08:00

5.3 KiB
Raw Permalink Blame History

每日学习计时上报功能

目标

在课程学习页面(CourseLearn)实现后台静默学习计时,每 5 分钟调用 tasks.track 上报分钟数,页面关闭/离线时再次上报剩余时长,驱动"每日学习"和"学习达人"任务自动完成。

现状分析

  • 后端已就绪:TaskTrackAPIView + TaskDefinition 种子数据(每日学习 5min / 学习达人 30min)
  • 前端已定义未调用:api_request.login.tasks.track() 已定义但从未使用
  • CourseLearn.tsx 无计时:当前无任何计时相关代码
  • 后端 count 单位 = 分钟:target_count=5 即 5 分钟

设计方案

计时逻辑

  • 进入课程学习页面即开始计时(页面打开即计时)
  • 切到后台也继续计算(不检测活跃时间)
  • 不显示计时器(后台静默)

上报策略

  • 每 5 分钟上报一次:count=5
  • 页面关闭/离线前上报剩余时长:监听 beforeunload + visibilitychange(隐藏时)
  • 首次进入立即上报 1 分钟:保证用户停留即有意义,后续每 5 分钟补报

数据存储(防丢失)

  • 用 localStorage 记录当前课程累计已上报分钟数
  • key:learn_time_{courseId},value:已上报分钟数
  • 页面卸载时同步保存,重新进入时恢复

防重复

  • 已上报的分钟数不重复上报(通过 localStorage 追踪)
  • 页面卸载时把未上报的剩余分钟数一次性补报

实现文件

1. 新建 src/hooks/useLearnTimer.ts(计时 Hook)

功能:
- 接收 courseId,返回空(纯副作用)
- 进入时读取 localStorage 恢复已上报时长
- 每 1 分钟 setInterval 累加本地时长
- 每满 5 分钟调用 api_request.login.tasks.track({ action_type: 'learn', count: 5 })
- 上报成功后更新 localStorage 中的已上报值
- beforeunload / visibilitychange(hidden) 时:
  - 上报剩余未报分钟数(count = 剩余分钟)
  - 使用 navigator.sendBeacon 或同步 XHR 保证可靠性

2. 修改 src/pages/CourseLearn/CourseLearn.tsx

- 导入 useLearnTimer
- 在组件内调用 useLearnTimer(courseData?.id)
- 无需新增 UI 元素(后台静默)

关键代码结构

useLearnTimer Hook

export const useLearnTimer = (courseId: number | undefined) => {
  const [reportedMinutes, setReportedMinutes] = useState(0);
  const [currentMinutes, setCurrentMinutes] = useState(0);

  useEffect(() => {
    if (!courseId) return;
    
    // 从 localStorage 恢复
    const saved = localStorage.getItem(`learn_time_${courseId}`);
    const savedReported = saved ? parseInt(saved, 10) : 0;
    setReportedMinutes(savedReported);
    setCurrentMinutes(savedReported);

    const interval = setInterval(() => {
      setCurrentMinutes(prev => {
        const next = prev + 1;
        const newReported = reportedMinutes + Math.floor((next - reportedMinutes) / 5) * 5;
        
        // 每满 5 分钟上报
        const batchesReady = Math.floor((next - reportedMinutes) / 5);
        if (batchesReady >= 1) {
          const toReport = batchesReady * 5;
          trackLearnTime(toReport);
          setReportedMinutes(reportedMinutes + toReport);
          localStorage.setItem(`learn_time_${courseId}`, String(reportedMinutes + toReport));
        }
        
        return next;
      });
    }, 60000); // 每分钟检查

    // 页面卸载上报剩余
    const handleUnload = () => {
      // 上报剩余分钟(不足 5 分钟的部分也上报)
      const remaining = currentMinutes - reportedMinutes;
      if (remaining > 0) {
        trackLearnTime(remaining);
        localStorage.removeItem(`learn_time_${courseId}`);
      }
    };

    window.addEventListener('beforeunload', handleUnload);
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') handleUnload();
    });

    return () => {
      clearInterval(interval);
      window.removeEventListener('beforeunload', handleUnload);
    };
  }, [courseId]);
};

trackLearnTime 辅助函数

const trackLearnTime = async (minutes: number) => {
  try {
    await api_request.login.tasks.track({
      action_type: 'learn',
      count: minutes,
    });
  } catch {
    // 静默失败,不阻塞用户
  }
};

上报时序示例

时间 动作 累计学习 上报 后端进度
00:00 进入页面 0 - 0/5
01:00 第 1 分钟 1 - -
05:00 满 5 分钟 5 count=5 5/5 ✅
10:00 满 10 分钟 10 count=5 10/5(每日学习已完成,累计到学习达人)
15:00 关闭页面 12 count=2(剩余) 12/5

验证步骤

  1. 打开任意课程学习页面
  2. 等待 5 分钟,检查 TaskCenter 页面"每日学习"进度是否更新
  3. 关闭页面,检查是否有补报请求(Network 面板)
  4. 切换语言后确认功能不受影响
  5. 未登录用户:计时不上报(或游客模式本地存储,登录后合并上报)

假设与决策

  • 计时从页面打开开始:用户选择,无需检测活跃时间
  • 每 5 分钟上报一次:用户选择,平衡实时性与请求频率
  • 关闭页面用 beforeunload + visibilitychange:双重保障,防止离线丢失
  • 后台静默不显示 UI:用户选择
  • action_type='learn':匹配种子数据中每日学习/学习达人的 action_type