511617be55
CI / docs-build (push) Successful in 36s
openapi.yaml 1.1.0 → 1.2.0:宠物 CRUD、品种/疫苗目录、体重、疫苗、健康事件、 照护提醒、档案聚合摘要共 12 路径 / 18 操作 / 30 schema 合入正典,按 iteration-2 报告 13/16/17/18 定型表修正草案(响应主键裸 id、vaccineName、40904/42202 新码、 42200 不引入、PATCH 不支持清空回 null、cursor 分页正典、PetSummary 四聚合口径 逐字收录、tz 参数缺省 UTC、防枚举 40401/40402 语义定型)。错误码表 +8: 40300/40401/40402/40902/40903/40904/42201/42202。docs/api/index.md 端点清单同步。 冻结后任何字段变更须显著上报、两端同步。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2386 lines
84 KiB
YAML
2386 lines
84 KiB
YAML
openapi: 3.0.3
|
||
info:
|
||
title: Patbond API — Auth / Me / Events / Pets(公开契约)
|
||
version: 1.2.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)。
|
||
|
||
## 通用约定(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 改宠物档案) |
|
||
| 40400 | 404 | 资源不存在 |
|
||
| 40401 | 404 | PET_NOT_FOUND:宠物不存在、已软删除或调用者与宠物无关系(防枚举,三种情况响应完全一致) |
|
||
| 40402 | 404 | RECORD_NOT_FOUND:顶层记录路径下记录不存在或所属宠物对调用者不可见(记录级防枚举,两种情况响应完全一致) |
|
||
| 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) |
|
||
| 42201 | 422 | VACCINATION_RULE_VIOLATION:疫苗状态机非法迁移或状态-日期规则违反 |
|
||
| 42202 | 422 | REMINDER_RULE_VIOLATION:提醒状态机非法迁移或 completed-completedAt 一致性违反 |
|
||
| 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;
|
||
- `MANAGE`——仅 owner:宠物档案 PATCH(含状态流转)。
|
||
权限每请求实时查库、无缓存:撤销照护关系立即生效。
|
||
- **防枚举语义**:宠物不存在、已软删除、调用者与宠物无 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}` 未收录。
|
||
|
||
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)
|
||
- url: http://127.0.0.1:8083
|
||
description: patbond-pet(本地开发,pets 域全部端点)
|
||
|
||
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)
|
||
|
||
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 服务。
|
||
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'
|
||
|
||
/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: |
|
||
权限档:MANAGE(**仅 owner**);caregiver/viewer 更新得 403/40300。
|
||
|
||
- 部分更新:缺席字段不变;**不支持将可选字段清空回 null**。
|
||
- 例外:品种对(`breedId`/`customBreedName`)**整体替换**——提交任一侧即替换
|
||
整对,互斥校验同创建。
|
||
- `species` 不可改(创建即定,避免与品种配对失效,请求体不含该字段)。
|
||
- `status` 可迁移至 active/lost/deceased/archived;**`deleted` 不可经 PATCH
|
||
设置**(400/40000,软删除留待专用端点,M2 契约不含)。
|
||
- `version` 必填(缺失 400/40000),比对通过才写入并 +1;过期 409/40902。
|
||
- 芯片号改为已被登记的值:409/40903。
|
||
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':
|
||
$ref: '#/components/responses/PetNotFound'
|
||
'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 }
|
||
|
||
/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'
|
||
|
||
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)。不带键则无幂等语义。
|
||
|
||
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 改
|
||
宠物档案。仅发给对宠物「可见」的调用者,不泄露新信息。
|
||
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 }
|
||
|
||
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: 当前用户资料(冻结契约,恰好这 4 个字段)
|
||
required: [userId, username, createdAt]
|
||
properties:
|
||
userId:
|
||
type: string
|
||
format: uuid
|
||
example: 019212aa-0000-7000-8000-000000000001
|
||
username:
|
||
type: string
|
||
example: demo_user
|
||
phone:
|
||
type: string
|
||
nullable: true
|
||
description: E.164;未绑定时为 null
|
||
example: '+8613800138000'
|
||
createdAt:
|
||
type: string
|
||
format: date-time
|
||
example: '2026-09-04T04:05:06.789Z'
|
||
|
||
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。`avatarAssetId` 不出现在 M2 契约(ADR-010)。
|
||
required:
|
||
- id
|
||
- name
|
||
- species
|
||
- sex
|
||
- birthDateEstimated
|
||
- status
|
||
- 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)
|
||
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 不可改(不在请求体)。
|
||
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)
|
||
|
||
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'
|