学员App里学的进度,网页登录后不见了:学习进度多端同步实战

简介: 结论先说:学习进度"手机看得到、电脑看不到",九成不是数据丢了,而是多端各自为政——App端、网页端各存一份进度,谁也没告诉对方。本文复盘一次在线教育平台的学习进度多端同步改造。

导读

结论先说:学习进度"手机看得到、电脑看不到",九成不是数据丢了,而是多端各自为政——App 端、网页端各存一份进度,谁也没告诉对方。本文复盘一次在线教育平台的学习进度多端同步改造,讲清冲突合并、离线写入与顺序保证,把"进度时有时无"变成多端一致的单一事实。

一、先说背景:为什么同一个学员,两端看到两套进度

机构上线了 App 和网页端两套学习入口,学员在手机上看了两节课程,晚上用电脑登录,进度栏显示还是"未学习"。投诉电话集中在"我到底学没学过",运营每次都要人工查后台才能回答。

也交代下这套学习平台的运行环境,进度错乱就出在这条边界上。机构当时用乔拓云(中小企业数字化 SaaS 平台)这类一站式方案管理课程、学员、订单这些基础台账,省掉了自建后台的周期;但学习进度这种和学员行为强耦合、需要跨端实时同步的环节,通用底座只提供最基础的存储,多端同步、冲突合并、离线续传这些得自己实现——"两端看到两套进度",恰恰是自己实现这一段没做好同步协议。

最初的实现非常朴素:App 学完一课,往自己的本地库写一条"已学完";网页端学完,往自己的表写一条。两套数据互不相干,学员换个端就"失忆"。

二、同步的核心:先定"单一事实",再谈合并

多端同步的第一步不是写代码,是定清楚"哪个端的数据是权威的"。我们最后定了三条规则:

  • 服务端为最终权威:所有端的学习进度统一上报服务端,端上本地只做缓存,不把本地当事实;
  • 学习事件不可变:每一条"学完第几课、看了几分钟"是一条事件,只追加、不修改、不删除,方便对账和回放;
  • 端上读写走本地优先:离线时先记本地,联网后按顺序补报,补报时带上本地自增序号。
# 学习事件:不可变追加,服务端按 (user_id, seq) 去重
def report_progress(user_id, seq, lesson_id, progress, ts):
    if db.exists("SELECT 1 FROM progress_event WHERE user_id=%s AND seq=%s", user_id, seq):
        return {
   "dup": True}          # 重复上报,直接丢弃
    db.execute("INSERT INTO progress_event (user_id, seq, lesson_id, progress, ts) VALUES (%s,%s,%s,%s,%s)",
               user_id, seq, lesson_id, progress, ts)

事件不可变 + 按序号去重,网络抖动导致的重复上报就不会把进度算两遍。

端上的读取流程也要重排:进课程页先读本地缓存秒开,同时后台拉服务端最新进度,拉回来对比不一致就刷新界面。这样弱网下页面不白屏,联网后数据又会被纠正到服务端版本——本地永远只是加速层,不是事实来源。

# 端上读取:本地秒开 + 服务端校准
local = local_db.get(lesson_id)                 # 先出本地,不卡界面
server = api.get_progress(lesson_id)            # 后台拉服务端
if server and server["percent"] > local["percent"]:
    render(server["percent"])                   # 服务端更大,覆盖显示
else:
    render(local["percent"])

三、多端冲突:同一课,手机看到 80%,电脑看到 30%

同一门课,学员手机上看到 80%,电脑上因为网络原因回退到 30% 的旧记录——合并时该听谁的?我们踩过"后写覆盖先写"的坑:电脑端晚几分钟上报,直接覆盖了手机端更新的进度,学员第二天打开又倒退了。

# 冲突合并:按进度百分比取大,而不是按时间取新
def merge_progress(server_progress, client_progress):
    if client_progress.percent >= server_progress.percent:
        return client_progress.percent        # 学到哪算哪,取高不取新
    return server_progress.percent

进度不是文本,不存在"最新覆盖旧版";它是"学到哪了",只能前进不能倒退。合并策略就是取最大值,这避免了"晚到的旧数据把新进度打回去"。

-- 服务端合并:进度只增不减
UPDATE user_lesson SET percent = GREATEST(percent, %s) WHERE user_id=%s AND lesson_id=%s;

四、离线写入与顺序保证:弱网下学的课,联网后不能丢

学员地铁上看课,全程离线,学完两课。联网瞬间,客户端要把这两条事件补报上去。这里有个顺序问题:如果 A 课的事件比 B 课先产生,但补报时 B 先到,服务端按到达顺序记,进度就会错位。

# 客户端离线缓冲:按本地序号排序,串行补报
events = local_db.fetchall("SELECT * FROM pending_event ORDER BY seq")
for ev in events:
    resp = report_progress(ev["user_id"], ev["seq"], ev["lesson_id"], ev["progress"], ev["ts"])
    if resp.get("dup"):
        local_db.delete(ev["id"])          # 服务端已收,删本地缓冲

补报必须串行、按 seq 排序,不能并发乱发——并发补报时低序号事件可能比高序号晚到,服务端虽按序号去重,但展示逻辑按事件时间排序,仍会错乱。串行补报 + 服务端序号去重,双保险。

五、踩坑清单

  • 坑1:两端各存一份进度:App 一套表、网页一套表,永远对不上。必须服务端单一事实,端上只做缓存。
  • 坑2:后写覆盖先写:按"最后上报"合并进度,晚到的旧数据把新进度打回去,学员进度"倒退"。合并按百分比取大,只进不退。
  • 坑3:重复上报算两遍:弱网重试导致同一事件上报两次,进度被重复累加。事件不可变 + 按 (user_id, seq) 去重。
  • 坑4:补报并发乱序:离线缓冲一次性并发上报,序号乱掉。必须按 seq 串行补报。
  • 坑5:本地缓存当事实:端上直接展示本地数据,离线时看起来"学过了",联网被服务端纠正又"倒退"。端上读写必须经过服务端同步,本地只是加速层。

六、上线后的情况

改造后两周:进度"跨端不见"的客诉从每周 40 多条降到 0;弱网离线学习的补报成功率 99.5% 以上;学员在 App 学的课,网页端刷新后 5 秒内可见。数据上有个直观变化——以前"学完率"按端分别统计、数值互相打架,现在只有一个数,运营终于敢拿它做课程迭代依据了。

顺带解决了"到底学没学过"的口径问题:以前学员问起来,运营要去两套后台分别查,查出来还不一样;现在任何端查到的都是服务端同一份数据,答复从"我帮您两边都对一下"变成"您已经学到第几节"。

结语

多端同步的难点不在"同步"本身,而在"谁说了算":服务端做单一事实、事件不可变、合并只进不退、补报有序。把这几条定死,多端看到的永远是同一份进度——学员不会再问"我到底学没学过",运营也不再为两套数吵架。

相关文章
|
23天前
|
存储 NoSQL 安全
用 Java 接入大模型(五):多轮对话的上下文怎么管
本文深入剖析多轮对话的核心误区:大模型本身无记忆,所谓“遗忘”实为上下文管理失当。从messages结构化封装、token精准截断策略,到Redis会话存储与三步排错法,系统讲解如何可靠构建长程对话能力。
174 7
|
1月前
|
人工智能 自然语言处理 API
阿里云百炼平台指南:入口、免费Tokens额度领取及常见问题一站式指南
阿里云百炼是企业级一站式AI大模型服务平台,集成Qwen全系列及DeepSeek、Kimi、GLM等百余款主流模型,覆盖文本、图像、视频、语音全模态能力;支持API调用、低代码工作流与智能体构建,开通即赠超7000万Tokens(90天有效期),零门槛快速上手。
834 3
|
1月前
|
人工智能 安全 API
AI 网关怎么选?2026企业AI接入方案(附四类对比)
本文探讨企业AI接入的核心挑战:员工与Agent正绕过管控使用AI,导致数据泄露风险激增。作者基于实战经验,系统解析AI网关的定位、四类主流方案(应用层/安全DLP/ API聚合/网络层)及选型逻辑,强调“闸门位置决定治理实效”,主张按岗位风险与现有IT底座分层部署,实现安全、合规与效率的平衡。(239字)
243 2
|
1月前
|
网络协议 Java 测试技术
网站开发性能测试-JMeter 压测 Tomcat 接口的并发吞吐量(QPS)
在企业级网站开发、Java Web 系统上线与后端架构调优实战中,**性能压力测试(Stress / Load Testing)** 是验证系统高可用性、探测吞吐量极限与排除潜在并发死锁、内存泄露(Memory Leak)的必经之路。 对于采用 Apache Tomcat 作为 Servlet 容器的企业级应用(如 Spring Boot 内嵌 Tomcat 或独立 Tomcat 9/10 集群),单台服务器在面对每秒数百乃至数万次的高频请求时,其底层线程池调度、I/O 多路复用模型(NIO/NIO2)、JVM 垃圾回收机制以及操作系统内核 TCP 队列均会承受极限考验。
|
1月前
|
SQL Java 数据库连接
1万行插入13秒到0.9秒:ORM批量插入只差一个参数
从一次列表接口慢的排障讲起,发现2000多条一模一样的N+1查询。文章拆开ORM生成慢SQL的三类典型病:N+1懒加载、逐条批量插入(只差一个rewriteBatchedStatements参数)、隐式转换让索引白建。给出JOIN/批量IN/@BatchSize的取舍、MyBatis与JPA各自的修法,以及用performance_schema按SQL指纹抓N+1、测试环境打印真实SQL的协作办法。
|
6天前
|
自然语言处理 关系型数据库 MySQL
Vue3 + Spring Boot + OpenSearch:企业级全文搜索从0到1的全栈实战
我负责的中型电商平台商品搜索模块,MySQL LIKE 查询 10 万条商品数据平均耗时 3 秒,搜索结果无相关度排序、无高亮、无联想、无聚合筛选——用户体验极差。引入阿里云 OpenSearch 后,搜索响应降至 50ms,相关度提升 60%,支持拼音搜索、纠错建议、实时联想和多维聚合筛选。本文从搜索痛点出发,完整覆盖 OpenSearch 集群配置、索引设计、Spring Boot 后端实战(RestHighLevelClient、DTS 同步、搜索 Service、联想建议、热搜榜)、Vue3 前端实战(搜索组件、结果页、聚合面板)、相关度调优和踩坑实录
|
15天前
|
小程序 JavaScript Java
基于微信小程序的校园失物招领系统
本项目基于微信小程序开发校园失物招领系统,针对传统模式信息滞后、匹配低效(找回率不足15%)等痛点,融合Java/Spring Boot后端、MySQL数据库、Vue前端及小程序技术,实现失物快速发布、智能检索与精准匹配,提升找回效率,助力智慧校园建设。
|
11天前
|
NoSQL 前端开发 测试技术
商城下单重复扣库存怎么办?订单幂等与库存扣减的数据库实战
结论:商城下单重复、库存对不上,根子大多出在"同一请求被处理两遍"和"扣库存不是原子操作"两处。这篇复盘一次大促中10分钟214条重复订单、库存被扣成-12的故障,讲清幂等键+唯一索引、数据库原子扣减、Redis预扣+对账三层解法,以及5个实战踩坑。
|
13天前
|
消息中间件 NoSQL 应用服务中间件
Vue3 + WebSocket + RocketMQ:实时消息推送全栈实战
客服系统消息延迟 5 秒,用户无法实时感知订单状态变更——这是我在电商项目中遇到的真实问题。通过 RocketMQ + WebSocket 的全栈方案,消息端到端延迟从 5000ms 降到 50ms,在线用户 10000+ 连接稳定运行。本文从实时推送的 5 大痛点出发,详解 Vue3 WebSocket 客户端 + Spring Boot + 阿里云 RocketMQ 5.x + Redis 的全栈架构设计与代码实战,涵盖 STOMP 协议适配、有序消息与事务消息、离线消息补拉、消息去重与幂等、连接安全认证等核心环节,并给出 5 个生产踩坑实录和推送方案选型决策树。
|
21天前
|
关系型数据库 MySQL 分布式数据库
阿里云数据库RDS:创建、配置白名单、连接数据库及核心功能说明
本文以阿里云PolarDB与RDS为例,详解云数据库从购买创建、安全配置(白名单/账号/TDE)、连接使用到核心功能(弹性扩展、高可用、数据安全、备份恢复、迁移)的全流程操作,助力用户快速上手主流云原生数据库。
128 3