Files
patbond-doc/docs/development/iterations/iteration-3/openapi-community-draft.yaml
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

1380 lines
48 KiB
YAML
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.
openapi: 3.0.3
info:
title: Patbond API — Community / Media 域契约草案(T3-10 起草态,非冻结稿)
version: 1.3.0-draft.1
description: |
M3 社区迭代 community + media 域契约**草案**T3-10 第一波起草态)。
本文件独立可解析,仅供评审与实现对照;**不是** `docs/api/openapi.yaml` 的一部分,
冻结合入须待 T3-03/04/05 定型回填后按 M2 迭代式冻结流程升版 v1.3.0。
## 沿用正典约定(openapi.yaml v1.2.0 零偏差)
- 统一前缀 `/api/v1`JSON 一律 camelCaseID 为 UUID 字符串。
- 时间字段 ISO 8601 带时区偏移(timestamptz)。
- 统一信封 `{"code": 0, "message": "success", "data": …}`;错误同时带正确 HTTP
状态码与稳定业务码,业务码永不复用或改号。
- cursor 分页正典:`data: {items, nextCursor, hasMore}``limit` 1~100 缺省 20
游标不透明,客户端不得解析;禁 OFFSET。
- 创建操作返回 201(pets 域先例);全部端点强制 Bearer 鉴权,无匿名端点。
## 本域新增约定(草案)
- **幂等按域(ADR-019)**:二元互动(点赞/收藏/关注)用 PUT/DELETE 语义幂等,
重复调用返回同一权威终态(200,非 409),无键管理;创建型写入(发帖/评论/
媒体登记)`Idempotency-Key` 头**必带**,落表内幂等列并比对 request_hash
(请求体规范化 SHA-256),同键不同 payload 返回 409/40905。
与 pets 域「可选键、不比对请求体」并存,pets 不回改。
- **媒体两步上传(ADR-016**:创建 upload(登记 asset + 签发 MinIO 预签名 PUT
凭据)→ 客户端直传 → complete 确认(服务端 HEAD 校验后 uploading→ready)。
业务引用只接受本人所有且 `status='ready'` 的 asset,否则 422/42203。
- **防枚举 404**:帖子不存在 / 已软删 / hidden / 他人 draft 或 private 响应完全一致
(40403);评论、asset、用户同理(40404/40405/40406)。
- **写响应携带权威终态**like/bookmark/follow 响应回 `{…State, …Count}`
客户端乐观更新以响应对账回滚。
## community/media 域错误码段(草案,待 T3-04 定型冻结)
| 业务码 | HTTP | 场景 |
| --- | --- | --- |
| 40301 | 403 | POST_ACCESS_DENIED:帖子/评论可见但无权操作(如改删他人帖) |
| 40403 | 404 | POST_NOT_FOUND:帖子不存在、已删、hidden 或对调用者不可见(防枚举合并) |
| 40404 | 404 | COMMENT_NOT_FOUND:评论不存在、已删或所属帖子不可见 |
| 40405 | 404 | MEDIA_NOT_FOUNDasset 不存在或非本人所有(防枚举合并) |
| 40406 | 404 | USER_NOT_FOUND:目标用户不存在或已注销(关注/计数端点) |
| 40905 | 409 | IDEMPOTENCY_PAYLOAD_MISMATCH:同 Idempotency-Key 不同 payloadrequest_hash 不符) |
| 42203 | 422 | MEDIA_NOT_READY:引用了非 ready 状态或超挂接窗口的 asset |
| 42204 | 422 | FOLLOW_RULE_VIOLATION:自关注(库层 ck_user_follows_self 兜底) |
| 42205 | 422 | MEDIA_UPLOAD_STATE_INVALIDcomplete 时 asset 非 uploading 态(幂等重复 ready 除外) |
复用既有码:40000 参数校验、40101 token、40902 VERSION_CONFLICT、50000/50300。
servers:
- url: http://127.0.0.1:8082
description: patbond-usermedia 上传流程,ADR-017
- url: http://127.0.0.1:8084
description: patbond-community(社区域全部端点,ADR-017
security:
- bearerAuth: []
tags:
- name: media
description: 媒体上传两步流程(patbond-userADR-016 MinIO 预签名直传)
- name: posts
description: 帖子生命周期:草稿/编辑/发布/删除/详情/我的帖子(patbond-community
- name: feed
description: 公共 Feed 游标分页(patbond-community
- name: comments
description: 单层平铺评论 + @ 回复(patbond-communityADR-018
- name: interactions
description: 点赞/收藏 PUT+DELETE 幂等与收藏列表(patbond-communityADR-019
- name: follows
description: 关注最小数据接口:follow/unfollow + 计数(patbond-communityADR-018
paths:
/api/v1/media/uploads:
post:
tags: [media]
summary: 创建上传(登记 asset 并签发预签名直传凭据)
description: |
两步上传第一步:校验 mime 白名单与大小上限 → 写 `media.assets` 行
status=uploadingbucket/objectKey 服务端生成、不含用户输入)→
返回预签名 PUT 凭据(短 TTL)。客户端凭凭据直传 MinIO,不经应用服务器。
M3 仅 `kind=image`ADR-018 视频后置)。
# TODO-FREEZE: 等待 T3-03 —— purpose 白名单、mime 白名单、单文件大小上限数值定型
operationId: createMediaUpload
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateMediaUploadRequest'
responses:
'201':
description: asset 已登记(uploading),返回直传凭据
content:
application/json:
schema:
$ref: '#/components/schemas/MediaUploadEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
/api/v1/media/uploads/{assetId}/complete:
post:
tags: [media]
summary: 确认上传完成(uploading → ready
description: |
两步上传第二步:服务端对对象 HEAD 校验存在性与 byteSize(有 sha256 则一并核)
→ status=ready、写 readyAt,返回可引用的 asset。校验失败置 failed。
对已 ready 的 asset 重复 complete 幂等返回同一 asset200 语义下仍走 201?——
草案取 200 返回既有 ready asset);对 failed/deleted 态返回 422/42205。
operationId: completeMediaUpload
parameters:
- $ref: '#/components/parameters/AssetIdParam'
responses:
'200':
description: 确认成功(或幂等重复确认),asset 为 ready
content:
application/json:
schema:
$ref: '#/components/schemas/MediaAssetEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/MediaNotFound'
'422':
description: asset 非 uploading 态,或对象校验失败已置 failedcode 42205
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
stateInvalid:
value: { code: 42205, message: 上传状态不允许确认, data: null }
/api/v1/posts:
post:
tags: [posts]
summary: 创建帖子(草稿或直接发布)
description: |
`Idempotency-Key` **必带**(开发计划 6.1 强制名单含帖子):落
`uq_posts_author_idempotency`,同键重试返回首次创建的帖子(同样 201);
同键不同 payload 返回 409/40905request_hash 比对,ADR-019)。
`status` 可 draft(缺省)或 published(直接发布,服务端写 publishedAt)。
media 挂接只接受本人所有且 ready 的 asset(否则 422/42203),每帖 ≤9 图
D3-4),position 0 起连续,isCover 至多一个(缺省取 position 0)。
纯文字帖合法(media 可空数组或缺席)。
operationId: createPost
parameters:
- $ref: '#/components/parameters/IdempotencyKeyRequiredHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePostRequest'
responses:
'201':
description: 创建成功(或同键幂等重试返回首次结果)
content:
application/json:
schema:
$ref: '#/components/schemas/PostEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'409':
$ref: '#/components/responses/IdempotencyPayloadMismatch'
'422':
$ref: '#/components/responses/MediaNotReady'
/api/v1/posts/{postId}:
get:
tags: [posts]
summary: 帖子详情
description: |
published 对可见者开放;draft/hidden 仅作者可见,他人 404/40403 防枚举。
响应含 likedByMe/bookmarkedByMe 与作者公开摘要。
# TODO-FREEZE: 等待 T3-04 —— 权限矩阵与 403/404 边界语义随实现定型
operationId: getPost
parameters:
- $ref: '#/components/parameters/PostIdParam'
responses:
'200':
description: 帖子详情
content:
application/json:
schema:
$ref: '#/components/schemas/PostEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PostNotFound'
patch:
tags: [posts]
summary: 编辑帖子 / 发布草稿(部分更新 + version 乐观锁)
description: |
仅作者(非作者对可见帖 403/40301,不可见帖 404/40403)。PATCH 部分更新惯例:
缺席字段不变,不支持清空回 null(M2 先例)。`version` 必带(缺失 400/40000
过期 409/40902)。发布 = `status: published` 的状态迁移(draft→published
服务端写 publishedAt,校验 ck_posts_publish_state);published→draft 不支持;
hidden/archived 不开放任何端点(D3-7 运营位保留)。
operationId: updatePost
parameters:
- $ref: '#/components/parameters/PostIdParam'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdatePostRequest'
responses:
'200':
description: 更新成功,返回新 version 的完整帖子
content:
application/json:
schema:
$ref: '#/components/schemas/PostEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PostAccessDenied'
'404':
$ref: '#/components/responses/PostNotFound'
'409':
$ref: '#/components/responses/VersionConflict'
'422':
$ref: '#/components/responses/MediaNotReady'
delete:
tags: [posts]
summary: 删除帖子(软删,仅作者)
description: |
软删(deleted_at),删除后详情/Feed/列表一律不可见(M3 验收标准四)。
重复删除返回 404/40403(已删与不存在合并防枚举)。
operationId: deletePost
parameters:
- $ref: '#/components/parameters/PostIdParam'
responses:
'200':
description: 删除成功
content:
application/json:
schema:
$ref: '#/components/schemas/VoidEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PostAccessDenied'
'404':
$ref: '#/components/responses/PostNotFound'
/api/v1/me/posts:
get:
tags: [posts]
summary: 我的帖子列表(含草稿)
description: |
作者视角:含 draft 与 published(软删不含)。排序 `(created_at DESC, id DESC)`
走 `ix_posts_author_created` 游标。`status` 过滤可选(draft|published)。
operationId: listMyPosts
parameters:
- $ref: '#/components/parameters/PageLimitParam'
- $ref: '#/components/parameters/PageCursorParam'
- name: status
in: query
required: false
schema:
type: string
enum: [draft, published]
description: 按状态过滤;缺省返回全部(不含已删)
responses:
'200':
description: cursor 分页帖子列表(完整 Post 形态)
content:
application/json:
schema:
$ref: '#/components/schemas/PostListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
/api/v1/feed:
get:
tags: [feed]
summary: 公共 Feed(游标分页)
description: |
谓词恒为 `status='published' AND visibility='public' AND deleted_at IS NULL`
与 `ix_posts_feed` 部分索引一致;复合游标 `(published_at DESC, id DESC)`
keyset 翻页不丢不重,禁 OFFSET。删除/hidden 帖子下一次请求即不可见。
likedByMe/bookmarkedByMe 批量查询(避免 N+1)。
# TODO-FREEZE: 等待 T3-05 —— Feed 卡片字段定型:content 摘要截断规则、
# 封面选取规则(isCover 优先→position 0 兜底)、计数口径逐项写死
operationId: getFeed
parameters:
- $ref: '#/components/parameters/PageLimitParam'
- $ref: '#/components/parameters/PageCursorParam'
responses:
'200':
description: cursor 分页 Feed 卡片列表
content:
application/json:
schema:
$ref: '#/components/schemas/FeedListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
/api/v1/posts/{postId}/comments:
get:
tags: [comments]
summary: 评论列表(单层平铺,游标分页)
description: |
排序 `(created_at DESC, id DESC)` 走 `ix_comments_post_created`
仅 status=visible;帖子不可见则 404/40403。
operationId: listComments
parameters:
- $ref: '#/components/parameters/PostIdParam'
- $ref: '#/components/parameters/PageLimitParam'
- $ref: '#/components/parameters/PageCursorParam'
responses:
'200':
description: cursor 分页评论列表
content:
application/json:
schema:
$ref: '#/components/schemas/CommentListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PostNotFound'
post:
tags: [comments]
summary: 创建评论(幂等 + 可选 @ 回复)
description: |
`Idempotency-Key` **必带**,落 `client_request_id + request_hash`
uq author×client_request_id),同键重试返回首条评论(同样 201),
payload 不符 409/40905。`replyToUserId` 可选 @ 回复(单层,无楼中楼,
ADR-018)。帖子不可见则 404/40403。comment_count 同事务 +1。
operationId: createComment
parameters:
- $ref: '#/components/parameters/PostIdParam'
- $ref: '#/components/parameters/IdempotencyKeyRequiredHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateCommentRequest'
responses:
'201':
description: 创建成功(或同键幂等重试返回首次结果)
content:
application/json:
schema:
$ref: '#/components/schemas/CommentEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PostNotFound'
'409':
$ref: '#/components/responses/IdempotencyPayloadMismatch'
/api/v1/comments/{commentId}:
delete:
tags: [comments]
summary: 删除评论(仅评论作者;顶层短路径)
description: |
顶层短路径先例(pets 域子资源 PATCH 同理):commentId 全局唯一。
仅评论作者可删(status→deletedcomment_count 同事务 -1);帖主删他人评论
首版不做(D3-7)。不存在/已删/所属帖不可见合并 404/40404。
operationId: deleteComment
parameters:
- name: commentId
in: path
required: true
schema:
type: string
format: uuid
description: 评论 ID
responses:
'200':
description: 删除成功
content:
application/json:
schema:
$ref: '#/components/schemas/VoidEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PostAccessDenied'
'404':
$ref: '#/components/responses/CommentNotFound'
/api/v1/posts/{postId}/like:
put:
tags: [interactions]
summary: 点赞(PUT 语义幂等)
description: |
主键 (post_id, user_id) 即幂等键:`ON CONFLICT DO NOTHING`,仅实际插入才
like_count 同事务 +1。重复 PUT 返回 200 同一权威态(非 409),并发 N 次
计数恰为 1(M3 验收标准二)。帖子不可见 404/40403。
operationId: likePost
parameters:
- $ref: '#/components/parameters/PostIdParam'
responses:
'200':
description: 权威终态(liked 恒 true
content:
application/json:
schema:
$ref: '#/components/schemas/LikeStateEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PostNotFound'
delete:
tags: [interactions]
summary: 取消点赞(DELETE 语义幂等)
description: 取消不存在的点赞不报错不减计数,返回 200 权威态(liked 恒 false)。
operationId: unlikePost
parameters:
- $ref: '#/components/parameters/PostIdParam'
responses:
'200':
description: 权威终态(liked 恒 false
content:
application/json:
schema:
$ref: '#/components/schemas/LikeStateEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PostNotFound'
/api/v1/posts/{postId}/bookmark:
put:
tags: [interactions]
summary: 收藏(PUT 语义幂等,与点赞同构)
operationId: bookmarkPost
parameters:
- $ref: '#/components/parameters/PostIdParam'
responses:
'200':
description: 权威终态(bookmarked 恒 true
content:
application/json:
schema:
$ref: '#/components/schemas/BookmarkStateEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PostNotFound'
delete:
tags: [interactions]
summary: 取消收藏(DELETE 语义幂等)
operationId: unbookmarkPost
parameters:
- $ref: '#/components/parameters/PostIdParam'
responses:
'200':
description: 权威终态(bookmarked 恒 false
content:
application/json:
schema:
$ref: '#/components/schemas/BookmarkStateEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PostNotFound'
/api/v1/me/bookmarks:
get:
tags: [interactions]
summary: 我的收藏列表(游标分页)
description: |
排序 `(bookmarks.created_at DESC, post_id DESC)` 走
`ix_post_bookmarks_user_created`。被收藏帖子已删/hidden 时该项不返回
(草案取「静默剔除」,是否保留占位随 T3-05 联动定型)。项形态复用 Feed 卡片。
operationId: listMyBookmarks
parameters:
- $ref: '#/components/parameters/PageLimitParam'
- $ref: '#/components/parameters/PageCursorParam'
responses:
'200':
description: cursor 分页收藏卡片列表
content:
application/json:
schema:
$ref: '#/components/schemas/FeedListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
/api/v1/users/{userId}/follow:
put:
tags: [follows]
summary: 关注(PUT 语义幂等)
description: |
主键 (follower, followee) 幂等,重复 PUT 返回 200 权威态;自关注 422/42204
(库层 ck_user_follows_self 兜底);目标用户不存在/已注销 404/40406。
关注 Feed 与 followers 可见性不在 M3ADR-018 最小数据接口)。
operationId: followUser
parameters:
- $ref: '#/components/parameters/UserIdParam'
responses:
'200':
description: 权威终态(following 恒 true
content:
application/json:
schema:
$ref: '#/components/schemas/FollowStateEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/UserNotFound'
'422':
description: 自关注(code 42204
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
selfFollow:
value: { code: 42204, message: 不能关注自己, data: null }
delete:
tags: [follows]
summary: 取消关注(DELETE 语义幂等)
operationId: unfollowUser
parameters:
- $ref: '#/components/parameters/UserIdParam'
responses:
'200':
description: 权威终态(following 恒 false
content:
application/json:
schema:
$ref: '#/components/schemas/FollowStateEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/UserNotFound'
/api/v1/users/{userId}/follow-stats:
get:
tags: [follows]
summary: 关注计数(关注数/粉丝数/我是否已关注)
description: |
ADR-018 最小接口的「数量」端点:followerCount/followingCount 实时 COUNT
user_follows 双向索引支撑),followedByMe 为调用者视角。
查自己时 followedByMe 恒 false。关注/粉丝**列表**端点不在 M3 草案
(最小接口裁剪,需时按纯增量补入)。
operationId: getFollowStats
parameters:
- $ref: '#/components/parameters/UserIdParam'
responses:
'200':
description: 计数与关注状态
content:
application/json:
schema:
$ref: '#/components/schemas/FollowStatsEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/UserNotFound'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'Authorization: Bearer <accessToken>RS256 JWT'
parameters:
PostIdParam:
name: postId
in: path
required: true
schema:
type: string
format: uuid
description: 帖子 ID
AssetIdParam:
name: assetId
in: path
required: true
schema:
type: string
format: uuid
description: 媒体 asset ID
UserIdParam:
name: userId
in: path
required: true
schema:
type: string
format: uuid
description: 目标用户 ID
PageLimitParam:
name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
description: 每页条数(1~100,缺省 20);越界 400/40000
PageCursorParam:
name: cursor
in: query
required: false
schema:
type: string
description: 上一页返回的 nextCursor(不透明字符串,客户端不得解析),首页不传;无效 400/40000
IdempotencyKeyRequiredHeader:
name: Idempotency-Key
in: header
required: true
schema:
type: string
maxLength: 128
description: |
**必带**幂等键(1~128 字符,缺失/超长 400/40000;与 pets 域可选 255 字符
不同——本域按 ADR-019 落表内幂等列,列宽 128)。键按作者隔离;同键重试
返回首次创建的资源(同样 201);**比对 request_hash**:同键不同 payload
返回 409/40905。客户端每次逻辑提交换新键(建议 UUID),重试间保持不变。
responses:
ValidationError:
description: 参数校验失败(code 40000
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
validation:
value: { code: 40000, message: 参数校验失败, data: null }
AccessTokenInvalid:
description: access token 缺失、无效或过期(code 40101)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
tokenInvalid:
value: { code: 40101, message: token 无效或过期, data: null }
PostNotFound:
description: |
帖子不存在、已软删、hidden,或对调用者不可见(他人 draft/private)——
防枚举语义:全部情况响应完全一致(code 40403)。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
postNotFound:
value: { code: 40403, message: 帖子不存在, data: null }
CommentNotFound:
description: 评论不存在、已删或所属帖子不可见(code 40404,防枚举合并)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
commentNotFound:
value: { code: 40404, message: 评论不存在, data: null }
MediaNotFound:
description: asset 不存在或非本人所有(code 40405,防枚举合并)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
mediaNotFound:
value: { code: 40405, message: 媒体不存在, data: null }
UserNotFound:
description: 目标用户不存在或已注销(code 40406)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
userNotFound:
value: { code: 40406, message: 用户不存在, data: null }
PostAccessDenied:
description: |
对可见帖子/评论无权操作(code 40301):如编辑/删除他人已发布帖子。
仅发给对资源「可见」的调用者,不泄露新信息。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
accessDenied:
value: { code: 40301, message: 无权限执行该操作, data: null }
IdempotencyPayloadMismatch:
description: 同 Idempotency-Key 不同 payloadrequest_hash 不符(code 40905
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
mismatch:
value: { code: 40905, message: 幂等键已用于不同请求, data: null }
MediaNotReady:
description: 引用了非 ready 状态、非本人或不存在的 asset(存在性合并 40405,状态问题 code 42203
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
notReady:
value: { code: 42203, message: 媒体尚未就绪, data: null }
VersionConflict:
description: 乐观锁版本冲突(code 40902):提交的 version 已过期,刷新后重试
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
versionConflict:
value: { code: 40902, message: 数据已被修改,请刷新后重试, data: null }
schemas:
# ---------- 通用 ----------
VoidEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
nullable: true
example: null
ErrorEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
description: 稳定业务错误码(见顶部错误码段草案)
example: 40403
message:
type: string
example: 帖子不存在
data:
nullable: true
example: null
AuthorSummary:
type: object
description: |
作者公开摘要(D3-9 方案 B 预设字段:昵称 + 头像,community 跨 schema
只读 identity 取数,ADR-017)。nickname 为空时回退 username 由**服务端**
完成(客户端不做回退拼装);avatarUrl 为 ready 头像 asset 的访问 URL
无头像为 null(客户端出占位)。
# TODO-FREEZE: 等待 T3-05 —— 公开资料形态定型(是否补 bio/是否露 username、
# 注销用户的墓碑形态)
required: [userId, nickname]
properties:
userId:
type: string
format: uuid
nickname:
type: string
maxLength: 32
description: 昵称(空昵称已由服务端回退为 username)
example: 毛毛的铲屎官
avatarUrl:
type: string
nullable: true
description: 头像访问 URL;无头像为 null
example: http://127.0.0.1:9000/patbond-media/user_avatar/2026/09/018f...jpg
# ---------- media ----------
CreateMediaUploadRequest:
type: object
required: [kind, purpose, mimeType, byteSize]
properties:
kind:
type: string
enum: [image]
description: M3 仅 imageADR-018 视频后置;video/document 为向后新增枚举预留)
purpose:
type: string
enum: [post_image]
description: |
用途白名单,决定 objectKey 前缀。
# TODO-FREEZE: 等待 T3-03 —— 是否随 P6 扩 user_avatar/pet_avatar
mimeType:
type: string
enum: [image/jpeg, image/png, image/webp]
description: |
白名单外 400/40000。
# TODO-FREEZE: 等待 T3-03 —— 白名单与 HEIC 支持定型
byteSize:
type: integer
format: int64
minimum: 1
maximum: 10485760
description: |
声明的文件字节数,complete 时与对象实测比对。
# TODO-FREEZE: 等待 T3-03 —— 单文件上限数值(草案 10 MiB)定型
sha256:
type: string
pattern: '^[0-9a-f]{64}$'
description: 可选,小写 hex;提供则 complete 时一并核对
MediaUploadCredentials:
type: object
description: |
预签名直传凭据(MinIO PUT,短 TTLADR-016)。
# TODO-FREEZE: 等待 T3-03 —— 凭据形态整体定型:uploadUrl 是否含 query 签名、
# requiredHeaders 具体键集、TTL 数值、读取侧 URL 形态(公共读桶稳定 URL vs 签名读)
required: [assetId, uploadUrl, method, expiresAt]
properties:
assetId:
type: string
format: uuid
description: 已登记的 asset IDstatus=uploading
uploadUrl:
type: string
description: 预签名 PUT 目标(客户端直传,不经应用服务器)
method:
type: string
enum: [PUT]
requiredHeaders:
type: object
additionalProperties:
type: string
description: 直传请求必须原样携带的头(如 Content-Type),键集草案态
expiresAt:
type: string
format: date-time
description: 凭据过期时刻(草案 TTL 10 分钟);过期后须重新创建上传
MediaAsset:
type: object
required: [id, kind, purpose, mimeType, status, createdAt]
properties:
id:
type: string
format: uuid
kind:
type: string
enum: [image]
purpose:
type: string
example: post_image
mimeType:
type: string
example: image/jpeg
byteSize:
type: integer
format: int64
widthPx:
type: integer
nullable: true
heightPx:
type: integer
nullable: true
status:
type: string
enum: [uploading, ready, failed]
description: deleted 态对外恒 404/40405,不出现在响应
url:
type: string
nullable: true
description: |
访问 URL,仅 ready 态非空。
# TODO-FREEZE: 等待 T3-03 —— 公共读稳定 URL vs 签名读(随凭据形态一并定型)
readyAt:
type: string
format: date-time
nullable: true
createdAt:
type: string
format: date-time
MediaUploadEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
$ref: '#/components/schemas/MediaUploadCredentials'
MediaAssetEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
$ref: '#/components/schemas/MediaAsset'
# ---------- posts ----------
PostMediaItem:
type: object
description: 帖子挂接的一张图(响应形态)
required: [assetId, position, isCover, url]
properties:
assetId:
type: string
format: uuid
position:
type: integer
minimum: 0
maximum: 8
isCover:
type: boolean
url:
type: string
description: 图片访问 URL(形态随 T3-03 凭据侧一并定型)
widthPx:
type: integer
nullable: true
heightPx:
type: integer
nullable: true
caption:
type: string
nullable: true
maxLength: 300
PostMediaAttachRequest:
type: object
description: 帖子挂接的一张图(请求形态);asset 须本人所有且 ready,否则 422/42203
required: [assetId]
properties:
assetId:
type: string
format: uuid
position:
type: integer
minimum: 0
maximum: 8
description: 0 起连续;缺省按数组序
isCover:
type: boolean
default: false
description: 至多一个 trueuq_post_media_cover);全 false 则服务端取 position 0
caption:
type: string
maxLength: 300
CreatePostRequest:
type: object
required: [content]
properties:
title:
type: string
minLength: 1
maxLength: 120
description: 可选标题(ck_posts_title
content:
type: string
minLength: 1
maxLength: 10000
description: 正文,必填(ck_posts_content;纯文字帖合法,D3-4
category:
type: string
enum: [general, help]
default: general
description: ai_creation 为 M4 预留值,M3 不开放写入
status:
type: string
enum: [draft, published]
default: draft
description: published = 创建即发布(服务端写 publishedAt
petId:
type: string
format: uuid
description: 可选关联宠物;须为调用者可见宠物,否则 404/40401 语义沿 pets 域
media:
type: array
maxItems: 9
description: ≤9 图(D3-4);空数组或缺席 = 纯文字帖
items:
$ref: '#/components/schemas/PostMediaAttachRequest'
UpdatePostRequest:
type: object
required: [version]
description: |
部分更新:缺席字段不变;不支持清空回 null(M2 惯例)。media 若出现则**整组替换**
(position 全量重排,草案态,随 T3-04 定型)。
properties:
version:
type: integer
minimum: 0
description: 乐观锁,必带;过期 409/40902
title:
type: string
minLength: 1
maxLength: 120
content:
type: string
minLength: 1
maxLength: 10000
category:
type: string
enum: [general, help]
petId:
type: string
format: uuid
status:
type: string
enum: [published]
description: 唯一开放的状态迁移 draft→published(发布动作);其余迁移 400/40000
media:
type: array
maxItems: 9
items:
$ref: '#/components/schemas/PostMediaAttachRequest'
Post:
type: object
description: |
帖子完整形态(详情 / 我的帖子列表 / 写响应共用)。region/generationJob/topics
等裁剪字段整体不出现(ADR-018 + ADR-010 先例),后续按新增可选字段纯增量补入。
required:
- id
- author
- category
- content
- status
- visibility
- media
- likeCount
- commentCount
- bookmarkCount
- likedByMe
- bookmarkedByMe
- createdAt
- updatedAt
- version
properties:
id:
type: string
format: uuid
author:
$ref: '#/components/schemas/AuthorSummary'
petId:
type: string
format: uuid
nullable: true
category:
type: string
enum: [general, help, ai_creation]
description: ai_creation 仅读侧预留(M3 无法写入)
title:
type: string
nullable: true
maxLength: 120
content:
type: string
maxLength: 10000
status:
type: string
enum: [draft, published]
description: hidden/archived 对非作者恒 404;对作者是否露出运营态待 T3-04(草案不露)
visibility:
type: string
enum: [public]
description: M3 恒 publicADR-018followers/private 语义后置,字段保留)
media:
type: array
items:
$ref: '#/components/schemas/PostMediaItem'
likeCount:
type: integer
format: int64
commentCount:
type: integer
format: int64
bookmarkCount:
type: integer
format: int64
likedByMe:
type: boolean
bookmarkedByMe:
type: boolean
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
publishedAt:
type: string
format: date-time
nullable: true
description: 仅 published 非空
version:
type: integer
FeedCard:
type: object
description: |
Feed / 收藏列表卡片形态(较 Post 裁剪)。
# TODO-FREEZE: 等待 T3-05 —— 卡片字段整体定型:contentPreview 截断长度与规则、
# coverImage 选取规则(isCover→position 0)、是否带 mediaCount 之外的图列表
required:
- id
- author
- category
- contentPreview
- mediaCount
- likeCount
- commentCount
- bookmarkCount
- likedByMe
- bookmarkedByMe
- publishedAt
properties:
id:
type: string
format: uuid
author:
$ref: '#/components/schemas/AuthorSummary'
category:
type: string
enum: [general, help, ai_creation]
title:
type: string
nullable: true
contentPreview:
type: string
description: 正文摘要(服务端截断,规则待 T3-05 定型;草案 200 字符 + 完整边界截断)
coverImage:
nullable: true
allOf:
- $ref: '#/components/schemas/PostMediaItem'
description: 封面图;纯文字帖为 null
mediaCount:
type: integer
description: 帖子图片总数(卡片角标「1/9」类展示)
likeCount:
type: integer
format: int64
commentCount:
type: integer
format: int64
bookmarkCount:
type: integer
format: int64
likedByMe:
type: boolean
bookmarkedByMe:
type: boolean
publishedAt:
type: string
format: date-time
PostEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
$ref: '#/components/schemas/Post'
PostListEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
type: object
description: cursor 分页正典信封;排序 created_at DESC, id DESC
required: [items, hasMore]
properties:
items:
type: array
items:
$ref: '#/components/schemas/Post'
nextCursor:
type: string
nullable: true
description: 下一页游标(不透明 base64url),hasMore=false 时恒为 null
hasMore:
type: boolean
FeedListEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
type: object
description: |
cursor 分页正典信封;公共 Feed 排序 published_at DESC, id DESC
收藏列表排序 bookmarks.created_at DESC, post_id DESC
required: [items, hasMore]
properties:
items:
type: array
items:
$ref: '#/components/schemas/FeedCard'
nextCursor:
type: string
nullable: true
description: 下一页游标(不透明 base64url),hasMore=false 时恒为 null
hasMore:
type: boolean
# ---------- comments ----------
CreateCommentRequest:
type: object
required: [content]
properties:
content:
type: string
minLength: 1
maxLength: 2000
description: ck_comments_content 同宽
replyToUserId:
type: string
format: uuid
description: 可选 @ 回复目标(单层平铺,无 parentCommentIdADR-018
Comment:
type: object
required: [id, postId, author, content, createdAt]
properties:
id:
type: string
format: uuid
postId:
type: string
format: uuid
author:
$ref: '#/components/schemas/AuthorSummary'
replyToUser:
nullable: true
allOf:
- $ref: '#/components/schemas/AuthorSummary'
description: '@ 回复目标的公开摘要;非回复为 null'
content:
type: string
maxLength: 2000
createdAt:
type: string
format: date-time
CommentEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
$ref: '#/components/schemas/Comment'
CommentListEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
type: object
description: cursor 分页正典信封;排序 created_at DESC, id DESC
required: [items, hasMore]
properties:
items:
type: array
items:
$ref: '#/components/schemas/Comment'
nextCursor:
type: string
nullable: true
description: 下一页游标(不透明 base64url),hasMore=false 时恒为 null
hasMore:
type: boolean
# ---------- interactions / follows ----------
LikeState:
type: object
description: 点赞权威终态(乐观更新以此对账回滚)
required: [liked, likeCount]
properties:
liked:
type: boolean
likeCount:
type: integer
format: int64
BookmarkState:
type: object
description: 收藏权威终态(与点赞同构)
required: [bookmarked, bookmarkCount]
properties:
bookmarked:
type: boolean
bookmarkCount:
type: integer
format: int64
FollowState:
type: object
description: 关注权威终态;followerCount 为目标用户的粉丝数
required: [following, followerCount]
properties:
following:
type: boolean
followerCount:
type: integer
format: int64
FollowStats:
type: object
required: [followerCount, followingCount, followedByMe]
properties:
followerCount:
type: integer
format: int64
description: 目标用户的粉丝数
followingCount:
type: integer
format: int64
description: 目标用户关注的人数
followedByMe:
type: boolean
description: 调用者是否已关注目标用户;查自己恒 false
LikeStateEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
$ref: '#/components/schemas/LikeState'
BookmarkStateEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
$ref: '#/components/schemas/BookmarkState'
FollowStateEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
$ref: '#/components/schemas/FollowState'
FollowStatsEnvelope:
type: object
required: [code, message, data]
properties:
code: { type: integer, enum: [0] }
message: { type: string, example: success }
data:
$ref: '#/components/schemas/FollowStats'