- 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>
10 KiB
13 · T2-03 宠物 CRUD 与 pet_owners 权限框架交付报告
- 日期:2026-09-07
- 工单:T2-03(M2 第二波关键路径)
- 仓库:patbond-api,dev 分支
- 角色:Senior Developer(后端)
1. 交付范围
patbond-pet 模块(ADR-009)从第一波骨架升级为完整业务服务:
GET /api/v1/pets、POST /api/v1/pets、GET /api/v1/pets/{petId}、PATCH /api/v1/pets/{petId}GET /api/v1/breeds(只读字典,?species=dog|cat|other过滤)- RS256 bearer 鉴权接入(与 patbond-user 同一公钥约定,
PATBOND_JWT_PUBLIC_KEY) - 统一权限框架
PetAccessService(T2-04~07 的复用入口,见 §4) version乐观锁、ck_pets_breed互斥、软删除防护、芯片号唯一冲突- docker-compose 的 pet 服务挂载 JWT 公钥(与 user 同一 deploy/keys)
明确不在本单:DELETE /api/v1/pets/{petId}(软删除端点)。D2-7 拍板首版前端只出「归档」入口;PATCH 已显式禁止 status=deleted(防绕过 ck_pets_deleted 的 deleted_at 记账),软删除端点留待契约冻结时决定是否收录(02 号报告亦标注「是否进 M2 契约冻结时定」)。归档(status=archived)已实现并有测试。
2. 端点清单与语义定型表(T2-09 契约冻结输入)
2.1 端点
| 端点 | 鉴权 | 权限级别 | 成功响应 |
|---|---|---|---|
GET /api/v1/breeds?species= |
Bearer | 无(字典非用户数据) | 200,全量数组(种子约 30 行,不分页) |
GET /api/v1/pets |
Bearer | 隐式(查询按调用者 pet_owners 行过滤) | 200,数组按 created_at DESC;无分页(单人宠物量小,02 号报告建议) |
POST /api/v1/pets |
Bearer | 任何登录用户 | 201,返回完整 PetResponse;调用者自动写入 pet_owners(role=owner, is_primary=true),与建宠同事务 |
GET /api/v1/pets/{petId} |
Bearer | READ(三角色皆可) | 200,含 myRole 字段(调用者自己的角色,客户端据此显隐写入口) |
PATCH /api/v1/pets/{petId} |
Bearer | MANAGE(仅 owner) | 200,返回更新后完整 PetResponse |
2.2 PetResponse 字段(camelCase,UUID 字符串,日期 ISO 8601)
id, name, species, breedId, breedDisplayName, customBreedName, sex, birthDate, birthDateEstimated, personality, microchipNo, sterilizedOn, status, myRole, createdAt, updatedAt, version
breedId/customBreedName恰有其一非空(ck_pets_breed);breedDisplayName由字典解出,随 breedId 存在。avatarAssetId不出现在 M2 契约(ADR-010 照片裁出)。myRole∈ owner/caregiver/viewer。
2.3 PATCH 语义(定型)
- 部分更新:缺席/null 字段不变;M2 不支持将可选字段清空回 null(把 null-vs-absent 歧义挡在契约外)。
- 例外:品种对(breedId/customBreedName)整体替换 —— 提交任一侧即替换整对,二者互斥校验同创建。
version必填(40000 缺失即拒),比对通过才写入并 +1。species不可改(创建即定,避免与品种配对失效)。status可迁移至 active/lost/deceased/archived;deleted不可经 PATCH 设置(40000)。
2.4 错误/权限语义定型表(冻结候选)
| 场景 | HTTP | code | 说明 |
|---|---|---|---|
| 未带/无效/过期 token 访问 /api/v1/** | 401 | 40101 | BearerAuthFilter,先于一切业务逻辑 |
| 参数校验失败(含品种互斥、species 白名单、PATCH 缺 version、PATCH status=deleted、breeds 非法 species 参数、品种与物种错配、品种不存在或停用) | 400 | 40000 | message 携带具体字段原因 |
| 宠物不存在 / 已软删除 / 调用者与宠物无 pet_owners 关系 | 404 | 40401 | 防枚举语义(推荐定案):三种情况响应完全一致,随机探测 UUID 无法得知命中真实记录。GET 与 PATCH 一致适用 |
| 有关系但角色不覆盖操作(viewer 或 caregiver PATCH 档案) | 403 | 40300 | 只有对宠物「可见」的用户才可能收到 403 |
| PATCH version 过期(并发冲突/重试) | 409 | 40902 | 明确冲突,不静默覆盖;客户端刷新取新 version |
| 芯片号已被登记(uq_pets_microchip) | 409 | 40903(新增) | 新错误码 MICROCHIP_EXISTS,延续 409xx 段;跨用户唯一,属可公开的业务冲突 |
防枚举推荐及理由(供拍板):采纳 02 号报告 P7 —— 无关系一律 404/40401。403 会向无关用户泄露「该 UUID 存在一只宠物」;宠物 id 会出现在分享场景(M3+ 邀请),枚举面必须封死。403/40300 仅保留给「可见但越权」:该用户本就能读到这只宠物,403 不泄露新信息,且给客户端明确的「无权操作」提示语义。此语义已在 PetAccessService 单点实现,T2-04~07 自动继承。
幂等定型:pets 不在开发计划 6.1 的 Idempotency-Key 强制名单,写接口不要求幂等键。重试安全由乐观锁 + 唯一约束兜底:PATCH 重发(version 已消耗)得 409/40902,刷新即见已生效结果;POST 带芯片号重发得 409/40903。均有集成测试锁定。
2.5 未登录/失败样例(统一信封)
{ "code": 40401, "message": "宠物不存在", "data": null }
3. 数据库约束对齐
| 约束 | 应用层行为 |
|---|---|
| ck_pets_breed | 服务层先校验互斥 + 字典品种存在/启用/物种匹配 → 40000 可读消息;约束兜底 |
| uq_pets_microchip | DuplicateKeyException → 40903 |
| ck_pets_status | DTO @Pattern 白名单(且排除 deleted)→ 40000 |
| ck_pets_deleted | PATCH 不可达 deleted 状态;软删除留待专用端点统一写 status+deleted_at |
| ck_pets_version | version 必填非负;UPDATE 条件比对 version 才 +1 |
| uq_pet_primary_owner | 创建事务内写唯一 primary owner 行 |
4. 权限框架与 T2-04~07 复用方式
核心类(patbond-pet 模块 access 包):
PetRole:owner/caregiver/viewer,映射 pet_owners.role。AccessLevel:三档操作级别,一处定义角色矩阵:READ— 三角色皆可(GET 详情、列表类子资源);WRITE— owner + caregiver(T2-04~07 的健康记录写接口用这一档:体重、疫苗、健康事件、提醒的 POST/PATCH);MANAGE— 仅 owner(宠物档案 PATCH、状态流转,将来的成员管理/软删除)。
PetAccessService.require(userId, petId, level):唯一权限闸口。一条索引查询(pets ⋈ pet_owners,双主键)完成「存在性 + 可见性 + 角色」三合一判定,异常语义即 §2.4 的 40401/40300。返回PetAccess(petId, role)供需要角色的 handler 使用。
T2-04~07 接入模板(每个子资源 handler 第一行):
petAccessService.require(userId, petId, AccessLevel.WRITE); // 写记录
petAccessService.require(userId, petId, AccessLevel.READ); // 读记录
- userId 来自
@RequestAttribute(BearerAuthFilter.USER_ID_ATTRIBUTE)(过滤器已验签注入)。 - 子资源自身的「记录不存在」用 40402 RECORD_NOT_FOUND(权限闸后才查记录,故 40402 不会泄露越权信息)。
- 每请求实时查库、无缓存:撤销照护关系立即生效(有测试
revokedViewerImmediatelyLosesAccess),这是 M2 不需要 access token 黑名单的前提(02 号报告 §6)。 - 选择「显式 service 调用」而非注解/切面:pet 域全部端点都以 petId 为路径变量,一行调用无重复膨胀;切面需要反射提参、隐藏了「先鉴权后查数」的顺序约束,且测试更难定位。若 M5+ 端点形态多样化再评估注解化。
选型说明:鉴权(BearerAuthFilter/JwtVerifier/RsaPublicKeyLoader)从 patbond-user 复制到 pet 模块而非下沉 common —— patbond-common 是纯契约模块(仅 validation-api + jackson-annotations,无 servlet/jjwt 依赖,见其 pom 注释),为三个类引入 web 依赖破坏其定位;两服务独立部署,安全代码各自持有与 auth 公钥约定对齐。pet 模块去掉了 user 特有的 /api/v1/events 匿名白名单 —— pet 域全部端点强制登录。
5. 测试
5.1 测试基建
- pet 模块测试引入
patbond-user(test scope)+ Flyway(test scope):Testcontainers postgres:18 上执行与生产完全相同的 V1..V4 迁移链。生产 wiring 不变(pet 服务仍不带 Flyway,链由 user 启动执行)。 - 三角色场景按 T2-10 要求以测试数据直写 pet_owners 构造(ADR-015 邀请流后置,
grantRolehelper)。 - JWT 密钥每次测试运行时生成,不入库(沿用第一迭代 TestJwtKeys 模式)。
5.2 覆盖矩阵(T2-10 六类路径)
| 类别 | 用例 |
|---|---|
| 成功 | 建→列→详→改→归档全链路(真实 PG,含 primary owner 落库断言、部分更新字段保持);breeds 按 species 过滤 |
| 参数错误 | 品种双填/双空/物种错配、非法 species、PATCH 缺 version、PATCH status=deleted、breeds 非法参数 |
| 不存在 | GET/PATCH 随机 UUID → 404/40401 |
| 无权限 | 陌生人 GET/PATCH → 404(与不存在响应一致,防枚举断言);列表隔离;viewer 读通过/写 403;caregiver 读通过/档案 PATCH 403;撤销关系即时生效;401 三例(缺 token/错签名/过期) |
| 并发冲突 | 旧 version PATCH → 409/40902,先写者数据保留 |
| 幂等/重复 | 芯片号重复 → 409/40903;同 version 重发 PATCH → 409 不重复生效(version 落库断言) |
5.3 测试数变化
| 模块 | 交付前 | 交付后 |
|---|---|---|
| patbond-common | 3 | 3 |
| patbond-user | 59 | 59 |
| patbond-auth | 31 | 31 |
| patbond-pet | 2 | 25(+23:CRUD/字典 14 + 权限/鉴权 9) |
| 合计 | 95 | 118 |
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test 全绿(2026-09-07)。
6. 遗留与移交
- T2-09:§2 全表为契约冻结输入;两处需 PM/契约侧确认:40903 新错误码收录;软删除端点是否进 M2 契约(本单按 D2-7 未实现)。
- T2-04~07:按 §4 模板接入;WRITE 档在本单只有矩阵定义与 caregiver 403 反证,第一个子资源单(T2-04)须补 caregiver 写成功的正向用例。
- T2-08:summary 聚合同样以
require(userId, petId, READ)开闸。 - compose 的 pet 服务已挂 JWT 公钥;E2E(T2-18)无需额外配置。