d2867826d3
CI / docs-build (push) Successful in 55s
- iteration-3 报告 01-08(PM 拆解/后端/Flutter/RC 首个 CERTIFIED/UI/埋点/证据基线/Git) - ADR-016 自托管 MinIO 起步预留迁云(用户确认现无云存储)、ADR-017 patbond-community :8084 + media 归 user + 作者信息跨 schema 只读、ADR-018 范围裁剪(话题剪出/单层评论)、 ADR-019 幂等按域(PUT/DELETE + request_hash)、ADR-020 聚合 feed_viewed/字典 v3/ 北极星不变/队列三项升第一波、ADR-021 Git 修订(main 更正/PR 情形触发/首发布重建 main/ 防泄漏 grep 先行) - mkdocs 挂「第三迭代」导航,build --strict 通过 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
179 lines
16 KiB
Markdown
179 lines
16 KiB
Markdown
# 03 · Flutter 前端技术评估(M3:社区)
|
||
|
||
> 作者:Frontend Developer
|
||
> 日期:2026-09-08
|
||
> 依据:开发计划 §M3、M2 收官报告(iteration-2/29)、22/23/26 号报告交接约定、15 号队列报告 §4
|
||
> 性质:开工前评估,只读分析 + 验证性测试,未改动任何生产代码。
|
||
|
||
## 0. 基线验证
|
||
|
||
```text
|
||
$ flutter test # patbond-flutter dev@720865b @ Flutter 3.44.6 stable
|
||
00:27 +272: All tests passed! # 272 个测试全绿,与 M2 收官记录一致
|
||
```
|
||
|
||
**M3 以 272 为基线**,收官时只增不减。
|
||
|
||
## 1. 社区 demo 现状盘点(实际读码结论)
|
||
|
||
### 1.1 替换面总览
|
||
|
||
社区 demo 分布在四处,总计约 1700 行,其中**数据层是 100% 替换、UI 骨架大半可保留**:
|
||
|
||
| 文件 | 行数 | demo 面 | 可保留骨架 |
|
||
| --- | --- | --- | --- |
|
||
| `lib/state/app_state.dart` | 97 | `posts` demo 列表 + shared_preferences 持久化(`_postsKey`)、`updatePost`/`publishPost` | 无——`posts` 相关全部退役;`pet`/`locationWeather` demo 归首页/创作页家具,本迭代不动 |
|
||
| `lib/features/home/home_page.dart` | 863 | Feed segment:`_PostCard` 列表直读 `appState.posts`、客户端关键词过滤、**RefreshIndicator 是 500ms 假延时**、点赞就地翻转 demo 数据;`_StoryRow` 圈子/`_PromoCard` 促销硬编码 | `_PostCard` 版式、天气条/问候卡/搜索框/segment 结构、服务 segment(M5 范围)全保留 |
|
||
| `lib/features/post/post_detail_page.dart` | 277 | **整页数据 demo**:`toggleLike`/收藏本地翻转、`sendComment` 本地插入(作者硬编码「萌宠新手」)、关注按钮纯 `setState` 布尔、分享是演示 SnackBar | 版式(大图头、作者卡、正文卡、评论列表、底部输入条)整体保留 |
|
||
| `lib/features/create/create_page.dart` | 547 | `publishPost` 落 AppState;700/650/500ms 假延时是 **AI 生成模拟,属 M4 范围** | M3 只接「发布 → 社区服务」半边(草稿/发布);AI 模拟原样留给 M4 |
|
||
|
||
- **模型不复用**:`PostModel`/`CommentModel`(`lib/models/models.dart`)是 demo 形态——`time` 是「刚刚」类字符串、无服务端 id/authorId、无 version/游标字段。照 M2 先例新建 community 模型,demo 模型随页面替换下线。
|
||
- **埋点基础就绪**:`main_shell_page.openPost` 已带 `RouteSettings(name: postDetail)`,页名枚举已有 `post_detail`;社区事件按 `pet_analytics.dart` 同款强类型封装新建 `community_analytics.dart`。
|
||
- **图片入口集中**:全仓远程图统一走 `widgets/common.dart` 的 `RemoteImage`(内部 `Image.network`,仅内存缓存)——媒体缓存改造成本集中一处(§4.1),但替换波及全仓图片(含 pets 头像),需全局回归。
|
||
|
||
一句话:**post_detail 数据层整页重写(UI 骨架保留),home 的 Feed segment 重做数据源与分页,create 只接发布半边,`AppState.posts` 退役**。
|
||
|
||
### 1.2 可直接复用的 M2 资产
|
||
|
||
- 分层模板:`PetsController`(ChangeNotifier 四态)→ `PetsRepository`(抽象 + Api 实现)→ `ApiClient`(错误信封、401/40101 单飞刷新重放、429 类型化)。
|
||
- `CursorPage<T>` 正典信封(`{items, nextCursor, hasMore}`)与「加载更多失败保留重试」页面交互(体重/健康事件列表已验证)。
|
||
- 分端口直连模式:`--dart-define` 注入 base url(auth :8081 / user :8082 / pet :8083),community 服务照加 `PATBOND_COMMUNITY_API_BASE_URL`(默认 :8084,以后端为准);同一 `SessionManager`/`TokenRefresher` 共享,新建一个指向 community 端口的 `ApiClient` 实例即可,**网络层零改动**。
|
||
- Idempotency-Key 先例:pets 域四个 POST 每次逻辑提交换新键、token 刷新重放沿用同键。
|
||
- 测试手法:`FakeRepository` + `Completer` 控时序(`test/helpers/` 先例)、四态 widget 测试。
|
||
|
||
## 2. community feature 分层规划
|
||
|
||
### 2.1 目录与分层(照 pets 模式,一处例外)
|
||
|
||
```
|
||
lib/features/community/
|
||
community_models.dart # Post / PostComment / FeedPage 等,手写 JSON
|
||
community_exceptions.dart # 业务码 → 类型化异常映射
|
||
community_repository.dart # 抽象接口 + ApiCommunityRepository
|
||
feed_controller.dart # Feed 状态机(见 §2.2),Tab 级注入
|
||
post_detail_page.dart # 重写现 features/post/(旧目录随迁移删除)
|
||
post_analytics / media/... # 随工单拆分
|
||
```
|
||
|
||
例外在**控制器职责**:`PetsController` 的 `refresh()` 一次拉全量,而 Feed 是游标累积流、且详情页/首页共享同一份帖子内存副本(点赞状态要跨页一致),所以 `FeedController` 是 Tab 级单例(`app.dart` 装配注入主壳,同 PetsController),**不做页面级 state**。评论列表则相反——只属详情页,照 26 号报告「页面级状态按页自建」纪律放详情页 State 里,不膨胀 FeedController。
|
||
|
||
### 2.2 Feed 状态机(对 pets 四态的两点扩展)
|
||
|
||
```dart
|
||
enum FeedPhase { initial, loading, ready, error } // 首屏四态,同 pets
|
||
enum LoadMorePhase { idle, loading, error } // 尾部加载态,新增
|
||
|
||
class FeedController extends ChangeNotifier {
|
||
List<Post> _items; // 累积列表(多页内存缓存即「多页缓存」,不落盘)
|
||
String? _nextCursor;
|
||
bool _hasMore;
|
||
FeedPhase _phase;
|
||
LoadMorePhase _loadMorePhase;
|
||
int _generation = 0; // 刷新代次,丢弃过期响应(见下)
|
||
}
|
||
```
|
||
|
||
- **下拉刷新与游标的关系**:刷新 = 丢弃游标、从头拉第一页、**成功后整体替换**累积列表(不做增量 prepend/「有新内容」提示,M3 不引入 since 语义);**刷新失败保留旧列表** + SnackBar,不清空不闪空态。刷新使 `_generation++`,在途的旧代次加载更多响应到达时直接丢弃——这是 pets 没有的并发点,必须做,否则「刷新后旧尾页追加」会产生重复/错位。
|
||
- **加载更多**:滚动近底触发;失败置 `LoadMorePhase.error`,尾部渲染重试条(复刻体重列表交互);`hasMore=false` 渲染到底提示。
|
||
- **详情页同步**:详情页构造注入 `FeedController` + postId,读 controller 副本渲染;进入时 `getPost(id)` 拉详情并 `_replaceInList` 回写(照 `PetsController.getPet` 先例),点赞/收藏经 controller 统一走 §3 状态机,Feed 卡片与详情天然一致。
|
||
- **登出 reset()**:清列表回 initial,同 pets 纪律。
|
||
- 首页现有的客户端关键词过滤在真实分页下语义不成立(只能过滤已加载页),M3 建议搜索框对 Feed segment 降级为占位/隐藏,真搜索留给后端搜索接口(范围归 PM)。
|
||
|
||
## 3. 乐观更新回滚设计草案(点赞/收藏)
|
||
|
||
M3 前端最大新课题。核心:**乐观翻转 + 快照回滚 + 单飞合并意图 + 代次守卫**,点赞/收藏共用一套 `ToggleSync` 小状态机(字段读写与端点参数化,避免复制两份)。
|
||
|
||
### 3.1 状态机
|
||
|
||
对每个 postId 维护(Map 存于 FeedController,随 reset 清空):
|
||
|
||
```
|
||
inFlight: bool # 该 post 是否有请求在途(单飞)
|
||
pendingTarget: bool? # 在途期间用户又点出的最终意图
|
||
snapshot: (liked, likeCount) # 本轮操作链起点快照,用于回滚
|
||
```
|
||
|
||
1. **点击**:立即翻转内存副本(`hasLiked` 取反、`likeCount ±1`)并 notify——反馈是同帧的。若 `inFlight`,只记 `pendingTarget` 并返回(不发新请求)。
|
||
2. **发请求**:非在途则记快照、置 `inFlight`,按当前目标态发送。
|
||
3. **成功**:若 `pendingTarget` 与已确认态不一致 → 以 pendingTarget 为目标**补发一次**(连续快速点击最多两个请求,中间抖动全被合并);一致则用服务端返回的权威 `likeCount` 覆盖乐观计数(吸收他人并发点赞造成的偏差),清状态。
|
||
4. **失败**:恢复快照并 notify,SnackBar 轻提示(「点赞失败,请重试」),**不自动重试**(用户可再点,重点一次即新一轮);清状态。
|
||
5. **守卫**:请求携带发起时的 `_generation`,响应到达时代次不符(期间发生过刷新,列表已被服务端数据整体替换)→ 丢弃该响应、不回滚不覆盖——避免用陈旧快照污染新数据。快照恢复前同样校验该 postId 仍在列表且当前态仍是本轮乐观写入的目标态。
|
||
|
||
### 3.2 与后端幂等的配合
|
||
|
||
开发计划要求「点赞、收藏使用幂等写入」。两种契约形态对客户端的影响:
|
||
|
||
- **语义幂等(推荐)**:`PUT /posts/{id}/like` / `DELETE /posts/{id}/like`,重复调用收敛到同一终态、服务端返回权威 `{liked, likeCount}`。客户端**无需 Idempotency-Key**(PUT/DELETE 天然可安全重放,token 刷新后的自动重放也安全),补发/重点都不会重复计数——正是验收标准「重复点赞不重复计数」的最省事实现。
|
||
- **POST + Idempotency-Key**:若后端坚持 `POST /likes` 形态,客户端沿用 pets 先例(每轮逻辑操作换新键、刷新重放同键)。代价:toggle 语义下「点了又取消」是两个不同逻辑操作两个键,键管理与 §3.1 的意图合并叠加后复杂度明显更高。
|
||
|
||
跨端待拍板(§6-A),前端强烈建议前者。评论创建则相反:非幂等 POST,照 pets 四 POST 先例带 Idempotency-Key;**评论不做乐观插入**(发送中态 + 成功后插入服务端返回实体),回滚一条已渲染的评论气泡收益低、复杂度高,M3 不做。
|
||
|
||
### 3.3 测试清单
|
||
|
||
- Controller 单测:成功覆盖计数 / 失败恢复快照 / 在途连点只发一请求且完成后补发 / 补发目标与终态一致不再发 / 刷新代次不符丢弃响应 / reset 清状态。`FakeRepository` + `Completer` 控时序。
|
||
- Widget 测试:点击图标同帧变红计数 +1;失败回滚且 SnackBar 出现;连点若干次最终态正确。
|
||
|
||
## 4. 媒体客户端链路草案
|
||
|
||
### 4.1 依赖选型(新增依赖是 M3 最大的 pubspec 变更,逐项理由)
|
||
|
||
| 能力 | 推荐包 | 备选与理由 |
|
||
| --- | --- | --- |
|
||
| 图片选择 | `image_picker`(flutter.dev 官方维护,`pickMultiImage` 支持多选) | `wechat_assets_picker` 功能强但依赖重、维护面大;M3 用系统选择器足够 |
|
||
| 压缩 | `flutter_image_compress`(原生编解码,快;支持质量 + 尺寸重采样 + EXIF 方向自动矫正) | 纯 Dart 的 `image` 包在中端机上压一张 12MP 图秒级卡顿,排除 |
|
||
| 展示缓存 | `cached_network_image`(磁盘缓存) | Feed 无限流 + 反复滚动下 `Image.network` 仅内存缓存不可接受。**改造点集中在 `RemoteImage` 一处**,全仓受益,但需全局回归(pets 头像等) |
|
||
| 大图预览 | Flutter 内置 `InteractiveViewer`(零依赖,捏合缩放/平移够用) | `photo_view` 手势更全(双击缩放曲线、画廊),体验不满意再引,待拍板 §6-E |
|
||
|
||
压缩策略草案:长边 ≤2048 重采样 + JPEG 质量 80(Feed 场景肉眼无损、体积约降一个量级);`flutter_image_compress` 默认不保留 EXIF——**注意不要开 `keepExif`,顺带剥离 GPS 定位隐私**;`autoCorrectionAngle` 处理方向。九宫格缩略图靠 `cached_network_image` 的 resize 或后端缩略图 URL(依赖后端媒体方案给不给多尺寸,向后端提需求)。
|
||
|
||
### 4.2 上传进度与失败重试 UI
|
||
|
||
- 进度:dio 原生 `onSendProgress`,无需新依赖。
|
||
- 创作页九宫格每张图独立小状态机:`待传 → 压缩中 → 上传中(进度环) → 成功 / 失败(蒙层 + 点按重试)`;单图失败只重传该图。
|
||
- 发布 gating:全部图片成功(拿到 mediaId/URL)才允许提交发布;正文先行、图片后台传的「先发后补」模式 M3 不做。
|
||
|
||
### 4.3 预签名直传 vs 后端中转(客户端影响面对比)
|
||
|
||
| 维度 | 预签名直传 | 后端中转 |
|
||
| --- | --- | --- |
|
||
| 请求步数 | 两步:`POST /media`(取签名 URL)→ `PUT` 对象存储(+ 可能的 confirm 回调) | 一步 multipart POST |
|
||
| 网络层 | 需**另建一个裸 Dio**:对象存储不认 Bearer、响应不是业务信封,不能走 `ApiClient`/`AuthInterceptor` | 完全复用既有 `ApiClient`(鉴权/信封/40x 映射/刷新重放全白拿) |
|
||
| 错误处理 | 两段异构:取签名的业务错误 + 存储 PUT 的原始 HTTP 错误(含签名过期重取) | 一段,既有类型化异常分层 |
|
||
| 客户端成本 | 多约 1 个封装 + 裸 dio + 两段错误测试 | 最小 |
|
||
|
||
客户端两种都可行、成本差约一天。**解耦手段:先冻结 `MediaUploader` 抽象接口**(`Future<MediaRef> upload(XFile file, {void Function(double) onProgress})`),创作页只依赖接口,后端对象存储选型拍板后填实现——媒体不阻塞创作页开工。前端不对后端选型施加约束(§6-F)。
|
||
|
||
## 5. M2 遗留纳入评估
|
||
|
||
| 遗留项 | 内容 | 建议 |
|
||
| --- | --- | --- |
|
||
| T2-12 §8 三项交互 | 单宠直进/切换器、归档入口(依赖 listPets 对 archived 的过滤语义契约确认)、sterilizedOn 编辑 | **随 M3 消化**:三项都是 S 级、纯 pets 域文件,与社区工单零文件冲突,适合作为波次间隙的独立小工单;归档入口需后端先明确过滤语义 |
|
||
| 埋点队列完善(15 号 §4) | 30s 定时冲刷、指数退避(5s ×2 上限 5min)+ 429 按 Retry-After、`anonymousId`/`lastActiveAt` 持久化 | **必须随 M3 且排第一波**:社区事件量(feed 加载/点赞/发布)远超 pets,现状「4xx 整批永久丢弃 + 无定时冲刷」在高频事件下丢数风险放大;三项均不依赖社区契约,可与契约冻结完全并行。429 的 Retry-After 语义依赖后端限流落地,可先实现通用退避、Retry-After 留接线点。30s 定时器测试用 `fakeAsync`;anonymousId 落 `pb.analytics.lastActiveAt` 同款 shared_preferences 键位 |
|
||
|
||
另提醒数据侧:若 M3 要开 feed 曝光类事件(`post_impression`),事件量将冲击持久化队列 500 条上限,采样策略需在字典 v3 评审时一并定(§6-H)。
|
||
|
||
## 6. 权衡与待拍板
|
||
|
||
| # | 议题 | 选项 | 推荐 |
|
||
| --- | --- | --- | --- |
|
||
| A | 点赞/收藏幂等形态(跨端契约) | ① `PUT/DELETE /posts/{id}/like` 语义幂等;② `POST` + Idempotency-Key | **①**。客户端免键管理、重放天然安全、服务端回权威计数即满足「重复点赞不重复计数」(§3.2) |
|
||
| B | 并发点击策略 | ① 在途忽略点击;② 单飞 + 最终意图合并(最多补发一次);③ 300ms debounce 后发 | **②**(§3.1)。①在快速「点了又取消」时 UI 与服务端脱节;③延迟真实提交、时序更难测 |
|
||
| C | 下拉刷新语义 | ① 从头拉第一页整体替换;② 增量 prepend + 新内容提示 | **①**。②需要 since 游标语义与去重合并,M3 收益不匹配 |
|
||
| D | Feed 只读冷启动缓存(首页 JSON 落盘先渲染) | ① 做;② 纯在线 + 四态 | **②**。M2 pets 最终拍板即纯在线(22 号:服务端唯一事实源);M3 新面已大,缓存一致性(点赞态陈旧)另添课题,留 M4+ 评估 |
|
||
| E | 大图预览 | ① `InteractiveViewer` 内置;② `photo_view` | **①**,体验不达再升级,少一个依赖 |
|
||
| F | 媒体上传通道 | ① 预签名直传;② 后端中转 | 前端**跟随后端选型**,两案成本差约 1 天;`MediaUploader` 接口先冻结解耦(§4.3) |
|
||
| G | M2 遗留纳入波次 | 见 §5 | 埋点队列第一波必做;T2-12 三项作间隙工单 |
|
||
| H | `post_impression` 曝光事件是否 M3 开报 | 归数据侧 | 若开报须定采样,且以 §5 队列完善为前置 |
|
||
|
||
## 7. 风险与依赖小结
|
||
|
||
1. **社区契约是关键路径**:openapi 尚无任何社区路径(Feed 游标信封、点赞返回体、媒体接口、评论分页);前端第一波可并行做:埋点队列三项、`FeedController`/`ToggleSync` 状态机 + 假仓实现、`RemoteImage` 缓存化改造、创作页九宫格 UI。
|
||
2. **点赞契约形态(§6-A)影响 §3 状态机的键管理分支**,建议契约评审最先拍这一项。
|
||
3. **M3/M4 边界**:create_page 的 AI 生成模拟必须原样保留(属 M4),M3 只替换发布落库半边——工单里写明改动边界,避免顺手清理越界。
|
||
4. **`RemoteImage` 缓存化波及全仓图片**,改动一处但回归面全局,建议独立小工单先行合入。
|
||
5. 关注/话题在开发计划 M3 条目内,但现状 demo 只有详情页一个孤立关注按钮、无关注流/话题页——范围裁剪归 PM 工单拆解,本评估未按全量规划。
|
||
6. 本评估未改任何生产代码;测试基线 272 全绿已复验。
|
||
|
||
---
|
||
**Frontend Developer** · 2026-09-08 · patbond-flutter dev@720865b
|