5.3 KiB
5.3 KiB
每日学习计时上报功能
目标
在课程学习页面(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 |
验证步骤
- 打开任意课程学习页面
- 等待 5 分钟,检查 TaskCenter 页面"每日学习"进度是否更新
- 关闭页面,检查是否有补报请求(Network 面板)
- 切换语言后确认功能不受影响
- 未登录用户:计时不上报(或游客模式本地存储,登录后合并上报)
假设与决策
- 计时从页面打开开始:用户选择,无需检测活跃时间
- 每 5 分钟上报一次:用户选择,平衡实时性与请求频率
- 关闭页面用 beforeunload + visibilitychange:双重保障,防止离线丢失
- 后台静默不显示 UI:用户选择
- action_type='learn':匹配种子数据中每日学习/学习达人的 action_type