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

12 KiB
Raw Permalink Blame History

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/amountCentsversion 必填乐观锁
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 响应字段

  • HealthEventResponseid, 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 纯增量补入)。
  • CareReminderResponseid, 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 列出、创建即 pendingcaregiver 建+完成成功双方可读;?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

测试数变化

模块 交付前 交付后
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 内改期只能忽略后重建,契约冻结时可确认是否接受。