Files
patbond-doc/docs/development/iterations/iteration-3/11-community-contract-draft.md
T
lixi f17e6f1215
CI / docs-build (push) Successful in 34s
docs: M3 第一波收口——报告 09~14 与契约草案入档挂导航
- 09 V5+community 骨架(191→206)、10 埋点队列三项(272→286)、
  11 契约草案(13 路径/19 操作)、12 防泄漏三仓落地、
  13 media MinIO 闭环 + auth 契约测试(→226,抓修 1 漂移)、14 收口总表
- backend-modules.md 更新五模块/六容器口径
- 媒体凭据形态已定型(契约冻结输入),剩余待定型点在 T3-04/05

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 17:13:49 +08:00

135 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M3 community/media 域契约草案说明(T3-10 起草态)
> 作者:API 契约工程师
> 日期:2026-09-08
> 状态:**草案(DRAFT)——非冻结稿**。冻结须待 T3-03(媒体凭据)/T3-04(权限与错误语义)/T3-05(Feed 卡片)定型回填,按 M2 迭代式冻结流程升版 v1.3.0 合入 `docs/api/openapi.yaml` 并同步 api 侧字节级快照。本文与草案文件均不触碰正典 openapi.yaml。
> 草案文件:`openapi-community-draft.yaml`(同目录,独立可解析,13 路径 / 19 操作)
> 依据:iteration-3/01T3-03~09 端点定义与 T3-10 规范)、iteration-3/02(表结构、错误码段、media 状态机)、ADR-016~021、`docs/api/openapi.yaml` v1.2.0 通用约定
## 0. 字段正典基准声明
**T3-01 的 Flyway V5 迁移尚未推送 dev**(起草时 patbond-api 迁移链仅 V1~V4),本草案以 `docs/database/patbond_postgresql.sql` 的 community schema718~875 行)与 media.assets272~331 行)为字段正典。V5 落地后若与 bootstrap 有差异(预期仅两处:剪 `generation_job_id` 外键为裸列、主键默认值改应用侧 UUIDv7,均不影响契约面),以 V5 为准复核本草案。
## 1. 端点清单(13 路径 / 19 操作)
| # | 端点 | 操作 | 对应工单 | 说明 |
| --- | --- | --- | --- | --- |
| 1 | `POST /api/v1/media/uploads` | 1 | T3-03 | 登记 asset + 签发预签名 PUT 凭据(201 |
| 2 | `POST /api/v1/media/uploads/{assetId}/complete` | 1 | T3-03 | HEAD 校验后 uploading→ready200,幂等重复确认返回同 asset) |
| 3 | `POST /api/v1/posts` | 1 | T3-04 | 创建草稿或直接发布;Idempotency-Key 必带 |
| 4 | `/api/v1/posts/{postId}` | GET/PATCH/DELETE | T3-04 | 详情 / 编辑与发布(version 乐观锁)/ 软删 |
| 5 | `GET /api/v1/me/posts` | 1 | T3-04 | 我的帖子(含草稿),`(created_at,id)` 游标,status 过滤 |
| 6 | `GET /api/v1/feed` | 1 | T3-05 | 公共 Feed`(published_at,id)` 游标,谓词=ix_posts_feed |
| 7 | `/api/v1/posts/{postId}/comments` | GET/POST | T3-07 | 评论列表(游标)/ 创建(幂等 + replyToUserId |
| 8 | `DELETE /api/v1/comments/{commentId}` | 1 | T3-07 | 顶层短路径(pets 域先例),仅评论作者 |
| 9 | `/api/v1/posts/{postId}/like` | PUT/DELETE | T3-06 | 语义幂等,响应回 `{liked, likeCount}` 权威态 |
| 10 | `/api/v1/posts/{postId}/bookmark` | PUT/DELETE | T3-06 | 同构,`{bookmarked, bookmarkCount}` |
| 11 | `GET /api/v1/me/bookmarks` | 1 | T3-06 | 收藏列表,`(bookmarks.created_at, post_id)` 游标,项复用 FeedCard |
| 12 | `/api/v1/users/{userId}/follow` | PUT/DELETE | T3-08 | 语义幂等;自关注 422/42204 |
| 13 | `GET /api/v1/users/{userId}/follow-stats` | 1 | T3-08 | 计数 + followedByMeADR-018「最小接口 + 数量」口径) |
裁剪不出现(与 ADR-018 对齐):话题全部端点(T3-09 条件单未启)、关注/粉丝**列表**(最小接口仅留 follow/unfollow + 计数,列表需时纯增量补)、作者主页 `GET /users/{userId}/posts`M3 工单未列)、`region`/`generationJob`/`visibility=followers|private` 字段整体不出现(ADR-010「裁剪字段整体不出现,后续按新增可选字段补入」先例)。
## 2. 设计决策记录
1. **幂等按域(ADR-019**:二元互动(like/bookmark/followPUT/DELETE 语义幂等,重复调用返回 200 同一权威终态(非 409)——复合主键即幂等键,无键管理;创建型(发帖/评论)`Idempotency-Key` **必带**(与 pets 域「可选、≤255、不比对请求体」刻意不同:本域 ≤128 对齐表列宽,且比对 request_hash,不符 40905)。差异已在草案头参数描述中显式声明,防止 SDK/客户端按 pets 惯例误用。
2. **写响应携带权威终态**like/bookmark 回 `{liked|bookmarked, count}`follow 回 `{following, followerCount}`——iteration-3/02 §7.5 的乐观更新对账契约,客户端回滚=用响应覆盖本地值。
3. **防枚举 404 沿 pets 先例并分域给码**:帖子(40403,合并不存在/软删/hidden/他人 draft)、评论(40404)、asset(40405,合并非本人所有)、用户(40406)。403/40301 只发给「可见但无权」的调用者。
4. **发布即状态迁移**:不设独立 `/publish` 端点,`PATCH {status: published}` 是唯一开放迁移(draft→published),与 pets 域「状态流转走 PATCH」惯例一致,少一个端点少一处幂等语义。
5. **PATCH media 整组替换**(草案态):部分更新语义下图片增删排序的逐项 diff 契约复杂且易错,草案取「media 字段出现即全量替换」,随 T3-04 实现定型。
6. **AuthorSummary 服务端回退**:nickname 为空时由服务端回退 username,required 非空——客户端不做拼装(R10「缺失字段不留本地拼凑」);取数为 community 跨 schema 只读 identityADR-017),契约面不感知取数方式。
7. **列表信封零新形态**:全部列表复用 v1.2.0 cursor 分页正典 `{items, nextCursor, hasMore}`limit 1~100 缺省 20,游标不透明;每列表排序键与支撑索引在 description 中逐一写死(iteration-3/02 §7.3 对照)。
8. **complete 幂等语义**:重复 complete 已 ready 的 asset 返回 200 同 asset(客户端弱网重试友好);failed/deleted 态 422/42205——比「非 uploading 一律拒」多保留一条安全重试路径。
## 3. 与 bootstrap SQL 的字段对照
### 3.1 media.assets → MediaAsset / CreateMediaUploadRequest
| DB 列 | 契约字段 | 说明 |
| --- | --- | --- |
| id | id / assetId | UUID 字符串 |
| kind | kind | 契约 M3 仅 `image`DB CHECK 含 video/document,读侧枚举预留) |
| purpose | purpose | 白名单草案仅 `post_image`TODO-FREEZE #1 |
| mime_type | mimeType | 白名单草案 jpeg/png/webpTODO-FREEZE #1 |
| byte_size | byteSize | 创建时声明,complete 实测比对;上限草案 10 MiBTODO-FREEZE #1 |
| sha256 (bytea) | sha256 | 契约为 64 位小写 hex 字符串,可选 |
| width_px / height_px | widthPx / heightPx | complete 后回填,可空 |
| status | status | 契约仅露 uploading/ready/faileddeleted 恒 404 |
| ready_at / created_at | readyAt / createdAt | ISO 8601 |
| bucket / object_key / storage_type / duration_ms / external_url / owner_user_id | **不出现** | 存储内部细节不进契约;owner 由 token 隐含;duration 视频后置 |
### 3.2 community.posts → Post / CreatePostRequest / UpdatePostRequest
| DB 列 | 契约字段 | 说明 |
| --- | --- | --- |
| id / author_user_id | id / author(AuthorSummary) | 作者展开为公开摘要,不露裸 authorUserId(含在 author.userId |
| pet_id | petId | 可空 |
| category | category | 写侧 enum [general, help]ai_creation M4 预留只读) |
| title / content | title / content | 长度约束与 ck_posts_title/content 同宽(1~120 / 1~10000 |
| status | status | 契约露 draft/publishedhidden/archived 不开放(D3-7),草案对作者也不露 |
| visibility | visibility | M3 恒 `public`ADR-018DB 三值保留) |
| like/comment/bookmark_count | 同名 camelCase | int64 |
| idempotency_key / request_hash | Idempotency-Key 头 | 不进 body;≤128 对齐列宽 |
| published_at / created_at / updated_at / version | 同名 camelCase | version 进 PATCH 请求体(必带) |
| deleted_at | **不出现** | 软删即 404 |
| generation_job_id / region_id / location_text_snapshot | **不出现** | M4/M5 裁剪(V5 剪外键,ADR-018 |
| (关联)post_likes/post_bookmarks 行 | likedByMe / bookmarkedByMe | 批量查询组装,required |
### 3.3 community.post_media → PostMediaItem / PostMediaAttachRequest
| DB 列 | 契约字段 | 说明 |
| --- | --- | --- |
| asset_id / position / is_cover / caption | assetId / position / isCover / caption | position 0~8(≤9 图,D3-4);isCover 至多一(uq_post_media_cover),全 false 服务端取 position 0 |
| — | url / widthPx / heightPx | 响应侧由 asset 展开,免客户端二次请求 |
### 3.4 community.comments → Comment / CreateCommentRequest
| DB 列 | 契约字段 | 说明 |
| --- | --- | --- |
| id / post_id | id / postId | — |
| author_user_id / reply_to_user_id | author / replyToUser(均 AuthorSummary | 请求侧 replyToUserId 裸 UUID |
| content | content | 1~2000 同宽 |
| client_request_id / request_hash | Idempotency-Key 头 | 落 client_request_id 列 |
| status / deleted_at / updated_at | **不出现** | deleted/hidden 过滤在列表外;契约无评论编辑,不露 updatedAt |
### 3.5 post_likes / post_bookmarks / user_follows
关系行不作为资源暴露,仅以 `likedByMe`/`bookmarkedByMe`/`following`/`followedByMe` 布尔态与计数出现;复合主键 = PUT/DELETE 幂等的实现本体(`ON CONFLICT DO NOTHING` + 同事务计数增减)。`ck_user_follows_self` → 422/42204。
## 4. TODO-FREEZE 清单(PM 三处 + 补充一处)
草案 YAML 内共 10 处 `# TODO-FREEZE` 标注,归并为 4 个待定型点:
| # | 待定型点 | 等待 | 草案内位置 | 草案预设 |
| --- | --- | --- | --- | --- |
| 1 | **媒体凭据形态**PM 列①):uploadUrl 签名形态、requiredHeaders 键集、TTL、读取侧 URL(公共读稳定 URL vs 签名读);连带 purpose/mime 白名单与大小上限数值 | T3-03 | `createMediaUpload``MediaUploadCredentials``MediaAsset.url``CreateMediaUploadRequest` 三字段 | 预签名 PUT + TTL 10 分钟 + 10 MiB + jpeg/png/webp + purpose 仅 post_image |
| 2 | **Feed 卡片字段**PM 列②):contentPreview 截断规则、coverImage 选取规则、是否需 mediaCount 外的图列表 | T3-05 | `getFeed``FeedCard`;收藏列表「已删帖静默剔除 vs 占位」联动 | 200 字符截断 + isCover→position 0 + 仅封面一图 + mediaCount |
| 3 | **作者公开资料形态**(PM 列③,D3-9 方案 B 预设字段) | T3-05 | `AuthorSummary` | userId + nickname(服务端回退 username+ avatarUrl 可空;bio/username 露出与注销墓碑待定 |
| 4 | **权限矩阵与错误语义边界**(补充,冻结条件之一) | T3-04 | `getPost``UpdatePostRequest.media` 整组替换语义、作者视角 hidden 露出 | 403/404 边界按 §2-3 草案;media 整组替换 |
收敛期限沿 PM 要求:第二波中期。冻结时逐项回填、删除标注、升版 v1.3.0、同步 api 侧字节级快照。
## 5. 错误码段草案(新增 9 码,延续既有分段不重编号)
| 业务码 | HTTP | 稳定名 | 场景 |
| --- | --- | --- | --- |
| 40301 | 403 | POST_ACCESS_DENIED | 可见但无权操作(改删他人帖/评论) |
| 40403 | 404 | POST_NOT_FOUND | 不存在/软删/hidden/不可见,防枚举合并 |
| 40404 | 404 | COMMENT_NOT_FOUND | 评论不存在/已删/所属帖不可见 |
| 40405 | 404 | MEDIA_NOT_FOUND | asset 不存在或非本人所有 |
| 40406 | 404 | USER_NOT_FOUND | 关注目标用户不存在/已注销(**草案新增**,02 号报告未列) |
| 40905 | 409 | IDEMPOTENCY_PAYLOAD_MISMATCH | 同键不同 payloadrequest_hash 不符) |
| 42203 | 422 | MEDIA_NOT_READY | 引用非 ready 的 asset |
| 42204 | 422 | FOLLOW_RULE_VIOLATION | 自关注(**草案新增** |
| 42205 | 422 | MEDIA_UPLOAD_STATE_INVALID | complete 时 asset 非 uploading(幂等 ready 除外)(**草案新增** |
复用既有码:40000(参数校验,含 mime/大小白名单拒绝、游标非法、limit 越界、Idempotency-Key 缺失/超长、非法状态迁移)、40101token)、40401petId 引用不可见宠物,沿 pets 域语义)、40902version 冲突)、50000/50300。与 iteration-3/02 §6 六码草案的差异:新增 40406/42204/42205 三码(关注与 complete 状态机在 02 号报告端点表中有行为但无码位),冻结评审时定夺。
## 6. 冻结前必办事项(交接给冻结时点)
1. T3-01 V5 推送后与 bootstrap 复核一遍字段对照(§0)。
2. 四个 TODO-FREEZE 点逐项回填(§4),删除全部标注。
3. 错误码段三枚草案新增码(40406/42204/42205)评审定夺。
4. 合入正典 openapi.yaml:升版 1.3.0、错误码表并入 info 头、servers 增 :8084、tags 并入;`mkdocs build --strict` + api 侧字节级快照同步。
5. Idempotency-Key「必带 + 比对 hash + ≤128」与 pets 域差异在正典 info 头「通用约定」中显式成文。