f5457c2f4c
CI / docs-build (push) Successful in 2m2s
- 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>
159 lines
8.7 KiB
Markdown
159 lines
8.7 KiB
Markdown
# 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
|