diff --git a/.workbuddy/launch-readiness-review.md b/.workbuddy/launch-readiness-review.md new file mode 100644 index 0000000..d992e2c --- /dev/null +++ b/.workbuddy/launch-readiness-review.md @@ -0,0 +1,72 @@ +# 平板支撑训练小程序 · 上线前审查报告 + +日期:2026-08-04 | 审查范围:上线就绪(合规 / 包体 / 性能 / 云函数部署) + +## 一、总体结论 + +代码质量高,主包仅 **540KB**(远低于 2MB 上限),网络请求**全部走云开发 `wx.cloud.*`**(免 request 合法域名白名单配置),隐私合规链路已在上架期验证可行。 + +**有 1 个上线阻断项(云函数部署)+ 1 个部署前置(tts 环境变量)必须处理**,其余为建议优化项。未发现功能性 bug、无合规硬伤。 + +--- + +## 二、🔴 阻断项(上线前必须完成) + +### 1. 云函数必须重新部署(最关键) + +> 注意:**git 提交 ≠ 云函数部署**。云函数代码要生效,必须在微信开发者工具里「上传并部署」。 + +| 云函数 | 近期状态 | 动作 | +|---|---|---| +| `leaderboard` | **本次 b52db87 改了 17 行**(含 totalDays / 全榜勋章逻辑) | **必须重新部署**,否则线上排行榜跑旧逻辑 | +| `tts` | 8-03 改(循环训练语音播报) | 需部署 | +| `getOpenid` | 基础服务,排行榜定位 + 云同步依赖 | 确认已部署 | +| `admin-dedupe` | 一次性清理工具(文档注明部署一次后删除) | 可选 | + +部署方式:微信开发者工具 → 右键云函数 → **「上传并部署:云端安装依赖」**(项目无本地 node_modules,需云端 `npm install`)。`tts` 依赖 `tencentcloud-sdk-nodejs` 体积较大,云端安装需等待。 + +### 2. tts 云函数环境变量必须配好(部署前置) + +`cloudfunctions/tts/index.js` 从环境变量读取密钥: + +``` +TTS_SECRET_ID / TTS_SECRET_KEY / TTS_REGION +``` + +若未配置,语音合成直接失败(循环训练 / 语音播报失效)。 +确认路径:云开发控制台 → `tts` 云函数 → 环境变量;并确认腾讯云 **TTS 服务已开通、密钥有效**。 + +--- + +## 三、🟡 建议项(不阻断,建议上线前做) + +1. **清理调试 `console.log`**:`utils/cloud.js` 14 处 + `storage.js` / `voice.js` / `app.js` / `leaderboard` 云函数等。上线应移除或降级(减小包体、避免泄露内部状态、减少日志噪声)。微信审核不强制。 +2. **`project.config.json` 上线开关**:`minified: false` → `true`;`uploadWithSourceMap: true` → `false`。防源码泄露、减小包体(开发者工具上传界面也有独立勾选,会覆盖此项)。 +3. **隐私授权自定义弹窗**:代码无 `wx.onNeedPrivacyAuthorize` 处理。当前能跑(微信自动弹系统默认隐私窗,且后台隐私协议已配、已上架运行一月),但建议加自定义弹窗以更合规、体验更佳。**非阻断**。 +4. **全局错误上报**:无 `wx.onError` / `onUnhandledRejection` / `onPageNotFound`。建议加全局异常捕获 + 轻量上报(云监控 / 自建),便于线上排查崩溃。 +5. **清理残留文件**:`cloudfunctions/shared/`(空死目录,重构遗留;`leaderboard` 实际引用 `./data`,不受影响)、根目录 `preview-buttons.html`、`.claude/` / `.codegraph/`(工具目录)。小程序只打包组成文件,不影响包体,但建议加 `packOptions.ignore` 或删除以免误传。 +6. **确认生产 env**:`utils/cloud.js` 硬编码 `ENV_ID=cloudbase-d1g56kl2q8f4f7d8a`,确认是生产环境(通常单一 env 即可)。 + +--- + +## 四、✅ 已核实合规 / 无风险项 + +- 主包 540KB << 2MB,无需分包 +- 无硬编码 `http://`,全走 `wx.cloud`(免 request 合法域名配置) +- 隐私接口仅 `chooseAvatar`(button `open-type`,后台已声明「用户信息」) +- `app.json` 无需 `requiredPrivateInfos`(`chooseAvatar` 不在此列) +- `sitemap.json` 标准 allow +- `tabBar` custom + `custom-tab-bar` 目录齐全 +- 云函数依赖声明齐全(`wx-server-sdk` / `tencentcloud-sdk-nodejs`) +- 内容安全风险低(昵称 / 头像均为用户自身数据,无实时 UGC 发布链路) + +--- + +## 五、上线 Checklist + +- [ ] 部署 `leaderboard` / `tts` / `getOpenid` 云函数(云端安装依赖) +- [ ] 配好 `tts` 环境变量并验证 TTS 可用 +- [ ] 开发者工具「上传」时勾选 **压缩** + **不上传 sourcemap** +- [ ] 体验版真机回归:训练 → 记录 → 排行榜(含空榜 / 1-2 名虚拟台)→ 设置改头像 +- [ ] 确认 MP 后台类目(运动健身类)与隐私协议有效 +- [ ] 提交审核,准备测试说明(功能无需登录,排行榜可空榜展示) diff --git a/.workbuddy/memory/2026-08-04.md b/.workbuddy/memory/2026-08-04.md index 95f2997..b494c61 100644 --- a/.workbuddy/memory/2026-08-04.md +++ b/.workbuddy/memory/2026-08-04.md @@ -131,3 +131,22 @@ - 用户要求前三名勋章一致大小(此前首名 34rpx 强调、2/3 名 30rpx)。 - 改动:leaderboard.wxss `.podium-badge--first` 34→30rpx;配套 `.podium-name--first` line-height 34→30rpx(保持首名昵称与勋章等高盒对齐)。 - 结果:领奖台前三名勋章统一 30rpx,与全局基准(列表 .rank-badge 30、设置页 .profile-badge 30、记录页 .badge-node-icon 30)完全一致。纯样式无语法风险。 + +## 提交与推送 +- commit `b52db87`(17 文件,+690/-67)包含今天整轮(虚拟领奖台+勋章对齐结构修复+全 app 勋章 30rpx 统一)。 +- 已推 `origin`(github.com/cnliucheng/wx_pbzc) + 新增远程 `soao`(https://git.soao.net/lc/wx_pbzc.git) 并推 `main`。两个远程同步于 b52db87。 +- 提醒:cloudfunctions/leaderboard 若含线上逻辑改动需开发者工具「上传并部署」才生效(本次主要改前端)。 + +## 上线前审查(launch readiness) +- 主包 540KB(<<2MB,无需分包);网络全走 wx.cloud(免 request 域名白名单);无硬编码 http;隐私接口仅 chooseAvatar(后台已配)。**合规面干净**。 +- 🔴 阻断项:①云函数 git 提交≠部署——`leaderboard`(本次 b52db87 改 17 行 totalDays/全榜勋章)、`tts`(8-03 改)需重新部署;②`tts` 环境变量 TTS_SECRET_ID/KEY/REGION 须在云函数控制台配好(否则语音失败)。 +- 🟡 建议项:清理调试 console.log(cloud.js 14 处等)、project.config.json 上线开关(minified→true/uploadWithSourceMap→false)、加 wx.onNeedPrivacyAuthorize 自定义隐私弹窗、加全局错误上报、清理残留(cloudfunctions/shared 空死目录/根 preview-buttons.html/.claude/.codegraph)。 +- 核实:leaderboard 实际 require('./data') 非 '../shared',空 shared 目录无害;`data.js`/`cache.js` 同目录存在,部署含之;无分包;无 wx.onError。 +- 报告:.workbuddy/launch-readiness-review.md。 + +## 执行:上线前建议项清理(用户确认"需要") +- 清调试 console.*:脚本 .workbuddy/clean-console.js 批量删(独立 console 行 + 箭头函数体 `=> console`→`=> {}`),删 34 处;漏 2 处 `if(){console.log();return}` 行内形式手动改;Grep 复扫全项目 *.js 已无 console.* 残留。涉及 cloud.js/storage.js/voice.js/theme.js/app.js/leaderboard.js/timer.js/settings.js/cloudfunctions/leaderboard/index.js,均 node --check 通过。 +- project.config.json:minified false→true、uploadWithSourceMap true→false(上线防源码泄露+减小包体)。 +- 清残留:删 preview-buttons.html + rmdir cloudfunctions/shared(空死目录,重构遗留);.claude/.codegraph 已被 packOptions.ignore 的 `.!*` 忽略,不动。 +- 未做:全局错误上报(wx.onError)、自定义隐私弹窗(用户仅确认做这三个清理项)。 +- 改动未提交(git status 显示 M/D 文件)。clean-console.js 为一性工具,留 .workbuddy 不进包。 diff --git a/app.js b/app.js index 52732af..085b27d 100644 --- a/app.js +++ b/app.js @@ -118,7 +118,6 @@ App({ } // else: no data anywhere — nothing to sync } catch (e) { - console.error('Cloud sync on launch failed:', e) } } }, @@ -145,7 +144,6 @@ App({ const localChangedAt = wx.getStorageSync('_local_changed_at') || 0 const keepLocal = cloudTs > 0 && localChangedAt > cloudTs if (keepLocal) { - console.log('[cloud] restore: keeping local single-value fields (local changed after cloud snapshot)') } else { if (cloudData.settings) { wx.setStorageSync('user_settings', cloudData.settings) @@ -169,7 +167,6 @@ App({ // records so it reflects actual history on this device. storage.recomputeStreak() } catch (e) { - console.error('Cloud restore failed:', e) } }, diff --git a/cloudfunctions/leaderboard/index.js b/cloudfunctions/leaderboard/index.js index ba0dc74..e7728e0 100644 --- a/cloudfunctions/leaderboard/index.js +++ b/cloudfunctions/leaderboard/index.js @@ -117,7 +117,6 @@ const _resolveAvatars = async (urls) => { if (f && f.fileID && f.tempFileURL) map[f.fileID] = f.tempFileURL }) } catch (e) { - console.warn('[leaderboard] getTempFileURL batch failed:', e) } } return (urls || []).map(u => (u && map[u]) ? map[u] : u) @@ -305,12 +304,9 @@ const _persistSnapshot = async (period, result) => { updatedAt: new Date().toISOString() } } - console.log('[leaderboard] persisting snapshot', period, 'rankedLen=', ranked.length) const docRef = db.collection(SNAPSHOT_COLLECTION).doc(period) const setRes = await docRef.set(payload) - console.log('[leaderboard] persist ok', period, JSON.stringify(setRes)) } catch (e) { - console.warn('[leaderboard] persist snapshot failed for', period, e && e.errMsg ? e.errMsg : e) } } @@ -346,7 +342,6 @@ const _rebuildSnapshots = async () => { const latestByOpenid = await _fetchLatestByOpenid(true) for (const p of ['day', 'month', 'year', 'endurance']) { const result = await _buildFromScan(latestByOpenid, p, SNAPSHOT_TOP, null) - console.log('[leaderboard] rebuilt', p, 'rankedLen=', (result && result.ranked) ? result.ranked.length : 'undef') await _persistSnapshot(p, result) } return { ok: true, rebuiltAt: new Date().toISOString() } diff --git a/pages/leaderboard/leaderboard.js b/pages/leaderboard/leaderboard.js index c40c7ba..8fefce9 100644 --- a/pages/leaderboard/leaderboard.js +++ b/pages/leaderboard/leaderboard.js @@ -138,7 +138,6 @@ Page({ // local data silently. No toast here: empty day-leaderboard is // normal (user didn't train today), and a toast would mislead // them into thinking the cloud is broken. - console.warn('[leaderboard] cloud function failed, using local fallback:', err) if (seq !== this._fetchSeq) { if (cb) cb(); return } // 已有缓存秒显时后台刷新失败:保持旧数据,不打断用户;仅无缓存才走本地兜底。 if (cached && cached.rankedList) { diff --git a/pages/records/records.json b/pages/records/records.json index 9355eea..8cbc8fe 100644 --- a/pages/records/records.json +++ b/pages/records/records.json @@ -2,7 +2,8 @@ "usingComponents": { "calendar-heatmap": "/components/calendar-heatmap/calendar-heatmap", "trend-chart": "/components/trend-chart/trend-chart", - "ui-card": "/components/ui-card/ui-card" + "ui-card": "/components/ui-card/ui-card", + "ui-skeleton": "/components/ui-skeleton/ui-skeleton" }, "navigationBarTitleText": "训练记录" } diff --git a/pages/settings/settings.js b/pages/settings/settings.js index 81ff457..d68bbab 100644 --- a/pages/settings/settings.js +++ b/pages/settings/settings.js @@ -187,11 +187,9 @@ Page({ try { permanentUrl = await this._uploadAvatar(avatarUrl) } catch (err) { - console.warn('[avatar] cloud upload failed, trying base64:', err) try { permanentUrl = await util.fileToDataURI(avatarUrl) } catch (err2) { - console.warn('[avatar] base64 convert failed, saving temp URL:', err2) permanentUrl = avatarUrl } } @@ -377,7 +375,6 @@ Page({ await cloud.clearAll() } catch (e) { cloudClearFailed = true - console.warn('[settings] cloud clear failed:', e) } // 4. Reinstate default settings and streak diff --git a/pages/timer/timer.js b/pages/timer/timer.js index e60c6b7..aa5f6de 100644 --- a/pages/timer/timer.js +++ b/pages/timer/timer.js @@ -286,7 +286,7 @@ Page({ if (s.vibrate === false) return const intensity = s.vibrateIntensity || 'heavy' const doVibrate = () => { - const fail = (err) => console.warn('[vibrate] failed:', err.errMsg || err) + const fail = (err) => {} if (intensity === 'light') wx.vibrateShort({ type: 'light', fail }) else if (intensity === 'medium') wx.vibrateShort({ type: 'medium', fail }) else wx.vibrateLong({ fail }) @@ -452,7 +452,7 @@ Page({ // vibrateLong is the most reliable on iOS real devices; the short // types are nicer but may be silent on some devices - user picks. const doVibrate = () => { - const fail = (err) => console.warn('[vibrate] failed:', err.errMsg || err) + const fail = (err) => {} if (intensity === 'light') wx.vibrateShort({ type: 'light', fail }) else if (intensity === 'medium') wx.vibrateShort({ type: 'medium', fail }) else wx.vibrateLong({ fail }) diff --git a/preview-buttons.html b/preview-buttons.html deleted file mode 100644 index 627d900..0000000 --- a/preview-buttons.html +++ /dev/null @@ -1,160 +0,0 @@ - - -
- - -左侧是小程序现状(圆角 48rpx),右侧是本次改后的效果(直角 0)。
同一套按钮、同一套配色,方便你直接对比「好不好看」。
- 小圆角(16rpx)比 10rpx 更圆润一点,更接近卡片的 24rpx 圆角语言,但仍不是全胶囊,保留了清爽感。
- 已应用到首页 5 个按钮(仅首页、不动全局组件)。若还想微调,往上可贴近卡片的 24rpx、往下回到 10rpx,说一声即可。
-