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,163 @@
# T2-12 宠物列表、详情与编辑页接入真实数据(交付报告)
**执行日期**2026-09-08
**角色**Frontend DeveloperFlutter
**工单**T2-12L,关键路径)+ DEBT-1 偿还 + T2-17 前端半边(pet 域三事件)
**依据**01 号拆解 T2-12 节、22 号数据层交付(T2-11)、05 号 UI 设计规范、06 号埋点规划
**提交**patbond-flutter dev@`97a1f46`(基线 7fb9031,已 push origin dev),拆 3 个提交:
| 提交 | 内容 |
|------|------|
| `3179528` | 共享组件三件(PetAvatar / RecordTypeDot / EmptyStateIllustration+ TagPill 深变体映射(DEBT-1 |
| `c0a8a56` | pet 域埋点强类型封装(pet_analytics.dart 三事件) |
| `97a1f46` | 列表/详情/表单页接入真实数据 + app 装配 + demo 清理 + 全部页面测试 |
---
## 0. 结论摘要
- 档案 Tab 替换为真实宠物列表;列表 / 详情 / 建档 / 编辑全链路走 T2-11 数据层(PetsController → PetsRepository → ApiClient),页面零直连 ApiClient、零 AppState demo 依赖;
- **四态硬要求达成**:列表、详情、表单内品种目录三处网络面均有 loading / empty / error / retry 且有 widget 测试锁定;
- 40902 版本冲突有「明确提示 + 自动取新 version 重提」路径(测试锁定 version 3→4 重提序列);40903 芯片号冲突字段级报错(测试锁定);
- DEBT-1 随本单偿还:TagPill 深变体映射落地,全部组合 ≥5.78:1(AA),既有调用零参数回归;
- 埋点:pet 域三事件 + page_viewed 的 pet_form / pet_detail / pet_list 接线完成(观察者路由名采集有测试证据);
- 测试 **126 → 177 全绿(+51**`flutter analyze` 0 问题,`dart format` 无 diff
- compose 真实后端实测:注册 → 空态 → 品种目录 → 建档 → 列表 → 详情 → 差量编辑 → 40902 → 40903 → 自定义品种建档,全部符合契约预期(§6)。
## 1. 页面与四态覆盖表
| 页面 / 网络面 | loading | empty | error | retry | 测试文件 |
|---|---|---|---|---|---|
| P1 宠物列表(档案 Tab`pets_page.dart` | 居中转圈 ✅ | `EmptyStateIllustration`「还没有宠物档案」+ 建档 CTA ✅ | `InlineErrorBanner`(按错误类型分文案)✅ | 重试按钮 + 下拉刷新 ✅ | `pets_page_test.dart`6 |
| P2 宠物详情(`pet_detail_page.dart`) | 无内存副本时转圈 ✅(有副本即时渲染、后台刷新失败降级 SnackBar,有测试) | 「不存在」态:40401 → 提示 + 返回列表并刷新 ✅(详情页的 empty 语义即目标缺席) | 横幅 ✅ | 重试按钮 ✅ | `pet_detail_page_test.dart`(8) |
| 表单页品种目录(`pet_form_page.dart` 内) | 内联转圈 ✅ | 目录空 → 仅「自定义品种…」可选(结构兜底) | 「目录加载失败」提示 + 回落自定义输入 ✅ | 内联重试按钮 ✅ | `pet_form_page_test.dart`11 |
页面结构与导航:
```text
档案 TabIndexedStack,页名 pet_list
└─ P1 宠物列表:宠物卡(PetAvatar lg + 名字 + 品种·性别·年龄 + 状态 TagPill)
├─ 「添加」/ 空态 CTA / 虚线卡 → PetFormPage.createfadePageRoute,路由名 pet_form
└─ 点卡 → PetDetailPage(路由名 pet_detail
└─ owner 编辑徽标 / 编辑按钮 → PetFormPage.edit(无路由名,见 §4 决策 3)
```
## 2. 表单与冲突处理(对齐冻结契约)
- **字段**:昵称\*、物种\*SegmentedButton 犬/猫/其他,编辑锁定静态显示——species 不可改)、性别\*male/female/unknown,契约必填,未选提交拦截)、品种(目录下拉 + 「自定义品种…」互斥,二选一必填;编辑时目录缺席的既有品种保底成项防下拉失配)、生日(DatePicker + 「估算」勾选)、芯片号(可选)、性格(可选)。头像按 ADR-010 本地占位形态(`PetAvatar` url 缺省),不做上传。
- **校验**:失焦 + 提交双校验,`errorText` 受控、`onChanged` 即清(登录纵切模式,昵称 Focus 失焦有测试)。
- **部分更新**:编辑只发送改动字段 + version(测试锁定 `{version:3, name:…}` 精确形状);品种对整体替换;无变更不发 PATCH 直接返回(有测试)。
- **错误分层**(对齐 22 号报告 §3 处理语义,各有测试或复用既有锁定):
| 错误 | 呈现 |
|---|---|
| 40903 芯片号冲突 | 芯片号字段级 errorText「该芯片号已被登记,请核对后重试」 |
| 40902 版本冲突 | 横幅「资料已在其他设备被修改,已获取最新版本,请核对后重新保存」+ 自动 `getPet` 更新基线 version(保留用户输入),重提即用新 version——测试锁定提交序列 [3, 4] |
| 40401 不存在 | SnackBar + 返回列表并刷新 |
| 40300 无权限 | 横幅;且详情页对非 owner 隐藏全部编辑入口(viewer 用例有测试) |
| 40000 / 其他业务码 | 横幅通用文案(原始 message 不上屏) |
| 429 | 横幅「操作过于频繁」 |
| 网络/超时/5xx | SnackBar + 重试动作 |
| 会话失效 | 静默(认证状态机自动回登录页;登出同时 `PetsController.reset()` 防跨账号泄漏,有测试) |
## 3. DEBT-1 偿还证据(TagPill 深变体)
方案照 05 号规范 §5.3 落地:`TagPill` 增可选 `inkColor`,缺省按 `color` 查内置映射;底色维持 `withAlpha(20)` 不变;字号 11/w700 不变。
| 组合(文字色 / 8% 淡底) | 修复前对比度 | 修复后对比度 | 判定 |
|---|---|---|---|
| primary → **primaryDark** | 2.55 | **8.74:1** | AA ✅ |
| success → **successInk** | 2.50 | **7.39:1** | AA ✅ |
| accent → **accentDark** | 1.67 | **7.07:1** | AA ✅ |
| error → **errorDark**(新 token `#B02C25` | — | **5.78:1** | AA ✅ |
| 未命中映射 → **ink** 兜底 | — | ≥12:1 | AA ✅ |
- 新 token 落位 `AppColors``errorDark #B02C25``inkSoft #6B5A4A`(05 D8;本单页面族次级信息文字一律 `inkSoft``muted` 只作占位/禁用/装饰——DEBT-2 局部规避执行)。
- 回归:既有零参数调用(post_detail 话题标签、services「认证服务」、services 商家标签)**零参数变更**,全量 177 测试回归通过;映射行为由 `test/widgets/tag_pill_test.dart` 5 个用例锁定(含显式 `inkColor` 覆盖与兜底)。
- 同工单落位(05 §5.3 第 4 点建议):`RecordTypeDot` 五类型三色映射唯一出口(`lib/core/widgets/record_type_dot.dart`,含 §2 表全量映射常量与测试),供 T2-13/14 时间线直接取用;`PetAvatar` 四尺寸档收敛重复头像实现,编辑徽标底修订为 `primaryStrong`(白图标 4.49:1 达非文字 3:1,修复原 `primary` 底 2.75:1 不达标)。
## 4. 埋点挂接清单(T2-17 前端半边)
强类型封装 `lib/features/pets/pet_analytics.dart`(13 号规范 §3.1 惯例,枚举编译期锁死),注入链 app.dart → MainShellPage → PetsPage → 表单页:
| # | 事件 / 页名 | 触发点 | 属性 | 测试 |
|---|---|---|---|---|
| 1 | `pet_create_started` | 建宠表单**首次输入**(任一字段/选择器,每次进入一次) | `entryPoint``profile_empty_state` / `pet_list``post_register_guide` 预留) | 首次输入仅一次 ✅ |
| 2 | `pet_create_succeeded` | 建宠接口 code=0 | `durationMs`(表单打开→成功)、`species``petIndex` | 三属性齐备、petIndex=1 ✅ |
| 3 | `pet_create_failed` | 失败响应 / 超时 / 本地校验拦截 | `failureReason``errorCode`(可空)、`httpStatus`(由业务码 `~/100` 推导,可空)、`attemptSeq` | 校验拦截 / 40903(409) / 网络三路径 ✅ |
| 4 | `page_viewed(pet_form)` | 建宠表单页 push`RouteSettings(name: 'pet_form')`fadePageRoute 为 PageRoute,被既有 AnalyticsRouteObserver 采集)| 既有 pageName/referrer | push 路由名断言 ✅(06 §1.6 三段漏斗到达段接通) |
| 5 | `page_viewed(pet_detail)` | 详情页 push 路由名 `pet_detail` | 同上 | push 路由名断言 ✅ |
| 6 | `page_viewed(pet_list)` | 档案 Tab 页名由 `pet_archive` 改报 `pet_list`(Tab 曝光补点机制不变) | 同上 | 既有 Tab 补点测试覆盖机制 |
映射决策(报数据侧知悉):
1. `failureReason` 枚举照 06 §4 四值(`pet_limit_reached` 因产品未设上限未纳入);客户端网络层不区分 5xx 与断网/超时(同为 `ApiNetworkException`),两者并入 `network_error``server_error` 留作兜底;40903 等业务拒绝归 `validation_error` 并以 `errorCode` 细分。
2. `durationMs` 口径 = 表单打开(页面 initState)→ 成功响应(06 未定义精确口径,此口径对「动笔→成功」段更有解释力)。
3. **编辑表单不带路由名**`pet_form` 是建宠漏斗到达段专属页名(06 §1.6),编辑曝光计入会使「到达→动笔」分母系统性虚高;编辑本身不设事件(06 §1.4 既定取舍)。
4. 后端白名单:patbond-api dev@64c9b72 已含 pet 域 10 事件(T2-17 后端半边先行完成),三事件可直接落库。
## 5. 测试数变化
| 时点 | 测试数 | 说明 |
|------|--------|------|
| 基线(dev@7fb9031 | 126 | T2-11 数据层交付 |
| 本单(dev@97a1f46 | **177+51,全绿)** | 见下分布 |
| 文件 | 数量 | 覆盖 |
|------|------|------|
| `test/widgets/tag_pill_test.dart` | 5 | DEBT-1 映射四组 + 兜底 + inkColor 覆盖 + 底色不变 |
| `test/core/widgets/pet_avatar_test.dart` | 5 | 四尺寸档、占位形态、徽标底色修订、sm/md 无徽标、点击/禁用 |
| `test/core/widgets/record_type_dot_test.dart` | 3 | 五类映射齐备、渲染规格(50% 图标/8% 底)、三尺寸档 |
| `test/core/widgets/empty_state_illustration_test.dart` | 2 | 全要素渲染 + CTA 回调、无 CTA/说明不渲染 |
| `test/features/pets/pet_analytics_test.dart` | 4 | 三事件属性形状、可空属性缺席语义、httpStatus 推导 |
| `test/features/pets/pets_page_test.dart` | 6 | 列表四态、pet_form/pet_detail 路由名、状态标签 |
| `test/features/pets/pet_detail_page_test.dart` | 8 | 详情四态(含 40401 返回刷新)、副本即时渲染 + 降级 SnackBar、viewer 隐藏入口、编辑跳转预填、估算标记/未填写兜底 |
| `test/features/pets/pet_form_page_test.dart` | 11 | 校验拦截、started 去重、目录/自定义互斥请求形状、40903 字段级、网络 SnackBar、目录失败回落+重试、失焦校验、编辑差量、40902 冲突重提序列、无变更不发 PATCH |
| `test/features/pets/pet_display_test.dart` | 4 | 年龄边界(岁/月/未满月/未知)、元信息行、错误文案分档、标签 |
| `pets_controller_test.dart` 增量 | 3 | loadBreeds 物种缓存、失败重试、reset 登出清空 |
质量门禁:`flutter test` 177/177 全绿;`flutter analyze` No issues found`dart format --set-exit-if-changed` 无 diff(三个提交逐个通过)。
## 6. compose 真实后端实测记录(验收链路)
环境:patbond-api dev@64c9b72`./mvnw -DskipTests package` + `docker compose up -d --build`auth :8081 / pet :8083,均本机默认端口,客户端无需 --dart-define)。curl 按页面实际发出的请求逐步复演(token 已脱敏,测试账号随机生成、用后随 compose down 丢弃):
| 步骤 | 请求 | 结果 |
|------|------|------|
| 1 | POST /api/v1/auth/register(新用户) | code=0,取得 accessToken |
| 2 | GET /api/v1/pets | `{"code":0,"data":[]}` —— **新用户空态** ✅ |
| 3 | GET /api/v1/breeds?species=dog | 目录返回(中华田园犬/金毛/拉布拉多…),表单下拉数据源 ✅ |
| 4 | POST /api/v1/pets(表单同构体:name/species/sex/breedId/birthDate/birthDateEstimated/microchipNo/personality | 201 语义 code=0,返回完整 Petversion=0myRole=owner)—— **建档** ✅ |
| 5 | GET /api/v1/pets | 列表含新宠物 —— **列表** ✅ |
| 6 | GET /api/v1/pets/{id} | 详情字段逐一回读 —— **详情** ✅ |
| 7 | PATCH /api/v1/pets/{id}`{"version":0,"name":"豆豆二世"}` 差量) | code=0name 更新 —— **编辑** ✅ |
| 8 | PATCH 携带旧 version=0 | `{"code":40902,"message":"数据已被修改,请刷新后重试"}` —— 冲突路径与页面处理对齐 ✅ |
| 9 | POST 同芯片号再建档 | `{"code":40903,"message":"芯片号已被其他宠物登记"}` —— 字段级报错路径对齐 ✅ |
| 10 | POST 自定义品种(customBreedName,无 breedId | code=0`breedId=null, customBreedName="狸花"` —— 互斥另一半 ✅ |
结论:**空态 → 建档 → 列表/详情全链路 + 两类冲突码在真实后端全部符合冻结契约与页面实现预期**;未发现契约偏差。UI 侧同构行为由 §5 的 widget 测试(注入假仓库)锁定。实测后 `docker compose down`patbond-api 仓库零改动。
## 7. AppState demo 清理
- 删除:`AppState.vaccines` / `updateVaccines` / `updatePet` 及其持久化键、`initialVaccines`、models 中 `VaccineRecord` / `VaccineItem` / `VaccineStatus`(消费方仅原 pets_page,随页面替换全部失效);原 `EditPetSheet` / `VaccineSheet` demo 随页面重写移除。
- 保留(未越界):`AppState.pet` demo 仍被首页问候卡、创作页上传占位、主壳头部头像消费——属其他 Tab 的 demo 家具,留待相应工单收敛(AppState 内已注释标记)。
## 8. 决策与遗留
| # | 事项 | 说明 |
|---|------|------|
| 1 | 05 D1「单宠物跳过列表直进 P2」未采纳 | 该项待拍板;本单始终显示列表(P2 头部宠物切换器同属 D1,未做)。拍板后为小改动 |
| 2 | P2 的 stat 卡行 / AI 提醒 / 健康时间线未渲染 | T2-13/14 接摘要与记录接口时加回;不渲染 demo 占位(ADR-004),`RecordTypeDot` / `HealthTimelineTile` 所需映射已备好(前者已交付) |
| 3 | 档案 Tab 页名 `pet_archive``pet_list` | 字典 v2 初始集合本含 pet_list;数据侧看板注意 2026-09-08 起的页名断点 |
| 4 | `sterilizedOn` 详情展示、表单暂不可编辑 | 工单字段清单(品种/性别/生日/芯片号)之外,避免表单过长;记小遗留 |
| 5 | 归档入口(D2-7「首版仅归档」)未做 | 依赖 listPets 对 archived 的过滤语义确认(契约未明示列表是否含 archived),建议随 T2-13 或收口单补一个详情页归档动作 |
| 6 | HealthTimelineTile05 §3.3)未随本单交付 | 其唯一消费方是 T2-14 时间线,留给 T2-14 与真实数据一并落地 |
## 9. 交接 T2-13/14
- 页面骨架:`PetDetailPage._content` 的「基本资料」卡之上/之下即 stat 行与时间线的落位点;`RecordTypeDot``EmptyStateIllustration`、TagPill 深变体、`recordTypeStyles` 映射可直接取用。
- 数据获取范式:页内四态 + `petLoadErrorMessage` 文案分档 + 内存副本先渲染的模式可复制;分页用 `CursorPage`22 号报告 §2)。
- 埋点:`health_record_*` 事件按 `pet_analytics.dart` 同款强类型封装新建 `health_record_analytics.dart``record_form` / `record_detail` 页名枚举已就位待接线。
---
**Frontend Developer** · 2026-09-08 · patbond-flutter dev@97a1f46