diff --git a/docs/api/index.md b/docs/api/index.md index 2b2ebbd..5655712 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,8 +1,18 @@ # API 契约 -正式契约见 [openapi.yaml](openapi.yaml)(OpenAPI 3),当前 6 个端点: +正式契约见 [openapi.yaml](openapi.yaml)(OpenAPI 3,v1.2.0),当前 18 路径 / 24 操作: -- 认证域(第一迭代冻结):注册、登录、刷新、退出、当前用户 5 个端点,统一错误信封 `{code, message, data}` 与错误码表(40000/40100/40101/40102/40900/40901/42300),以及会话轮换与登录锁定策略说明。 +- 认证域(第一迭代冻结):注册、登录、刷新、退出、当前用户 5 个端点,统一错误信封 `{code, message, data}` 与错误码表,以及会话轮换与登录锁定策略说明。 - 埋点域(M2 第一波补录):`POST /api/v1/events` 批量上报产品事件——单批 1–50 条、202 逐条结果(accepted/duplicate/rejected)、`eventId` 幂等去重、唯一允许匿名的写端点(携带 Bearer 则完整校验)。 +- 宠物健康档案域(M2 第二波冻结,12 路径;冻结报告为 iteration-2 的 19 号报告,波末入档): + - 宠物 CRUD:`GET/POST /api/v1/pets`、`GET/PATCH /api/v1/pets/{petId}`(乐观锁、防枚举 404/40401、MANAGE 仅 owner) + - 只读字典:`GET /api/v1/breeds`、`GET /api/v1/vaccine-catalog`(`?species=` 过滤) + - 体重记录:`GET/POST /api/v1/pets/{petId}/weights`(cursor 分页正典 `{items, nextCursor, hasMore}`) + - 疫苗记录:`GET/POST /api/v1/pets/{petId}/vaccinations`、`PATCH /api/v1/vaccinations/{vaccinationId}`(状态机 422/42201、剂次唯一 409/40904) + - 健康事件:`GET/POST /api/v1/pets/{petId}/health-events`、`PATCH /api/v1/health-events/{eventId}`(cursor 分页、金额整数分) + - 照护提醒:`GET/POST /api/v1/pets/{petId}/care-reminders`、`PATCH /api/v1/care-reminders/{reminderId}`(`?status=` 过滤、流转 422/42202) + - 档案聚合:`GET /api/v1/pets/{petId}/summary`(最新体重、疫苗进度、下次接种、当月花费;`?tz=` 缺省 UTC) -约定:契约变更须先改本文件目录下的 OpenAPI,再改实现(契约先行);错误码只增不改义。 + 权限三档 READ/WRITE/MANAGE(ADR-015 三角色)、创建返回 201、PATCH 不支持清空回 null、四个记录类 POST 支持可选 `Idempotency-Key`;错误码新增 40300/40401/40402/40902/40903/40904/42201/42202。 + +约定:契约变更须先改本文件目录下的 OpenAPI,再改实现(契约先行);错误码只增不改义;**pets 域已冻结(1.2.0)——冻结后任何字段变更须显著上报、两端同步**。 diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index a6a71f2..1ecb5a9 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,14 +1,18 @@ openapi: 3.0.3 info: - title: Patbond API — Auth / Me / Events(公开契约) - version: 1.1.0 + 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`)。 + - 所有时间字段为 ISO 8601 且带时区偏移(如 `2026-09-04T04:05:06.789Z`); + 纯日期字段(生日、接种日期等)为 `YYYY-MM-DD`。 - 统一响应信封 `{"code": 0, "message": "success", "data": …}`;错误同时携带正确的 HTTP 状态码与稳定业务码,业务码永不复用或改号。 - `/internal/**` 为服务间接口,不属于本公开契约,需 `X-Internal-Token` 服务凭证, @@ -22,9 +26,17 @@ info: | 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 | 依赖服务暂不可用 | @@ -39,11 +51,46 @@ info: - 登录失败限制:同一账号在 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 @@ -52,6 +99,12 @@ tags: 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: @@ -269,6 +322,700 @@ paths: 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: @@ -277,6 +1024,44 @@ components: bearerFormat: JWT description: 'Authorization: Bearer (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) @@ -296,6 +1081,50 @@ components: 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: @@ -608,3 +1437,949 @@ components: 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'