Files
patbond-doc/docs/development/iterations/iteration-3/21-community-datalayer.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

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: truestatus: 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