习惯档位体系优化 — 复盘

背景

基于《微习惯》方法论,将五个习惯的档位从 6 级扩展到 11 级,优化起步值和阶梯间距。早起改为动态阶梯,根据用户基准起床时间自适应。

改动概览

6 个 commit(1 feat + 5 fix)

| Commit | 类型 | 内容 | |--------|------|------| | 6e4ecb9 | feat | 优化习惯档位体系,重新设计11级阶梯 | | 4912b21 | fix | indexOf 精确匹配导致只有2档 | | fca2dfb | fix | dataset 字符串拼接导致跳档 | | f8ac937 | fix | 边界按钮仍触发 toast | | e1fad32 | fix | 设置页没有云同步 |

踩坑记录

坑 1:indexOf 精确匹配

现象:设置页 stepper 点击 +/- 只有 2 个值可以切换。

根因:用 levels.indexOf(currentVal) 找当前档位索引。当 currentVal 不在 levels 数组中时返回 -1(mIdx >= 0 ? mIdx : 0) + delta 导致只能在 index 0 和 1 之间跳。

修复:新增 _findClosestLevelIndex() 辅助函数,找最接近的 level 索引。

影响范围:settings.js 的冥想/阅读/写作/运动四个习惯。

坑 2:dataset 类型陷阱(最深)

现象:从第 2 档按 + 直接跳到最后一档(第 11 档)。

根因:微信小程序 e.currentTarget.dataset 的值全是字符串。data-delta="1" 传到 JS 是 "1" 而非 1mIdx(1) + delta("1") 变成字符串拼接 "1" + "1" = "11"Math.min(10, 11) = 10,直接跳到最后一档。

日志证据:

mIdx: 0 delta: 1 mIdx+delta: 01    ← "0" + "1" = "01"(字符串拼接)
mIdx: 1 delta: 1 mIdx+delta: 11    ← "1" + "1" = "11"(字符串拼接)

修复adjustTarget 入口加 Number(e.currentTarget.dataset.delta) 强转。

教训:这个 bug 在 Node.js 测试环境完全复现不了,因为 dataset 只在小程序运行时存在。小程序特有的 API 行为必须在真机/模拟器环境验证,纯单元测试覆盖不到。

坑 3:边界按钮仍可点击

现象:到达最高档时 + 按钮灰了,但点击仍弹出"目标已调整"提示。

根因:CSS 的 disabled 样式只是视觉灰化,bindtap 事件照样触发。

修复adjustTarget 入口加 if (delta < 0 && habit.atMin) return; if (delta > 0 && habit.atMax) return;

教训:UI 禁用不能只靠样式,必须配合 JS 逻辑拦截。

坑 4:设置页没有云同步

现象:调整档位后重新编译,设置回到初始值。

根因:设置页的 adjustTargetonWakeTimeChangeonHabitToggle 只写了 localStorage,没有调用云函数同步。清缓存后数据丢失。

修复:新增 _syncSettingsToCloud() 方法,三个写操作点都调用云函数 save action。

教训:Local-First 不等于只写本地。每个写操作必须同时写本地 + 云端,不能遗漏任何入口。

经验总结

微信小程序开发经验

  1. dataset 取值必须 Number() 转换——dataset 里所有值都是字符串
  2. UI 禁用 = CSS 灰化 + JS 拦截——缺一不可
  3. 小程序特有行为要在小程序环境验证——Node.js 测试有盲区

Local-First 架构经验

  1. 写操作必须本地 + 云端双写——审查所有写入口,确保不遗漏
  2. 数据迁移要考虑旧值不在新数组中的情况——用 closest 匹配而非精确匹配
  3. 缓存清除后要有云端恢复机制——syncFromCloud 要能完整恢复所有设置

测试经验

  1. 50 个单元测试覆盖了 constants.js 的所有函数和边界,但没有覆盖 settings.js 的事件处理逻辑(因为依赖小程序运行时)
  2. dataset 类型问题无法在 Node.js 测试中暴露——需要集成测试或真机验证
  3. 测试应该覆盖"值不在 levels 数组中"的场景——这正是实际用户升级时会遇到的情况