Files
patbond-doc/docs/development/iterations/iteration-3/25-detail-interactions-report.md
T
lixi f5457c2f4c
CI / docs-build (push) Successful in 2m2s
docs: M3 第三波收口——报告 21~27 入档挂导航
- 21~26 Flutter 社区接入五单 + 字典 v3 白名单(flutter 286→502、api 325→334)
- 27 收口总表:社区 demo 三页消亡、M3 四条验收标准逐条取证、
  乐观更新与媒体链路端到端、实现期修正记录
- device-verification.md 的 M3 四项真机步骤已由各单收口补全

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 14:48:33 +08:00

185 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 25 M3 第三波:帖子详情页替换 + 互动接线(T3-15/T3-16
**执行日期**2026-09-09
**工单**:T3-15 帖子详情页整页替换(demo 数据层退役)+ T3-16 互动接线(ToggleSync UI 层),同域合并交付
**依赖**21 号(T3-12 数据层,ToggleSync/CommunityController 就位)、24 号(T3-14 组件与遗留交接)、05 号 UI 规范 §2.2/§3.4/§3.5/§4、22 号事件白名单 v3、17 号后端评论/互动语义
**提交**patbond-flutter dev `92524da`T3-16 基建)+ `f873acf`(T3-15/16 页面与接线),基线 `8aac8c5`,已推送 origin/dev
---
## 0. 概要
`post_detail_page.dart` 整页重写为真实数据:四态首屏、媒体全量渲染(真九宫格 +
全屏大图)、作者卡关注双态、评论区(游标列表 / 输入条创建 / 仅本人可删)全部
落地;点赞/收藏经共享 ToggleSync 接入 Feed 卡片与详情页(同一 controller 实例,
互动状态跨页一致),按 05 号 §4 三层视觉抑制实现;互动域 8 事件挂接完成。
Feed 整卡点按导航详情接通,T3-14 的占位 SnackBar 与禁用态移除。**未动
patbond-api**create 页 demo 留给 T3-17。
**质量门禁**`flutter test` 458/458 全绿(基线 421+37;另 2 个 env 门控
compose 冒烟默认跳过)、`flutter analyze` 0 问题、`dart format
--set-exit-if-changed` 无 diff、compose 六容器真链路实测通过(§5,含断网
点赞回滚)。
## 1. 详情页四态与结构(T3-15)
| 态 | 渲染 | widget 测试 |
|----|------|------------|
| loading(无内存副本) | 居中转圈;评论区独立骨架 2 个(32 圆 + 圆角 16 块高 7205 §3.7 | ✓ |
| 内存副本先渲染 | 进入即展示 controller 缓存内容,`getPost` 后台拉新静默替换(Feed 卡片互动字段一并回写) | ✓ |
| error | `InlineErrorBanner`(pets 同款话术映射)+ 「重试」;有副本时后台刷新失败不打断阅读 | ✓ |
| **40403 不存在态** | SnackBar「帖子不存在或已被删除」→ **返回 Feed 并触发整体刷新**(失效帖剔除);详情 / 评论 / 评论创建三条路径均可触发,单次守卫防重复 pop | ✓ |
| ready | 媒体区 → 作者卡 → 正文卡 → 操作行 → 评论区,底部固定输入条 | ✓ |
结构落点(05 §2.2 对照):
- **媒体区(本单裁定:真九宫格,D11 轮播方案弃用)**:单图原比例通栏、高度
钳制 [宽×0.75, 宽×1.33]widthPx/heightPx 缺失回落 4:3);多图走
`PostMediaGrid` 全量形态(24 号预留的列数规则 2/4→2 列、3/5–9→3 列与
超 9 折叠「+N」直接生效)。点格进全屏大图:黑底 + `InteractiveViewer`
(03 号拍板 E 选①内置方案,零依赖)+ 横滑翻页 + 双击定点 2.5x 缩放 +
右上「n/N」ink 胶囊(13.50:1)与关闭钮。**偏差**:05 §2.2 的「下滑关闭」
与 InteractiveViewer 平移手势冲突,本版未做(关闭钮 + 返回手势可退出),
留待 photo_view 复评(03 号 E 的升级条件「体验不达标」)。
- **作者卡**`PetAvatar` md44 + 名字/相对时间;关注双态钮见 §3。
- **正文卡**:标题 titleMedium + 求助帖 `TagPill(accent)` + 正文 14/1.6 全文 +
「发布于 …」12 `inkSoft`。契约 Post 无话题字段,TopicChip 不涉本单。
- **操作行**:与 Feed 卡片同一套组件卡外裸排(demo 的 FilledButton.tonalIcon
弃用);评论锚点钮点按聚焦底部输入框(唤起键盘直接开写)。
- **输入条**surface 底 + 顶部 border 1px 分隔线(demo 缺失,已补)+ isDense
输入框 + filled 发送钮(空文本禁用;发送中 18 转圈锁尺寸)。
- **AppBar 分享**:占位 SnackBar「分享功能即将上线」(无契约端点)。
## 2. 评论区(T3-15
- **游标列表**`(created_at DESC, id DESC)` 服务端序原样渲染,触底(余量
400)携 nextCursor 补页,失败态只走显式重试(Feed 同款守卫);空态
「还没有评论,来抢沙发」(装饰图标 muted 合法、文案 inkSoftDEBT-2 零新增)。
- **创建**:仓库层 Idempotency-Key 每次提交换新键(21 号已测线上语义);成功
插入列表头 + `adjustCommentCount(+1)` 同源写入(详情副本与 Feed 卡片
commentCount 一并更新)+ 清空输入收起键盘;失败保留输入 + 按类型话术
SnackBar(40000 →「评论内容不合规」等),撞 40403 走不存在态流程。
- **仅本人可删(UI 呈现)**`currentUserId`app.dart 注入 sessionManager.userId
与评论 author.userId 相等才渲染「删除」入口——权限判定只做 UI 自见性,
服务端 40301/40404 仍是裁决者(17 号 §2.3)。删除经确认弹窗 → 软删成功
剔除 + 计数 -1;40404(已在别处删)本地同步剔除;40301 提示无权限。
- **CommentTile 升共享组件**`lib/core/widgets/comment_tile.dart`05 §3.4):
PetAvatar sm32 + 气泡(surface/border 1px/圆角 16/padding 12);@ 回复以
「回复 @昵称:」前缀呈现(响应 replyToUser,含降级「宠友」占位);删除
in-flight 转圈锁定。**取舍**:评论点赞(§3.4 底行右端)无契约端点不渲染;
@ 回复的**发起** UI 与长按操作 sheet(回复/复制/举报)留待后续工单
(数据层 replyToUserId 已支持,isReply 埋点属性预留)。
## 3. 互动视觉实现(T3-16,05 §3.5/§4 三层抑制对照)
| 层 | 规范 | 实现落点 |
|----|------|---------|
| 即时反馈 | 点按即刻翻转 + 激活动画 | ToggleSync 乐观写入同帧 notifyLikeButton 升 Stateful——点按驱动的激活播 240ms 弹性缩放(1→1.25→1+ 120ms 图标淡入,取消仅 120ms 颜色渐出无缩放 |
| 连点合并 | 只发最终态 | 由数据层单飞合并承担(在途链只并入 pendingTarget、完成后按最终意图至多补发一次,连点至多两在途);UI 不再叠加 600ms 计时防抖——ToggleSync 已保证「合并后只发最终态」的语义,双状态机会打架(D8 的跨角色确认以 21 号定稿为准) |
| 回滚静默化 | 零动画 + 成对恢复 + SnackBar | 非点按驱动的状态变化(回滚/对账)直接跳变;**计数与展示态成对更新**(不出现「心已灭计数未减」中间帧);激活动画未播完等播完再跳(§4.3a);`toggleError` 一次性消费出 SnackBar「操作失败,请重试」(Feed 页与详情页共用消费口,先消费者清空,同帧恰一条) |
| 对账不打扰 | 静默替换计数 | 服务端权威计数与乐观值不同(他人并发)时数字直接替换、无动画(LikeButton 对「状态不变的计数变化」不播任何过渡) |
关注钮同策略(§4.5):乐观翻转、失败直接跳回 + SnackBar;取关先确认
「不再关注 TA?」;本人帖不渲染(自关注 42204 不给触发面);关注状态经
`getFollowStats.followedByMe` 拉取,拉取失败不渲染钮(不阻塞阅读)。
系统「减弱动态效果」开启时全部动画降级瞬变(LikeButton 与 FeedSkeleton 同口径)。
**跨页一致**:Feed 卡片与详情页共享同一 CommunityController/ToggleSync 实例,
互动写入经 `_writeInteraction` 同帧更新详情副本与 Feed 卡片(widget 测试从
UI 侧断言「详情点赞、卡片同帧 +1」)。
## 4. 埋点挂接清单(8 事件 + 口径)
新增 `community_interaction_analytics.dart`22 号白名单键集逐一对齐,
编译期锁死):
| # | 事件 | 触发点 | props | 挂接位置 |
|---|------|--------|-------|---------|
| 1 | `post_liked` | 点赞**成功响应后** | sourcefeed / post_detail | CommunityController send 闭包(触点在 toggle 调用处归因) |
| 2 | `post_unliked` | 取消点赞成功响应后 | source | 同上 |
| 3 | `post_favorited` | 收藏成功响应后 | source | 同上 |
| 4 | `post_unfavorited` | 取消收藏成功响应后 | source | 同上 |
| 5 | `comment_create_succeeded` | 评论创建成功响应后 | durationMs、isReply、textLengthBucket | 详情页提交回调 |
| 6 | `comment_create_failed` | 评论创建失败 | failureReason、errorCode、httpStatus、attemptSeq | 详情页提交回调 |
| 7 | `user_followed` | 关注成功响应后 | source=post_detail | 详情页关注钮 |
| 8 | `user_unfollowed` | 取关成功响应后 | source=post_detail | 详情页关注钮 |
口径说明(widget/单元测试逐字段断言):
- **成功才报**:乐观翻转与失败回滚不报(06 §1.4「点赞/收藏/关注不埋失败」);
单飞合并链每个**实际抵达服务端并成功**的状态变更各报一条(快速连点合并后
至多两条、方向相反,与「成功响应后」字典口径一致)。
- **comment_create_started 不发**22 号锁死 unknown);`durationMs`
「输入会话首字符 → 成功响应」(评论无 started 事件,时长随成功事件带出);
`textLengthBucket` 分桶 empty/short(≤50)/medium(51500)/long(>500),精确
字数不出端(红线 1);`attemptSeq` 输入会话内从 1 递增,成功或清空输入重置;
`httpStatus = code ~/ 100`(pet 域同款);会话失效不上报;postId/commentId
等内容 ID 一律不进 props(红线 2)。
- **follow UI 判定**:详情页作者卡有关注钮(demo 形态保留升级双态),故
user_followed/unfollowed 本单接通;user_profile / follow_list 触点随
后续页面启用。
## 5. compose 真链路实测
后端 patbond-api dev@`8089c06` 六容器 `docker compose up -d --build` 全部
Up、postgres/minio healthy;测毕 `docker compose down` 干净退出,patbond-api
零改动。
**a)互动一轮(`test/smoke/detail_interactions_smoke_test.dart`env 门控
`PATBOND_DETAIL_SMOKE=1`,生产 ApiClient/Repository/Controller 全真实现)**
注册一次性账号 → 发帖(published)→ 点赞(`{liked:true, likeCount:1}`)→
**重复 PUT 幂等不重复计数** → 收藏/取消(计数 1→0)→ 评论创建
Idempotency-Key`commentCount` 0→1)→ 仅作者删除评论(`commentCount`
回 0、列表剔除)→ 权威计数逐步对账。**一次通过**。
**(b)断网点赞回滚(同测试内,生产 CommunityController + ToggleSync**
Feed 刷新拿到该帖(likedByMe=true/count=1)→ community 端点整体切至不可达
端口模拟断网(连接拒绝走生产 ApiClient 的真实 ApiNetworkException 链路)→
`toggleLike`:乐观翻转**同帧可见**(false/0)→ 请求失败后**快照成对回滚**
true/1)、`toggleError` 为 ApiNetworkExceptionSnackBar 消费口就位)→
恢复网络再 toggle → 服务端权威终态收敛(likedByMe=false/likeCount=0)。
**一次通过**(首轮实测暴露测试自身竞态:以乐观值判收敛会早退,已改为轮询
服务端权威终态)。
**(c)互动 8 事件白名单验真(curl,客户端真实 payload 形状)**:
`POST /api/v1/events`user :8082)一批 8 条(platform=android)→
**202 accepted 8 / rejected 0**`platform.product_events` 落库 props 完整
source / durationMs+isReply+textLengthBucket / failureReason+attemptSeq+
errorCode+httpStatus 逐键核对无剥离)。
真机专属项(乐观更新手感:连点合并请求数、回滚动画帧率、跨页一致、减弱
动态降级)已补全 device-verification.md「M3 预登记」第 2 项的步骤与通过标准。
## 6. 测试数变化
| 项 | 基线 | 本单后 |
|----|------|--------|
| flutter test | 421+1 门控冒烟跳过) | **458+37,门控冒烟跳过 2** |
| flutter analyze | 0 | 0 |
| dart format | 无 diff | 无 diff |
新增分布:post_detail_page widget 测试 17(四态 5 含 40403 弹回刷新与内存
副本先渲染、媒体九宫格与全屏大图 1、评论区 5——游标补页/失败重试/创建成败
与 attemptSeq/删除权限与 40301、互动 4——乐观翻转/失败回滚/收藏事件/跨页
一致、关注 5)、LikeButton 动画 6(点按激活缩放/取消无缩放/外部零动画跳变/
动画中回滚等播完/对账静默/禁用态)、CommentTile 3、互动埋点单测 6(键集/
分桶边界/失败原因映射)、home_page 更新 3(导航接通替换 T3-14 占位断言、
点赞接线成功事件、失败回滚 SnackBar)、helpers 扩展(评论/关注假仓钩子)。
另 compose 冒烟 1 条(env 门控,默认跳过不计入套件)。
## 7. 遗留与交接
- **T3-17 发布页**create 页 demo 数据层(AppState.publishPost/updatePost 与
`posts` 字段本体)随发布页真实化退役;demo 发布流现只回 Feed 不再导航
demo 详情页已消亡);PostMediaGrid 编辑态 + UploadProgressOverlay 已在。
- **@ 回复发起 UI / 长按操作 sheet(举报)**:数据层与埋点属性(isReply)
已支持,交互留待范围拍板。
- **大图浏览下滑关闭**:与 InteractiveViewer 平移手势冲突未做,photo_view
复评条件不变(03 号 E)。
- **真机项**device-verification.md M3 预登记第 2 项待真机执行(连点合并
请求数 ≤2 的抓包核对只能在真机/真网完成)。
---
**Frontend DeveloperFlutter**
**日期**2026-09-09