修复云端数据一致性问题

This commit is contained in:
2026-07-29 11:03:44 +08:00
parent a3dd080b6f
commit 17727ab1dd
12 changed files with 389 additions and 41 deletions
@@ -0,0 +1,200 @@
# 云端数据正确性修复 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: PASS2 个测试均通过。
### 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 缓存后第二次调用命中服务端缓存。