# 云端数据正确性修复 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 修复排行榜快照的本人识别、云端数据全量清除、TTS 缓存读取及排行榜列表 key。 **Architecture:** 在云函数侧生成仅用于服务端匹配的 SHA-256 `memberKey`;快照保留该字段,但云函数响应会剥离它。将可独立验证的快照匹配和 TTS 缓存读取抽入纯帮助模块,并用 Node 内置测试验证;客户端云端清除按 openid 分页删除全部文档。 **Tech Stack:** 微信小程序原生 JavaScript、CloudBase `wx-server-sdk`、Node.js `node:test`、Node.js `crypto`。 --- ## 文件结构 - 新建 `cloudfunctions/shared/cloud-data.js`:用户指纹、快照本人项查找、响应脱敏、TTS 单文档缓存读取。 - 新建 `utils/cloud-delete.js`:可在小程序运行时和 Node 测试中复用的分页删除控制流。 - 新建 `tests/cloud-data-correctness.test.js`:纯帮助模块的回归测试。 - 修改 `cloudfunctions/leaderboard/index.js`:构建快照指纹,并在快照和实时响应返回前剥离内部字段。 - 修改 `cloudfunctions/tts/index.js`:使用单文档缓存读取帮助函数。 - 修改 `utils/cloud.js`:分页删除当前 openid 的全部文档,失败向调用方传播。 - 修改 `pages/settings/settings.js`:只在云端删除成功后显示成功提示;失败时给出可理解的反馈。 - 修改 `pages/leaderboard/leaderboard.wxml`:循环 key 使用 `rank`。 ### Task 1: 纯数据帮助模块与回归测试 **Files:** - Create: `cloudfunctions/shared/cloud-data.js` - Create: `tests/cloud-data-correctness.test.js` - [ ] **Step 1: 写失败测试** ```js const test = require('node:test') const assert = require('node:assert/strict') const { memberKey, findMyEntry, publicEntry, cachedFileId } = require('../cloudfunctions/shared/cloud-data') test('findMyEntry matches a hashed snapshot member and strips its internal key', () => { const key = memberKey('my-openid') const entry = findMyEntry([{ rank: 2, memberKey: key, duration: 90 }], key) assert.deepEqual(entry, { rank: 2, duration: 90, isMe: true }) }) test('cachedFileId reads a CloudBase doc object rather than an array', () => { assert.equal(cachedFileId({ fileID: 'cloud://env.tts-cache/start.mp3' }), 'cloud://env.tts-cache/start.mp3') }) ``` - [ ] **Step 2: 运行测试确认失败** Run: `node --test tests/cloud-data-correctness.test.js` Expected: FAIL,原因是 `cloudfunctions/shared/cloud-data.js` 尚不存在。 - [ ] **Step 3: 实现最小帮助模块** ```js const crypto = require('crypto') const memberKey = (openid) => crypto.createHash('sha256').update(String(openid || '')).digest('hex') const publicEntry = (entry) => { if (!entry) return null const { memberKey: _memberKey, ...publicData } = entry return publicData } const findMyEntry = (ranked, key) => { const item = (ranked || []).find(entry => entry.memberKey === key) return item ? { ...publicEntry(item), isMe: true } : null } const cachedFileId = (data) => data && typeof data.fileID === 'string' ? data.fileID : null module.exports = { memberKey, publicEntry, findMyEntry, cachedFileId } ``` - [ ] **Step 4: 运行测试确认通过** Run: `node --test tests/cloud-data-correctness.test.js` Expected: PASS,2 个测试均通过。 ### Task 2: 排行榜快照匹配与脱敏 **Files:** - Modify: `cloudfunctions/leaderboard/index.js:1-348` - Test: `tests/cloud-data-correctness.test.js` - [ ] **Step 1: 扩充失败测试** ```js test('publicEntry never returns memberKey', () => { assert.deepEqual(publicEntry({ rank: 1, memberKey: 'secret', name: '甲' }), { rank: 1, name: '甲' }) }) test('publicRanked strips memberKey from every snapshot entry', () => { assert.deepEqual(publicRanked([{ rank: 1, memberKey: 'secret' }]), [{ rank: 1 }]) }) ``` - [ ] **Step 2: 运行测试确认失败** Run: `node --test tests/cloud-data-correctness.test.js` Expected: FAIL,原因是 `publicRanked` 尚未导出。 - [ ] **Step 3: 接入帮助模块** 在帮助模块中增加 `publicRanked = (ranked) => (ranked || []).map(publicEntry)`。在排行榜云函数中 `require('../shared/cloud-data')`;创建榜单项时加入 `memberKey: memberKey(item.openid)`;构建 `myEntry` 与返回 `ranked` 时统一使用 `publicEntry`,快照读取时调用 `findMyEntry(data.ranked, memberKey(myOpenid))`,并对 `ranked` 调用 `publicRanked` 后返回客户端。 - [ ] **Step 4: 运行测试与语法检查** Run: `node --test tests/cloud-data-correctness.test.js && node --check cloudfunctions/leaderboard/index.js` Expected: PASS,且语法检查退出码为 0。 ### Task 3: TTS 单文档缓存读取 **Files:** - Modify: `cloudfunctions/tts/index.js:1-123` - Test: `tests/cloud-data-correctness.test.js` - [ ] **Step 1: 扩充失败测试** ```js test('cachedFileId rejects missing or non-string file IDs', () => { assert.equal(cachedFileId({}), null) assert.equal(cachedFileId({ fileID: 1 }), null) }) ``` - [ ] **Step 2: 运行测试确认失败** Run: `node --test tests/cloud-data-correctness.test.js` Expected: FAIL,直到帮助函数对无效数据返回 `null`。 - [ ] **Step 3: 替换错误的数组读取** 将 `r.data.length` / `r.data[0]` 访问替换为: ```js const cached = cachedFileId(r.data) if (cached) resolvedFileID = cached ``` - [ ] **Step 4: 验证** Run: `node --test tests/cloud-data-correctness.test.js && node --check cloudfunctions/tts/index.js` Expected: PASS,且语法检查退出码为 0。 ### Task 4: 云端全量清除与界面反馈 **Files:** - Modify: `utils/cloud.js:215-250` - Modify: `pages/settings/settings.js:339-382` - Create: `utils/cloud-delete.js` - [ ] **Step 1: 写失败测试** 在 `tests/cloud-data-correctness.test.js` 中为独立的 `deleteAllPages(fetchPage, remove)` 辅助函数添加测试:两页共三条文档时,断言三个 id 都传给 `remove`;任一 `remove` 抛错时,断言 Promise reject。 - [ ] **Step 2: 运行测试确认失败** Run: `node --test tests/cloud-data-correctness.test.js` Expected: FAIL,原因是 `utils/cloud-delete.js` 尚不存在。 - [ ] **Step 3: 实现与接入分页删除** 在 `utils/cloud-delete.js` 增加 `deleteAllPages`:循环调用注入的 `fetchPage(cursor)`,逐条 await `remove(id)`,当本页数量小于页大小时结束。`utils/cloud.js` 使用 `_openid`、按 `_id` 游标查询并调用该帮助函数;查询或删除失败直接抛出。设置页捕获异常后显示“云端数据未清除,请重试”,仅成功路径显示“已清除”。 - [ ] **Step 4: 验证** Run: `node --test tests/cloud-data-correctness.test.js && node --check utils/cloud.js && node --check pages/settings/settings.js` Expected: PASS,且三个语法检查退出码为 0。 ### Task 5: 修复列表 key 与完整验证 **Files:** - Modify: `pages/leaderboard/leaderboard.wxml:81-97` - [ ] **Step 1: 修改循环 key** 将 `wx:key="openid"` 改为 `wx:key="rank"`,因为返回给客户端的每个榜单项都包含稳定且唯一的 `rank`。 - [ ] **Step 2: 运行完整验证** Run: ```bash node --test tests/cloud-data-correctness.test.js for f in app.js config.js utils/*.js pages/*/*.js components/*/*.js custom-tab-bar/index.js cloudfunctions/*/index.js cloudfunctions/shared/*.js; do node --check "$f"; done git diff --check ``` Expected: 所有测试通过、所有语法检查退出码为 0、`git diff --check` 无输出。 - [ ] **Step 3: 人工验收清单** 在微信开发者工具中验证:日/月/年榜快照命中时本人高亮;榜外但位于快照前 500 名时显示底部本人条目;设置页清除后重启不恢复训练数据;清空本地 TTS fileID 缓存后第二次调用命中服务端缓存。