Files
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

164 lines
15 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.
# 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