Files
patbond-doc/docs/development/iterations/iteration-2/13-pets-crud-permission-report.md
T
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

10 KiB
Raw Blame History

13 · T2-03 宠物 CRUD 与 pet_owners 权限框架交付报告

  • 日期2026-09-07
  • 工单T2-03M2 第二波关键路径)
  • 仓库patbond-apidev 分支
  • 角色Senior Developer(后端)

1. 交付范围

patbond-pet 模块(ADR-009)从第一波骨架升级为完整业务服务:

  • GET /api/v1/petsPOST /api/v1/petsGET /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_ownersrole=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 字段(camelCaseUUID 字符串,日期 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/archiveddeleted 不可经 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 包):

  • PetRoleowner/caregiver/viewer,映射 pet_owners.role。
  • AccessLevel:三档操作级别,一处定义角色矩阵:
    • READ — 三角色皆可(GET 详情、列表类子资源);
    • WRITE — owner + caregiverT2-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-usertest scope+ Flywaytest scope):Testcontainers postgres:18 上执行与生产完全相同的 V1..V4 迁移链。生产 wiring 不变(pet 服务仍不带 Flyway,链由 user 启动执行)。
  • 三角色场景按 T2-10 要求以测试数据直写 pet_owners 构造(ADR-015 邀请流后置,grantRole helper)。
  • 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 读通过/写 403caregiver 读通过/档案 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+23CRUD/字典 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-08summary 聚合同样以 require(userId, petId, READ) 开闸。
  • compose 的 pet 服务已挂 JWT 公钥;E2E(T2-18)无需额外配置。