Files
lixi 1891d9b7b4
CI / docs-build (push) Successful in 1m3s
docs: M2 开工前分析 10 份报告入档 + ADR-009~015 拍板决策
- 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>
2026-09-07 13:54:35 +08:00

136 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 attribute `patbond.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 侧触发即可。
- **测试设施**TestcontainersPostgreSQL 18+ `TestcontainersConfiguration`,集成测试模式成熟,pet 域测试直接套用。
### 1.2 数据库现状
Flyway 链在 patbond-userV1identity/media/platform 基线)+ V2platform.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)。
- `marketplace` schema 未建:目标模型中 `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-pet` Maven 模块(独立服务)**。符合开发计划 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_health` schema(读 `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` | 提醒 CRUDP6 |
| `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_catalogV3 结构 + 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 分钟 TTLADR-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 结束时测试数只增不减,且该命令保持一次通过。