memory文档

This commit is contained in:
2026-07-29 11:05:03 +08:00
parent 17727ab1dd
commit 3db170c4b9
5 changed files with 379 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+139
View File
@@ -0,0 +1,139 @@
# 2026-07-21
## 代码审查:平板支撑训练小程序(wx_pbzc)
对微信小程序做了完整 code review,报告见 `/Users/liubleed/Documents/wx_pbzc/code-review-report.md`
关键问题:
- **高#1**`utils/plan.js getPlanDay()` 用完整时间戳(`r.date`含时分秒)做 `new Set().size` 统计训练天数,同一天多次训练会多算天数、错误跳过计划进度。应改用 `dateOnly(r.date)`
- **高#2**:系统深色模式切换时 index/records/leaderboard 三页不实时重绘——它们没定义 `applyThemeToPage` 方法,而 app.js `watchDarkMode` 回调只在有该方法时才调用。只有 settings 页有该方法。修复:回调补兜底或给三页加方法。
- **中**:排行榜完全信任客户端上传的 records.duration,可伪造;`admin-dedupe` 云函数无鉴权且具破坏性。
- **低**Timer 的 `wx.onAppShow` 只注册不注销;`ui-modal` 的 customStyle 是死属性;`getTodayTarget` 空 days 会崩溃;darkMode.js 用了废弃的 getSystemInfoSync。
代码整体质量高(云同步去重/合并、降级处理都到位)。
## 修复记录(2026-07-21,两轮)
**第一轮(高优)**
- `utils/plan.js getPlanDay()`:改用 `dateOnly(r.date)` 去重,修同一天多次训练错误跳计划天数。
- `app.js watchDarkMode` 回调:补兜底,对无 `applyThemeToPage` 方法的页面直接调 `themeMod.applyThemeToPage(p)`,并通知自定义 tabBar `updateTheme()`,修系统深/浅色切换时三主页面不实时刷新。
**第二轮("接着修"**
- `cloudfunctions/admin-dedupe/index.js`:入口加 openid 白名单闸门(读环境变量 `ADMIN_OPENIDS`,未配置默认拒绝=失败安全),防任意用户调用批量删文档。部署时须配置该变量。
- `utils/timer.js`:抽 `_bindAppShow/_unbindAppShow`,在 `stop()``wx.offAppShow`,修每次进训练页累积 onAppShow 监听的内存泄漏。
- `utils/plan.js getTodayTarget`:空 days 数组返回 0,防 `plan.days[0].target` 崩溃。
- `utils/darkMode.js``wx.getSystemInfoSync()` 废弃,改 `wx.getWindowInfo()`
- `pages/timer/timer.js onTrainAgain``_countUpTask` 改为实例字段赋值,不再写进 setData。
- `pages/index/index.js refresh()``planProgress` 在 totalDays=0 返回 0,防 NaN。
- 复核纠正两条误报(已写进报告):#3 排行榜 duration 实为服务端从 doc.records 重算,不读客户端,无需修;#8 ui-modal 的 customStyle 在 wxml 第3行已被 `style="{{customStyle}}"` 使用,非死属性。
- 全部 6 文件 `node --check` 通过;报告 `code-review-report.md` 已标修复状态并纠正误报。
## UI 美化第一轮(2026-07-21
基于 UI 分析(色彩层级单薄/排版无标尺/进度环太素/深色不一致),按约定先做见效最快的三项:
1. **进度环**`components/progress-ring/progress-ring.js`):canvas 分段绘制锥形渐变弧(primary→primaryLight,每段 0.06rad、上限 120 段),末端加 `shadowBlur` 发光圆点+白色内核,起点圆头补圆;`primaryLightColor` 加入 `_draw` observer;环宽 timer 页 16→22rpx,时间字 80→88rpx/800 字重;呼吸环从描边圆改 radial-gradient 弥散光晕(`timer.wxss`)。
2. **首页**`ui-card` 新增 `gradient` 变体(主色渐变底,局部重定义 `--text/--text-secondary/--border` 为白色系,后代自动反白);连续打卡卡用 `variant="gradient in"`,卡内 streak 数字/进度条/光点显式改白;今日目标环 200→260rpx、时间 64→112rpx、加外圈光晕。
3. **背景分层+深色对齐**`theme.js getThemeStyle` 浅色背景渐变起点改用各主题 `primaryBg`(背景随主题变色),深色统一为 `#131316` 系(bg/card `#26262B`/soft `#343439`);`applyThemeToPage/applyWindowBg` 窗口背景同步;`app.wxss` 冷启动变量、`theme.json`、timer `trackColor`(深色 `#3A3A40`)、`ui-skeleton``custom-tab-bar``records.wxss` 注释全部对齐新色值;`ui-card` 深色下加深阴影+微亮描边防"卡片沉底"。
验证:`node --check` 通过;全库 grep 旧深色值 `#0F0F12/#2C2C2E/#1C1C1E` 仅剩 docs/ 历史文档(无需改)。
待办(用户确认后做):排行榜领奖台+period tab 改分段控件、Emoji→SVG 统一、排版/圆角标尺收敛、设置页主题色卡横滑、微交互(数字滚动/按钮回弹)。
## UI 美化第二轮(2026-07-21
4. **排行榜**period-tabs(白底下划线)→ `period-seg` 胶囊分段控件(呼应设置页 .segmented,sticky 保留);≥3 人时前三名上**领奖台**(冠军居中放大头像 128rpx+皇冠图标+渐变金牌,2/3 名银铜奖牌,阶梯底座,跳过列表前三);`my-bar` 从半透明描边→主色渐变实色卡+白字+主色投影;rank-item 的 is-me 判空保护(myEntry 可为 null)。
5. **Emoji→SVG**`utils/icons.js` 新增 trophy/party/sparkle+Fill)及白色语义变体 `trophyWhite/partyWhite/hotWhite/sparkleWhite/checkWhite`timer 完成弹窗 emoji 文本→SVG 图标(.completion-icon 80rpx),庆祝粒子 emoji→sparkle/crown/like 图片(.spark 改 44rpx 图片),streak 🔥→hotFill 图片;成就横幅文案去掉内嵌 emoji(渐变横幅自身够视觉)。排行榜奖牌 emoji 已在任务4随领奖台移除。
6. **设置页**:配色方案从纵向列表→**横向渐变色卡**(3 列,primary→primaryLight 渐变 swatch + 白色对勾,选中态放大 1.04+主色外描边,名称高亮);深色模式选择保留原行式(.theme-item 样式保留);计划选中项加左侧 6rpx 主色渐变竖条。
校验:全部改动文件 node --check 通过;grep 确认无残留界面 emoji(仅 settings 关于页 ❤️ 与文档,保留)。
## UI 对比度修复(2026-07-21
修首页渐变 hero 卡(训练首页顶部"连续打卡")数字/文案看不清:
- **根因**:小程序组件默认 `style isolation``.ui-card.gradient <slot后代>` 跨作用域选择器全部失效;hero 卡内 `.streak-num`/`.progress-fill``var(--primary)`(橙)画在橙色渐变底上=隐形。
- **修复**`ui-card` 渐变变体新增 `--on-primary:#FFFFFF`(自定义属性能跨隔离边界继承进 slot);`.streak-num``var(--text)`(渐变卡继承为白,普通卡为深);`.progress-fill`/`.progress-glow``var(--on-primary, <原渐变/主色>)``--text-secondary` 渐变卡内由 0.75 提到 0.85 提升小标签可读性;删除 index.wxss 里 4 条失效的 `.ui-card.gradient ...` 页面选择器。
- 全局扫描确认其余彩色/渐变底(按钮/横幅/完成弹窗/my-bar/主题色卡/奖牌/列表时长)均显式白字或位于浅色底,无同类问题。
## UI 对比度审计(2026-07-21 下午)
用户反馈:训练首页(index)顶部"连续打卡"数字与渐变卡底几乎同色、看不清。
根因(关键项目陷阱):`ui-card` 自定义组件默认 style isolationisolated),`variant="gradient in"` 产生的 `gradient`/`in` 类在**组件作用域**内,而卡内 slot 内容(`.streak-num` 等)的样式写在**页面作用域**。因此页面里的 `.ui-card.gradient .streak-num{color:#FFF}` 这类跨作用域后代选择器**全部失效**(运行时症状即证明)。唯一能跨边界生效的是 **CSS 自定义属性**——`.ui-card.gradient` 重定义的 `--text/--text-secondary/--border` 会向下继承进 slot,所以标签等能反白,但 `.streak-num` 用的 `var(--primary)` 没被重定义 → 仍是橙色画在橙色渐变上 = 看不见。
同类受损(同一机制):index 渐变 hero 卡内 `.progress-fill`/`.progress-glow``var(--primary)` 渐变,覆盖选择器同样失效 → 进度条橙色填充在橙色卡上不可见(细,用户未提但已确认)。
已确认安全(渐变/主色底 + 文字全部显式 `#FFFFFF`):ui-btn--primary、achievement-banner、completion-emoji-wrap(白色 SVG)、completion-btn--primary、stop-confirm-btn--primary、leaderboard my-bar(全显式白)、editor-action-save、records first-train-cta、settings 主题色卡(白对勾 + 名称在卡底)、rank-dur/podium-dur--first(主色字放在白卡/页面底)、target-time(主色字放在 0.10 浅色环)。
修复原则(待用户确认):渐变/主色容器内文字只用 `var(--text)/var(--text-secondary)`(随卡反白),绝不用 `var(--primary)`;进度填充改 `var(--on-primary, <原渐变>)` 让容器可覆盖为白。删掉那些失效的 `.ui-card.gradient ...` 页面选择器。
## UI 图标对比度修复(2026-07-21
用户反馈:index 页几个图标颜色与背景基本一样、看不清(同 gradient hero 卡问题延续)。
根因:图标是 SVG data URI**颜色烤死在 URI 里**icons.js `build()``*Fill` 变体统一用 `primary` 主题色绘制),CSS 的 `color`/变量均无法改它。index 渐变 hero 卡上的两个图标因此仍是橙色,画在橙色渐变底上=隐形:
- `.streak-icon``icons.hotFill`(橙)on 渐变卡
- `.plan-badge-icon``icons.formFill`(橙)on 渐变卡
修复:
- `utils/icons.js` 新增 `formWhite`(白填色,与已有 `hotWhite` 并列,注释说明渐变卡必须用白填色)。
- `pages/index/index.wxml``.streak-icon``icons.hotWhite``.plan-badge-icon``icons.formWhite`(白色图标在橙卡上清晰)。
- 校验 grepindex 页仅此两处用主色 Fill 变量,均已改;timer/settings/records 页的 hotFill/formFill 在深色/白卡背景,对比正常,不在本次范围。
结论:index 首页其余图标(targetFill 在白卡、time 灰在白卡、lightFill 在浅 soft 卡、successFill 在绿徽章)对比均正常。精力/问候图标(emojiFill 橙)在页面浅色底,橙主题下略低对比但可见,未改。
## UI 图标尺寸 + 主按钮图标对比度 + 目标数字溢出(2026-07-21
用户反馈:1) index「开始训练」前的图标太小也看不清;2) 今日目标数字太大超出圆环。并要求排查项目类似小图标。
排查(全项目 `icon-src` 审计):主按钮用 `playFill`(橙) 放在橙渐变背景上=隐形且 36rpx 太小,同病根的有 3 处——index 开始训练、timer 开始、timer 继续。ghost(浅底)/outline(灰底)/delete(灰) 按钮图标对比正常。
修复:
- `utils/icons.js` 新增 `playWhite`(白色播放图标,与 hotWhite/formWhite 并列;注释强调主按钮必须白图标)。
- 3 个主按钮 `icon-src``playFill``playWhite`index.wxml:73、timer.wxml:90、timer.wxml:109)。
- `components/ui-btn/ui-btn.wxss``.ui-btn__icon` 尺寸 `1em``1.3em`xl 按钮 36→约 47rpx,随按钮字号自适应放大)。
- `pages/index/index.wxss``.target-ring` 260→280rpxbox-shadow 16→14rpx 收外扩、加 overflow:hidden);`.target-time` 112→84rpxletter-spacing -1rpx、max-width:240rpx、text-align:center、overflow:hidden),3 位数不再溢出环。
- 校验 node --check 通过;grep 确认全项目无残留 `playFill`
- 项目 MEMORY.md 补充"图标烤色无法被 CSS 改 + 主按钮须用 *White 变体"陷阱。
## 配色方案 +12026-07-21
用户反馈:设置页配色方案只有 5 个、3 列网格下不对称(2 行满+1 孤)。要求再加一个。
新增第 6 个主题 `teal`(湖青):`utils/theme.js` 的 THEMES 末尾追加
- id:'teal', name:'湖青', primary:'#00BCD4', primaryLight:'#4DD0E1', primaryBg:'#E0F7FA', primaryRgb:'0,188,212'
选青/湖蓝原因:区别于已有蓝/绿,且避开红色(项目 --danger 用红,主色不宜红)。
效果:设置页色卡变 6 个=2 行×3 列,对称。theme.js 单源、settings.js 直接读 themeMod.THEMES,无数量硬限制(grep 仅 cloudfunctions/admin-dedupe 有 slice(0,5),无关)。node --check 通过。
## 排行榜头像不显示 - 修复(2026-07-21
根因:排行榜头像写死 `icons.peopleFill`(统一占位人像)。App 其实支持头像(settings 页 chooseAvatar→上传云存储 fileID→存 profile.avatarUrlcloud.js:146 同步整份 profile 到云端),但 `cloudfunctions/leaderboard/index.js` 只读了 `profile.nickname`、返回的 ranked/myEntry 没有 avatarUrl 字段 → 前端无真头像可用。
修复:
- `cloudfunctions/leaderboard/index.js`userMap.set、ranked map、myEntry 三处均加 `avatarUrl: profile.avatarUrl || ''`(后者用 item.avatarUrl)。
- `pages/leaderboard/leaderboard.wxml`:领奖台 3 处 `rankedList[0/1/2].avatarUrl || icons.peopleFill`、列表项 `item.avatarUrl || icons.peopleFill`、my-bar `myEntry.avatarUrl || icons.peopleFill`(my-bar 原无头像,补上)。共 5 处。
- `leaderboard.wxss`my-bar 头像 `.my-bar__avatar` opacity:1 + 白底白边(渐变条上占位图更协调)。
- `leaderboard.js._applyResult``...item` 透传,avatarUrl 自动带入,无需改。
- 校验:云函数 node --check 通过;grep 确认 5 处头像均改、占位图保留为兜底。
- ⚠️ 需重新部署 leaderboard 云函数(`wx cloud deploy` 或开发者工具上传)后头像才会在真机/线上生效;本地仅前端改动预览看不到他人头像(本地 fallback 也只取 profile.nickname)。
## 排行榜头像方形未裁圆(2026-07-21)
用户验收:头像已显示,但是方形图套在圆框里(border-radius:50% 加在 <image> 上,部分微信基础库不裁切图片内容)。
修复(微信可靠做法:圆角 <view overflow:hidden> 包裹 <image> 让父层裁切):
- `leaderboard.wxss`:删除 `.podium-avatar`/`.podium-avatar--first`/`.rank-avatar`,新增统一 `.avatar`(圆角+overflow:hidden+背景)、`.avatar__img`100%填充 block)、`.avatar--sm/md/lg` 三档尺寸;`.podium-avatar-wrap` 去掉 margin-bottom(改由 avatar--md/lg 自带)。
- `leaderboard.wxml`5 处头像 `<image>` 改为 `<view class="avatar avatar--sm/md/lg [my-bar__avatar]"><image class="avatar__img" .../></view>`(md/lg 用于领奖台 2/3 名与冠军放大;sm 用于列表项与 my-bar)。
- `.my-bar__avatar` 覆盖(opacity:1+白底白边)仍作用于新元素。
- 校验 grep:无悬空旧类 `podium-avatar`/`rank-avatar`(仅 `.podium-avatar-wrap` 定位容器保留);5 处均改新结构。
- 此坑记入 MEMORY.md(微信圆形头像裁剪陷阱)。
## 月榜/年榜头像仍方(仅日榜圆)二次修复(2026-07-21)
用户验收:日榜正常,月榜/年榜头像还是方的。
根因确认:三档周期共用同一 `.avatar` 结构(WXML/WXSS 完全一致),差异只在图片内容——
- 日榜其他用户多未设头像 → 显示 `peopleFill` 占位 SVG → 被外层 view 的 overflow:hidden 裁圆;
- 月榜/年榜出现真实照片头像(cloud://)→ 该基础库下,外层 view 的 overflow:hidden **裁不掉远程照片的原生图片层**,照片露方。
修复:给 `.avatar__img` 自身补 `border-radius:50%`(双保险,对齐设置页 `.avatar-btn`+`.avatar-img` 的实测可用写法),与父层 overflow:hidden 并存。
- 设置页 `settings.wxml:10` 的"我的头像"正是此写法(button overflow:hidden + image border-radius:50%)且用户未投诉,证明该组合在用户机器上可靠。
- 已更新 MEMORY.md 圆形头像陷阱条目为更精确结论:必须 image 自身 border-radius,不能只靠父层 overflow。
## 月/年榜头像"大圆套小圆"缩小问题(2026-07-21
用户验收:圆形有了,但真实照片偏小,像大圆里套小圆,不像设置页头像。
根因:`.avatar--md/lg/sm` 自带 `padding:20/26/0 rpx` 留白——占位 SVG 是"图标居中+外圈"(OK),但真实照片被这圈留白缩成小图 → 大圆套小圆。
修复(对齐设置页"有头像就填满"的写法):
- `leaderboard.wxml` 5 处头像加条件 `has-avatar` 类(`{{x.avatarUrl ? 'has-avatar' : ''}}`),且 `<image>``mode` 改为 `{{x.avatarUrl ? 'aspectFill' : 'aspectFit'}}`(真实照片填满圆、占位图仍 fit 居中)。
- `leaderboard.wxss` 新增 `.avatar.has-avatar { padding: 0; opacity: 1; }`(双类选择器特异性高于 `.avatar--md/lg/sm`,覆盖留白与淡透明度),照片填满圆且不发虚。
- 未设头像的占位图仍保留 padding 环(设计预期),仅真实照片受影响。
- 仅前端改动,结构与设置页完全一致。
+72
View File
@@ -0,0 +1,72 @@
# 2026-07-22 工作日志
## 修复:首页「今日目标」数字在真机被圆圈遮挡
- 现象:模拟器正常,真机(用户开了系统/微信字体缩放)大字号下数字超出固定圆形 `.target-ring`(280rpx, overflow:hidden) 被裁切。
- 根因:微信按 `fontSizeScaleFactor` 自动放大所有文字(含 rpx 字号),但不放大 rpx 盒子尺寸 → 真机文字变大、容器不变 → 溢出裁切。模拟器跑标准档(1.0)不重现。
- 改动:
- `pages/index/index.js`:新增 `targetTimeFontSize`(默认 84) 数据;`onLoad``_readFontScale()`,用 `wx.getAppBaseInfo().fontSizeScaleFactor`(回退 `getSystemInfoSync().fontSizeSetting/17`)反向补偿 `84/factor`clamp 到 [48,96],抵消系统缩放使数字视觉恒定。
- `pages/index/index.wxml``target-time` 增加 `style="font-size: {{targetTimeFontSize}}rpx;"`
- `pages/index/index.wxss``.target-time``white-space:nowrap`(防换行顶出)。
- 校验:`node --check index.js` 通过。
- 此坑记入 MEMORY.md(微信字体缩放导致文字溢出固定盒子)。
## 修复:同上改动导致模拟器数字变小
- 现象:真机修复后正常,但模拟器「今日目标」数字又变小/不正常。
- 根因:模拟器 `getAppBaseInfo().host.env === 'devtools'`,会**原样返回手机真实缩放倍率**(如 1.3)却**渲染时不放大文字**;真机才会放大。之前的补偿把字号缩成 `84/1.3≈65rpx`,模拟器按 65 渲染→偏小。
- 改动:`pages/index/index.js` `_readFontScale()` 增加判断——`host.env === 'devtools'` 时跳过补偿、直接用 84rpx 设计字号;仅真机/PC 微信才做 `84/factor` 补偿。
- 校验:`node --check index.js` 通过。devtools 不对称坑补入 MEMORY.md。
## 优化:排行榜头像延迟(批准实施 ①②)
- 措施①云函数预解析:`cloudfunctions/leaderboard/index.js` 新增模块级 `_resolveAvatars(urls)`,在 `exports.main` return 前收集 ranked+myEntry 的全部 `avatarUrl`,过滤 `cloud://` 的批量调 `cloud.getTempFileURL({fileList})` 换成临时 https URL(按 50/批分页;失败 catch 降级保留原 fileID)。前端 `<image>` 不再在渲染时做 getTempFileURL 往返。
- 措施②上传压缩:`pages/settings/settings.js``_uploadAvatar` 上传前先 `_compressAvatar(filePath)``wx.compressImage({quality:80, compressedWidth:200})` 把 chooseAvatar 原图压到最长边 200px;失败/不支持(旧基础库/devtools)回退原图,上传不崩。
- 校验:`node --check` 两文件均通过。
- ⚠️ 部署要求:`leaderboard` 云函数改动**必须重新部署**才能生效(前端 settings.js 改了重新编译即可,对之后上传的头像生效;已存旧 cloud:// 头像由云函数预解析加速)。两者互补。
## 优化:记录页底部列表与月份选择器联动(方案 B 分段切换)
- 现象:切月份时日历热力图会变,但下方「最近记录」列表不变(它绑的是全局 top-30 `historyList`,与月份无关)。
- 改动(`pages/records/`):
- `records.js`data 加 `listMode:'month'``refresh()` 新增 `displayList`month 模式=`monthRecords` 即所选月逐条记录,recent 模式=全局 top-30 `historyList`);新增 `onToggleListMode(e)``data-mode` 切模式后 `refresh()`。切月时 `listMode` 保持不变。
- `records.wxml`:历史卡片头部加 `[本月|最近]` 分段控件(`.rec-seg`,复用排行榜/设置页 `.period-seg` 视觉语言);标题动态为 `{{year}}年{{month}}月记录``最近记录`;列表 `wx:for="{{displayList}}"`;空态文案随模式切换(本月→"本月还没有训练记录/换个有训练的月份看看吧")。
- `records.wxss`:新增 `.rec-seg`/`.rec-seg-item`/`.rec-seg-item.active` 样式;`.history-hint``margin-bottom:8rpx`
- 校验:`node --check records.js` 通过。
- 注:`getRecordsByMonth(monthKey)` 返回该月逐条记录且存储时已按最新在前排序,故月份联动零额外数据成本。日历热力图仍用 `monthRecords`,不受 listMode 影响。
## 内容产出:公众号文章(产品介绍)
- 用户要求写公众号文章并发送草稿箱。确认:环境无微信公众号 connector/MCP、无 appid/appsecret,且本机对外 TLS 不稳(同 git push 的 SSL_ERROR_SYSCALL),无法真发草稿箱。用户选择「产品介绍 + 先成稿自行发布」。
- 产出:`docs/公众号文章_平板支撑小程序.md`(约 1200 字,面向健身/普通用户,含封面/配图建议注释与文末二维码占位)。结构:痛点引入 → 训练计时 → 首页打卡 → 排行榜领奖台 → 记录日历 → 个性化主题 → 为什么是它 → CTA。所有功能点均基于真实已实现功能,未编造。
- 注:「小程序名称」为占位,需用户替换实际上架名;发布方式:复制 markdown 到公众号编辑器(或转 HTML 富文本粘贴)。
## 配置:微信公众号 MCP connector 骨架(真发草稿箱)
- 用户选择「有公众号+凭据,帮我配」。已实现自定义 MCP server。
- 脚本:`~/.workbuddy/wx-mcp-server.js`(纯 Node + curl 调微信 APIcurl 自动走 HTTPS_PROXY5 工具:get_access_token/md_to_html/upload_thumb/add_draft/publish_article)。凭据存 `~/.workbuddy/wx_credentials.json`(占位模板,chmod 600,不进 mcp.json),token 缓存 `~/.workbuddy/wx_token.json`
- 合并进 `~/.workbuddy/.mcp.json``mcpServers.wx-mcp`(保留原 `connector-proxy`)。
- 握手测试(initialize+tools/list)通过;网络探活 `api.weixin.qq.com` 直连 TLS 正常(返回 invalid appid,国内直连可达,与 github 国外被墙不同)。
- 待用户:①填真实 appid/appsecret ②提供封面图(路径/URL)③确认公众号为认证服务号(drafts/add 权限)。之后用 publish_article 把 docs/公众号文章 发到草稿箱。
- 注:实际 MCP 配置点在带点的 `.mcp.json`(系统说明的无点版不存在),以实际文件为准。
## 设置页:加「功能介绍」引导到公众号文章入口
- 技术结论:微信小程序**不支持**直接点击跳转公众号文章(wx.navigateTo 只能跳内部页面;web-view 仅限已配置业务域名,且公众号文章 mp.weixin.qq.com/s/ 被禁)。故用「复制链接→微信打开」引导。
- 实现(用户选方案 B 引导到公众号文章):
- `config.js`:新增 `articleUrl` 常量(占位 `https://mp.weixin.qq.com/s/REPLACE_WITH_ARTICLE_LINK`,文章发到公众号后填真实永久链接)。
- `pages/settings/settings.js`data 加 `appArticleUrl`/`showArticleGuide`;新增 `onOpenArticle`/`onCloseArticle`/`onCopyArticle`wx.setClipboardData 复制链接+toast,复用 onCopyWechat 同款交互)。
- `pages/settings/settings.wxml`:「关于」卡片加「功能介绍」about-rowbindtap onOpenArticle);末尾加底部 sheet 引导弹窗(显示链接+复制按钮,仿 plan-editor 风格,style=themeStyle 跟随暗黑)。
- `pages/settings/settings.wxss`:新增 `.article-guide-*` 样式(复用 --card-bg/--primary 等变量与 planEditorSlideUp 动画)。
- 校验:`node --check settings.js` 通过;grep 确认 wxml/js/config 引用一致。
- 待办:①把真实文章链接填进 config.articleUrl(需先把文章发到公众号拿永久链接,可走之前搭的 wx-mcp 草稿箱流程)②重新编译小程序验证弹窗。
## 设置页:「功能介绍」复制链接 → 直接显示公众号名称
- 用户反馈复制链接打开太麻烦,改为在「关于」直接显示公众号名称,且名称定义在 config.js。
- 改动:
- `config.js``articleUrl` 替换为 `officialAccount: '爱咖啡的人'`
- `pages/settings/settings.js`data 删除 `appArticleUrl`/`showArticleGuide`,加 `appOfficialAccount: config.officialAccount`;删除 `onOpenArticle/onCloseArticle/onCopyArticle`,新增 `onCopyOfficialAccount`wx.setClipboardData 复制公众号名 + toast「已复制,去微信搜索关注」)。
- `pages/settings/settings.wxml`:「关于」的「功能介绍」about-row 改成「公众号」行,value=`{{appOfficialAccount}}`bindtap=`onCopyOfficialAccount`;删除末尾 article-guide 底部 sheet 弹窗。
- `pages/settings/settings.wxss`:删除全部 `.article-guide-*` 无用样式。
- 校验:`node --check` settings.js/config.js 通过;grep 确认无 article-guide/articleUrl/showArticleGuide 残留。
- 约定:公众号名称后续若变更,只改 `config.js``officialAccount` 即可。
## 提交流程:git push 到 github 的可用配置(本机)
- 本机有本地代理 `HTTPS_PROXY=http://127.0.0.1:59305`github.com 经其透明转发。直接推会失败(沙箱无网 / HTTP/2 framing / TLS 握手失败)。
- **可用命令**(关键:必须 `dangerouslyDisableSandbox` 才能访问 127.0.0.1 的代理;并关掉 HTTP/2,否则代理报 framing 错误):
`git -c http.version=HTTP/1.1 push origin main`(沙箱禁用 + 走 env 的 HTTPS_PROXY)。
- 试过不可用的组合:显式 `-c http.proxy=... -c https.proxy=... -c http.sslVersion=tlsv1.2` → 代理空响应;默认 HTTP/2 → framing 错误。
- 本次提交 `3e1d3bd`(公众号名称改动 + docs 文章),已成功 push70b87d4..3e1d3bd)。`.DS_Store`/`.workbuddy/` 保持未跟踪、不提交。
+125
View File
@@ -0,0 +1,125 @@
# 2026-07-28 工作日志
## 记录页「训练勋章」UI 改造(去掉 emoji 图标)
用户反馈勋章卡片 UI 问题并要求"不用 emoji 图标"。完成:
- `utils/icons.js`:新增 3 个勋章 PATHSbolt 闪电 / moon 月亮 / diamond 钻石),并补 white 变体
boltWhite / moonWhite / diamondWhite / crownWhitecrown/hot/trophy 复用已有 PATHS)。
- `config.js``trainingDayBadges` 每项 `icon`/`iconWhite` 改为 icons.js 的 key(如 'hot','bolt'),不再用 emoji 字符串。
- `pages/records/records.wxml`:节点 `<text>`emoji 改 `<image class="badge-node-icon">`,按 unlocked 选 `icons[item.iconWhite]`(白) 或 `icons[item.icon]`(灰)
新增 `.badge-names` 层显示勋章名;把 `nextBadgeHint` 从标题右侧移到"累计训练 X 天"下方,避免标题行被拉扯。
- `pages/records/records.wxss`:节点 `.badge-node.unlocked` 改为实心主题色圆(背景 var(--primary))+ 白色图标;未解锁用灰描边+灰图标(去掉 grayscale/opacity 压暗,保证可见);
`.badge-node-icon` 改 32rpx image;新增 `.badge-names`/`.badge-name` 样式;`.badge-next` 改为居中块。
- `pages/records/records.js`:去掉"全部勋章已解锁 🎉"里的 emoji。
验证:`node --check` 三个 JS 全过;项目内已无 emoji。
## 训练勋章 P0/P1 修复 + 标题/文案对齐调整
用户确认统计口径为"累计天数、可中断",并截图指出标题图标与文字未对齐、图标太小、中间文案拥挤。完成:
- `config.js`:勋章命名全部改为累计语义(一周累计 / 半月累计 / 月度累计 / 双月累计 / 百日累计),注释明确"可中断、非连续打卡"。
- `pages/records/records.js`
- 进度算法改为 sqrt 感知映射:`pos(d)=5+sqrt(d)/sqrt(100)*90`,节点与填充同映射 → 解锁时填充正好抵达节点、满级时 100% 满格;首枚节点从 20.6% 开始,新手期进展可见。
- 新增 `_checkBadgeCelebration()`:持久化 `badge_celebrated` 集合,首次仅初始化不刷屏,后续有新解锁时弹 toast"解锁勋章:XX"并给节点打 `justUnlocked` 标记。
- `pages/records/records.wxml`:标题下新增 `.badge-sub`"累计训练天数,非连续打卡";节点加 `celebrate` 类。
- `pages/records/records.wxss`
- 标题 crown 图标从 28rpx 放大到 36rpx,加 `align-self:center``margin-top:-2rpx` 光学对齐;重置 `.badge-header .section-title` 的 margin-bottom。
- 新增 `.badge-sub` 样式;调整 `.badge-days``.badge-next` 的上下间距,减少中间文案拥挤感。
- 新增 `.badge-node.celebrate` pulse 动画与 `@keyframes badgePop`
验证:`node --check` 通过;旧名/emoji 已无残留。
## 训练勋章卡片去拥挤化
用户截图反馈勋章卡片整体拥挤、文案过多、节点图标挤压。完成:
- `pages/records/records.wxml`
- 删除 `.badge-sub`("累计训练天数,非连续打卡")这一行;
-`.badge-days``.badge-next` 合并为同一行 `.badge-status`(中间用圆点分隔),减少顶部文案层数;
- 删除常驻的 `.badge-names` 层;勋章名改为点击节点时通过 `onBadgeTap` toast 展示;
- 节点加 `data-days` + `bindtap="onBadgeTap"`
- `pages/records/records.js`
- 新增 `onBadgeTap()`:点击已解锁节点提示"已解锁:XX",未解锁提示"再练 N 天解锁「XX」"
- 轨道首尾留白从 5% 缩小到 2.5%,让 6 个节点在窄屏分布更舒展。
- `pages/records/records.wxss`
- 删除 `.badge-sub``.badge-names``.badge-name` 样式;
- 新增 `.badge-status` 及其子元素样式(居中、允许折行、圆点分隔);
- 调整 `.badge-labels``margin-top``opacity`,让天数数字更轻盈;
- 保留 `.badge-node`/`.badge-node-icon`/`.badge-track` 等核心样式。
验证:`node --check` 通过;旧 class 名(badge-sub/badge-names/badge-name)已无残留。
## 训练勋章卡片间距与节点尺寸微调
用户再次截图反馈行间距拥挤、勋章图标粘连、节点圆盘遮挡底部天数。完成:
- `pages/records/records.wxss`
- 状态行 `.badge-status` 下边距从 28rpx 增大到 44rpx
- 轨道 `.badge-track` 上下间距从 `36rpx 0 4rpx` 调整为 `44rpx 0 6rpx`,整体呼吸感加大;
- 节点圆盘从 56rpx 缩小到 44rpx,图标从 32rpx 缩小到 26rpx,避免遮挡下方天数;
- 天数标签层 `.badge-labels``margin-top` 从 14rpx 增大到 22rpx`height` 从 26rpx 增大到 30rpx,让数字与圆盘彻底分离;
- 解锁 pulse 光晕从 8rpx 缩小到 6rpx,匹配更小节点。
验证:`node --check` 通过。
## 训练勋章:改回等距节点 + 恢复大图标
用户指出前 3 个节点间距不一致(sqrt 映射副作用),并希望不缩小图标、改用增大区域高度来避免遮挡。完成:
- `pages/records/records.js`:进度算法从 sqrt 感知映射**改回等距排列**——`segWidth=(100-2*TRACK_PAD)/(n-1)``pos(i)=TRACK_PAD+i*segWidth`,节点与填充同套线性插值。TRACK_PAD 回到 4%;副作用是节点间距完全一致、视觉更整齐,但"新手期进度条动得快"的感知弱化(用户更看重整齐)。
- `pages/records/records.wxss`
- 节点圆盘恢复 56rpx、图标恢复 32rpx、celebrate 光晕恢复 8rpx(撤销上一轮的缩小);
- 通过增大区域高度避免遮挡:`.badge-track` margin 改为 `48rpx 0 20rpx``.badge-labels` margin-top 改为 32rpx;圆盘下缘到天数字起始约 52rpx(圆盘半径 28rpx + 留白),不再遮挡;
- 状态行 `.badge-status` 下边距保持 44rpx。
验证:`node --check` 通过。
## 训练勋章:让"已拥有勋章"直观可见(方案 1 + 3)
用户确认保持等距进度条布局不变,痛点转为"用户无法直观看到自己目前有哪些勋章"。落地方案 1(已解锁名字常驻)+ 方案 3(✓ 角标强化拥有感):
- `pages/records/records.wxml`
- 已解锁节点加 `<view class="badge-check" wx:if="{{item.unlocked}}">` 角标;
- `.badge-label` 改为 `item.unlocked ? item.name : item.days`——已解锁渲染勋章名、未解锁只渲染天数数字(避免之前 6 名字常驻拥挤,现在只有已解锁的 1–3 个显示名字)。
- `pages/records/records.wxss`
- 新增 `.badge-check`24rpx 主题色圆 + 2rpx 白描边,定位在节点 `right:-3rpx; bottom:-3rpx`)与 `.badge-check::after`(纯 CSS border 旋转 45° 画白勾;因图标 SVG 颜色烤死无法用变量改色,故用伪元素画);
- `.badge-label.unlocked` 字号 20→22rpx、字重 600→700,让已拥有的勋章名更醒目。
验证:`node --check` 三个 JS 全过;wxml 的 badge-check/badge-label 与 wxss 样式一致。首尾 TRACK_PAD=4%,节点在 4%96%,✓ 角标不溢出卡片。
## 训练勋章:新增 80 天「洪荒神登」图标(star)
用户在 config.js 的 trainingDayBadges 加了 80 天「洪荒神登」,临时复用了 diamond 图标(与 60 天「上古神登」撞图标)。完成:
- `utils/icons.js`PATHS 新增 `star`Material 五角星路径,24x24 单色);build() 的 Badge white variants 段新增 `out.starWhite = _svg(PATHS.star, '#FFFFFF')`(解锁态白星,坐在主题色圆盘上)。
- `config.js`:第 69 行 80 天勋章 `icon/iconWhite``diamond/diamondWhite` 改为 `star/starWhite`;并修正上方过时注释(原写"name 一律用累计措辞",实际用户已改成"登"系列幽默命名),改为"口径=累计去重训练日(可中断);name 仅作趣味命名,不暗示连续打卡"。
验证:`node --check` 通过 icons.js / config.js。注意:勋章现 7 枚,等距每段约 115rpx、节点间隙约 59rpx,仍不重叠(之前评估 8–10 枚才明显拥挤)。
## 排行榜首开延迟分析(3-4s)+ P1 索引/投影落地
用户反馈排行榜首次打开有时延迟 3-4s 才显示数据。分析结论(已用示意图拆解):
- 根因:排行榜在**用户请求时同步全量计算** `cloudfunctions/leaderboard/index.js``_fetchLatestByOpenid()` 全表扫描 plank_data;没有预计算快照。客户端缓存(30min)只缓解"重复进入",云实例缓存(5min, 内存不跨容器)随低流量容器回收频繁变冷。
- 耗时拆解(冷路径):云函数冷启动 ~1.2s + 全表扫描 ~1.5-2.5s + 头像 getTempFileURL ~0.3-0.5s;热路径 <0.6s。
- 关键坑:原查询 `_.or([{updatedAt:gte(90d)},{updatedAt:exists(false)}])``exists(false)` 分支无法走索引 → 实质全集合扫描;且每次读全字段(settings/streak/customPlans/themeId 都在 doc 里),载荷大。
- 修复分层:P0 预计算快照(定时触发器写 leaderboard_snapshot,用户函数只读1 doc)P1 updatedAt 索引+投影;P1 App 启动预热;P2 force 不再绕过云缓存。
用户已在控制台给 plank_data.updatedAt 建单字段索引。我落地 P1 代码侧:
- `cloudfunctions/leaderboard/index.js``_fetchLatestByOpenid`:去掉 `exists(false)` OR 兜底分支(让 gte(90d) 干净走索引),并给查询加 `.field({records,profile,_openid,updatedAt})` 投影,跳过 settings/streak/customPlans/themeId 大字段,缩小单次读取载荷。
- 取舍:无 updatedAt 的极旧遗留 doc(必已 90+ 天未同步)将不再入榜,可接受。
- 验证:`node --check cloudfunctions/leaderboard/index.js` 通过。
下一步可选:P0 定时快照(彻底消除首开等待与容器冷启动影响)。
## 排行榜延迟根治:P0 预计算快照 + P1 预热 + P2 force(已落地)
用户确认做 P0/P1/P2。完成:
- `cloudfunctions/leaderboard/index.js` 重写:
-`_buildFromScan(latestByOpenid, period, limit, myOpenid)`:从已抓取 Map 算单周期榜(含头像 getTempFileURL 解析),供实时与定时两条路径共用;
- 新增 `_persistSnapshot(period, result)`:写 `leaderboard_snapshot` 集合(doc._id=周期,字段 period/ranked/myOpenid/updatedAt);首次写自动建集合;
- 新增 `_serveFromSnapshot(period, limit, myOpenid)`:读快照(1 次 get≈50ms),按 openid 计算 myEntry;快照缺失或过期(>6min)返回 null 回退实时;
- 新增 `_rebuildSnapshots()`:扫描一次、`_buildFromScan` 复用算 day/month/year 三榜并各自 `_persistSnapshot`SNAPSHOT_TOP=500=函数 maxRank 上限);
- 新增 `_computeBoard(period, limit, myOpenid, {force,persist})`:实时重算并可选回写快照;
- `exports.main` 区分:**timer 触发**(event.type==='timer'||Type||triggerName||MessageType) → `_rebuildSnapshots()`;客户端请求 → force 则 `_computeBoard(force:true,persist:true)`(训练后那次,唯一整表扫描),否则 `_serveFromSnapshot`,缺失/过期则 `_computeBoard(force:false,persist:true)` 兜底并回写。
- 响应结构保持 `{period,ranked,myOpenid,myEntry,updatedAt}` 不变,客户端 `_applyResult` 无改动。
- `cloudfunctions/leaderboard/config.json`:加 `triggers` 定时器 `snapshotTimer` type=timer config=`0 */4 * * * * *`WeChat 7 字段 cron,每 4 分钟触发一次重建快照)。**注意:需重新部署该云函数定时器才生效。**
- `app.js`onLaunch 在 `cloud.init()` 后 fire-and-forget 调一次 `leaderboard`(period=day, maxRank=config.leaderboardMaxRank),提前填充云实例/快照(P1 启动预热);guard 用 `cloud.enabled`;新增 `require('./config')`
- `pages/leaderboard/leaderboard.js`:仅更新过时注释(原"云函数全表扫描冷启动 2-3s"改为"现读预计算快照 1 次 get≈50ms")。force 语义:快照架构下正常打开读快照(已满足"force 不再绕过云缓存"诉求),仅训练后那次 force 触发实时重算+回写。
验证:`node --check` 通过 index.js / app.js / leaderboard.js / config.js;函数内调用图一致。
效果预期:首开/冷启动排行榜从 2-3s 降到 ≈50ms(1 次 get);容器冷启动不再慢;每 4min 离线刷新三榜,数据最多延迟 4min(训练后 force 立即刷新)。
## 排行榜快照写入失败修复(wx-server-sdk set 需 { data } 包裹)
用户手动触发云函数,leaderboard_snapshot 集合仍无数据,日志报 `parameter.data should be object instead of undefined`day/month/year 三榜 persist 全失败)。
- 排查:本地 `node --check` 通过;本地 mock 跑 `_persistSnapshot` 逻辑确认 `set` 收到的是对象(data 在 console.log 时存在、rankedLen 能打印);WebSearch 定位根因——wx-server-sdk 写操作(add/update/set)参数必须是 `{ data: {...} }` 包裹,SDK 读 `parameter.data`,裸对象会让 `data.data`=undefined 触发该报错(读操作 `get()` 不受影响)。此前判断"部署代码未刷新"是错的。
- 修复:`cloudfunctions/leaderboard/index.js``_persistSnapshot` 改为 `docRef.set({ data: { period, ranked, myOpenid, updatedAt } })`(引用拆分存 docRef + data 包裹 + 诊断日志 + .errMsg 容错)。读取 `doc.data` 兼容(SDK 解开 data 外套,文档内容即裸对象)。
- `day rankedLen=0` 非 bug`record.date` 格式为 `YYYY-MM-DD HH:MM:SS`formatDate 生成,`pages/records/records.js:225` 注释确认),`_buildFromScan``r.date.startsWith('2026-07-28')` 能匹配;0 多为测试时(09:30)当天确实无训练记录,month/year 历史累计有 5 条属正常。
- 验证:`node --check` 通过。
- 待办:用户需**重新部署 leaderboard 云函数**(含最新 _persistSnapshot),再手动触发/进榜下拉刷新,日志应现 `persist ok`、集合出现 day/month/year 三文档。
+43
View File
@@ -0,0 +1,43 @@
# 项目长期记忆(wx_pbzc 平板支撑训练小程序)
## 关键技术陷阱
- **小程序组件 style isolation**:自定义组件默认 `isolated`。组件内 `variant` 拼出的类(`gradient`/`in` 等)在组件作用域,页面里写的 `.组件类 <slot后代>` 跨作用域选择器**全部失效**(如 `.ui-card.gradient .streak-num`)。唯一能跨边界传递的是 **CSS 自定义属性**(会沿 DOM 继承进 slot)。
- 推论:渐变/主色容器内要反白的文字,必须用 `var(--text)/var(--text-secondary)`(容器在 `.gradient` 里重定义过即会变白),**绝不能用 `var(--primary)`**——否则橙色字画在橙色底上=看不见。
- 进度条填充同理:用 `var(--on-primary, <原渐变>)` 让容器可覆盖为白,而非依赖失效的后代选择器。
- **图标 SVG 颜色是烤死在 data URI 里的**(`utils/icons.js``build()``*Fill``primary` 主题色、`*`(无 Fill)用灰 `#999`)。CSS 的 `color`/`currentColor`/变量**都改不了 `<image src>` 这种图标**。所以:
- 放在**主色/渐变底**gradient hero 卡、primary 实心按钮)上的图标必须用**白色变体**(`hotWhite`/`formWhite`/`playWhite`/`trophyWhite` 等),否则同色隐形。
- 加新图标或新彩色容器时,先想清楚底色:浅色底用 `*Fill`(主题色),彩色/主色底用 `*White` 变体。
- 主按钮 `ui-btn--primary` 背景是橙渐变,`playFill`(橙) 放上去即隐形——须用 `playWhite`。ghost/outline 按钮底色浅或灰,用 `*Fill`/灰色变体即可。
- `components/ui-btn` 图标尺寸 `1.3em`(随按钮字号自适应),不要回到 `1em` 以免显得过小。
- 深色模式有两套事实源,改配色前先统一:`theme.json` 的 dark 与 `app.wxss`/`utils/theme.js` 要一致(已统一到 `#131316` 系)。
- **微信圆形头像/图片裁剪陷阱(用户基础库实测)**:本项目的圆形头像在用户机器上表现如下——
- 单独给 `<image>``border-radius:50%`(无 overflow 父层)→ 圆框里露方图,失效。
- 外层 `<view>``overflow:hidden`+`border-radius:50%` 包裹 → **能裁切 SVG 占位图,但裁不掉远程照片(cloud:// 真实头像)**,照片仍方。所以日榜(多为占位图)正常、月榜/年榜(真实照片)却方。
- **唯一可靠写法**:外层 `overflow:hidden` 父层 **且** `<image>` 自身也加 `border-radius:50%`(双保险)。设置页 `.avatar-btn`(button,overflow:hidden)+`.avatar-img`(image,border-radius:50%) 即是此写法且实测正常。排行榜 `.avatar`/`.avatar__img` 已对齐这一写法。
- 推论:以后做圆形头像,**必须** image 自身 `border-radius:50%`,不能只靠父层 overflow。
- **微信系统字体缩放导致文字溢出固定盒子(模拟器不重现、真机重现)**:微信会按用户「设置→通用→字体大小」自动放大**所有文字(含 rpx 字号)**,但**不放大 rpx 盒子尺寸**width/height/padding)。症状:固定尺寸的圆形/方形容器里的文字在真机被放大、超出后被 `overflow:hidden` 裁掉,模拟器(标准档倍率 1.0)正常。
- 获取缩放:`wx.getAppBaseInfo().fontSizeScaleFactor`(当前字号÷标准 17px);旧接口 `getSystemInfoSync().fontSizeSetting`px,标准 17)。
- **真机/模拟器不对称(关键坑)**:`getAppBaseInfo().host.env` 在模拟器是 `'devtools'`、真机是 `'wechat'`。模拟器会**原样返回你手机的真实缩放倍率**,但**渲染时并不会把文字放大**;真机才会真的放大。所以若不做区分,补偿把字号缩成 `design/factor` 后:真机放大回 design(正常),模拟器按缩小值渲染(数字变小=「不正常」)。→ **必须在 `host.env === 'devtools'` 时跳过补偿、直接用设计字号**;仅在真机/PC 微信才补偿。首页 `_readFontScale()` 已加此判断。
- 补偿法:把容器内的关键文字字号反向除以该倍率(`designRpx / factor`),并在 JS 里 clamp 到安全区间(如 48–96),使真机渲染成和模拟器一致的视觉大小、永不溢出。首页 `.target-time` 已用此法(`index.js` `_readFontScale()`)。
- 配套 CSS`white-space:nowrap` 防换行顶出圆圈。
- 推论:以后凡把文字放进**固定尺寸的圆形/方形容器**且依赖 `overflow:hidden` 裁切,都必须考虑字体缩放补偿,否则真机大字体档必溢出。
- **排行榜头像 1–2s 延迟根因(设计使然,非 bug)**:`profile.avatarUrl` 存的是 `cloud://` 云存储 fileID`settings._uploadAvatar``wx.cloud.uploadFile` 返回 fileID);云函数 `leaderboard` 直接透传,前端 `<image src="cloud://...">``setData` 渲染时才把 fileID 解析成临时 https URL(等效一次 `getTempFileURL` 网络往返)再下载。文字(昵称/时长/排名)是本地字符串瞬时渲染 → 故"文字先出、头像后出",延迟正是头像专属的「云存储解析 + 原图下载」段,叠加云函数冷启动。
- 占位图 `peopleFill` 是本地 SVG data URI 瞬时,所以日榜(多为占位)无感、月/年榜(真实照片)延迟明显。
- 优化方向:①云函数返回前对 `cloud://``avatarUrl` 批量 `cloud.getTempFileURL` 预解析成 https;②`_uploadAvatar` 上传前把 `chooseAvatar` 原图压缩/裁剪到最长边 ~200px、转 jpg/webp,体积降一个数量级;③云函数保活降冷启动。
- **云函数数据库写操作必须 `{ data: {...} }` 包裹(wx-server-sdk 关键坑)**`wx-server-sdk`(本项目 2.6.3) 的 `db.collection().doc(id).set(x)` / `.update(x)` / `.add(x)` **写操作**,参数必须是 `{ data: realObj }` 包裹形式——SDK 内部读 `parameter.data` 当作要写入的文档。直接传裸对象 `set(realObj)` 会让 SDK 读 `realObj.data` === `undefined`,报 `parameter.data should be object instead of undefined`(读操作 `get()` 不受影响,裸对象/无参都行)。`leaderboard` 云函数 `_persistSnapshot` 踩过此坑:写快照全失败、集合一直空。
- 修复:`docRef.set({ data: { period, ranked, myOpenid, updatedAt } })`。写入后文档内容即 `realObj`(data 外套被 SDK 解开),读取 `doc.data` 仍是 `realObj`,读逻辑无需改。
- 推论:**以后任何云函数写库代码,先写 `{ data: ... }`**,别沿用客户端 SDK 的裸对象习惯;出错时先怀疑是不是漏了 data 包裹,而不是怀疑部署。
## 排行榜架构(2026-07-28 落地 P0/P1/P2 后)
- **预计算快照**`cloudfunctions/leaderboard``exports.main` 现在区分两种调用——
- **定时触发器**`config.json``triggers``snapshotTimer` / `0 */4 * * * * *`,每 4 分钟)进入 `_rebuildSnapshots()`:扫描一次 `plank_data``_fetchLatestByOpenid`,走 updatedAt 索引+投影),复用 `_buildFromScan()` 算 day/month/year 三榜,各自写进 `leaderboard_snapshot` 集合(doc._id=周期,字段 `period/ranked/myOpenid/updatedAt`;首次写自动建集合)。
- **客户端请求**:默认走 `_serveFromSnapshot()`1 次 `get` ≈ 50ms),快照缺失/过期(>6min)才 `_computeBoard()` 实时重算并回写。**首开/冷启动已从 2-3s 降到 ≈50ms。**
- **force 语义**:仅训练后那一次(客户端 `onShow``_lb_force_refresh``force=true`)走 `_computeBoard(force:true,persist:true)` 实时重算+回写快照;其他 99% 请求读快照,已满足"force 不再绕过云缓存"诉求。
- **P1 启动预热**`app.js` `onLaunch``cloud.init()` 后 fire-and-forget 调一次 `leaderboard`(day),提前暖云实例/快照。
- **头像延迟**:云函数内 `getTempFileURL` 预解析已落地(在 `_buildFromScan` 里),前端不再懒加载,故"文字先出头像后出"的 1-2s 延迟已大幅消除;快照每 4min 重写,临时 URL 始终新鲜。
- **改动注意**:要生效必须**重新部署 `leaderboard` 云函数**(定时器随部署注册)。`plank_data.updatedAt` 单字段索引需在控制台已建(用户已建)。响应结构 `{period,ranked,myOpenid,myEntry,updatedAt}` 不变,客户端 `leaderboard.js` 无需改逻辑(仅注释更新)。
## 约定
- UI 动词说法:index=首页/训练首页,timer=训练页,leaderboard=排行榜,records=记录,settings=设置。
- 美化按"见效快优先"分批做,每轮 node --check + grep 残留再交付。
- **git 推送需走 sandbox 外**:本机 Bash 工具默认 sandbox 隔离网络,`git push` 到 github.com 会 `SSL connection timeout`(约 5 分钟才报错)。必须用 `dangerouslyDisableSandbox: true` 在沙箱外执行推送(或 `run_in_background` + 沙箱外)才能连上。无 SSH keyremote 仅 HTTPS。