- iteration-2 报告 01-08(PM 拆解/后端/Flutter 评估/现实核查/UI 规范/埋点规划/证据基线/Git 规划),04、06 已由正式角色复核定稿 - mkdocs 挂「第二迭代」导航,build --strict 通过 - ADR-009 新建 patbond-pet 模块、ADR-010 照片剪出 M2、ADR-011 dev 主干/master 发布、ADR-012 北极星与 H1-H4、ADR-013 废弃 health_record_action、ADR-014 DEBT-1 随 M2、ADR-015 照护人邀请后置 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
Patbond 第二迭代后端技术评估(Dev)
- 日期:2026-09-07
- 评估范围:patbond-api 承接 M2「宠物健康档案」的改动面、建模与 API 草案、迁移规划、遗留项耦合
- 代码基线:patbond-api
0d81c38(2026-09-04,工作区干净) - 结论先行:当前基线 82 个测试全绿(50.6s);建议 M2 在 patbond-user 内以独立包实现 pet_health 域,建模跟随 patbond-doc 目标模型(体重/疫苗强结构子表 + health_events 单表),共 7 项待拍板。
1. 现状盘点(实际读码结论)
1.1 模块与代码结构
Maven 三模块:patbond-common(错误码/响应契约/内部 DTO)、patbond-auth(8081,无库,Feign 调 user)、patbond-user(8082,唯一持库服务,Flyway 归属方)。
与 M2 直接相关的既有设施,全部可复用:
- 鉴权链路:
patbond-user的BearerAuthFilter(patbond-user/src/main/java/com/patbond/patbond/user/security/BearerAuthFilter.java)拦截/api/v1/*,RS256 本地验签后把 userId 放进 request attributepatbond.authenticatedUserId,controller 用@RequestAttribute取。宠物接口直接挂在同一过滤器下,零新增鉴权代码。 - 异常/错误码契约:
ErrorCode枚举(common)+ 每服务一个GlobalExceptionHandler,{code, message, data}信封 + 正确 HTTP 状态。扩展 = 往枚举追加值(不重编号)。 - 数据访问:无 JPA,统一
JdbcClient+ 手写 SQL(见UserRepository),约束下沉数据库(CHECK/部分唯一索引),updated_at由触发器维护。pet 域照此风格即可。 - 主键:应用侧生成 UUIDv7(
patbond-user/src/main/java/com/patbond/patbond/user/support/UuidV7.java)。 - 埋点挂接点:
EventDictionary已预置health_record_action(props 白名单recordType/actionType),M2 后端无需改埋点代码,Flutter 侧触发即可。 - 测试设施:Testcontainers(PostgreSQL 18)+
TestcontainersConfiguration,集成测试模式成熟,pet 域测试直接套用。
1.2 数据库现状
Flyway 链在 patbond-user:V1(identity/media/platform 基线)+ V2(platform.product_events)。pet_health schema 尚未创建(V1 只建了 platform/identity/media 三个 schema)。开发种子在 db/dev/afterMigrate__dev_seed.sql,默认不执行。
patbond-doc 目标模型(patbond-doc/docs/database/patbond_postgresql.sql 333-560 行)已给出完整的 pet_health 设计,共 8 张表:breeds、pets、pet_owners、pet_weight_records、vaccine_catalog、pet_vaccinations、health_events、health_event_media、care_reminders。该模型已经过评审,M2 建模应以它为正典裁剪,而不是另起炉灶。
1.3 缺口
- media 上传流程未实现:仓库中没有任何 media 相关代码(无 controller/service),
media.assets只有表。目标模型中宠物头像、疫苗证书、健康事件附件全部 FK 到media.assets——附件能力被 media 上传流程阻塞(见待拍板 P3)。 marketplaceschema 未建:目标模型中pet_vaccinations.provider_id/booking_id、health_events.provider_id/booking_id本就未设 FK(预留列),M2 保留可空列即可,无阻塞。
2. 改动面评估
| 改动面 | 内容 | 量级 |
|---|---|---|
| Flyway | V3 pet_health 结构基线(从目标模型裁剪)+ V4 字典种子(若拍板引入) | 中 |
| 新代码 | pet 域 controller/service/repository/DTO(约 5 组资源) | 大(M2 主体) |
| common | ErrorCode 追加 3~4 个值;若拍板新模块则需下沉 UuidV7/BearerAuthFilter |
小 |
| 契约 | openapi.yaml 冻结新增 pets 相关 path(实现前先冻结,本评估不动契约) | 中 |
| 既有代码 | 零改动(鉴权过滤器、异常处理、埋点均直接复用) | — |
| 依赖 | 无需新增任何依赖(JdbcClient + Flyway + Testcontainers 足够,不引 JPA) | — |
3. 领域建模草案
3.1 模块归属【待拍板 P1】
- 方案 A:新建
patbond-petMaven 模块(独立服务)。符合开发计划 4.1「按迭代增加模块,边界与 schema 对齐」的字面方向。代价:新端口/compose 服务/CI 矩阵;BearerAuthFilter、JwtVerifier、GlobalExceptionHandler、UuidV7需下沉 common 或复制;Flyway 单链归属要拆(共库单flyway_schema_history,需为新模块配独立 history 表),部署与联调面翻倍。 - 方案 B(推荐):在 patbond-user 内新增独立顶层包
com.patbond.patbond.user.pethealth。零基础设施成本,Flyway 链自然延续(V3+),鉴权/异常/UUIDv7 直接复用。约束:包内不 import user 域内部类(只经 service 接口),SQL 只碰pet_healthschema(读identity.users仅限权限校验 join),保证未来抽成独立模块时是「搬包 + 拆迁移」而非重写。 - 推荐 B:MVP 单实例共库阶段,「模块边界与 schema 对齐」用包边界 + schema 读写纪律即可兑现,把工程成本留给业务代码。
3.2 表结构草案(Flyway V3,从目标模型裁剪)
按目标模型原样建(列、CHECK、部分唯一索引、set_updated_at 触发器全保留),仅做以下裁剪调整:
| 表 | M2 处置 | 调整点 |
|---|---|---|
pets |
建 | 主键去掉 DEFAULT gen_random_uuid(),应用侧 UUIDv7(与 users 做法对齐);avatar_asset_id 保留可空列(media 未实现,暂不写入) |
pet_owners |
建 | 目标模型原样;创建宠物时自动写入 (pet_id, creator, 'owner', is_primary=true) |
pet_weight_records |
建 | 原样 |
breeds + vaccine_catalog |
建(P2 拍板) | 若引入:结构进 V3、种子进 V4 正式迁移(字典是生产数据,不放 db/dev);若不引入:pets 全走 custom_breed_name(ck_pets_breed 约束允许),疫苗表需把 vaccine_id 放宽为自由文本——偏离目标模型,后续迁移代价大 |
pet_vaccinations |
建 | provider_id/booking_id/certificate_asset_id 保留可空预留列,M2 不写入 |
health_events |
建 | event_type 枚举沿用目标模型 6 值(medical/feeding/deworming/grooming/measurement/note) |
health_event_media |
不建,推迟 | 依赖 media 上传流程(P3);纯增量表,后续 V5+ 补零成本 |
care_reminders |
建(P6 拍板) | 纯 CRUD,无推送 |
与 user/auth 的关系:pet_owners.user_id -> identity.users(id) 与 health_events.created_by_user_id -> identity.users(id) 两个跨 schema FK,共库阶段保留(与 V1 中 media.assets.owner_user_id 先例一致)。鉴权只用 JWT 里的 userId,不新增 auth 侧改动、不新增 /internal 接口。
3.3 健康记录类型建模:单表 + type vs 每类型子表【已由目标模型定调,确认即可】
- 纯单表(所有记录一张表 + type + jsonb):查询简单,但体重/疫苗的强约束(剂次唯一、状态-日期一致性、数值范围)全丢给应用层。
- 纯子表(每类型一张表):表爆炸,时间线聚合要 UNION 多表。
- 推荐(= 目标模型的混合方案):
pet_weight_records、pet_vaccinations独立强结构子表(各自的 CHECK 与部分唯一索引是业务规则本体,如「同系列同剂次未取消唯一」);其余低结构记录统一进health_events+event_type枚举。时间线视图由 health_events 承载,体重/疫苗页各查各表。
4. API 资源设计草案(供契约冻结参考,本评估不改 openapi.yaml)
路径与开发计划 6.2 对齐,全部挂 BearerAuthFilter 强制鉴权:
| 接口 | 说明 |
|---|---|
GET /api/v1/pets |
当前用户可见宠物列表(经 pet_owners join);量小,建议一次性返回不分页(契约冻结时定) |
POST /api/v1/pets |
创建,创建者自动 primary owner,返回 201 |
GET /api/v1/pets/{petId} |
详情(含调用者自己的 role) |
PATCH /api/v1/pets/{petId} |
更新,请求体带 version 乐观锁,冲突返回 409/40902 |
DELETE /api/v1/pets/{petId} |
软删(status=deleted),仅 owner;是否进 M2 契约冻结时定 |
GET/POST /api/v1/pets/{petId}/weights |
体重记录(GET 按 measured_at 倒序,cursor 分页) |
GET/POST /api/v1/pets/{petId}/vaccinations、PATCH .../vaccinations/{id} |
疫苗记录(PATCH 带 version;状态迁移 scheduled→completed/cancelled) |
GET/POST /api/v1/pets/{petId}/health-events |
健康时间线(cursor 分页:(occurred_at, id) 复合游标,与既有索引对齐) |
GET/POST/PATCH /api/v1/pets/{petId}/reminders |
提醒 CRUD(P6) |
GET /api/v1/pets/{petId}/health-summary |
服务端聚合:最新体重与趋势、疫苗进度、下次接种、当月花费(P5) |
GET /api/v1/breeds、GET /api/v1/vaccines |
字典只读接口(若 P2 拍板引入;query 参数 species) |
权限规则:owner 全权;caregiver 可读写记录、不可改宠物档案与成员;viewer 只读。M2 只实现 owner 路径(P4),但 repository 层权限查询按三档写好。
错误码扩展(追加进 ErrorCode,延续现有编号段):
| code | HTTP | 语义 |
|---|---|---|
40300 PET_ACCESS_DENIED |
403 | 对可见宠物无相应操作权限(如 viewer 尝试写) |
40401 PET_NOT_FOUND |
404 | 宠物不存在或调用者不可见(防 ID 枚举,见 P7) |
40402 RECORD_NOT_FOUND |
404 | 宠物下的记录不存在 |
40902 VERSION_CONFLICT |
409 | 乐观锁版本冲突(对应验收标准「并发更新返回明确冲突」) |
幂等:开发计划 6.1 的 Idempotency-Key 强制名单(帖子/生成任务/预约)不含 pets,M2 写接口不强制幂等键;客户端重试语义靠乐观锁 + 唯一约束兜底。
5. 待拍板清单
| # | 事项 | 选项 | 推荐 |
|---|---|---|---|
| P1 | 模块归属 | 新建 patbond-pet 模块 vs patbond-user 内独立包 | user 内独立包(3.1) |
| P2 | 品种/疫苗字典 | 引入 breeds + vaccine_catalog(V3 结构 + V4 种子)vs 自由文本 | 引入字典,种子最小集(犬猫核心疫苗),避免偏离目标模型 |
| P3 | 附件/图片 | 进 M2(需先实现 media 上传流程)vs 推迟 | 推迟出 M2;media 上传是独立工作量,不该给健康档案当前置;表列已预留 |
| P4 | 共同照护人 | 邀请/成员管理 API 进 M2 vs 只做 owner 自动归属 | 只做 owner,权限校验按三档 role 实现好,邀请 API 下迭代 |
| P5 | 健康汇总聚合 | 服务端 health-summary 接口 vs 客户端自聚合 |
服务端聚合(计划 M2 验收提到「从事实表聚合生成」,且跨设备一致) |
| P6 | 提醒 | care_reminders CRUD 进 M2(无推送)vs 推迟 | 进 M2 做纯 CRUD(计划 M2 范围明确包含),推送依赖通知基础设施、明确不做 |
| P7 | 无权限读取语义 | 403 vs 404 | 不可见宠物一律 404/40401(防枚举);可见但越权操作 403/40300 |
6. 遗留中低优先项与 M2 的耦合评估
- access token 黑名单:与 M2 弱耦合,建议不进本迭代。宠物权限每次请求实时查
pet_owners,撤销照护关系立即生效,不依赖 token 吊销;access token 15 分钟 TTL(ADR-003)对健康档案的敏感级别足够。黑名单需求真正的触发点是「改密/封号即时踢出」,属身份域主题,与 pet 域实现无交集。 - /internal 改 mTLS:与 M2 无耦合,建议不进本迭代。M2 不新增任何
/internal接口(按 P1 推荐方案,pet 域与 user 同进程,连内部调用都没有);即使 P1 拍板为独立模块,也应沿用现有静态 service token 方案,mTLS 留给微服务化阶段(与 ADR-002 的节奏一致)。
7. 构建与测试基线(2026-09-07 实测)
命令:JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw test(系统默认 JDK 26 不可用于构建,须显式指定)。
| 模块 | 测试数 | 结果 |
|---|---|---|
| patbond-common | 3 | 通过 |
| patbond-user | 48 | 通过(含 Testcontainers 集成测试) |
| patbond-auth | 31 | 通过 |
| 合计 | 82 | 全绿,BUILD SUCCESS,总耗时 50.6s |
与第一迭代收官基线(82 测试)一致,无回归。此为 M2 开工基线:M2 结束时测试数只增不减,且该命令保持一次通过。