- 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:
@@ -0,0 +1,141 @@
|
||||
# T2-11 pets feature 状态拆分与 API Client(数据层交付报告)
|
||||
|
||||
**执行日期**:2026-09-08
|
||||
**角色**:Frontend Developer(Flutter)
|
||||
**工单**:T2-11(M2 第三波前置,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 feature(Controller → Repository → API Client);pets feature 零依赖 AppState demo 数据(AppState 的既有消费方按工单不动,留给 T2-12);
|
||||
- 测试 **64 → 126 全绿**,`flutter analyze` 0 问题,`dart format` 无 diff。
|
||||
|
||||
## 1. 分层结构
|
||||
|
||||
```text
|
||||
(T2-12 接入)Page/Widget
|
||||
│
|
||||
▼
|
||||
PetsController(lib/features/pets/pets_controller.dart)
|
||||
· ChangeNotifier;宠物档案列表/详情内存副本
|
||||
· 四态:initial / loading / ready(含 isEmpty 空态) / error(+lastError)
|
||||
· refresh 收敛错误为 error 态;create/update 类型化异常外抛给表单层
|
||||
│
|
||||
▼
|
||||
PetsRepository(抽象)/ ApiPetsRepository(lib/features/pets/pets_repository.dart)
|
||||
· 18 操作全量方法;路径/方法/查询参数/请求体按契约组装
|
||||
· 四个 POST 自动携带 Idempotency-Key(uuid v4,每次逻辑提交换新键)
|
||||
· ApiBusinessException → pets 域类型化异常升格(pet_exceptions.dart)
|
||||
│
|
||||
▼
|
||||
ApiClient(lib/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`(不收 status;Idempotency-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 三项无记录为 null;monthlyExpense 恒非 null、无支出 amountCents=0;dueOn 允许过去日期(逾期针)——均有 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
|
||||
Reference in New Issue
Block a user