- 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>
12 KiB
17 · T2-06/T2-07 健康事件时间线与照护提醒接口交付报告
- 日期:2026-09-07
- 工单:T2-06(健康事件时间线,M)+ T2-07(照护提醒,M),同域内聚一并交付
- 仓库:patbond-api,dev 分支(提交
d8303bfT2-06、3b27f9fT2-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 |
测试数变化
| 模块 | 交付前 | 交付后 |
|---|---|---|
| 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 内改期只能忽略后重建,契约冻结时可确认是否接受。