222990e587
CI / docs-build (push) Successful in 1m19s
- 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>
119 lines
12 KiB
Markdown
119 lines
12 KiB
Markdown
# 17 · T2-06/T2-07 健康事件时间线与照护提醒接口交付报告
|
||
|
||
- **日期**:2026-09-07
|
||
- **工单**:T2-06(健康事件时间线,M)+ T2-07(照护提醒,M),同域内聚一并交付
|
||
- **仓库**:patbond-api,dev 分支(提交 `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 号 §3,resource 前缀 `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 + caregiver(T2-03 §4 矩阵);两单均有 caregiver 写成功正向用例(§5)。
|
||
|
||
### 1.1 响应字段
|
||
|
||
- **HealthEventResponse**:`id, petId, eventType, occurredAt, title, notes, amountCents, createdByUserId, createdAt, updatedAt, version`。eventType ∈ medical/feeding/deworming/grooming/measurement/note;amountCents 整数分、可空、非负(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),比对通过才写入并 +1;updated_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 记 caregiver;PATCH 部分更新字段保持、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 同为 40402;viewer 读通过/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 内改期只能忽略后重建,契约冻结时可确认是否接受。
|