Files
patbond-doc/docs/development/iterations/iteration-2/22-pets-feature-datalayer.md
T
lixi b81c050e03
CI / docs-build (push) Successful in 58s
docs: M2 第三波收口——报告 22~27 入档挂导航
- 22~26 Flutter 接入五单报告(T2-11~14 + 白名单扩充,flutter 测试 64→272)
- 27 第三波收口总表:demo 数据消亡、四态纪律、埋点端到端贯通、DEBT-1 偿还
- 三次 compose 实测无契约偏差;波内 agent 中断续跑事故记录在案

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 14:10:36 +08:00

11 KiB
Raw Blame History

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. 分层结构

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) CreatePetRequestPet(201;不带幂等键,契约由唯一约束兜底)
3 getPet GET /api/v1/pets/{petId} getPet(petId) Pet
4 updatePet PATCH /api/v1/pets/{petId} updatePet(petId, UpdatePetRequest) UpdatePetRequestversion 必填、缺席字段不发)
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(...) CreateWeightRequestWeightRecordIdempotency-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(...) CreateVaccinationRequestIdempotency-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(...) CreateHealthEventRequestamountCents 整数分;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}) CareReminderstatus 白名单过滤参数)
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}) PetSummarytz 参数;四聚合嵌套对象)

契约语义落点:

  • 分页信封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._requestmapPetBusinessException),全部继承 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)、ApiRateLimitException429)、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 founddart 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.dartApiCodes 补 pets 域 8 码;ApiBusinessExceptionfinal class 改为可继承 classpets 类型化异常的基类,sealed ApiException 的穷举性不受影响)。

AppStatepets_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.dartFakePetsRepository 可直接复用/搬升 helpers)。
  • T2-13/14:体重/疫苗/事件/提醒直接经 Repository 取数;页面级状态可扩展 PetsController 或按页自建轻量控制器。
  • 40902 处理路径已定型:提示「数据已被修改」→ getPet/重新拉取取新 version → 重提。
  • 金额输入框用 parseYuanToCentsnull 即格式错误),展示用 formatCentsAsYuan

Frontend Developer · 2026-09-08 · patbond-flutter dev@7fb9031