Files
patbond-doc/docs/api/openapi.yaml
T
lixi 5f02909af6
CI / docs-build (push) Failing after 1s
docs(api): M3.5 契约冻结 v1.4.0——用户资料与头像
按 iteration-3.5/03 号报告定型表冻结第一波后端交付,相对 v1.3.0 纯增量
(无字段删改、无类型变更、无必填收紧),v1.3.0 客户端无需改动:

- GET /api/v1/me 响应补 nickname(DB 原值、不做 username 回退)与
  avatarUrl(时效性预签名 GET,会过期、客户端不得持久化),两者键恒在值可空
- 新增 PATCH /api/v1/me:三态部分更新(键缺省=不改 / 显式 null=清空 /
  给值=设置),空 patch 与纯空白昵称 400/40000,无乐观锁无幂等键
- Pet 补 avatarUrl(列表/详情/创建/更新四处统一);PATCH /api/v1/pets/{petId}
  收三态 avatarAssetId,补 404/40405 与 422/42203 两格,权限按本次触及字段
  定档(仅头像 WRITE、触及资料 MANAGE、混合取更严)
- 新增 GET /api/v1/me/community-stats:receivedLikeCount/publishedPostCount
  (int64,空数据 0,永不 404),聚合口径逐条进描述
- 两处均不外露 avatarAssetId(只写不读,"有头像"等价 avatarUrl != null)
- 媒体 purpose 白名单枚举追加 user_avatar/pet_avatar
- 零新增错误码:复用 40000/40101/40300/40400/40401/40405/40902/42203,
  错误码表只补语义(40300/40405/42203 三行)

规模 31→32 路径 / 43→45 操作 / 72→75 schemas(UpdateMeRequest、
CommunityStats、CommunityStatsEnvelope——后者为与全 API「每个 200 响应引一个
XxxEnvelope」的既有形态保持一致,故比 03 号报告预估多一个)。
校验:yaml 解析通过、96 处 $ref 全解析、45 个 operationId 无重复、
零未引用 schema、mkdocs build --strict 通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 10:19:19 +08:00

4148 lines
158 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 — Auth / Me / Events / Pets / Community / Media(公开契约)
version: 1.4.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)。
**1.4.0 M3.5 契约冻结:用户资料与头像**——`GET /api/v1/me` 补 `nickname`/`avatarUrl`
新增 `PATCH /api/v1/me`(昵称与头像读写,三态部分更新),`Pet` 补 `avatarUrl` 且
`PATCH /api/v1/pets/{petId}` 收 `avatarAssetId`,新增 `GET /api/v1/me/community-stats`
(获赞总数与作品数),媒体 `purpose` 白名单追加 `user_avatar`/`pet_avatar`
iteration-3.5 报告 03 定型表;冻结报告见 iteration-3.5/04)。
**1.4.0 相对 1.3.0 纯增量**:无字段删改、无类型变更、无必填收紧,仅新增操作、
新增响应字段(键恒在、值可空)、新增可选请求字段、新增响应格与枚举追加,
v1.3.0 客户端无需改动即可继续工作。
## 通用约定(development-plan 第 6 节)
- 公开接口统一前缀 `/api/v1`JSON 字段一律 `camelCase`;资源 ID 为 UUID 字符串。
- 所有时间字段为 ISO 8601 且带时区偏移(如 `2026-09-04T04:05:06.789Z`);
纯日期字段(生日、接种日期等)为 `YYYY-MM-DD`。
- 统一响应信封 `{"code": 0, "message": "success", "data": …}`;错误同时携带正确的
HTTP 状态码与稳定业务码,业务码永不复用或改号。
- `/internal/**` 为服务间接口,不属于本公开契约,需 `X-Internal-Token` 服务凭证,
未携带或错误一律 401。
## 错误码表
| 业务码 | HTTP | 场景 |
| --- | --- | --- |
| 0 | 200 | 成功 |
| 40000 | 400 | 参数校验失败(含 JSON 不可解析;message 为首个字段错误) |
| 40100 | 401 | 用户名或密码错误 |
| 40101 | 401 | access token 无效或过期(缺失、伪造、篡改、过期) |
| 40102 | 401 | refresh token 已失效或被重用(未知、过期、已轮换、已退出、家族已撤销) |
| 40300 | 403 | PET_ACCESS_DENIED:对可见宠物无相应操作权限(viewer 写记录或改头像、caregiver 改宠物档案——头像除外,见 M3.5 分档) |
| 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_FOUNDasset 不存在、非本人所有、已删,或**用途与引用场景不符**(帖图当头像、user_avatar 当宠物头像等);防枚举合并,同码各情形响应一致(用途不符分支只对调用者自己的 asset 可达,故 message 可具体) |
| 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:引用了本人所有、用途相符但非 readyuploading/failed)状态的 asset(帖图与用户/宠物头像同构) |
| 42204 | 422 | FOLLOW_RULE_VIOLATION:自关注(仅 PUT;自取关为 200 幂等 no-op |
| 42205 | 422 | MEDIA_UPLOAD_STATE_INVALIDcomplete 时 asset 非 uploading——对象未上传保持可重试、大小/类型不符置 failed 终态、failed 态再确认;已 ready 幂等 200 除外 |
| 42300 | 423 | 登录失败次数过多,账号已临时锁定(见下) |
| 50000 | 500 | 服务器内部错误 |
| 50300 | 503 | 依赖服务暂不可用 |
## 会话模型(ADR-003,数值均为服务端配置项)
- access tokenJWTRS256),有效期 15 分钟;由资源服务用公钥本地验签。
- refresh token:不透明随机串,有效期 30 天;**每次刷新即轮换**,旧值立即失效。
- 已轮换/已失效的 refresh token 再次被使用时,判定为重用,**整个 token family
(该登录会话链)全部撤销**,持有者需重新登录。
- 允许多设备并行会话;退出仅撤销当前会话(由所提交的 refreshToken 标识),
其他设备不受影响。已签发的 access token 在剩余有效期内仍可用。
- 登录失败限制:同一账号在 15 分钟窗口内密码错误累计 5 次(配置项),账号锁定
15 分钟;锁定期间即使密码正确也返回 423/42300;一次成功登录重置计数窗口。
## Pets 域约定(M2 冻结,iteration-2 报告 13/16/17/18 定型)
- **鉴权**pets 域全部端点强制 Bearer 鉴权,无匿名端点。
- **权限模型(ADR-015owner/caregiver/viewer 三角色,pet_owners 表)**,操作分三档:
- `READ`——三角色皆可:宠物详情/列表、各记录列表、档案摘要;
- `WRITE`——owner + caregiver:体重/疫苗/健康事件/提醒的 POST 与 PATCH
**M3.5 起还含宠物头像字段 `avatarAssetId`**ADR-022:头像属日常照护信息)。
- `MANAGE`——仅 owner:宠物档案 PATCH 的资料字段(含状态流转)。
**同一端点按「本次请求触及哪些字段」定档**(不是端点级降档):仅改头像走 WRITE,
触及任一资料字段走 MANAGE,混合请求取更严的一半。
权限每请求实时查库、无缓存:撤销照护关系立即生效。
- **防枚举语义**:宠物不存在、已软删除、调用者与宠物无 pet_owners 关系三种情况
响应完全一致(404/40401),GET 与写操作一致适用;顶层记录路径下「记录不存在」与
「记录所属宠物对调用者不可见」响应完全一致(404/40402)。403/40300 只可能发给
「对宠物可见但角色不覆盖该操作」的调用者,不泄露新信息。
- **PATCH 一律部分更新**:缺席字段不变;**M2 不支持将可选字段清空回 null**
null-vs-absent 歧义挡在契约外)。pets / vaccinations / health-events 的 PATCH
必须携带 `version` 乐观锁字段(缺失 400/40000,过期 409/40902,比对通过才写入并 +1)。
- **子资源 PATCH 走顶层短路径**`/api/v1/vaccinations/{id}` 等):记录 ID 全局唯一
UUID),短路径避免 path petId 与记录归属不一致的报错歧义。
- **创建操作返回 201**pets 域新约定;既有 auth 端点维持 200 不追改)。
- **cursor 分页正典(全 API 唯一分页形态)**:响应 `data: {items, nextCursor, hasMore}`
`limit` 1~100 缺省 20`cursor` 传上一页返回的 `nextCursor`(不透明字符串,客户端不得
解析),首页不传;`hasMore=false` 时 `nextCursor` 恒为 null。体重与健康事件列表采用;
疫苗列表(`series_key, dose_no, created_at, id` 排序)与提醒列表(`due_at ASC, id`
排序 + `status` 过滤)量级小,不分页。
- **幂等(可选 `Idempotency-Key` 头,≤255 字符)**weights / vaccinations /
health-events / care-reminders 四个 POST 支持。键按「调用者 × 宠物 × 资源」隔离,
两个用户的同名键不互斥;同键重试返回首次创建的记录(同样 201);**不比对请求体**
(客户端每次逻辑提交应换新键,建议 UUID);键永久幂等(无 TTL)。不带键则无幂等
语义,重复提交各自成行(疫苗由剂次唯一约束兜底 40904)。pets 的写接口不用幂等键,
重试安全由乐观锁与唯一约束兜底。
- **ADR-010 裁剪**`avatarAssetId`、`certificateAssetId`、`providerId`、
`providerNameSnapshot`、`bookingId` 等字段整体不出现(响应与请求皆无),M5+ 按
「新增可选字段」纯增量补入。软删除端点不在 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/**` 服务间接口
(如作者公开资料批量接口)不属于本公开契约。
## 用户资料与头像域约定(M3.5 冻结,iteration-3.5 报告 03 定型;ADR-022
- **三态部分更新(仅本域,pets 域 M2 两态语义不回改)**`PATCH /api/v1/me` 的
`nickname`/`avatarAssetId` 与 `PATCH /api/v1/pets/{petId}` 的 `avatarAssetId`
按三态解释——**键缺省 = 不改;键出现且为 `null` = 清空;键出现且有值 = 设置**。
昵称与头像天生可选,「删掉我设的那个」是一等公民操作,只有两态无法表达。
同一请求体内的其余 pets 字段仍是 M2 的「缺省或 null 皆为不改」。
- **空 PATCH 与纯空白昵称一律 400/40000**,不静默 200、不隐式清空:清空只留
显式 `null` 一条路,否则「误提交空格」与「想删昵称」无法区分。
- **昵称**btrim 后 1~32 **码点**(非 UTF-16 长度);**不设唯一约束**ADR-022
允许重名,靠 userId 区分);注册不收昵称。`/api/v1/me` 返回 **DB 原值**
未设置即 `null`**不做 username 回退**——`/me` 是本人编辑态,回退会把展示约定
固化成真实数据;他人视角的展示回退在 `/internal/users/profiles`SQL COALESCE),
本人视角的展示回退由客户端做 `nickname ?? username`。
- **头像读取一律 `avatarUrl`(时效性预签名 GET,与帖图同一纪律)**:每次响应现签,
**会过期、客户端不得持久化**,过期即重取;无头像、asset 非 ready、对象存储未配置
三种情况均为 `null`(降级而非报错——签一个必然 404 的 URL 比给 null 更糟)。
指针不隐式清理:asset 事后退出 ready 时 `avatarUrl` 转 null 而引用保留。
- **`avatarAssetId` 只写不读**:请求体收,响应**一律不外露**(`Me` 与 `Pet` 皆无该字段);
「是否有头像」等价于 `avatarUrl != null`。
- **两种头像用途互不通用**`user_avatar` 不能当宠物头像、`pet_avatar` 不能当用户头像、
`post_image` 不能当任何头像——引用侧按 `purpose` 校验,不符者 404/40405
(四态校验:不存在/非本人/已删/用途不符 → 404/40405;本人且用途相符但
uploading/failed → 422/42203)。
- **宠物头像的权限档按「本次请求碰了哪些字段」定档**(ADR-022 头像为 WRITE 档):
仅改 `avatarAssetId` 时 owner + caregiver 皆可(viewer 403/40300);触及任一资料
字段时仍是 MANAGE(仅 owner);**混合请求取更严的一半**,堵住把改名夹带进头像
请求绕过 MANAGE 的路径。头像与资料共用同一把乐观锁 `version`(仍必填)。
- **`/api/v1/me` 无乐观锁、无幂等键**:只有一个合法写者(账号本人),暴露 `version`
只是给客户端加负担;丢失更新由**列级选择性 UPDATE** 排除(SET 列表只含本次请求
真正携带的列),并发改不同字段两者皆存活;同 body 重放天然幂等。
- **`GET /api/v1/me/community-stats` 为独立端点**ADR-022 决策 A,不并入
`/users/{userId}/follow-stats`——后者主体是「某用户的关注数」,混入「我的获赞」
会让一个载荷有两个主体)。路径上**没有 userId**:「查不到别人的获赞」不靠权限
判断,而是入口本身不存在,故**永不 404**,任何已认证用户都有 stats。
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、/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
description: 注册 / 登录 / 刷新 / 退出(patbond-auth
- name: user
description: 当前用户资料:读取与昵称/头像更新(patbond-user
- name: analytics
description: 产品事件批量上报(patbond-user
- name: pets
description: 宠物档案 CRUDpatbond-pet
- name: dictionaries
description: 品种与疫苗目录(只读字典,patbond-pet
- name: health-records
description: 体重、疫苗、健康事件、照护提醒、档案摘要(patbond-pet
- name: media
description: 媒体上传两步流程(patbond-userADR-016 预签名直传)
- 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/auth/register:
post:
tags: [auth]
summary: 注册并创建会话
operationId: register
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRequest'
responses:
'200':
description: 注册成功,返回令牌对
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'409':
description: 用户名或手机号已被占用(code 40900 / 40901
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
usernameTaken:
value: { code: 40900, message: 用户名已存在, data: null }
phoneTaken:
value: { code: 40901, message: 手机号已被使用, data: null }
/api/v1/auth/login:
post:
tags: [auth]
summary: 登录并创建会话
description: 多设备并行:每次登录开启独立会话(独立 token family),互不影响。
operationId: login
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: 登录成功,返回令牌对
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
description: 用户名或密码错误(code 40100)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
invalidCredentials:
value: { code: 40100, message: 用户名或密码错误, data: null }
'423':
description: 登录失败次数过多,账号临时锁定(code 42300)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
locked:
value: { code: 42300, message: 登录失败次数过多,账号已临时锁定, data: null }
/api/v1/auth/refresh:
post:
tags: [auth]
summary: 轮换 refresh token
description: |
成功时返回全新令牌对,旧 refreshToken 立即失效(轮换)。提交已轮换或已失效的
refreshToken 返回 401/40102,且视为重用攻击:该 token family 的全部会话被撤销。
operationId: refresh
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshRequest'
responses:
'200':
description: 轮换成功,返回新的令牌对
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
description: refresh token 已失效或被重用(code 40102
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
invalidated:
value: { code: 40102, message: refresh token 已失效或被重用, data: null }
/api/v1/auth/logout:
post:
tags: [auth]
summary: 退出(撤销当前会话)
description: |
撤销 body 中 refreshToken 对应的会话;其他设备的会话不受影响(ADR-003)。
需携带有效的 access token(从中取用户身份,防止跨账号撤销)。幂等:对已
失效的 refreshToken 仍返回成功。
operationId: logout
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LogoutRequest'
responses:
'200':
description: 已退出
content:
application/json:
schema:
$ref: '#/components/schemas/VoidEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
/api/v1/me:
get:
tags: [user]
summary: 当前用户资料
description: |
由 patbond-user 提供;access token 以 RS256 公钥本地验签,无需经过 auth 服务。
- `nickname` 为 **DB 原值**,未设置即 `null`**不做 username 回退**,见 info
「用户资料与头像域约定」);本人视角的展示回退由客户端做 `nickname ?? username`。
- `avatarUrl` 为**每次响应现签的时效性预签名 GET**:**会过期、客户端不得持久化**,
过期即重取;无头像、asset 非 ready、对象存储未配置均为 `null`。
- 响应**不含** `avatarAssetId`:客户端只写不读它,「是否有头像」等价于
`avatarUrl != null`。
operationId: me
security:
- bearerAuth: []
responses:
'200':
description: 当前用户
content:
application/json:
schema:
$ref: '#/components/schemas/MeEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
description: 用户不存在(如已注销;code 40400)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
patch:
tags: [user]
summary: 更新当前用户资料(昵称 / 头像,三态部分更新)
description: |
本人资料的唯一写入口(主体恒为 token 里的调用者,无「他人」情形)。
- **三态语义**:键缺省 = 不改;键出现且为 `null` = 清空;键出现且有值 = 设置。
- **空 patch 400/40000**(两字段都未出现,含只带未声明字段):不静默 200,
空 PATCH 几乎总是客户端 bug。纯空白/空串昵称同为 400/40000,不隐式清空。
- `avatarAssetId` 须为**调用者本人、用途为 `user_avatar`、状态 `ready`** 的 asset
不存在/非本人/已删/用途不符 404/40405;本人且用途相符但 uploading/failed
422/42203。
- **无 `version` 乐观锁、无 `Idempotency-Key`**:只有一个合法写者;丢失更新由
列级选择性 UPDATE 排除(并发改不同字段两者皆存活),同 body 重放天然幂等。
- 成功返回**与 GET 完全相同的 `Me` 全量形态**(回显更新后资料,`avatarUrl` 现签)。
operationId: updateMe
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateMeRequest'
responses:
'200':
description: 更新成功,返回更新后的完整 Me
content:
application/json:
schema:
$ref: '#/components/schemas/MeEnvelope'
'400':
description: |
参数校验失败(code 40000):空 patch、昵称 btrim 后长度不在 1~32 码点、
昵称纯空白或空串、`avatarAssetId` 非法 UUID、body 非法 JSON
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
emptyPatch:
value: { code: 40000, message: 请求未包含任何可更新字段, data: null }
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
description: |
用户不存在或已注销(code 40400,token 仍有效但账号已注销);或
`avatarAssetId` 引用的 asset 不存在/非本人/已删/用途不是 `user_avatar`
code 40405,防枚举合并)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
userNotFound:
value: { code: 40400, message: 用户不存在, data: null }
mediaNotFound:
value: { code: 40405, message: 媒体资源不存在, data: null }
'422':
$ref: '#/components/responses/MediaNotReady'
/api/v1/events:
post:
tags: [analytics]
summary: 批量上报产品事件
description: |
埋点批量上报(事件字典见迭代报告 13 与第二迭代 06 号报告)。
`/api/v1` 下唯一允许匿名调用的写端点:`Authorization: Bearer` 可选——
缺失时按匿名处理放行;**一旦携带则完整校验**,无效 token 仍返回 401/40101。
- 单批 1–50 条;条数越界、字段校验失败或 JSON 不可解析时**整批** 400/40000。
- 通过请求级校验的批次一律返回 **202**`data.results` 与请求 `events`
等长且按原顺序逐条给出结果(accepted / duplicate / rejected);
客户端收到 202 即可删除本地队列中该批全部事件(rejected 条目不重试)。
- 幂等以每条事件的 `eventId` 去重(落库 ON CONFLICT DO NOTHING),重复条目
返回 `duplicate`(视为成功);**不使用** `Idempotency-Key` 请求头。
- 单条拒绝原因:事件名不在字典(`unknown_event_name`);已认证请求中事件
`userId` 与 token subject 不一致(`identity_mismatch`);props 的键命中
隐私红线模式 password/token/secret/phone/mobile/email/credential/idfa/gaid
`forbidden_field`);落库失败(`schema_invalid`)。
- props 中字典白名单之外的键**剥离后入库**(事件保留,不拒绝)。
- 匿名请求中事件携带的 `userId` 原样落库(分析归因数据,不参与权限判断)。
operationId: trackEvents
security:
- {} # 匿名(注册/登录前)
- bearerAuth: [] # 登录后携带未过期 access token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TrackEventsRequest'
responses:
'202':
description: 批次已受理,逐条结果见 data.results(与请求 events 等长、原顺序)
content:
application/json:
schema:
$ref: '#/components/schemas/TrackEventsEnvelope'
'400':
description: 整批拒绝——JSON 不可解析、events 为空或超过 50 条、单条事件字段校验失败(code 40000)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
emptyBatch:
value: { code: 40000, message: events 长度必须在 1-50 之间, data: null }
'401':
description: 携带了 Authorization 头但 access token 无效或过期(code 40101);不携带该头则按匿名放行,不会返回 401
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
tokenInvalid:
value: { code: 40101, message: token 无效或过期, data: null }
# ======================================================================
# Pets 域(M2 冻结,12 路径;定型依据:iteration-2 报告 13/16/17/18
# ======================================================================
/api/v1/pets:
get:
tags: [pets]
summary: 当前用户可见宠物列表
description: |
返回当前用户拥有任意角色(owner/caregiver/viewer)的宠物,按 `created_at DESC`
排序,**不分页**(单人宠物量小)。每项含 `myRole`(调用者对该宠物的角色)。
列表按调用者的 pet_owners 关系行过滤,天然隔离他人宠物。
operationId: listPets
security:
- bearerAuth: []
responses:
'200':
description: 宠物列表(created_at DESC,不分页)
content:
application/json:
schema:
$ref: '#/components/schemas/PetListEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
post:
tags: [pets]
summary: 创建宠物
description: |
创建宠物,返回 **201** 与完整 Pet。创建者自动成为 primary owner
pet_owners 写入 role=owner、is_primary=true,与建宠同事务)。
- 品种:`breedId` 与 `customBreedName` 必须**二选一且互斥**(双填、双空、
品种与物种错配、品种不存在或已停用均为 400/40000,message 带具体原因)。
- 芯片号跨用户唯一(uq_pets_microchip):已被登记返回 409/40903。
- 不使用 `Idempotency-Key`:重试安全由唯一约束兜底(带芯片号重发得 40903)。
operationId: createPet
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePetRequest'
responses:
'201':
description: 创建成功,返回完整 PetmyRole 恒为 owner
content:
application/json:
schema:
$ref: '#/components/schemas/PetEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'409':
description: 芯片号已被登记(code 40903
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
microchipExists:
value: { code: 40903, message: 芯片号已被登记, data: null }
/api/v1/pets/{petId}:
get:
tags: [pets]
summary: 宠物详情
description: |
权限档:READ(三角色皆可)。返回宠物详情及 `myRole`(调用者对该宠物的角色,
客户端据此显隐写入口)。
operationId: getPet
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
responses:
'200':
description: 宠物详情(含 myRole
content:
application/json:
schema:
$ref: '#/components/schemas/PetEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PetNotFound'
patch:
tags: [pets]
summary: 更新宠物档案(含头像)
description: |
权限档**按本次请求触及的字段定档**(M3.5 起,ADR-022):
| 请求体触及 | 所需档位 | caregiver | viewer |
| --- | --- | --- | --- |
| 仅 `avatarAssetId`+ `version` | WRITE | ✅ | ✗ 403/40300 |
| 任一资料字段(name/sex/status/…) | MANAGE(仅 owner | ✗ 403/40300 | ✗ 403/40300 |
| 资料字段 + `avatarAssetId` 混合 | MANAGE**取更严的一半** | ✗ 403/40300 | ✗ 403/40300 |
混合请求取更严,是为了堵住「夹带」——否则 caregiver 可把改名塞进头像请求绕过 MANAGE。
- 部分更新:缺席字段不变;资料字段**不支持清空回 null**(M2 语义不回改)。
- **例外:`avatarAssetId` 是本端点唯一的三态字段**(M3.5)——键缺省 = 不改;
键出现且为 `null` = **清除头像**;键出现且有值 = 设置。差异刻意限定在有清空
需求的字段上。
- 例外:品种对(`breedId`/`customBreedName`**整体替换**——提交任一侧即替换
整对,互斥校验同创建。
- `species` 不可改(创建即定,避免与品种配对失效,请求体不含该字段)。
- `status` 可迁移至 active/lost/deceased/archived**`deleted` 不可经 PATCH
设置**(400/40000,软删除留待专用端点,M2 契约不含)。
- `version` **必填**(缺失 400/40000),即便只改头像;比对通过才写入并 +1;
过期 409/40902。头像与资料共用同一把乐观锁——头像变更也应让并发编辑者感知行已变。
- 芯片号改为已被登记的值:409/40903。
- `avatarAssetId` 须为**调用者本人、用途为 `pet_avatar`、状态 `ready`** 的 asset
不存在/非本人/已删/用途不符 404/40405;本人且用途相符但 uploading/failed 422/42203。
operationId: updatePet
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdatePetRequest'
responses:
'200':
description: 更新成功,返回更新后完整 Pet
content:
application/json:
schema:
$ref: '#/components/schemas/PetEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
description: |
宠物不存在、已软删除或调用者与宠物无关系(code 40401,防枚举合并);或
`avatarAssetId` 引用的 asset 不存在/非本人/已删/用途不是 `pet_avatar`
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':
description: 版本冲突(code 40902)或芯片号已被登记(code 40903)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
versionConflict:
value: { code: 40902, message: 数据已被修改,请刷新后重试, data: null }
microchipExists:
value: { code: 40903, message: 芯片号已被登记, data: null }
'422':
$ref: '#/components/responses/MediaNotReady'
/api/v1/breeds:
get:
tags: [dictionaries]
summary: 品种目录
description: |
品种目录(只读字典,非用户数据,仅需 Bearer 鉴权、无用户级权限)。
返回 enabled=true 的品种按 sort_order 排序,全量数组(种子约 30 行,不分页);
`?species=` 过滤,非法取值 400/40000。
operationId: listBreeds
security:
- bearerAuth: []
parameters:
- name: species
in: query
required: false
schema:
type: string
enum: [dog, cat, other]
description: 过滤物种;不传则返回全部
responses:
'200':
description: 品种列表
content:
application/json:
schema:
$ref: '#/components/schemas/BreedListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
/api/v1/pets/{petId}/weights:
get:
tags: [health-records]
summary: 体重记录列表
description: |
权限档:READ。cursor 分页(分页正典形态 `{items, nextCursor, hasMore}`),
按 `measured_at DESC, id DESC` 排序(与索引 ix_pet_weight_pet_measured 逐列对齐,
同刻多条时 id 大者在前)。`limit` 越界或 `cursor` 无效:400/40000。
operationId: listWeights
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/PageLimitParam'
- $ref: '#/components/parameters/PageCursorParam'
responses:
'200':
description: 体重记录分页结果
content:
application/json:
schema:
$ref: '#/components/schemas/WeightListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PetNotFound'
post:
tags: [health-records]
summary: 添加体重记录
description: |
权限档:WRITEowner + caregiver)。返回 **201** 与完整 WeightRecord。
支持可选 `Idempotency-Key`(语义见 info 的「幂等」段)。体重记录 append-only、
无乐观锁;同一时刻允许多条。`weightKg` 范围 (0, 500]、最多两位小数
numeric(6,2)),违反 400/40000。
operationId: createWeight
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateWeightRequest'
responses:
'201':
description: 创建成功(同 Idempotency-Key 重试返回首次创建的记录,同样 201)
content:
application/json:
schema:
$ref: '#/components/schemas/WeightEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/PetNotFound'
/api/v1/vaccine-catalog:
get:
tags: [dictionaries]
summary: 疫苗目录
description: |
疫苗目录(只读字典,非用户数据,仅需 Bearer 鉴权、无用户级权限)。
返回 enabled=true 的疫苗(V4 种子 10 行),`ORDER BY species, name`,不分页;
`?species=` 过滤,非法取值 400/40000。
operationId: listVaccineCatalog
security:
- bearerAuth: []
parameters:
- name: species
in: query
required: false
schema:
type: string
enum: [dog, cat, other]
description: 过滤物种;不传则返回全部
responses:
'200':
description: 疫苗目录列表
content:
application/json:
schema:
$ref: '#/components/schemas/VaccineCatalogListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
/api/v1/pets/{petId}/vaccinations:
get:
tags: [health-records]
summary: 疫苗记录列表
description: |
权限档:READ。**不分页**(单宠疫苗量级为个位数~十位数),排序服务端定死:
`ORDER BY series_key, dose_no, created_at, id`,客户端按系列直接分组成卡。
列表不过滤 status(含 cancelled 行,客户端自行按需过滤)。
operationId: listVaccinations
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
responses:
'200':
description: 疫苗记录列表(不分页,series_key/dose_no 排序)
content:
application/json:
schema:
$ref: '#/components/schemas/VaccinationListEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PetNotFound'
post:
tags: [health-records]
summary: 创建疫苗记录
description: |
权限档:WRITEowner + caregiver)。返回 **201** 与完整 Vaccination。
支持可选 `Idempotency-Key`。
- 创建状态仅 `scheduled` / `completed`(创建即 cancelled 无业务意义,400/40000)。
- 状态-日期规则(违反 422/42201):scheduled 必有 `plannedOn` 且不得带
`administeredOn`completed 必有 `administeredOn``nextDueOn` 与
`administeredOn` 同时存在时须 `nextDueOn ≥ administeredOn`。
- 疫苗必须存在、enabled 且 species 与宠物一致(400/40000)。
- 同宠物同疫苗同系列同剂次的非 cancelled 记录唯一(uq_pet_vaccination_dose):
重复 409/40904cancel 后同剂次可重新登记。
operationId: createVaccination
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateVaccinationRequest'
responses:
'201':
description: 创建成功(同 Idempotency-Key 重试返回首次创建的记录,同样 201)
content:
application/json:
schema:
$ref: '#/components/schemas/VaccinationEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/PetNotFound'
'409':
description: 同系列同剂次非 cancelled 记录已存在(code 40904
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
doseExists:
value: { code: 40904, message: 同系列同剂次记录已存在, data: null }
'422':
description: 状态机或状态-日期规则违反(code 42201)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
ruleViolation:
value: { code: 42201, message: scheduled 状态必须提供 plannedOn, data: null }
/api/v1/vaccinations/{vaccinationId}:
patch:
tags: [health-records]
summary: 更新疫苗记录
description: |
权限档:WRITE。顶层短路径,404/40402 为记录级防枚举语义。
- 部分更新:缺席字段不变;**不支持清空回 null**。
- `vaccineId` / `seriesKey` / `doseNo` 不可改(不在请求体)——登记错剂次的
修正路径是 cancel 后重建。
- `version` 必填(缺失 400/40000),比对通过才写入并 +1;过期 409/40902。
- 状态机:`scheduled → completed`(合并态必须有 administeredOn)、
`scheduled → cancelled`(合并态 administeredOn 必须为空);
**completed 与 cancelled 均为终态**completed→cancelled、cancelled→scheduled
等一律 422/42201);同状态编辑(补批号/备注等)始终允许。
- 校验时点:在「当前行 + 请求字段」的合并态上重跑与创建完全相同的状态-日期
规则,违反 422/42201。
operationId: updateVaccination
security:
- bearerAuth: []
parameters:
- name: vaccinationId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateVaccinationRequest'
responses:
'200':
description: 更新成功,返回更新后完整 Vaccination
content:
application/json:
schema:
$ref: '#/components/schemas/VaccinationEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/RecordNotFound'
'409':
$ref: '#/components/responses/VersionConflict'
'422':
description: 状态机非法迁移或状态-日期规则违反(code 42201)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
terminalState:
value: { code: 42201, message: completed 为终态,不可迁移至 cancelled, data: null }
/api/v1/pets/{petId}/health-events:
get:
tags: [health-records]
summary: 健康事件时间线
description: |
权限档:READ。cursor 分页(分页正典形态),按 `occurred_at DESC, id DESC` 排序
(与索引 ix_health_events_pet_time 逐列对齐)。`limit` 越界或 `cursor` 无效:
400/40000。
operationId: listHealthEvents
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/PageLimitParam'
- $ref: '#/components/parameters/PageCursorParam'
responses:
'200':
description: 健康事件分页结果
content:
application/json:
schema:
$ref: '#/components/schemas/HealthEventListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PetNotFound'
post:
tags: [health-records]
summary: 添加健康事件
description: |
权限档:WRITEowner + caregiver)。返回 **201** 与完整 HealthEvent。
支持可选 `Idempotency-Key`。
- 六类事件类型:medical/feeding/deworming/grooming/measurement/note。
- `createdByUserId` 取自验签 token**不收请求体**、永不可改。
- `title` 服务端 btrimtrim 后为空 400/40000。
- 金额 `amountCents` 以整数分传输、非负、可缺席;**提交小数一律 400/40000**
(不做静默截断)。
operationId: createHealthEvent
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateHealthEventRequest'
responses:
'201':
description: 创建成功(同 Idempotency-Key 重试返回首次创建的记录,同样 201)
content:
application/json:
schema:
$ref: '#/components/schemas/HealthEventEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/PetNotFound'
/api/v1/health-events/{eventId}:
patch:
tags: [health-records]
summary: 更新健康事件
description: |
权限档:WRITE。顶层短路径,404/40402 为记录级防枚举语义。
- **仅可编辑 `title` / `notes` / `amountCents`**`eventType` / `occurredAt`
为时间线条目的身份,不可改(不在请求体);`createdByUserId` 永不可改。
- 部分更新:缺席字段不变;**不支持清空回 null**。
- `version` 必填(缺失 400/40000),比对通过才写入并 +1;过期 409/40902。
- `title` 提交空白串(trim 后为空)400/40000。
operationId: updateHealthEvent
security:
- bearerAuth: []
parameters:
- name: eventId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateHealthEventRequest'
responses:
'200':
description: 更新成功,返回更新后完整 HealthEvent
content:
application/json:
schema:
$ref: '#/components/schemas/HealthEventEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/RecordNotFound'
'409':
$ref: '#/components/responses/VersionConflict'
/api/v1/pets/{petId}/care-reminders:
get:
tags: [health-records]
summary: 照护提醒列表
description: |
权限档:READ。**不分页**(单宠提醒量级小),`ORDER BY due_at ASC, id`
(待办最先到期在前)。`?status=` 白名单过滤(pending/completed/dismissed),
`?status=pending` 即「按 due_at 查询待办」视图;非法取值 400/40000。
operationId: listCareReminders
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- name: status
in: query
required: false
schema:
type: string
enum: [pending, completed, dismissed]
description: 按状态过滤;不传则返回全部
responses:
'200':
description: 提醒列表(不分页,due_at ASC 排序)
content:
application/json:
schema:
$ref: '#/components/schemas/CareReminderListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PetNotFound'
post:
tags: [health-records]
summary: 创建照护提醒
description: |
权限档:WRITEowner + caregiver)。返回 **201** 与完整 CareReminder。
支持可选 `Idempotency-Key`(提醒表无唯一约束兜底,重复提交只能靠键防)。
创建恒为 `pending`(请求体不收 status,多余字段被忽略,与全 API 一致)。
M2 仅 app 内数据,不做推送(ADR-010)。提醒的 title/dueAt 后续编辑与删除端点
不在 M2 契约,改期路径为 dismiss 后重建。
operationId: createCareReminder
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateCareReminderRequest'
responses:
'201':
description: 创建成功,状态恒为 pending(同 Idempotency-Key 重试返回首次创建的记录,同样 201)
content:
application/json:
schema:
$ref: '#/components/schemas/CareReminderEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/PetNotFound'
/api/v1/care-reminders/{reminderId}:
patch:
tags: [health-records]
summary: 更新提醒状态
description: |
权限档:WRITE。顶层短路径,404/40402 为记录级防枚举语义。
**状态流转专用**:请求体仅 `status` + `completedAt`。
- 状态机:`pending → completed`(必带 completedAt)、`pending → dismissed`
(禁带 completedAt);completed / dismissed 为终态;**同状态重放始终允许**
(客户端重试「标记完成」幂等成功)。
- completed-completedAt 一致性(违反 422/42202):`status=completed` 必带
`completedAt`、其余状态禁带;终态互迁与回退 pending 均拒绝。
- `completedAt` 由客户端提交(而非服务端 now()),允许补记实际完成时刻。
- 提醒表无 version 列:并发流转采用当前状态条件更新守卫,读写窗口内被并发
流转抢先则 409/40902(「数据已被修改请刷新」,客户端处理方式与乐观锁一致)。
operationId: updateCareReminder
security:
- bearerAuth: []
parameters:
- name: reminderId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateCareReminderRequest'
responses:
'200':
description: 更新成功,返回更新后完整 CareReminder
content:
application/json:
schema:
$ref: '#/components/schemas/CareReminderEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/RecordNotFound'
'409':
$ref: '#/components/responses/VersionConflict'
'422':
description: 状态机非法迁移或 completed-completedAt 一致性违反(code 42202
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
missingCompletedAt:
value: { code: 42202, message: 标记 completed 必须提供 completedAt, data: null }
/api/v1/pets/{petId}/summary:
get:
tags: [health-records]
summary: 档案聚合摘要
description: |
权限档:READ(三角色皆可读)。实时聚合生成档案页摘要:最新体重、疫苗进度、
下次接种、当月花费——四项聚合全部从事实表实时计算,**无任何写路径**
(不持久化展示字符串)。各聚合口径逐字见 PetSummary schema 字段描述
iteration-2 报告 18 §3 定型)。
`tz`:可选,IANA 时区标识(如 `Asia/Shanghai`,也接受固定偏移如 `+08:00`),
缺省 `UTC`,仅作用于当月花费的月度窗口;非法 tz 或超 64 字符 → 400/40000。
客户端应传自己的时区以获得符合直觉的月边界。
operationId: getPetSummary
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- name: tz
in: query
required: false
schema:
type: string
maxLength: 64
default: UTC
description: IANA 时区标识(如 Asia/Shanghai)或固定偏移(如 +08:00),仅作用于当月花费的月度窗口
example: Asia/Shanghai
responses:
'200':
description: 聚合摘要
content:
application/json:
schema:
$ref: '#/components/schemas/PetSummaryEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'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=uploadingbucket/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/42205asset **保持 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 的 assetuploading/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-op200version 照常 +1)**——同态提交不是迁移,弱网重发不报错;
draft/hidden/archived 目标值由请求枚举拒为 400/40000published→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/me/community-stats:
get:
tags: [posts]
summary: 我的社区数字(获赞总数 / 作品数)
description: |
主体恒为 token 里的调用者:无查询参数、无路径参数,**路径上没有 userId**——
「查不到别人的获赞」不靠权限判断,而是入口本身不存在。
- **统计集合 = 本人的、`status='published'` 的、`deleted_at IS NULL` 的帖**。
草稿不计(尚非作品,且未发布不可被赞);软删不计(删帖即撤回其数字,与
`/me/posts`、Feed 的可见性一致);运营态 hidden/archived 不计(对所有人不可见,
含作者本人);他人帖自然不计。
- **自己赞自己计入**——与帖子详情页的 `likeCount` 保持同一口径,两处数字必须能对上。
- `receivedLikeCount` = 该集合的 `like_count` 之和(读侧实时聚合,读的是写侧同事务
维护的帖级冗余列,故为精确值而非估算;ADR-022 不引入按人累计的冗余列)。
- **空数据返回 `0` 而非 null**,且**永不 404**:任何已认证用户都有 stats。
operationId: getMyCommunityStats
security:
- bearerAuth: []
responses:
'200':
description: 我的获赞总数与作品数
content:
application/json:
schema:
$ref: '#/components/schemas/CommunityStatsEnvelope'
'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 帖子下一次请求即不可见。
卡片形态见 FeedCarditeration-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:
type: http
scheme: bearer
bearerFormat: JWT
description: 'Authorization: Bearer <accessToken>RS256 JWT'
parameters:
PetIdParam:
name: petId
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
IdempotencyKeyHeader:
name: Idempotency-Key
in: header
required: false
schema:
type: string
maxLength: 255
description: |
可选幂等键(≤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:
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 }
PetNotFound:
description: |
宠物不存在、已软删除或调用者与宠物无关系(code 40401)。防枚举语义:三种情况
响应完全一致,随机探测 UUID 无法得知是否命中真实记录。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
petNotFound:
value: { code: 40401, message: 宠物不存在, data: null }
RecordNotFound:
description: |
记录不存在或记录所属宠物对调用者不可见(code 40402)。记录级防枚举语义:
两种情况响应完全一致;只有对宠物可见的调用者才可能收到 403/40300。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
recordNotFound:
value: { code: 40402, message: 记录不存在, data: null }
PetWriteDenied:
description: |
对可见宠物无相应操作权限(code 40300):viewer 写记录或改头像、caregiver/viewer
改宠物档案的资料字段(caregiver 仅改 `avatarAssetId` 时允许,M3.5 按字段分档)。
仅发给对宠物「可见」的调用者,不泄露新信息。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
accessDenied:
value: { code: 40300, message: 无权限执行该操作, data: null }
VersionConflict:
description: |
乐观锁版本冲突(code 40902):提交的 version 已过期(并发修改或重试)。
不静默覆盖,先写者数据保留;客户端刷新取新 version 后重提。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
versionConflict:
value: { code: 40902, message: 数据已被修改,请刷新后重试, data: null }
PostNotFound:
description: |
帖子不存在、已软删、hidden/archived(作者同样)或他人 draftcode 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: |
引用了本人所有但非 readyuploading/failed)状态的 assetcode 42203)。
asset 不存在/非本人/已删则合并为 404/40405。
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
notReady:
value: { code: 42203, message: 媒体尚未就绪, data: null }
schemas:
RegisterRequest:
type: object
required: [username, password]
properties:
username:
type: string
minLength: 3
maxLength: 32
description: 用户名,大小写不敏感唯一
example: demo_user
phone:
type: string
pattern: '^\+[1-9][0-9]{7,14}$'
description: 手机号,E.164 格式;可选,唯一
example: '+8613800138000'
password:
type: string
format: password
minLength: 6
maxLength: 64
example: secret123
nickname:
type: string
minLength: 1
maxLength: 32
description: 昵称;可选(冻结稿之外的可选扩展字段,前端可忽略)
example: 小柴
LoginRequest:
type: object
required: [username, password]
properties:
username:
type: string
example: demo_user
password:
type: string
format: password
example: secret123
RefreshRequest:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
description: 当前持有的 refresh token(不透明随机串)
example: Zx3v…43位base64url…Qk
LogoutRequest:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
description: 要撤销的当前会话的 refresh token
example: Zx3v…43位base64url…Qk
AuthTokens:
type: object
description: 注册 / 登录 / 刷新共用的令牌对(冻结契约,恰好这 6 个字段)
required:
- userId
- tokenType
- accessToken
- accessTokenExpiresAt
- refreshToken
- refreshTokenExpiresAt
properties:
userId:
type: string
format: uuid
example: 019212aa-0000-7000-8000-000000000001
tokenType:
type: string
enum: [Bearer]
example: Bearer
accessToken:
type: string
description: RS256 JWT,有效期 15 分钟(配置项)
example: eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI…
accessTokenExpiresAt:
type: string
format: date-time
description: ISO 8601 带时区
example: '2026-09-04T04:20:06.789Z'
refreshToken:
type: string
description: 不透明随机串,有效期 30 天(配置项),每次刷新轮换
example: Zx3v…43位base64url…Qk
refreshTokenExpiresAt:
type: string
format: date-time
example: '2026-10-04T04:05:06.789Z'
Me:
type: object
description: |
本人资料(`GET` 与 `PATCH /api/v1/me` 的统一响应形态,冻结契约恰好这 6 个字段)。
**不含** `avatarAssetId`:客户端只写不读它,「是否有头像」等价于 `avatarUrl != null`。
required: [userId, username, nickname, avatarUrl, createdAt]
properties:
userId:
type: string
format: uuid
example: 019212aa-0000-7000-8000-000000000001
username:
type: string
example: demo_user
nickname:
type: string
nullable: true
minLength: 1
maxLength: 32
description: |
昵称,**DB 原值**;未设置为 null(键恒在)。长度按**码点**计 1~32(btrim 后)。
**本端点不做 username 回退**——`/me` 是本人编辑态,回退会把展示约定固化成
真实数据;本人视角的展示回退由客户端做 `nickname ?? username`,他人视角的
回退在 `/internal/users/profiles`SQL COALESCE,不属本公开契约)。
不设唯一约束(ADR-022,允许重名)。
example: 小柴
phone:
type: string
nullable: true
description: E.164;未绑定时为 null
example: '+8613800138000'
avatarUrl:
type: string
nullable: true
description: |
头像访问 URL——时效性预签名 GET(TTL 默认 1 小时,配置项),每次响应现签,
**客户端不得持久化、过期即重取**;桶保持私有,无签名直访被拒。
无头像、asset 非 ready、对象存储未配置三种情况均为 null(键恒在)。
example: https://minio.example.com/patbond-media/user_avatar/2026/09/019212aa…?X-Amz-Signature=…
createdAt:
type: string
format: date-time
example: '2026-09-04T04:05:06.789Z'
UpdateMeRequest:
type: object
description: |
本人资料部分更新(**三态语义**,与 pets 域 M2 的两态刻意不同):
**键缺省 = 不改;键出现且为 `null` = 清空;键出现且有值 = 设置**。
两字段都未出现(含只带未声明字段)为**空 patch**,答 400/40000 而非静默 200。
无必填字段、无 `version` 乐观锁、无 `Idempotency-Key`(同 body 重放天然幂等)。
properties:
nickname:
type: string
nullable: true
minLength: 1
maxLength: 32
description: |
昵称;btrim 后长度按**码点**计须在 1~32,否则 400/40000。
显式 `null` = **清空昵称**;纯空白或空串是 400/40000**不是隐式清空**
(清空只留显式 null 一条路,否则「误提交空格」与「想删昵称」无法区分)。
example: 小柴
avatarAssetId:
type: string
format: uuid
nullable: true
description: |
头像 asset ID(两步上传的产物,`purpose` 须为 `user_avatar`)。
显式 `null` = **清除头像**。校验:不存在/非本人/已删/用途不符 404/40405
本人且用途相符但 uploading/failed 422/42203;非法 UUID 400/40000。
**响应不回显该字段**(只写不读)。
example: 019212bb-0000-7000-8000-000000000009
AuthTokenEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/AuthTokens'
MeEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/Me'
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: 40101
message:
type: string
example: token 无效或过期
data:
nullable: true
example: null
TrackEventsRequest:
type: object
required: [events]
properties:
events:
type: array
minItems: 1
maxItems: 50
description: 单批 150 条;越界整批 400/40000
items:
$ref: '#/components/schemas/TrackedEvent'
TrackedEvent:
type: object
required:
- eventId
- eventName
- eventVersion
- anonymousId
- sessionId
- clientTs
- appVersion
- platform
- osVersion
properties:
eventId:
type: string
format: uuid
description: 客户端生成的 UUID(规范要求 v7),服务端幂等去重键
example: 019212aa-4444-7000-8000-000000000001
eventName:
type: string
pattern: '^[a-z][a-z0-9_]{1,63}$'
description: 须在服务端事件字典内;不在字典中的事件名整条 rejectedunknown_event_name
example: auth_login_succeeded
eventVersion:
type: integer
description: 事件 schema 版本(字典 v1 全部为 1)
example: 1
anonymousId:
type: string
format: uuid
description: 设备级匿名标识,首次启动生成
example: 019212aa-0000-7000-8000-000000000001
userId:
type: string
format: uuid
nullable: true
description: |
登录后填充,可选。已认证请求中若与 token subject 不一致,该条
rejectedidentity_mismatch);匿名请求中原样落库,不做校验。
example: 019212aa-0000-7000-8000-000000000001
sessionId:
type: string
format: uuid
description: 客户端会话标识
example: 019212aa-1111-7000-8000-000000000001
clientTs:
type: string
format: date-time
description: 客户端本地时间(ISO 8601 带时区);serverTs 由服务端补写,客户端不发
example: '2026-09-07T04:05:06.789Z'
appVersion:
type: string
minLength: 1
maxLength: 32
example: 1.0.0+12
platform:
type: string
enum: [android, ios]
example: android
osVersion:
type: string
minLength: 1
maxLength: 32
example: android-14
props:
type: object
additionalProperties: true
description: |
事件专有属性,可选。按事件字典白名单处理:白名单外的键剥离后入库
(事件保留);键名命中隐私红线模式(password/token/secret/phone/
mobile/email/credential/idfa/gaid,不区分大小写、子串匹配)则整条
rejectedforbidden_field)。
example: { identifierType: username, durationMs: 123 }
TrackEventsResult:
type: object
description: 批次逐条结果(results 与请求 events 等长、按原顺序对应)
required: [accepted, duplicated, rejected, results]
properties:
accepted:
type: integer
description: 新落库条数
example: 1
duplicated:
type: integer
description: eventId 去重命中条数(视为成功,客户端不必重试)
example: 0
rejected:
type: integer
description: 被拒条数(客户端不重试)
example: 0
results:
type: array
items:
$ref: '#/components/schemas/EventResult'
EventResult:
type: object
required: [eventId, status]
properties:
eventId:
type: string
format: uuid
example: 019212aa-4444-7000-8000-000000000001
status:
type: string
enum: [accepted, duplicate, rejected]
example: accepted
reason:
type: string
enum: [unknown_event_name, identity_mismatch, forbidden_field, schema_invalid]
description: 仅 status=rejected 时出现(accepted/duplicate 不含该字段)
example: unknown_event_name
TrackEventsEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/TrackEventsResult'
# ==================================================================
# Pets 域 schemasM2 冻结;响应主键统一裸 `id`,关联字段带类型名)
# ==================================================================
Pet:
type: object
description: |
宠物档案(列表 / 详情 / 创建 / 更新的统一响应形态,皆含 myRole)。
`breedId` 与 `customBreedName` 恰有其一非空(ck_pets_breed);
`breedDisplayName` 由品种字典解出,随 breedId 存在。
软删除态(deleted)的宠物在全部端点表现为 404/40401,本 schema 的
status 永不出现 deleted。头像以 `avatarUrl` 的现签形式返回,
**`avatarAssetId` 不外露**(只写不读,写入口为 `PATCH /api/v1/pets/{petId}`)。
required:
- id
- name
- species
- sex
- birthDateEstimated
- status
- avatarUrl
- myRole
- createdAt
- updatedAt
- version
properties:
id:
type: string
format: uuid
name:
type: string
minLength: 1
maxLength: 64
species:
type: string
enum: [dog, cat, other]
description: 物种;创建即定,不可修改
breedId:
type: string
format: uuid
nullable: true
description: 品种 ID(与 customBreedName 互斥,恰有其一非空)
breedDisplayName:
type: string
nullable: true
description: 品种展示名,由字典解出,随 breedId 存在
customBreedName:
type: string
nullable: true
minLength: 1
maxLength: 64
description: 自定义品种名(与 breedId 互斥)
sex:
type: string
enum: [male, female, unknown]
birthDate:
type: string
format: date
nullable: true
description: 生日(YYYY-MM-DD
birthDateEstimated:
type: boolean
description: 生日是否为估计值
personality:
type: string
nullable: true
maxLength: 64
description: 性格标签
microchipNo:
type: string
nullable: true
description: 芯片号(跨用户唯一)
sterilizedOn:
type: string
format: date
nullable: true
description: 绝育日期
status:
type: string
enum: [active, lost, deceased, archived]
description: 状态(deleted 为内部软删态,接口永不返回;软删宠物一律 404/40401)
avatarUrl:
type: string
nullable: true
description: |
宠物头像访问 URL——时效性预签名 GET(TTL 默认 1 小时,配置项),每次响应现签,
**客户端不得持久化、过期即重取**;桶保持私有,无签名直访被拒。
无头像、asset 非 ready、对象存储未配置三种情况均为 null(键恒在)。
asset 事后退出 ready 时转 null 而引用保留(读请求不做写副作用)。
example: https://minio.example.com/patbond-media/pet_avatar/2026/09/019212cc…?X-Amz-Signature=…
myRole:
type: string
enum: [owner, caregiver, viewer]
description: 调用者对该宠物的权限角色(客户端据此显隐写入口)
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
version:
type: integer
description: 乐观锁版本号(PATCH 时必须提交)
CreatePetRequest:
type: object
required: [name, species, sex]
properties:
name:
type: string
minLength: 1
maxLength: 64
species:
type: string
enum: [dog, cat, other]
description: 创建即定,之后不可修改
breedId:
type: string
format: uuid
description: 品种 ID(与 customBreedName 二选一且互斥;双填/双空/物种错配/品种不存在或停用 → 400/40000)
customBreedName:
type: string
minLength: 1
maxLength: 64
description: 自定义品种名(与 breedId 二选一且互斥)
sex:
type: string
enum: [male, female, unknown]
birthDate:
type: string
format: date
birthDateEstimated:
type: boolean
default: false
personality:
type: string
maxLength: 64
microchipNo:
type: string
description: 芯片号;已被登记 → 409/40903
sterilizedOn:
type: string
format: date
UpdatePetRequest:
type: object
description: |
部分更新:缺席字段不变;资料字段不支持清空回 null。例外:品种对
breedId/customBreedName)整体替换——提交任一侧即替换整对,互斥校验同创建。
species 不可改(不在请求体)。
**`avatarAssetId` 是本 schema 唯一的三态字段**(M3.5):键缺省 = 不改;
键出现且为 `null` = 清除头像;键出现且有值 = 设置。其余字段保持 M2 两态语义。
权限档按本次触及的字段决定(仅头像 → WRITE;触及资料字段 → MANAGE;混合取更严),
见端点描述。
required: [version]
properties:
version:
type: integer
description: 当前持有的版本号(乐观锁,必填;缺失 400/40000,过期 409/40902);即便只改头像也必带
name:
type: string
minLength: 1
maxLength: 64
breedId:
type: string
format: uuid
description: 品种对整体替换(与 customBreedName 互斥)
customBreedName:
type: string
minLength: 1
maxLength: 64
description: 品种对整体替换(与 breedId 互斥)
sex:
type: string
enum: [male, female, unknown]
birthDate:
type: string
format: date
birthDateEstimated:
type: boolean
personality:
type: string
maxLength: 64
microchipNo:
type: string
description: 已被登记 → 409/40903
sterilizedOn:
type: string
format: date
status:
type: string
enum: [active, lost, deceased, archived]
description: 状态流转;deleted 不可经 PATCH 设置(400/40000
avatarAssetId:
type: string
format: uuid
nullable: true
description: |
宠物头像 asset ID(两步上传的产物,`purpose` 须为 `pet_avatar`)。
**三态**:缺省 = 不改;显式 `null` = 清除头像;给值 = 设置。校验:不存在/
非本人/已删/用途不符 404/40405;本人且用途相符但 uploading/failed 422/42203
非法 UUID 400/40000。**响应不回显该字段**(只写不读,读取见 `Pet.avatarUrl`)。
example: 019212cc-0000-7000-8000-00000000000a
PetEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/Pet'
PetListEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
type: array
description: created_at DESC 排序,不分页
items:
$ref: '#/components/schemas/Pet'
Breed:
type: object
required: [id, species, code, displayName]
properties:
id:
type: string
format: uuid
species:
type: string
enum: [dog, cat, other]
code:
type: string
description: 品种代码(唯一标识)
displayName:
type: string
description: 展示名称
BreedListEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
type: array
items:
$ref: '#/components/schemas/Breed'
WeightRecord:
type: object
required: [id, petId, weightKg, measuredAt, source, createdAt]
properties:
id:
type: string
format: uuid
petId:
type: string
format: uuid
weightKg:
type: number
format: double
minimum: 0.01
maximum: 500
description: 体重(公斤),最多两位小数(numeric(6,2)
measuredAt:
type: string
format: date-time
description: 称重时间
source:
type: string
enum: [manual, clinic, device]
description: 来源
note:
type: string
nullable: true
maxLength: 500
createdAt:
type: string
format: date-time
CreateWeightRequest:
type: object
required: [weightKg, measuredAt]
properties:
weightKg:
type: number
format: double
minimum: 0.01
maximum: 500
description: 体重(公斤),(0, 500],最多两位小数;越界或三位小数 400/40000
measuredAt:
type: string
format: date-time
source:
type: string
enum: [manual, clinic, device]
default: manual
note:
type: string
maxLength: 500
WeightEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/WeightRecord'
WeightListEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
type: object
description: cursor 分页正典信封;排序 measured_at DESC, id DESC
required: [items, hasMore]
properties:
items:
type: array
items:
$ref: '#/components/schemas/WeightRecord'
nextCursor:
type: string
nullable: true
description: 下一页游标(不透明 base64url),hasMore=false 时恒为 null
hasMore:
type: boolean
VaccineCatalogItem:
type: object
required: [id, code, name, species]
properties:
id:
type: string
format: uuid
code:
type: string
description: 疫苗代码(唯一标识)
name:
type: string
description: 疫苗名称
species:
type: string
enum: [dog, cat, other]
description:
type: string
nullable: true
maxLength: 500
VaccineCatalogListEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
type: array
items:
$ref: '#/components/schemas/VaccineCatalogItem'
Vaccination:
type: object
description: |
疫苗记录。`vaccineName` 由疫苗目录解出(同 Pet.breedDisplayName 先例,列表页免
二次查字典)。`certificateAssetId / providerId / providerNameSnapshot / bookingId`
整体不出现(ADR-010,M5 时纯增量补入)。
required:
- id
- petId
- vaccineId
- vaccineName
- seriesKey
- doseNo
- status
- createdAt
- updatedAt
- version
properties:
id:
type: string
format: uuid
petId:
type: string
format: uuid
vaccineId:
type: string
format: uuid
vaccineName:
type: string
description: 疫苗名称(出自疫苗目录)
seriesKey:
type: string
minLength: 1
maxLength: 64
description: 系列键(区分初次/加强等,与 doseNo 共同唯一);创建后不可改
doseNo:
type: integer
minimum: 1
maximum: 32767
description: 剂次号(smallint);创建后不可改
doseLabel:
type: string
nullable: true
maxLength: 64
description: 剂次标签(如「第一针」)
status:
type: string
enum: [scheduled, completed, cancelled]
plannedOn:
type: string
format: date
nullable: true
description: 计划接种日期(scheduled 必有)
administeredOn:
type: string
format: date
nullable: true
description: 实际接种日期(completed 必有;scheduled/cancelled 必空)
nextDueOn:
type: string
format: date
nullable: true
description: 下次到期日期(与 administeredOn 同时存在时 ≥ administeredOn
manufacturer:
type: string
nullable: true
maxLength: 128
batchNo:
type: string
nullable: true
maxLength: 64
notes:
type: string
nullable: true
maxLength: 1000
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
version:
type: integer
description: 乐观锁版本号(PATCH 时必须提交)
CreateVaccinationRequest:
type: object
description: |
创建状态仅 scheduled / completed(创建即 cancelled 无业务意义,400/40000)。
疫苗必须存在、enabled 且 species 与宠物一致(400/40000)。
状态-日期规则违反 → 422/42201。
required: [vaccineId, seriesKey, doseNo, status]
properties:
vaccineId:
type: string
format: uuid
seriesKey:
type: string
minLength: 1
maxLength: 64
doseNo:
type: integer
minimum: 1
maximum: 32767
doseLabel:
type: string
maxLength: 64
status:
type: string
enum: [scheduled, completed]
plannedOn:
type: string
format: date
description: scheduled 状态必填
administeredOn:
type: string
format: date
description: completed 状态必填;scheduled 不得携带
nextDueOn:
type: string
format: date
description: 与 administeredOn 同时存在时须 ≥ administeredOn
manufacturer:
type: string
maxLength: 128
batchNo:
type: string
maxLength: 64
notes:
type: string
maxLength: 1000
UpdateVaccinationRequest:
type: object
description: |
部分更新:缺席字段不变;不支持清空回 null。vaccineId / seriesKey / doseNo
不可改(不在请求体)——登记错剂次的修正路径是 cancel 后重建。
合并态重跑与创建相同的状态-日期规则,违反 422/42201。
required: [version]
properties:
version:
type: integer
description: 乐观锁(必填;缺失 400/40000,过期 409/40902
status:
type: string
enum: [scheduled, completed, cancelled]
description: scheduled→completed / scheduled→cancelledcompleted 与 cancelled 均为终态(非法迁移 422/42201);同状态编辑始终允许
plannedOn:
type: string
format: date
administeredOn:
type: string
format: date
nextDueOn:
type: string
format: date
doseLabel:
type: string
maxLength: 64
manufacturer:
type: string
maxLength: 128
batchNo:
type: string
maxLength: 64
notes:
type: string
maxLength: 1000
VaccinationEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/Vaccination'
VaccinationListEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
type: array
description: 不分页;ORDER BY series_key, dose_no, created_at, id(含 cancelled 行)
items:
$ref: '#/components/schemas/Vaccination'
HealthEvent:
type: object
description: |
健康事件。`providerId / providerNameSnapshot / bookingId` 整体不出现
(ADR-010,M5 时纯增量补入)。
required:
- id
- petId
- eventType
- occurredAt
- title
- createdByUserId
- createdAt
- updatedAt
- version
properties:
id:
type: string
format: uuid
petId:
type: string
format: uuid
eventType:
type: string
enum: [medical, feeding, deworming, grooming, measurement, note]
description: 事件类型;创建后不可改
occurredAt:
type: string
format: date-time
description: 事件发生时间;创建后不可改
title:
type: string
minLength: 1
maxLength: 160
description: 标题(服务端 btrimtrim 后为空 400/40000
notes:
type: string
nullable: true
maxLength: 2000
description: 备注(上限 2000 字符)
amountCents:
type: integer
format: int64
nullable: true
minimum: 0
description: 金额(整数分,非负);提交小数 400/40000(不做静默截断)
createdByUserId:
type: string
format: uuid
description: 创建者用户 ID(取自验签 token,永不可改)
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
version:
type: integer
description: 乐观锁版本号(PATCH 时必须提交)
CreateHealthEventRequest:
type: object
required: [eventType, occurredAt, title]
properties:
eventType:
type: string
enum: [medical, feeding, deworming, grooming, measurement, note]
occurredAt:
type: string
format: date-time
title:
type: string
minLength: 1
maxLength: 160
notes:
type: string
maxLength: 2000
amountCents:
type: integer
format: int64
minimum: 0
description: 金额(整数分,非负);提交小数 400/40000
UpdateHealthEventRequest:
type: object
description: |
部分更新:缺席字段不变;不支持清空回 null。仅可编辑 title / notes / amountCents
eventType / occurredAt / createdByUserId 不可改(不在请求体)。
required: [version]
properties:
version:
type: integer
description: 乐观锁(必填;缺失 400/40000,过期 409/40902
title:
type: string
minLength: 1
maxLength: 160
description: 提交空白串(trim 后为空)400/40000
notes:
type: string
maxLength: 2000
amountCents:
type: integer
format: int64
minimum: 0
description: 提交小数 400/40000
HealthEventEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/HealthEvent'
HealthEventListEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
type: object
description: cursor 分页正典信封;排序 occurred_at DESC, id DESC
required: [items, hasMore]
properties:
items:
type: array
items:
$ref: '#/components/schemas/HealthEvent'
nextCursor:
type: string
nullable: true
description: 下一页游标(不透明 base64url),hasMore=false 时恒为 null
hasMore:
type: boolean
CareReminder:
type: object
description: |
照护提醒。**无 version 字段**care_reminders 表无该列,状态流转用当前状态
条件更新守卫,守卫落空 409/40902)。completedAt 非空当且仅当 status=completed。
required:
- id
- petId
- reminderType
- title
- dueAt
- status
- createdAt
- updatedAt
properties:
id:
type: string
format: uuid
petId:
type: string
format: uuid
reminderType:
type: string
enum: [deworming, checkup, medication, other]
title:
type: string
minLength: 1
maxLength: 160
dueAt:
type: string
format: date-time
description: 到期时间
status:
type: string
enum: [pending, completed, dismissed]
completedAt:
type: string
format: date-time
nullable: true
description: 完成时间;非空当且仅当 status=completed
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
CreateCareReminderRequest:
type: object
description: 创建恒为 pending(不收 status 字段,多余字段被忽略)
required: [reminderType, title, dueAt]
properties:
reminderType:
type: string
enum: [deworming, checkup, medication, other]
title:
type: string
minLength: 1
maxLength: 160
dueAt:
type: string
format: date-time
UpdateCareReminderRequest:
type: object
description: |
状态流转专用(仅 status + completedAt)。pending→completed 必带 completedAt、
pending→dismissed 禁带;终态互迁与回退 pending 拒绝(422/42202);
同状态重放始终允许(幂等成功)。completedAt 由客户端提交,允许补记实际完成时刻。
required: [status]
properties:
status:
type: string
enum: [pending, completed, dismissed]
completedAt:
type: string
format: date-time
description: status=completed 时必填;其余状态禁带(违反 422/42202)
CareReminderEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/CareReminder'
CareReminderListEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
type: array
description: 不分页;ORDER BY due_at ASC, id;支持 ?status= 白名单过滤
items:
$ref: '#/components/schemas/CareReminder'
PetSummary:
type: object
description: |
档案聚合摘要(四项聚合全部从事实表实时计算,无持久化;口径为 iteration-2
报告 18 §3 定型表逐字收录)。latestWeight / vaccinationProgress /
nextVaccination 三项可为 null(无对应记录);monthlyExpense 恒非 null。
required: [petId, monthlyExpense]
properties:
petId:
type: string
format: uuid
description: 恒非 null,回显路径参数
latestWeight:
type: object
nullable: true
description: |
最新体重。口径:pet_weight_records 按 (measured_at DESC, id DESC) 取首行——
与体重列表接口首行完全一致(同一索引 ix_pet_weight_pet_measured、同一
tie-break),同刻多条时后写入者(id 更大)胜出。无记录 → null。
required: [weightKg, measuredAt]
properties:
weightKg:
type: number
format: double
description: 两位小数(numeric(6,2)),对象存在时非 null
measuredAt:
type: string
format: date-time
description: 对象存在时非 null
vaccinationProgress:
type: object
nullable: true
description: |
疫苗进度。口径:范围 = 该宠物非 cancelled 的 pet_vaccinations 行。
completedDoses = 其中 status=completed 的行数;totalDoses = 全部非 cancelled
行数(= scheduled + completed,即「已登记剂次」——数据模型没有权威的
「系列应打总针数」,分母取用户已登记数)。cancelled 分子分母皆不计入。
totalDoses=0 → 整体 null**不是 0/0**)。
required: [completedDoses, totalDoses]
properties:
completedDoses:
type: integer
minimum: 0
description: 已完成剂次,对象存在时非 null
totalDoses:
type: integer
minimum: 1
description: 已登记剂次(scheduled + completed),对象存在时非 null=0 即整体 null
nextVaccination:
type: object
nullable: true
description: |
下次接种。口径:候选集两类并集:① 全部 scheduled 行的 planned_on(约束保证
非空;含过期——逾期计划在完成/取消前仍是下一针),source=planned
② completed 行的非空 next_due_on,仅当同 (pet, vaccine, series_key) 不存在
更高 dose_no 的非 cancelled 记录(后续针一经登记,其自身即代表下一针,
前一针的到期日失效),source=nextDue。cancelled 行不产生任何候选。
取 dueOn 最小者;同日 planned 优先于 nextDue,再按 id 升序保证确定性。
候选集空 → null。
required: [vaccinationId, vaccineId, vaccineName, doseNo, dueOn, source]
properties:
vaccinationId:
type: string
format: uuid
description: 命中的疫苗记录 id(客户端可跳详情),非 null
vaccineId:
type: string
format: uuid
description: 非 null
vaccineName:
type: string
description: 非 null,出自 vaccine_catalog(同 breedDisplayName 先例)
doseNo:
type: integer
description: 非 null
doseLabel:
type: string
nullable: true
description: 记录本身可无标签
dueOn:
type: string
format: date
description: 非 null;**可为过去日期**(逾期针仍是下一针)
source:
type: string
enum: [planned, nextDue]
description: 非 null,标注取值来源(scheduled 的 plannedOn 或 completed 的 nextDueOn
monthlyExpense:
type: object
description: |
当月花费,**恒非 null**(月份/时区总可确定)。口径:health_events.amount_cents
求和,窗口为请求时刻在 tz 时区的自然月半开区间 [当月1日00:00, 次月1日00:00)
对 occurred_attimestamptz)比较;月初第一刻含、次月第一刻不含。
amount_cents 为 NULL 的事件不计入;不按 event_type 过滤(任何事件类型的
金额都算支出)。tz 缺省 UTC,客户端应传自己的时区获得符合直觉的月边界——
月边界随 tz 移动。恒返回对象:month 为窗口所属 ISO 年月、timezone 回显、
无支出 amountCents=0。
required: [month, timezone, amountCents]
properties:
month:
type: string
description: ISO year-month(如 2026-09),非 null
example: '2026-09'
timezone:
type: string
description: 回显窗口所用时区(缺省 UTC),非 null
example: UTC
amountCents:
type: integer
format: int64
minimum: 0
description: 非 null,无支出为 0
PetSummaryEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/PetSummary'
# ==================================================================
# Community / Media 域 schemasM3 冻结;定型依据:iteration-3 报告 13/15/16/17
# ==================================================================
AuthorSummary:
type: object
description: |
作者公开摘要(D3-9 方案 Biteration-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 仅 imageADR-018 视频后置;video/document 为向后新增枚举预留)
purpose:
type: string
enum: [post_image, user_avatar, pet_avatar]
description: |
用途白名单(服务端配置项 `patbond.media.allowed-purposes`,决定 objectKey 前缀
`<purpose>/yyyy/MM/{assetId}`)。M3.5 起为三值:`post_image`(帖子配图)、
`user_avatar`(用户头像)、`pet_avatar`(宠物头像)——相对 M3 的纯枚举追加。
**用途即引用侧的类型检查**:引用时校验 `purpose` 相符,故帖图不能当头像、
两种头像也互不通用(不符者 404/40405)。白名单外的取值 400/40000。
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-016iteration-3 报告 13 定型)
required: [assetId, uploadUrl, method, requiredHeaders, expiresAt]
properties:
assetId:
type: string
format: uuid
description: 已登记的 asset IDstatus=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: 至多一个 trueuq_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(200version 照常 +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 恒 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
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~2000ck_comments_content 同宽)
replyToUserId:
type: string
format: uuid
description: |
可选 @ 回复目标(单层平铺,无 parentCommentIdADR-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'
CommunityStats:
type: object
description: |
调用者本人的社区数字(M3.5)。两数同一集合:本人的、`status='published'` 的、
未软删的帖(草稿 / 软删 / hidden / archived 均不计)。读侧实时聚合,无冗余计数列。
required: [receivedLikeCount, publishedPostCount]
properties:
receivedLikeCount:
type: integer
format: int64
description: |
获赞总数 = 该集合的 `like_count` 之和(写侧同事务维护的帖级冗余列,精确值)。
**自己赞自己计入**,与帖子详情的 `likeCount` 同一口径。空数据为 0,非 null。
example: 128
publishedPostCount:
type: integer
format: int64
description: 作品数 = 该集合的帖子数。空数据为 0,非 null。
example: 12
CommunityStatsEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/CommunityStats'