Files
lixi 222990e587
CI / docs-build (push) Successful in 1m19s
docs: M2 第二波收口——报告 13~21 与契约草案入档挂导航
- 13~18 后端纵切六单报告(T2-03~08,测试 95→182)
- 14 + openapi-pets-draft.yaml 契约起草档案
- 19 契约冻结报告(v1.2.0,22 项草案修正对照)
- 20 契约一致性测试(快照机制 + 1 漂移修复)
- 21 第二波收口总表(定型语义汇总,第三波接入依据)
- mkdocs build --strict 通过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 10:40:27 +08:00

119 lines
12 KiB
Markdown
Raw Permalink 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.
# 17 · T2-06/T2-07 健康事件时间线与照护提醒接口交付报告
- **日期**2026-09-07
- **工单**:T2-06(健康事件时间线,M)+ T2-07(照护提醒,M),同域内聚一并交付
- **仓库**patbond-apidev 分支(提交 `d8303bf` T2-06、`3b27f9f` T2-07,已推送)
- **角色**Senior Developer(后端)
- **前置**:完全复用 T2-03 的 `PetAccessService.require(userId, petId, AccessLevel)` 单一闸口(13 号报告 §4),未新造任何权限逻辑;零新增数据库迁移(V3 表结构原样够用);cursor 分页 / Idempotency-Key / 顶层短路径 40402 / 乐观锁 40902 全部沿用 T2-04/05 定型惯例(16 号报告)
---
## 1. 端点清单与语义定型表(T2-09 契约冻结输入)
| 端点 | 权限级别 | 成功响应 | 说明 |
| --- | --- | --- | --- |
| `GET /api/v1/pets/{petId}/health-events?limit=&cursor=` | READ | 200`{items, nextCursor, hasMore}` | cursor 分页,`occurred_at DESC, id DESC`(与 ix_health_events_pet_time 逐列对齐);limit 1~100 默认 20 |
| `POST /api/v1/pets/{petId}/health-events` | WRITE | 201,完整 HealthEventResponse | 可选 `Idempotency-Key` 头(≤255 字符,键派生确定性主键 + ON CONFLICT,语义细则同 16 号 §3resource 前缀 `health-event`);`created_by_user_id` 取自验签 token,不收请求体 |
| `PATCH /api/v1/health-events/{eventId}` | WRITE | 200,更新后完整 HealthEventResponse | 顶层短路径;仅可编辑 title/notes/amountCents`version` 必填乐观锁 |
| `GET /api/v1/pets/{petId}/care-reminders?status=` | READ | 200,数组**不分页** | 单宠提醒量级小(同疫苗先例);`ORDER BY due_at ASC, id`(待办最先到期在前);`?status=pending` 即「按 due_at 查询待办」,走 ix_care_reminders_due 部分索引 |
| `POST /api/v1/pets/{petId}/care-reminders` | WRITE | 201,完整 CareReminderResponse | 创建恒为 `pending`(请求体不收 status);可选 `Idempotency-Key`(前缀 `care-reminder` |
| `PATCH /api/v1/care-reminders/{reminderId}` | WRITE | 200,更新后完整 CareReminderResponse | 顶层短路径;状态流转专用(请求体仅 status + completedAt |
WRITE 档 = owner + caregiverT2-03 §4 矩阵);两单均有 caregiver 写成功正向用例(§5)。
### 1.1 响应字段
- **HealthEventResponse**`id, petId, eventType, occurredAt, title, notes, amountCents, createdByUserId, createdAt, updatedAt, version`。eventType ∈ medical/feeding/deworming/grooming/measurement/noteamountCents 整数分、可空、非负(bigint)。`provider_id / provider_name_snapshot / booking_id` **整体不出现**`health_event_media` 本迭代不实现(ADR-010,M5 纯增量补入)。
- **CareReminderResponse**`id, petId, reminderType, title, dueAt, status, completedAt, createdAt, updatedAt`。reminderType ∈ deworming/checkup/medication/other**无 version 字段**(表无该列,见 §2.2)。completedAt 非空当且仅当 status=completed。
- 分页信封与游标形态与 T2-04 完全一致(`nextCursor` 为 base64url(微秒:id)`hasMore=false` 时恒为 null)。
### 1.2 PATCH 健康事件语义(定型)
- 部分更新:缺席字段不变;沿用 T2-03 定型的「M2 不支持清空回 null」。
- `eventType / occurredAt` 不可改(时间线条目的身份,不在请求体);`createdByUserId` 永不可改。
- `version` 必填(缺失 40000),比对通过才写入并 +1updated_at 由 V3 触发器维护。
- title 服务端 btrim(镜像 ck_health_event_title),trim 后为空 → 40000。
## 2. 状态机实现说明(care_reminders
```
pending ──→ completed (必带 completedAt
pending ──→ dismissed (禁带 completedAt
completed / dismissed:终态;同状态重放始终允许(客户端重试「标记完成」幂等成功)
```
### 2.1 completed/completedAt 一致性
- 应用层先于数据库校验(镜像 ck_care_reminder_completed):`status=completed` 必带 completedAt、其余状态禁带,违反 → **42202** 可读消息而非约束 500;数据库约束保持兜底。
- 终态互迁(completed↔dismissed)与回退 pending(复活)均拒绝 → 42202。dismissed 不写 completedAt,落库断言见 §5。
- completedAt 由客户端提交(而非服务端 now()):照草案「标记 completed 时必填」形态,允许补记实际完成时刻。
### 2.2 无 version 列的并发语义
care_reminders 是 V3 中唯一无 version 列的业务表(状态流转单向、无字段编辑,设计如此)。流转采用**当前状态条件更新**守卫:`UPDATE ... WHERE id = ? AND status = <校验时快照>`,读写窗口内被并发流转抢先则 0 行命中 → **40902**(复用「数据已被修改请刷新」语义,客户端处理方式与乐观锁一致);窗口外的迟到流转由终态检查拦成 42202。守卫落空路径有仓储级测试锁定(§5)。
## 3. 幂等实现
与 16 号 §3 完全同构:`(资源前缀, userId, petId, key)` SHA-256 派生主键 + `ON CONFLICT (id) DO NOTHING`,同键重试返回原记录(同样 201),键按调用者 × 宠物 × 资源隔离、不比对请求体、无 TTL。零新增表/迁移。
## 4. 错误码定型表(含新码,供 T2-09 冻结采用)
| 场景 | HTTP | code | 说明 |
| --- | --- | --- | --- |
| 参数形状/字典错误:eventType/reminderType 非白名单、title 缺失/空白/超 160、缺 occurredAt/dueAt、amountCents 负数或**非整数**(见下)、notes 超 2000、limit 越界、cursor 无效、列表 status 过滤参数非法、PATCH 事件缺 version、PATCH 提醒缺 status 或 status 非法、Idempotency-Key 超长 | 400 | 40000 | 沿用既有码,message 带具体字段原因 |
| 宠物不存在/软删/无关系(两资源的宠物级路径 GET/POST | 404 | 40401 | 防枚举语义自动继承 T2-03 闸口 |
| 顶层记录路径 `PATCH /health-events/{id}``PATCH /care-reminders/{id}`:记录不存在 **或** 所属宠物对调用者不可见 | 404 | 40402 | 记录级防枚举,照 T2-05 §4 定型语义,两种情况响应完全一致 |
| 有关系但角色不覆盖(viewer 写事件/提醒、viewer PATCH 记录) | 403 | 40300 | 沿用 |
| 事件 PATCH version 过期;提醒流转状态守卫落空(读写窗口竞态) | 409 | 40902 | 沿用;先写者数据保留(有测试) |
| 提醒状态机非法迁移 / completed-completedAt 一致性违反(completed 缺 completedAt、非 completed 带 completedAt、终态互迁、回退 pending | 422 | **42202(新增)** | `REMINDER_RULE_VIOLATION`。草案提议复用 42201,未采纳(见 §6 偏差 #1);一码多场景、message 说明具体规则 |
健康事件无状态机,本单未用到 42201;42201 语义保持疫苗专属不变。
**金额整数分收紧**pet 服务全局禁用 Jackson `ACCEPT_FLOAT_AS_INT`——`"amountCents": 45.5` 此前会被静默截断为 45 入库,现按 40000 拒绝(验收标准「金额只收整数分」的必要条件)。该收紧同时作用于 pet 服务其余整数字段(doseNo、version 等收到小数同样 400),属纯收紧、既有测试全部通过。
## 5. 测试
覆盖 T2-10 六类路径,沿用既有测试基建(Testcontainers postgres:18 + 完整 V1..V4 迁移链、真实 BearerAuthFilter、pet_owners 直写构造角色):
| 类别 | 健康事件(11 用例) | 提醒(10 用例) |
| --- | --- | --- |
| 成功 | owner 建(含金额/备注/零金额边界)→列全链路;**caregiver 建+改成功**且 createdByUserId 记 caregiverPATCH 部分更新字段保持、title trim | 乱序创建按 due_at ASC 列出、创建即 pending**caregiver 建+完成成功**双方可读;?status=pending 待办视图;dismiss 流转 |
| 参数错误 | 缺/非法 eventType、缺 occurredAt、title 缺失/空白/161、金额 -1/45.5、limit 0/101、cursor 乱串、PATCH 缺 version、PATCH title 空白(皆 40000);amountCents=0 边界合法 | 缺/非法 reminderType、title 缺失/空白/161、缺 dueAt、列表 status=done、PATCH 缺 status/status 非法(皆 40000 |
| 不存在 | 随机 petId GET/POST → 40401;随机 eventId PATCH → 40402 | 随机 petId GET/POST → 40401;随机 reminderId PATCH → 40402 |
| 无权限 | 陌生人与随机 petId 响应逐字一致(防枚举断言);陌生人 PATCH 真实记录与随机 id 同为 40402viewer 读通过/POST 与 PATCH 40300 | 同左(记录级防枚举断言 + viewer 三断言) |
| 并发冲突 | 旧 version PATCH → 40902,先写者 notes 保留(落库断言) | 状态守卫以过期 pending 快照写入 → 0 行、先写者 completed 保留(仓储级断言);迟到流转经 API → 42202 |
| 幂等重试 | 同键两次 201 同 id;换键各自成行(落库计数断言) | 同键两次 201 同 id、落库 1 行 |
| 专项 | 分页:5 条走 3 页不丢不重、严格 DESC、同 occurred_at 三条跨页断续(id 断续断言)、nextCursor 收尾 null | 状态-completedAt 一致性:两次违规后落库仍 `pending|null`、完成后 completed_at 非空;终态四组非法迁移 + 同状态重放幂等 |
### 测试数变化
| 模块 | 交付前 | 交付后 |
| --- | --- | --- |
| patbond-common | 3 | 3 |
| patbond-user | 59 | 59 |
| patbond-auth | 31 | 31 |
| patbond-pet | 45 | **66**+21:事件 11 + 提醒 10 |
| **合计** | **138** | **159** |
`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test` 全绿(2026-09-07,一次通过)。
## 6. 与契约草案(openapi-pets-draft.yaml)的偏差清单(6 项,供冻结评审)
| # | 草案 | 实现定案 | 理由 |
| --- | --- | --- | --- |
| 1 | 提醒 422 复用 42201 | **42202 REMINDER_RULE_VIOLATION(新增)** | 42201 已在 T2-05 定型为 `VACCINATION_RULE_VIOLATION`(疫苗专属消息与语义);按 16 号 §4 确立的「错误码不复用不改号」原则,跨资源另立新码 |
| 2 | 资源自身 ID 用类型化名(eventId/reminderId 作 schema 主键名) | **裸 `id`** | 与 16 号偏差 #3 同一决定,域内一致性优先;路径参数名不受影响 |
| 3 | UpdateHealthEventRequest 的 notes/amountCents 标 nullable(暗示可清空) | **缺席=不变,不支持清空回 null** | 沿用 T2-03 §2.3 冻结的 PATCH 语义(与 16 号偏差 #7 同源) |
| 4 | care-reminders 列表「是否分页、待办过滤参数」TODO-FREEZE 待定 | **不分页 + `?status=` 白名单过滤,`due_at ASC, id` 排序** | 单宠提醒量级小(同疫苗不分页先例);pending 过滤恰好命中 V3 部分索引;排序服务端定死 |
| 5 | POST care-reminders 无 Idempotency-Key 头 | **支持可选 Idempotency-Key** | 16 号 §7 移交明示「换 resource 前缀即用」;提醒表无任何唯一约束兜底,重复提交只能靠键防;纯增量 |
| 6 | care-reminders PATCH 无 409 响应 | **补 40902(状态守卫落空)** | 表无 version 列,读写窗口竞态需要明确错误而非静默覆盖(§2.2);建议契约补录该响应 |
实现侧新增而草案未提的收紧(建议一并写入契约描述):notes 上限 2000 字符(草案未设上限,text 列防滥用);amountCents 拒绝小数(§4 末段);PATCH 事件 title 提交空白串 → 40000;创建提醒不收 status 字段(多余字段被忽略,与全 API 一致)。
## 7. 遗留与移交
- **T2-09 冻结**:§1/§4 为定型输入;§6 六项偏差需评审拍板(#1 42202、#2 裸 id 与 16 号先例同构,建议直接采纳)。
- **T2-08 summary**:当月花费可聚合 `SUM(amount_cents)`(注意 NULL 行不计入、月度边界口径待 T2-08 定);「下次接种」与「待办提醒」两个口径并存——前者出自 pet_vaccinations.next_due_on,后者出自 care_reminders pending 行,聚合字段命名时需区分。
- **T2-07 与疫苗 next_due_on 的联动**(完成接种自动生成 deworming/checkup 提醒)本单未做——工单为纯数据接口,联动属产品逻辑,建议 M2 收尾或 M3 拍板。
- 提醒的 title/dueAt 后续编辑与删除端点均不在本单(草案亦无);M2 内改期只能忽略后重建,契约冻结时可确认是否接受。