# 汪圈小程序 - 功能缺陷核验与修复设计

> 核验日期：2026-06-20  
> 来源报告：`docs/功能缺陷审查报告.md`  
> 核验基线：当前工作区源码，重点核验 `src` 与 `cloudfunctions`，忽略 `dist` 构建产物  
> 目标：逐项确认报告中仍被标为缺陷/风险的条目是否真实存在，并为需要修改的条目形成可落地设计

## 1. 总体结论

报告中的“已修复确认”条目，当前代码整体能支撑报告结论；本次未发现需要重新打开的已修复项。消息页、发布页、附近页、聊天页的主要 v1 修复点均能在当前实现中找到对应逻辑。

报告中仍被标为遗留、新发现或系统性问题的条目共核验 35 项：

| 结论 | 数量 | 说明 |
|------|------|------|
| 成立 | 24 | 当前代码中确实存在对应行为或缺口 |
| 部分成立 | 5 | 现象存在，但报告描述、严重程度或根因需要修正 |
| 不成立 | 6 | 当前代码已解决或报告判断与实现不符 |

需要特别修正报告优先级：

- `nearbyPets` 云函数不按距离排序：不成立。当前 `cloudfunctions/nearbyPets/index.js` 会计算距离、筛选半径内位置，并按 `_dist` 升序排序。
- `messageList` 变量遮蔽：成立，但只是维护性风险，不应列为 P0。
- 聊天图片 `cloud://` 不显示：部分成立。服务层已经尝试解析 `cloud://` 为临时 URL；真实问题是解析失败后会丢失可重试的 fileId，页面只能显示“图片暂不可用”。
- 当前没有阻断级 P0 缺陷；建议从 P1 的数据一致性、分页、错误回滚开始修。

## 2. 已修复条目复核摘要

| 页面/模块 | 报告已修复项 | 本次复核 |
|-----------|--------------|----------|
| 广场页 | 分页、动态话题、广告随机、详情页、点赞竞态、搜索入口 | 代码中存在 `nextCursor`、`normalizeTopicOptions`、`createAdStartIndex`、`post-detail` 导航、`pendingLikeIds`/`pendingLikeOverrides`、搜索防抖 |
| 附近页 | 位置上报、尊重可见性、喜欢乐观更新、online 过滤 | 代码中存在 `updateLocation`、`nearbyVisible` 分支、`pendingLikeIds` 回滚、前后端 online 过滤 |
| 发布页 | 草稿恢复、字数上限、上传失败提示、关闭前保存草稿提示 | 代码中存在 `getDraft`/`restoreDraft`、`textLength <= 2000`、`UploadPostMediaError`、`handleClose` 草稿弹窗 |
| 消息页 | 已读等待、系统会话区分、搜索 activeUsers | 代码中 `openConversation` 会等待跳转/已读结果，系统消息不进聊天，搜索同时过滤在线汪友 |
| 聊天页 | 历史分页、智能轮询、发送失败不清空、发送状态、可靠滚动、图片/表情 | 代码中存在 `loadOlder`、3s/12s 轮询、成功后清空输入、`sending` 状态、`scrollIntoView`、图片和表情发送 |
| 我的动态 | 首次 onShow 重复加载 | `useRefreshOnShow` 会跳过首次 show |
| 跨页面 | 草稿系统、操作错误提示、`commentCreate` 类型 | 当前代码已接入草稿解析、常见操作 try/catch、`CloudFunctionName` 包含 `commentCreate` |

## 3. 逐项核验清单

| # | 报告条目 | 核验结论 | 是否需要修改 | 设计归属 |
|---|----------|----------|--------------|----------|
| 1 | 广场收藏功能入口缺失 | 成立。`Post`/云函数有收藏字段与能力，但 `PostCard` 无入口，`feed.service.ts` 也没有封装 `setPostFavorite` | 需要 | D1 |
| 2 | 广场 `loadFeed` 依赖 `debouncedKeyword` 导致频繁清空 | 部分成立。每次防抖关键词变化都会重载并清空列表；这是搜索行为的一部分，但会造成闪空体验 | 建议修改 | D4 |
| 3 | `PostCard` 移除更多按钮 | 成立。当前只剩点赞/评论，无举报、分享等入口 | 暂缓，需产品确认更多菜单内容 | D6 |
| 4 | 话题搜索与关键词不能同时生效 | 不成立。前端同时传 `topic` 和 `keyword`，云函数也会按 topic 查询后做关键词匹配 | 不修改，仅可优化提示文案 | D4 |
| 5 | 附近页 `nearbyVisible=null` 时显示默认上海与空列表 | 成立。偏好加载期间页面会渲染默认中心与空状态 | 需要 | D4 |
| 6 | 关闭附近可见不清理旧位置 | 不成立。`locationUpdate` 在 `visible=false` 时会写入 `location:null`、`geo:null`、`visible:false`，不会被附近查询命中 | 不修改 | - |
| 7 | `useUploadMedia` 暴露内部 `setMedia` | 成立。发布页直接用 setter 恢复草稿 | 建议小改 | D4 |
| 8 | 草稿恢复后宠物默认选中逻辑冲突 | 部分成立。当前逻辑会把“草稿无 petId”解释为“不关联宠物”，但无法区分历史缺字段与用户主动不关联 | 需要明确语义后修改 | D4 |
| 9 | 聊天图片 `cloud://` 不显示 | 部分成立。正常路径会解析为临时 URL；解析失败时会丢失 fileId，无法重试 | 需要 | D3 |
| 10 | `loadLatest` 提前返回不清 `loadingInitial` | 不成立。`return` 位于 `try` 内，`finally` 仍会执行 | 不修改 | - |
| 11 | 个人页偏好开关无失败回滚 | 成立。`changePreference` 先改 UI，`await updateProfile` 无 catch | 需要 | D1 |
| 12 | `useSession` 与个人页重复 `ensureLogin` | 成立但影响很低。store 有 inflight 去重，不会造成重复请求 | 暂不修改，可作为结构清理 | D6 |
| 13 | 资料编辑 `type='nickname'` 仅真机生效 | 成立但属于微信平台限制 | 不按缺陷修，可补充提示 | D6 |
| 14 | `avatarUrl` 与 `avatarKey` 共存冲突 | 不成立。当前是图片优先、渐变头像兜底；`getProfile` 已解析 cloud URL | 不修改 | - |
| 15 | 资料编辑页无未保存离开提示 | 成立。关闭按钮直接 `navigateBack` | 需要 | D4 |
| 16 | 宠物数量无上限 | 部分成立。当前确实无限制，但是否是缺陷取决于产品策略 | 需要产品确定上限后修改 | D6 |
| 17 | 宠物照片交互暗示必填 | 不成立。当前无必填标识，校验也只要求名字和品种 | 不修改，可加“可选”文案 | - |
| 18 | 联系人互关后 `isFriend` 未即时更新 | 成立。`toggleFollow` 返回 `mutual`，页面未使用 | 需要 | D1 |
| 19 | 联系人“发现”搜索需手动提交 | 成立。`onInput` 只更新本地关键词，只有 `onConfirm` 请求 | 建议修改 | D1 |
| 20 | 点击用户只能发起聊天，无个人主页 | 成立。当前没有用户主页页面 | 中期功能 | D6 |
| 21 | 我的动态点赞无错误回滚 | 成立。`likePost` 无 try/catch | 需要 | D1 |
| 22 | 我的动态无分页，云函数 `limit(50)` | 成立。`userPosts` 无 cursor，前端一次加载 | 需要 | D2 |
| 23 | 我的动态点赞事件导致重复 setPosts | 成立但影响低。服务成功会广播，页面本地也已设置 | 可随点赞回滚一起收敛 | D1 |
| 24 | 帖子详情评论缺少分页 | 成立。`postDetail` 一次最多 200 条评论 | 需要 | D2 |
| 25 | 帖子详情点赞闭包竞态 | 成立。回滚用当前渲染闭包中的 `post` | 需要 | D1 |
| 26 | 发送评论后作者硬编码“我” | 成立。乐观评论未使用当前用户资料 | 需要 | D1 |
| 27 | 帖子详情加载失败也设 `commentsLoaded=true` | 成立。`load` 无 catch，失败后会显示空评论 | 需要 | D4 |
| 28 | 全局登录态守卫缺失 | 成立。`logout` 是本地标记，非 profile 页面未统一拦截 | 需要设计为轻量会话门禁 | D5 |
| 29 | `StatsRow` 计数可能不同步 | 部分成立。`profileGet` 已实时计算，但 follow/favorite 等操作后没有统一标记 profile 失效 | 需要 | D5 |
| 30 | `cloud-file.ts` 临时 URL 缓存永不过期 | 成立 | 需要 | D5 |
| 31 | `dataBus.ts` `postCache` 永不清除 | 成立 | 需要 | D5 |
| 32 | `messageList` 中 `openid` 回调变量遮蔽 | 成立，但只是可维护性问题 | 顺手修 | D6 |
| 33 | `messageThread` 每次轮询都写已读 | 成立。云函数每次取线程都会 update 会话已读 | 需要 | D3 |
| 34 | `nearbyPets` 不按距离排序 | 不成立。当前会按距离排序 | 不修改 | - |
| 35 | 消息通知/推送未接入 | 成立，但这是新功能，不是当前消息列表逻辑缺陷 | 长期迭代 | D6 |

## 4. 修复设计

### D1. 互动状态一致性与回滚

目标：统一点赞、收藏、关注、偏好开关、评论乐观更新的失败回滚与跨页面同步，优先解决用户可感知的数据错乱。

涉及文件：

- `src/services/feed.service.ts`
- `src/services/social.service.ts`
- `src/pages/my-posts/index.tsx`
- `src/pages/post-detail/index.tsx`
- `src/pages/profile/index.tsx`
- `src/pages/contacts/index.tsx`
- `src/features/feed/components/PostCard/index.tsx`
- `cloudfunctions/postFavorite/index.js`

设计：

1. 收藏能力补齐：
   - 在 `feed.service.ts` 新增 `setPostFavorite(postId, favorited)`，调用 `postFavorite` 云函数。
   - `postFavorite` 云函数补齐缺集合兜底，避免新库第一次收藏失败。
   - `PostCard` 增加收藏按钮与数量展示，使用 `post.favoritedByMe` 和 `counts.favorites`。
   - 广场、我的动态、帖子详情统一实现乐观收藏、失败回滚、成功后写入服务端返回值。
   - 新增收藏同步事件或复用 dataBus 扩展，例如 `emitPostFavorite({ postId, favorited, favorites })`。

2. 我的动态点赞：
   - 引入 `postsRef` 和 `pendingLikeIds`，按广场页模式保存操作前快照。
   - `setPostLike` 失败时恢复 `likedByMe` 与 `counts.likes`。
   - `onPostLike` 监听中先比较目标 post 当前值，相同则返回原数组，减少重复 re-render。

3. 帖子详情点赞：
   - 新增 `postRef` 保存最新 post，`likePost` 以 ref 为准，不用渲染闭包中的 `post`。
   - 加 `pendingLikeIds` 防止同一帖子连续点击造成并发请求。
   - 失败时回滚到请求开始时的 snapshot。

4. 评论乐观作者：
   - `PostDetailPage` 使用 `useSession()` 获取当前用户。
   - 乐观评论 author 使用 `user._id`、`user.nickname`、`user.avatarKey`、`user.avatarUrl`。
   - `commentCreate` 可在后续返回完整 comment；短期先保持 `{ commentId, comments }` 返回结构不变。

5. 联系人关注：
   - `onFollow` 使用 `toggleFollow` 返回的 `following` 与 `mutual` 回写 `isFollowing`、`isFriend`。
   - 失败回滚到完整旧 user snapshot，而不是只反转 `isFollowing`。
   - `social.service.toggleFollow` 成功后调用 `markStale('profile')`，让资料页统计在返回时刷新。

6. 偏好开关：
   - `changePreference` 保存 previous user。
   - 乐观更新后 `try/catch` 调用 `updateProfile`；失败恢复 previous user 并 toast。
   - `updateProfile` 的 `markStale('profile')` 建议移动到云函数成功后，避免失败也标记新鲜/脏状态混乱。

验收：

- 断网或云函数失败时，点赞/收藏/关注/偏好开关都能回滚 UI。
- 互关后联系人页立即显示“汪友”。
- 新评论展示当前用户昵称和头像，不再硬编码“我”。
- 同一帖子在广场、我的动态、详情页的点赞/收藏状态保持一致。

### D2. 列表与评论分页

目标：移除 50/200 条硬上限，保证长列表可继续加载。

涉及文件：

- `cloudfunctions/userPosts/index.js`
- `cloudfunctions/postDetail/index.js`
- `src/services/feed.service.ts`
- `src/pages/my-posts/index.tsx`
- `src/pages/post-detail/index.tsx`
- `src/types/cloud.ts`

设计：

1. `userPosts` 分页：
   - 云函数入参新增 `cursor?: string`、`pageSize?: number`。
   - 按 `createdAt desc` 查询 `pageSize + 1` 条，cursor 使用上一页最后一条 `createdAt`。
   - 返回 `{ list, nextCursor }`。
   - `getUserPosts` 改为返回 `CursorResponse<Post>`，兼容 `authorId`。
   - `MyPostsPage` 增加 `nextCursor`、`loadingMore`、`loadMore`，在滚动到底部加载。

2. 评论分页：
   - `postDetail` 入参新增 `commentCursor?: string`、`commentPageSize?: number`。
   - 评论按 `createdAt asc` 返回，初始取最早一页；下一页使用 `createdAt > commentCursor`。
   - 返回 `{ post, comments, commentsNextCursor }`。
   - `PostDetailPage` 增加 `commentsNextCursor`、`loadingCommentsMore`、底部“加载更多评论”。
   - 为避免刷新帖子详情时重复替换图片，继续保留当前“只更新计数”的策略。

验收：

- 我的动态超过 50 条时可以继续加载。
- 帖子评论超过单页限制时可以继续加载下一页。
- 分页加载失败有 toast，当前列表不被清空。

### D3. 聊天图片可靠展示与已读写入控制

目标：图片临时 URL 解析失败后可重试；聊天轮询不再每次写数据库。

涉及文件：

- `src/services/message.service.ts`
- `src/pages/chat/index.tsx`
- `src/types/domain.ts`
- `cloudfunctions/messageThread/index.js`

设计：

1. 图片消息数据结构：
   - `ChatMessage` 增加可选字段 `fileId?: string`、`assetState?: 'ready' | 'unresolved'`。
   - `resolveThreadAssets` 解析图片时保留原始 `cloud://` 到 `fileId`，`content` 存临时 URL；解析失败时 `content=''` 且 `assetState='unresolved'`。
   - 页面渲染 unresolved 图片时展示占位和“重试”操作。
   - 重试时调用 `resolveCloudFileUrl(fileId)`，成功后只更新该条消息。

2. 已读写入控制：
   - `messageThread` 入参新增 `markRead?: boolean`，默认 `true` 以兼容旧调用。
   - 首次打开会话、用户主动刷新时传 `markRead:true`。
   - 静默轮询 `loadLatest({ silent:true })` 和加载历史传 `markRead:false`。
   - 云函数仅在 `markRead !== false` 且会话存在时执行已读 update。

验收：

- 图片临时 URL 获取失败后，页面不会永久丢失 cloud fileId。
- 静默轮询不再触发会话已读 update。
- 首次进入聊天仍会清除当前用户未读数。

### D4. 页面加载、搜索与离开保护

目标：减少空白闪烁，补齐错误状态与未保存提示。

涉及文件：

- `src/pages/plaza/index.tsx`
- `src/pages/nearby/index.tsx`
- `src/features/nearby/components/NearbySheet/index.tsx`
- `src/pages/post-detail/index.tsx`
- `src/pages/profile-edit/index.tsx`
- `src/hooks/useUploadMedia.ts`
- `src/pages/publish/index.tsx`

设计：

1. 广场搜索：
   - 搜索参数变化时保留旧列表，设置 `searching=true`，新结果回来后替换。
   - 或仅在用户按确认键时清空列表，防抖输入走静默刷新。
   - 搜索结果文案增加当前 topic，例如“在 #遛弯 中搜索...”，解决语义提示问题。

2. 附近偏好加载：
   - 增加 `preferenceLoading = nearbyVisible === null`。
   - 偏好未返回前，地图可显示默认中心，但列表区展示“正在读取附近设置”，不展示“附近暂时没有汪友”。
   - `NearbySheet` 接收 `loading` 或 `state`，区分 loading、disabled、empty 三种状态。

3. 帖子详情加载失败：
   - `load` 增加 catch，设置 `commentsError`。
   - 失败时展示“评论加载失败，点击重试”，不显示“还没有评论”。
   - `finally` 只负责关闭 loading，不吞掉错误状态。

4. 资料编辑未保存提示：
   - 加 `initialRef` 保存首次加载后的表单快照。
   - 关闭按钮走 `handleClose`：dirty 时弹窗确认放弃/继续编辑。
   - 保存成功后更新 dirty 基线或直接返回。

5. 上传媒体 hook：
   - `useUploadMedia` 对外暴露 `replaceMedia(next)`，发布页草稿恢复改用该方法。
   - 保留内部 `setMedia` 私有，后续上传状态不会被外部绕过。

6. 草稿宠物语义：
   - 新草稿保存时增加 `petSelectionExplicit: boolean` 或将 `petId` 扩展为 `string | null`。
   - `petId=null` 表示用户明确选择“不关联宠物”；`petId` 缺失表示历史草稿/未选择，可默认第一只宠物。
   - 对旧草稿做兼容：没有该字段时默认第一只宠物，除非后续产品明确要求保持“不关联”。

验收：

- 广场搜索输入时不出现明显空白闪烁。
- 附近页读取设置期间不显示错误的空列表语义。
- 评论加载失败有可见错误和重试。
- 编辑资料有未保存离开确认。
- 草稿恢复不会误把历史缺字段理解为用户主动不关联。

### D5. 会话门禁、统计失效与缓存淘汰

目标：统一“本地登出/资料未完成”的门禁语义，并控制长期运行内存增长。

涉及文件：

- `src/store/session.store.ts`
- `src/services/auth.service.ts`
- `src/services/social.service.ts`
- `src/services/feed.service.ts`
- `src/services/pet.service.ts`
- `src/store/dataBus.ts`
- `src/services/cloud-file.ts`
- `src/pages/home/index.tsx`

设计：

1. 轻量登录态守卫：
   - 新增 `useSessionGate({ requireCompleted?: boolean })` 或等价 helper。
   - 对发布、消息、联系人、附近等需要真实用户态的页面/操作，若 `isLoggedOut()` 或 `!user.profileCompleted`，引导到“我的”完成登录资料。
   - 广场可保持只读，但点赞、收藏、评论、关注等写操作必须先过 gate。
   - 由于微信 openid 是静默登录，本设计不把“未登录”理解为无 openid，而是“用户本地退出或资料未完成”。

2. 统计失效：
   - follow、favorite、post create/delete、pet save/delete 成功后统一 `markStale('profile')`。
   - 资料页返回时通过现有 `useTabRefresh('profile')` 获取实时 `profileGet` 统计。
   - 如后续新增联系人资源 key，可扩展 `ResourceKey` 为 `contacts`，用于关注列表自身失效。

3. 临时 URL 缓存：
   - `tempUrlCache` 从 `Map<string,string>` 改为 `Map<string,{ url:string; expiresAt:number; lastAccess:number }>`。
   - 默认 TTL 50 分钟，低于微信临时 URL 常见有效期。
   - 最大容量建议 500；超出时按 `lastAccess` 淘汰最旧项。
   - 每次 resolve 前执行轻量 prune。

4. 帖子 hand-off 缓存：
   - `postCache` 增加 TTL 10 分钟、最大容量 100。
   - `getCachedPost` 读取过期项时删除并返回 `undefined`。
   - `cachePost` 写入时执行容量淘汰。

验收：

- 本地退出后，写操作不会继续静默执行。
- 关注/收藏后回到资料页，统计能刷新。
- 长时间浏览大量帖子/图片后，缓存 Map 不会无界增长。

### D6. 中长期产品能力与低风险清理

这些条目成立或部分成立，但不建议混入第一批稳定性修复。

1. 更多菜单：
   - `PostCard` 预留 `onMore`。
   - 菜单项建议先做“分享”“复制内容/链接”“举报”。
   - 举报若上线，需要新增 `postReport` 云函数和审核字段，不只是前端按钮。

2. 用户个人主页：
   - 新增 `/pages/user-profile/index?userId=...`。
   - 复用 `profileGet({ userId })`、`userPosts({ authorId })`，只展示公开资料与公开动态。
   - 联系人列表头像/名称点击进主页，聊天按钮保留为独立操作。

3. 宠物数量上限：
   - 需要先确定产品上限，例如 6 或 10。
   - 前端 `PetShelf` 达上限隐藏/禁用“添加宠物”。
   - 后端 `petSave` 新增创建前 count 校验，防止绕过前端。

4. 消息推送：
   - 作为订阅消息能力单独立项。
   - 需要用户授权订阅模板、发送时机、频率限制、退订处理。

5. 低风险清理：
   - `messageList` 回调参数 `openid` 改名为 `memberOpenid`。
   - 个人页重复 `ensureLogin` 可在重构时简化，但当前无需优先。
   - `type='nickname'` 平台限制可在开发/非真机环境补充提示，不作为功能修复。

## 5. 推荐实施顺序

| 阶段 | 内容 | 原因 |
|------|------|------|
| P1 | D1 互动状态一致性、D2 我的动态分页、D3 聊天图片重试、D4 帖子详情错误状态 | 直接影响用户操作结果与内容可见性 |
| P2 | D2 评论分页、D4 广场/附近加载体验、资料编辑离开保护、D5 统计失效与会话门禁 | 提升长列表、返回刷新和基础流程可靠性 |
| P3 | D5 缓存淘汰、D6 更多菜单/用户主页/宠物上限/推送/清理项 | 风险较低或属于产品能力扩展 |

## 6. 开发校验建议

1. 每个云函数分页改动都保留旧入参兼容，避免旧包调用失败。
2. 点赞、收藏、关注、偏好开关需要分别模拟云函数失败，确认 UI 回滚。
3. 图片消息需要模拟 `getTempFileURL` 失败，确认 fileId 被保留且可重试。
4. 使用 `npm run typecheck` 验证类型改动。
5. 微信开发者工具中重点回归：广场搜索、附近首次进入、聊天轮询、我的动态滚动加载、帖子详情评论加载。
