# 微信小程序代码分析报告(平板支撑训练) 审查范围:`app.*`、`pages/*`(5 页)、`utils/*`(11 模块)、`cloudfunctions/*`(4 个)、`custom-tab-bar`、`components/*`。 整体评价:代码质量较高,日志、容错、云同步去重/合并都做得很到位。以下为发现的问题,按严重程度排列。 > **修复进度**(2026-07-21):高优 #1、#2 已修;中优 #4、#5、#6 已修;低优 #7、#9、#10 已修。 > #3、#8 经复核为**误报**,无需修改(见正文)。 --- ## 🔴 高(High) ### 1. 同一天多次训练会错误跳过计划天数 ✅ 已修复 - **位置**:`utils/plan.js` → `getPlanDay()` - **问题**: ```js const trainedDays = new Set(eligibleRecords.map(r => r.date)).size ``` `r.date` 是 `"YYYY-MM-DD HH:MM:SS"`(含时分秒)。同一天训练两次会得到两条不同的字符串, `Set` 计成 2 天,导致 `planDay` 被 +2,**平白跳过一天的进度**。计划进度应按"日历天"而非"训练次数"计算。 - **修复**:改用 `dateOnly` 去重 ```js const trainedDays = new Set(eligibleRecords.map(r => dateOnly(r.date))).size ``` (`dateOnly` 已在 `utils/util.js` 中导出,本文件已 `require`) ### 2. 系统深色模式切换时,3 个 tab 页不会实时重绘 ✅ 已修复 - **位置**:`app.js` 的 `darkMod.watchDarkMode` 回调 + `pages/index|records|leaderboard/*.js` - **问题**:回调只对有 `applyThemeToPage` 方法的页面生效: ```js pages.forEach(p => { if (p && typeof p.applyThemeToPage === 'function') p.applyThemeToPage() }) ``` 但 **index / records / leaderboard 三个页面都没有定义 `applyThemeToPage` 方法**(只有 `settings` 定义了)。 结果:用户在手机系统里切深色/浅色时,这三页不会立刻变色,要等下次 `onShow` 才刷新。 - **修复**:在 app.js 的回调里补上兜底分支(与 settings 保持一致),并对自定义 tabBar 调用 `updateTheme()`: ```js pages.forEach(p => { if (p && typeof p.applyThemeToPage === 'function') p.applyThemeToPage() else if (p && p.route) { try { themeMod.applyThemeToPage(p) } catch(e){} } try { const tb = p.getTabBar && p.getTabBar(); if (tb && tb.updateTheme) tb.updateTheme() } catch(e){} }) ``` (三页 `data` 中本就有 `themeStyle`,且 onLoad/onShow 都套用过主题,兜底可直接生效。) --- ## 🟠 中(Medium) ### 3. 排行榜数据可伪造(⚠️ 经复核为误报 — 无需修复) - **位置**:`cloudfunctions/leaderboard/index.js` - **复核结论**:初审认为"信任客户端上传的 `records.duration`",但实际查看云函数实现后发现—— 该函数**只接收 `period` / `maxRank`**,duration 是在服务端从用户自己的 `doc.records` 重新累加出来的 (第 102–132 行遍历 `records[monthKey]` 求和),并不读取客户端传入的 `duration`。 因此单个用户最多只能伪造自己那条记录,无法影响他人或全局榜单,属于客户端上报类健身应用的固有信任模型,可接受。 - **结论**:**无需修改**。若未来要做反作弊,可再加单条时长上限 / 频率校验,但属于增强项而非缺陷。 ### 4. `admin-dedupe` 云函数无鉴权且具破坏性 ✅ 已修复 - **位置**:`cloudfunctions/admin-dedupe/index.js` - **问题**:该函数以管理员身份运行(wx-server-sdk 默认绕过安全规则),会**批量删除文档**, 但函数体内**没有任何调用者鉴权**。一旦部署,任意小程序用户都能调用它删数据。 - **修复**:在 `exports.main` 入口加 openid 白名单闸门(读环境变量 `ADMIN_OPENIDS`,逗号分隔): ```js const ctx = cloud.getWXContext() const caller = (ctx && ctx.OPENID) || '' const allowlist = (process.env.ADMIN_OPENIDS || '').split(',').map(s => s.trim()).filter(Boolean) if (!caller || allowlist.length === 0 || !allowlist.includes(caller)) { return { err: 'forbidden', msg: '无权限执行该操作' } } ``` - **部署要求**:必须在云函数环境变量中配置 `ADMIN_OPENIDS`(你的微信 openid);未配置时函数**默认拒绝执行(失败安全)**。 - 仍建议用毕即删/禁用该函数,白名单只是兜底闸门。 ### 5. Timer 前台监听只注册不注销(监听器泄漏) ✅ 已修复 - **位置**:`utils/timer.js` → `start()` 注册 `wx.onAppShow`,但 `stop()/pause()/onUnload` 均未 `wx.offAppShow` - **问题**:每次进入训练页新建 Timer 都会注册一个 `onAppShow`,旧的不会被移除。 虽有 `if (this._running && !this._paused)` 守卫不会出错,但多次训练后会累积监听器(内存泄漏、潜在性能问题)。 - **修复**:抽出 `_bindAppShow()` / `_unbindAppShow()`,并在 `stop()` 中调用 `wx.offAppShow(this._onAppShow)` 且重置 `_appShowBound = false`。 页面 `onUnload` 已调用 `this._timer.stop()`,故页面销毁时监听一并注销,无遗漏。 ### 6. `getTodayTarget` 在空计划天数数组时会崩溃 ✅ 已修复 - **位置**:`utils/plan.js` → `getTodayTarget()` ```js return day ? day.target : plan.days[0].target ``` - **问题**:若 `plan.days` 为空(totalDays=0,极端数据损坏场景),`plan.days[0]` 为 `undefined` → 访问 `.target` 抛异常。 - **修复**: ```js if (!plan.days || plan.days.length === 0) return 0 ``` --- ## 🟡 低(Low) ### 7. `darkMode.js` 使用了已废弃 API ✅ 已修复 - `wx.getSystemInfoSync()` 已废弃,真机会打印警告。已改用 `wx.getWindowInfo()`(同样返回 `theme` 字段)。 ### 8. `ui-modal` 的 `customStyle` 属性(⚠️ 误报 — 实际已使用) - 初审称"wxml 中未使用",但复核 `components/ui-modal/ui-modal.wxml` 第 3 行: ```xml ``` 该属性确实绑定到了 body 的 `style`,属正常可用的样式覆写入口,**不是死属性**。无需修改。 - 注:`timer.wxml` 里 `custom-style="margin-right:28rpx;"` 挂在 `ui-btn` 组件上,与 `ui-modal` 无关,亦无误。 ### 9. `onTrainAgain` 用 `setData` 写入下划线前缀字段 ✅ 已修复 - `pages/timer/timer.js`:`this.setData({ ..., _countUpTask: null })`。 - **修复**:改为实例字段赋值 `this._countUpTask = null`(下划线前缀字段不应写进 page data)。 ### 10. `index.js` 的 planProgress 边界为 NaN ✅ 已修复 - `Math.round((Math.min(planDay, plan.totalDays) / plan.totalDays) * 100)`,当 `plan.totalDays` 为 0 时得到 `NaN`。 - **修复**: ```js const planProgress = plan.totalDays > 0 ? Math.round((clampedDay / plan.totalDays) * 100) : 0 ``` --- ## ✅ 亮点(值得肯定) - 云同步的"按 openid 定位 + 合并去重 + 永不重复 add"逻辑处理得很扎实,避免了旧版"每次冷启动新建文档导致排行翻倍"的坑。 - 录音/语音、震动提醒、暗色模式、自定义计划编辑器、完成庆祝动画等交互细节都考虑周全,且有完善的降级(云未部署/无密钥时静默回落本地)。 - `custom-tab-bar` 单独监听了系统主题变化,避免了切 tab 闪白,处理到位。 --- ## 建议优先处理顺序(最终) 1. ~~#1 计划天数统计 bug~~ ✅ 2. ~~#2 深色模式实时刷新~~ ✅ 3. ~~#4 admin 函数鉴权~~ ✅(高优安全项,已修;部署时记得配 `ADMIN_OPENIDS`) 4. ~~#5 / #6 / #7 / #9 / #10~~ ✅ 5. #3、#8 经复核为误报,无需处理。