22 KiB
22 KiB
汪圈小程序 — 功能逻辑缺陷审查报告 v2.0
审查日期:2026-06-19 基线版本:v1.0 报告审查后的最新代码 审查范围:全部 11 个页面、10 个 Service、25+ 个云函数、4 个类型文件、3 个 Store/Hook、1 个新增
cloud-file工具模块 审查分支:master
v1 → v2 修复总结
v1 报告共提出 36 条 问题(含跨页面系统性问题 6 条),本次审查确认的修复情况如下:
| 类别 | v1 问题数 | ✅ 已修复 | ⚠️ 部分修复 | ❌ 未修复 | 🆕 新发现 |
|---|---|---|---|---|---|
| 广场页 | 7 | 5 | 0 | 1 | 3 |
| 附近页 | 5 | 4 | 0 | 0 | 2 |
| 发布页 | 4 | 4 | 0 | 0 | 2 |
| 消息页 | 4 | 4 | 0 | 0 | 0 |
| 聊天页 | 6 | 6 | 0 | 0 | 2 |
| 个人资料页 | 2 | 0 | 1 | 0 | 1 |
| 资料编辑页 | 3 | 1 | 0 | 2 | 0 |
| 宠物编辑页 | 2 | 0 | 0 | 2 | 0 |
| 联系人页 | 3 | 0 | 1 | 2 | 0 |
| 我的动态页 | 3 | 2 | 0 | 0 | 2 |
| 帖子详情页 | — | — | — | — | 4 (新增页) |
| 跨页面系统 | 6 | 4 | 0 | 2 | 2 |
| 合计 | 36+ | 26 | 2 | 7 | 18 |
一、广场页 pages/plaza/index.tsx
✅ v1 修复确认
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 1 | Feed 无分页 / 无限滚动 | ✅ 已修复 | 引入 nextCursor、loadNextPage、PageShell lowerThreshold={160}、loadingMore 状态,实现了完整的无限滚动。 |
| 2 | 话题列表硬编码 | ✅ 已修复 | 引入 topicOptions 状态 + normalizeTopicOptions(),从云端 result.topics 动态合并。 |
| 3 | 广告卡片位置固定 | ✅ 已修复 | 引入 createAdStartIndex() 随机起点 + AD_INTERVAL = 6 间隔,每次加载重新随机化。 |
| 5 | 没有帖子详情页 | ✅ 已修复 | 新增 pages/post-detail/index 页面,PostCard 支持 onOpen 导航。 |
| 6 | 点赞乐观更新竞态 | ✅ 已修复 | 引入 feedRequestSeq + pendingLikeIds / pendingLikeOverrides 防重复机制,失败时回滚到 previousPost 快照。 |
| 7 | 搜索栏无功能 | ✅ 已修复 | 绑定 onInput + 300ms 防抖 debouncedKeyword,搜索时展示结果计数 + 清除按钮。 |
❌ v1 遗留
| v1 # | 问题 | 说明 |
|---|---|---|
| 4 | 收藏功能入口缺失 | Post.favoritedByMe 字段和 feed.service.setPostFavorite() 仍然存在,PostCard 仍然没有收藏操作的 UI 入口。跨页面系统性问题 #2 未解决。 |
🆕 v2 新发现
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|---|---|---|---|
| 1 | loadFeed 依赖数组包含 debouncedKeyword 导致频繁重置 |
🟡 中 | useEffect (L156-158) |
loadFeed 的 useCallback 依赖了 debouncedKeyword。防抖 300ms 后 debouncedKeyword 变化 → loadFeed 重建 → useEffect 触发 → 清空列表重新加载。用户每输入一个字符(停顿 300ms 后),整个列表会被清空重载一次,体验上表现为列表频繁闪烁空白再重新出现。应将 loadFeed 拆分为"参数变化时的首次加载"和"追加加载",或使用 ref 追踪 keyword 变化而非作为 useCallback 依赖。 |
| 2 | PostCard 移除了 more 按钮但无替代操作 |
🟡 中 | PostCard (L44-66) |
v1 中 PostCard 有一个 more 图标按钮(三点点),v2 中完全移除了。用户无法对帖子执行更多操作(举报、分享、复制链接等),丢失了内容管理入口。 |
| 3 | 话题搜索结果与关键词搜索混合逻辑不一致 | 🟡 中 | 搜索逻辑 (L278-306) | 关键词搜索通过 debouncedKeyword 传给 getFeedList,服务端过滤。但话题筛选通过 topic 参数传给同一个 getFeedList。两者不能同时生效——用户选择了某个话题后又输入搜索词,行为不明确。UI 上也没有层级提示告诉用户"在话题内搜索"还是"全局搜索"。 |
二、附近页 pages/nearby/index.tsx
✅ v1 修复确认
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 1 | 没有调用 updateLocation 上报自身位置 |
✅ 已修复 | 获取位置后调用 updateLocation(loc, true),relocate 时也会更新,nearbyVisible === false 时传 null, false。 |
| 2 | 没有尊重 preferences.nearbyVisible 偏好 |
✅ 已修复 | 先 getProfile() 读取偏好,nearbyVisible === false 时不请求位置、清空宠物列表、不上报位置。 |
| 3 | likePet 没有乐观更新和错误回滚 |
✅ 已修复 | 引入 pendingLikeIds、乐观设置 likedByMe: true,失败时回滚到 previousLiked。 |
| 4 | filter: 'online' 后端过滤能力不确定 |
✅ 已修复 | nearby.service.ts 新增 filterNearbyPets() 前端兜底过滤。nearbyPets 云函数也已支持 online filter。 |
🆕 v2 新发现
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|---|---|---|---|
| 1 | nearbyVisible 为 null 时地图显示默认中心上海但无数据 |
🟡 中 | 初始化流程 (L32, L57-83) | nearbyVisible 初始值为 null(等待 getProfile 返回),此时地图渲染 DEFAULT_CENTER(上海),center 为 null 所以 loadPets 不触发,宠物列表为空。用户看到一个空地图和空列表,直到 getProfile 完成(可能 1-2 秒)。应显示加载指示器或骨架屏。 |
| 2 | 没有清理自身旧位置(登出 / 关闭可见性) | 🟢 低 | nearbyVisible === false 分支 (L61-67) |
调用 updateLocation(null, false) 但没有确认云函数是否正确处理 null location(从数据库中移除旧坐标)。如果云函数只是不写入,用户的旧位置数据会一直留在数据库中。 |
三、发布页 pages/publish/index.tsx
✅ v1 修复确认
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 1 | 没有恢复草稿的逻辑 | ✅ 已修复 | useEffect 调用 getDraft(),通过 hasDraftContent() 判断后 restoreDraft() 回填所有表单字段。 |
| 2 | 没有内容字数上限预警 | ✅ 已修复 | canSubmit 检查 textLength <= 2000,超限时 submit() 弹 toast: '正文最多 2000 字'。 |
| 3 | 图片上传失败静默跳过 | ✅ 已修复 | 新增 UploadPostMediaError 自定义错误类,showUploadFailure() 弹窗明确告知第几张图片上传失败。 |
| 4 | 返回没有未保存提示 | ✅ 已修复 | handleClose() 检查 hasContent,弹窗询问"保存草稿?" 或 "不保存"离开。 |
🆕 v2 新发现
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|---|---|---|---|
| 1 | useUploadMedia 的 setMedia 暴露了内部 setter |
🟢 低 | useUploadMedia 解构 (L53) |
const { media, setMedia, addImages, removeMedia } = useUploadMedia([]) — 直接暴露 setMedia 并在 restoreDraft 中使用。绕过了 useUploadMedia 的内部管理逻辑,如果该 hook 未来添加了额外状态(如上传状态),直接 setMedia 不会触发这些逻辑。应通过 hook 暴露一个 replaceMedia 方法。 |
| 2 | 草稿恢复成功后宠物默认选中逻辑冲突 | 🟡 中 | useEffect (L81-87) |
getPetList 的 useEffect 有 setSelectedPetId(prev => prev || (draftRestoredRef.current ? '' : list[0]._id))。当草稿恢复成功时 draftRestoredRef.current = true,但如果草稿没有设置 petId(draft.petId 为空),宠物不会被默认选中——这可能不是用户期望的(用户之前可能选中了一个宠物但草稿没保存 petId)。 |
四、消息页 pages/messages/index.tsx
✅ v1 修复确认
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 1 | markConversationRead 是 fire-and-forget |
✅ 已修复 | openConversation 中正确 await markConversationRead,失败时回滚 unreadCount。navigateTo 失败时也回滚。 |
| 2 | 没有区分 single / system 会话交互 | ✅ 已修复 | item.type === 'system' 时直接调用 markConversationRead 标记已读并 toast "系统消息已读",不再跳转聊天页。 |
| 3 | 没有消息通知 / 推送机制 | ⚠️ 部分修复 | — |
| 4 | 搜索不会过滤 activeUsers | ✅ 已修复 | 搜索时也过滤 activeUsers → visibleActiveUsers,有匹配时显示"在线汪友"分区。 |
v2 无新增问题
五、聊天页 pages/chat/index.tsx
✅ v1 修复确认
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 1 | 消息列表没有分页加载历史 | ✅ 已修复 | 引入 loadOlder、nextCursor、upperThreshold={48} 触发加载。加载后滚动到锚点消息位置。 |
| 2 | 轮询频率固定 5 秒 | ✅ 已修复 | 引入 ACTIVE_POLL_MS = 3000 / IDLE_POLL_MS = 12000 / ACTIVE_POLL_WINDOW_MS = 60000 三档智能调节。 |
| 3 | 发送失败时输入框闪空 | ✅ 已修复 | send() 中不再先 setDraft(''),而是在 sendMessage 成功后才 setDraft('')。sendImageMessage 也类似处理。 |
| 4 | 没有消息发送状态指示 | ✅ 已修复 | sending 状态驱动 UI:按钮显示"发送中"、输入框 disabled、工具栏按钮 disabled。 |
| 5 | scrollTop 递增 hack 不可靠 |
✅ 已修复 | 改用 scrollIntoView + 动态 bottomAnchorId(scrollSeq 递增)实现可靠滚动到底部。 |
| 6 | 不支持图片 / 表情消息 | ✅ 已修复 | 新增图片发送 (sendImages)、表情面板 (EMOJI_OPTIONS)、图片消息展示 + 预览 (previewImage)。 |
🆕 v2 新发现
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|---|---|---|---|
| 1 | 图片消息只显示非 cloud:// URL 的图片 |
🔴 高 | 图片渲染逻辑 (L442) | m.content && !m.content.startsWith('cloud://') — 当消息的 content 是 cloud:// 文件 ID(已上传到云端但 resolveCloudFileUrl 没有成功转换时),会显示"图片暂不可用"文本气泡。messageThread 云函数和 resolveThreadAssets 应该已经解析了这些 URL,但如果解析失败,用户会看到"图片暂不可用",没有重试机制。更严重的是,任何以 cloud:// 开头的内容都被认为是"不可用图片"而非普通文本,如果某些消息的 type 被错误标记为 image 但 content 是 cloud ID,将无法展示。 |
| 2 | loadLatest 提前返回时可能不清除 loadingInitial |
🟢 低 | loadLatest (L183-220) |
如果 requestId !== latestRequestSeq.current 导致提前 return,loadingInitial 不会被清除——但 finally 块仍会执行(return 在 try 块内),所以实际不会出现此问题。仅限极端竞态场景。 |
六、个人资料页 pages/profile/index.tsx
⚠️ v1 部分修复
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 1 | 偏好 changePreference 无错误回滚 |
⚠️ 部分修复 | updateProfile 内部调用 markStale('profile'),数据会在下次 useRefreshOnShow 时刷新。但 changePreference 本身仍然没有 try-catch——如果 updateProfile 抛出异常,UI 状态已改但数据未持久化,直到下次刷新才恢复。用户仍然会看到一个短暂的"假成功"状态。 |
🆕 v2 新发现
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|---|---|---|---|
| 1 | useSession 初始化时 ensureLogin 与 ProfilePage 自身的 ensureLogin 重复调用 |
🟢 低 | useEffect (L40-47) |
useSession hook 内部在 mount 时已经调用了 ensureLogin(),ProfilePage 在 useEffect 中又调用了一次。虽然有 inflight 去重机制不会产生两次网络请求,但代码结构上造成了理解困惑。可考虑依赖 useSession 的 user 即可。 |
七、资料编辑页 pages/profile-edit/index.tsx
❌ v1 遗留
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 1 | type='nickname' 仅真机生效 |
❌ 未修复 | 仍然是 type='nickname',没有非真机环境的校验提示。这是微信平台限制,可接受。 |
| 2 | avatarUrl 与 avatarKey 共存冲突 |
❌ 未修复 | 新增了 avatarDisplayUrl 变量(L25: avatarUrl && !avatarUrl.startsWith('cloud://') ? avatarUrl : ''),解决了 cloud:// ID 在 <Image> 中无法显示的问题。但 avatarKey 在头像已上传的情况下仍然是一个无用的遗留值,Avatar 组件和 Image 组件的优先级关系仍不明确。 |
| 3 | 没有未保存离开提示 | ❌ 未修复 | 关闭按钮仍然是直接 navigateBack(),没有检测是否有未保存修改。 |
v2 无新增问题
八、宠物编辑页 pages/pet-edit/index.tsx
❌ v1 遗留
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 1 | 没有宠物数量上限限制 | ❌ 未修复 | PetShelf 组件仍然无条件显示"添加宠物"按钮。后端也没有数量限制。 |
| 2 | 照片隐式非必填但交互暗示必填 | ❌ 未修复 | 提交验证只检查 name 和 breed,无照片也能保存。新增了 photoDisplayUrl 逻辑(L81),解决了 cloud:// ID 显示问题。 |
v2 无新增问题
九、联系人页 pages/contacts/index.tsx
⚠️ v1 部分修复
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 3 | 互关后 isFriend 未即时更新 |
⚠️ 部分修复 | v1 报告指出 onFollow 只更新 isFollowing 不更新 isFriend。当前代码仍然只更新 isFollowing,isFriend 未同步。需调用 toggleFollow 后根据返回的 mutual 更新 isFriend。 |
❌ v1 遗留
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 1 | "发现"搜索需手动提交 | ❌ 未修复 | 仍然只有 onConfirm 触发搜索,没有实时搜索或防抖输入触发。 |
| 2 | 点击用户只能发起聊天 | ❌ 未修复 | 仍然没有用户个人主页入口。 |
v2 无新增问题
十、我的动态页 pages/my-posts/index.tsx
✅ v1 修复确认
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 2 | useDidShow 首次触发导致重复加载 |
✅ 已修复 | 改用 useRefreshOnShow('myPosts', () => load(true)),首次 onShow 被跳过。 |
❌ v1 遗留
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 3 | likePost 没有错误回滚 |
❌ 未修复 | likePost 仍然没有 try-catch 和回滚逻辑——await setPostLike(postId, target.likedByMe) 失败时不会恢复之前的点赞状态。 |
🆕 v2 新发现
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|---|---|---|---|
| 1 | 仍然没有分页加载 | 🟡 中 | load() (L21-31) |
userPosts 云函数硬编码 limit(50),前端一次性加载全部 50 条。如果用户帖子超过 50 条,超出部分无法查看。应添加 cursor / pageSize 参数到 userPosts 云函数和前端。 |
| 2 | likePost 通过 onPostLike 事件同步但有重复调用 |
🟡 中 | likePost (L49-63) |
likePost 做了乐观更新 + 调用 setPostLike(后者通过 emitPostLike 广播)。同时还有一个 useEffect 订阅 onPostLike 事件。当用户在"我的动态"页点赞时,setPostLike → emitPostLike → 本页的 onPostLike 监听器也会触发 setPosts,导致同一条帖子的点赞状态被设置了两次(第一次是 likePost 直接设置,第二次是事件监听器设置)。虽然结果相同,但多了一次不必要的 re-render。 |
十一、帖子详情页 pages/post-detail/index.tsx 🆕 新增页面
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|---|---|---|---|
| 1 | 评论缺少分页加载 | 🟡 中 | load() (L30-59) |
postDetail 云函数一次返回最多 200 条评论(safeGet('comments', { postId }, 200)),前端没有分页。如果帖子评论超过 200 条,超出部分无法查看。 |
| 2 | likePost 的乐观更新使用闭包中的 post 而非 ref |
🟡 中 | likePost (L66-85) |
const previousPost = post(L82 回滚时使用)来自当前渲染闭包中的 post state。如果用户快速双击点赞(第一次 setPost(next) 还没 re-render,第二次进入时 post 仍是旧值),乐观更新的"之前"状态会不准确。应使用 ref 追踪最新的 post。 |
| 3 | 发送评论后新评论的 author 硬编码为"我" |
🟡 中 | send() (L87-116) |
新评论被乐观添加到列表,但 author: { id: 'me', name: '我', avatarKey: 'gradient-avatar-1' } 是硬编码的,没有使用当前用户的真实昵称和头像。用户看到自己评论显示为"我"而非自己的昵称。应从 useSession() 获取当前用户信息。 |
| 4 | load 函数的 finally 中总是设置 commentsLoaded = true |
🟢 低 | load() (L58) |
即使 getPostDetail 抛出异常,commentsLoaded 也会被设为 true,导致"评论加载中…"变成"还没有评论"。但此时用户不知道加载失败了(没有错误提示)。应在 catch 中显示错误状态。 |
十二、跨页面系统性问题
✅ v1 修复确认
| v1 # | 问题 | 状态 | 说明 |
|---|---|---|---|
| 3 | 草稿系统断裂 | ✅ 已修复 | 发布页 mount 时调用 getDraft(),resolveDraftMedia() 解析草稿中的 cloud file URL,表单完整回填。 |
| 5 | 没有全局错误边界 | ✅ 已修复 | 各页面和 service 层普遍添加了 try-catch 和错误回滚。虽然不是 React ErrorBoundary,但每个操作都有用户可感知的错误提示。 |
| 6 | CloudFunctionName 缺少 commentCreate |
✅ 已修复 | cloud.ts 中已包含 commentCreate。 |
❌ v1 遗留
| v1 # | 问题 | 说明 |
|---|---|---|
| 1 | 全应用没有全局登录态守卫 | session.store.ts 实现了共享 session + ensureLogin,app.tsx 在 launch 时预热登录。但广场、附近、消息、发布等页面仍然没有检查登录态——未登录用户能看到空数据但不会跳转到登录页。 |
| 4 | StatsRow 数据可能与子页面不同步 |
未修复。profile.getProfile() 和 contacts.getFollowList() 返回的计数仍然可能因缓存延迟不一致。 |
🆕 v2 新发现
| # | 问题 | 影响范围 | 严重程度 | 说明 |
|---|---|---|---|---|
| 1 | cloud-file.ts 的 tempUrlCache 永不过期 |
全应用 | 🟡 中 | resolveCloudFileUrls 将 cloud file ID 到临时 URL 的映射缓存在内存 Map 中,但永不清除。长时间使用小程序后,这个 Map 会持续增长(每次加载帖子/头像/评论都添加条目)。虽然单个 URL 映射很小,但如果用户大量浏览内容(数百帖子,数千评论),累积效果不应忽视。应添加 LRU 或 TTL 淘汰策略。 |
| 2 | dataBus.ts 的 postCache 永不清除 |
跨页面 | 🟢 低 | cachePost 将帖子数据存入全局 Map,getCachedPost 只在 PostDetailPage 中调用。这个 Map 永远不会被清除——如果用户浏览了很多帖子,所有数据都会留在内存中。应在一定时间后或 Map 大小超过阈值时清除。 |
| 3 | messageList 云函数中 peerOpenids 过滤的变量遮蔽 |
消息页 | 🟡 中 | messageList/index.js L88: (c.memberOpenids || []).find(openid => openid !== OPENID) — find 的回调参数名 openid 遮蔽了外层的 OPENID 常量。逻辑上结果正确(参数 openid !== OPENID 等价于"找到不是我的那个成员"),但变量命名遮蔽在未来维护时容易引入真实 bug。应重命名回调参数(如 member)。 |
| 4 | messageThread 云函数每次轮询都执行一次已读标记 update |
聊天页 → 消息页 | 🟢 低 | messageThread/index.js L97-99: 每次请求消息列表都会 update conversation 清除未读。聊天页的轮询(3-12 秒一次)会频繁触发不必要的数据库写入。如果两个用户同时在看同一会话,一方的轮询会清除另一方的未读状态——虽然 _.pull(OPENID) 只清除当前用户,但频繁 update 仍是不必要的开销。应区分首次加载(标记已读)和轮询刷新(不标记已读)。 |
| 5 | nearbyPets 云函数不按距离排序 |
附近页 | 🔴 高 | nearbyPets/index.js L86: db.collection('pets').where(query).limit(50).get() — 没有 .orderBy 按距离排序。返回的宠物列表按 MongoDB 默认的写入顺序排列,用户看到的附近宠物不是按距离远近排列的。对于"附近"功能来说,距离排序是核心体验。应使用 geoNear 或在应用层按 distanceText 排序。 |
十三、修复优先级建议
P0 — 必须立即修复
nearbyPets云函数不按距离排序 — "附近"功能的核心体验缺失,用户期望看到最近的宠物排在最前面- 聊天页图片消息
cloud://不显示 — 用户发送/接收的图片消息可能显示"图片暂不可用" messageList云函数peerOpenids变量遮蔽 — 代码可维护性风险,未来修改时容易引入真实 bug
P1 — 短期内修复
- 联系人页
onFollow互关后未即时更新isFriend - 联系人页"发现"搜索无实时搜索
- 我的动态页
likePost错误回滚 - 我的动态页无分页 +
userPosts云函数limit(50)硬编码 - 广场页搜索输入导致列表频繁重载(
debouncedKeyword→loadFeed重建 → 清空列表) - 帖子详情页评论
author硬编码"我" - 收藏功能 UI 入口补全(
PostCard添加收藏按钮) changePreference添加try-catch- 帖子详情页
likePost闭包问题 → ref
P2 — 中期迭代
- 资料编辑页 / 宠物编辑页未保存离开提示
- 帖子详情页评论分页
PostCard恢复"更多操作"按钮- 宠物数量上限
- 全局登录态守卫(非 TabBar 页面检查登录)
- 联系人页添加用户个人主页入口
messageThread轮询时避免不必要的已读标记 update- 附近页
nearbyVisible加载状态提示
P3 — 长期优化
cloud-file.tstempUrlCache 添加 TTL / LRU 淘汰dataBus.tspostCache 添加大小限制 / TTL- 消息通知 / 推送接入
- 用户个人主页(完整帖子列表 + 宠物列表展示)
- 广场页话题与关键词搜索混合逻辑优化
本报告基于源码静态审查生成,所有问题均指向代码层面的逻辑缺陷,不涉及 UI/UX 视觉设计层面的评审。