5f02909af6
CI / docs-build (push) Failing after 1s
按 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>
4148 lines
158 KiB
YAML
4148 lines
158 KiB
YAML
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_FOUND:asset 不存在、非本人所有、已删,或**用途与引用场景不符**(帖图当头像、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:引用了本人所有、用途相符但非 ready(uploading/failed)状态的 asset(帖图与用户/宠物头像同构) |
|
||
| 42204 | 422 | FOLLOW_RULE_VIOLATION:自关注(仅 PUT;自取关为 200 幂等 no-op) |
|
||
| 42205 | 422 | MEDIA_UPLOAD_STATE_INVALID:complete 时 asset 非 uploading——对象未上传保持可重试、大小/类型不符置 failed 终态、failed 态再确认;已 ready 幂等 200 除外 |
|
||
| 42300 | 423 | 登录失败次数过多,账号已临时锁定(见下) |
|
||
| 50000 | 500 | 服务器内部错误 |
|
||
| 50300 | 503 | 依赖服务暂不可用 |
|
||
|
||
## 会话模型(ADR-003,数值均为服务端配置项)
|
||
- access token:JWT(RS256),有效期 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-015:owner/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: 宠物档案 CRUD(patbond-pet)
|
||
- name: dictionaries
|
||
description: 品种与疫苗目录(只读字典,patbond-pet)
|
||
- name: health-records
|
||
description: 体重、疫苗、健康事件、照护提醒、档案摘要(patbond-pet)
|
||
- name: media
|
||
description: 媒体上传两步流程(patbond-user,ADR-016 预签名直传)
|
||
- name: posts
|
||
description: |
|
||
帖子生命周期:草稿/编辑/发布/删除/详情/我的帖子;含由帖子派生的「我的社区数字」
|
||
聚合(patbond-community)
|
||
- name: feed
|
||
description: 公共 Feed 游标分页(patbond-community)
|
||
- name: comments
|
||
description: 单层平铺评论 + @ 回复(patbond-community,ADR-018)
|
||
- name: interactions
|
||
description: 点赞/收藏 PUT+DELETE 幂等与收藏列表(patbond-community,ADR-019)
|
||
- name: follows
|
||
description: 关注最小数据接口:follow/unfollow + 计数(patbond-community,ADR-018)
|
||
|
||
paths:
|
||
/api/v1/auth/register:
|
||
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: 创建成功,返回完整 Pet(myRole 恒为 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: |
|
||
权限档:WRITE(owner + 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: |
|
||
权限档:WRITE(owner + 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/40904;cancel 后同剂次可重新登记。
|
||
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: |
|
||
权限档:WRITE(owner + caregiver)。返回 **201** 与完整 HealthEvent。
|
||
支持可选 `Idempotency-Key`。
|
||
|
||
- 六类事件类型:medical/feeding/deworming/grooming/measurement/note。
|
||
- `createdByUserId` 取自验签 token,**不收请求体**、永不可改。
|
||
- `title` 服务端 btrim,trim 后为空 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: |
|
||
权限档:WRITE(owner + 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=uploading,bucket/objectKey
|
||
服务端生成、不含任何用户输入)→ 返回预签名 PUT 直传凭据(TTL 10 分钟,配置项)。
|
||
客户端凭凭据直传对象存储,不经应用服务器;直传必须**原样携带 requiredHeaders**
|
||
(Content-Type 已签进签名,改动即被存储侧拒绝)。M3 仅 `kind=image`
|
||
(ADR-018 视频后置;video/document 为向后新增枚举预留)。
|
||
operationId: createMediaUpload
|
||
security:
|
||
- bearerAuth: []
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CreateMediaUploadRequest'
|
||
responses:
|
||
'201':
|
||
description: asset 已登记(uploading),返回直传凭据
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/MediaUploadEnvelope'
|
||
'400':
|
||
$ref: '#/components/responses/ValidationError'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
|
||
/api/v1/media/uploads/{assetId}/complete:
|
||
post:
|
||
tags: [media]
|
||
summary: 确认上传完成(uploading → ready)
|
||
description: |
|
||
两步上传第二步:服务端对对象 HEAD 校验存在性与 byteSize/Content-Type →
|
||
uploading→ready、写 readyAt,返回可引用的 asset(含现签预签名 GET URL)。
|
||
|
||
- **幂等**:对已 ready 的 asset 重复 complete 返回 200 同一 asset(现签新 GET URL)。
|
||
- 对象尚不存在(直传完成前确认)→ 422/42205,asset **保持 uploading 可重试**
|
||
(补传后再确认即恢复,凭据未过期时无须重新创建上传)。
|
||
- 对象存在但大小/类型与登记不符 → 置 failed(终态),422/42205,须重新创建上传。
|
||
- failed 态再确认 → 422/42205(终态);不存在/非本人/已删 → 404/40405(防枚举合并)。
|
||
- `sha256` 照收照存,M3 不做内容核验(存储侧 HEAD 不返回内容散列;后续经
|
||
存储侧 checksum 特性补齐,不改契约形态)。
|
||
operationId: completeMediaUpload
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/AssetIdParam'
|
||
responses:
|
||
'200':
|
||
description: 确认成功(或幂等重复确认),asset 为 ready
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/MediaAssetEnvelope'
|
||
'400':
|
||
$ref: '#/components/responses/ValidationError'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
$ref: '#/components/responses/MediaNotFound'
|
||
'422':
|
||
description: |
|
||
asset 非 uploading 态或对象校验未通过(code 42205):对象未上传保持可重试、
|
||
大小/类型不符置 failed 终态、failed 态再确认(已 ready 幂等 200 除外)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
stateInvalid:
|
||
value: { code: 42205, message: 上传状态不允许确认, data: null }
|
||
|
||
/api/v1/posts:
|
||
post:
|
||
tags: [posts]
|
||
summary: 创建帖子(草稿或直接发布)
|
||
description: |
|
||
`Idempotency-Key` **必带**(语义见该头参数描述与 info「Community / Media 域
|
||
约定」)。`status` 可 draft(缺省)或 published(直接发布,服务端写
|
||
publishedAt)。纯文字帖合法(media 空数组或缺席,D3-4)。
|
||
|
||
media 挂接(每帖 ≤9 图):只接受本人所有且 ready 的 asset(uploading/failed
|
||
422/42203;不存在/非本人/已删 404/40405);`position` **全给或全不给**——全给
|
||
须恰为 0..n-1 连续不重复,全不给按数组序,混合 400/40000;`isCover` 至多一个
|
||
true,全 false 时服务端将 position 0 行落库置为封面(库内恒有唯一封面行);
|
||
同帖 assetId 不重复;caption trim 后 ≤300。
|
||
`petId` 须为调用者可见宠物,否则 404/40401(沿 pets 域防枚举语义)。
|
||
operationId: createPost
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/IdempotencyKeyRequiredHeader'
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CreatePostRequest'
|
||
responses:
|
||
'201':
|
||
description: 创建成功(或同键幂等重试返回首次结果)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/PostEnvelope'
|
||
'400':
|
||
$ref: '#/components/responses/ValidationError'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
description: petId 引用不可见宠物(code 40401)或 media 引用的 asset 不存在/非本人/已删(code 40405)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
petNotFound:
|
||
value: { code: 40401, message: 宠物不存在, data: null }
|
||
mediaNotFound:
|
||
value: { code: 40405, message: 媒体资源不存在, data: null }
|
||
'409':
|
||
$ref: '#/components/responses/IdempotencyPayloadMismatch'
|
||
'422':
|
||
$ref: '#/components/responses/MediaNotReady'
|
||
|
||
/api/v1/posts/{postId}:
|
||
get:
|
||
tags: [posts]
|
||
summary: 帖子详情
|
||
description: |
|
||
权限矩阵(iteration-3 报告 15 定型):published 对全部登录用户开放;draft 仅
|
||
作者可见;hidden/archived(运营态)**对作者同样 404/40403**——M3 无端点能产生
|
||
或解除运营态,status 枚举保持两值。一切不可见情形响应完全一致(防枚举)。
|
||
响应含 likedByMe/bookmarkedByMe 与作者公开摘要(AuthorSummary)。
|
||
operationId: getPost
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PostIdParam'
|
||
responses:
|
||
'200':
|
||
description: 帖子详情
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/PostEnvelope'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
$ref: '#/components/responses/PostNotFound'
|
||
patch:
|
||
tags: [posts]
|
||
summary: 编辑帖子 / 发布草稿(部分更新 + version 乐观锁)
|
||
description: |
|
||
仅作者(非作者对已发布帖 403/40301;一切不可见情形——含他人 draft——404/40403)。
|
||
PATCH 部分更新惯例:缺席字段不变,不支持清空回 null(M2 先例)。`version`
|
||
必带(缺失 400/40000,过期 409/40902)。
|
||
|
||
- **发布** = `status: published` 的状态迁移(draft→published 是唯一开放迁移,
|
||
服务端写 publishedAt,恰写一次);**对已发布帖重复提交 `status: published`
|
||
为幂等 no-op(200,version 照常 +1)**——同态提交不是迁移,弱网重发不报错;
|
||
draft/hidden/archived 目标值由请求枚举拒为 400/40000(published→draft 不支持)。
|
||
- media 出现即**整组替换**(删旧插新;`[]` 清空为纯文字帖;缺席不动),
|
||
校验规则同创建。
|
||
operationId: updatePost
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PostIdParam'
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/UpdatePostRequest'
|
||
responses:
|
||
'200':
|
||
description: 更新成功,返回新 version 的完整帖子
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/PostEnvelope'
|
||
'400':
|
||
$ref: '#/components/responses/ValidationError'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'403':
|
||
$ref: '#/components/responses/PostAccessDenied'
|
||
'404':
|
||
description: |
|
||
帖子不可见(code 40403,防枚举合并);或 petId 引用不可见宠物(code 40401);
|
||
或 media 引用的 asset 不存在/非本人/已删(code 40405)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
postNotFound:
|
||
value: { code: 40403, message: 帖子不存在, data: null }
|
||
petNotFound:
|
||
value: { code: 40401, message: 宠物不存在, data: null }
|
||
mediaNotFound:
|
||
value: { code: 40405, message: 媒体资源不存在, data: null }
|
||
'409':
|
||
$ref: '#/components/responses/VersionConflict'
|
||
'422':
|
||
$ref: '#/components/responses/MediaNotReady'
|
||
delete:
|
||
tags: [posts]
|
||
summary: 删除帖子(软删,仅作者)
|
||
description: |
|
||
软删(deleted_at 为全域唯一删除判定基准),删除后详情/Feed/列表/互动一切路径
|
||
404/40403。重复删除与删不存在的帖同响应 404/40403(防枚举合并)。
|
||
不提供恢复端点(M3 无回收站)。非作者对已发布帖 403/40301。
|
||
operationId: deletePost
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PostIdParam'
|
||
responses:
|
||
'200':
|
||
description: 删除成功
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/VoidEnvelope'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'403':
|
||
$ref: '#/components/responses/PostAccessDenied'
|
||
'404':
|
||
$ref: '#/components/responses/PostNotFound'
|
||
|
||
/api/v1/me/posts:
|
||
get:
|
||
tags: [posts]
|
||
summary: 我的帖子列表(含草稿)
|
||
description: |
|
||
作者视角:含 draft 与 published(软删不含,hidden/archived 不含)。排序
|
||
`(created_at DESC, id DESC)` 走 `ix_posts_author_created`,keyset 游标。
|
||
`status` 过滤可选(draft|published)。
|
||
operationId: listMyPosts
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PageLimitParam'
|
||
- $ref: '#/components/parameters/PageCursorParam'
|
||
- name: status
|
||
in: query
|
||
required: false
|
||
schema:
|
||
type: string
|
||
enum: [draft, published]
|
||
description: 按状态过滤;缺省返回全部(不含已删)
|
||
responses:
|
||
'200':
|
||
description: cursor 分页帖子列表(完整 Post 形态)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/PostListEnvelope'
|
||
'400':
|
||
$ref: '#/components/responses/ValidationError'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
|
||
/api/v1/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 帖子下一次请求即不可见。
|
||
卡片形态见 FeedCard(iteration-3 报告 16 定型);likedByMe/bookmarkedByMe
|
||
为当前用户视角。
|
||
operationId: getFeed
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PageLimitParam'
|
||
- $ref: '#/components/parameters/PageCursorParam'
|
||
responses:
|
||
'200':
|
||
description: cursor 分页 Feed 卡片列表
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/FeedListEnvelope'
|
||
'400':
|
||
$ref: '#/components/responses/ValidationError'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
|
||
/api/v1/posts/{postId}/comments:
|
||
get:
|
||
tags: [comments]
|
||
summary: 评论列表(单层平铺,游标分页)
|
||
description: |
|
||
排序 `(created_at DESC, id DESC)` 走 `ix_comments_post_created`,keyset 游标;
|
||
仅 visible 评论。互动面 = 帖子公开面:帖子不可见(**含作者本人草稿**)
|
||
404/40403。作者与 @ 目标均为 AuthorSummary。
|
||
operationId: listComments
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PostIdParam'
|
||
- $ref: '#/components/parameters/PageLimitParam'
|
||
- $ref: '#/components/parameters/PageCursorParam'
|
||
responses:
|
||
'200':
|
||
description: cursor 分页评论列表
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CommentListEnvelope'
|
||
'400':
|
||
$ref: '#/components/responses/ValidationError'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
$ref: '#/components/responses/PostNotFound'
|
||
post:
|
||
tags: [comments]
|
||
summary: 创建评论(幂等 + 可选 @ 回复)
|
||
description: |
|
||
`Idempotency-Key` **必带**(语义见该头参数描述),落 `client_request_id +
|
||
request_hash`(键按作者隔离、天然全局跨帖)。`replyToUserId` 可选 @ 回复
|
||
(单层平铺,无楼中楼,ADR-018);目标须为存活用户,不存在/已注销 404/40406
|
||
(合并不泄露成因)。互动面 = 帖子公开面:帖子不可见(**含作者本人草稿**)
|
||
404/40403。content trim 后 1~2000。comment_count 同事务 +1。
|
||
operationId: createComment
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PostIdParam'
|
||
- $ref: '#/components/parameters/IdempotencyKeyRequiredHeader'
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CreateCommentRequest'
|
||
responses:
|
||
'201':
|
||
description: 创建成功(或同键幂等重试返回首次结果)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/CommentEnvelope'
|
||
'400':
|
||
$ref: '#/components/responses/ValidationError'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
description: 帖子不可见——含作者本人草稿(code 40403);或 replyToUserId 目标用户不存在/已注销(code 40406)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
postNotFound:
|
||
value: { code: 40403, message: 帖子不存在, data: null }
|
||
userNotFound:
|
||
value: { code: 40406, message: 用户不存在, data: null }
|
||
'409':
|
||
$ref: '#/components/responses/IdempotencyPayloadMismatch'
|
||
|
||
/api/v1/comments/{commentId}:
|
||
delete:
|
||
tags: [comments]
|
||
summary: 删除评论(仅评论作者;顶层短路径)
|
||
description: |
|
||
顶层短路径先例(pets 域子资源同理):commentId 全局唯一。**仅评论作者可删——
|
||
帖主不可删除他人评论(D3-7 首版不做)**:对可见评论的非作者(含帖主)
|
||
403/40301;不存在/已删/所属帖不可见合并 404/40404(防枚举)。
|
||
软删(status→deleted),comment_count 同事务 -1。
|
||
operationId: deleteComment
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- name: commentId
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
description: 评论 ID
|
||
responses:
|
||
'200':
|
||
description: 删除成功
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/VoidEnvelope'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'403':
|
||
$ref: '#/components/responses/PostAccessDenied'
|
||
'404':
|
||
$ref: '#/components/responses/CommentNotFound'
|
||
|
||
/api/v1/posts/{postId}/like:
|
||
put:
|
||
tags: [interactions]
|
||
summary: 点赞(PUT 语义幂等)
|
||
description: |
|
||
主键 (post_id, user_id) 即幂等键:重复 PUT 返回 200 同一权威终态(非 409),
|
||
仅实际插入才 like_count 同事务 +1,并发 N 次计数恰为 1(M3 验收标准二)。
|
||
互动面 = 帖子公开面:帖子不可见(**含作者本人草稿**)404/40403。
|
||
operationId: likePost
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PostIdParam'
|
||
responses:
|
||
'200':
|
||
description: 权威终态(liked 恒 true)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/LikeStateEnvelope'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
$ref: '#/components/responses/PostNotFound'
|
||
delete:
|
||
tags: [interactions]
|
||
summary: 取消点赞(DELETE 语义幂等)
|
||
description: |
|
||
取消不存在的点赞不报错不减计数,返回 200 权威终态(liked 恒 false)。
|
||
帖子不可见(含作者本人草稿)404/40403。
|
||
operationId: unlikePost
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PostIdParam'
|
||
responses:
|
||
'200':
|
||
description: 权威终态(liked 恒 false)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/LikeStateEnvelope'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
$ref: '#/components/responses/PostNotFound'
|
||
|
||
/api/v1/posts/{postId}/bookmark:
|
||
put:
|
||
tags: [interactions]
|
||
summary: 收藏(PUT 语义幂等,与点赞同构)
|
||
description: 帖子不可见(含作者本人草稿)404/40403。
|
||
operationId: bookmarkPost
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PostIdParam'
|
||
responses:
|
||
'200':
|
||
description: 权威终态(bookmarked 恒 true)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/BookmarkStateEnvelope'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
$ref: '#/components/responses/PostNotFound'
|
||
delete:
|
||
tags: [interactions]
|
||
summary: 取消收藏(DELETE 语义幂等)
|
||
description: 帖子不可见(含作者本人草稿)404/40403。
|
||
operationId: unbookmarkPost
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PostIdParam'
|
||
responses:
|
||
'200':
|
||
description: 权威终态(bookmarked 恒 false)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/BookmarkStateEnvelope'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
$ref: '#/components/responses/PostNotFound'
|
||
|
||
/api/v1/me/bookmarks:
|
||
get:
|
||
tags: [interactions]
|
||
summary: 我的收藏列表(游标分页)
|
||
description: |
|
||
排序 `(bookmarks.created_at DESC, post_id DESC)` 走
|
||
`ix_post_bookmarks_user_created`,游标键在收藏关系行上。项形态 = FeedCard,
|
||
谓词与公共 Feed 恒等:被收藏帖软删/hidden/archived 后**静默剔除**(剔除在页
|
||
查询内完成,不破坏翻页不丢不重;publishedAt 恒非空不变式对本列表继续成立)。
|
||
operationId: listMyBookmarks
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/PageLimitParam'
|
||
- $ref: '#/components/parameters/PageCursorParam'
|
||
responses:
|
||
'200':
|
||
description: cursor 分页收藏卡片列表
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/FeedListEnvelope'
|
||
'400':
|
||
$ref: '#/components/responses/ValidationError'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
|
||
/api/v1/users/{userId}/follow:
|
||
put:
|
||
tags: [follows]
|
||
summary: 关注(PUT 语义幂等)
|
||
description: |
|
||
主键 (follower, followee) 幂等,重复 PUT 返回 200 权威终态;自关注 422/42204
|
||
(库层 ck_user_follows_self 兜底);目标用户不存在/已注销 404/40406。
|
||
关注 Feed 与关注/粉丝列表不在 M3(ADR-018 最小数据接口)。
|
||
operationId: followUser
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/UserIdParam'
|
||
responses:
|
||
'200':
|
||
description: 权威终态(following 恒 true)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/FollowStateEnvelope'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
$ref: '#/components/responses/UserNotFound'
|
||
'422':
|
||
description: 自关注(code 42204;仅 PUT——自取关走 DELETE 的 200 幂等 no-op)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
selfFollow:
|
||
value: { code: 42204, message: 不能关注自己, data: null }
|
||
delete:
|
||
tags: [follows]
|
||
summary: 取消关注(DELETE 语义幂等)
|
||
description: |
|
||
取消不存在的关注不报错,返回 200 权威终态(following 恒 false)。
|
||
**自取关同样 200 幂等 no-op**(关系行不可能存在,权威 false 即事实;42204
|
||
只在 PUT)。目标用户不存在/已注销 404/40406。
|
||
operationId: unfollowUser
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/UserIdParam'
|
||
responses:
|
||
'200':
|
||
description: 权威终态(following 恒 false)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/FollowStateEnvelope'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
$ref: '#/components/responses/UserNotFound'
|
||
|
||
/api/v1/users/{userId}/follow-stats:
|
||
get:
|
||
tags: [follows]
|
||
summary: 关注计数(关注数/粉丝数/我是否已关注)
|
||
description: |
|
||
ADR-018 最小接口的「数量」端点:followerCount/followingCount 实时 COUNT
|
||
(user_follows 双向索引支撑,无冗余计数列),followedByMe 为调用者视角,
|
||
查自己时恒 false。目标用户不存在/已注销 404/40406。关注/粉丝**列表**端点
|
||
不在 M3(需时按纯增量补入)。
|
||
operationId: getFollowStats
|
||
security:
|
||
- bearerAuth: []
|
||
parameters:
|
||
- $ref: '#/components/parameters/UserIdParam'
|
||
responses:
|
||
'200':
|
||
description: 计数与关注状态
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/FollowStatsEnvelope'
|
||
'401':
|
||
$ref: '#/components/responses/AccessTokenInvalid'
|
||
'404':
|
||
$ref: '#/components/responses/UserNotFound'
|
||
|
||
components:
|
||
securitySchemes:
|
||
bearerAuth:
|
||
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(作者同样)或他人 draft(code 40403)。
|
||
防枚举语义:全部情况响应完全一致;评论与互动路径上含作者本人草稿
|
||
(互动面 = 帖子公开面)。
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
postNotFound:
|
||
value: { code: 40403, message: 帖子不存在, data: null }
|
||
CommentNotFound:
|
||
description: 评论不存在、已删或所属帖子不可见(code 40404,防枚举合并)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
commentNotFound:
|
||
value: { code: 40404, message: 评论不存在, data: null }
|
||
MediaNotFound:
|
||
description: |
|
||
asset 不存在、非本人所有、已删,或**用途与引用场景不符**(code 40405,防枚举合并)。
|
||
用途不符即「从头像域看,一张帖子配图不是头像」;两种头像用途亦互不通用。
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
mediaNotFound:
|
||
value: { code: 40405, message: 媒体资源不存在, data: null }
|
||
UserNotFound:
|
||
description: 目标用户不存在或已注销(code 40406,合并不泄露成因)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
userNotFound:
|
||
value: { code: 40406, message: 用户不存在, data: null }
|
||
PostAccessDenied:
|
||
description: |
|
||
对可见帖子/评论无相应操作权限(code 40301):改删他人已发布帖、删他人可见评论
|
||
(含帖主删他人评论)。仅发给对资源「可见」的调用者,不泄露新信息。
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
accessDenied:
|
||
value: { code: 40301, message: 无权限执行该操作, data: null }
|
||
IdempotencyPayloadMismatch:
|
||
description: 同 Idempotency-Key 不同 payload,规范化 request_hash 不符(code 40905)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
mismatch:
|
||
value: { code: 40905, message: 幂等键已用于不同请求, data: null }
|
||
MediaNotReady:
|
||
description: |
|
||
引用了本人所有但非 ready(uploading/failed)状态的 asset(code 42203)。
|
||
asset 不存在/非本人/已删则合并为 404/40405。
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/ErrorEnvelope'
|
||
examples:
|
||
notReady:
|
||
value: { code: 42203, message: 媒体尚未就绪, data: null }
|
||
|
||
schemas:
|
||
RegisterRequest:
|
||
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: 单批 1–50 条;越界整批 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: 须在服务端事件字典内;不在字典中的事件名整条 rejected(unknown_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 不一致,该条
|
||
rejected(identity_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,不区分大小写、子串匹配)则整条
|
||
rejected(forbidden_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 域 schemas(M2 冻结;响应主键统一裸 `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→cancelled;completed 与 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: 标题(服务端 btrim,trim 后为空 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_at(timestamptz)比较;月初第一刻含、次月第一刻不含。
|
||
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 域 schemas(M3 冻结;定型依据:iteration-3 报告 13/15/16/17)
|
||
# ==================================================================
|
||
|
||
AuthorSummary:
|
||
type: object
|
||
description: |
|
||
作者公开摘要(D3-9 方案 B,iteration-3 报告 16 定型;community 跨 schema
|
||
只读 identity 取数,ADR-017)。正常路径 nickname 恒非空——空昵称由服务端
|
||
回退为 username(客户端不做回退拼装,回退后的展示名不标注来源);
|
||
nickname 与 avatarUrl 同为 null 即「降级/墓碑」形态(作者资料暂不可得,
|
||
或用户已注销)——两种情形同一形态,客户端只需一种占位逻辑。
|
||
不露 bio、不露 username。
|
||
required: [userId]
|
||
properties:
|
||
userId:
|
||
type: string
|
||
format: uuid
|
||
description: 恒非空,任何情形都在
|
||
nickname:
|
||
type: string
|
||
nullable: true
|
||
maxLength: 32
|
||
description: 昵称(空昵称已由服务端回退为 username);null 仅出现在降级/注销墓碑形态
|
||
example: 毛毛的铲屎官
|
||
avatarUrl:
|
||
type: string
|
||
nullable: true
|
||
description: |
|
||
头像访问 URL——时效性预签名 GET(TTL 默认 1 小时,配置项),每次响应现签,
|
||
客户端不得持久化、过期即重取;无头像 / 头像 asset 非 ready / 降级 → null
|
||
(客户端出占位)
|
||
|
||
# ---------- media ----------
|
||
CreateMediaUploadRequest:
|
||
type: object
|
||
required: [kind, purpose, mimeType, byteSize]
|
||
properties:
|
||
kind:
|
||
type: string
|
||
enum: [image]
|
||
description: M3 仅 image(ADR-018 视频后置;video/document 为向后新增枚举预留)
|
||
purpose:
|
||
type: string
|
||
enum: [post_image, 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-016,iteration-3 报告 13 定型)
|
||
required: [assetId, uploadUrl, method, requiredHeaders, expiresAt]
|
||
properties:
|
||
assetId:
|
||
type: string
|
||
format: uuid
|
||
description: 已登记的 asset ID(status=uploading)
|
||
uploadUrl:
|
||
type: string
|
||
description: |
|
||
预签名 PUT 完整 URL——签名以 query 参数携带(X-Amz-Algorithm/-Credential/
|
||
-Signature 族),指向客户端可达的对象存储端点;客户端直传,不经应用服务器
|
||
method:
|
||
type: string
|
||
enum: [PUT]
|
||
requiredHeaders:
|
||
type: object
|
||
additionalProperties:
|
||
type: string
|
||
description: |
|
||
直传请求必须**原样携带**的头。键集定型为恒且仅一键:
|
||
`{"Content-Type": <声明的 mimeType>}`——Content-Type 已签进签名,
|
||
改动即被存储侧拒绝
|
||
expiresAt:
|
||
type: string
|
||
format: date-time
|
||
description: |
|
||
凭据过期时刻 = 签发时刻 + TTL(默认 10 分钟,配置项);过期后重新创建
|
||
上传(原 asset 在补传后仍可确认)
|
||
|
||
MediaAsset:
|
||
type: object
|
||
required: [id, kind, purpose, mimeType, status, createdAt]
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
kind:
|
||
type: string
|
||
enum: [image]
|
||
purpose:
|
||
type: string
|
||
example: post_image
|
||
mimeType:
|
||
type: string
|
||
example: image/jpeg
|
||
byteSize:
|
||
type: integer
|
||
format: int64
|
||
widthPx:
|
||
type: integer
|
||
nullable: true
|
||
description: complete 后回填,可空
|
||
heightPx:
|
||
type: integer
|
||
nullable: true
|
||
status:
|
||
type: string
|
||
enum: [uploading, ready, failed]
|
||
description: deleted 态对外恒 404/40405,不出现在响应
|
||
url:
|
||
type: string
|
||
nullable: true
|
||
description: |
|
||
访问 URL,仅 ready 态非空——时效性预签名 GET(TTL 默认 1 小时,配置项),
|
||
每次响应现签,客户端不得持久化、过期即重取;桶保持私有,无签名直访被拒
|
||
readyAt:
|
||
type: string
|
||
format: date-time
|
||
nullable: true
|
||
createdAt:
|
||
type: string
|
||
format: date-time
|
||
|
||
MediaUploadEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
$ref: '#/components/schemas/MediaUploadCredentials'
|
||
|
||
MediaAssetEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
$ref: '#/components/schemas/MediaAsset'
|
||
|
||
# ---------- posts ----------
|
||
PostMediaItem:
|
||
type: object
|
||
description: 帖子挂接的一张图(响应形态)
|
||
required: [assetId, position, isCover, url]
|
||
properties:
|
||
assetId:
|
||
type: string
|
||
format: uuid
|
||
position:
|
||
type: integer
|
||
minimum: 0
|
||
maximum: 8
|
||
isCover:
|
||
type: boolean
|
||
description: 库内恒有唯一封面行(写侧保证:全 false 时服务端将 position 0 行置真)
|
||
url:
|
||
type: string
|
||
description: |
|
||
图片访问 URL——时效性预签名 GET(TTL 默认 1 小时,配置项),每次响应现签,
|
||
客户端不得持久化、过期即重取(运维前提:生产环境对象存储恒配置)
|
||
widthPx:
|
||
type: integer
|
||
nullable: true
|
||
heightPx:
|
||
type: integer
|
||
nullable: true
|
||
caption:
|
||
type: string
|
||
nullable: true
|
||
maxLength: 300
|
||
|
||
PostMediaAttachRequest:
|
||
type: object
|
||
description: 帖子挂接的一张图(请求形态);asset 须本人所有且 ready,否则 422/42203(不存在/非本人/已删 404/40405)
|
||
required: [assetId]
|
||
properties:
|
||
assetId:
|
||
type: string
|
||
format: uuid
|
||
description: 同帖 assetId 不得重复(400/40000)
|
||
position:
|
||
type: integer
|
||
minimum: 0
|
||
maximum: 8
|
||
description: |
|
||
**全给或全不给**:全给须恰为 0..n-1 连续不重复;全不给按数组序;
|
||
混合 400/40000
|
||
isCover:
|
||
type: boolean
|
||
default: false
|
||
description: 至多一个 true(uq_post_media_cover);全 false 时服务端将 position 0 行落库置为封面
|
||
caption:
|
||
type: string
|
||
maxLength: 300
|
||
description: trim 后 ≤300
|
||
|
||
CreatePostRequest:
|
||
type: object
|
||
required: [content]
|
||
properties:
|
||
title:
|
||
type: string
|
||
minLength: 1
|
||
maxLength: 120
|
||
description: 可选标题(ck_posts_title;空白串 400/40000)
|
||
content:
|
||
type: string
|
||
minLength: 1
|
||
maxLength: 10000
|
||
description: 正文,必填(ck_posts_content;纯文字帖合法,D3-4)
|
||
category:
|
||
type: string
|
||
enum: [general, help]
|
||
default: general
|
||
description: ai_creation 为 M4 预留值,M3 不开放写入(提交 400/40000)
|
||
status:
|
||
type: string
|
||
enum: [draft, published]
|
||
default: draft
|
||
description: published = 创建即发布(服务端写 publishedAt)
|
||
petId:
|
||
type: string
|
||
format: uuid
|
||
description: 可选关联宠物;须为调用者可见宠物,否则 404/40401(沿 pets 域防枚举语义)
|
||
media:
|
||
type: array
|
||
maxItems: 9
|
||
description: ≤9 图(D3-4);空数组或缺席 = 纯文字帖
|
||
items:
|
||
$ref: '#/components/schemas/PostMediaAttachRequest'
|
||
|
||
UpdatePostRequest:
|
||
type: object
|
||
description: |
|
||
部分更新:缺席字段不变;不支持清空回 null(M2 惯例)。media 若出现则
|
||
**整组替换**(删旧插新;`[]` 清空为纯文字帖;缺席不动),校验规则同创建。
|
||
required: [version]
|
||
properties:
|
||
version:
|
||
type: integer
|
||
minimum: 0
|
||
description: 乐观锁,必带(缺失 400/40000);过期 409/40902
|
||
title:
|
||
type: string
|
||
minLength: 1
|
||
maxLength: 120
|
||
content:
|
||
type: string
|
||
minLength: 1
|
||
maxLength: 10000
|
||
category:
|
||
type: string
|
||
enum: [general, help]
|
||
petId:
|
||
type: string
|
||
format: uuid
|
||
status:
|
||
type: string
|
||
enum: [published]
|
||
description: |
|
||
唯一开放的状态迁移 draft→published(发布动作,服务端写 publishedAt);
|
||
对已发布帖重复提交为幂等 no-op(200,version 照常 +1);
|
||
draft/hidden/archived 目标值 400/40000
|
||
media:
|
||
type: array
|
||
maxItems: 9
|
||
items:
|
||
$ref: '#/components/schemas/PostMediaAttachRequest'
|
||
|
||
Post:
|
||
type: object
|
||
description: |
|
||
帖子完整形态(详情 / 我的帖子列表 / 写响应共用)。region/generationJob/topics
|
||
等裁剪字段整体不出现(ADR-018 + ADR-010 先例),后续按新增可选字段纯增量补入。
|
||
required:
|
||
- id
|
||
- author
|
||
- category
|
||
- content
|
||
- status
|
||
- visibility
|
||
- media
|
||
- likeCount
|
||
- commentCount
|
||
- bookmarkCount
|
||
- likedByMe
|
||
- bookmarkedByMe
|
||
- createdAt
|
||
- updatedAt
|
||
- version
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
author:
|
||
$ref: '#/components/schemas/AuthorSummary'
|
||
petId:
|
||
type: string
|
||
format: uuid
|
||
nullable: true
|
||
category:
|
||
type: string
|
||
enum: [general, help, ai_creation]
|
||
description: ai_creation 仅读侧预留(M3 无法写入)
|
||
title:
|
||
type: string
|
||
nullable: true
|
||
maxLength: 120
|
||
content:
|
||
type: string
|
||
maxLength: 10000
|
||
status:
|
||
type: string
|
||
enum: [draft, published]
|
||
description: |
|
||
hidden/archived(运营态)永不出现在响应——对作者与他人一律 404/40403
|
||
(M3 无端点能产生或解除运营态)
|
||
visibility:
|
||
type: string
|
||
enum: [public]
|
||
description: M3 恒 public(ADR-018:followers/private 语义后置,字段保留)
|
||
media:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/PostMediaItem'
|
||
likeCount:
|
||
type: integer
|
||
format: int64
|
||
commentCount:
|
||
type: integer
|
||
format: int64
|
||
bookmarkCount:
|
||
type: integer
|
||
format: int64
|
||
likedByMe:
|
||
type: boolean
|
||
bookmarkedByMe:
|
||
type: boolean
|
||
createdAt:
|
||
type: string
|
||
format: date-time
|
||
updatedAt:
|
||
type: string
|
||
format: date-time
|
||
description: |
|
||
行最后更新时刻——互动计数维护亦会推动该值;判断「内容是否编辑过」
|
||
以 version 为准,勿以 updatedAt 判断
|
||
publishedAt:
|
||
type: string
|
||
format: date-time
|
||
nullable: true
|
||
description: 仅 published 非空(发布时恰写一次)
|
||
version:
|
||
type: integer
|
||
|
||
FeedCard:
|
||
type: object
|
||
description: |
|
||
Feed / 收藏列表卡片形态(较 Post 裁剪,iteration-3 报告 16 定型:只带
|
||
coverImage + mediaCount,不带整组图;content 全文、petId、visibility、
|
||
version、media 整组、created/updated 时间戳对均不出现,全文走帖子详情)。
|
||
required:
|
||
- id
|
||
- author
|
||
- category
|
||
- contentPreview
|
||
- mediaCount
|
||
- likeCount
|
||
- commentCount
|
||
- bookmarkCount
|
||
- likedByMe
|
||
- bookmarkedByMe
|
||
- publishedAt
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
author:
|
||
$ref: '#/components/schemas/AuthorSummary'
|
||
category:
|
||
type: string
|
||
enum: [general, help, ai_creation]
|
||
title:
|
||
type: string
|
||
nullable: true
|
||
description: 原样透传,无标题为 null
|
||
contentPreview:
|
||
type: string
|
||
description: |
|
||
正文前 200 个 Unicode 码点,**码点边界截断**(emoji 等增补面字符绝不
|
||
劈开),不追加省略号;短于 200 码点原样透传。全文恒走帖子详情端点
|
||
coverImage:
|
||
nullable: true
|
||
allOf:
|
||
- $ref: '#/components/schemas/PostMediaItem'
|
||
description: |
|
||
封面图 = 库中唯一 is_cover 行(写侧保证有图必有唯一封面行,读侧零特判);
|
||
纯文字帖为 null
|
||
mediaCount:
|
||
type: integer
|
||
minimum: 0
|
||
maximum: 9
|
||
description: 帖子图片总数(卡片角标「1/9」类展示)
|
||
likeCount:
|
||
type: integer
|
||
format: int64
|
||
commentCount:
|
||
type: integer
|
||
format: int64
|
||
bookmarkCount:
|
||
type: integer
|
||
format: int64
|
||
likedByMe:
|
||
type: boolean
|
||
bookmarkedByMe:
|
||
type: boolean
|
||
publishedAt:
|
||
type: string
|
||
format: date-time
|
||
description: 恒非空(Feed 与收藏列表谓词只放行 published)
|
||
|
||
PostEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
$ref: '#/components/schemas/Post'
|
||
|
||
PostListEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
type: object
|
||
description: cursor 分页正典信封;排序 created_at DESC, id DESC
|
||
required: [items, hasMore]
|
||
properties:
|
||
items:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Post'
|
||
nextCursor:
|
||
type: string
|
||
nullable: true
|
||
description: 下一页游标(不透明 base64url),hasMore=false 时恒为 null
|
||
hasMore:
|
||
type: boolean
|
||
|
||
FeedListEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
type: object
|
||
description: |
|
||
cursor 分页正典信封;公共 Feed 排序 published_at DESC, id DESC;
|
||
收藏列表排序 bookmarks.created_at DESC, post_id DESC
|
||
required: [items, hasMore]
|
||
properties:
|
||
items:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/FeedCard'
|
||
nextCursor:
|
||
type: string
|
||
nullable: true
|
||
description: 下一页游标(不透明 base64url),hasMore=false 时恒为 null
|
||
hasMore:
|
||
type: boolean
|
||
|
||
# ---------- comments ----------
|
||
CreateCommentRequest:
|
||
type: object
|
||
required: [content]
|
||
properties:
|
||
content:
|
||
type: string
|
||
minLength: 1
|
||
maxLength: 2000
|
||
description: trim 后 1~2000(ck_comments_content 同宽)
|
||
replyToUserId:
|
||
type: string
|
||
format: uuid
|
||
description: |
|
||
可选 @ 回复目标(单层平铺,无 parentCommentId,ADR-018);
|
||
目标须为存活用户,不存在/已注销 404/40406
|
||
|
||
Comment:
|
||
type: object
|
||
description: M3 无评论编辑,不带 updatedAt
|
||
required: [id, postId, author, content, createdAt]
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
postId:
|
||
type: string
|
||
format: uuid
|
||
author:
|
||
$ref: '#/components/schemas/AuthorSummary'
|
||
replyToUser:
|
||
nullable: true
|
||
allOf:
|
||
- $ref: '#/components/schemas/AuthorSummary'
|
||
description: '@ 回复目标的公开摘要(含降级 id-only 形态);非回复为 null'
|
||
content:
|
||
type: string
|
||
maxLength: 2000
|
||
createdAt:
|
||
type: string
|
||
format: date-time
|
||
|
||
CommentEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
$ref: '#/components/schemas/Comment'
|
||
|
||
CommentListEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
type: object
|
||
description: cursor 分页正典信封;排序 created_at DESC, id DESC
|
||
required: [items, hasMore]
|
||
properties:
|
||
items:
|
||
type: array
|
||
items:
|
||
$ref: '#/components/schemas/Comment'
|
||
nextCursor:
|
||
type: string
|
||
nullable: true
|
||
description: 下一页游标(不透明 base64url),hasMore=false 时恒为 null
|
||
hasMore:
|
||
type: boolean
|
||
|
||
# ---------- interactions / follows ----------
|
||
LikeState:
|
||
type: object
|
||
description: 点赞权威终态(乐观更新以此对账回滚,回滚基准取响应值)
|
||
required: [liked, likeCount]
|
||
properties:
|
||
liked:
|
||
type: boolean
|
||
likeCount:
|
||
type: integer
|
||
format: int64
|
||
|
||
BookmarkState:
|
||
type: object
|
||
description: 收藏权威终态(与点赞同构)
|
||
required: [bookmarked, bookmarkCount]
|
||
properties:
|
||
bookmarked:
|
||
type: boolean
|
||
bookmarkCount:
|
||
type: integer
|
||
format: int64
|
||
|
||
FollowState:
|
||
type: object
|
||
description: 关注权威终态;followerCount 为目标用户的粉丝数(实时 COUNT)
|
||
required: [following, followerCount]
|
||
properties:
|
||
following:
|
||
type: boolean
|
||
followerCount:
|
||
type: integer
|
||
format: int64
|
||
|
||
FollowStats:
|
||
type: object
|
||
required: [followerCount, followingCount, followedByMe]
|
||
properties:
|
||
followerCount:
|
||
type: integer
|
||
format: int64
|
||
description: 目标用户的粉丝数(实时 COUNT)
|
||
followingCount:
|
||
type: integer
|
||
format: int64
|
||
description: 目标用户关注的人数(实时 COUNT)
|
||
followedByMe:
|
||
type: boolean
|
||
description: 调用者是否已关注目标用户;查自己恒 false
|
||
|
||
LikeStateEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
$ref: '#/components/schemas/LikeState'
|
||
|
||
BookmarkStateEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
$ref: '#/components/schemas/BookmarkState'
|
||
|
||
FollowStateEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
$ref: '#/components/schemas/FollowState'
|
||
|
||
FollowStatsEnvelope:
|
||
type: object
|
||
required: [code, message, data]
|
||
properties:
|
||
code:
|
||
type: integer
|
||
enum: [0]
|
||
message:
|
||
type: string
|
||
example: success
|
||
data:
|
||
$ref: '#/components/schemas/FollowStats'
|
||
|
||
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'
|