- 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>
This commit is contained in:
@@ -111,10 +111,57 @@ _(待真机到位后填写:日期、设备型号/Android 版本、两项结
|
||||
|
||||
以下为 M3 交付过程中预计产生的真机专属验证项,**各工单收口时在此补全具体步骤与通过标准**:
|
||||
|
||||
1. **媒体上传弱网表现**:真机蜂窝/弱 Wi-Fi 下选图→压缩→预签名直传→确认全链路;中断重试不产生孤儿 asset(对应 T3-13 验收的真机侧)。
|
||||
2. **乐观更新真机手感**:点赞/收藏快速连点的防抖与回滚动画在真机帧率下的表现(对应 T3-15/16)。
|
||||
3. **Feed 图片加载**:真机上滚动 Feed 的图片加载/缓存/占位表现;MinIO 经局域网/公网访问 URL 的可达性差异。
|
||||
4. **社区事件落库**:community 域 v3 事件(platform=android)落库观察(沿 M2 验证一的方法,事件名换 v3 增量)。
|
||||
1. **媒体上传弱网表现**(T3-13 收口补全,2026-09-09):真机蜂窝/弱 Wi-Fi 下选图→压缩→预签名直传→确认全链路;中断重试不产生孤儿 asset。
|
||||
|
||||
**前置**:通用前置准备的后端六容器在位;`PATBOND_MINIO_PUBLIC_ENDPOINT` 必须配置为手机可达地址(工作机局域网 IP:9000,.env 覆盖后重启 compose)——预签名直传 URL 直指 MinIO,漏配则手机端 PUT 必然连不上;`flutter run` 时四个 base URL 全传(含 `PATBOND_COMMUNITY_API_BASE_URL`),media 上传走 user 服务 :8082(`PATBOND_USER_API_BASE_URL`)。入口:发布页(T3-17 落地后)九宫格选图。
|
||||
|
||||
**步骤与通过标准**:
|
||||
- (a)**蜂窝正常网**:相册多选 3 张 12MP 大图 → 逐格出现进度环且百分比递增(非一跳 100%)→ 全部转 ready;后端 `media.assets` 对应 3 行 `status='ready'`。压缩耗时中端机单张 ≤2s(超出记录机型上报)。
|
||||
- (b)**弱网中断重试**:开发者选项限速或电梯/地库弱网,上传中开飞行模式掐断直传 → 该格转失败态(红色蒙层 + 重试通栏),其余图不受影响;恢复网络点格内重试 → 转 ready。
|
||||
- (c)**孤儿不引用**:在(b)失败态与上传中态各尝试一次发布 → 发布钮 gating 拦截(全部 ready 前不可提交);发帖成功后 psql 核对 `community.post_media` 引用的 assetId 全部 `status='ready'`,且不含(b)中断产生的旧 assetId(该行保持 `uploading`,属服务端超时清理范围,不算失败)。
|
||||
- (d)**凭据过期**:选一张图后挂起 App >10 分钟再恢复触发重试 → 客户端自动换新凭据完成上传(用户无感知,不弹「签名过期」类错误)。
|
||||
- (e)**HEIC/方向**:iPhone 传输的 HEIC 图与横拍竖拍各一张 → 压缩层统一出 jpeg 且方向正确(服务端 mime 白名单不收 HEIC,此项只能真机验证原生编解码)。
|
||||
2. **乐观更新真机手感**(T3-15/16 收口补全,2026-09-09):点赞/收藏快速连点的合并与回滚动画在真机帧率下的表现;Feed 卡片与详情页跨页状态一致。
|
||||
|
||||
**前置**:通用前置准备的后端六容器在位;`flutter run` 时四个 base URL 全传(含 `PATBOND_COMMUNITY_API_BASE_URL=http://<局域网IP>:8084`)。数据:Feed 内至少一条他人发布的帖子(可按 iteration-3 24 号报告 §5(a)种子方式造)。
|
||||
|
||||
**步骤与通过标准**:
|
||||
- (a)**激活动画帧率**:Feed 卡片与详情页各点赞一次 → 图标同帧翻转 + 240ms 弹性缩放(1→1.25→1)+ 计数即时 ±1;中低端机无可见掉帧或延迟出现的「二次跳动」。取消点赞仅颜色渐出、无缩放。
|
||||
- (b)**快速连点合并**:同一帖 1 秒内连点点赞 5~6 次 → 视觉每次即时翻转;抓包或服务端访问日志核对该帖 like 端点请求 ≤2 个(单飞 + 最终意图补发);停点后终态与最后一次点击一致,计数与 `GET /api/v1/posts/{id}` 权威值相符。
|
||||
- (c)**断网回滚**:开飞行模式后点赞 → 图标即时翻转,数秒内**零动画直接跳回**原状态(不得出现「心已灭计数未减」的中间帧或回弹动画)+ SnackBar「操作失败,请重试」恰一条;恢复网络重点 → 正常收敛。
|
||||
- (d)**跨页一致**:Feed 卡片点赞 → 进详情页应已是激活态;详情页取消收藏 → 返回 Feed 卡片同步取消(同一 ToggleSync 实例,无需刷新)。
|
||||
- (e)**减弱动态**:系统开启「移除/减弱动画」后点赞 → 状态瞬变、无缩放动画,功能不受影响。
|
||||
3. **Feed 图片加载**(T3-14 收口补全,2026-09-09):真机上滚动 Feed 的图片加载/缓存/占位表现;MinIO 经局域网/公网访问 URL 的可达性差异。
|
||||
|
||||
**前置**:通用前置准备的后端六容器在位;`PATBOND_MINIO_PUBLIC_ENDPOINT` 必须配置为手机可达地址(工作机局域网 IP:9000,.env 覆盖后重启 compose)——Feed 卡片封面 URL 是服务端现签的预签名 GET、直指 MinIO,漏配则真机图片全部走失败兜底(`surfaceTint` 底 + pets 图标);`flutter run` 时四个 base URL 全传(含 `PATBOND_COMMUNITY_API_BASE_URL=http://<局域网IP>:8084`)。数据:桌面/工作机先按 iteration-3 24 号报告 §5(a)的种子方式发 ≥26 帖(含单图/多图),保证两页以上可翻。
|
||||
|
||||
**步骤与通过标准**:
|
||||
- (a)**首屏与占位**:登录进 Feed → 图片卡先出 `surfaceTint` 加载块(无白闪/布局跳动),随后出图;多图卡右下「+N」角标可读(ink 80% 胶囊白字)。
|
||||
- (b)**滚动加载**:连续滚到列表底再回顶 → 中低端机不掉帧卡死;回滚经过已看过的图**不重新转圈**(缓存 key 已剥签名参数,同图不同签名命中同一内存缓存——若出现「每次刷新同图重新下载」即为缓存 key 回归,判失败)。
|
||||
- (c)**下拉刷新后的缓存命中**:下拉刷新(服务端对同一批图重新现签、URL 必然变化)→ 已展示过的封面应即时出图不过转圈;抓包或 MinIO 访问日志核对同对象未重复 GET。
|
||||
- (d)**过期 URL 重取**:Feed 停留 >1 小时(预签名 TTL)后滚到未加载过的卡 → 旧 URL 过期图走失败兜底属预期,下拉刷新取新签 URL 后恢复出图,无崩溃。
|
||||
- (e)**可达性差异**:Wi-Fi(局域网 IP)与蜂窝(若 MinIO 未公网暴露)各滚一遍——蜂窝下连不上 MinIO 时应稳定显示失败兜底图标而非无限转圈;记录两种网络的首图出图耗时。
|
||||
4. **社区事件落库**(T3-17 收口补全,2026-09-10):community 域 v3 事件(platform=android)落库观察(沿 M2 验证一的方法,事件名换 v3 增量)。**桌面端不可替代**:Linux 桌面的 `platform=linux` 不在契约枚举内,整批 400 被拒(`analytics_service.dart` 既有预期行为),故 v3 事件的**落库**只能在 Android 上验证;键集与形态的落库正确性已在工作机以 curl 造真实 payload 验证(iteration-3/26 §5c 发布/媒体 8 事件、iteration-3/25 §5c 互动 8 事件)。
|
||||
|
||||
**前置**:通用前置准备的后端六容器在位;`flutter run` 时四个 base URL 全传(含 `PATBOND_COMMUNITY_API_BASE_URL`);`PATBOND_MINIO_PUBLIC_ENDPOINT` 配为手机可达地址(媒体三段需真实直传)。可与第 1、2 项同一轮操作合并执行。
|
||||
|
||||
**步骤**:登录 → 首页 Feed 滚两屏并下拉刷新一次 → 进一条帖详情点赞/收藏/评论一次 → 返回 → 创作 Tab「发布动态」→ 输入正文 + 选 2 张图 → 「存草稿」一次 → 「发布」→ 回 Feed 确认新帖 → **退到后台等 5 秒**(触发冲刷)→ 工作机查库:
|
||||
|
||||
```bash
|
||||
docker exec patbond-postgres-1 psql -U patbond -d patbond -c \
|
||||
"SELECT event_name, platform, props FROM platform.product_events
|
||||
WHERE event_name LIKE 'post\_%' OR event_name LIKE 'feed\_%'
|
||||
OR event_name LIKE 'comment\_%' OR event_name LIKE 'user\_%'
|
||||
ORDER BY client_ts DESC LIMIT 40;"
|
||||
```
|
||||
|
||||
**通过标准**:
|
||||
- [ ] (a)**发布漏斗成链**:`post_create_started`(entryPoint=create_tab) → `post_draft_saved`(trigger=manual, mediaCount=2) → `post_publish_succeeded`(fromDraft=true、mediaCount=2、topicCount=0、textLengthBucket、durationMs>0) 三条齐全且 `platform=android`;无 `post_publish_failed`(顺利路径)。
|
||||
- [ ] (b)**媒体三段逐文件成对**:`post_media_upload_started` / `_succeeded` 各 **2** 条(每张图一条),`sizeBucket` 同一张图的 started/succeeded 取值一致,`durationMs` 为真实上传耗时(非 0);中断重试的那张(与第 1 项(b)合并执行时)另有 `post_media_upload_failed`(failureReason=network_error, attemptSeq=1) + 重试后 started 的 `attemptSeq` 递进。
|
||||
- [ ] (c)**隐私红线**:上述 props 中**不含** postId / assetId / commentId / 文件名 / 本地路径 / URL / 精确字数(`textLength`)/ 精确字节数(`byteSize`)——出现任一即验收失败(红线 1/2/4)。
|
||||
- [ ] (d)**互动与 Feed**:`post_liked`/`post_favorited`(source=feed 或 post_detail)、`comment_create_succeeded`、`feed_viewed`(离开 Feed 时一条,impressionCount>0、refreshCount=1)落库;**无** `post_impression`/`post_viewed`(字典锁死为 unknown,若出现即客户端违规)。
|
||||
- [ ] (e)**页名归一化**:`page_viewed` 出现 `pageName='post_form'`(发布页)与 `'post_detail'`,且 pageName/referrer 中**不含 UUID**。
|
||||
- [ ] (f)拒绝计数为 0:查 app 日志无 `Analytics batch permanently rejected`,或服务端响应 `rejected=0`(有 rejected 说明事件名/键集与字典不符,属回归)。
|
||||
|
||||
## 执行记录(M3)
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# 21 M3 第三波:community feature 数据层(T3-12)
|
||||
|
||||
**执行日期**:2026-09-09
|
||||
**工单**:T3-12 community feature 数据层——第三波前置,T3-13~17 全依赖本单
|
||||
**契约依据**:openapi.yaml v1.3.0(冻结稿,community/media 域 13 路径 / 19 操作)
|
||||
**提交**:patbond-flutter dev `19bd8c1`(基线 `66f983d`)
|
||||
|
||||
---
|
||||
|
||||
## 0. 概要
|
||||
|
||||
照 M2 pets 数据层模式(Controller / Repository / ApiClient 分层、类型化异常、
|
||||
分端口直连)新建 `lib/features/community/`,交付五个生产文件 + 四个测试文件:
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `lib/features/community/community_models.dart` | 全部 DTO,手写 JSON 逐字段照契约;未知枚举抛 FormatException 暴露漂移 |
|
||||
| `lib/features/community/community_exceptions.dart` | v1.3.0 新增 9 码 + 40902 共码的类型化异常与映射 |
|
||||
| `lib/features/community/community_repository.dart` | 抽象接口 + ApiCommunityRepository,19 操作全覆盖 |
|
||||
| `lib/features/community/toggle_sync.dart` | 点赞/收藏共用的乐观更新状态机(数据层部分) |
|
||||
| `lib/features/community/community_controller.dart` | Feed 多页缓存 + 四态骨架、详情副本、reset |
|
||||
| `lib/core/models/cursor_page.dart` | CursorPage 自 pet_models 上移 core(pets 侧 export 兼容,零调用方改动) |
|
||||
|
||||
配套改动:`lib/core/network/api_client.dart` 新增 `patbondCommunityApiBaseUrl`
|
||||
(`--dart-define=PATBOND_COMMUNITY_API_BASE_URL`,默认 `http://127.0.0.1:8084`);
|
||||
`lib/core/network/api_exception.dart` ApiCodes 增 9 码;`lib/app/app.dart` 装配
|
||||
CommunityController(共享 TokenRefresher,登出与 pets 同步 reset)。
|
||||
主壳 UI 未接线(T3-14 挂 Feed segment 时注入)。
|
||||
|
||||
**质量门禁**:`flutter test` 347/347 全绿(基线 286,+61)、`flutter analyze`
|
||||
0 问题、`dart format --set-exit-if-changed` 无 diff。
|
||||
|
||||
---
|
||||
|
||||
## 1. 19 操作覆盖对照表
|
||||
|
||||
| # | operationId | 方法/路径 | 仓库方法 | 备注 |
|
||||
|---|-------------|-----------|----------|------|
|
||||
| 1 | createMediaUpload | POST /api/v1/media/uploads | `createMediaUpload` | 两步上传第一步,返回预签名 PUT 凭据(TTL 10 min,不持久化) |
|
||||
| 2 | completeMediaUpload | POST /api/v1/media/uploads/{assetId}/complete | `completeMediaUpload` | 服务端幂等(已 ready 重复 confirm 200 同 asset) |
|
||||
| 3 | createPost | POST /api/v1/posts | `createPost` | **Idempotency-Key 必带** |
|
||||
| 4 | getPost | GET /api/v1/posts/{postId} | `getPost` | 防枚举 40403 |
|
||||
| 5 | updatePost | PATCH /api/v1/posts/{postId} | `updatePost` | version 乐观锁;`publish: true` 即 `status: published`;media 三态(缺席/[]/整组替换) |
|
||||
| 6 | deletePost | DELETE /api/v1/posts/{postId} | `deletePost` | 软删,重复删同 404/40403 |
|
||||
| 7 | listMyPosts | GET /api/v1/me/posts | `listMyPosts` | keyset 游标 + status 过滤(draft\|published) |
|
||||
| 8 | getFeed | GET /api/v1/feed | `getFeed` | (published_at,id) 游标 |
|
||||
| 9 | listComments | GET /api/v1/posts/{postId}/comments | `listComments` | 游标分页,单层平铺 |
|
||||
| 10 | createComment | POST /api/v1/posts/{postId}/comments | `createComment` | **Idempotency-Key 必带**;replyToUserId 可选 @ |
|
||||
| 11 | deleteComment | DELETE /api/v1/comments/{commentId} | `deleteComment` | 顶层短路径先例 |
|
||||
| 12 | likePost | PUT /api/v1/posts/{postId}/like | `likePost` | 语义幂等,返回权威 {liked,likeCount} |
|
||||
| 13 | unlikePost | DELETE /api/v1/posts/{postId}/like | `unlikePost` | 取消不存在的点赞 200 no-op |
|
||||
| 14 | bookmarkPost | PUT /api/v1/posts/{postId}/bookmark | `bookmarkPost` | 与点赞同构 |
|
||||
| 15 | unbookmarkPost | DELETE /api/v1/posts/{postId}/bookmark | `unbookmarkPost` | 同上 |
|
||||
| 16 | listMyBookmarks | GET /api/v1/me/bookmarks | `listMyBookmarks` | 项形态 = FeedCard |
|
||||
| 17 | followUser | PUT /api/v1/users/{userId}/follow | `followUser` | 自关注 422/42204 |
|
||||
| 18 | unfollowUser | DELETE /api/v1/users/{userId}/follow | `unfollowUser` | 自取关 200 幂等 no-op |
|
||||
| 19 | getFollowStats | GET /api/v1/users/{userId}/follow-stats | `getFollowStats` | 实时 COUNT,查自己 followedByMe 恒 false |
|
||||
|
||||
定型语义落点(20 号收口 §1 逐条):
|
||||
|
||||
- **预签名 URL 不持久化**:MediaUploadCredentials / MediaAsset.url /
|
||||
PostMediaItem.url / AuthorSummary.avatarUrl 的 doc 注释均标注「每次响应现签,
|
||||
不得持久化、过期即重取」,DTO 不做任何本地缓存。直传 PUT 本体属 T3-13,
|
||||
本单只到协议层(凭据 DTO 含 `requiredHeaders` 原样映射)。
|
||||
- **Idempotency-Key 必带 + 刷新重放同键**:键在仓库层每次调用生成一次
|
||||
(UUID v4,≤128 字符),ApiClient 401/40101 单飞刷新后的重放走同一 headers
|
||||
——同键命中服务端首次结果,不重复建帖/评论(测试断言两次请求同键)。
|
||||
- **PUT/DELETE 权威终态**:四个互动方法与关注两方法直接返回服务端
|
||||
LikeState/BookmarkState/FollowState,ToggleSync 以此对账。
|
||||
- **防枚举 40403**:PostNotFoundException 注明「hidden/archived 对作者亦不露、
|
||||
互动面 = 帖子公开面含本人草稿」。
|
||||
- **AuthorSummary nullable 降级**:`isDegraded`(nickname 与 avatarUrl 同为
|
||||
null)一个占位判定口,客户端不做昵称回退拼装。
|
||||
|
||||
## 2. 错误码映射(v1.3.0 新增 9 码 + 共码)
|
||||
|
||||
| 码 | 类型化异常 | 语义 |
|
||||
|----|-----------|------|
|
||||
| 40301 | PostAccessDeniedException | 对可见帖/评论无操作权限 |
|
||||
| 40403 | PostNotFoundException | 帖子防枚举合并 |
|
||||
| 40404 | CommentNotFoundException | 评论防枚举合并 |
|
||||
| 40405 | MediaAssetNotFoundException | asset 防枚举合并 |
|
||||
| 40406 | CommunityUserNotFoundException | 目标用户不存在/已注销 |
|
||||
| 40902 | PostVersionConflictException | 乐观锁共码,community 域独立类型 |
|
||||
| 40905 | IdempotencyMismatchException | 同键异 payload |
|
||||
| 42203 | MediaNotReadyException | 引用非 ready asset |
|
||||
| 42204 | SelfFollowException | 自关注(仅 PUT) |
|
||||
| 42205 | MediaUploadStateException | confirm 状态不允许 |
|
||||
|
||||
未覆盖码(40000、40401 宠物码等)原样透传通用 ApiBusinessException,
|
||||
既有按基类捕获的处理不受影响(与 pets 域映射器同构)。
|
||||
|
||||
## 3. ToggleSync 状态机(数据层部分)
|
||||
|
||||
03 号评估 §3 草案的定稿实现,点赞/收藏共用一套(字段读写 read/write、
|
||||
端点 send、代次 generation 全参数化,like/bookmark 各持一实例):
|
||||
|
||||
```
|
||||
点击 toggle(id)
|
||||
├─ read(id) == null(已被刷新剔除)→ 作废
|
||||
├─ 立即 write 翻转内存副本(计数 ±1,同帧反馈)
|
||||
├─ 无在途链 → 记快照(链起点)+ 记代次 → send(target)
|
||||
└─ 有在途链 → 只并入 pendingTarget,不发新请求(单飞)
|
||||
|
||||
响应到达
|
||||
├─ 链已被 reset / 代次不符(期间刷新)→ 丢弃,不覆盖不回滚
|
||||
├─ 成功且 pendingTarget ≠ 确认态 → 以最终意图补发一次(连点至多两在途)
|
||||
├─ 成功且意图一致 → write 服务端权威 {active,count}(吸收他人并发偏差),清链
|
||||
└─ 失败 → 校验「id 仍可读且当前态 == 本轮乐观目标」后恢复快照,
|
||||
onError 轻提示,不自动重试,清链
|
||||
```
|
||||
|
||||
一句话:**乐观翻转 + 快照回滚 + 单飞合并最终意图 + 代次守卫,以服务端
|
||||
权威终态收敛**。UI 侧 SnackBar/图标反馈属 T3-15/16(controller 已暴露
|
||||
`toggleError` 一次性消费口)。
|
||||
|
||||
CommunityController 骨架:首屏四态(initial/loading/ready/error)+ 尾部
|
||||
LoadMorePhase(idle/loading/error)+ 多页内存缓存;刷新 = 代次 +1 + 整体
|
||||
替换(失败保留旧列表走 refreshError);loadMore 携带上一页 nextCursor,
|
||||
旧代次尾页响应丢弃(避免刷新后重复/错位);详情 `getPost` 入 `_postCache`
|
||||
并回写卡片互动字段(Feed 卡片与详情页同源);`reset()` 登出清态
|
||||
(app.dart 与 pets 同一监听点)。
|
||||
|
||||
## 4. 测试数变化
|
||||
|
||||
| 项 | 基线 | 本单后 |
|
||||
|----|------|--------|
|
||||
| flutter test | 286 | **347(+61)** |
|
||||
| flutter analyze | 0 | 0 |
|
||||
| dart format | 无 diff | 无 diff |
|
||||
|
||||
新增分布:模型映射与请求序列化 15、仓库 19 操作线路 + 幂等键 + 错误映射 24、
|
||||
controller 竞态序列(四态/游标拼接/单飞补发/代次守卫/reset)22。
|
||||
竞态序列全部用 FakeCommunityRepository + Completer 控时序(test/helpers 先例)。
|
||||
|
||||
验证命令(仓库根目录执行):
|
||||
|
||||
```bash
|
||||
cd <patbond-flutter 仓库根>
|
||||
flutter analyze
|
||||
flutter test
|
||||
dart format --set-exit-if-changed --output=none .
|
||||
```
|
||||
|
||||
## 5. 契约核对与遗留
|
||||
|
||||
- 本单实现与 openapi.yaml v1.3.0 逐字段核对,**未发现契约不一致**,
|
||||
未改动契约与 patbond-api。
|
||||
- CreatePostRequest.category 只开放 general/help(ai_creation 提交
|
||||
400/40000),DTO 读侧三值、写侧由调用方约束;Post/FeedCard 读侧可解析
|
||||
ai_creation。
|
||||
- 遗留给后续工单:T3-13 预签名 PUT 直传客户端(裸 Dio,两段异构错误)、
|
||||
T3-14 Feed segment UI 接线(主壳注入 CommunityController)、
|
||||
T3-15/16 互动 UI 反馈(SnackBar 消费 toggleError)、T3-17 发布页。
|
||||
|
||||
---
|
||||
**Frontend Developer(Flutter)**
|
||||
**日期**:2026-09-09
|
||||
@@ -0,0 +1,76 @@
|
||||
# 22 事件字典 v3 白名单扩充(T3-20 后端,ADR-020)
|
||||
|
||||
**执行日期**:2026-09-09
|
||||
**交付**:EventDictionary v2 → v3(22 → 42 事件)+ 全套边界测试,patbond-api dev @ `8089c06`
|
||||
**依据**:06 号报告 §1.4/§1.5(事件与 props schema)、§1.2(feed_viewed 聚合裁定)、§1.3(隐私红线增量)、§6.1(pageName 页面族)
|
||||
|
||||
---
|
||||
|
||||
## 0. 概要
|
||||
|
||||
| 项 | 值 |
|
||||
|------|------|
|
||||
| 新增事件 | **20**(06 号 §1.5 的 19 个 + experiment_exposed 已含其中,编号 22~40) |
|
||||
| 字典总量 | 22 → **42** |
|
||||
| 测试 | 325 → **334**(+9:EventDictionaryTest +6、AnalyticsIntegrationTest +3),全绿 |
|
||||
| openapi.yaml | **零变更**——/api/v1/events 契约对事件名开放(键级校验在字典层),复核无需动 |
|
||||
| check-secrets.sh --all | 通过(exit 0) |
|
||||
|
||||
改动仅限 patbond-user analytics 包三个文件:`EventDictionary.java`、`EventDictionaryTest.java`、`AnalyticsIntegrationTest.java`。
|
||||
|
||||
## 1. 新增事件与 06 号对照清单
|
||||
|
||||
props 键集与 06 号 §1.5「工单可直接抄」代码块**逐键一致**(原样落地,零偏差):
|
||||
|
||||
| # | 事件名 | props 白名单 | 06 号出处 |
|
||||
|---|--------|--------------|-----------|
|
||||
| 22 | `post_create_started` | entryPoint | §1.4 发布漏斗 |
|
||||
| 23 | `post_draft_saved` | trigger, mediaCount | §1.4 发布漏斗 |
|
||||
| 24 | `post_publish_succeeded` | durationMs, mediaCount, topicCount, textLengthBucket, fromDraft | §1.4 发布漏斗(漏斗事件) |
|
||||
| 25 | `post_publish_failed` | failureReason, errorCode, httpStatus, attemptSeq | §1.4 发布漏斗 |
|
||||
| 26 | `post_deleted` | (空集——单事件风格无专有属性) | §1.4 发布漏斗 |
|
||||
| 27 | `post_media_upload_started` | mediaType, sizeBucket | §1.4 媒体漏斗(逐文件) |
|
||||
| 28 | `post_media_upload_succeeded` | mediaType, sizeBucket, durationMs | §1.4 媒体漏斗(漏斗事件) |
|
||||
| 29 | `post_media_upload_failed` | mediaType, sizeBucket, failureReason, errorCode, httpStatus, attemptSeq | §1.4 媒体漏斗 |
|
||||
| 30 | `feed_viewed` | feedTab, durationMs, impressionCount, loadMoreCount, refreshCount | §1.2/§1.4 聚合曝光(首个高频事件) |
|
||||
| 31 | `feed_load_failed` | feedTab, loadType, failureReason, errorCode, httpStatus | §1.4 Feed 消费 |
|
||||
| 32 | `post_liked` | source | §1.4 互动 |
|
||||
| 33 | `post_unliked` | source | §1.4 互动 |
|
||||
| 34 | `post_favorited` | source | §1.4 互动 |
|
||||
| 35 | `post_unfavorited` | source | §1.4 互动 |
|
||||
| 36 | `comment_create_succeeded` | durationMs, isReply, textLengthBucket | §1.4 互动 |
|
||||
| 37 | `comment_create_failed` | failureReason, errorCode, httpStatus, attemptSeq | §1.4 互动 |
|
||||
| 38 | `user_followed` | source | §1.4 互动 |
|
||||
| 39 | `user_unfollowed` | source | §1.4 互动 |
|
||||
| 40 | `experiment_exposed` | experimentKey, variant | §1.4 实验基建(A/B 前置 #5,M4 启用字典先行) |
|
||||
|
||||
**故意不进字典**(测试侧同步锁死为 unknown):`post_impression`(§1.2 逐卡曝光否决)、`post_viewed`(§1.4 由 page_viewed(post_detail) 覆盖)、`comment_create_started`(短表单不设 started)、`post_like_failed`/`user_follow_failed` 等单点互动失败(靠服务端错误率观测)、`topic_followed/unfollowed`(§1.6 缺口 3,UI 定稿前挂起待拍板)。
|
||||
|
||||
## 2. pageName 页面族核对(§6.1)
|
||||
|
||||
字典侧 pageName 的登记处只有 EventDictionary 的 javadoc 注释(ingest 只校验 props **键**,`page_viewed` 键集 pageName/referrer 不变)——已按 §6.1 同步为 v3 页面族:v2 九个 + 收编 4(create/pet_archive/services/post_detail)+ 新增 9(post_form/topic_list/topic_detail/user_profile/follower_list/following_list/favorite_list/draft_list)。与 §6.1「后端零改动提示」一致,无任何校验代码变更;值级枚举仍由客户端编译期 + 离线巡检兜底。
|
||||
|
||||
## 3. 测试增量(325 → 334)
|
||||
|
||||
**EventDictionaryTest +6**(沿既有 `containsExactlyInAnyOrder` 键集锁定模式):
|
||||
|
||||
1. `v3PostPublishFunnelMatchesDictionary` — 发布漏斗五事件,含 post_deleted 空集断言
|
||||
2. `v3MediaUploadFunnelMatchesDictionary` — 媒体三段漏斗
|
||||
3. `v3FeedDomainMatchesDictionary` — feed_viewed 聚合键集(无任何内容 ID 键)+ feed_load_failed
|
||||
4. `v3InteractionEventsMatchDictionary` — 互动八事件(分立事件名,无 action 属性)
|
||||
5. `v3ExperimentExposedRegisteredAheadOfM4Use` — experimentKey/variant
|
||||
6. `v3DeliberatelyAbsentEventsStayUnknown` — §1 末段七个故意不设事件
|
||||
|
||||
**AnalyticsIntegrationTest +3**(沿 v2 端到端先例):
|
||||
|
||||
1. `acceptsV3FeedViewedAggregateEvent` — feed_viewed 全键入库落表
|
||||
2. `stripsContentIdPropsFromV3InteractionEvent` — post_liked 混入白名单外 `postId` 被剥离(红线 2 的 ingest 侧兜底)
|
||||
3. `rejectedPerCardImpressionStaysOutOfDictionary` — post_impression 按 unknown_event_name 拒绝(§1.2 裁定锁死)
|
||||
|
||||
全套 `./mvnw clean test`:**334 测试 0 失败**(user/auth/pet/community/common 五模块 BUILD SUCCESS)。
|
||||
|
||||
## 4. 边界与遗留
|
||||
|
||||
- **契约零变更**:events 接口对事件名开放,openapi.yaml/契约快照均不需动,本工单未触碰。
|
||||
- **Flutter 半边未动**:客户端强类型封装(post_analytics.dart / feed_analytics.dart / community_interaction_analytics.dart / analytics_page_name.dart 增量)属 T3-20 客户端半边,不在本工单。
|
||||
- **待拍板项不预埋**:`content_rejected` 失败枚举(审核环节待拍板)与 `topic_followed`(UI 定稿)均未进字典,拍板后按 eventVersion 惯例增补。
|
||||
@@ -0,0 +1,182 @@
|
||||
# 23 M3 第三波:媒体上传客户端(T3-13)
|
||||
|
||||
**执行日期**:2026-09-09
|
||||
**工单**:T3-13 媒体上传客户端(L,关键路径)——选图到确认的完整客户端链路,T3-17 发布页依赖本单
|
||||
**协议依据**:13 号报告 §3 凭据形态定型表 + §4 偏差清单(两步上传协议权威描述)、契约 v1.3.0
|
||||
**提交**:patbond-flutter dev `1441f01`(基线 `19bd8c1`)
|
||||
|
||||
---
|
||||
|
||||
## 0. 概要
|
||||
|
||||
在 T3-12 数据层(createUpload / confirm 协议层)之上补齐直传 PUT 本体与编排,
|
||||
交付五个生产文件 + 五个测试文件:
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `lib/features/community/media_uploader.dart` | MediaUploader 编排状态机(本单核心,接口按 03 号评估 §4.3 冻结稿定稿) |
|
||||
| `lib/features/community/media_picking.dart` | 选图抽象 + image_picker 系统选择器实现 |
|
||||
| `lib/features/community/media_compression.dart` | 压缩抽象 + flutter_image_compress 原生实现(长边 ≤2048、统一转码 jpeg、不保留 EXIF) |
|
||||
| `lib/features/community/media_direct_upload.dart` | 预签名 PUT 直传客户端(裸 Dio,无鉴权拦截器,进度回调) |
|
||||
| `lib/core/widgets/upload_progress_overlay.dart` | 可复用进度覆盖层(05 号规范 §3.3 四态) |
|
||||
|
||||
**并发现并修正一处 T3-12 遗留缺陷**(§4):media 两步上传端点误挂 community
|
||||
客户端。**compose 六容器真链路实测通过**(§5)。
|
||||
|
||||
**质量门禁**:`flutter test` 379/379 全绿(基线 347,+32;另有 1 个默认跳过的
|
||||
compose 冒烟测试)、`flutter analyze` 0 问题、`dart format --set-exit-if-changed`
|
||||
无 diff。
|
||||
|
||||
## 1. MediaUploader 状态机
|
||||
|
||||
单张图生命周期(`MediaItemPhase`):
|
||||
|
||||
```
|
||||
queued ──► compressing ──► uploading(progress 0..1) ──► confirming ──► ready(assetId)
|
||||
│ │ │ │
|
||||
│ 超限终态失败 网络中断/存储拒绝 42205 / 网络异常
|
||||
│ ▼ ▼ ▼
|
||||
└──────► failed(retryable?) ◄─────┴───────────────────────┘
|
||||
│ retry(仅 retryable)
|
||||
└──► queued(复用压缩产物,从 createUpload 全新开始,换新 assetId)
|
||||
```
|
||||
|
||||
uploader 级另有 `isPicking`(系统选择器拉起中)。编排要点:
|
||||
|
||||
- **压缩策略**(03 号 §4.1 + 13 号偏差 #6):长边 ≤2048 重采样、统一转码
|
||||
JPEG、降质阶梯 80 → 60;两档后仍超 10 MiB → **终态失败(不可重试)**,
|
||||
不发起任何网络调用。`keepExif` 保持关闭,顺带剥离 GPS 隐私;
|
||||
`autoCorrectionAngle` 矫正方向。
|
||||
- **多图并发与顺序保持**:并发槽位默认 2(信号量覆盖压缩到 confirm 全段);
|
||||
items 顺序 = 加入顺序 = position 语义,完成先后乱序不影响
|
||||
`buildAttachRequests` 发号(测试实证第 2 张先 ready 仍归位 index 1)。
|
||||
单图失败不拖垮整批,其余照常 ready。
|
||||
- **凭据纪律**:预签名凭据只以局部变量存在、用完即弃,不持久化(沿用纪律);
|
||||
直传 PUT 原样携带 `requiredHeaders`(Content-Type 已签进签名)。
|
||||
- **孤儿防护(未 confirm 的 asset 不得被引用)三重保证**:
|
||||
1. confirm 前的服务端 assetId 只以管线局部变量存在,不落任务状态;
|
||||
2. 对外快照 `MediaUploadItem.assetId` 与 ready 态**构造期断言绑定**;
|
||||
3. 交付口 `buildAttachRequests` 在任何非 ready 项在场时抛 `StateError`。
|
||||
移除/reset 后的在途结果一律作废(不 confirm,服务端 uploading 超时清理
|
||||
兜底,13 号 §6 方案)。
|
||||
|
||||
## 2. 弱网 / 失败语义矩阵
|
||||
|
||||
| 故障点 | 表现 | 客户端语义 | 自动处置 | 手动 retry 后 |
|
||||
|--------|------|-----------|---------|--------------|
|
||||
| 压缩后仍超 10 MiB | 本地判定 | failed **终态** | 无 | no-op |
|
||||
| createUpload 400/40000(mime/大小白名单外) | 参数拒绝 | failed **终态** | 无 | no-op |
|
||||
| createUpload 网络异常 | — | failed 可重试 | 无 | 全新 createUpload |
|
||||
| PUT 前凭据已过期(30s 安全边距预检) | 本地判定 | 透明恢复 | 重新 createUpload **一次**(换新 assetId/凭据),仍过期才 failed | 全新 createUpload |
|
||||
| 直传 PUT 403(签名过期/被改动) | 存储侧拒绝 | 透明恢复 | 重新 createUpload **一次**并重传,再 403 才 failed(可重试) | 全新 createUpload |
|
||||
| 直传 PUT 断连/超时 | 网络型 | failed 可重试 | 无 | 全新 createUpload |
|
||||
| confirm 42205(对象未上传,服务端保持 uploading) | 可恢复 | failed 可重试 | 无 | 全新 createUpload |
|
||||
| confirm 42205(内容不符,服务端置 failed 终态) | 不可恢复 | failed 可重试* | 无 | 全新 createUpload |
|
||||
| confirm 返回非 ready(防御分支) | — | failed 可重试 | 无 | 全新 createUpload |
|
||||
|
||||
\* 两种 42205 客户端不可区分(同码同形态),统一按「可重试 + 重试换新
|
||||
asset」处理:对「保持 uploading」分支旧 asset 成为服务端可清理的 uploading
|
||||
僵尸,对「置 failed」分支旧 asset 本就终态——两分支都正确收敛,旧 assetId
|
||||
一律弃引用(孤儿防护保证其不会被发帖引用)。重试复用压缩产物(不重压缩)。
|
||||
|
||||
## 3. 可复用进度组件
|
||||
|
||||
`UploadProgressOverlay`(05 号 §3.3 逐条落位):排队(ink 40% scrim +
|
||||
「等待中」白字衬 ink 80% 胶囊)/ 上传中(白色环形进度 36 value 态 + 百分比
|
||||
胶囊;confirming 定格 100%)/ 成功(scrim 150ms 淡出无残留,IgnorePointer
|
||||
不拦截点击)/ 失败(error 12% scrim + errorDark 图标 + 底部「重试」通栏,
|
||||
整格点按重试;**终态失败不显示重试通栏**)。九宫格组装与页级线性汇总条
|
||||
(`overallProgress` 已暴露)留 T3-17。
|
||||
|
||||
## 4. T3-12 遗留缺陷修正:media 端点线路
|
||||
|
||||
**发现**:media 两步上传端点(`POST /api/v1/media/uploads[...]`)由 **user
|
||||
服务**提供(13 号 §2,MediaController 在 patbond-user :8082),community
|
||||
服务只有帖子/评论路由与媒体**读取侧**签名(MediaUrlSigner);而 T3-12 的
|
||||
`ApiCommunityRepository` 把 19 操作全部挂在 community 客户端(:8084)——
|
||||
media 两操作真链路必 404(T3-12 只做了协议层,无实测暴露点)。
|
||||
|
||||
**修正**:`ApiCommunityRepository` 增可选 `mediaApi` 客户端,media 两方法
|
||||
走它(未提供回落主客户端,既有测试桩不受影响);app.dart 装配处为其构建
|
||||
user 服务基址(`patbondUserApiBaseUrl`)的第二 ApiClient,共享
|
||||
SessionManager 与单飞 TokenRefresher。仓库测试改为双 adapter 断言线路不串。
|
||||
compose 真链路实测(§5)证实修正必要且有效。**未动 patbond-api。**
|
||||
|
||||
## 5. compose 六容器真链路实测
|
||||
|
||||
实测记录(2026-09-09,本机):
|
||||
|
||||
```bash
|
||||
cd <你的工作区>/patbond-api
|
||||
./deploy/init-secrets.sh
|
||||
JAVA_HOME=<你的 JDK17 路径> ./mvnw -DskipTests package # BUILD SUCCESS
|
||||
docker compose up -d --build # 六容器全部 Up,postgres/minio healthy
|
||||
|
||||
cd <你的工作区>/patbond-flutter
|
||||
PATBOND_MEDIA_SMOKE=1 flutter test test/smoke/media_upload_smoke_test.dart
|
||||
# 00:01 +1: All tests passed!
|
||||
|
||||
cd <你的工作区>/patbond-api && docker compose down # 干净退出
|
||||
```
|
||||
|
||||
冒烟测试(`test/smoke/media_upload_smoke_test.dart`,默认 skip 不计入常规
|
||||
套件)驱动**真实 MediaUploader** 走完整链路:注册一次性账号取 token →
|
||||
createUpload(user :8082,凭据 uploadUrl 指向 MinIO :9000)→ 预签名 PUT
|
||||
直传(真实 DioMediaDirectUploadClient)→ confirm → ready assetId →
|
||||
`buildAttachRequests` 引用发帖(community :8084,published)→ 帖子响应中
|
||||
预签名 GET URL 回读 **200 且字节与上传逐字节一致** → 删帖收尾。压缩层用
|
||||
透传实现(flutter test VM 无原生编解码平台通道),其余全为生产实现。
|
||||
期间修正一处冒烟脚本自身问题(注册手机号须 E.164 格式)。
|
||||
|
||||
## 6. 依赖新增说明
|
||||
|
||||
| 依赖 | 版本 | 理由 |
|
||||
|------|------|------|
|
||||
| `image_picker` | ^1.2.0 | 03 号评估 §4.1 选型:官方维护、pickMultiImage 多选;不引入重型相册组件 |
|
||||
| `flutter_image_compress` | ^2.4.0 | 同上:原生编解码(纯 Dart image 包中端机秒级卡顿排除);质量 + 尺寸重采样 + EXIF 方向矫正 |
|
||||
|
||||
直传 PUT 未新增依赖(复用既有 dio,独立裸实例)。桌面平台 generated
|
||||
plugin 注册文件随 pub get 更新一并入库。
|
||||
|
||||
## 7. 测试数变化
|
||||
|
||||
| 项 | 基线 | 本单后 |
|
||||
|----|------|--------|
|
||||
| flutter test | 347 | **379(+32,另 1 个默认跳过的 compose 冒烟)** |
|
||||
| flutter analyze | 0 | 0 |
|
||||
| dart format | 无 diff | 无 diff |
|
||||
|
||||
新增分布:MediaUploader 状态机 20(happy path 3、并发顺序 2、弱网失败语义
|
||||
9、孤儿防护 4、选图容量 4,含凭据过期重取、403 换凭据、42205 重试换新
|
||||
asset、终态 retry no-op、在途 remove/reset 作废不 confirm、全生命周期快照
|
||||
assetId 仅 ready 非空);直传层本地 HttpServer 3(200 逐字节到达 +
|
||||
requiredHeaders 原样 + 无 Bearer/设备头泄漏、403 → isCredentialRejected、
|
||||
半途断连 → 网络型可重试,照埋点队列测试先例);UploadProgressOverlay
|
||||
widget 6(三态 + confirming 定格 + 终态无重试 + 成功淡出);仓库 media
|
||||
线路双 adapter 改造 2(计入原有数);FakeCommunityRepository 扩 media 钩子。
|
||||
|
||||
验证命令(patbond-flutter 仓库根执行):
|
||||
|
||||
```bash
|
||||
flutter analyze
|
||||
flutter test
|
||||
dart format --set-exit-if-changed --output=none .
|
||||
```
|
||||
|
||||
## 8. 遗留与交接
|
||||
|
||||
- **T3-17(发布页)接入面**:`MediaUploader`(注入 CommunityController 同源
|
||||
repository 即可,其余依赖有生产默认值)+ `UploadProgressOverlay` +
|
||||
`buildAttachRequests(coverIndex:)`;`overallProgress`/`readyCount` 供页级
|
||||
汇总条;发布 gating 用 `allReady`(05 号 §2.2:全部 ready 才放行提交)。
|
||||
- **真机专属项**:蜂窝/弱 Wi-Fi 实测已按维护约定登记到
|
||||
`docs/development/device-verification.md` M3 预登记第 1 项(步骤与通过
|
||||
标准已补全)。
|
||||
- flutter_image_compress 的原生压缩行为(HEIC 输入转码、超大图内存)只能
|
||||
真机验证,随上项一并覆盖。
|
||||
- uploading 僵尸 asset 服务端清理任务(13 号 §6)仍未实现,客户端弃引用
|
||||
策略已按其到位为前提设计,无正确性风险(业务侧只认 ready)。
|
||||
|
||||
---
|
||||
**Frontend Developer(Flutter)**
|
||||
**日期**:2026-09-09
|
||||
@@ -0,0 +1,149 @@
|
||||
# 24 M3 第三波:首页 Feed 接入真实数据(T3-14)
|
||||
|
||||
**执行日期**:2026-09-09
|
||||
**工单**:T3-14 首页 Feed segment 替换真实数据——社区 demo 消亡的第一页
|
||||
**依赖**:21 号(T3-12 数据层,CommunityController 就位)、05 号 UI 规范、06 号埋点规划(feed 域白名单已随 api dev@8089c06 就绪)
|
||||
**提交**:patbond-flutter dev `8aac8c5`(基线 `1441f01`)
|
||||
|
||||
---
|
||||
|
||||
## 0. 概要
|
||||
|
||||
`home_page.dart` 的 Feed segment 由「AppState demo 帖子 + 500ms 假延时刷新」
|
||||
整体切换为 `CommunityController` 真实数据:四态首屏、尾部三态、下拉刷新与
|
||||
游标翻页、聚合曝光埋点全部落地;PostCard 三形态等 4 个共享组件入
|
||||
`lib/core/widgets/`;预签名 URL 的图片缓存 key 剥签名改造全仓生效。
|
||||
**未动 patbond-api**;create/post_detail 的 demo 按工单边界留给 T3-15/17。
|
||||
|
||||
**质量门禁**:`flutter test` 421/421 全绿(基线 379,+42)、`flutter analyze`
|
||||
0 问题、`dart format --set-exit-if-changed` 无 diff、compose 六容器真链路
|
||||
实测通过(§5)。
|
||||
|
||||
## 1. 四态与尾部三态覆盖表
|
||||
|
||||
| 态 | 渲染 | 交互 | widget 测试 |
|
||||
|----|------|------|------------|
|
||||
| 首屏 loading(initial/loading) | `FeedSkeleton` 连排 3 张(呼吸动效,尊重系统减弱动态设置静止 1.0) | — | ✓ |
|
||||
| 首屏 error | `InlineErrorBanner`(pets 同款话术映射)+ 「重试」FilledButton | 重试 = 用户刷新(计入浏览段 refreshCount)| ✓(含恢复 ready)|
|
||||
| 首屏 empty | `EmptyStateIllustration`(forum_outlined「还没有动态」)+ CTA「发布第一条」 | CTA → 创作 Tab | ✓ |
|
||||
| ready | `PostCard` 列表(卡间距 16) | 见 §2 | ✓(含降级作者)|
|
||||
| 尾部 loading | 24 转圈(primary)居中,上下留白 16 | 滚动近底(余量 400)自动触发,携上页 nextCursor | ✓ |
|
||||
| 尾部 error | 错误话术 + 「加载失败,点此重试」 | **只走显式点按重试**——失败态不随滚动通知自动重打(实测发现滚动风暴会把失败态冲掉并重复请求,已加守卫) | ✓(含事件上报与重试补页)|
|
||||
| 尾部到底 | 「没有更多了」12 `inkSoft` 居中 | — | ✓ |
|
||||
|
||||
刷新语义照 controller 契约:下拉刷新失败且旧列表在手 → 保留列表不闪空态,
|
||||
SnackBar 轻提示 + `feed_load_failed` 上报(widget 测试覆盖「失败保留旧列表 →
|
||||
再刷成功整体替换不残留」全序列)。翻页不丢不重由 controller 代次守卫保证
|
||||
(21 号已测),本单 widget 测试再从 UI 侧验证:两页游标取齐后三帖各恰一张。
|
||||
|
||||
搜索框保留 demo 交互(客户端过滤已加载多页缓存;契约 v1.3.0 无检索端点),
|
||||
过滤中不渲染尾部三态(翻页语义混淆);无命中沿用既有 `EmptyState`。
|
||||
|
||||
## 2. 组件落位
|
||||
|
||||
| 组件 | 落位 | 说明 |
|
||||
|------|------|------|
|
||||
| `PostCard` | `lib/core/widgets/post_card.dart` | 三形态:单图(mediaCount≤1 有封面)通栏出血 4:3;多图(mediaCount>1)走 PostMediaGrid 折叠封面;纯文字正文放宽 6 行、15/1.6。头部 `PetAvatar` sm32 + 名字 14/w700 + 相对时间 12 `inkSoft`;求助帖追加 `TagPill(accent)`。次级文字全部显式 `inkSoft`(DEBT-2 零新增) |
|
||||
| `PostMediaGrid` | `lib/core/widgets/post_media_grid.dart` | 展示态:列数规则 2/4→2 列、3/5–9→3 列,格间距 4、圆角 sm12;超 9 图末格 `ink` 80% scrim + 白字 +N 20/w800(05 §5.1 精算,60% 档弃用)。**形态偏差**:FeedCard 契约只带 coverImage+mediaCount(裁剪形态),Feed 卡多图实渲染为 4:3 封面 + 右下 +N 胶囊角标(同 80% scrim 精算);真九宫格留给详情/发布页全量媒体场景。编辑态(+格/删除角标)随 T3-17 扩展 |
|
||||
| `LikeButton` | `lib/core/widgets/like_button.dart` | 点赞/收藏参数化一件:未激活 `inkSoft`;点赞激活 `error` 图标 + `errorDark` 计数(demo `Colors.red` 3.13:1 修订清零,D6);收藏激活 `accentDark`。触控 44×44 |
|
||||
| `FeedSkeleton` | `lib/core/widgets/feed_skeleton.dart` | 05 §3.7 单元结构;0.6↔1.0 呼吸 1200ms,`disableAnimations` 静止 |
|
||||
| `SignedNetworkImage` | `lib/core/network/signed_network_image.dart` | 预签名 URL 缓存 key 剥离 `X-Amz-*` 签名参数(大小写不敏感、保留其余 query),`RemoteImage` 全仓换用——同对象两次响应 URL 必然不同,剥签名后命中同一 ImageCache 条目,未命中仍以完整签名 URL 请求 |
|
||||
| `community_display.dart` | `lib/features/community/` | 相对时间、加载失败话术、降级作者「宠友」统一占位(`isDegraded` 一个判定口 + `PetAvatar` 无图占位形态) |
|
||||
|
||||
**T3-14 互动取舍**(工单预留的选项里选了禁用态):demo 详情页按
|
||||
`appState.posts` 查 demo id,无法渲染服务端 postId 的真实帖,导航过去即崩;
|
||||
故整卡点按先弹 SnackBar「帖子详情正在接入真实数据」,点赞/收藏/评论/分享
|
||||
按钮为**纯展示禁用态**(真实计数与激活态照常渲染,`onPressed` 传 null)。
|
||||
T3-15 详情页重写后接导航,T3-15/16 接 ToggleSync 与激活动画。
|
||||
|
||||
主壳装配:`app.dart` 注入 `communityController` + `feedAnalytics` 进
|
||||
`MainShellPage` → `HomePage`(`isActive = currentIndex == 0` 驱动浏览段);
|
||||
`openPost(PostModel)` 保留给 create demo 流(T3-17 收编)。home 对
|
||||
`AppState.posts` 的消费清零,`posts` 字段本体随 T3-15/17 退役。
|
||||
|
||||
## 3. 曝光结算设计(feed_viewed / feed_load_failed)
|
||||
|
||||
一句话:**浏览段聚合**——进入 Feed 面开段,离开(切 Tab / 切服务分段 /
|
||||
退后台 / 页面销毁)时结算发**一条** `feed_viewed`,postId 只作段内内存
|
||||
去重键、绝不上报(06 §1.2 裁定 + 隐私红线 2)。
|
||||
|
||||
- **判定**:卡片可见面积 ≥50%(列表视口与卡片 RenderBox 纵向交叠比例)且
|
||||
驻留 ≥500ms;驻留计时在 `FeedViewSegment`(`feed_exposure.dart`),跌破
|
||||
阈值/滚出视口即取消。扫描统一调度到 post-frame(滚动通知发生在本帧布局
|
||||
前,同步读 RenderBox 是旧位置——实测踩到,已修)且一帧至多一次。
|
||||
- **计数口径**:`refreshCount` = 用户下拉/错误重试(首屏自动预取不计);
|
||||
`loadMoreCount` = 触底翻页请求(含尾部显式重试);`durationMs` 前台
|
||||
时长(退后台即结算,段天然前台连续),30 分钟截断。
|
||||
- **生命周期**:`WidgetsBindingObserver` 只在离开 resumed 的**第一次**变更
|
||||
结算(inactive→hidden→paused 级联不重复,SessionTracker 同款处理);
|
||||
回前台若仍在 Feed 面开新段。`settle()` 幂等,一段恰一条。
|
||||
- **feed_load_failed**:refresh / load_more 双路,`failureReason` 网络归并
|
||||
口径同 pet 域(断网/超时/5xx → network_error),`errorCode` 仅业务码、
|
||||
`httpStatus` 由五位码推导;**会话失效不上报**(应用即将回登录页)。
|
||||
- **页名核对**:Feed 属首页 Tab,`page_viewed(home)` 由既有 Tab 补点覆盖,
|
||||
无新增页名;`post_detail` 枚举已在(T3-15 接线导航后自动生效)。
|
||||
|
||||
两事件线上验证见 §5(白名单 202 accepted + `platform.product_events` 落库)。
|
||||
|
||||
## 4. 测试数变化
|
||||
|
||||
| 项 | 基线 | 本单后 |
|
||||
|----|------|--------|
|
||||
| flutter test | 379 | **421(+42,另 1 个既有默认跳过冒烟)** |
|
||||
| flutter analyze | 0 | 0 |
|
||||
| dart format | 无 diff | 无 diff |
|
||||
|
||||
新增分布:home_page widget 测试 13(四态 4、尾部三态与翻页 3、刷新失败
|
||||
序列 1、曝光结算 4——切 Tab/快速滑过/退后台/切分段、取舍与搜索 2)、
|
||||
PostCard 6(三形态/求助标/降级作者/操作行展示态)、PostMediaGrid +
|
||||
FeedSkeleton 7、FeedViewSegment 5(fake_async 控驻留时序)、FeedAnalytics 4
|
||||
(属性逐字段 + 异常映射)、缓存 key 与 provider 判等 7。另
|
||||
`integration_test/feed_live_test.dart` 桌面真链路 1 条(环境变量门控,
|
||||
默认跳过不计入套件)。
|
||||
|
||||
实现期修正两处(widget 测试暴露):尾部失败态被滚动通知自动重试冲掉
|
||||
(加 idle 守卫);回前台 `_lastLifecycle` 读旧值导致不开新段(resumed
|
||||
分支先置状态)。
|
||||
|
||||
## 5. compose 真链路实测
|
||||
|
||||
后端 patbond-api dev@8089c06(含 feed 域白名单),六容器 `docker compose
|
||||
up -d --build` 全部 Up、postgres/minio healthy。
|
||||
|
||||
**(a)数据种子 + 接口链路(curl)**:注册一次性账号 → 发 26 帖
|
||||
(24 纯文字 + 1 单图 + 1 双图,图走 media 两步上传:预签名 PUT 直传
|
||||
MinIO → confirm → ready assetId 引用发帖,全部 published)。
|
||||
`GET /api/v1/feed?limit=20` 首页 20 条 hasMore=true → 携 nextCursor 取第二页
|
||||
6 条 hasMore=false,**两页零重叠、26 条取齐**;封面预签名 GET 回读字节与
|
||||
上传原件 `cmp` 一致。`feed_viewed` / `feed_load_failed` 按客户端真实 payload
|
||||
形状 `POST /api/v1/events` → 双双 202 accepted,`platform.product_events`
|
||||
落库 props 完整(feedTab/durationMs/impressionCount/loadMoreCount/
|
||||
refreshCount;loadType/failureReason)。
|
||||
|
||||
**(b)Linux 桌面真跑(integration_test)**:
|
||||
`PATBOND_FEED_LIVE=1 flutter test integration_test/feed_live_test.dart -d linux`
|
||||
——真实 App 桌面渲染管线 + 真实 HTTP + MinIO 预签名图片(仅注入内存
|
||||
token 存储,桌面无 keyring):注册 → UI 登录 → Feed 首屏真数据卡片
|
||||
(含单图/多图 +1 角标卡)→ fling 触底游标翻页至「没有更多了」且最早
|
||||
一帖(#1)在列(两页取齐直接证据)→ 回顶下拉刷新列表仍健。**一次通过**
|
||||
(约 25s)。测后 `docker compose down` 干净退出,patbond-api 零改动。
|
||||
|
||||
**遗留观察**:桌面端 analytics 真实上报因 platform 枚举不含桌面值被服务端
|
||||
整批拒绝(既有已知约束,device-verification 通用前置已记载),不影响
|
||||
本单验证((a) 已按契约 platform 验真);Feed 图片加载的真机表现(蜂窝
|
||||
网络、缓存命中、局域网/公网 MinIO 可达性)预登记补全见 device-verification
|
||||
M3 第 3 项。
|
||||
|
||||
## 6. 遗留与交接
|
||||
|
||||
- T3-15:详情页重写后把 PostCard `onTap` 接 `openPost` 导航(页名
|
||||
`post_detail` 既有)、评论钮锚点;LikeButton 接 ToggleSync + §3.5 激活
|
||||
动画与回滚零动画;`AppState.posts` 消费面只剩 create/post_detail。
|
||||
- T3-16/17:收藏交互、发布页(PostMediaGrid 编辑态 + UploadProgressOverlay
|
||||
已在)。
|
||||
- 多图九宫格全量形态:详情页拿到 `Post.media` 全量后启用(PostMediaGrid
|
||||
列数规则与 +N 已就绪并有测试)。
|
||||
|
||||
---
|
||||
**Frontend Developer(Flutter)**
|
||||
**日期**:2026-09-09
|
||||
@@ -0,0 +1,184 @@
|
||||
# 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 块高 72,05 §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 合法、文案 inkSoft,DEBT-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 乐观写入同帧 notify;LikeButton 升 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` | 点赞**成功响应后** | source(feed / 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(51–500)/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` 为 ApiNetworkException(SnackBar 消费口就位)→
|
||||
恢复网络再 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 Developer(Flutter)**
|
||||
**日期**:2026-09-09
|
||||
@@ -0,0 +1,303 @@
|
||||
# 26 M3 第三波:发布页替换(T3-17)
|
||||
|
||||
**执行日期**:2026-09-10
|
||||
**工单**:T3-17 发布页替换——第三波收尾单,组装 T3-13 的 MediaUploader,接通发布漏斗埋点
|
||||
**依赖**:21 号(T3-12 数据层)、23 号(T3-13 MediaUploader 与孤儿防护)、15 号(后端帖子生命周期语义)、05 号 §2.3/§3.2/§3.3(P3 规范)、22 号(事件白名单 v3 实际收录名)、25 号(T3-15/16 埋点封装与页面惯例)
|
||||
**提交**:patbond-flutter dev `9892b65`(基线 `f873acf`),已推送 origin/dev
|
||||
|
||||
---
|
||||
|
||||
## 0. 概要
|
||||
|
||||
社区发布链路整条真实化:新建 `PostComposePage`(05 号 P3 规范,push 全屏页、
|
||||
路由名 `post_form`),组装 `MediaUploader` + `UploadProgressOverlay` 成编辑态
|
||||
九宫格;发布按「**createPost(draft) → PATCH status=published**」两步走,
|
||||
三条失败语义(40905 / 42203 / 网络)各有明确 UI 与埋点;发布漏斗五事件 +
|
||||
媒体上传三段全部挂接。create 页只余 AI 生成模拟(M4 原样保留),
|
||||
`AppState.posts` / `publishPost` / `updatePost` 及其 shared_preferences
|
||||
持久化整体退役。**未动 patbond-api。**
|
||||
|
||||
| 文件 | 性质 | 职责 |
|
||||
|------|------|------|
|
||||
| `lib/features/community/post_compose_page.dart` | 新增 | 发布页本体(结构、gating、两路径、失败语义、草稿恢复) |
|
||||
| `lib/features/community/post_analytics.dart` | 新增 | post 域埋点封装(发布漏斗 5 + 媒体三段 3,枚举编译期锁死) |
|
||||
| `lib/core/widgets/post_media_grid.dart` | 扩展 | 新增 `PostMediaEditGrid` 编辑态(+格/删除角标/进度层/重试) |
|
||||
| `lib/features/community/media_uploader.dart` | 扩展 | 媒体三段埋点挂接(attemptSeq / durationMs / cancelled) |
|
||||
| `lib/features/community/community_repository.dart` | 扩展 | `createPost` 支持调用方持键(同键重放) |
|
||||
| `lib/features/community/community_display.dart` | 扩展 | 发布/草稿失败话术映射(服务端 message 不上屏) |
|
||||
| `lib/analytics/analytics_page_name.dart` | 扩展 | 页名枚举补 `post_form`(字典 v3 页面族) |
|
||||
| `lib/features/main/main_shell_page.dart` | 扩展 | `openCompose(entryPoint)` push + 发布成功回 Feed 刷新 |
|
||||
| `lib/features/create/create_page.dart` | 改造 | demo 发布流退役 + 顶部「发布动态」真入口;AI 模拟零改动 |
|
||||
| `lib/features/home/home_page.dart` | 改造 | story「发布」与空态 CTA 改为 push 发布页(entryPoint=feed) |
|
||||
| `lib/state/app_state.dart` / `profile_page.dart` | 改造 | demo 帖子列表与持久化退役;「我的作品」改直读 demo 家具 |
|
||||
| `integration_test/publish_live_test.dart` | 新增 | 桌面真链路实测(env 门控,默认跳过) |
|
||||
|
||||
**质量门禁**:`flutter test` 502/502 全绿(基线 458,+44;另 2 个 env 门控
|
||||
compose 冒烟默认跳过)、`flutter analyze` 0 问题、
|
||||
`dart format --set-exit-if-changed` 无 diff、compose 六容器真链路实测通过(§5)。
|
||||
|
||||
## 1. 页面结构(05 号 §2.3 逐条对照)
|
||||
|
||||
自上而下:`已恢复上次草稿`提示条 → 发布失败横幅(+「草稿已保存」附注)→
|
||||
`已保存草稿 ✓` → **媒体编辑区**(3 列九宫格 + 「+」格)→ 页级上传汇总条 →
|
||||
正文(`minLines 6` 自增、`maxLength 1000` 计数器)→ 分类(`日常分享` /
|
||||
`求助` 二选)→ 位置 ListTile(占位);AppBar:左「取消」、标题「发布动态」、
|
||||
右「存草稿」+「发布」(高 40 / 水平 padding 20,禁用与转圈锁定)。
|
||||
|
||||
`PostMediaEditGrid`(05 §3.2 编辑态)实现要点:1:1 `cover`、格间距 4、圆角
|
||||
`sm`(12);缩略图直接 `Image.memory(previewBytes)`(选图原始字节,不落磁盘、
|
||||
不走网络,解码失败回落 `surfaceTint` + pets 图标);每格叠
|
||||
`UploadProgressOverlay` 六态→四视觉态;删除角标 22 圆 `ink` 80% + 白 close
|
||||
14(padding 撑到 32 触控热区);「+」格 1.5px **虚线**(Flutter 无内置虚线
|
||||
边框,按规范自绘 `_DashedBorderPainter`)、满 9 张隐藏。页级汇总条为
|
||||
「正在上传 n/N」+ `LinearProgressIndicator`(值条 `primaryStrong`、轨道
|
||||
`surfaceTint`)。
|
||||
|
||||
**与 05 号的偏差(4 项,均记录理由)**:
|
||||
|
||||
| # | 规范 | 本单实现 | 理由 |
|
||||
|---|------|---------|------|
|
||||
| 1 | 可发布条件「正文非空**或**媒体 ≥1」 | 正文非空 **且** 在场媒体全 ready | 后端 `content` 全程必填 1~10000(15 号 §2.4),「只发图不写字」在服务端不可能成功,不给按不亮的钮 |
|
||||
| 2 | 展示态与编辑态「一个组件」 | 同文件两个类(`PostMediaGrid` / `PostMediaEditGrid`) | 展示态以「≥1 张图 + URL 列表」为构造前提(既有断言),编辑态常态是「0 张图 + 一个+格」;共用签名会让两边都别扭 |
|
||||
| 3 | 内容变更后**静默自动保存**(防抖 2s) | 不做自动保存,只有「存草稿」与「取消 → 保留」两个显式动作 | 一次 `createPost` 只能建一份草稿(幂等键一次一用),自动保存要么反复建草稿要么每次 PATCH,收益不抵复杂度;且 06 §1.4 明确「自动保存不埋点」,无观测价值。列入遗留(§7) |
|
||||
| 4 | 「已保存草稿 ✓」置底部安全区上方 | 置提示条区(AppBar 之下) | 提交钮在 AppBar(顶部),反馈跟随触点;置底会出现「点了顶部按钮、底部看不见的反馈」 |
|
||||
| 5 | 话题行(TopicChip + 话题选择 shet) | 不渲染 | 契约无话题端点(21 号 §5),`topicCount` 埋点恒 0;随话题域落地补 |
|
||||
|
||||
拖拽排序(05 §6 D9 可选项)未做:**删格即整组重排**——position 由
|
||||
`buildAttachRequests` 按当前列表序 0..n-1 重发号(widget 测试实证「3 图删中间
|
||||
→ position 0,1、assetId 为 a-1/a-3」)。
|
||||
|
||||
## 2. 两条提交路径与草稿最小实现
|
||||
|
||||
```
|
||||
直接发布:createPost(status=draft, media=全ready挂接) ──► PATCH {version, status=published}
|
||||
↑ 幂等键由页面持有(同键重放) ↑ 失败时草稿已在服务端
|
||||
存草稿退出:createPost(status=draft) 或 PATCH(已有草稿:内容/类目/media 增量)──► 离页
|
||||
```
|
||||
|
||||
**为什么发布也先建草稿**:这样「发布失败但草稿已保存」是事实而非话术——
|
||||
迁移那一步失败时草稿已落库,UI 才敢显示「草稿已保存,可稍后继续发布」,
|
||||
重试也只补 PATCH 不重建帖(widget 测试断言 `created` 仍为 1 条)。
|
||||
|
||||
**幂等纪律**:建草稿的 `Idempotency-Key` 由**页面**持有(仓库层新增
|
||||
`createPost(request, {idempotencyKey})`,缺省仍是每次换新键,既有调用方
|
||||
不受影响):网络失败重试沿用同键 → 服务端命中首帖不重复建帖;**表单一经
|
||||
改动即弃用旧键**(下次提交换新键),使 40905 不会常态化。
|
||||
|
||||
**media 三态用法**(15 号 §2.6):以「上次同步到服务端的 ready assetId 签名」
|
||||
与当前签名比对——一致则 PATCH **缺席不动**(刚建的草稿不重复整组替换,也
|
||||
保住恢复草稿的既有图),不一致则整组替换,本地清空则传 `[]`。
|
||||
|
||||
**草稿管理最小实现**:进页 `listMyPosts(status=draft, limit=1)` 恢复最新一条
|
||||
(提示条「已恢复上次草稿」+「清空」;正文/类目预填;既有图以「草稿已含 N
|
||||
张图片(发布时保留;重新选图将整组替换)」呈现——`MediaUploader` 只持本地
|
||||
选图字节,服务端 asset 不回灌编辑器)。恢复失败静默降级为新建,不打扰。
|
||||
「取消 → 不保留」且服务端已有草稿 → `deletePost` 软删(`post_deleted` 的
|
||||
M3 唯一触点)。**完整草稿列表页(`draft_list`)留待**(§7)。
|
||||
|
||||
## 3. gating 与失败语义
|
||||
|
||||
**gating**:`正文非空 && (无媒体 || 全部 ready) && 无在途提交`——「全部
|
||||
ready」直接用 `MediaUploader.allReady`,与 `buildAttachRequests` 的
|
||||
`StateError` 孤儿防护形成双保险(gating 拦在前,类型层兜在后)。
|
||||
|
||||
| 失败 | UI 呈现(横幅,页内停留) | 客户端动作 | 埋点 failureReason |
|
||||
|------|--------------------------|-----------|-------------------|
|
||||
| **40905** 同键异 hash | 「提交内容与上次重试不一致,已重置提交标识,请再点一次「发布」」 | 弃用旧幂等键(下次换新键即成功) | `validation_error`(+errorCode 40905 / httpStatus 409) |
|
||||
| **42203** asset 未 ready | 「有图片还没上传完成,请等图片就绪后再发布」 | 保留内容,等图 ready 后重试 | `media_upload_incomplete` |
|
||||
| **网络失败** | 「网络异常,请检查网络后重试」(+ 草稿已落则附「草稿已保存,可稍后继续发布」) | 同键重放;已建草稿只补 PATCH | `network_error`(无 errorCode) |
|
||||
| 40000 参数 | 「内容不符合发布要求,请修改后重试」 | 保留内容 | `validation_error` |
|
||||
| 40403 草稿已被别处删 | 「草稿已不存在(可能已在别处删除),请重新发布」 | 解除草稿关联,重试走全新建草稿 | `not_found`(沿 06 §1.4 失败枚举基底的 not_found 复用条) |
|
||||
| 40902 乐观锁 | (不上屏)自动 `getPost` 取新 version 重提一次 | 再失败才落横幅 | `server_error` 兜底 |
|
||||
| 会话失效 | 应用自动回登录页 | — | **不上报**(feed / 互动域同款口径) |
|
||||
|
||||
发布成功:`MediaUploader.reset()` → `pop(true)` → 主壳切首页 Tab +
|
||||
`controller.refresh()` 整体替换 → 新帖按 `(published_at DESC, id DESC)`
|
||||
落首位 + SnackBar「已发布,去首页看看吧 🐾」(主壳级 widget 测试逐条断言)。
|
||||
|
||||
## 4. 埋点挂接清单(8 事件,按 22 号实际收录名)
|
||||
|
||||
| # | 事件 | 触发点 | props | 挂接位置 |
|
||||
|---|------|--------|-------|---------|
|
||||
| 1 | `post_create_started` | 进页后**首次输入**(首个字符或首次选媒体),每次进入一次 | entryPoint(create_tab / feed) | 发布页输入与 uploader 监听 |
|
||||
| 2 | `post_draft_saved` | 草稿保存**成功响应后** | trigger(manual / on_exit)、mediaCount | 「存草稿」与「取消 → 保留」 |
|
||||
| 3 | `post_publish_succeeded` | 迁移发布成功响应后 | durationMs、mediaCount、topicCount、textLengthBucket、fromDraft | 发布回调 |
|
||||
| 4 | `post_publish_failed` | 发布任一步失败 | failureReason、errorCode、httpStatus、attemptSeq | 发布回调(§3 映射表) |
|
||||
| 5 | `post_deleted` | 「不保留草稿」软删成功后 | (空集) | 离页确认弹窗 |
|
||||
| 6 | `post_media_upload_started` | 单文件一次尝试开始(含压缩段) | mediaType、sizeBucket | `MediaUploader._run` |
|
||||
| 7 | `post_media_upload_succeeded` | confirm 返回 ready 后 | mediaType、sizeBucket、durationMs | `MediaUploader._uploadAndConfirm` |
|
||||
| 8 | `post_media_upload_failed` | 单文件失败 / 在途被删格(cancelled) | mediaType、sizeBucket、failureReason、errorCode、httpStatus、attemptSeq | `MediaUploader._fail` / `_reportCancelled` |
|
||||
|
||||
口径说明(单测/widget 测试逐字段断言):
|
||||
|
||||
- **`entryPoint` 收敛为两值**:`create_tab`(创作 Tab 顶部「发布动态」)与
|
||||
`feed`(首页 story 环「发布」+ Feed 空态 CTA);topic_detail / pet_detail
|
||||
随对应页面启用。
|
||||
- **`fromDraft` 口径**:指「本次发布基于**先前保存/恢复的草稿**」;发布内部
|
||||
的建草稿→迁移两步**不算**(否则该字段恒真、失去分析意义)。
|
||||
- **`durationMs`**:`post_create_started` → 发布成功;媒体段为单次尝试
|
||||
started → ready。
|
||||
- **`sizeBucket` 取原图字节数**(压缩前),保证同一次尝试三段事件桶值一致;
|
||||
精确字节数、文件名、路径、URL 一律不出端(红线 4)。
|
||||
- **`attemptSeq`**:发布为本页发布尝试序号;媒体为单图尝试序号(retry 递增,
|
||||
重试的 started 与 failed 同序号)。
|
||||
- **`textLengthBucket`** 复用 `community_interaction_analytics.dart` 的
|
||||
`textLengthBucketOf`(不重复实现),精确字数不出端(红线 1)。
|
||||
- **`topicCount` 恒 0**(无话题端点);postId / assetId 等内容 ID 一律不进
|
||||
props(红线 2)。
|
||||
- **自动保存不埋**(06 §1.4)——本单索性不做自动保存(§1 偏差 3)。
|
||||
- **锁死事件不发**:`post_impression` / `post_viewed` / `comment_create_started`
|
||||
/ 单点互动失败等 7 项(22 号 §1 末段)本单未提供任何封装。
|
||||
- **page_viewed 页名核对**:发布页是 push 路由,`RouteSettings(name:
|
||||
'post_form')` 由既有 `AnalyticsRouteObserver` 自动上报;`post_form` 已在
|
||||
22 号 §2 的 v3 页面族内(字典侧仅 javadoc 登记,**后端零改动**)。客户端
|
||||
枚举补 `postForm`。创作 Tab 仍报 `create`(AI 创作面,语义未变)。
|
||||
- **未接触点**:`post_deleted` 除草稿丢弃外的「删已发布帖」触点无 UI(M3
|
||||
无删帖入口),随删帖 UI 启用;`experiment_exposed` 仍属 M4。
|
||||
|
||||
## 5. compose 实测
|
||||
|
||||
后端 patbond-api dev@`8089c06`(零改动)六容器 `docker compose up -d --build`
|
||||
全部 Up、postgres/minio healthy;测毕 `docker compose down` 干净退出。
|
||||
|
||||
```bash
|
||||
cd <你的工作区>/patbond-api
|
||||
./deploy/init-secrets.sh
|
||||
JAVA_HOME=<你的 JDK17 路径> ./mvnw -DskipTests package # BUILD SUCCESS
|
||||
docker compose up -d --build
|
||||
|
||||
cd <你的工作区>/patbond-flutter
|
||||
PATBOND_PUBLISH_LIVE=1 flutter test integration_test/publish_live_test.dart -d linux
|
||||
# 00:06 +1: All tests passed!
|
||||
|
||||
cd <你的工作区>/patbond-api && docker compose down
|
||||
```
|
||||
|
||||
### (a)Linux 桌面真链路 + 跨客户端可见性取证
|
||||
|
||||
`integration_test/publish_live_test.dart`(env 门控 `PATBOND_PUBLISH_LIVE=1`,
|
||||
默认跳过)驱动**真实 App**(桌面渲染管线 + 生产 ApiClient / Repository /
|
||||
CommunityController / MediaUploader / 直传客户端)走完整一轮:
|
||||
|
||||
注册两个一次性账号 → **A 登录** → 创作 Tab「发布动态」→ 输入正文 → 选图
|
||||
(真 `createUpload`@user:8082 → 真预签名 PUT@MinIO:9000 → 真 `confirm`)→
|
||||
发布钮由禁用转可点(gating 实证)→ 发布(建草稿 → PATCH 迁移)→
|
||||
**回首页 Feed,新帖置顶且 mediaCount=1** → **另起一个全新 App 实例**
|
||||
(换 key 强制重建:新 SessionManager / 新 Controller / 新 HTTP 客户端,
|
||||
等价于另一台客户端首次登录)**以 B 账号登录 → B 的 Feed 首位就是该帖**
|
||||
——M3 验收「发布后可在另一客户端看到」取证。**一次通过。**
|
||||
|
||||
桌面替身仅两处:**选图与压缩**——`image_picker` 与
|
||||
`flutter_image_compress` 均无 Linux 平台实现(桌面选图这一步在 Linux 上物理
|
||||
不可达),实测注入 1x1 真 PNG 字节与透传压缩,其余全为生产实现。原生选图/
|
||||
压缩行为仍属真机项(device-verification M3 第 1 项 (e))。
|
||||
|
||||
落库核对(psql):
|
||||
|
||||
```text
|
||||
community.posts: status=published, category=general, version=1, published_at≠null, media=1
|
||||
media.assets: status=ready, mime_type=image/png, byte_size=70
|
||||
```
|
||||
|
||||
### (b)后端语义三点复核(curl,与客户端实现对齐)
|
||||
|
||||
| 复核 | 结果 |
|
||||
|------|------|
|
||||
| 建草稿 → 迁移发布的 version 走线 | `createPost(draft)` 返回 **version 0** → `PATCH {version:0, status:published}` → **published / version 1 / publishedAt 非空**(客户端用响应 version,不硬编码) |
|
||||
| 同键异 payload | 同 `Idempotency-Key` 改正文 → **40905「幂等键已用于不同请求」** |
|
||||
| 引用未 ready asset | `createUpload` 后不上传直接发帖 → **42203「媒体尚未就绪」** |
|
||||
|
||||
三条与 §3 的 UI 语义一一对应,映射无偏差。
|
||||
|
||||
### (c)发布/媒体 8 事件白名单验真(curl,客户端真实 payload 形状)
|
||||
|
||||
`POST /api/v1/events`(user :8082)一批 8 条(platform=android,props 逐键
|
||||
按 §4 客户端实际形状)→ **202 accepted 8 / duplicated 0 / rejected 0**;
|
||||
`platform.product_events` 落库 props 完整无剥离:
|
||||
|
||||
```text
|
||||
post_create_started {"entryPoint": "create_tab"}
|
||||
post_draft_saved {"trigger": "on_exit", "mediaCount": 2}
|
||||
post_publish_succeeded {"fromDraft": true, "durationMs": 18200, "mediaCount": 2, "topicCount": 0, "textLengthBucket": "short"}
|
||||
post_publish_failed {"errorCode": 42203, "attemptSeq": 1, "httpStatus": 422, "failureReason": "media_upload_incomplete"}
|
||||
post_deleted {}
|
||||
post_media_upload_started {"mediaType": "image", "sizeBucket": "lt_1mb"}
|
||||
post_media_upload_succeeded {"mediaType": "image", "durationMs": 640, "sizeBucket": "lt_1mb"}
|
||||
post_media_upload_failed {"mediaType": "image", "attemptSeq": 2, "sizeBucket": "mb_1_5", "failureReason": "cancelled"}
|
||||
```
|
||||
|
||||
### (d)实测附带发现:桌面端埋点整批被拒(非回归,属既有预期)
|
||||
|
||||
桌面真链路运行时日志出现 `Analytics batch permanently rejected (400)`——
|
||||
原因是桌面 `platform` 值为 `linux`,而契约校验为
|
||||
`@Pattern(^(android|ios)$)`,**bean 校验整批 400**(不是逐条 rejected)。
|
||||
`analytics_service.dart` 的注释已声明桌面属「开发调试形态、上报被拒属预期」,
|
||||
但措辞是「逐条 rejected」,与实况(整批 400)有出入——**不改行为**,已在
|
||||
device-verification M3 第 4 项写明「v3 事件落库只能在 Android 上验证」,
|
||||
措辞修正留给埋点侧工单顺带处理(§7)。
|
||||
|
||||
## 6. 测试数变化
|
||||
|
||||
| 项 | 基线 | 本单后 |
|
||||
|----|------|--------|
|
||||
| flutter test | 458(+2 门控冒烟跳过) | **502(+44,门控冒烟跳过 2)** |
|
||||
| flutter analyze | 0 | 0 |
|
||||
| dart format | 无 diff | 无 diff |
|
||||
|
||||
新增分布:
|
||||
|
||||
- **发布页 widget 22**(`post_compose_page_test.dart`):gating 2(空正文禁用
|
||||
/ 在途禁用与全 ready 放行 + 汇总条)、直接发布 3(纯文字帖请求形状与漏斗
|
||||
事件、求助类目两图 position/封面/mediaCount、**删格重排** position 重发号)、
|
||||
失败三语义 4(网络**同键重放**实证两次同键、迁移失败「草稿已保存」且重试只
|
||||
补 PATCH、40905 换新键、42203 提示与埋点)、存草稿 5(manual / on_exit /
|
||||
「不保留」软删 + post_deleted / 「继续编辑」不动服务端 / 空表单直接离页)、
|
||||
草稿恢复 6(提示条与预填、恢复后只 PATCH 且 fromDraft=true、重新选图整组
|
||||
替换、40902 自动重提、「清空」、恢复失败静默降级)、结构 2。
|
||||
- **post 域埋点单测 11**(键集与白名单逐一对齐、分桶四档边界、异常 →
|
||||
failureReason 映射、隐私红线断言「无 postId / 无精确字数 / 无字节数」)。
|
||||
- **媒体三段埋点 7**(`media_uploader_test.dart` 扩展):成功一对且 sizeBucket
|
||||
同值 + durationMs、压缩终态 media_too_large、断连 → retry 的 attemptSeq
|
||||
递增、createUpload 40000 → unsupported_format 带 errorCode/httpStatus、
|
||||
在途删格 cancelled 与 ready 后删格不报、会话失效不上报。
|
||||
- **编辑态九宫格 widget 4**(空列表只出+格 / 满 9 隐藏+格 / 删除角标回传
|
||||
localId / 上传中与失败态覆盖层与整格重试,终态无重试通栏)。
|
||||
- **主壳发布闭环 1**(`main_shell_publish_test.dart`):创作 Tab 入口 → 发布 →
|
||||
回首页 + **两次 getFeed(整体刷新)** + 新帖置顶 + SnackBar。
|
||||
- 首页测试 1 处随回调改名更新(`onOpenCreate` → `onOpenCompose`),helpers 扩
|
||||
createPost/updatePost/deletePost/listMyPosts 钩子与幂等键记录、真 PNG 字节。
|
||||
|
||||
验证命令(patbond-flutter 仓库根执行):
|
||||
|
||||
```bash
|
||||
flutter analyze
|
||||
flutter test
|
||||
dart format --set-exit-if-changed --output=none .
|
||||
```
|
||||
|
||||
## 7. 遗留与交接
|
||||
|
||||
- **草稿自动保存与草稿列表页**:本单只做「显式两路径 + 进页恢复最新一条」。
|
||||
完整草稿管理(`draft_list` 页名已在 v3 页面族预留、我的帖子按 status 过滤
|
||||
的接口已就位)与 05 §2.3 的自动保存(防抖 2s)留待——自动保存需先定「一份
|
||||
草稿反复 PATCH」的语义与 `post_draft_saved` 不埋自动保存的口径衔接。
|
||||
- **话题域**:TopicChip / 话题选择 sheet / `topic_followed` 事件均待契约端点,
|
||||
`topicCount` 现恒 0。
|
||||
- **位置**:ListTile 为占位(无契约字段),点按 SnackBar 提示。
|
||||
- **AI 作品发布**:create 页 AI 结果是生成图(无本地文件、无 media asset),
|
||||
走不了两步上传,其「发布到社区」现为占位提示,随 M4 AI 能力一并接。
|
||||
- **拖拽排序**(05 §6 D9 可选)未做;删格重排已保证 position 正确。
|
||||
- **真机项**:device-verification.md「M3 预登记」第 4 项(社区事件落库)
|
||||
**本单已补全细则**——含发布漏斗成链、媒体三段逐文件成对与 attemptSeq、
|
||||
隐私红线核对、Feed 与互动事件、`page_viewed(post_form)` 页名归一化、
|
||||
rejected=0 六条通过标准,并写明「桌面 platform=linux 整批 400,落库只能在
|
||||
Android 验证」的前置事实。第 1 项(媒体弱网)与本项建议同一轮执行。
|
||||
- **埋点侧措辞修正**(非阻塞):`analytics_service.dart` 关于桌面上报被拒的
|
||||
注释应由「逐条 rejected」改为「整批 400」(§5d),留给埋点侧工单顺带处理。
|
||||
- `MediaUploaderFactory` 是**测试与桌面实测专用**注入口(生产恒缺省):
|
||||
Linux 桌面既无 image_picker 也无 flutter_image_compress 原生实现,真链路
|
||||
实测只替换选图与压缩两层。
|
||||
|
||||
---
|
||||
**Frontend Developer(Flutter)**
|
||||
**日期**:2026-09-10
|
||||
@@ -0,0 +1,40 @@
|
||||
# 27 M3 第三波收口:Flutter 社区接入完成
|
||||
|
||||
**执行日期**:2026-09-09
|
||||
**交付**:冻结契约 v1.3.0 下社区全页面族接入真实后端,社区 demo 数据消亡
|
||||
|
||||
---
|
||||
|
||||
## 0. 概要
|
||||
|
||||
| 工单 | 交付 | 提交(flutter dev) | 测试 |
|
||||
|------|------|------|------|
|
||||
| T3-12 数据层 | 19 操作 DTO/Client/Repository + 9 新错误码 + ToggleSync + CursorPage 上移 core | 19bd8c1 | 286→347 |
|
||||
| T3-13 媒体上传客户端 | MediaUploader 六态 + 孤儿防护 + 降质阶梯 + 凭据过期重取 | 1441f01 | →379 |
|
||||
| T3-14 Feed 替换 | 四态 + 尾部三态 + 曝光浏览段聚合 + SignedNetworkImage | 8aac8c5 | →421 |
|
||||
| T3-15/16 详情与互动 | 整页替换 + 真九宫格大图 + 评论区 + ToggleSync 跨页一致 + 互动 8 事件 | 92524da / f873acf | →458 |
|
||||
| T3-17 发布页 | PostComposePage + 草稿两路径 + gating + 发布漏斗 8 事件 | 9892b65 | →502 |
|
||||
| 字典 v3 白名单(api) | EventDictionary 22→42 事件 + 7 锁死事件边界 | api dev@8089c06 | api 325→334 |
|
||||
|
||||
**波末状态**:patbond-flutter **502 测试**全绿、analyze 0 问题、format 无 diff;patbond-api **334 测试**全绿。
|
||||
|
||||
## 1. 里程碑意义
|
||||
|
||||
- **社区 demo 在三页全面消亡**:`AppState.posts/publishPost/updatePost` 及其持久化整体退役;home Feed、post_detail、create 发布半边全部真实后端驱动(create 页仅余 AI 生成模拟,属 M4 范围零改动)
|
||||
- **M3 验收标准逐条取证**:发布后另一客户端可见(T3-17 双 App 实例实测)、重复点赞不重复计数(后端真并发 + 前端 ToggleSync)、分页不丢不重(T3-14 游标专项 + 26 帖实测)、删除/隐藏不出 Feed(后端谓词 + 40403 防枚举)
|
||||
- **媒体链路端到端**:选图→压缩→预签名直传 MinIO→confirm→引用发帖→预签名 GET 展示,四次 compose 实测无契约偏差;签名 URL 缓存 key 剥离(SignedNetworkImage)全仓生效
|
||||
- **乐观更新完整落地**:ToggleSync(乐观翻转/单飞合并最终意图/代次守卫/服务端权威终态收敛)+ 三层视觉抑制(240ms 弹性动画/失败零动画跳变/对账静默替换),Feed 与详情页共享实例同帧一致
|
||||
- **埋点 v3 端到端**:客户端挂接 21 事件(feed 2 + 互动 8 + 媒体 3 + 发布漏斗 5 + page_viewed 页名增量),后端白名单 42 事件承接,7 个被否决事件在字典层锁死
|
||||
|
||||
## 2. 实现期修正与发现
|
||||
|
||||
- **T3-13 抓修 T3-12 遗留缺陷**:media 两步上传端点在 user 服务(:8082),T3-12 误挂 community 客户端(:8084 无 media 路由,真链路必 404);compose 实测暴露,增 mediaApi 分端口直连修正
|
||||
- **T3-14 测试暴露两处真 bug**:尾部失败态被滚动自动重试冲掉、回前台不开新曝光段
|
||||
- **T3-17 两步发布定型**:createPost(draft) → PATCH published,使「发布失败但草稿已保存」成为事实而非话术
|
||||
- **桌面替身局限记录**:Linux 桌面无 image_picker/compress 平台实现(用替身)、`platform=linux` 使埋点整批 400(既有预期),两者均已写入真机验证清单前置
|
||||
|
||||
## 3. 遗留与下波
|
||||
|
||||
- 完整草稿列表与自动保存(26 号 §7)、大图「下滑关闭」手势(photo_view 复评)、话题功能(ADR-018 剪出)
|
||||
- uploading 超时清理定时任务、429 Retry-After 分支(待后端限流)
|
||||
- **第四波收官**:E2E 烟囱(社区全链路 + M3 四条验收标准取证)→ M3 收官总结 + feature-checklist 增补;真机四项已在 device-verification.md 备齐步骤,待设备到位执行
|
||||
Reference in New Issue
Block a user