Lee's Blog
  • 首页
  • 博客
  • 作品
  • 成长
  • 学习
  • 打榜
  • 练习
  • 关于
登录 / 注册
✦

保持联系

关注我的最新动态

GitHubbilibili

© 2026 Lee's Blog

首页/博客/EZTor/用户明明默写了 20 多个单词,为什么任务栏还停在 4/20?
EZTor2026年8月9日6 分钟阅读

用户明明默写了 20 多个单词,为什么任务栏还停在 4/20?

一次"计数没坏,却看起来卡死"的排查实录:任务进度条停在 4/20 的背后,是五层互相掩盖的线索。

后端部署服务器维护
X微博

用户明明默写了 20 多个单词,为什么任务栏还停在 4/20?

一次"计数没坏,却看起来卡死"的排查实录:任务进度条停在 4/20 的背后,是五层互相掩盖的线索。

问题现象

安卓用户 ᗜ֊ᗜ 反馈:自己明明默写了 20 多个单词,首页任务栏里的"默写复习"却一直停在 4/20,怎么都不涨。

我的第一反应是"计数丢了"。结果拉数据一看,结论完全相反——计数一直在涨,只是用户看到的永远是旧值。

排查过程

第一步:服务端计数其实是对的

从生产库看这位用户的 DailyTaskCompletion:

taskType currentValue targetValue updatedAt
COMPLETE_REVIEWS 17 20 09:50:58

而词库表里今天所有 dictation 答题数 sum(correctCount + incorrectCount) 恰好也是 17。两边完全对得上——每次 /api/dictation/update 都正确 +1 了,FLASHCARD_INTERACT=15/15、REACH_ACCURACY=81/80 也都正常。计数链路是活的。

那问题出在哪?

第二步:updatedAt 冻住了

任务行的 updatedAt 永远停在创建时刻 09:50:58,哪怕 currentValue 已经从 4 涨到 17。

去翻 GameService.reportTaskProgress:

await prisma.dailyTaskCompletion.upsert({
  where: { userId_date_task_type: {...} },
  update: {
    currentValue: newValue,   // ← 绝对值覆盖,不是原子自增
    isCompleted,
    completedAt: ...,
    // ← 没有 updatedAt: new Date()
  },
  ...
})

两个问题叠加:

  1. update 分支没写 updatedAt,所以任务行的时间戳永远冻结在创建时刻。任何"看时间戳判断有没有更新"的方式都会被骗。
  2. 读改写不是原子的:先 findUnique 读 currentValue,再计算 newValue 覆盖写回。并发场景下两个请求都读到 4,都算出 5,一起写回 5——一次 +1 就丢了。移动端快速答题时这就是真实风险。

第三步:上报失败被静默吞掉

后端:gameService.reportTaskProgress(...).catch(() => {})——异常全吞。

前端(dictation/page.tsx):

if (!res.ok && process.env.NODE_ENV === 'development') {
  console.error('Dictation update failed:', ...)
}

生产环境只打印到 dev 才有的 console,WebView 里网络抖动导致上报失败时,用户完全无感知,任务栏自然不动。

第四步:任务栏组件不刷新

DailyTaskCard 只在挂载 / refreshKey 变化时拉数据。安卓 WebView 从默写页返回首页时走的是 bfcache(后退缓存),组件根本不重挂载,useEffect 不重跑——显示的还是旧快照 4/20。

讽刺的是,同页的 FullscreenFlashcard 早就专门加了 pageshow 处理 bfcache,唯独任务卡漏了。

修复

  1. reportTaskProgress:upsert update 分支补 updatedAt: new Date();
  2. 计数改为原子 { increment: value },用增量后的实际值判断是否达标,再配合条件 updateMany(isCompleted: false)保证"首次达标"只发一次学力奖励,并发不重复发;
  3. DailyTaskCard 增加 pageshow + visibilitychange 重拉;
  4. 默写上报失败时 toast.error 提示并允许重试。

版本 1.13.1,构建 → deploy-full.sh → 按部署 checklist 验证 BUILD_ID / HTTP 200 / chunks / sidecar,全部通过。

为什么之前没发现?

五个掩盖层,层层抹掉线索:

  1. 计数没坏——不存在能告警的"数据丢失",它是"对的",只是展示旧值;
  2. updatedAt 冻结——任何按时间戳的检查都会误判成卡死;
  3. 异常被吞——无日志、无提示,无从发现;
  4. 零测试覆盖——src/__tests__ 里没有 GameService / reportTaskProgress 的任何用例;
  5. 只在 Android WebView 复现——服务端永远对,客户端从默写页返回才显示旧值,纯服务端监控天然看不见。

经验

  • 时间戳要写就写对:upsert 的 update 分支漏掉 updatedAt,会让"卡死"假象骗过所有人。
  • 计数一律原子化:increment 比"读出来+1再写回"安全得多,且不增加查询量。
  • bfcache 面前,useEffect 会骗你:移动端凡是"返回页面要刷新数据"的组件,都要处理 pageshow。
  • 用户报告"没反应",先怀疑是展示层,再怀疑是数据层。这次数据层一分没丢。
← 上一篇一次登录 / 资料 / 头像的全面体检——5 个用户报告 + curl 抓出的真问题下一篇 →meteorological 变成了 neteorological?记一次长单词被"削头"的 UI 排查

相关文章

  • 退出后重登卡在"登录中":一次靠 Network 面板定位的 NextAuth session 时序问题约 5 分钟
  • 用户说"第一天的闪卡更好玩"——一次把 bug 当需求改的复盘约 6 分钟
  • meteorological 变成了 neteorological?记一次长单词被"削头"的 UI 排查约 6 分钟

评论

需要登录账号才能发表评论。

  • 加载中…

本页内容

  • 用户明明默写了 20 多个单词,为什么任务栏还停在 4/20?
  • 问题现象
  • 排查过程
  • 第一步:服务端计数其实是对的
  • 第二步:`updatedAt` 冻住了
  • 第三步:上报失败被静默吞掉
  • 第四步:任务栏组件不刷新
  • 修复
  • 为什么之前没发现?
  • 经验