- 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
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# T2-12 宠物列表、详情与编辑页接入真实数据(交付报告)
|
||||||
|
|
||||||
|
**执行日期**:2026-09-08
|
||||||
|
**角色**:Frontend Developer(Flutter)
|
||||||
|
**工单**:T2-12(L,关键路径)+ 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
|
||||||
|
档案 Tab(IndexedStack,页名 pet_list)
|
||||||
|
└─ P1 宠物列表:宠物卡(PetAvatar lg + 名字 + 品种·性别·年龄 + 状态 TagPill)
|
||||||
|
├─ 「添加」/ 空态 CTA / 虚线卡 → PetFormPage.create(fadePageRoute,路由名 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,返回完整 Pet(version=0,myRole=owner)—— **建档** ✅ |
|
||||||
|
| 5 | GET /api/v1/pets | 列表含新宠物 —— **列表** ✅ |
|
||||||
|
| 6 | GET /api/v1/pets/{id} | 详情字段逐一回读 —— **详情** ✅ |
|
||||||
|
| 7 | PATCH /api/v1/pets/{id}(`{"version":0,"name":"豆豆二世"}` 差量) | code=0,name 更新 —— **编辑** ✅ |
|
||||||
|
| 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 | HealthTimelineTile(05 §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
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# 24 · 事件白名单 v2 扩充(T2-17 后端半边)
|
||||||
|
|
||||||
|
> 依据:`06-experiment-tracking-plan.md` §1.4/§1.5(事件字典 v2 增量)、§5.2(page_viewed 转正稿)、§6.4(值级巡检);ADR-013(health_record_action 移除,dev@58576f8)
|
||||||
|
>
|
||||||
|
> 交付:`patbond-api` dev@`64c9b72`(`patbond-user` 模块 analytics 包,3 文件,+216/−9)
|
||||||
|
|
||||||
|
## 1. 结论速览
|
||||||
|
|
||||||
|
| 项 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| 新增白名单事件 | 10 个(pet 域 3 + health_record 域 7),props 键集与 06 号 §1.5 可直抄块逐条一致 |
|
||||||
|
| page_viewed 转正核对 | **一致,零修正**:现行白名单已是 `Set.of("pageName", "referrer")`,与 v2 正稿键集相同;仅更新注释标注正稿地位与 pageName 枚举(含 §1.6 修订的 `pet_form`) |
|
||||||
|
| health_record_action | 保持移除(ADR-013),新增集成测试锁定其仍被 `unknown_event_name` 拒绝 |
|
||||||
|
| 测试数 | 182 → **191**(+9:字典边界 5 + 接收端集成 4),`mvnw clean test` 全绿 |
|
||||||
|
| 契约变更 | **无需**:`openapi.yaml` 的 events 契约对事件名开放(字符串 + 后端字典校验),本次未触碰 |
|
||||||
|
|
||||||
|
## 2. 新增事件与 props 对照(vs 06 号 §1.4/§1.5)
|
||||||
|
|
||||||
|
`EventDictionary.java`(`patbond-user/src/main/java/com/patbond/patbond/user/analytics/`)`WHITELIST` 增量,逐条对照字典 v2:
|
||||||
|
|
||||||
|
### 2.1 pet 域(3 事件)
|
||||||
|
|
||||||
|
| 事件名 | 白名单 props | 与 06 号 §1.5 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `pet_create_started` | `entryPoint` | 一致 |
|
||||||
|
| `pet_create_succeeded` | `durationMs`、`species`、`petIndex` | 一致 |
|
||||||
|
| `pet_create_failed` | `failureReason`、`errorCode`、`httpStatus`、`attemptSeq` | 一致 |
|
||||||
|
|
||||||
|
### 2.2 health_record 域(7 事件)
|
||||||
|
|
||||||
|
| 事件名 | 白名单 props | 与 06 号 §1.5 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `health_record_create_started` | `recordType`、`entryPoint` | 一致 |
|
||||||
|
| `health_record_create_succeeded` | `recordType`、`durationMs`、`photoCount` | 一致 |
|
||||||
|
| `health_record_create_failed` | `recordType`、`failureReason`、`errorCode`、`httpStatus`、`attemptSeq` | 一致 |
|
||||||
|
| `health_record_viewed` | `recordType`、`source` | 一致 |
|
||||||
|
| `health_record_edit_succeeded` | `recordType`、`fieldCount` | 一致 |
|
||||||
|
| `health_record_edit_failed` | `recordType`、`failureReason`、`errorCode`、`httpStatus`(无 `attemptSeq`,正稿如此) | 一致 |
|
||||||
|
| `health_record_deleted` | `recordType` | 一致 |
|
||||||
|
|
||||||
|
### 2.3 page_viewed 转正核对
|
||||||
|
|
||||||
|
现行条目 `Map.entry("page_viewed", Set.of("pageName", "referrer"))` 与 v2 正稿(§5.2)键集**完全一致,无需修正**。差异只在语义层:v2 要求 pageName 为编译期枚举(`login/register/home/profile/pet_list/pet_detail/pet_form/record_form/record_detail`)——这是客户端约束(T2-17 Flutter 半边)+ §6.4 值级巡检的职责,后端键级白名单结构不承载值枚举(见 §3)。已将枚举全集写入 `EventDictionary` 类注释作字典说明。
|
||||||
|
|
||||||
|
## 3. 枚举值的校验边界(设计决策,沿用现行架构)
|
||||||
|
|
||||||
|
当前 `EventDictionary` 是**键级白名单**(白名单外键剥离、红线键拒绝、未知事件名拒绝),不做值级枚举校验。v2 的 `recordType`(`weight/vaccine/health_event/reminder`)、失败枚举(含 `permission_denied/conflict/not_found`)、`pageName` 枚举维持同一分层:
|
||||||
|
|
||||||
|
1. **客户端编译期枚举**是第一道约束(06 号 §5.2 明确 pageName 为「编译期枚举」;recordType 同理);
|
||||||
|
2. **接收端只校验键**——枚举外的值(如 `recordType: "grooming"`)**过 ingest 不拒绝**,由 §6.4 值级巡检 SQL 兜底发现。06 号 §1.5 的「可直抄」Java 块本身就是纯键集,本实现与其逐字一致,未擅自加严接收契约(加严会使客户端枚举漂移时整条事件丢失,与 §5.2 第 4 条「宁可不上报、不要报错名」的防洪水思路相悖)。
|
||||||
|
|
||||||
|
此边界已用集成测试 `enumOutRecordTypeValuePassesIngestForOfflinePatrol` 显式锁定为文档化行为,避免后人误当漏洞「修复」。
|
||||||
|
|
||||||
|
## 4. 测试增量(182 → 191,全绿)
|
||||||
|
|
||||||
|
`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`:总计 191,failures 0,errors 0。
|
||||||
|
|
||||||
|
**`EventDictionaryTest`(3 → 8,+5)**:
|
||||||
|
|
||||||
|
| 测试 | 边界 |
|
||||||
|
| --- | --- |
|
||||||
|
| `v2PetDomainEventsMatchDictionary` | pet 域 3 事件 props 键集 `containsExactlyInAnyOrder` 全矩阵 |
|
||||||
|
| `v2HealthRecordCreateFunnelMatchesDictionary` | 创建漏斗 3 事件键集全矩阵 |
|
||||||
|
| `v2HealthRecordLifecycleEventsMatchDictionary` | viewed/edit/deleted 4 事件键集(含锁定 edit_failed 无 attemptSeq) |
|
||||||
|
| `pageViewedFormalizedPropsAreExactlyPageNameAndReferrer` | 正稿键集恰为 pageName+referrer |
|
||||||
|
| `deliberatelyAbsentEventsStayUnknown` | §1.4 刻意不设的 `pet_viewed`/`health_record_edit_started`/`health_record_delete_failed` 保持 unknown |
|
||||||
|
|
||||||
|
**`AnalyticsIntegrationTest`(7 → 11,+4)**:
|
||||||
|
|
||||||
|
| 测试 | 边界 |
|
||||||
|
| --- | --- |
|
||||||
|
| `acceptsV2HealthRecordFunnelEvent` | v2 事件(合法 recordType)端到端 accepted 且落库 |
|
||||||
|
| `stripsPropsOutsideV2Whitelist` | v2 事件白名单外键(内容型 `recordTitle`)被剥离,`recordType` 保留 |
|
||||||
|
| `enumOutRecordTypeValuePassesIngestForOfflinePatrol` | 枚举外 recordType 值过 ingest(§3 决策的锁定) |
|
||||||
|
| `retiredHealthRecordActionStaysRejected` | 废弃事件带 v2 同名 props 上报仍整条 rejected(`unknown_event_name`) |
|
||||||
|
|
||||||
|
## 5. 未尽事项
|
||||||
|
|
||||||
|
- `entryPoint` 枚举(`profile_empty_state/pet_list/post_register_guide` 等)06 号标注「待 UI 定稿收敛」——键已入白名单,枚举收敛属 Flutter 半边与 UI 定稿,后端无阻塞。
|
||||||
|
- `pet_create_failed.failureReason` 的 `pet_limit_reached` 待拍板(无上限则删)——纯值级枚举,不影响本次键级白名单。
|
||||||
|
- T2-17 Flutter 半边(细分事件挂接、pageName 编译期枚举、RouteObserver)不在本工单范围。
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# T2-13 体重与疫苗模块接入(交付报告)
|
||||||
|
|
||||||
|
**执行日期**:2026-09-08
|
||||||
|
**角色**:Frontend Developer(Flutter)
|
||||||
|
**工单**:T2-13(L,关键路径最后一个 L 单)
|
||||||
|
**依据**:01 号拆解 T2-13 节、22 号数据层交付(T2-11)、23 号页面交付(T2-12)、05 号 UI 规范、06 号埋点规划、24 号白名单 v2(后端 dev@64c9b72)、冻结契约 openapi.yaml v1.2.0
|
||||||
|
**提交**:patbond-flutter dev@`c91f18a`(基线 97a1f46,已 push origin dev),拆 2 个逻辑提交:
|
||||||
|
|
||||||
|
| 提交 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `5b34fa3` | 体重半边:体重录入表单 + 历史列表(cursor 分页四态)、health_record 埋点封装、展示纯函数、控制器 repository 暴露 |
|
||||||
|
| `c91f18a` | 疫苗半边:疫苗登记表单 + 记录列表(状态机拦截)、档案页数据卡行接 summary、埋点装配 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 结论摘要
|
||||||
|
|
||||||
|
- 体重(录入 + cursor 分页历史)与疫苗(目录选择登记 + 系列分组列表)全链路走 T2-11 数据层,页面零直连 ApiClient;
|
||||||
|
- **档案页数据卡行改接 `GET /pets/{id}/summary` 实时聚合**:最新体重 / 疫苗进度 / 下一针三卡取数,null 语义为空态文案而非 0/0(demo 的 `vaccines.reminderVaccine` 等本地字符串已在 T2-12 随 AppState.vaccines 删除,本单完成「接真实数」的另一半);
|
||||||
|
- 疫苗状态机非法路径前端拦截(结构化 + 纯函数校验)+ 后端 42201/40904 兜底提示,**均有测试与 compose 实测**;
|
||||||
|
- 埋点:health_record 域 4 事件挂通(create 三事件 recordType=weight/vaccine + viewed),照 T2-12 强类型封装模式;
|
||||||
|
- 四态硬要求达成:体重列表、疫苗列表、疫苗目录、摘要卡行四个网络面均 loading/empty/error/retry 齐备且有 widget 测试;
|
||||||
|
- **跨设备验收(工单硬项)通过**:compose 实测建档→记体重→登疫苗后,同账号全新会话(等价清本地数据重登/第二设备)数据全量可见;第二账号访问 40401 防枚举(§6);
|
||||||
|
- 测试 **177 → 224 全绿(+47)**,`flutter analyze` 0 问题,`dart format` 无 diff(两个提交逐个通过门禁:5b34fa3 时点 205 全绿)。
|
||||||
|
|
||||||
|
## 1. 页面与四态覆盖表
|
||||||
|
|
||||||
|
| 页面 / 网络面 | loading | empty | error | retry | 测试文件 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 体重历史列表(`weight_records_page.dart`) | 居中转圈 ✅ | `EmptyStateIllustration`「还没有体重记录」+ 录入 CTA(canWrite)✅ | `InlineErrorBanner` 按错误分档 ✅ | 重试按钮 + 下拉刷新 ✅ | `weight_records_page_test.dart`(7) |
|
||||||
|
| 体重分页(同页「加载更多」) | 行内小转圈 ✅ | 末页收起按钮 ✅ | 翻页失败 SnackBar、按钮保留 ✅ | 可再点 ✅ | 同上(cursor 透传/追加不重不漏有测试) |
|
||||||
|
| 疫苗记录列表(`vaccination_records_page.dart`) | 居中转圈 ✅ | 「还没有疫苗记录」+ 登记 CTA ✅ | 横幅 ✅ | 重试按钮 + 下拉刷新 ✅ | `vaccination_records_page_test.dart`(5) |
|
||||||
|
| 疫苗表单目录面(`vaccination_form_page.dart` 内) | 内联转圈 ✅ | 「该物种暂无可选疫苗目录」✅ | 「目录加载失败」提示 ✅ | 内联重试 ✅ | `vaccination_form_page_test.dart`(9) |
|
||||||
|
| 档案页摘要卡行(`pet_detail_page.dart` 内) | 卡行小转圈 ✅ | 逐卡 null 空态文案(§2)✅ | 行内「健康数据加载失败」✅(不阻塞档案主链路,有测试) | 行内重试 ✅ | `pet_detail_page_test.dart` 增量(5) |
|
||||||
|
|
||||||
|
页面结构与导航:
|
||||||
|
|
||||||
|
```text
|
||||||
|
P2 宠物详情(pet_detail)
|
||||||
|
├─ 健康数据卡行(summary 三卡,可点)
|
||||||
|
│ ├─ 最新体重卡 ──→ 体重历史列表(无路由名,曝光走 viewed)
|
||||||
|
│ │ └─ + → 体重录入表单(路由名 record_form)
|
||||||
|
│ └─ 疫苗进度卡 / 下一针卡 ──→ 疫苗记录列表(按系列分组)
|
||||||
|
│ └─ + → 疫苗登记表单(路由名 record_form)
|
||||||
|
└─ 基本资料(T2-12 既有)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 从记录页返回详情即重拉 summary(服务端实时聚合是唯一事实来源);
|
||||||
|
- 权限:记录写入为 WRITE 档(owner+caregiver),`viewer` 在两个列表页均隐藏录入/登记入口(40300 语义前置,有测试);40300 后端兜底为表单横幅。
|
||||||
|
|
||||||
|
## 2. summary 取数替换 demo 对照
|
||||||
|
|
||||||
|
| 展示位 | demo 时代(T2-12 前) | 现取数(本单) | null 语义 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 最新体重卡 | `AppState.pet.weight` 本地常量(5.2) | `summary.latestWeight.weightKg`(口径:weights 列表首行同源) | null → 「暂无记录」 |
|
||||||
|
| 疫苗进度卡 | `AppState.vaccines` 推导字符串(T2-12 已删) | `summary.vaccinationProgress` 的 `completedDoses/totalDoses` | null → 「未登记」(**不是 0/0**,有测试锁定) |
|
||||||
|
| 下一针卡 | `vaccines.reminderVaccine` 本地字符串(T2-12 已删) | `summary.nextVaccination` 的 `dueOn + vaccineName`(planned/nextDue 并集口径,dueOn 可为过去日期) | null → 「暂无安排」 |
|
||||||
|
|
||||||
|
- 展示字符串全部由服务端事实字段即时计算(第 4.3 节「不持久化展示字符串」红线,客户端同样不缓存);
|
||||||
|
- `tz` 参数本单不传(缺省 UTC):三卡均不消费 monthlyExpense,月度窗口口径留给 T2-14 月度花费卡一并接(测试锁定 tz 缺席)。
|
||||||
|
|
||||||
|
## 3. 疫苗状态机拦截(前端 + 后端兜底)
|
||||||
|
|
||||||
|
前端两层拦截:
|
||||||
|
|
||||||
|
1. **结构化拦截**:scheduled 态只渲染「计划接种日期」、completed 态只渲染「接种日期(+可选下次接种日期)」——「scheduled 携带 administeredOn」在 UI 上不可表达;请求体按状态只发对应字段(测试锁定 scheduled 请求无 `administeredOn`/`nextDueOn` 键)。
|
||||||
|
2. **纯函数校验** `vaccinationDateRuleError`(`health_record_display.dart`,与 42201 规则逐条对齐,9 分支单测):scheduled 必有 plannedOn;completed 必有 administeredOn(「未填接种日期就标完成」拦截,验收标准原文场景);nextDueOn ≥ administeredOn。
|
||||||
|
|
||||||
|
后端兜底(均有 widget 测试 + compose 实测):
|
||||||
|
|
||||||
|
| 码 | 场景 | 呈现 |
|
||||||
|
|---|---|---|
|
||||||
|
| 42201 | 状态-日期规则违反(前端拦截被绕过/契约漂移兜底) | 横幅「接种状态与日期不符合规则,请核对后重试」 |
|
||||||
|
| 40904 | 同系列同剂次非 cancelled 记录已存在 | 横幅「该系列该剂次已有记录(40904);如登记有误,可取消原记录后重新登记」 |
|
||||||
|
|
||||||
|
其余错误分层沿用 T2-12:40300 横幅、40401 SnackBar+返回、40000 横幅、429、网络 SnackBar+重试、会话失效静默(两表单同款矩阵,测试锁定)。
|
||||||
|
|
||||||
|
体重表单前端校验对齐契约:weightKg (0, 500] 且最多两位小数(正则 + 区间,越界/三位小数/非数字拦截有测试),40000 后端兜底横幅;称重时刻今日取此刻、历史日期取当日 12:00,**转 UTC(ISO 带 Z)上送**,规避无时区后缀的解析歧义。
|
||||||
|
|
||||||
|
## 4. 埋点挂接清单(T2-17 前端半边 · health_record 域)
|
||||||
|
|
||||||
|
强类型封装 `lib/features/pets/health_record_analytics.dart`(枚举编译期锁死;后端白名单 dev@64c9b72 已就绪,24 号 §2.2),注入链 app.dart → MainShellPage → PetsPage → PetDetailPage → 记录页面族:
|
||||||
|
|
||||||
|
| # | 事件 / 页名 | 触发点 | 属性 | 测试 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | `health_record_create_started` | 体重/疫苗表单**首次输入**(每次进入一次,表单层去重) | `recordType`(weight/vaccine)、`entryPoint`(`record_list`——表单均由列表页进入) | 去重 ✅ |
|
||||||
|
| 2 | `health_record_create_succeeded` | 创建接口 code=0 | `recordType`、`durationMs`(表单打开→成功)、`photoCount`(M2 无媒体恒 0) | 属性齐备 ✅ |
|
||||||
|
| 3 | `health_record_create_failed` | 失败响应 / 本地校验拦截 / 网络 | `recordType`、`failureReason`(六值枚举)、`errorCode`(可空)、`httpStatus`(`code ~/ 100` 推导)、`attemptSeq` | 校验/40904/42201/40000/40300/网络路径 ✅ |
|
||||||
|
| 4 | `health_record_viewed` | 体重/疫苗**列表页每次进入的首个成功加载**(工单口径:列表曝光) | `recordType`、`source=pet_detail`(列表由详情页进入) | 仅一次 ✅ |
|
||||||
|
| 5 | `page_viewed(record_form)` | 两个表单页 push(`RouteSettings(name: 'record_form')`,既有 AnalyticsRouteObserver 采集) | 既有 pageName/referrer | 路由名断言 ✅ |
|
||||||
|
|
||||||
|
口径决策(报数据侧知悉):
|
||||||
|
|
||||||
|
1. **viewed 时点与 06 §1.4 的出入**:06 定义 viewed 在记录「详情页」可见;M2 体重/疫苗无独立详情页,按工单指令取「列表曝光」——每次进入列表页在首个成功加载时上报一次,不随滚动逐条上报,06 的防事件洪水意图保持。`source` 取进入来源 `pet_detail`。若后续增设记录详情页(05 §4.3 P3),届时 viewed 语义回归 06 原文。
|
||||||
|
2. **列表页不设 page_viewed**:字典 v2 pageName 枚举无「记录列表」页名(仅 record_form/record_detail),按 06 §5.2 验收 4「字典外不上报」处理,列表曝光已由 viewed 承载;如数据侧需要,建议字典 v3 增补 `record_list` 页名。
|
||||||
|
3. `failureReason` 沿用 T2-12 口径:业务拒绝(40904/42201/40000)归 `validation_error` 以 `errorCode` 细分;断网/超时/5xx 并入 `network_error`;`permission_denied`/`not_found` 对应 40300/4040x。
|
||||||
|
4. 编辑/删除交互本单未落地(见 §7),`health_record_edit_*`/`deleted` 事件白名单已就绪、暂无挂接点。
|
||||||
|
|
||||||
|
## 5. 测试数变化
|
||||||
|
|
||||||
|
| 时点 | 测试数 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 基线(dev@97a1f46) | 177 | T2-12 交付 |
|
||||||
|
| 体重半边(dev@5b34fa3) | 205(+28,全绿) | 分提交门禁 |
|
||||||
|
| 本单(dev@`c91f18a`) | **224(+47,全绿)** | 见下分布 |
|
||||||
|
|
||||||
|
| 文件 | 数量 | 覆盖 |
|
||||||
|
|------|------|------|
|
||||||
|
| `health_record_analytics_test.dart` | 5 | 四事件属性形状、httpStatus 推导、可空属性缺席语义 |
|
||||||
|
| `health_record_display_test.dart` | 9 | 体重解析全矩阵(含 500 边界/三位小数/科学计数拒绝)、去尾零展示、疫苗状态/剂次/日期行映射、42201 规则函数 9 分支 |
|
||||||
|
| `weight_form_page_test.dart` | 7 | 空值/越界/三位小数拦截不发请求、成功请求形状(UTC 时间戳/可选 note/无 source)、started 去重、40000/40300/网络三兜底 + 事件断言 |
|
||||||
|
| `weight_records_page_test.dart` | 7 | 四态、cursor 透传与追加、末页收起、翻页失败保留重试、viewed 一次、viewer 无入口、录入闭环(record_form 路由名 + 插入列表头) |
|
||||||
|
| `vaccination_form_page_test.dart` | 9 | 目录按物种过滤/失败重试、疫苗与日期双拦截、completed 缺接种日期拦截、seriesKey 目录 code 预填、scheduled/completed 请求形状(scheduled 无 administeredOn 键)、40904/42201 兜底 + 事件、剂次非法拦截 |
|
||||||
|
| `vaccination_records_page_test.dart` | 5 | 四态、系列分组头/剂次/日期行/三态 TagPill(含 cancelled)、viewed 一次、登记闭环(成功重拉列表)、viewer 无入口 |
|
||||||
|
| `pet_detail_page_test.dart` 增量 | 5 | 三卡取数值、**null 空态而非 0/0**、摘要失败不阻塞主链路 + 行内重试、点卡导航 + 返回重拉摘要、viewer 权限透传 |
|
||||||
|
|
||||||
|
质量门禁:`flutter test` 224/224 全绿;`flutter analyze` No issues found;`dart format --set-exit-if-changed` 无 diff(两个提交逐个通过)。
|
||||||
|
|
||||||
|
## 6. 跨设备验收实测记录(工单硬项)
|
||||||
|
|
||||||
|
环境:patbond-api dev@64c9b72,`JAVA_HOME=java-17 ./mvnw -DskipTests package` + `docker compose up -d --build`(auth :8081 / pet :8083)。curl 按页面实际请求复演,测试账号随机生成、token 脱敏、用后随 `docker compose down` 丢弃:
|
||||||
|
|
||||||
|
| 步骤 | 设备/账号 | 请求 | 结果 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 1 | 设备A · 账号A | POST /auth/register → POST /pets(柴犬「验收豆豆」) | code=0,petId=01a07f70…(UUIDv7) |
|
||||||
|
| 2 | 设备A | POST /pets/{id}/weights(4.35kg,UTC 时间戳,带 Idempotency-Key) | code=0,回读 weightKg=4.35 |
|
||||||
|
| 3 | 设备A | GET /vaccine-catalog?species=dog → POST vaccinations 第1针 completed(administeredOn 2026-08-10、nextDueOn 2027-08-10)+ 第2针 scheduled(plannedOn 2026-10-01) | 两针 code=0(犬二联疫苗,seriesKey=canine_2in1) |
|
||||||
|
| 4 | 设备A | 兜底路径:重复登记第1针 / 第3针 completed 不带 administeredOn | `40904 该疫苗系列剂次已登记` / `42201 completed 状态必须填写 administeredOn` —— 与表单兜底提示路径对齐 ✅ |
|
||||||
|
| 5 | 设备A | GET /pets/{id}/summary | latestWeight=4.35、vaccinationProgress **1/2**、nextVaccination=第2针 dueOn 2026-10-01(source=planned)——三卡口径逐一核对 ✅ |
|
||||||
|
| 6 | **设备B(同账号清本地重登)** | POST /auth/login 取全新会话 → GET pets / weights / vaccinations / summary | 宠物、1 条体重、2 条疫苗、摘要三聚合**全量可见**——M2「数据可跨设备读取」✅ |
|
||||||
|
| 7 | **无关系账号B** | GET 宠物详情 / 体重 / 摘要、POST 体重 | 四路均 `40401 宠物不存在`(防枚举三态同响应)——「无权限用户不能访问」✅ |
|
||||||
|
|
||||||
|
结论:**跨设备读取与越权拒绝两条 M2 验收标准在真实后端逐条通过;40904/42201 兜底真实响应与前端提示路径一致;未发现契约偏差**。实测后 `docker compose down`,patbond-api 仓库零改动。
|
||||||
|
|
||||||
|
## 7. 决策与遗留
|
||||||
|
|
||||||
|
| # | 事项 | 说明 |
|
||||||
|
|---|------|------|
|
||||||
|
| 1 | 记录表单用整页而非 05 §4.4 底部 sheet | 沿 T2-12 PetFormPage 整页先例:`record_form` 路由名可被既有 RouteObserver 采集(sheet 为 PopupRoute 采不到),漏斗到达段不缺口;视觉骨架与 05 字段规范一致 |
|
||||||
|
| 2 | 疫苗表单未含厂商/批号字段 | 契约可选字段,控制表单长度;PATCH 支持补录,随「编辑疫苗记录」交互一并落地(记小遗留) |
|
||||||
|
| 3 | 疫苗 scheduled→completed/cancelled 的列表操作未做 | 工单范围为登记表单+记录列表;PATCH updateVaccination 数据层就绪(T2-11),交互建议随 T2-14 或收口单补「标记完成/取消登记」,届时挂 `health_record_edit_*` 事件(白名单已就绪) |
|
||||||
|
| 4 | 体重表单不暴露 source 选择 | 客户端录入恒 manual(服务端缺省),clinic/device 留给后续接入场景 |
|
||||||
|
| 5 | seriesKey 交互 | 以目录 code 自动预填、可改;「系列」概念的更友好交互(预设初免/加强)待 UI 侧定稿 |
|
||||||
|
| 6 | 归档入口(T2-12 遗留 5) | 本单未动,仍留收口单 |
|
||||||
|
| 7 | 05 §4.2 stat 行第三卡「本月记录/花费」 | 本单第三卡为「下一针」(工单指定 nextVaccination 落点);月度花费卡随 T2-14 接 `monthlyExpense`(届时补 `tz` 透传) |
|
||||||
|
|
||||||
|
## 8. 交接 T2-14 / T2-18
|
||||||
|
|
||||||
|
- 时间线/提醒页可直接复用:`health_record_display.dart` 纯函数模式、列表页四态骨架、`HealthRecordAnalytics`(recordType 枚举已含 `health_event`/`reminder`)、`_SummaryCard`(月度花费卡加一列即可,记得透传 `tz`——`monthlyExpense` 月边界随 tz 移动);
|
||||||
|
- E2E 烟囱(T2-18):本单 §6 的 curl 序列可直接并入烟囱脚本(建档→记体重→登疫苗→摘要核对→第二账号拒绝→重登可见)。
|
||||||
|
|
||||||
|
---
|
||||||
|
**Frontend Developer** · 2026-09-08 · patbond-flutter dev@`c91f18a`
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# T2-14 健康时间线与提醒页接入 + T2-13 遗留收尾(交付报告)
|
||||||
|
|
||||||
|
**执行日期**:2026-09-08
|
||||||
|
**角色**:Frontend Developer(Flutter)
|
||||||
|
**工单**:T2-14(M,第三波收尾单)+ 25 号报告 §7 移交遗留①②③ + T2-17 前端半边收尾(health_record 域)
|
||||||
|
**依据**:01 号拆解 T2-14 节、23/25 号页面交付先例、05 号 UI 规范、06 号埋点规划、冻结契约 openapi.yaml v1.2.0
|
||||||
|
**提交**:patbond-flutter dev@`ba50332`(基线 c91f18a,已 push origin dev),拆 2 个逻辑提交:
|
||||||
|
|
||||||
|
| 提交 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `e186ba3` | 时间线半边:健康事件时间线(月分组 + cursor 分页)+ 事件录入/编辑(顶层 PATCH + 40902 重提)+ 档案页月度花费卡(tz 透传)+ edit 事件封装 |
|
||||||
|
| `ba50332` | 提醒半边:照护提醒列表(过滤 + 逾期标识)+ 创建 + 完成/忽略流转 + 档案页提醒卡真实数据驱动 + 疫苗流转遗留①② |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 结论摘要
|
||||||
|
|
||||||
|
- 健康事件时间线(六类事件、occurred_at DESC cursor 分页、按月分组)与照护提醒(status 过滤、due_at ASC、逾期红标、完成/忽略流转)全链路走 T2-11 数据层,页面零直连 ApiClient;
|
||||||
|
- **金额以元展示 / 整数分传输**:录入、编辑、时间线尾值、月度花费卡四处全部经 `money.dart` 换算,单测锁定双向换算与往返一致(§2);
|
||||||
|
- 档案页「月度花费」卡接 `summary.monthlyExpense`,**`tz` 透传设备时区固定偏移**(T2-13 遗留③闭环,测试锁定格式与实值);
|
||||||
|
- demo 硬编码的「健康提醒:已经半年没有进行体内外驱虫」语义位改为**真实待办提醒驱动**的 alert 卡(最近到期一条,逾期切警示形态;无待办不渲染占位);
|
||||||
|
- T2-13 遗留①②收尾:疫苗 scheduled 行「标记完成 / 取消登记」PATCH 流转 + 完成时厂商/批号补录(契约字段存在,已做);
|
||||||
|
- 埋点:health_record 域 7 事件 **6 挂通 / 1 留待**(`deleted` 因 M2 契约无删除端点无挂接点,§3);
|
||||||
|
- 四态硬要求达成:时间线、提醒列表、事件表单内无独立网络面、档案页两个新增面(月度花费随摘要卡行、提醒入口副行)均齐备且有 widget 测试;无提醒/无事件空态正确(含过滤空态无 CTA);
|
||||||
|
- 测试 **224 → 272 全绿(+48)**,`flutter analyze` 0 问题,`dart format` 无 diff(两个提交逐个通过门禁:e186ba3 时点 250 全绿);
|
||||||
|
- compose 真实后端实测:事件创建/分页/顶层 PATCH/40902、提醒状态机全矩阵(42202 三路)、summary tz 双口径、疫苗完成补录、第二账号 40401 防枚举,逐一符合契约(§5)。
|
||||||
|
|
||||||
|
## 1. 页面与四态覆盖表
|
||||||
|
|
||||||
|
| 页面 / 网络面 | loading | empty | error | retry | 测试文件 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 健康时间线(`health_events_page.dart`) | 居中转圈 ✅ | `EmptyStateIllustration`「还没有健康记录」+ 录入 CTA(canWrite)✅ | `InlineErrorBanner` 分档文案 ✅ | 重试按钮 + 下拉刷新 ✅ | `health_events_page_test.dart`(7) |
|
||||||
|
| 时间线分页(同页「加载更多」) | 行内小转圈 ✅ | 末页收起按钮 ✅ | 翻页失败 SnackBar、按钮保留 ✅ | 可再点 ✅ | 同上(cursor 透传/追加不重不漏有测试) |
|
||||||
|
| 照护提醒列表(`care_reminders_page.dart`) | 居中转圈 ✅ | 「还没有照护提醒」+ CTA;**过滤空态**「暂无「某状态」提醒」无 CTA ✅ | 横幅 ✅ | 重试按钮 + 下拉刷新 ✅ | `care_reminders_page_test.dart`(8) |
|
||||||
|
| 档案页月度花费卡(摘要卡行第 4 列) | 随卡行小转圈 ✅ | monthlyExpense 恒非 null,¥0 弱化视觉 ✅ | 随卡行行内错误 ✅ | 行内重试 ✅ | `pet_detail_page_test.dart` 复用摘要面测试 |
|
||||||
|
| 档案页提醒 alert 卡 / 入口副行 | 副行「加载中…」✅ | 无待办 → 无 alert 卡(无 demo 占位)+「暂无待办提醒」✅ | 副行「提醒加载失败,点击查看」,不阻塞主链路 ✅ | 点入口进提醒页(页内自带重试)✅ | `pet_detail_page_test.dart` 增量(4) |
|
||||||
|
|
||||||
|
页面结构与导航:
|
||||||
|
|
||||||
|
```text
|
||||||
|
P2 宠物详情(pet_detail)
|
||||||
|
├─ 健康数据卡行(四卡:最新体重 / 疫苗进度 / 下一针 / 本月花费)
|
||||||
|
│ └─ 本月花费卡 ──→ 健康时间线
|
||||||
|
├─ 健康提醒 alert 卡(真实待办驱动,最近到期一条,逾期警示形态)──→ 照护提醒页
|
||||||
|
└─ 记录导航区
|
||||||
|
├─ 健康时间线(六类事件) ──→ 时间线页
|
||||||
|
│ ├─ + → 事件录入表单(路由名 record_form)
|
||||||
|
│ └─ 点条目(canWrite)→ 事件编辑页(无路由名,T2-12 先例)
|
||||||
|
└─ 照护提醒(副行:N 条待办 / 暂无 / 失败降级) ──→ 提醒页
|
||||||
|
├─ + → 提醒创建表单(路由名 record_form)
|
||||||
|
└─ 待办行「标记完成 / 忽略」(完成对话框支持补记日期)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 从时间线返回详情重拉摘要(月度花费实时聚合);从提醒页返回重拉待办;
|
||||||
|
- 权限:录入/编辑/流转均 WRITE 档,`viewer` 在时间线(无+、点条目不进编辑)、提醒页(无+、无完成/忽略)、疫苗列表(无流转动作)全部前置隐藏(有测试)。
|
||||||
|
|
||||||
|
关键实现决策:
|
||||||
|
|
||||||
|
1. **六类事件的 RecordTypeDot 映射**:`RecordType` 增补 `feeding/grooming/measurement` 三型(色族复用 05 §2 已审计四色对,仅图标/文案区分,对比度结论不变;8 图标彼此不重,测试锁定);`note` 归「其他」族。映射唯一出口 `recordTypeForHealthEvent`(`health_record_display.dart` 纯函数,6 分支测试)。
|
||||||
|
2. **事件编辑的 40902 路径**:契约无按 id 读取端点,照 T2-12「明确提示 + 自动取新 version(保留输入)+ 重提」模式,最新版本经时间线 cursor 分页检索取回(上限 10 页防御截断;检索不到按已删除处理)。测试锁定重提序列 [3, 7] 与 conflict 失败事件。
|
||||||
|
3. **提醒流转的错误矩阵**:42202(状态-completedAt 一致性,前端已按状态结构化发字段,兜底提示后重拉)、40902(条件更新守卫落空 =「已在其他设备被处理」重拉)、40402 重拉——三路均有测试与 compose 实测。
|
||||||
|
4. **时间约定**沿 T2-13:事件发生时刻 / 提醒到期 / 完成补记均为「今日取此刻、历史(或未来)日期取当日 12:00」转 UTC 带 Z 上送。
|
||||||
|
5. 创建成功后**重拉首页而非本地插入**(时间线月分组与提醒 due_at ASC 的排序键都在服务端),与疫苗列表先例一致。
|
||||||
|
|
||||||
|
## 2. 金额换算证据(工单硬项)
|
||||||
|
|
||||||
|
- DTO 层保持整数分(`HealthEvent.amountCents`、请求体 `amountCents`),换算只发生在 UI 边界,出口唯一为 `lib/features/pets/money.dart`;
|
||||||
|
- 消费点:事件录入表单(元输入 → `parseYuanToCents`)、事件编辑页(分回显 `formatCentsAsYuan` + 元输入回传)、时间线尾值(`¥128.50`)、档案页月度花费卡(`¥` + 分→元);
|
||||||
|
- 单测锁定(`money_test.dart` 6 例,T2-11 交付、本单消费):整元不带小数(12800→"128")、非整元固定两位(12850→"128.50")、负数抛错、非法输入(三位小数/字符/负号)返回 null、**往返一致 format(parse(x))**;
|
||||||
|
- widget 级锁定:表单提交 `amountCents: 12850`(输入 "128.50")、无金额键整体缺席(非 0 非 null)、编辑差量 `{version:3, title:…, amountCents:9900}` 精确形状、金额非法("12.345")本地拦截不发请求;
|
||||||
|
- compose 实测:服务端对小数金额 `12.5` 拒绝 400/40000(不静默截断),与前端拦截口径互为冗余(§5 步骤 3)。
|
||||||
|
|
||||||
|
## 3. 埋点挂接总表(health_record 域 7 事件盘点,T2-17 前端半边收官)
|
||||||
|
|
||||||
|
封装唯一出口 `lib/features/pets/health_record_analytics.dart`(枚举编译期锁死;后端白名单 dev@64c9b72 已含全部 7 事件):
|
||||||
|
|
||||||
|
| # | 事件 | 状态 | recordType 覆盖 | 挂接点 | 测试 |
|
||||||
|
|---|------|------|------|------|------|
|
||||||
|
| 1 | `health_record_create_started` | ✅ 挂通(本波补全) | weight/vaccine(T2-13)+ **health_event/reminder(本单)** | 各表单首次输入去重上报,entryPoint=record_list | 去重 ✅ |
|
||||||
|
| 2 | `health_record_create_succeeded` | ✅ 挂通(本波补全) | 同上四值 | 创建接口 code=0(durationMs/photoCount=0) | 属性齐备 ✅ |
|
||||||
|
| 3 | `health_record_create_failed` | ✅ 挂通(本波补全) | 同上四值 | 失败响应/本地校验/网络(failureReason 六值 + errorCode/httpStatus/attemptSeq) | 多路径 ✅ |
|
||||||
|
| 4 | `health_record_viewed` | ✅ 挂通(本波补全) | 同上四值 | 各列表页每次进入首个成功加载一次(source=pet_detail,T2-13 口径沿用) | 仅一次 ✅ |
|
||||||
|
| 5 | `health_record_edit_succeeded` | ✅ **本单新挂** | **health_event**(编辑保存)+ **vaccine**(标记完成/取消登记,遗留①指定) | PATCH code=0,fieldCount=差量键数(不含 version) | fieldCount ✅ |
|
||||||
|
| 6 | `health_record_edit_failed` | ✅ **本单新挂** | health_event + vaccine | 编辑/流转失败;**failureReason 含 `conflict`(40902)**——M2「并发冲突明确」验收的数据面;属性集无 attemptSeq(对齐 06 §1.5 白名单) | conflict/notFound 等 ✅ |
|
||||||
|
| 7 | `health_record_deleted` | ⏸ **留待** | — | **M2 契约无任何删除端点**(pets 域 12 路径均无 DELETE),无删除交互即无挂接点;白名单已就绪,随删除功能(05 §4.3 P3 提案含删除入口,待拍板)落地即挂 | — |
|
||||||
|
|
||||||
|
**结论:7 事件 6 挂通 / 1 留待(deleted)**。口径决策(报数据侧知悉):
|
||||||
|
|
||||||
|
1. **提醒完成/忽略不埋事件**:06 §7 缺口 3 既定取舍——提醒完成率从 `care_reminders` 事实表(status/completed_at)出数;本单遵循,未给 pending→completed/dismissed 挂 edit 事件(页内注释注明 M3+ 推送实验时增补 `reminder_completed` 的复活条件)。因此 `edit_*` 的 recordType 实际取值为 health_event/vaccine 两种。
|
||||||
|
2. `HealthRecordFailureReason` 枚举增 `conflict`(06 §1.4 edit_failed 属性原文),创建链路不产生该值(创建无版本语义)。
|
||||||
|
3. 时间线/提醒列表页与 T2-13 同理不设 `page_viewed`(字典 v2 无 record_list 页名),曝光由 viewed 承载;两个创建表单带 `record_form` 路由名走既有 RouteObserver(测试锁定),编辑页不带路由名(record_form 专属创建漏斗到达段,T2-12 决策 3 沿用)。
|
||||||
|
|
||||||
|
## 4. T2-13 移交遗留处理结果
|
||||||
|
|
||||||
|
| # | 遗留(25 号 §7) | 处理 |
|
||||||
|
|---|------|------|
|
||||||
|
| ① 疫苗 scheduled→completed/cancelled 列表操作 | ✅ 完成。scheduled 行「标记完成」(对话框:接种日期默认今天 + 可选下次接种,日期规则复用 `vaccinationDateRuleError` 前置拦截 42201)与「取消登记」(确认对话框,仅发 `{version, status:cancelled}`,测试锁定精确形状);挂 `health_record_edit_succeeded/failed(recordType=vaccine)`;40902 提示「已在其他设备被修改」+ 重拉取新 version 后由用户重试(列表行动作与表单场景不同,不做静默自动重提);42201/40402/40300/网络兜底齐备 |
|
||||||
|
| ② 完成时厂商/批号补录 | ✅ 完成(契约有字段:`UpdateVaccinationRequest.manufacturer/batchNo` 可选)。标记完成对话框含两个可选输入,既有值预填、空值不发键;compose 实测补录回读一致(§5 步骤 8) |
|
||||||
|
| ③ summary `tz` 透传 | ✅ 完成。`getPetSummary(tz: tzOffsetQueryValue(设备偏移))`,固定偏移形如 `+08:00`(契约明示接受;Flutter 无 IANA 名可取,语义等价——tz 只作用月度窗口)。纯函数测试覆盖正/负/零/半小时偏移;widget 测试锁定实际透传值 |
|
||||||
|
|
||||||
|
## 5. compose 真实后端实测记录
|
||||||
|
|
||||||
|
环境:patbond-api dev@64c9b72,`JAVA_HOME=java-17 ./mvnw -DskipTests package` + `docker compose up -d --build`(auth :8081 / pet :8083)。curl 按页面实际请求复演,测试账号随机生成、token 不落盘留存、用后随 `docker compose down` 丢弃;patbond-api 仓库零改动:
|
||||||
|
|
||||||
|
| 步骤 | 请求 | 结果 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1 | 注册 → POST /pets(「验收豆豆二号」自定义品种) | code=0,petId=01a07f9e…(UUIDv7) |
|
||||||
|
| 2 | POST health-events:medical + amountCents=12850(带 Idempotency-Key);grooming 无金额 | 两条 code=0;无金额回读 amountCents=null ✅ |
|
||||||
|
| 3 | POST health-events 携带小数金额 `12.5` | `40000 参数校验失败`——不静默截断,与前端元→分整数换算拦截互为冗余 ✅ |
|
||||||
|
| 4 | GET health-events?limit=1 → 携 nextCursor 翻页 | 页1「皮肤检查」hasMore=true → 页2「洗澡美容」hasMore=false,occurred_at DESC ✅ |
|
||||||
|
| 5 | PATCH /health-events/{id}(version=0,title+amountCents) | code=0,title=皮肤复查、amount=9900、version→1 ✅ |
|
||||||
|
| 6 | 同 PATCH 旧 version=0 重放 | `40902 数据已被修改,请刷新后重试`——编辑页冲突路径对齐 ✅ |
|
||||||
|
| 7 | POST care-reminders ×2(未来到期 + 过去到期)→ GET ?status=pending | 创建恒 pending;待办视图 due_at ASC(逾期「年度体检」在前)——逾期标识与排序依据 ✅ |
|
||||||
|
| 8 | 提醒状态机矩阵:dismissed 带 completedAt / completed 缺 completedAt / 终态回退 pending | 三路均 `42202`(文案逐条明确);正常 completed(补记 completedAt)与 dismissed 均 code=0 ✅ |
|
||||||
|
| 9 | GET summary?tz=%2B08:00 与缺省 | `{month: 2026-09, timezone: +08:00, amountCents: 9900}` / `{…, timezone: UTC, …}`——固定偏移被接受、金额随事件编辑实时聚合 ✅ |
|
||||||
|
| 10 | 疫苗 scheduled 登记 → PATCH `{version:0, status:completed, administeredOn, nextDueOn, manufacturer:硕腾, batchNo:LOT-2026-09}` | code=0,status=completed、厂商/批号回读一致、version→1——遗留①②链路 ✅ |
|
||||||
|
| 11 | 第二账号 GET 时间线 / 提醒 | 均 `40401 宠物不存在`(防枚举)——越权拒绝 ✅ |
|
||||||
|
|
||||||
|
结论:**时间线分页/编辑冲突、提醒状态机全矩阵、tz 双口径、疫苗完成补录在真实后端逐条通过;未发现契约偏差**。
|
||||||
|
|
||||||
|
## 6. 测试数变化
|
||||||
|
|
||||||
|
| 时点 | 测试数 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 基线(dev@c91f18a) | 224 | T2-13 交付 |
|
||||||
|
| 时间线半边(dev@e186ba3) | 250(+26,全绿) | 分提交门禁 |
|
||||||
|
| 本单(dev@`ba50332`) | **272(+48,全绿)** | 见下分布 |
|
||||||
|
|
||||||
|
| 文件 | 数量 | 覆盖 |
|
||||||
|
|------|------|------|
|
||||||
|
| `health_events_page_test.dart` | 7(新) | 四态、月分组组头、金额元展示、类型 TagPill、cursor 透传/追加/末页收起/翻页失败保留、录入闭环(record_form 路由名 + 重拉)、编辑闭环(无路由名 + 就地替换)、viewer 三重隐藏、viewed 一次 |
|
||||||
|
| `health_event_form_page_test.dart` | 5(新) | 类型/标题/金额三重本地拦截不发请求、请求形状(eventType/UTC 时间戳/元→分/无金额键缺席)、started 去重、40300/40000/网络兜底 + 失败事件 |
|
||||||
|
| `health_event_edit_page_test.dart` | 5(新) | 预填(分→元回显)、差量精确形状 + fieldCount、无变更不发 PATCH(清空视为不变更)、40902 检索取新 version 重提序列 [3,7] + conflict 事件、40402/40300/网络 + SnackBar 重试接线 |
|
||||||
|
| `care_reminders_page_test.dart` | 8(新) | 四态(含过滤空态无 CTA)、status 参数透传、逾期/待办/已完成三态标签、创建闭环(校验拦截 + 请求形状 + 三事件)、完成(completedAt UTC 必带)/忽略(键缺席)精确形状 + 不埋 edit 事件断言、42202/40902 兜底重拉、viewer 无动作 |
|
||||||
|
| `care_reminder_form_page_test.dart` | 2(新) | started 去重 + 四类型齐备、40300/网络兜底 + 失败事件属性全形状 |
|
||||||
|
| `vaccination_records_page_test.dart` 增量 | +5 | 标记完成(厂商/批号补录请求形状 + fieldCount=4 + 动作仅 scheduled 行)、取消登记(精确 `{version, status}` 形状)、40902 conflict 事件 + 重拉、42201 兜底、viewer 无流转动作 |
|
||||||
|
| `pet_detail_page_test.dart` 增量 | +6 | 四卡取数(¥128.50 + tz 实值与格式)、花费卡→时间线 + 返回重拉、时间线入口 viewer 透传、alert 卡真实数据(最近到期 + 待办数 + 点卡导航 + 返回重拉待办)、逾期警示形态、无待办无占位、失败降级不阻塞 |
|
||||||
|
| `health_record_display_test.dart` 增量 | +7 | 六类映射齐备、六类文案、月组头、tz 偏移四象限、提醒四类文案与映射、逾期判定(终态不算逾期)+ 标签/基色切换、时间副行三态 |
|
||||||
|
| `health_record_analytics_test.dart` 增量 | +3 | editSucceeded 形状、editFailed conflict + httpStatus 推导 + 无 attemptSeq、可空属性缺席 |
|
||||||
|
| `record_type_dot_test.dart` 更新 | — | 全类型映射齐备(8 型)+ 图标互异断言 |
|
||||||
|
|
||||||
|
质量门禁:`flutter test` 272/272 全绿;`flutter analyze` No issues found;`dart format --set-exit-if-changed` 无 diff(两个提交逐个通过)。
|
||||||
|
|
||||||
|
## 7. 决策与遗留
|
||||||
|
|
||||||
|
| # | 事项 | 说明 |
|
||||||
|
|---|------|------|
|
||||||
|
| 1 | `health_record_deleted` 未挂 | M2 契约无删除端点(本单核对 12 路径),留待删除交互(05 §4.3 P3 提案)落地,白名单已就绪 |
|
||||||
|
| 2 | 提醒完成/忽略不埋事件 | 06 §7 缺口 3 既定取舍,完成率走事实表;M3+ 推送实验需增补 `reminder_completed` |
|
||||||
|
| 3 | 档案页摘要卡行为四卡 | 05 §4.2 正典第三卡即「本月花费」;25 号 §8 交接指定「加一列」;窄屏靠 ellipsis 兜底 |
|
||||||
|
| 4 | 时间线未做类型筛选 chips | 05 §6 D2 待拍板项,工单验收不含;拍板后为小改动(列表已按类型渲染标签) |
|
||||||
|
| 5 | 提醒改期 | 契约明示无 title/dueAt 编辑端点,路径为忽略后重建(提醒页忽略文案已引导) |
|
||||||
|
| 6 | AppState demo 清理 | 时间线/提醒页零 AppState 依赖,无需清理;`AppState.pet` 残余消费方仍为首页问候卡/创作页/主壳头像(T2-12 报告 §7 既有标记),属其他 Tab demo 家具,本单未越界 |
|
||||||
|
| 7 | 归档入口(T2-12 遗留 5) | 仍留收口单 |
|
||||||
|
|
||||||
|
## 8. 交接 T2-18(E2E 烟囱)
|
||||||
|
|
||||||
|
- 本单 §5 的 curl 序列可直接并入烟囱脚本(事件分页/PATCH 冲突、提醒状态机矩阵、tz 双口径、疫苗完成补录、第二账号拒绝);
|
||||||
|
- M2 四条验收标准的前端证据位:跨设备(T2-13 §6 + 本单数据全走服务端)、无权限拒绝(§5 步骤 11 + viewer 前置隐藏测试)、**并发冲突明确**(事件编辑 40902 重提 + 疫苗/提醒 40902 提示重拉,conflict 事件落数据面)、双端测试齐备(后端 95 + 前端 272)。
|
||||||
|
|
||||||
|
---
|
||||||
|
**Frontend Developer** · 2026-09-08 · patbond-flutter dev@`ba50332`
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# M2 第三波收口报告:Flutter 页面接入完成
|
||||||
|
|
||||||
|
**执行日期**:2026-09-08
|
||||||
|
**参与方**:Frontend Developer × 4 批次 / Senior Developer(后端白名单)/ 主会话协调
|
||||||
|
**交付形态**:冻结契约下宠物健康档案全页面族接入真实后端,demo 数据消亡
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 执行概要
|
||||||
|
|
||||||
|
第三波目标:冻结契约(v1.2.0)下 Flutter 页面接入(T2-11~14)+ 埋点挂接(T2-17)。
|
||||||
|
|
||||||
|
**结果:全部完成。** patbond-flutter 测试 64 → **272** 全绿,patbond-api 追加白名单扩充(182→191)。档案 Tab 从 demo 数据全面切换到真实后端,四态齐备,三次 compose 实测均无契约偏差。
|
||||||
|
|
||||||
|
| 工单 | 交付 | 提交(flutter dev) | 测试增量 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| T2-11 数据层 | DTO/Client/Repository 18 操作全覆盖 + 8 新错误码类型化 | 7fb9031 | 64→126 |
|
||||||
|
| T2-12 宠物页面 | 列表/详情/表单四态 + DEBT-1 偿还 + pet 域埋点 | 3179528/c0a8a56/97a1f46 | 126→177 |
|
||||||
|
| T2-13 体重疫苗 | 记录页 + 表单 + summary 接数替换 demo | 5b34fa3/c91f18a | 177→224 |
|
||||||
|
| T2-14 时间线提醒 | 六类事件 + 四类提醒 + 月度花费 + T2-13 遗留 | e186ba3/ba50332 | 224→272 |
|
||||||
|
| T2-17 后端半边 | EventDictionary 白名单 +10 事件(api dev@64c9b72) | — | 182→191 |
|
||||||
|
|
||||||
|
## 1. 里程碑意义
|
||||||
|
|
||||||
|
- **demo 数据在档案域消亡**:AppState 的宠物/疫苗 demo 及其持久化全部删除,体重/疫苗进度/下一针/月度花费全部改为服务端事实字段实时聚合(summary 接口),不持久化展示字符串的红线两端贯通
|
||||||
|
- **四态纪律建立**:所有网络页面 loading/empty/error+retry/ready 四态齐备且有 widget 测试,含 cursor 分页的加载更多/翻页失败保留重试交互
|
||||||
|
- **埋点端到端贯通**:字典 v2 的 13 个事件(pet 域 3 + health_record 域 6 + page_viewed 正稿)客户端挂接 + 后端白名单承接;deleted 事件因 M2 无删除端点合理留白
|
||||||
|
- **DEBT-1 正式偿还**:TagPill 深变体映射四组全达 WCAG AA,既有调用零参数回归;PetAvatar/RecordTypeDot/EmptyStateIllustration 三组件按 05 号规范落位
|
||||||
|
- **冲突体验闭环**:40902 乐观锁冲突自动取新 version 重提(测试锁定提交序列),40903/40904/42201/42202 字段级/横幅分层提示
|
||||||
|
|
||||||
|
## 2. 实测证据(三次 compose 全链路)
|
||||||
|
|
||||||
|
- T2-12:空态→品种目录→建档→列表→详情→差量编辑→40902→40903→自定义品种,无契约偏差
|
||||||
|
- T2-13(跨设备验收):建档记体重登疫苗后同账号全新会话全量可见;第二账号四路访问均 40401 防枚举;summary 三聚合逐项核对无偏差
|
||||||
|
- T2-14:11 步实测(事件分页/PATCH/40902、提醒状态机 42202 三路、tz 双口径、疫苗补录、第二账号 40401)全部符合契约
|
||||||
|
|
||||||
|
每次实测后 compose down,patbond-api 代码零改动。
|
||||||
|
|
||||||
|
## 3. 波内事故记录
|
||||||
|
|
||||||
|
T2-13 agent 首跑因平台 API 错误中途终止(仅留 2 个早期文件),经上下文续跑无损完成——半成品检查 + 断点续作模式有效。
|
||||||
|
|
||||||
|
## 4. 遗留(第四波/后续)
|
||||||
|
|
||||||
|
1. health_record_deleted 事件(待删除端点,非 M2 范围)
|
||||||
|
2. 提醒完成/忽略不埋点(06 号 §7 既定取舍)
|
||||||
|
3. T2-12 报告 §8 三项交互待拍板:单宠直进/切换器、归档入口(listPets 过滤语义)、sterilizedOn 表单编辑
|
||||||
|
4. 真机联调补验(第一波方案 A 挂起项)
|
||||||
|
5. auth 域契约测试补齐(机制可复用,另立工单)
|
||||||
|
|
||||||
|
## 5. 三仓状态(收口时点)
|
||||||
|
|
||||||
|
| 仓库 | HEAD | 测试 |
|
||||||
|
|------|------|------|
|
||||||
|
| patbond-api | dev@64c9b72 | 191/191 |
|
||||||
|
| patbond-flutter | dev@ba50332 | 272/272 |
|
||||||
|
| patbond-doc | 本收口提交 | strict 通过 |
|
||||||
|
|
||||||
|
## 6. 下一步:第四波收官
|
||||||
|
|
||||||
|
- **T2-18 E2E 烟囱**:compose 起后端→登录→建档→记体重→登记疫苗→记事件→摘要核对→第二账号被拒→跨设备读取,收集脱敏证据(需用户配合联调;真机若到位一并补第一波挂起项)
|
||||||
|
- **T2-19 文档收口**:OpenAPI 定稿归档、feature-checklist 增补 M2、任务板更新、收官总结
|
||||||
@@ -52,6 +52,12 @@ nav:
|
|||||||
- 19 契约冻结报告: development/iterations/iteration-2/19-contract-freeze-report.md
|
- 19 契约冻结报告: development/iterations/iteration-2/19-contract-freeze-report.md
|
||||||
- 20 契约一致性测试: development/iterations/iteration-2/20-contract-test-report.md
|
- 20 契约一致性测试: development/iterations/iteration-2/20-contract-test-report.md
|
||||||
- 21 第二波收口: development/iterations/iteration-2/21-wave2-closure.md
|
- 21 第二波收口: development/iterations/iteration-2/21-wave2-closure.md
|
||||||
|
- 22 pets 数据层: development/iterations/iteration-2/22-pets-feature-datalayer.md
|
||||||
|
- 23 宠物页面接入: development/iterations/iteration-2/23-pets-pages-report.md
|
||||||
|
- 24 埋点白名单 v2: development/iterations/iteration-2/24-event-whitelist-v2.md
|
||||||
|
- 25 体重疫苗模块: development/iterations/iteration-2/25-weights-vaccines-ui-report.md
|
||||||
|
- 26 时间线与提醒页: development/iterations/iteration-2/26-timeline-reminders-report.md
|
||||||
|
- 27 第三波收口: development/iterations/iteration-2/27-wave3-closure.md
|
||||||
- API:
|
- API:
|
||||||
- 契约说明: api/index.md
|
- 契约说明: api/index.md
|
||||||
- 架构:
|
- 架构:
|
||||||
|
|||||||
Reference in New Issue
Block a user