Files
patbond-doc/docs/api/openapi.yaml
T
lixi 511617be55
CI / docs-build (push) Successful in 36s
docs(api): M2 契约冻结 v1.2.0——pets 域 12 路径合入
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>
2026-09-08 10:10:37 +08:00

2386 lines
84 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.0.3
info:
title: Patbond API — Auth / Me / Events / Pets(公开契约)
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 tokenJWTRS256),有效期 15 分钟;由资源服务用公钥本地验签。
- refresh token:不透明随机串,有效期 30 天;**每次刷新即轮换**,旧值立即失效。
- 已轮换/已失效的 refresh token 再次被使用时,判定为重用,**整个 token family
(该登录会话链)全部撤销**,持有者需重新登录。
- 允许多设备并行会话;退出仅撤销当前会话(由所提交的 refreshToken 标识),
其他设备不受影响。已签发的 access token 在剩余有效期内仍可用。
- 登录失败限制:同一账号在 15 分钟窗口内密码错误累计 5 次(配置项),账号锁定
15 分钟;锁定期间即使密码正确也返回 423/42300;一次成功登录重置计数窗口。
## Pets 域约定(M2 冻结,iteration-2 报告 13/16/17/18 定型)
- **鉴权**pets 域全部端点强制 Bearer 鉴权,无匿名端点。
- **权限模型(ADR-015owner/caregiver/viewer 三角色,pet_owners 表)**,操作分三档:
- `READ`——三角色皆可:宠物详情/列表、各记录列表、档案摘要;
- `WRITE`——owner + caregiver:体重/疫苗/健康事件/提醒的 POST 与 PATCH
- `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: 宠物档案 CRUDpatbond-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: 创建成功,返回完整 PetmyRole 恒为 owner
content:
application/json:
schema:
$ref: '#/components/schemas/PetEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'409':
description: 芯片号已被登记(code 40903
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
microchipExists:
value: { code: 40903, message: 芯片号已被登记, data: null }
/api/v1/pets/{petId}:
get:
tags: [pets]
summary: 宠物详情
description: |
权限档:READ(三角色皆可)。返回宠物详情及 `myRole`(调用者对该宠物的角色,
客户端据此显隐写入口)。
operationId: getPet
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
responses:
'200':
description: 宠物详情(含 myRole
content:
application/json:
schema:
$ref: '#/components/schemas/PetEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PetNotFound'
patch:
tags: [pets]
summary: 更新宠物档案
description: |
权限档: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: |
权限档:WRITEowner + caregiver)。返回 **201** 与完整 WeightRecord。
支持可选 `Idempotency-Key`(语义见 info 的「幂等」段)。体重记录 append-only、
无乐观锁;同一时刻允许多条。`weightKg` 范围 (0, 500]、最多两位小数
numeric(6,2)),违反 400/40000。
operationId: createWeight
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateWeightRequest'
responses:
'201':
description: 创建成功(同 Idempotency-Key 重试返回首次创建的记录,同样 201)
content:
application/json:
schema:
$ref: '#/components/schemas/WeightEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/PetNotFound'
/api/v1/vaccine-catalog:
get:
tags: [dictionaries]
summary: 疫苗目录
description: |
疫苗目录(只读字典,非用户数据,仅需 Bearer 鉴权、无用户级权限)。
返回 enabled=true 的疫苗(V4 种子 10 行),`ORDER BY species, name`,不分页;
`?species=` 过滤,非法取值 400/40000。
operationId: listVaccineCatalog
security:
- bearerAuth: []
parameters:
- name: species
in: query
required: false
schema:
type: string
enum: [dog, cat, other]
description: 过滤物种;不传则返回全部
responses:
'200':
description: 疫苗目录列表
content:
application/json:
schema:
$ref: '#/components/schemas/VaccineCatalogListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
/api/v1/pets/{petId}/vaccinations:
get:
tags: [health-records]
summary: 疫苗记录列表
description: |
权限档:READ。**不分页**(单宠疫苗量级为个位数~十位数),排序服务端定死:
`ORDER BY series_key, dose_no, created_at, id`,客户端按系列直接分组成卡。
列表不过滤 status(含 cancelled 行,客户端自行按需过滤)。
operationId: listVaccinations
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
responses:
'200':
description: 疫苗记录列表(不分页,series_key/dose_no 排序)
content:
application/json:
schema:
$ref: '#/components/schemas/VaccinationListEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PetNotFound'
post:
tags: [health-records]
summary: 创建疫苗记录
description: |
权限档:WRITEowner + caregiver)。返回 **201** 与完整 Vaccination。
支持可选 `Idempotency-Key`。
- 创建状态仅 `scheduled` / `completed`(创建即 cancelled 无业务意义,400/40000)。
- 状态-日期规则(违反 422/42201):scheduled 必有 `plannedOn` 且不得带
`administeredOn`completed 必有 `administeredOn``nextDueOn` 与
`administeredOn` 同时存在时须 `nextDueOn ≥ administeredOn`。
- 疫苗必须存在、enabled 且 species 与宠物一致(400/40000)。
- 同宠物同疫苗同系列同剂次的非 cancelled 记录唯一(uq_pet_vaccination_dose):
重复 409/40904cancel 后同剂次可重新登记。
operationId: createVaccination
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateVaccinationRequest'
responses:
'201':
description: 创建成功(同 Idempotency-Key 重试返回首次创建的记录,同样 201)
content:
application/json:
schema:
$ref: '#/components/schemas/VaccinationEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/PetNotFound'
'409':
description: 同系列同剂次非 cancelled 记录已存在(code 40904
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
doseExists:
value: { code: 40904, message: 同系列同剂次记录已存在, data: null }
'422':
description: 状态机或状态-日期规则违反(code 42201)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
ruleViolation:
value: { code: 42201, message: scheduled 状态必须提供 plannedOn, data: null }
/api/v1/vaccinations/{vaccinationId}:
patch:
tags: [health-records]
summary: 更新疫苗记录
description: |
权限档:WRITE。顶层短路径,404/40402 为记录级防枚举语义。
- 部分更新:缺席字段不变;**不支持清空回 null**。
- `vaccineId` / `seriesKey` / `doseNo` 不可改(不在请求体)——登记错剂次的
修正路径是 cancel 后重建。
- `version` 必填(缺失 400/40000),比对通过才写入并 +1;过期 409/40902。
- 状态机:`scheduled → completed`(合并态必须有 administeredOn)、
`scheduled → cancelled`(合并态 administeredOn 必须为空);
**completed 与 cancelled 均为终态**completed→cancelled、cancelled→scheduled
等一律 422/42201);同状态编辑(补批号/备注等)始终允许。
- 校验时点:在「当前行 + 请求字段」的合并态上重跑与创建完全相同的状态-日期
规则,违反 422/42201。
operationId: updateVaccination
security:
- bearerAuth: []
parameters:
- name: vaccinationId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateVaccinationRequest'
responses:
'200':
description: 更新成功,返回更新后完整 Vaccination
content:
application/json:
schema:
$ref: '#/components/schemas/VaccinationEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/RecordNotFound'
'409':
$ref: '#/components/responses/VersionConflict'
'422':
description: 状态机非法迁移或状态-日期规则违反(code 42201)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
terminalState:
value: { code: 42201, message: completed 为终态,不可迁移至 cancelled, data: null }
/api/v1/pets/{petId}/health-events:
get:
tags: [health-records]
summary: 健康事件时间线
description: |
权限档:READ。cursor 分页(分页正典形态),按 `occurred_at DESC, id DESC` 排序
(与索引 ix_health_events_pet_time 逐列对齐)。`limit` 越界或 `cursor` 无效:
400/40000。
operationId: listHealthEvents
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/PageLimitParam'
- $ref: '#/components/parameters/PageCursorParam'
responses:
'200':
description: 健康事件分页结果
content:
application/json:
schema:
$ref: '#/components/schemas/HealthEventListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PetNotFound'
post:
tags: [health-records]
summary: 添加健康事件
description: |
权限档:WRITEowner + caregiver)。返回 **201** 与完整 HealthEvent。
支持可选 `Idempotency-Key`。
- 六类事件类型:medical/feeding/deworming/grooming/measurement/note。
- `createdByUserId` 取自验签 token**不收请求体**、永不可改。
- `title` 服务端 btrimtrim 后为空 400/40000。
- 金额 `amountCents` 以整数分传输、非负、可缺席;**提交小数一律 400/40000**
(不做静默截断)。
operationId: createHealthEvent
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateHealthEventRequest'
responses:
'201':
description: 创建成功(同 Idempotency-Key 重试返回首次创建的记录,同样 201)
content:
application/json:
schema:
$ref: '#/components/schemas/HealthEventEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/PetNotFound'
/api/v1/health-events/{eventId}:
patch:
tags: [health-records]
summary: 更新健康事件
description: |
权限档:WRITE。顶层短路径,404/40402 为记录级防枚举语义。
- **仅可编辑 `title` / `notes` / `amountCents`**`eventType` / `occurredAt`
为时间线条目的身份,不可改(不在请求体);`createdByUserId` 永不可改。
- 部分更新:缺席字段不变;**不支持清空回 null**。
- `version` 必填(缺失 400/40000),比对通过才写入并 +1;过期 409/40902。
- `title` 提交空白串(trim 后为空)400/40000。
operationId: updateHealthEvent
security:
- bearerAuth: []
parameters:
- name: eventId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateHealthEventRequest'
responses:
'200':
description: 更新成功,返回更新后完整 HealthEvent
content:
application/json:
schema:
$ref: '#/components/schemas/HealthEventEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/RecordNotFound'
'409':
$ref: '#/components/responses/VersionConflict'
/api/v1/pets/{petId}/care-reminders:
get:
tags: [health-records]
summary: 照护提醒列表
description: |
权限档:READ。**不分页**(单宠提醒量级小),`ORDER BY due_at ASC, id`
(待办最先到期在前)。`?status=` 白名单过滤(pending/completed/dismissed),
`?status=pending` 即「按 due_at 查询待办」视图;非法取值 400/40000。
operationId: listCareReminders
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- name: status
in: query
required: false
schema:
type: string
enum: [pending, completed, dismissed]
description: 按状态过滤;不传则返回全部
responses:
'200':
description: 提醒列表(不分页,due_at ASC 排序)
content:
application/json:
schema:
$ref: '#/components/schemas/CareReminderListEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PetNotFound'
post:
tags: [health-records]
summary: 创建照护提醒
description: |
权限档:WRITEowner + caregiver)。返回 **201** 与完整 CareReminder。
支持可选 `Idempotency-Key`(提醒表无唯一约束兜底,重复提交只能靠键防)。
创建恒为 `pending`(请求体不收 status,多余字段被忽略,与全 API 一致)。
M2 仅 app 内数据,不做推送(ADR-010)。提醒的 title/dueAt 后续编辑与删除端点
不在 M2 契约,改期路径为 dismiss 后重建。
operationId: createCareReminder
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateCareReminderRequest'
responses:
'201':
description: 创建成功,状态恒为 pending(同 Idempotency-Key 重试返回首次创建的记录,同样 201)
content:
application/json:
schema:
$ref: '#/components/schemas/CareReminderEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/PetNotFound'
/api/v1/care-reminders/{reminderId}:
patch:
tags: [health-records]
summary: 更新提醒状态
description: |
权限档:WRITE。顶层短路径,404/40402 为记录级防枚举语义。
**状态流转专用**:请求体仅 `status` + `completedAt`。
- 状态机:`pending → completed`(必带 completedAt)、`pending → dismissed`
(禁带 completedAt);completed / dismissed 为终态;**同状态重放始终允许**
(客户端重试「标记完成」幂等成功)。
- completed-completedAt 一致性(违反 422/42202):`status=completed` 必带
`completedAt`、其余状态禁带;终态互迁与回退 pending 均拒绝。
- `completedAt` 由客户端提交(而非服务端 now()),允许补记实际完成时刻。
- 提醒表无 version 列:并发流转采用当前状态条件更新守卫,读写窗口内被并发
流转抢先则 409/40902(「数据已被修改请刷新」,客户端处理方式与乐观锁一致)。
operationId: updateCareReminder
security:
- bearerAuth: []
parameters:
- name: reminderId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateCareReminderRequest'
responses:
'200':
description: 更新成功,返回更新后完整 CareReminder
content:
application/json:
schema:
$ref: '#/components/schemas/CareReminderEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'403':
$ref: '#/components/responses/PetWriteDenied'
'404':
$ref: '#/components/responses/RecordNotFound'
'409':
$ref: '#/components/responses/VersionConflict'
'422':
description: 状态机非法迁移或 completed-completedAt 一致性违反(code 42202
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
missingCompletedAt:
value: { code: 42202, message: 标记 completed 必须提供 completedAt, data: null }
/api/v1/pets/{petId}/summary:
get:
tags: [health-records]
summary: 档案聚合摘要
description: |
权限档:READ(三角色皆可读)。实时聚合生成档案页摘要:最新体重、疫苗进度、
下次接种、当月花费——四项聚合全部从事实表实时计算,**无任何写路径**
(不持久化展示字符串)。各聚合口径逐字见 PetSummary schema 字段描述
iteration-2 报告 18 §3 定型)。
`tz`:可选,IANA 时区标识(如 `Asia/Shanghai`,也接受固定偏移如 `+08:00`),
缺省 `UTC`,仅作用于当月花费的月度窗口;非法 tz 或超 64 字符 → 400/40000。
客户端应传自己的时区以获得符合直觉的月边界。
operationId: getPetSummary
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PetIdParam'
- name: tz
in: query
required: false
schema:
type: string
maxLength: 64
default: UTC
description: IANA 时区标识(如 Asia/Shanghai)或固定偏移(如 +08:00),仅作用于当月花费的月度窗口
example: Asia/Shanghai
responses:
'200':
description: 聚合摘要
content:
application/json:
schema:
$ref: '#/components/schemas/PetSummaryEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
$ref: '#/components/responses/PetNotFound'
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: 单批 150 条;越界整批 400/40000
items:
$ref: '#/components/schemas/TrackedEvent'
TrackedEvent:
type: object
required:
- eventId
- eventName
- eventVersion
- anonymousId
- sessionId
- clientTs
- appVersion
- platform
- osVersion
properties:
eventId:
type: string
format: uuid
description: 客户端生成的 UUID(规范要求 v7),服务端幂等去重键
example: 019212aa-4444-7000-8000-000000000001
eventName:
type: string
pattern: '^[a-z][a-z0-9_]{1,63}$'
description: 须在服务端事件字典内;不在字典中的事件名整条 rejectedunknown_event_name
example: auth_login_succeeded
eventVersion:
type: integer
description: 事件 schema 版本(字典 v1 全部为 1)
example: 1
anonymousId:
type: string
format: uuid
description: 设备级匿名标识,首次启动生成
example: 019212aa-0000-7000-8000-000000000001
userId:
type: string
format: uuid
nullable: true
description: |
登录后填充,可选。已认证请求中若与 token subject 不一致,该条
rejectedidentity_mismatch);匿名请求中原样落库,不做校验。
example: 019212aa-0000-7000-8000-000000000001
sessionId:
type: string
format: uuid
description: 客户端会话标识
example: 019212aa-1111-7000-8000-000000000001
clientTs:
type: string
format: date-time
description: 客户端本地时间(ISO 8601 带时区);serverTs 由服务端补写,客户端不发
example: '2026-09-07T04:05:06.789Z'
appVersion:
type: string
minLength: 1
maxLength: 32
example: 1.0.0+12
platform:
type: string
enum: [android, ios]
example: android
osVersion:
type: string
minLength: 1
maxLength: 32
example: android-14
props:
type: object
additionalProperties: true
description: |
事件专有属性,可选。按事件字典白名单处理:白名单外的键剥离后入库
(事件保留);键名命中隐私红线模式(password/token/secret/phone/
mobile/email/credential/idfa/gaid,不区分大小写、子串匹配)则整条
rejectedforbidden_field)。
example: { identifierType: username, durationMs: 123 }
TrackEventsResult:
type: object
description: 批次逐条结果(results 与请求 events 等长、按原顺序对应)
required: [accepted, duplicated, rejected, results]
properties:
accepted:
type: integer
description: 新落库条数
example: 1
duplicated:
type: integer
description: eventId 去重命中条数(视为成功,客户端不必重试)
example: 0
rejected:
type: integer
description: 被拒条数(客户端不重试)
example: 0
results:
type: array
items:
$ref: '#/components/schemas/EventResult'
EventResult:
type: object
required: [eventId, status]
properties:
eventId:
type: string
format: uuid
example: 019212aa-4444-7000-8000-000000000001
status:
type: string
enum: [accepted, duplicate, rejected]
example: accepted
reason:
type: string
enum: [unknown_event_name, identity_mismatch, forbidden_field, schema_invalid]
description: 仅 status=rejected 时出现(accepted/duplicate 不含该字段)
example: unknown_event_name
TrackEventsEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/TrackEventsResult'
# ==================================================================
# Pets 域 schemasM2 冻结;响应主键统一裸 `id`,关联字段带类型名)
# ==================================================================
Pet:
type: object
description: |
宠物档案(列表 / 详情 / 创建 / 更新的统一响应形态,皆含 myRole)。
`breedId` 与 `customBreedName` 恰有其一非空(ck_pets_breed);
`breedDisplayName` 由品种字典解出,随 breedId 存在。
软删除态(deleted)的宠物在全部端点表现为 404/40401,本 schema 的
status 永不出现 deleted。`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→cancelledcompleted 与 cancelled 均为终态(非法迁移 422/42201);同状态编辑始终允许
plannedOn:
type: string
format: date
administeredOn:
type: string
format: date
nextDueOn:
type: string
format: date
doseLabel:
type: string
maxLength: 64
manufacturer:
type: string
maxLength: 128
batchNo:
type: string
maxLength: 64
notes:
type: string
maxLength: 1000
VaccinationEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/Vaccination'
VaccinationListEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
type: array
description: 不分页;ORDER BY series_key, dose_no, created_at, id(含 cancelled 行)
items:
$ref: '#/components/schemas/Vaccination'
HealthEvent:
type: object
description: |
健康事件。`providerId / providerNameSnapshot / bookingId` 整体不出现
(ADR-010,M5 时纯增量补入)。
required:
- id
- petId
- eventType
- occurredAt
- title
- createdByUserId
- createdAt
- updatedAt
- version
properties:
id:
type: string
format: uuid
petId:
type: string
format: uuid
eventType:
type: string
enum: [medical, feeding, deworming, grooming, measurement, note]
description: 事件类型;创建后不可改
occurredAt:
type: string
format: date-time
description: 事件发生时间;创建后不可改
title:
type: string
minLength: 1
maxLength: 160
description: 标题(服务端 btrimtrim 后为空 400/40000
notes:
type: string
nullable: true
maxLength: 2000
description: 备注(上限 2000 字符)
amountCents:
type: integer
format: int64
nullable: true
minimum: 0
description: 金额(整数分,非负);提交小数 400/40000(不做静默截断)
createdByUserId:
type: string
format: uuid
description: 创建者用户 ID(取自验签 token,永不可改)
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
version:
type: integer
description: 乐观锁版本号(PATCH 时必须提交)
CreateHealthEventRequest:
type: object
required: [eventType, occurredAt, title]
properties:
eventType:
type: string
enum: [medical, feeding, deworming, grooming, measurement, note]
occurredAt:
type: string
format: date-time
title:
type: string
minLength: 1
maxLength: 160
notes:
type: string
maxLength: 2000
amountCents:
type: integer
format: int64
minimum: 0
description: 金额(整数分,非负);提交小数 400/40000
UpdateHealthEventRequest:
type: object
description: |
部分更新:缺席字段不变;不支持清空回 null。仅可编辑 title / notes / amountCents
eventType / occurredAt / createdByUserId 不可改(不在请求体)。
required: [version]
properties:
version:
type: integer
description: 乐观锁(必填;缺失 400/40000,过期 409/40902
title:
type: string
minLength: 1
maxLength: 160
description: 提交空白串(trim 后为空)400/40000
notes:
type: string
maxLength: 2000
amountCents:
type: integer
format: int64
minimum: 0
description: 提交小数 400/40000
HealthEventEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/HealthEvent'
HealthEventListEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
type: object
description: cursor 分页正典信封;排序 occurred_at DESC, id DESC
required: [items, hasMore]
properties:
items:
type: array
items:
$ref: '#/components/schemas/HealthEvent'
nextCursor:
type: string
nullable: true
description: 下一页游标(不透明 base64url),hasMore=false 时恒为 null
hasMore:
type: boolean
CareReminder:
type: object
description: |
照护提醒。**无 version 字段**care_reminders 表无该列,状态流转用当前状态
条件更新守卫,守卫落空 409/40902)。completedAt 非空当且仅当 status=completed。
required:
- id
- petId
- reminderType
- title
- dueAt
- status
- createdAt
- updatedAt
properties:
id:
type: string
format: uuid
petId:
type: string
format: uuid
reminderType:
type: string
enum: [deworming, checkup, medication, other]
title:
type: string
minLength: 1
maxLength: 160
dueAt:
type: string
format: date-time
description: 到期时间
status:
type: string
enum: [pending, completed, dismissed]
completedAt:
type: string
format: date-time
nullable: true
description: 完成时间;非空当且仅当 status=completed
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
CreateCareReminderRequest:
type: object
description: 创建恒为 pending(不收 status 字段,多余字段被忽略)
required: [reminderType, title, dueAt]
properties:
reminderType:
type: string
enum: [deworming, checkup, medication, other]
title:
type: string
minLength: 1
maxLength: 160
dueAt:
type: string
format: date-time
UpdateCareReminderRequest:
type: object
description: |
状态流转专用(仅 status + completedAt)。pending→completed 必带 completedAt、
pending→dismissed 禁带;终态互迁与回退 pending 拒绝(422/42202);
同状态重放始终允许(幂等成功)。completedAt 由客户端提交,允许补记实际完成时刻。
required: [status]
properties:
status:
type: string
enum: [pending, completed, dismissed]
completedAt:
type: string
format: date-time
description: status=completed 时必填;其余状态禁带(违反 422/42202)
CareReminderEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/CareReminder'
CareReminderListEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
type: array
description: 不分页;ORDER BY due_at ASC, id;支持 ?status= 白名单过滤
items:
$ref: '#/components/schemas/CareReminder'
PetSummary:
type: object
description: |
档案聚合摘要(四项聚合全部从事实表实时计算,无持久化;口径为 iteration-2
报告 18 §3 定型表逐字收录)。latestWeight / vaccinationProgress /
nextVaccination 三项可为 null(无对应记录);monthlyExpense 恒非 null。
required: [petId, monthlyExpense]
properties:
petId:
type: string
format: uuid
description: 恒非 null,回显路径参数
latestWeight:
type: object
nullable: true
description: |
最新体重。口径:pet_weight_records 按 (measured_at DESC, id DESC) 取首行——
与体重列表接口首行完全一致(同一索引 ix_pet_weight_pet_measured、同一
tie-break),同刻多条时后写入者(id 更大)胜出。无记录 → null。
required: [weightKg, measuredAt]
properties:
weightKg:
type: number
format: double
description: 两位小数(numeric(6,2)),对象存在时非 null
measuredAt:
type: string
format: date-time
description: 对象存在时非 null
vaccinationProgress:
type: object
nullable: true
description: |
疫苗进度。口径:范围 = 该宠物非 cancelled 的 pet_vaccinations 行。
completedDoses = 其中 status=completed 的行数;totalDoses = 全部非 cancelled
行数(= scheduled + completed,即「已登记剂次」——数据模型没有权威的
「系列应打总针数」,分母取用户已登记数)。cancelled 分子分母皆不计入。
totalDoses=0 → 整体 null**不是 0/0**)。
required: [completedDoses, totalDoses]
properties:
completedDoses:
type: integer
minimum: 0
description: 已完成剂次,对象存在时非 null
totalDoses:
type: integer
minimum: 1
description: 已登记剂次(scheduled + completed),对象存在时非 null=0 即整体 null
nextVaccination:
type: object
nullable: true
description: |
下次接种。口径:候选集两类并集:① 全部 scheduled 行的 planned_on(约束保证
非空;含过期——逾期计划在完成/取消前仍是下一针),source=planned
② completed 行的非空 next_due_on,仅当同 (pet, vaccine, series_key) 不存在
更高 dose_no 的非 cancelled 记录(后续针一经登记,其自身即代表下一针,
前一针的到期日失效),source=nextDue。cancelled 行不产生任何候选。
取 dueOn 最小者;同日 planned 优先于 nextDue,再按 id 升序保证确定性。
候选集空 → null。
required: [vaccinationId, vaccineId, vaccineName, doseNo, dueOn, source]
properties:
vaccinationId:
type: string
format: uuid
description: 命中的疫苗记录 id(客户端可跳详情),非 null
vaccineId:
type: string
format: uuid
description: 非 null
vaccineName:
type: string
description: 非 null,出自 vaccine_catalog(同 breedDisplayName 先例)
doseNo:
type: integer
description: 非 null
doseLabel:
type: string
nullable: true
description: 记录本身可无标签
dueOn:
type: string
format: date
description: 非 null;**可为过去日期**(逾期针仍是下一针)
source:
type: string
enum: [planned, nextDue]
description: 非 null,标注取值来源(scheduled 的 plannedOn 或 completed 的 nextDueOn
monthlyExpense:
type: object
description: |
当月花费,**恒非 null**(月份/时区总可确定)。口径:health_events.amount_cents
求和,窗口为请求时刻在 tz 时区的自然月半开区间 [当月1日00:00, 次月1日00:00)
对 occurred_attimestamptz)比较;月初第一刻含、次月第一刻不含。
amount_cents 为 NULL 的事件不计入;不按 event_type 过滤(任何事件类型的
金额都算支出)。tz 缺省 UTC,客户端应传自己的时区获得符合直觉的月边界——
月边界随 tz 移动。恒返回对象:month 为窗口所属 ISO 年月、timezone 回显、
无支出 amountCents=0。
required: [month, timezone, amountCents]
properties:
month:
type: string
description: ISO year-month(如 2026-09),非 null
example: '2026-09'
timezone:
type: string
description: 回显窗口所用时区(缺省 UTC),非 null
example: UTC
amountCents:
type: integer
format: int64
minimum: 0
description: 非 null,无支出为 0
PetSummaryEnvelope:
type: object
required: [code, message, data]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/PetSummary'