From f848476c169af049916a0d0fc582aa7d0050c782 Mon Sep 17 00:00:00 2001 From: Lixi20 Date: Wed, 9 Sep 2026 11:12:50 +0800 Subject: [PATCH] =?UTF-8?q?docs(api):=20M3=20=E5=A5=91=E7=BA=A6=E5=86=BB?= =?UTF-8?q?=E7=BB=93=20v1.3.0=E2=80=94=E2=80=94community/media=20=E5=9F=9F?= =?UTF-8?q?=E5=90=88=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按第二波定型表(iteration-3 报告 13/15/16/17)将 community/media 域草案 合入正典 openapi.yaml,1.2.0 → 1.3.0: - 新增 13 路径 / 19 操作(媒体两步上传、帖子生命周期、公共 Feed、 单层评论、点赞/收藏/关注最小接口),正典总量 31 路径 / 43 操作 - 新增 27 schemas / 4 参数 / 7 响应组件;错误码表补 9 码 (40301/40403/40404/40405/40406/40905/42203/42204/42205) - info 头新增「Community / Media 域约定」:Idempotency-Key 必带 + 规范化 request_hash 比对(与 pets 域差异成文)、私有桶 + 时效性 预签名 GET 读取语义、防枚举码族、互动面=帖子公开面 - 草案 10 处 TODO-FREEZE 全部回填删除;26 项草案→冻结修正照单全收 (对照见 iteration-3/18 冻结报告,波末入档) - index.md 端点清单同步;servers 增 :8084、tags 并入 6 个 api 侧字节级快照同步为硬依赖,由后续 api 侧工单执行。 Co-Authored-By: Claude Fable 5 --- docs/api/index.md | 14 +- docs/api/openapi.yaml | 1480 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 1489 insertions(+), 5 deletions(-) diff --git a/docs/api/index.md b/docs/api/index.md index 5655712..65f6d9a 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,6 +1,6 @@ # API 契约 -正式契约见 [openapi.yaml](openapi.yaml)(OpenAPI 3,v1.2.0),当前 18 路径 / 24 操作: +正式契约见 [openapi.yaml](openapi.yaml)(OpenAPI 3,v1.3.0),当前 31 路径 / 43 操作: - 认证域(第一迭代冻结):注册、登录、刷新、退出、当前用户 5 个端点,统一错误信封 `{code, message, data}` 与错误码表,以及会话轮换与登录锁定策略说明。 - 埋点域(M2 第一波补录):`POST /api/v1/events` 批量上报产品事件——单批 1–50 条、202 逐条结果(accepted/duplicate/rejected)、`eventId` 幂等去重、唯一允许匿名的写端点(携带 Bearer 则完整校验)。 @@ -15,4 +15,14 @@ 权限三档 READ/WRITE/MANAGE(ADR-015 三角色)、创建返回 201、PATCH 不支持清空回 null、四个记录类 POST 支持可选 `Idempotency-Key`;错误码新增 40300/40401/40402/40902/40903/40904/42201/42202。 -约定:契约变更须先改本文件目录下的 OpenAPI,再改实现(契约先行);错误码只增不改义;**pets 域已冻结(1.2.0)——冻结后任何字段变更须显著上报、两端同步**。 +- 社区与媒体域(M3 第二波冻结,13 路径;冻结报告为 iteration-3 的 18 号报告,波末入档): + - 媒体两步上传:`POST /api/v1/media/uploads`、`POST /api/v1/media/uploads/{assetId}/complete`(预签名 PUT 直传 + HEAD 校验确认;私有桶,一切读取 URL 为时效性预签名 GET) + - 帖子生命周期:`POST /api/v1/posts`、`GET/PATCH/DELETE /api/v1/posts/{postId}`、`GET /api/v1/me/posts`(草稿/编辑/发布/软删;发布 = `PATCH {status: published}`,乐观锁 409/40902,防枚举 404/40403) + - 公共 Feed:`GET /api/v1/feed`(`(published_at, id)` keyset 游标;FeedCard = 200 码点摘要 + 唯一封面行 + 计数) + - 单层评论:`GET/POST /api/v1/posts/{postId}/comments`、`DELETE /api/v1/comments/{commentId}`(@ 回复 `replyToUserId`;仅评论作者可删,帖主不可删他人评论) + - 点赞/收藏:`PUT/DELETE /api/v1/posts/{postId}/like|bookmark`、`GET /api/v1/me/bookmarks`(PUT/DELETE 语义幂等,响应回 `{liked, likeCount}` 族权威终态;收藏列表失效帖静默剔除) + - 关注最小接口:`PUT/DELETE /api/v1/users/{userId}/follow`、`GET /api/v1/users/{userId}/follow-stats`(自关注 422/42204,自取关 200 幂等 no-op) + + 创建型写入(发帖/评论)`Idempotency-Key` **必带**(1~128,比对规范化 request_hash,与 pets 域可选键刻意不同);互动面 = 帖子公开面(作者本人草稿在互动路径同样 404);错误码新增 40301/40403/40404/40405/40406/40905/42203/42204/42205。 + +约定:契约变更须先改本文件目录下的 OpenAPI,再改实现(契约先行);错误码只增不改义;**pets 域已冻结(1.2.0)、community/media 域已冻结(1.3.0)——冻结后任何字段变更须显著上报、两端同步**。 diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 1ecb5a9..c9d3072 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,13 +1,16 @@ openapi: 3.0.3 info: - title: Patbond API — Auth / Me / Events / Pets(公开契约) - version: 1.2.0 + title: Patbond API — Auth / Me / Events / Pets / Community / Media(公开契约) + version: 1.3.0 description: | Patbond 第一迭代「真实登录纵切」公开契约(冻结稿的正式化,字段与草案零偏差), 1.1.0 追加埋点上报端点 `POST /api/v1/events`(M2 第一波契约补录,以实现实测行为为准)。 **1.2.0 M2 契约冻结:pets 域 12 路径**(宠物 CRUD、品种/疫苗目录、体重记录、疫苗记录、 健康事件、照护提醒、档案聚合摘要)按第二波已定型实现合入 (iteration-2 报告 13/16/17/18 定型表;冻结报告见 iteration-2/19)。 + **1.3.0 M3 契约冻结:community/media 域 13 路径**(媒体两步上传、帖子生命周期、 + 公共 Feed、单层评论、点赞/收藏/关注最小接口)按第二波已定型实现合入 + (iteration-3 报告 13/15/16/17 定型表;冻结报告见 iteration-3/18)。 ## 通用约定(development-plan 第 6 节) - 公开接口统一前缀 `/api/v1`;JSON 字段一律 `camelCase`;资源 ID 为 UUID 字符串。 @@ -27,16 +30,25 @@ info: | 40101 | 401 | access token 无效或过期(缺失、伪造、篡改、过期) | | 40102 | 401 | refresh token 已失效或被重用(未知、过期、已轮换、已退出、家族已撤销) | | 40300 | 403 | PET_ACCESS_DENIED:对可见宠物无相应操作权限(viewer 写记录、caregiver 改宠物档案) | + | 40301 | 403 | POST_ACCESS_DENIED:对可见帖子/评论无相应操作权限(改删他人已发布帖、删他人可见评论——含帖主);仅发给对资源「可见」的调用者 | | 40400 | 404 | 资源不存在 | | 40401 | 404 | PET_NOT_FOUND:宠物不存在、已软删除或调用者与宠物无关系(防枚举,三种情况响应完全一致) | | 40402 | 404 | RECORD_NOT_FOUND:顶层记录路径下记录不存在或所属宠物对调用者不可见(记录级防枚举,两种情况响应完全一致) | + | 40403 | 404 | POST_NOT_FOUND:帖子不存在、已软删、hidden/archived(作者同样)或他人 draft(防枚举,全部情况响应完全一致);评论与互动路径上含作者本人草稿 | + | 40404 | 404 | COMMENT_NOT_FOUND:评论不存在、已删或所属帖子不可见(防枚举合并) | + | 40405 | 404 | MEDIA_NOT_FOUND:asset 不存在、非本人所有或已删(防枚举合并) | + | 40406 | 404 | USER_NOT_FOUND:目标用户不存在或已注销(关注端点与评论 replyToUserId;不复用 40400——该码已承担「路由级资源不存在」兜底语义,复用会使二者不可区分) | | 40900 | 409 | 用户名已存在(大小写不敏感) | | 40901 | 409 | 手机号已被使用 | | 40902 | 409 | VERSION_CONFLICT:乐观锁版本冲突(PATCH 提交的 version 过期);照护提醒流转的状态守卫落空复用此码 | | 40903 | 409 | MICROCHIP_EXISTS:芯片号已被登记(uq_pets_microchip,跨用户唯一) | | 40904 | 409 | VACCINATION_DOSE_EXISTS:同宠物同疫苗同系列同剂次已有非 cancelled 记录(uq_pet_vaccination_dose) | + | 40905 | 409 | IDEMPOTENCY_PAYLOAD_MISMATCH:同 Idempotency-Key 不同 payload(规范化 request_hash 不符,community 域创建型写入) | | 42201 | 422 | VACCINATION_RULE_VIOLATION:疫苗状态机非法迁移或状态-日期规则违反 | | 42202 | 422 | REMINDER_RULE_VIOLATION:提醒状态机非法迁移或 completed-completedAt 一致性违反 | + | 42203 | 422 | MEDIA_NOT_READY:引用了本人所有但非 ready(uploading/failed)状态的 asset | + | 42204 | 422 | FOLLOW_RULE_VIOLATION:自关注(仅 PUT;自取关为 200 幂等 no-op) | + | 42205 | 422 | MEDIA_UPLOAD_STATE_INVALID:complete 时 asset 非 uploading——对象未上传保持可重试、大小/类型不符置 failed 终态、failed 态再确认;已 ready 幂等 200 除外 | | 42300 | 423 | 登录失败次数过多,账号已临时锁定(见下) | | 50000 | 500 | 服务器内部错误 | | 50300 | 503 | 依赖服务暂不可用 | @@ -84,13 +96,45 @@ info: 「新增可选字段」纯增量补入。软删除端点不在 M2 契约(D2-7:首版仅归档 `status=archived`);`DELETE /api/v1/pets/{petId}` 未收录。 + ## Community / Media 域约定(M3 冻结,iteration-3 报告 13/15/16/17 定型) + - **鉴权**:全部端点强制 Bearer 鉴权,无匿名端点。帖子/Feed/评论/互动/关注在 + patbond-community(:8084),媒体上传两步流程在 patbond-user(:8082)。 + - **幂等按域(ADR-019,与 pets 域刻意不同,两域并存、pets 不回改)**:创建型写入 + (发帖/评论)`Idempotency-Key` **必带**(1~128 字符,trim 后计;缺失/空白/超长 + 400/40000),键按作者隔离,落表内幂等列并**比对规范化 request_hash**——hash 对象 + 是规范化后的创建命令(trim、缺省展开),语义相同仅格式不同的重试仍命中首个资源; + 同键同 payload 返回首次创建的资源(同样 201);同键不同 payload 409/40905; + 同键重试撞已删除的首个资源 404(帖子 40403 / 评论 40404)。 + - **二元互动语义幂等**:点赞/收藏/关注用 PUT/DELETE,复合主键即幂等键(无键管理), + 重复调用返回 200 同一**权威终态**(`{liked, likeCount}` 族);客户端乐观更新以 + 响应对账回滚(回滚基准取响应值而非本地推算)。 + - **媒体两步上传(ADR-016)**:创建上传(登记 asset + 签发预签名 PUT 直传凭据, + TTL 10 分钟,配置项)→ 客户端直传(原样携带 requiredHeaders)→ complete 确认 + (服务端 HEAD 校验后 uploading→ready)。桶保持私有:**一切媒体读取 URL + (asset/帖图/头像)均为时效性预签名 GET URL**(TTL 默认 1 小时,配置项),由 + 服务端每次响应现签;客户端不得持久化 URL,过期即重取。 + - **防枚举 404**:一切不可见情形按资源合并给码(帖子 40403、评论 40404、asset + 40405、用户 40406),同码各情形响应完全一致;403/40301 只发给对资源「可见但 + 无权」的调用者,不泄露新信息。 + - **互动面 = 帖子公开面**:评论(读写删)与点赞/收藏只对 published 且未删的帖子 + 开放,**作者本人的草稿在互动路径同样 404/40403**——可见性回答「能不能看」, + 互动门禁回答「能不能社交」。 + - **列表分页**:全部列表复用 cursor 分页正典 `{items, nextCursor, hasMore}` + (`limit` 1~100 缺省 20),各列表排序键在端点描述中写死。 + - **ADR-018 裁剪**:话题全部端点、关注/粉丝**列表**(最小接口仅 follow/unfollow + + 计数)、作者主页帖子列表、`region`/`generationJob`/`visibility=followers|private` + 整体不出现,后续按新增可选字段/端点纯增量补入。`/internal/**` 服务间接口 + (如作者公开资料批量接口)不属于本公开契约。 + servers: - url: http://127.0.0.1:8081 description: patbond-auth(本地开发,/api/v1/auth/**) - url: http://127.0.0.1:8082 - description: patbond-user(本地开发,/api/v1/me、/api/v1/events) + description: patbond-user(本地开发,/api/v1/me、/api/v1/events、/api/v1/media/**) - url: http://127.0.0.1:8083 description: patbond-pet(本地开发,pets 域全部端点) + - url: http://127.0.0.1:8084 + description: patbond-community(本地开发,帖子/Feed/评论/互动/关注全部端点) tags: - name: auth @@ -105,6 +149,18 @@ tags: description: 品种与疫苗目录(只读字典,patbond-pet) - name: health-records description: 体重、疫苗、健康事件、照护提醒、档案摘要(patbond-pet) + - name: media + description: 媒体上传两步流程(patbond-user,ADR-016 预签名直传) + - name: posts + description: 帖子生命周期:草稿/编辑/发布/删除/详情/我的帖子(patbond-community) + - name: feed + description: 公共 Feed 游标分页(patbond-community) + - name: comments + description: 单层平铺评论 + @ 回复(patbond-community,ADR-018) + - name: interactions + description: 点赞/收藏 PUT+DELETE 幂等与收藏列表(patbond-community,ADR-019) + - name: follows + description: 关注最小数据接口:follow/unfollow + 计数(patbond-community,ADR-018) paths: /api/v1/auth/register: @@ -1016,6 +1072,617 @@ paths: '404': $ref: '#/components/responses/PetNotFound' + # ====================================================================== + # Community / Media 域(M3 冻结,13 路径;定型依据:iteration-3 报告 13/15/16/17) + # ====================================================================== + + /api/v1/media/uploads: + post: + tags: [media] + summary: 创建上传(登记 asset 并签发预签名直传凭据) + description: | + 两步上传第一步:校验白名单与上限(`purpose` 仅 post_image、`mimeType` 仅 + image/jpeg|png|webp、`byteSize` ≤ 10485760,均为服务端配置项,后续扩展为 + 向后兼容的枚举追加)→ 写 `media.assets` 行(status=uploading,bucket/objectKey + 服务端生成、不含任何用户输入)→ 返回预签名 PUT 直传凭据(TTL 10 分钟,配置项)。 + 客户端凭凭据直传对象存储,不经应用服务器;直传必须**原样携带 requiredHeaders** + (Content-Type 已签进签名,改动即被存储侧拒绝)。M3 仅 `kind=image` + (ADR-018 视频后置;video/document 为向后新增枚举预留)。 + operationId: createMediaUpload + security: + - bearerAuth: [] + 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/Content-Type → + uploading→ready、写 readyAt,返回可引用的 asset(含现签预签名 GET URL)。 + + - **幂等**:对已 ready 的 asset 重复 complete 返回 200 同一 asset(现签新 GET URL)。 + - 对象尚不存在(直传完成前确认)→ 422/42205,asset **保持 uploading 可重试** + (补传后再确认即恢复,凭据未过期时无须重新创建上传)。 + - 对象存在但大小/类型与登记不符 → 置 failed(终态),422/42205,须重新创建上传。 + - failed 态再确认 → 422/42205(终态);不存在/非本人/已删 → 404/40405(防枚举合并)。 + - `sha256` 照收照存,M3 不做内容核验(存储侧 HEAD 不返回内容散列;后续经 + 存储侧 checksum 特性补齐,不改契约形态)。 + operationId: completeMediaUpload + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/AssetIdParam' + responses: + '200': + description: 确认成功(或幂等重复确认),asset 为 ready + content: + application/json: + schema: + $ref: '#/components/schemas/MediaAssetEnvelope' + '400': + $ref: '#/components/responses/ValidationError' + '401': + $ref: '#/components/responses/AccessTokenInvalid' + '404': + $ref: '#/components/responses/MediaNotFound' + '422': + description: | + asset 非 uploading 态或对象校验未通过(code 42205):对象未上传保持可重试、 + 大小/类型不符置 failed 终态、failed 态再确认(已 ready 幂等 200 除外) + 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` **必带**(语义见该头参数描述与 info「Community / Media 域 + 约定」)。`status` 可 draft(缺省)或 published(直接发布,服务端写 + publishedAt)。纯文字帖合法(media 空数组或缺席,D3-4)。 + + media 挂接(每帖 ≤9 图):只接受本人所有且 ready 的 asset(uploading/failed + 422/42203;不存在/非本人/已删 404/40405);`position` **全给或全不给**——全给 + 须恰为 0..n-1 连续不重复,全不给按数组序,混合 400/40000;`isCover` 至多一个 + true,全 false 时服务端将 position 0 行落库置为封面(库内恒有唯一封面行); + 同帖 assetId 不重复;caption trim 后 ≤300。 + `petId` 须为调用者可见宠物,否则 404/40401(沿 pets 域防枚举语义)。 + operationId: createPost + security: + - bearerAuth: [] + 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' + '404': + description: petId 引用不可见宠物(code 40401)或 media 引用的 asset 不存在/非本人/已删(code 40405) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + examples: + petNotFound: + value: { code: 40401, message: 宠物不存在, data: null } + mediaNotFound: + value: { code: 40405, message: 媒体不存在, data: null } + '409': + $ref: '#/components/responses/IdempotencyPayloadMismatch' + '422': + $ref: '#/components/responses/MediaNotReady' + + /api/v1/posts/{postId}: + get: + tags: [posts] + summary: 帖子详情 + description: | + 权限矩阵(iteration-3 报告 15 定型):published 对全部登录用户开放;draft 仅 + 作者可见;hidden/archived(运营态)**对作者同样 404/40403**——M3 无端点能产生 + 或解除运营态,status 枚举保持两值。一切不可见情形响应完全一致(防枚举)。 + 响应含 likedByMe/bookmarkedByMe 与作者公开摘要(AuthorSummary)。 + operationId: getPost + security: + - bearerAuth: [] + 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;一切不可见情形——含他人 draft——404/40403)。 + PATCH 部分更新惯例:缺席字段不变,不支持清空回 null(M2 先例)。`version` + 必带(缺失 400/40000,过期 409/40902)。 + + - **发布** = `status: published` 的状态迁移(draft→published 是唯一开放迁移, + 服务端写 publishedAt,恰写一次);**对已发布帖重复提交 `status: published` + 为幂等 no-op(200,version 照常 +1)**——同态提交不是迁移,弱网重发不报错; + draft/hidden/archived 目标值由请求枚举拒为 400/40000(published→draft 不支持)。 + - media 出现即**整组替换**(删旧插新;`[]` 清空为纯文字帖;缺席不动), + 校验规则同创建。 + operationId: updatePost + security: + - bearerAuth: [] + 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': + description: | + 帖子不可见(code 40403,防枚举合并);或 petId 引用不可见宠物(code 40401); + 或 media 引用的 asset 不存在/非本人/已删(code 40405) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + examples: + postNotFound: + value: { code: 40403, message: 帖子不存在, data: null } + petNotFound: + value: { code: 40401, message: 宠物不存在, data: null } + mediaNotFound: + value: { code: 40405, message: 媒体不存在, data: null } + '409': + $ref: '#/components/responses/VersionConflict' + '422': + $ref: '#/components/responses/MediaNotReady' + delete: + tags: [posts] + summary: 删除帖子(软删,仅作者) + description: | + 软删(deleted_at 为全域唯一删除判定基准),删除后详情/Feed/列表/互动一切路径 + 404/40403。重复删除与删不存在的帖同响应 404/40403(防枚举合并)。 + 不提供恢复端点(M3 无回收站)。非作者对已发布帖 403/40301。 + operationId: deletePost + security: + - bearerAuth: [] + 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(软删不含,hidden/archived 不含)。排序 + `(created_at DESC, id DESC)` 走 `ix_posts_author_created`,keyset 游标。 + `status` 过滤可选(draft|published)。 + operationId: listMyPosts + security: + - bearerAuth: [] + 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 帖子下一次请求即不可见。 + 卡片形态见 FeedCard(iteration-3 报告 16 定型);likedByMe/bookmarkedByMe + 为当前用户视角。 + operationId: getFeed + security: + - bearerAuth: [] + 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`,keyset 游标; + 仅 visible 评论。互动面 = 帖子公开面:帖子不可见(**含作者本人草稿**) + 404/40403。作者与 @ 目标均为 AuthorSummary。 + operationId: listComments + security: + - bearerAuth: [] + 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`(键按作者隔离、天然全局跨帖)。`replyToUserId` 可选 @ 回复 + (单层平铺,无楼中楼,ADR-018);目标须为存活用户,不存在/已注销 404/40406 + (合并不泄露成因)。互动面 = 帖子公开面:帖子不可见(**含作者本人草稿**) + 404/40403。content trim 后 1~2000。comment_count 同事务 +1。 + operationId: createComment + security: + - bearerAuth: [] + 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': + description: 帖子不可见——含作者本人草稿(code 40403);或 replyToUserId 目标用户不存在/已注销(code 40406) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + examples: + postNotFound: + value: { code: 40403, message: 帖子不存在, data: null } + userNotFound: + value: { code: 40406, message: 用户不存在, data: null } + '409': + $ref: '#/components/responses/IdempotencyPayloadMismatch' + + /api/v1/comments/{commentId}: + delete: + tags: [comments] + summary: 删除评论(仅评论作者;顶层短路径) + description: | + 顶层短路径先例(pets 域子资源同理):commentId 全局唯一。**仅评论作者可删—— + 帖主不可删除他人评论(D3-7 首版不做)**:对可见评论的非作者(含帖主) + 403/40301;不存在/已删/所属帖不可见合并 404/40404(防枚举)。 + 软删(status→deleted),comment_count 同事务 -1。 + operationId: deleteComment + security: + - bearerAuth: [] + 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) 即幂等键:重复 PUT 返回 200 同一权威终态(非 409), + 仅实际插入才 like_count 同事务 +1,并发 N 次计数恰为 1(M3 验收标准二)。 + 互动面 = 帖子公开面:帖子不可见(**含作者本人草稿**)404/40403。 + operationId: likePost + security: + - bearerAuth: [] + 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)。 + 帖子不可见(含作者本人草稿)404/40403。 + operationId: unlikePost + security: + - bearerAuth: [] + 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 语义幂等,与点赞同构) + description: 帖子不可见(含作者本人草稿)404/40403。 + operationId: bookmarkPost + security: + - bearerAuth: [] + 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 语义幂等) + description: 帖子不可见(含作者本人草稿)404/40403。 + operationId: unbookmarkPost + security: + - bearerAuth: [] + 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`,游标键在收藏关系行上。项形态 = FeedCard, + 谓词与公共 Feed 恒等:被收藏帖软删/hidden/archived 后**静默剔除**(剔除在页 + 查询内完成,不破坏翻页不丢不重;publishedAt 恒非空不变式对本列表继续成立)。 + operationId: listMyBookmarks + security: + - bearerAuth: [] + 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 与关注/粉丝列表不在 M3(ADR-018 最小数据接口)。 + operationId: followUser + security: + - bearerAuth: [] + 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;仅 PUT——自取关走 DELETE 的 200 幂等 no-op) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + examples: + selfFollow: + value: { code: 42204, message: 不能关注自己, data: null } + delete: + tags: [follows] + summary: 取消关注(DELETE 语义幂等) + description: | + 取消不存在的关注不报错,返回 200 权威终态(following 恒 false)。 + **自取关同样 200 幂等 no-op**(关系行不可能存在,权威 false 即事实;42204 + 只在 PUT)。目标用户不存在/已注销 404/40406。 + operationId: unfollowUser + security: + - bearerAuth: [] + 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 为调用者视角, + 查自己时恒 false。目标用户不存在/已注销 404/40406。关注/粉丝**列表**端点 + 不在 M3(需时按纯增量补入)。 + operationId: getFollowStats + security: + - bearerAuth: [] + 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: @@ -1061,6 +1728,46 @@ components: 可选幂等键(≤255 字符,超长 400/40000)。键按「调用者 × 宠物 × 资源」隔离; 同键重试返回首次创建的记录(同样 201);不比对请求体(每次逻辑提交应换新键, 建议 UUID);键永久幂等(无 TTL)。不带键则无幂等语义。 + 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 + IdempotencyKeyRequiredHeader: + name: Idempotency-Key + in: header + required: true + schema: + type: string + maxLength: 128 + description: | + **必带**幂等键(1~128 字符,trim 后计;缺失/空白/超长 400/40000。与 pets 域 + 「可选、≤255、不比对请求体」刻意不同——community 域按 ADR-019 落表内幂等列, + 列宽 128)。键按作者隔离(跨用户同键互不干扰);同键重试返回首次创建的资源 + (同样 201);**比对规范化 request_hash**——hash 对象是规范化后的创建命令 + (trim、缺省展开),语义相同仅格式不同的重试仍命中首个资源;同键不同 payload + 返回 409/40905;同键重试撞已删除的首个资源返回 404(帖子 40403 / 评论 40404, + 资源已消亡,不复活不另建)。客户端每次逻辑提交换新键(建议 UUID), + 重试间保持不变。 responses: ValidationError: @@ -1125,6 +1832,76 @@ components: examples: versionConflict: value: { code: 40902, message: 数据已被修改,请刷新后重试, data: null } + PostNotFound: + description: | + 帖子不存在、已软删、hidden/archived(作者同样)或他人 draft(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 不同 payload,规范化 request_hash 不符(code 40905) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + examples: + mismatch: + value: { code: 40905, message: 幂等键已用于不同请求, data: null } + MediaNotReady: + description: | + 引用了本人所有但非 ready(uploading/failed)状态的 asset(code 42203)。 + asset 不存在/非本人/已删则合并为 404/40405。 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + examples: + notReady: + value: { code: 42203, message: 媒体尚未就绪, data: null } schemas: RegisterRequest: @@ -2383,3 +3160,700 @@ components: example: success data: $ref: '#/components/schemas/PetSummary' + + # ================================================================== + # Community / Media 域 schemas(M3 冻结;定型依据:iteration-3 报告 13/15/16/17) + # ================================================================== + + AuthorSummary: + type: object + description: | + 作者公开摘要(D3-9 方案 B,iteration-3 报告 16 定型;community 跨 schema + 只读 identity 取数,ADR-017)。正常路径 nickname 恒非空——空昵称由服务端 + 回退为 username(客户端不做回退拼装,回退后的展示名不标注来源); + nickname 与 avatarUrl 同为 null 即「降级/墓碑」形态(作者资料暂不可得, + 或用户已注销)——两种情形同一形态,客户端只需一种占位逻辑。 + 不露 bio、不露 username。 + required: [userId] + properties: + userId: + type: string + format: uuid + description: 恒非空,任何情形都在 + nickname: + type: string + nullable: true + maxLength: 32 + description: 昵称(空昵称已由服务端回退为 username);null 仅出现在降级/注销墓碑形态 + example: 毛毛的铲屎官 + avatarUrl: + type: string + nullable: true + description: | + 头像访问 URL——时效性预签名 GET(TTL 默认 1 小时,配置项),每次响应现签, + 客户端不得持久化、过期即重取;无头像 / 头像 asset 非 ready / 降级 → null + (客户端出占位) + + # ---------- media ---------- + CreateMediaUploadRequest: + type: object + required: [kind, purpose, mimeType, byteSize] + properties: + kind: + type: string + enum: [image] + description: M3 仅 image(ADR-018 视频后置;video/document 为向后新增枚举预留) + purpose: + type: string + enum: [post_image] + description: | + 用途白名单(M3 定型仅 post_image,决定 objectKey 前缀);P6 扩 + user_avatar/pet_avatar 时为向后兼容的枚举追加(服务端纯配置扩展) + mimeType: + type: string + enum: [image/jpeg, image/png, image/webp] + description: 白名单外 400/40000;不收 HEIC(客户端压缩管线统一转码 jpeg) + byteSize: + type: integer + format: int64 + minimum: 1 + maximum: 10485760 + description: 声明的文件字节数,complete 时与对象实测比对;上限 10485760(10 MiB,服务端配置项) + sha256: + type: string + pattern: '^[0-9a-f]{64}$' + description: 可选,64 位小写 hex;照收照存,M3 不做内容核验(后续经存储侧 checksum 特性补齐,不改契约形态) + + MediaUploadCredentials: + type: object + description: 预签名直传凭据(ADR-016,iteration-3 报告 13 定型) + required: [assetId, uploadUrl, method, requiredHeaders, expiresAt] + properties: + assetId: + type: string + format: uuid + description: 已登记的 asset ID(status=uploading) + uploadUrl: + type: string + description: | + 预签名 PUT 完整 URL——签名以 query 参数携带(X-Amz-Algorithm/-Credential/ + -Signature 族),指向客户端可达的对象存储端点;客户端直传,不经应用服务器 + method: + type: string + enum: [PUT] + requiredHeaders: + type: object + additionalProperties: + type: string + description: | + 直传请求必须**原样携带**的头。键集定型为恒且仅一键: + `{"Content-Type": <声明的 mimeType>}`——Content-Type 已签进签名, + 改动即被存储侧拒绝 + expiresAt: + type: string + format: date-time + description: | + 凭据过期时刻 = 签发时刻 + TTL(默认 10 分钟,配置项);过期后重新创建 + 上传(原 asset 在补传后仍可确认) + + 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 + description: complete 后回填,可空 + heightPx: + type: integer + nullable: true + status: + type: string + enum: [uploading, ready, failed] + description: deleted 态对外恒 404/40405,不出现在响应 + url: + type: string + nullable: true + description: | + 访问 URL,仅 ready 态非空——时效性预签名 GET(TTL 默认 1 小时,配置项), + 每次响应现签,客户端不得持久化、过期即重取;桶保持私有,无签名直访被拒 + 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 + description: 库内恒有唯一封面行(写侧保证:全 false 时服务端将 position 0 行置真) + url: + type: string + description: | + 图片访问 URL——时效性预签名 GET(TTL 默认 1 小时,配置项),每次响应现签, + 客户端不得持久化、过期即重取(运维前提:生产环境对象存储恒配置) + 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(不存在/非本人/已删 404/40405) + required: [assetId] + properties: + assetId: + type: string + format: uuid + description: 同帖 assetId 不得重复(400/40000) + position: + type: integer + minimum: 0 + maximum: 8 + description: | + **全给或全不给**:全给须恰为 0..n-1 连续不重复;全不给按数组序; + 混合 400/40000 + isCover: + type: boolean + default: false + description: 至多一个 true(uq_post_media_cover);全 false 时服务端将 position 0 行落库置为封面 + caption: + type: string + maxLength: 300 + description: trim 后 ≤300 + + CreatePostRequest: + type: object + required: [content] + properties: + title: + type: string + minLength: 1 + maxLength: 120 + description: 可选标题(ck_posts_title;空白串 400/40000) + 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 不开放写入(提交 400/40000) + 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 + description: | + 部分更新:缺席字段不变;不支持清空回 null(M2 惯例)。media 若出现则 + **整组替换**(删旧插新;`[]` 清空为纯文字帖;缺席不动),校验规则同创建。 + required: [version] + properties: + version: + type: integer + minimum: 0 + description: 乐观锁,必带(缺失 400/40000);过期 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(发布动作,服务端写 publishedAt); + 对已发布帖重复提交为幂等 no-op(200,version 照常 +1); + draft/hidden/archived 目标值 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/40403 + (M3 无端点能产生或解除运营态) + visibility: + type: string + enum: [public] + description: M3 恒 public(ADR-018:followers/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 + description: | + 行最后更新时刻——互动计数维护亦会推动该值;判断「内容是否编辑过」 + 以 version 为准,勿以 updatedAt 判断 + publishedAt: + type: string + format: date-time + nullable: true + description: 仅 published 非空(发布时恰写一次) + version: + type: integer + + FeedCard: + type: object + description: | + Feed / 收藏列表卡片形态(较 Post 裁剪,iteration-3 报告 16 定型:只带 + coverImage + mediaCount,不带整组图;content 全文、petId、visibility、 + version、media 整组、created/updated 时间戳对均不出现,全文走帖子详情)。 + 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 + description: 原样透传,无标题为 null + contentPreview: + type: string + description: | + 正文前 200 个 Unicode 码点,**码点边界截断**(emoji 等增补面字符绝不 + 劈开),不追加省略号;短于 200 码点原样透传。全文恒走帖子详情端点 + coverImage: + nullable: true + allOf: + - $ref: '#/components/schemas/PostMediaItem' + description: | + 封面图 = 库中唯一 is_cover 行(写侧保证有图必有唯一封面行,读侧零特判); + 纯文字帖为 null + mediaCount: + type: integer + minimum: 0 + maximum: 9 + 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 + description: 恒非空(Feed 与收藏列表谓词只放行 published) + + 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: trim 后 1~2000(ck_comments_content 同宽) + replyToUserId: + type: string + format: uuid + description: | + 可选 @ 回复目标(单层平铺,无 parentCommentId,ADR-018); + 目标须为存活用户,不存在/已注销 404/40406 + + Comment: + type: object + description: M3 无评论编辑,不带 updatedAt + 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: '@ 回复目标的公开摘要(含降级 id-only 形态);非回复为 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 为目标用户的粉丝数(实时 COUNT) + 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: 目标用户的粉丝数(实时 COUNT) + followingCount: + type: integer + format: int64 + description: 目标用户关注的人数(实时 COUNT) + 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'