- 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>
8.7 KiB
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 先例)。
验证命令(仓库根目录执行):
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