docs: M2 第三波收口——报告 22~27 入档挂导航
CI / docs-build (push) Successful in 58s

- 22~26 Flutter 接入五单报告(T2-11~14 + 白名单扩充,flutter 测试 64→272)
- 27 第三波收口总表:demo 数据消亡、四态纪律、埋点端到端贯通、DEBT-1 偿还
- 三次 compose 实测无契约偏差;波内 agent 中断续跑事故记录在案

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 14:10:36 +08:00
parent 222990e587
commit b81c050e03
7 changed files with 768 additions and 0 deletions
@@ -0,0 +1,141 @@
# T2-11 pets feature 状态拆分与 API Client(数据层交付报告)
**执行日期**2026-09-08
**角色**Frontend DeveloperFlutter
**工单**T2-11M2 第三波前置,T2-12~14 依赖本单数据层)
**契约依据**`docs/api/openapi.yaml` v1.2.0(冻结)+ 21 号收口报告 §1 定型语义
**提交**patbond-flutter dev@`7fb9031`(基线 33b993c,已 push origin dev
---
## 0. 结论摘要
- pets 域 **12 路径 / 18 操作全部覆盖**DTO 逐字段对齐冻结契约;
- 新 8 个错误码全部映射为类型化异常,复用既有网络层与 token 拦截;
- 宠物档案状态自 `AppState` 拆出为独立 pets featureController → Repository → API Client);pets feature 零依赖 AppState demo 数据(AppState 的既有消费方按工单不动,留给 T2-12);
- 测试 **64 → 126 全绿**`flutter analyze` 0 问题,`dart format` 无 diff。
## 1. 分层结构
```text
T2-12 接入)Page/Widget
PetsControllerlib/features/pets/pets_controller.dart
· ChangeNotifier;宠物档案列表/详情内存副本
· 四态:initial / loading / ready(含 isEmpty 空态) / error(+lastError)
· refresh 收敛错误为 error 态;create/update 类型化异常外抛给表单层
PetsRepository(抽象)/ ApiPetsRepositorylib/features/pets/pets_repository.dart
· 18 操作全量方法;路径/方法/查询参数/请求体按契约组装
· 四个 POST 自动携带 Idempotency-Keyuuid v4,每次逻辑提交换新键)
· ApiBusinessException → pets 域类型化异常升格(pet_exceptions.dart
ApiClientlib/core/network/api_client.dart,既有复用)
· 统一信封解析 {code,message,data}、validateStatus 放行
· Bearer 注入 + 401/40101 单飞刷新重放(重放沿用同一幂等键,有测试锁定)
· 本单增量:query 参数支持;patbondPetApiBaseUrl:8083
patbond-pet 服务 http://127.0.0.1:8083--dart-define=PATBOND_PET_API_BASE_URL 可覆盖)
```
支撑文件:
| 文件 | 职责 |
|------|------|
| `lib/features/pets/pet_models.dart` | 全部响应/请求 DTO + 10 个枚举 + CursorPage 分页信封 |
| `lib/features/pets/pet_exceptions.dart` | 8 个类型化异常 + `mapPetBusinessException` |
| `lib/features/pets/money.dart` | 元/分换算工具(DTO 层保持整数分,T2-14 UI 使用) |
| `lib/core/network/api_exception.dart` | ApiCodes 补 pets 域 8 码;ApiBusinessException 开放继承 |
## 2. DTO / Client 覆盖清单(对照契约 18 操作)
| # | operationId | 方法 路径 | Repository 方法 | DTO | 状态 |
|---|-------------|-----------|-----------------|-----|------|
| 1 | listPets | GET /api/v1/pets | `listPets()` | `Pet`(含 myRole | ✅ |
| 2 | createPet | POST /api/v1/pets | `createPet(CreatePetRequest)` | `CreatePetRequest``Pet`(201;不带幂等键,契约由唯一约束兜底) | ✅ |
| 3 | getPet | GET /api/v1/pets/{petId} | `getPet(petId)` | `Pet` | ✅ |
| 4 | updatePet | PATCH /api/v1/pets/{petId} | `updatePet(petId, UpdatePetRequest)` | `UpdatePetRequest`(version 必填、缺席字段不发) | ✅ |
| 5 | listBreeds | GET /api/v1/breeds | `listBreeds({species})` | `Breed` | ✅ |
| 6 | listWeights | GET /api/v1/pets/{petId}/weights | `listWeights(petId, {limit, cursor})` | `CursorPage<WeightRecord>` | ✅ |
| 7 | createWeight | POST /api/v1/pets/{petId}/weights | `createWeight(...)` | `CreateWeightRequest``WeightRecord`Idempotency-Key ✅) | ✅ |
| 8 | listVaccineCatalog | GET /api/v1/vaccine-catalog | `listVaccineCatalog({species})` | `VaccineCatalogItem` | ✅ |
| 9 | listVaccinations | GET /api/v1/pets/{petId}/vaccinations | `listVaccinations(petId)` | `Vaccination`(不分页,服务端排序原样保留) | ✅ |
| 10 | createVaccination | POST /api/v1/pets/{petId}/vaccinations | `createVaccination(...)` | `CreateVaccinationRequest`Idempotency-Key ✅) | ✅ |
| 11 | updateVaccination | PATCH /api/v1/vaccinations/{vaccinationId} | `updateVaccination(...)` | `UpdateVaccinationRequest`(顶层短路径;vaccineId/seriesKey/doseNo 不在请求体) | ✅ |
| 12 | listHealthEvents | GET /api/v1/pets/{petId}/health-events | `listHealthEvents(petId, {limit, cursor})` | `CursorPage<HealthEvent>` | ✅ |
| 13 | createHealthEvent | POST /api/v1/pets/{petId}/health-events | `createHealthEvent(...)` | `CreateHealthEventRequest`amountCents 整数分;Idempotency-Key ✅) | ✅ |
| 14 | updateHealthEvent | PATCH /api/v1/health-events/{eventId} | `updateHealthEvent(...)` | `UpdateHealthEventRequest`(仅 title/notes/amountCents | ✅ |
| 15 | listCareReminders | GET /api/v1/pets/{petId}/care-reminders | `listCareReminders(petId, {status})` | `CareReminder`(status 白名单过滤参数) | ✅ |
| 16 | createCareReminder | POST /api/v1/pets/{petId}/care-reminders | `createCareReminder(...)` | `CreateCareReminderRequest`(不收 statusIdempotency-Key ✅) | ✅ |
| 17 | updateCareReminder | PATCH /api/v1/care-reminders/{reminderId} | `updateCareReminder(...)` | `UpdateCareReminderRequest`(仅 status+completedAt | ✅ |
| 18 | getPetSummary | GET /api/v1/pets/{petId}/summary | `getPetSummary(petId, {tz})` | `PetSummary`(tz 参数;四聚合嵌套对象) | ✅ |
契约语义落点:
- **分页信封**`CursorPage<T>` 严格按 `{items, nextCursor, hasMore}` 解析,nextCursor 视为不透明串;末页 nextCursor 缺席/null 同义处理(有测试)。
- **PetSummary null 语义**latestWeight / vaccinationProgress / nextVaccination 三项无记录为 nullmonthlyExpense 恒非 null、无支出 amountCents=0dueOn 允许过去日期(逾期针)——均有 DTO 测试锁定。
- **金额**DTO 层保持 `amountCents` 整数分(`int?`),换算工具 `formatCentsAsYuan` / `parseYuanToCents`(拒绝超两位小数/负数)随本单交付并带单测。
- **部分更新语义**:全部 Update 请求 toJson 只发送提交的字段(缺席≠null),version 恒带(提醒无 version,按契约仅 status+completedAt)。
- **枚举严格解析**:10 个枚举未知取值抛 FormatException——契约漂移在测试期显式暴露而非静默吞掉。
- **幂等**weights/vaccinations/health-events/care-reminders 四个 POST 自动携带 uuid v4 幂等键,每次逻辑提交换新键;token 刷新后的自动重放沿用同一键(测试锁定);createPet 按契约不带键。
## 3. 错误映射表(新 8 码 → 类型化异常)
映射发生在 `ApiPetsRepository._request``mapPetBusinessException`),全部继承 `ApiBusinessException`,既有按基类捕获的通用处理不受影响;每条映射均有单测。
| 错误码 | HTTP | 类型化异常 | 语义 / 客户端处理 |
|--------|------|-----------|------------------|
| 40300 | 403 | `PetAccessDeniedException` | 对可见宠物无操作权限(viewer 写、非 owner 改档案)→ 隐藏/禁用写入口 |
| 40401 | 404 | `PetNotFoundException` | 宠物不存在/软删/无关系(防枚举三态同响应)→ 返回列表并刷新 |
| 40402 | 404 | `PetRecordNotFoundException` | 记录级防枚举 → 刷新所在列表 |
| 40902 | 409 | `PetVersionConflictException` | 乐观锁冲突(提醒条件更新守卫同码)→ 提示刷新取新 version 重提 |
| 40903 | 409 | `MicrochipTakenException` | 芯片号已被登记 → 字段级报错 |
| 40904 | 409 | `VaccinationDoseExistsException` | 同系列同剂次已存在 → 表单提示(cancel 后可重建) |
| 42201 | 422 | `VaccinationRuleException` | 疫苗状态机/状态-日期规则违反 → 表单拦截兜底提示 |
| 42202 | 422 | `CareReminderRuleException` | 提醒状态机/completedAt 一致性违反 → 表单拦截兜底提示 |
| 40000 等未列码 | — | 保持 `ApiBusinessException` | 沿用通用处理(有测试锁定不误升格) |
网络/会话类沿用既有:`ApiNetworkException`(超时/断网/5xx)、`ApiRateLimitException`429)、`SessionExpiredException`(刷新失败清会话)。
## 4. 测试数变化
| 时点 | 测试数 | 说明 |
|------|--------|------|
| 基线(dev@33b993c | 64 | 第二波收口 |
| 本单(dev@7fb9031 | **126+62,全绿)** | 见下分布 |
新增测试分布(test/features/pets/):
| 文件 | 数量 | 覆盖 |
|------|------|------|
| `pet_models_test.dart` | 25 | 每个响应 DTO 全字段+null 变体映射、枚举严格性、请求体序列化(部分更新缺席字段、日期 YYYY-MM-DD)、分页信封、PetSummary null 语义 |
| `pets_repository_test.dart` | 22 | 18 操作请求线路(路径/方法/Bearer/查询参数/tz)、四 POST 幂等键(每次换新键+刷新重放同键)、8 码类型化映射+40000 不误升格、:8083 基地址常量 |
| `pets_controller_test.dart` | 9 | 四态流转(loading→ready/error、空态、重试恢复)、create 插头/update 与 getPet 回写副本、类型化异常外抛 |
| `money_test.dart` | 6 | 分→元格式化、元→分解析(拒超两位小数/负数/非法)、往返一致 |
质量门禁:`flutter test` 126/126 全绿;`flutter analyze` No issues found`dart format --set-exit-if-changed lib test` 无 diff。
## 5. 对既有代码的增量改动(仅 2 个核心文件)
1. `lib/core/network/api_client.dart`:新增 `patbondPetApiBaseUrl`(默认 `http://127.0.0.1:8083``--dart-define=PATBOND_PET_API_BASE_URL` 覆盖,照 patbondUserApiBaseUrl 先例);`ApiClient.request` 增加可选 `query` 参数(GET 过滤/分页所需,既有调用零改动)。
2. `lib/core/network/api_exception.dart``ApiCodes` 补 pets 域 8 码;`ApiBusinessException``final class` 改为可继承 `class`pets 类型化异常的基类,`sealed ApiException` 的穷举性不受影响)。
`AppState``pets_page.dart` 的 demo 数据消费方**未动**T2-12 范围);pets feature 不 import AppState/demo_data。
## 6. 契约出入记录
无。本单纯客户端按冻结契约实现,未做后端实测比对(契约测试已在 api 侧锁两端一致,21 号报告 §2);实现中未发现契约自身矛盾。
## 7. 交接给 T2-12~14
- T2-12:注入方式照 auth 先例——`buildPatbondDio(session, baseUrl: patbondPetApiBaseUrl)` + 共享 `TokenRefresher` 构造 `ApiClient`,再 `ApiPetsRepository(api: ...)``PetsController`;页面依赖 `PetsRepository` 抽象,widget 测试注入假仓库(`test/features/pets/pets_controller_test.dart``FakePetsRepository` 可直接复用/搬升 helpers)。
- T2-13/14:体重/疫苗/事件/提醒直接经 Repository 取数;页面级状态可扩展 PetsController 或按页自建轻量控制器。
- 40902 处理路径已定型:提示「数据已被修改」→ `getPet`/重新拉取取新 version → 重提。
- 金额输入框用 `parseYuanToCents`null 即格式错误),展示用 `formatCentsAsYuan`
---
**Frontend Developer** · 2026-09-08 · patbond-flutter dev@7fb9031