用户在手机上看课看到第 12 分钟,回到家打开电脑网页版,期望视频直接从第 12 分钟继续;可现实经常是:电脑还停在开头,或者进度条莫名其妙回退,甚至两端互相覆盖,刚看完的章节又变成"未学"。学习进度同步看似是个小功能,真正要做到"多端一致、弱网不丢、断点准确",需要把上报、合并、冲突三件事拆开设计。
一、先想清楚:进度数据的本质是什么
一条学习进度至少要回答四个问题:哪个用户、哪节课、看到了多少秒、这次上报发生在什么时候。最小数据模型:
CREATE TABLE learn_progress (
user_id BIGINT NOT NULL,
lesson_id BIGINT NOT NULL,
position INT NOT NULL, -- 已观看位置,单位秒
duration INT NOT NULL, -- 视频总时长,用于算完成度
finished TINYINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL, -- 客户端事件时间戳(毫秒)
server_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, lesson_id)
);
这里有个关键区分:updated_at 是客户端产生事件的时间,server_at 是服务器收到的时间。弱网下用户在地铁里看的进度可能延迟几十秒才上报,判断"哪条更新"时要看事件时间而不是到达顺序,否则旧进度后到会覆盖新进度。
二、断点续播:进度上报要"节流"而不是"每帧上报"
视频每秒都有播放回调,如果每个回调都写一次数据库,写量会被放大几十倍;但如果只在暂停或退出时上报,用户直接杀进程就会丢一大段进度。合理的上报策略是"时间 + 事件"双触发:
let lastReport = 0;
player.on('timeupdate', ({
currentTime }) => {
const now = Date.now();
// 每 5 秒上报一次,避免写放大
if (now - lastReport >= 5000) {
lastReport = now;
reportProgress(currentTime);
}
});
// 暂停、切后台、离开页面前补报一次,保证断点尽量新
['pause', 'beforeunload', 'visibilitychange'].forEach(ev =>
player.on(ev, () => reportProgress(player.currentTime))
);
beforeunload 里发请求要用 navigator.sendBeacon,普通 ajax 在页面卸载时可能发不出去。断点续播时,进入播放页先拉一次服务端进度,seek 到对应位置即可;为避免每次拖动都被拉回,可以只在"本地无进度或本地进度明显落后"时才 seek。
三、多端进度合并:取"最远位置"而不是"最后上报"
同一节课用户可能在手机、电脑、平板间切换,多端各自上报进度,服务端该信谁?对"已观看位置"这种单调不减的数据,正确的合并规则是取最大有效位置,而不是按上报到达时间覆盖:
-- 只有新进度比当前更远才更新,天然抵御旧消息乱序到达
UPDATE learn_progress
SET position = CASE WHEN ? > position THEN ? ELSE position END,
finished = finished OR ?,
updated_at = GREATEST(updated_at, ?)
WHERE user_id = ? AND lesson_id = ?;
这样即使一台旧设备延迟上报了一个较小的位置,也不会把进度往回拉。要注意"最远"必须有上界:位置不能超过视频总时长,客户端传上来的值要先和 duration 校验,过滤掉拖动条甩到末尾、脏数据导致的虚假完成。
四、冲突处理:这几类情况要单独对待
不是所有冲突都能简单取最大值,有三种边界要专门处理:
- 用户手动拖回复习:用户故意把进度拖回第 3 分钟重看,此时不应被"最大位置"顶回去。区分"自然播放位置"和"拖动目标",复习拖动只影响本地播放头,不上报为更小的学习进度即可。
- 换课/跳章:上报必须带 lesson_id,章节切换时先结算上一节、再初始化下一节,避免 A 课进度写到 B 课。
- 离线观看后联网同步:离线期间产生的多条进度,客户端按事件时间排队,联网后批量上报;服务端逐条用上面的"取最远"规则归并,最终收敛到离线期间到达的最远位置。
完成度判定建议由服务端统一计算(如 position/duration ≥ 0.95 记为完成),不要各端各算一套,否则同一节课在不同端"已学/未学"状态不一致。
五、踩坑清单
- 每帧上报进度,数据库写量放大数十倍;
- 只在退出时上报,杀进程、断网导致进度丢失;
- 按服务器到达时间覆盖,弱网旧进度把新进度冲掉;
- 进度不设上界,脏数据直接标记"已学完";
- beforeunload 用普通 ajax,卸载时请求发不出去;
- 完成度各端各算,章节状态对不上。
六、工程落地建议
落地时先把"进度单调不减、按事件时间归并、服务端统一判完成"这三条主规则定下来,再处理节流上报和离线补传;上报接口设计成幂等的(同一用户+课程+位置重复上报无副作用),方便客户端放心重试。成型的在线教育平台一般已内置多端进度同步、断点续播与章节完成判定(如基于成型平台的课程学习能力,如乔拓云教育系统),自研时重点保证多端合并规则与弱网补传的正确性。
七、上线自检清单
- 手机看到一半换电脑,是否能从断点附近续播;
- 弱网下进度延迟上报,是否不会把进度往回拉;
- 直接杀进程,最多丢失多少秒进度;
- 拖动复习是否不会错误覆盖真实学习进度;
- 同一节课在各端的"已学/未学"是否一致。
多端进度同步的核心,是认清"已看位置只增不减"这一业务特性,用取最远的归并规则替代简单的时间覆盖,再用节流和补传兜住弱网体验。以上为个人实践总结,具体方案请结合播放器与业务场景调整。